@tangle-network/agent-app 0.43.59 → 0.43.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-NBSBRZ6F.js → chunk-34M7AUWO.js} +1 -1
- package/dist/{chunk-NBSBRZ6F.js.map → chunk-34M7AUWO.js.map} +1 -1
- package/dist/profile/index.d.ts +8 -0
- package/dist/profile/index.js +12 -2
- package/dist/profile/index.js.map +1 -1
- package/dist/skills/index.d.ts +2 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills-placement/index.d.ts +34 -4
- package/dist/skills-placement/index.js +18 -4
- package/dist/skills-placement/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/skills/index.ts"],"sourcesContent":["/**\n * Unified skill + corpus mounter for agent products.\n *\n * Every agent product hand-rolls the same two file-mount systems and then\n * drifts on the seams between them. (1) An ALWAYS-MOUNTED markdown corpus —\n * (`skills/<slug>/SKILL.md`, `doctrine` and `knowledge` markdown trees) — discovered\n * by a Vite `?raw` glob in the Worker bundle and by Node `fs` under the eval\n * CLI, then projected into `resources.files`. (2) A TIER-GATED installable\n * registry — a hand-authored array of `SkillEntry` whose free tier mounts at\n * the harness skill-discovery path and whose paid tier is installed on demand.\n * Both ride the same `resources.files` channel but use different provenance\n * (file-backed vs inline), different mount paths (relative corpus path vs\n * `~/.claude/skills/<id>/SKILL.md`), and different selection rules. This module\n * makes both DATA: a corpus loader that accepts a Vite glob-result map (or an\n * fs fallback), a registry adapter that tier-gates, and a single\n * `composeShellResources` that projects either onto the SDK file-mount shape.\n *\n * A THIRD surface lives alongside those two: adoptable `SkillEntry`s sourced\n * from `SKILL.md` frontmatter rather than hand-authored fields.\n * `parseSkillFrontmatter` is the ONE frontmatter parser (hand-rolled, no YAML\n * dependency — fail loud on a malformed block rather than silently mis-reading\n * a field); `skillEntryFromMarkdown`/`parseCorpusSkills` turn raw markdown into\n * `SkillEntry`s. From there a skill reaches the agent by one of two DELIVERY\n * MODES: `inline` renders the skill body straight into the system prompt (every\n * harness can read it, at prompt-byte cost), or `mounted` projects it onto the\n * typed `resources.skills` channel (`AgentProfileResourceRef[]`) plus an index\n * section that just names the file, and lets the platform materializer place it\n * at the harness-native skill dir. `composeSkills` builds either shape;\n * `mergeComposedSkills` combines batches and throws (via\n * `assertSkillDeliveryDisjoint`) on a skill accidentally delivered both\n * ways. Picking WHICH harnesses can take `mounted` delivery is platform-bound\n * (see `@tangle-network/agent-app/skills-placement`) and deliberately kept out\n * of this substrate-free module.\n *\n * Substrate-free over storage, exact over the SDK boundary: the only inbound\n * seam is the glob-result map the consumer passes in (its call site keeps the\n * literal `import.meta.glob` Vite must static-analyze); the only outbound seam\n * is `@tangle-network/sandbox`'s `AgentProfileFileMount[]`/`AgentProfileResourceRef[]`,\n * the exact shapes `resources.files`/`resources.skills` consume. Node builtins\n * are resolved lazily via `process.getBuiltinModule` so a static `node:*`\n * import never reaches the Vite SSR bundle.\n */\n\nimport type { AgentProfileFileMount, AgentProfileResourceRef } from '@tangle-network/sandbox'\n\n/** Construct the inline arm of the SDK's `AgentProfileResourceRef`. Inlined here\n * so this leaf subpath stays type-only over `@tangle-network/sandbox` — it\n * carries no runtime dependency on the SDK, just its file-mount type contract. */\nfunction inlineResource(name: string, content: string): AgentProfileResourceRef {\n return { kind: 'inline', name, content }\n}\n\n/** A Vite eager `?raw` glob result: glob key -> raw file body. The consumer\n * produces this by calling `import.meta.glob('<lit>', { eager: true, query:\n * '?raw', import: 'default' })` at its own call site — the literal must stay\n * literal so Vite can static-analyze it; passing the result here keeps that\n * constraint at the edge and the loader substrate-free. */\nexport type GlobModules = Record<string, string>\n\n/** One markdown document discovered from the corpus. */\nexport interface CorpusEntry {\n /** Slug derived from the glob key (folder slug for `SKILL.md` layouts, or the\n * normalized relative path for flat `*.md` layouts). */\n id: string\n /** Glob/fs key the entry was loaded from, normalized to a stable relative\n * form (leading `./` and absolute prefixes stripped). */\n key: string\n /** Raw markdown body (including any frontmatter). */\n content: string\n}\n\n/** A hand-authored, tier-gated installable skill. Mirrors the per-product\n * registry entry (gtm/insurance `SkillEntry`); the runtime's certified `skill`\n * artifact kind is unrelated. `skillMd` is the inline body — file provenance\n * does not apply to the registry. */\nexport interface SkillEntry {\n id: string\n name: string\n description: string\n author?: { name: string; url?: string }\n source?: string\n category?: string\n tags?: string[]\n /** Gate keyword. `composeShellResources`/`registrySkills` treat `free` as\n * always-mounted; everything else is install-on-demand. */\n tier: string\n skillMd: string\n}\n\n/** Harness skill-discovery path the Claude Code backend reads natively. The\n * registry mounts here; the corpus mounts at its relative path.\n *\n * @deprecated Hardcodes the claude-code path (`~/.claude/skills/<id>/SKILL.md`).\n * It is NOT a path other harnesses read — OpenCode discovers `.opencode/skills`,\n * not `~/.claude/skills` (the doc comment here previously claimed otherwise);\n * codex, kimi-code, and the rest each have their own dir or none at all. Use\n * {@link skillRefs} to put a skill on the typed `resources.skills` channel and\n * let the platform materializer place it correctly, or resolve the\n * harness-native dir directly via `@tangle-network/agent-app/skills-placement`.\n * Kept only for the pre-existing `registrySkills`/`userSkillMounts` callers;\n * do not add new call sites. */\nexport function skillMountPath(id: string): string {\n return `~/.claude/skills/${id}/SKILL.md`\n}\n\n/** Strip a glob/fs key down to a stable relative form: drop a leading `./`,\n * and for an absolute fs path keep only the tail from the last anchor segment\n * the pattern implies. We normalize on the trailing `<dir>/.../*.md` so the\n * Vite key (`./skills/x/SKILL.md`) and the fs key (`/abs/.../skills/x/SKILL.md`)\n * collapse to the same value. */\nfunction normalizeKey(key: string, anchor: string): string {\n const marker = `${anchor}/`\n const at = key.lastIndexOf(marker)\n if (at >= 0) return key.slice(at)\n return key.startsWith('./') ? key.slice(2) : key\n}\n\n/** Folder-slug for a `<anchor>/<slug>/SKILL.md` layout; falls back to the\n * normalized key (sans `<anchor>/` prefix, sans `.md`) for flat layouts. */\nfunction toCorpusId(normalizedKey: string, anchor: string): string {\n const nested = normalizedKey.match(new RegExp(`${anchor}/([^/]+)/SKILL\\\\.md$`))\n if (nested) return nested[1]!\n const flat = normalizedKey.match(new RegExp(`${anchor}/(.+)\\\\.md$`))\n if (flat) return flat[1]!\n return normalizedKey\n}\n\n/** Resolve Node builtins lazily. `process.getBuiltinModule` (Node 22+) is\n * absent in workerd, so this returns undefined there and the fs path is never\n * taken — Workers always reach the loader through the Vite glob map. A static\n * `import 'node:fs'` would break Vite SSR bundling in the consumer apps, so it\n * is deliberately avoided. */\nfunction nodeBuiltins():\n | { fs: typeof import('node:fs'); path: typeof import('node:path'); url: typeof import('node:url') }\n | undefined {\n const getBuiltin = (globalThis as { process?: { getBuiltinModule?: (id: string) => unknown } })\n .process?.getBuiltinModule\n if (typeof getBuiltin !== 'function') return undefined\n return {\n fs: getBuiltin('node:fs') as typeof import('node:fs'),\n path: getBuiltin('node:path') as typeof import('node:path'),\n url: getBuiltin('node:url') as typeof import('node:url'),\n }\n}\n\n/** Options for {@link loadMarkdownCorpus}. */\nexport interface LoadCorpusOptions {\n /** The anchor folder name that appears in both glob keys and fs paths\n * (`skills`, `doctrine`, `knowledge`). Used to normalize keys + derive ids. */\n anchor: string\n /** Vite glob-result map. When present and non-empty it is authoritative and\n * the fs path is skipped. Omit it (or pass an empty map) only outside Vite. */\n globModules?: GlobModules\n /** Absolute or `import.meta.url`-relative base dir the fs fallback walks when\n * `globModules` is empty. Required for the fs path to run; without it the fs\n * fallback returns no entries (Workers never need it). */\n fsBaseDir?: string\n /** Walk strategy for the fs fallback. `nested` finds `<dir>/<slug>/SKILL.md`\n * one level deep; `flat` recurses for every `*.md`. Default: `flat`. */\n fsLayout?: 'nested' | 'flat'\n /** Drop an entry by its normalized key after load. Covers the per-product\n * skip lists (corpus index/log files, scaffold templates, allow-lists). */\n skip?: (normalizedKey: string) => boolean\n}\n\n/** Outcome of {@link loadMarkdownCorpus}: the entries plus which path produced\n * them, so a caller can fail loud when both are empty rather than silently\n * mounting nothing. */\nexport interface CorpusLoadResult {\n source: 'vite' | 'fs' | 'empty'\n entries: CorpusEntry[]\n}\n\nfunction fsWalkFlat(\n builtins: NonNullable<ReturnType<typeof nodeBuiltins>>,\n root: string,\n): GlobModules {\n const { fs, path } = builtins\n const out: GlobModules = {}\n if (!fs.existsSync(root)) return out\n const walk = (dir: string) => {\n let entries: import('node:fs').Dirent[]\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const full = path.join(dir, entry.name)\n if (entry.isDirectory()) walk(full)\n else if (entry.isFile() && entry.name.endsWith('.md')) out[full] = fs.readFileSync(full, 'utf8')\n }\n }\n walk(root)\n return out\n}\n\nfunction fsWalkNested(\n builtins: NonNullable<ReturnType<typeof nodeBuiltins>>,\n root: string,\n): GlobModules {\n const { fs, path } = builtins\n const out: GlobModules = {}\n if (!fs.existsSync(root)) return out\n let entries: import('node:fs').Dirent[]\n try {\n entries = fs.readdirSync(root, { withFileTypes: true })\n } catch {\n return out\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n const skillFile = path.join(root, entry.name, 'SKILL.md')\n if (!fs.existsSync(skillFile)) continue\n out[skillFile] = fs.readFileSync(skillFile, 'utf8')\n }\n return out\n}\n\n/** Resolve `fsBaseDir` against `import.meta.url` when relative-looking, so a\n * consumer can pass a bare folder name (`'skills'`) and have it land beside\n * the calling module. Absolute paths pass through. */\nfunction resolveFsBase(\n builtins: NonNullable<ReturnType<typeof nodeBuiltins>>,\n fsBaseDir: string,\n importMetaUrl?: string,\n): string {\n const { path, url } = builtins\n if (path.isAbsolute(fsBaseDir)) return fsBaseDir\n const here = importMetaUrl\n ? path.dirname(url.fileURLToPath(importMetaUrl))\n : process.cwd()\n return path.join(here, fsBaseDir)\n}\n\n/**\n * Load a markdown corpus, preferring a Vite glob-result map and falling back to\n * a Node fs walk. Selection is by non-empty glob result — never an env flag.\n * Entries are normalized, optionally skip-filtered, and sorted by id for\n * determinism. The `import.meta.glob` literal stays at the CONSUMER call site\n * (passed in as `globModules`); this loader never constructs a glob.\n */\nexport function loadMarkdownCorpus(\n options: LoadCorpusOptions,\n importMetaUrl?: string,\n): CorpusLoadResult {\n const { anchor, globModules, fsBaseDir, fsLayout = 'flat', skip } = options\n\n let modules: GlobModules\n let source: CorpusLoadResult['source']\n if (globModules && Object.keys(globModules).length > 0) {\n modules = globModules\n source = 'vite'\n } else {\n const builtins = nodeBuiltins()\n if (builtins && fsBaseDir) {\n const root = resolveFsBase(builtins, fsBaseDir, importMetaUrl)\n modules = fsLayout === 'nested' ? fsWalkNested(builtins, root) : fsWalkFlat(builtins, root)\n source = Object.keys(modules).length > 0 ? 'fs' : 'empty'\n } else {\n modules = {}\n source = 'empty'\n }\n }\n\n const entries: CorpusEntry[] = []\n for (const [rawKey, content] of Object.entries(modules)) {\n if (typeof content !== 'string') continue\n const key = normalizeKey(rawKey, anchor)\n if (skip && skip(key)) continue\n entries.push({ id: toCorpusId(key, anchor), key, content })\n }\n entries.sort((a, b) => a.id.localeCompare(b.id))\n return { source, entries }\n}\n\n/** Project corpus entries onto SDK file mounts at a relative path under\n * `<anchor>/`. Always-mounted: the corpus is the agent's baseline knowledge. */\nexport function corpusSkills(corpus: CorpusEntry[], anchor: string): AgentProfileFileMount[] {\n return corpus\n .map(\n (entry) =>\n ({\n path: `${anchor}/${entry.id}.md`,\n resource: inlineResource(`${anchor}-${entry.id}`, entry.content),\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/** Project the registry's free-tier (or `tier`-matched) entries onto SDK file\n * mounts at the harness skill-discovery path. Tier-gating is the registry's\n * only selection rule — paid skills are installed on demand, not at boot. */\nexport function registrySkills(\n registry: SkillEntry[],\n tier: string = 'free',\n): AgentProfileFileMount[] {\n return registry\n .filter((s) => s.tier === tier)\n .map(\n (s) =>\n ({\n path: skillMountPath(s.id),\n resource: inlineResource(s.id, s.skillMd),\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/** Inputs to {@link composeShellResources}. Each channel is optional so a\n * product mounts only the systems it has — corpus-only, registry-only, or\n * both — without conflating them. */\nexport interface ComposeShellResourcesInput {\n /** Corpus mounts (always-mounted baseline). Pass the result of\n * {@link corpusSkills}, or a hand-built mount list. */\n skills?: AgentProfileFileMount[]\n /** Knowledge-corpus mounts (a second always-mounted corpus, e.g. a domain\n * knowledge pack distinct from the skills corpus). */\n knowledge?: AgentProfileFileMount[]\n /** Evolvable / learned-guidance mounts (single-file corpora). */\n evolvable?: AgentProfileFileMount[]\n /** Registry mounts (tier-gated). Pass the result of {@link registrySkills}. */\n registry?: AgentProfileFileMount[]\n /** Final skip filter applied to the composed mount list by mount `path`. */\n predicate?: (mount: AgentProfileFileMount) => boolean\n}\n\n/**\n * Compose every mount channel into one `resources.files`-ready array. Corpus\n * channels come first (baseline), the tier-gated registry last (so a registry\n * entry can override a corpus entry that mounts at the same path). The result\n * is exactly `AgentProfileFileMount[]` — assign it straight into\n * `profile.resources.files` with no cast.\n */\nexport function composeShellResources(input: ComposeShellResourcesInput): AgentProfileFileMount[] {\n const { skills = [], knowledge = [], evolvable = [], registry = [], predicate } = input\n const composed = [...skills, ...knowledge, ...evolvable, ...registry]\n return predicate ? composed.filter(predicate) : composed\n}\n\n// ─── Adoptable skills: one frontmatter parser, two delivery modes ─────────────\n\n/** Fields a `SKILL.md` frontmatter block may declare. All optional — absent\n * frontmatter (or an absent field within it) is legal; callers fill defaults\n * (see {@link skillEntryFromMarkdown}). */\nexport interface SkillFrontmatter {\n id?: string\n name?: string\n description?: string\n author?: { name: string; url?: string }\n source?: string\n category?: string\n tags?: string[]\n tier?: string\n}\n\n/** The result of {@link parseSkillFrontmatter}: the parsed fields, the body\n * with the frontmatter block stripped, and the original untouched text. */\nexport interface ParsedSkill {\n frontmatter: SkillFrontmatter\n body: string\n raw: string\n}\n\n/** Quoted values (`description: \"...\"`, as emitted by materialize's\n * `normalizeSkillMd`) are JSON strings — decode with `JSON.parse` so escapes\n * round-trip; anything else is a bare scalar, trimmed. */\nfunction parseFrontmatterScalar(value: string): string {\n const trimmed = value.trim()\n if (trimmed.startsWith('\"')) return JSON.parse(trimmed) as string\n return trimmed\n}\n\n/** THE one `SKILL.md` frontmatter parser — hand-rolled, no YAML dependency.\n *\n * Absent frontmatter (text does not open with a `---` delimiter line) is\n * legal: returns `{frontmatter: {}, body: raw, raw}`. An OPENED block with no\n * closing `---` is truncated input and throws. Inside the block: scalar\n * `key: value` lines (value optionally double-quoted, decoded via\n * `JSON.parse`); a nested `author:` block whose indented `name:`/`url:` lines\n * are the only children it accepts; `tags:` as an inline `[a, b]` list or as\n * an indented `- item` block. Unknown scalar keys are ignored (forward-compat)\n * — but a line that matches NONE of these shapes (no colon, an orphaned\n * indented line, a bad dash) throws naming the offending line. Silently\n * mis-parsed metadata is the bug class this parser exists to kill; an\n * unrecognized shape is never guessed at.\n */\nexport function parseSkillFrontmatter(raw: string): ParsedSkill {\n const lines = raw.split('\\n')\n if ((lines[0] ?? '').trim() !== '---') {\n return { frontmatter: {}, body: raw, raw }\n }\n\n let closeIndex = -1\n for (let i = 1; i < lines.length; i++) {\n if ((lines[i] ?? '').trim() === '---') {\n closeIndex = i\n break\n }\n }\n if (closeIndex === -1) {\n throw new Error(\n 'parseSkillFrontmatter: opening \"---\" has no closing \"---\" — truncated frontmatter block',\n )\n }\n\n const blockLines = lines.slice(1, closeIndex)\n const body = lines.slice(closeIndex + 1).join('\\n').replace(/^\\n+/, '')\n const frontmatter: SkillFrontmatter = {}\n const scalarKeys = new Set(['id', 'name', 'description', 'source', 'category', 'tier'])\n const topLineRe = /^(\\S[^:]*):\\s*(.*)$/\n\n let i = 0\n while (i < blockLines.length) {\n const line = blockLines[i] ?? ''\n if (line.trim() === '') {\n i++\n continue\n }\n const match = line.match(topLineRe)\n if (!match) {\n throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`)\n }\n const key = (match[1] ?? '').trim()\n const rest = match[2] ?? ''\n\n if (key === 'author') {\n if (rest.trim() !== '') {\n throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`)\n }\n const author: { name: string; url?: string } = { name: '' }\n let sawName = false\n i++\n while (i < blockLines.length && /^\\s+\\S/.test(blockLines[i] ?? '')) {\n const sub = (blockLines[i] ?? '').trim()\n const subMatch = sub.match(/^(name|url):\\s*(.*)$/)\n if (!subMatch) {\n throw new Error(\n `parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(blockLines[i])}`,\n )\n }\n const subKey = subMatch[1] as 'name' | 'url'\n const subValue = parseFrontmatterScalar(subMatch[2] ?? '')\n if (subKey === 'name') {\n author.name = subValue\n sawName = true\n } else {\n author.url = subValue\n }\n i++\n }\n if (sawName) frontmatter.author = author\n continue\n }\n\n if (key === 'tags') {\n const inline = rest.trim()\n if (inline.startsWith('[') && inline.endsWith(']')) {\n const inner = inline.slice(1, -1).trim()\n frontmatter.tags = inner === '' ? [] : inner.split(',').map((t) => parseFrontmatterScalar(t))\n i++\n continue\n }\n if (inline !== '') {\n throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`)\n }\n const tags: string[] = []\n i++\n while (i < blockLines.length && /^\\s*-\\s*/.test(blockLines[i] ?? '')) {\n tags.push(parseFrontmatterScalar((blockLines[i] ?? '').replace(/^\\s*-\\s*/, '')))\n i++\n }\n frontmatter.tags = tags\n continue\n }\n\n if (scalarKeys.has(key)) {\n ;(frontmatter as Record<string, string>)[key] = parseFrontmatterScalar(rest)\n }\n // Unknown scalar key: ignored, forward-compat.\n i++\n }\n\n return { frontmatter, body, raw }\n}\n\n/** Build a {@link SkillEntry} from a raw `SKILL.md` body. `id` comes from\n * frontmatter, falling back to `fallbackId` (typically the corpus entry's\n * slug/filename); neither present throws. `name` defaults to `id`,\n * `description` to `''`, `tier` to `'free'`. `skillMd` is always the\n * untouched `raw` input — the full file, frontmatter included, is what a\n * `mounted` delivery writes to disk and what `renderInlineSkills` strips per\n * render. */\nexport function skillEntryFromMarkdown(raw: string, fallbackId?: string): SkillEntry {\n const { frontmatter } = parseSkillFrontmatter(raw)\n const id = frontmatter.id ?? fallbackId\n if (!id) {\n throw new Error(\n 'skillEntryFromMarkdown: no \"id\" in frontmatter and no fallbackId supplied — cannot build a SkillEntry',\n )\n }\n const entry: SkillEntry = {\n id,\n name: frontmatter.name ?? id,\n description: frontmatter.description ?? '',\n tier: frontmatter.tier ?? 'free',\n skillMd: raw,\n }\n if (frontmatter.author) entry.author = frontmatter.author\n if (frontmatter.source) entry.source = frontmatter.source\n if (frontmatter.category) entry.category = frontmatter.category\n if (frontmatter.tags) entry.tags = frontmatter.tags\n return entry\n}\n\n/** Map a loaded corpus (see {@link loadMarkdownCorpus}) onto `SkillEntry`s,\n * using each entry's `id` as the fallback when its `SKILL.md` carries no\n * frontmatter `id` of its own. */\nexport function parseCorpusSkills(corpus: CorpusEntry[]): SkillEntry[] {\n return corpus.map((entry) => skillEntryFromMarkdown(entry.content, entry.id))\n}\n\n/** Project skills onto the typed `resources.skills` channel\n * (`AgentProfileResourceRef[]`), tier-filtered (same `s.tier === tier`\n * semantics as {@link registrySkills}) when `opts.tier` is given, sorted by\n * id for determinism. `ref.name` MUST be the skill id: the platform\n * materializer writes each ref to `${skillDir}/${name}/SKILL.md`. */\nexport function skillRefs(\n skills: SkillEntry[],\n opts: { tier?: string } = {},\n): AgentProfileResourceRef[] {\n const filtered = opts.tier ? skills.filter((s) => s.tier === opts.tier) : skills\n return [...filtered]\n .sort((a, b) => a.id.localeCompare(b.id))\n .map((s) => inlineResource(s.id, s.skillMd))\n}\n\n/** Strip the frontmatter block from a skill's `skillMd` and render it as a\n * prompt subsection. The default `body` renderer {@link renderInlineSkills}\n * passes to itself. */\nfunction renderInlineSkillBody(skill: SkillEntry): string {\n const { body } = parseSkillFrontmatter(skill.skillMd)\n return `### ${skill.name}\\n\\n${body.trim()}`\n}\n\n/** Inputs to {@link renderInlineSkills}. */\nexport interface RenderInlineSkillsInput {\n skills: SkillEntry[]\n /** Section heading. Default `'## Skills'`. */\n heading?: string\n /** Tier filter — same semantics as {@link skillRefs}. */\n tier?: string\n /** Per-skill body renderer. Default strips frontmatter and emits\n * `### <name>\\n\\n<body>`. */\n body?: (skill: SkillEntry) => string\n}\n\n/**\n * Render every (tier-filtered) skill's full body inline into the prompt — the\n * `inline` delivery mode. Returns `''` when no skill survives the filter, else\n * a section starting `\\n\\n<heading>\\n\\n` with each skill's body joined by\n * `\\n\\n` — the same \"already carries its own leading `\\n\\n`\" shape\n * `assembleSystemPrompt` expects from every section it concatenates.\n */\nexport function renderInlineSkills(input: RenderInlineSkillsInput): string {\n const heading = input.heading ?? '## Skills'\n const filtered = input.tier ? input.skills.filter((s) => s.tier === input.tier) : input.skills\n if (filtered.length === 0) return ''\n const renderBody = input.body ?? renderInlineSkillBody\n return `\\n\\n${heading}\\n\\n${filtered.map(renderBody).join('\\n\\n')}`\n}\n\n/** Inputs to {@link renderSkillIndex}. */\nexport interface RenderSkillIndexInput {\n skills: SkillEntry[]\n /** cwd-relative directory the skills are mounted under (e.g.\n * `.opencode/skills`) — named in each index line so the agent knows where\n * to read the full `SKILL.md`. */\n skillDir: string\n /** Section heading. Default `'## Skills'`. */\n heading?: string\n /** Tier filter — same semantics as {@link skillRefs}. */\n tier?: string\n}\n\n/**\n * Render a one-line-per-skill INDEX (name, description, and the path to read\n * the full body) — the `mounted` delivery mode's prompt section, paired with\n * {@link skillRefs} putting the actual files on `resources.skills`. Same\n * empty/section shape as {@link renderInlineSkills}.\n */\nexport function renderSkillIndex(input: RenderSkillIndexInput): string {\n const heading = input.heading ?? '## Skills'\n const filtered = input.tier ? input.skills.filter((s) => s.tier === input.tier) : input.skills\n if (filtered.length === 0) return ''\n const lines = filtered.map(\n (s) => `- ${s.name}: ${s.description} (read ${input.skillDir}/${s.id}/SKILL.md)`,\n )\n return `\\n\\n${heading}\\n\\n${lines.join('\\n')}`\n}\n\n/** How a skill reaches the agent: `inline` renders its full body into the\n * system prompt; `mounted` puts it on the typed `resources.skills` channel\n * and renders only an index line. */\nexport type SkillDeliveryMode = 'inline' | 'mounted'\n\n/** The output of {@link composeSkills}: the refs to attach to\n * `resources.skills` (empty for `inline`) and the prompt section to fold into\n * the system prompt (already carries its own leading `\\n\\n`, or `''`).\n * `inlineIds`/`mountedIds` record which (tier-filtered) skills were delivered\n * in each mode so {@link mergeComposedSkills} can enforce the modes stay\n * disjoint when batches are combined. */\nexport interface ComposedSkills {\n refs: AgentProfileResourceRef[]\n promptSection: string\n inlineIds: string[]\n mountedIds: string[]\n}\n\n/** Inputs to {@link composeSkills}. */\nexport interface ComposeSkillsInput {\n skills: SkillEntry[]\n mode: SkillDeliveryMode\n tier?: string\n heading?: string\n /** REQUIRED (non-null) for `mode: 'mounted'` — the cwd-relative skill dir\n * the harness reads. Resolve it with `@tangle-network/agent-app/skills-placement`\n * rather than hardcoding it; omitted or `null` throws. */\n skillDir?: string | null\n}\n\n/**\n * Build the {@link ComposedSkills} for one delivery mode. `inline` never\n * touches `resources.skills` — `refs` is always `[]`. `mounted` requires a\n * non-null `skillDir` (the harness must have a native skill-discovery\n * directory); a null/absent one throws rather than silently falling back, so\n * the caller resolves the fallback deliberately (see\n * `@tangle-network/agent-app/skills-placement`'s `composeSkillsForHarness`,\n * which does exactly that).\n */\nexport function composeSkills(input: ComposeSkillsInput): ComposedSkills {\n const { skills, mode, tier, heading } = input\n const deliveredIds = (tier ? skills.filter((s) => s.tier === tier) : skills).map((s) => s.id)\n if (mode === 'inline') {\n return {\n refs: [],\n promptSection: renderInlineSkills({ skills, tier, heading }),\n inlineIds: deliveredIds,\n mountedIds: [],\n }\n }\n if (!input.skillDir) {\n throw new Error(\n 'composeSkills: mode \"mounted\" requires a non-null skillDir — this harness cannot receive ' +\n 'skill files at a cwd path; pass mode \"inline\" instead',\n )\n }\n return {\n refs: skillRefs(skills, { tier }),\n promptSection: renderSkillIndex({ skills, skillDir: input.skillDir, tier, heading }),\n inlineIds: [],\n mountedIds: deliveredIds,\n }\n}\n\n/** Throw when the same skill id is delivered both `inline` and `mounted` —\n * the agent would see it twice (once in the prompt body, once as a mounted\n * file it's told to go read), doubling prompt bytes and inviting drift\n * between the two copies. Lists every offending id in the message. */\nexport function assertSkillDeliveryDisjoint(\n inlineIds: Iterable<string>,\n mountedIds: Iterable<string>,\n): void {\n const inline = new Set(inlineIds)\n const overlap = [...new Set(mountedIds)].filter((id) => inline.has(id))\n if (overlap.length > 0) {\n throw new Error(\n `assertSkillDeliveryDisjoint: skill id(s) delivered both inline and mounted: ${overlap.join(', ')}`,\n )\n }\n}\n\n/**\n * Combine multiple {@link ComposedSkills} batches (e.g. a built-in corpus and\n * an installed catalog) into one, concatenating refs and prompt sections in\n * batch order. This is the seam where inline and mounted deliveries can first\n * collide, so it applies {@link assertSkillDeliveryDisjoint} — a skill id\n * delivered inline by one batch and mounted by another throws instead of\n * reaching the agent twice. Merge through this rather than spreading batch\n * fields by hand.\n */\nexport function mergeComposedSkills(batches: ComposedSkills[]): ComposedSkills {\n const merged: ComposedSkills = {\n refs: batches.flatMap((b) => b.refs),\n promptSection: batches.map((b) => b.promptSection).join(''),\n inlineIds: batches.flatMap((b) => b.inlineIds),\n mountedIds: batches.flatMap((b) => b.mountedIds),\n }\n assertSkillDeliveryDisjoint(merged.inlineIds, merged.mountedIds)\n return merged\n}\n"],"mappings":"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","names":["i"]}
|
|
1
|
+
{"version":3,"sources":["../src/skills/index.ts"],"sourcesContent":["/**\n * Unified skill + corpus mounter for agent products.\n *\n * Every agent product hand-rolls the same two file-mount systems and then\n * drifts on the seams between them. (1) An ALWAYS-MOUNTED markdown corpus —\n * (`skills/<slug>/SKILL.md`, `doctrine` and `knowledge` markdown trees) — discovered\n * by a Vite `?raw` glob in the Worker bundle and by Node `fs` under the eval\n * CLI, then projected into `resources.files`. (2) A TIER-GATED installable\n * registry — a hand-authored array of `SkillEntry` whose free tier mounts at\n * the harness skill-discovery path and whose paid tier is installed on demand.\n * Both ride the same `resources.files` channel but use different provenance\n * (file-backed vs inline), different mount paths (relative corpus path vs\n * `~/.claude/skills/<id>/SKILL.md`), and different selection rules. This module\n * makes both DATA: a corpus loader that accepts a Vite glob-result map (or an\n * fs fallback), a registry adapter that tier-gates, and a single\n * `composeShellResources` that projects either onto the SDK file-mount shape.\n *\n * A THIRD surface lives alongside those two: adoptable `SkillEntry`s sourced\n * from `SKILL.md` frontmatter rather than hand-authored fields.\n * `parseSkillFrontmatter` is the ONE frontmatter parser (hand-rolled, no YAML\n * dependency — fail loud on a malformed block rather than silently mis-reading\n * a field); `skillEntryFromMarkdown`/`parseCorpusSkills` turn raw markdown into\n * `SkillEntry`s. From there a skill reaches the agent by one of two DELIVERY\n * MODES: `inline` renders the skill body straight into the system prompt (every\n * harness can read it, at prompt-byte cost), or `mounted` projects it onto the\n * typed `resources.skills` channel (`AgentProfileResourceRef[]`) plus an index\n * section that just names the file, and lets the platform materializer place it\n * at the harness-native skill dir. `composeSkills` builds either shape;\n * `mergeComposedSkills` combines batches and throws (via\n * `assertSkillDeliveryDisjoint`) on a skill accidentally delivered both\n * ways. Picking WHICH harnesses can take `mounted` delivery is platform-bound\n * (see `@tangle-network/agent-app/skills-placement`) and deliberately kept out\n * of this substrate-free module.\n *\n * Substrate-free over storage, exact over the SDK boundary: the only inbound\n * seam is the glob-result map the consumer passes in (its call site keeps the\n * literal `import.meta.glob` Vite must static-analyze); the only outbound seam\n * is `@tangle-network/sandbox`'s `AgentProfileFileMount[]`/`AgentProfileResourceRef[]`,\n * the exact shapes `resources.files`/`resources.skills` consume. Node builtins\n * are resolved lazily via `process.getBuiltinModule` so a static `node:*`\n * import never reaches the Vite SSR bundle.\n */\n\nimport type { AgentProfileFileMount, AgentProfileResourceRef } from '@tangle-network/sandbox'\n\n/** Construct the inline arm of the SDK's `AgentProfileResourceRef`. Inlined here\n * so this leaf subpath stays type-only over `@tangle-network/sandbox` — it\n * carries no runtime dependency on the SDK, just its file-mount type contract. */\nfunction inlineResource(name: string, content: string): AgentProfileResourceRef {\n return { kind: 'inline', name, content }\n}\n\n/** A Vite eager `?raw` glob result: glob key -> raw file body. The consumer\n * produces this by calling `import.meta.glob('<lit>', { eager: true, query:\n * '?raw', import: 'default' })` at its own call site — the literal must stay\n * literal so Vite can static-analyze it; passing the result here keeps that\n * constraint at the edge and the loader substrate-free. */\nexport type GlobModules = Record<string, string>\n\n/** One markdown document discovered from the corpus. */\nexport interface CorpusEntry {\n /** Slug derived from the glob key (folder slug for `SKILL.md` layouts, or the\n * normalized relative path for flat `*.md` layouts). */\n id: string\n /** Glob/fs key the entry was loaded from, normalized to a stable relative\n * form (leading `./` and absolute prefixes stripped). */\n key: string\n /** Raw markdown body (including any frontmatter). */\n content: string\n}\n\n/** A hand-authored, tier-gated installable skill. Mirrors the per-product\n * registry entry (gtm/insurance `SkillEntry`); the runtime's certified `skill`\n * artifact kind is unrelated. `skillMd` is the inline body — file provenance\n * does not apply to the registry. */\nexport interface SkillEntry {\n id: string\n name: string\n description: string\n author?: { name: string; url?: string }\n source?: string\n category?: string\n tags?: string[]\n /** Gate keyword. `composeShellResources`/`registrySkills` treat `free` as\n * always-mounted; everything else is install-on-demand. */\n tier: string\n skillMd: string\n}\n\n/** Harness skill-discovery path the Claude Code backend reads natively. The\n * registry mounts here; the corpus mounts at its relative path.\n *\n * @deprecated Hardcodes the claude-code path (`~/.claude/skills/<id>/SKILL.md`).\n * It is NOT a path other harnesses read — OpenCode discovers `.opencode/skills`,\n * not `~/.claude/skills` (the doc comment here previously claimed otherwise);\n * codex, kimi-code, and the rest each have their own dir or none at all. Use\n * {@link skillRefs} to put a skill on the typed `resources.skills` channel and\n * let the platform materializer place it correctly, or resolve the\n * harness-native dir directly via `@tangle-network/agent-app/skills-placement`.\n * Kept only for the pre-existing `registrySkills`/`userSkillMounts` callers;\n * do not add new call sites. */\nexport function skillMountPath(id: string): string {\n return `~/.claude/skills/${id}/SKILL.md`\n}\n\n/** Strip a glob/fs key down to a stable relative form: drop a leading `./`,\n * and for an absolute fs path keep only the tail from the last anchor segment\n * the pattern implies. We normalize on the trailing `<dir>/.../*.md` so the\n * Vite key (`./skills/x/SKILL.md`) and the fs key (`/abs/.../skills/x/SKILL.md`)\n * collapse to the same value. */\nfunction normalizeKey(key: string, anchor: string): string {\n const marker = `${anchor}/`\n const at = key.lastIndexOf(marker)\n if (at >= 0) return key.slice(at)\n return key.startsWith('./') ? key.slice(2) : key\n}\n\n/** Folder-slug for a `<anchor>/<slug>/SKILL.md` layout; falls back to the\n * normalized key (sans `<anchor>/` prefix, sans `.md`) for flat layouts. */\nfunction toCorpusId(normalizedKey: string, anchor: string): string {\n const nested = normalizedKey.match(new RegExp(`${anchor}/([^/]+)/SKILL\\\\.md$`))\n if (nested) return nested[1]!\n const flat = normalizedKey.match(new RegExp(`${anchor}/(.+)\\\\.md$`))\n if (flat) return flat[1]!\n return normalizedKey\n}\n\n/** Resolve Node builtins lazily. `process.getBuiltinModule` (Node 22+) is\n * absent in workerd, so this returns undefined there and the fs path is never\n * taken — Workers always reach the loader through the Vite glob map. A static\n * `import 'node:fs'` would break Vite SSR bundling in the consumer apps, so it\n * is deliberately avoided. */\nfunction nodeBuiltins():\n | { fs: typeof import('node:fs'); path: typeof import('node:path'); url: typeof import('node:url') }\n | undefined {\n const getBuiltin = (globalThis as { process?: { getBuiltinModule?: (id: string) => unknown } })\n .process?.getBuiltinModule\n if (typeof getBuiltin !== 'function') return undefined\n return {\n fs: getBuiltin('node:fs') as typeof import('node:fs'),\n path: getBuiltin('node:path') as typeof import('node:path'),\n url: getBuiltin('node:url') as typeof import('node:url'),\n }\n}\n\n/** Options for {@link loadMarkdownCorpus}. */\nexport interface LoadCorpusOptions {\n /** The anchor folder name that appears in both glob keys and fs paths\n * (`skills`, `doctrine`, `knowledge`). Used to normalize keys + derive ids. */\n anchor: string\n /** Vite glob-result map. When present and non-empty it is authoritative and\n * the fs path is skipped. Omit it (or pass an empty map) only outside Vite. */\n globModules?: GlobModules\n /** Absolute or `import.meta.url`-relative base dir the fs fallback walks when\n * `globModules` is empty. Required for the fs path to run; without it the fs\n * fallback returns no entries (Workers never need it). */\n fsBaseDir?: string\n /** Walk strategy for the fs fallback. `nested` finds `<dir>/<slug>/SKILL.md`\n * one level deep; `flat` recurses for every `*.md`. Default: `flat`. */\n fsLayout?: 'nested' | 'flat'\n /** Drop an entry by its normalized key after load. Covers the per-product\n * skip lists (corpus index/log files, scaffold templates, allow-lists). */\n skip?: (normalizedKey: string) => boolean\n}\n\n/** Outcome of {@link loadMarkdownCorpus}: the entries plus which path produced\n * them, so a caller can fail loud when both are empty rather than silently\n * mounting nothing. */\nexport interface CorpusLoadResult {\n source: 'vite' | 'fs' | 'empty'\n entries: CorpusEntry[]\n}\n\nfunction fsWalkFlat(\n builtins: NonNullable<ReturnType<typeof nodeBuiltins>>,\n root: string,\n): GlobModules {\n const { fs, path } = builtins\n const out: GlobModules = {}\n if (!fs.existsSync(root)) return out\n const walk = (dir: string) => {\n let entries: import('node:fs').Dirent[]\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const full = path.join(dir, entry.name)\n if (entry.isDirectory()) walk(full)\n else if (entry.isFile() && entry.name.endsWith('.md')) out[full] = fs.readFileSync(full, 'utf8')\n }\n }\n walk(root)\n return out\n}\n\nfunction fsWalkNested(\n builtins: NonNullable<ReturnType<typeof nodeBuiltins>>,\n root: string,\n): GlobModules {\n const { fs, path } = builtins\n const out: GlobModules = {}\n if (!fs.existsSync(root)) return out\n let entries: import('node:fs').Dirent[]\n try {\n entries = fs.readdirSync(root, { withFileTypes: true })\n } catch {\n return out\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n const skillFile = path.join(root, entry.name, 'SKILL.md')\n if (!fs.existsSync(skillFile)) continue\n out[skillFile] = fs.readFileSync(skillFile, 'utf8')\n }\n return out\n}\n\n/** Resolve `fsBaseDir` against `import.meta.url` when relative-looking, so a\n * consumer can pass a bare folder name (`'skills'`) and have it land beside\n * the calling module. Absolute paths pass through. */\nfunction resolveFsBase(\n builtins: NonNullable<ReturnType<typeof nodeBuiltins>>,\n fsBaseDir: string,\n importMetaUrl?: string,\n): string {\n const { path, url } = builtins\n if (path.isAbsolute(fsBaseDir)) return fsBaseDir\n const here = importMetaUrl\n ? path.dirname(url.fileURLToPath(importMetaUrl))\n : process.cwd()\n return path.join(here, fsBaseDir)\n}\n\n/**\n * Load a markdown corpus, preferring a Vite glob-result map and falling back to\n * a Node fs walk. Selection is by non-empty glob result — never an env flag.\n * Entries are normalized, optionally skip-filtered, and sorted by id for\n * determinism. The `import.meta.glob` literal stays at the CONSUMER call site\n * (passed in as `globModules`); this loader never constructs a glob.\n */\nexport function loadMarkdownCorpus(\n options: LoadCorpusOptions,\n importMetaUrl?: string,\n): CorpusLoadResult {\n const { anchor, globModules, fsBaseDir, fsLayout = 'flat', skip } = options\n\n let modules: GlobModules\n let source: CorpusLoadResult['source']\n if (globModules && Object.keys(globModules).length > 0) {\n modules = globModules\n source = 'vite'\n } else {\n const builtins = nodeBuiltins()\n if (builtins && fsBaseDir) {\n const root = resolveFsBase(builtins, fsBaseDir, importMetaUrl)\n modules = fsLayout === 'nested' ? fsWalkNested(builtins, root) : fsWalkFlat(builtins, root)\n source = Object.keys(modules).length > 0 ? 'fs' : 'empty'\n } else {\n modules = {}\n source = 'empty'\n }\n }\n\n const entries: CorpusEntry[] = []\n for (const [rawKey, content] of Object.entries(modules)) {\n if (typeof content !== 'string') continue\n const key = normalizeKey(rawKey, anchor)\n if (skip && skip(key)) continue\n entries.push({ id: toCorpusId(key, anchor), key, content })\n }\n entries.sort((a, b) => a.id.localeCompare(b.id))\n return { source, entries }\n}\n\n/** Project corpus entries onto SDK file mounts at a relative path under\n * `<anchor>/`. Always-mounted: the corpus is the agent's baseline knowledge. */\nexport function corpusSkills(corpus: CorpusEntry[], anchor: string): AgentProfileFileMount[] {\n return corpus\n .map(\n (entry) =>\n ({\n path: `${anchor}/${entry.id}.md`,\n resource: inlineResource(`${anchor}-${entry.id}`, entry.content),\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/** Project the registry's free-tier (or `tier`-matched) entries onto SDK file\n * mounts at the harness skill-discovery path. Tier-gating is the registry's\n * only selection rule — paid skills are installed on demand, not at boot. */\nexport function registrySkills(\n registry: SkillEntry[],\n tier: string = 'free',\n): AgentProfileFileMount[] {\n return registry\n .filter((s) => s.tier === tier)\n .map(\n (s) =>\n ({\n path: skillMountPath(s.id),\n resource: inlineResource(s.id, s.skillMd),\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/** Inputs to {@link composeShellResources}. Each channel is optional so a\n * product mounts only the systems it has — corpus-only, registry-only, or\n * both — without conflating them. */\nexport interface ComposeShellResourcesInput {\n /** Corpus mounts (always-mounted baseline). Pass the result of\n * {@link corpusSkills}, or a hand-built mount list. */\n skills?: AgentProfileFileMount[]\n /** Knowledge-corpus mounts (a second always-mounted corpus, e.g. a domain\n * knowledge pack distinct from the skills corpus). */\n knowledge?: AgentProfileFileMount[]\n /** Evolvable / learned-guidance mounts (single-file corpora). */\n evolvable?: AgentProfileFileMount[]\n /** Registry mounts (tier-gated). Pass the result of {@link registrySkills}. */\n registry?: AgentProfileFileMount[]\n /** Final skip filter applied to the composed mount list by mount `path`. */\n predicate?: (mount: AgentProfileFileMount) => boolean\n}\n\n/**\n * Compose every mount channel into one `resources.files`-ready array. Corpus\n * channels come first (baseline), the tier-gated registry last (so a registry\n * entry can override a corpus entry that mounts at the same path). The result\n * is exactly `AgentProfileFileMount[]` — assign it straight into\n * `profile.resources.files` with no cast.\n */\nexport function composeShellResources(input: ComposeShellResourcesInput): AgentProfileFileMount[] {\n const { skills = [], knowledge = [], evolvable = [], registry = [], predicate } = input\n const composed = [...skills, ...knowledge, ...evolvable, ...registry]\n return predicate ? composed.filter(predicate) : composed\n}\n\n// ─── Adoptable skills: one frontmatter parser, two delivery modes ─────────────\n\n/** Fields a `SKILL.md` frontmatter block may declare. All optional — absent\n * frontmatter (or an absent field within it) is legal; callers fill defaults\n * (see {@link skillEntryFromMarkdown}). */\nexport interface SkillFrontmatter {\n id?: string\n name?: string\n description?: string\n author?: { name: string; url?: string }\n source?: string\n category?: string\n tags?: string[]\n tier?: string\n}\n\n/** The result of {@link parseSkillFrontmatter}: the parsed fields, the body\n * with the frontmatter block stripped, and the original untouched text. */\nexport interface ParsedSkill {\n frontmatter: SkillFrontmatter\n body: string\n raw: string\n}\n\n/** Quoted values (`description: \"...\"`, as emitted by materialize's\n * `normalizeSkillMd`) are JSON strings — decode with `JSON.parse` so escapes\n * round-trip; anything else is a bare scalar, trimmed. */\nfunction parseFrontmatterScalar(value: string): string {\n const trimmed = value.trim()\n if (trimmed.startsWith('\"')) return JSON.parse(trimmed) as string\n return trimmed\n}\n\n/** THE one `SKILL.md` frontmatter parser — hand-rolled, no YAML dependency.\n *\n * Absent frontmatter (text does not open with a `---` delimiter line) is\n * legal: returns `{frontmatter: {}, body: raw, raw}`. An OPENED block with no\n * closing `---` is truncated input and throws. Inside the block: scalar\n * `key: value` lines (value optionally double-quoted, decoded via\n * `JSON.parse`); a nested `author:` block whose indented `name:`/`url:` lines\n * are the only children it accepts; `tags:` as an inline `[a, b]` list or as\n * an indented `- item` block. Unknown scalar keys are ignored (forward-compat)\n * — but a line that matches NONE of these shapes (no colon, an orphaned\n * indented line, a bad dash) throws naming the offending line. Silently\n * mis-parsed metadata is the bug class this parser exists to kill; an\n * unrecognized shape is never guessed at.\n */\nexport function parseSkillFrontmatter(raw: string): ParsedSkill {\n const lines = raw.split('\\n')\n if ((lines[0] ?? '').trim() !== '---') {\n return { frontmatter: {}, body: raw, raw }\n }\n\n let closeIndex = -1\n for (let i = 1; i < lines.length; i++) {\n if ((lines[i] ?? '').trim() === '---') {\n closeIndex = i\n break\n }\n }\n if (closeIndex === -1) {\n throw new Error(\n 'parseSkillFrontmatter: opening \"---\" has no closing \"---\" — truncated frontmatter block',\n )\n }\n\n const blockLines = lines.slice(1, closeIndex)\n const body = lines.slice(closeIndex + 1).join('\\n').replace(/^\\n+/, '')\n const frontmatter: SkillFrontmatter = {}\n const scalarKeys = new Set(['id', 'name', 'description', 'source', 'category', 'tier'])\n const topLineRe = /^(\\S[^:]*):\\s*(.*)$/\n\n let i = 0\n while (i < blockLines.length) {\n const line = blockLines[i] ?? ''\n if (line.trim() === '') {\n i++\n continue\n }\n const match = line.match(topLineRe)\n if (!match) {\n throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`)\n }\n const key = (match[1] ?? '').trim()\n const rest = match[2] ?? ''\n\n if (key === 'author') {\n if (rest.trim() !== '') {\n throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`)\n }\n const author: { name: string; url?: string } = { name: '' }\n let sawName = false\n i++\n while (i < blockLines.length && /^\\s+\\S/.test(blockLines[i] ?? '')) {\n const sub = (blockLines[i] ?? '').trim()\n const subMatch = sub.match(/^(name|url):\\s*(.*)$/)\n if (!subMatch) {\n throw new Error(\n `parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(blockLines[i])}`,\n )\n }\n const subKey = subMatch[1] as 'name' | 'url'\n const subValue = parseFrontmatterScalar(subMatch[2] ?? '')\n if (subKey === 'name') {\n author.name = subValue\n sawName = true\n } else {\n author.url = subValue\n }\n i++\n }\n if (sawName) frontmatter.author = author\n continue\n }\n\n if (key === 'tags') {\n const inline = rest.trim()\n if (inline.startsWith('[') && inline.endsWith(']')) {\n const inner = inline.slice(1, -1).trim()\n frontmatter.tags = inner === '' ? [] : inner.split(',').map((t) => parseFrontmatterScalar(t))\n i++\n continue\n }\n if (inline !== '') {\n throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`)\n }\n const tags: string[] = []\n i++\n while (i < blockLines.length && /^\\s*-\\s*/.test(blockLines[i] ?? '')) {\n tags.push(parseFrontmatterScalar((blockLines[i] ?? '').replace(/^\\s*-\\s*/, '')))\n i++\n }\n frontmatter.tags = tags\n continue\n }\n\n if (scalarKeys.has(key)) {\n ;(frontmatter as Record<string, string>)[key] = parseFrontmatterScalar(rest)\n }\n // Unknown scalar key: ignored, forward-compat.\n i++\n }\n\n return { frontmatter, body, raw }\n}\n\n/** Build a {@link SkillEntry} from a raw `SKILL.md` body. `id` comes from\n * frontmatter, falling back to `fallbackId` (typically the corpus entry's\n * slug/filename); neither present throws. `name` defaults to `id`,\n * `description` to `''`, `tier` to `'free'`. `skillMd` is always the\n * untouched `raw` input — the full file, frontmatter included, is what a\n * `mounted` delivery writes to disk and what `renderInlineSkills` strips per\n * render. */\nexport function skillEntryFromMarkdown(raw: string, fallbackId?: string): SkillEntry {\n const { frontmatter } = parseSkillFrontmatter(raw)\n const id = frontmatter.id ?? fallbackId\n if (!id) {\n throw new Error(\n 'skillEntryFromMarkdown: no \"id\" in frontmatter and no fallbackId supplied — cannot build a SkillEntry',\n )\n }\n const entry: SkillEntry = {\n id,\n name: frontmatter.name ?? id,\n description: frontmatter.description ?? '',\n tier: frontmatter.tier ?? 'free',\n skillMd: raw,\n }\n if (frontmatter.author) entry.author = frontmatter.author\n if (frontmatter.source) entry.source = frontmatter.source\n if (frontmatter.category) entry.category = frontmatter.category\n if (frontmatter.tags) entry.tags = frontmatter.tags\n return entry\n}\n\n/** Map a loaded corpus (see {@link loadMarkdownCorpus}) onto `SkillEntry`s,\n * using each entry's `id` as the fallback when its `SKILL.md` carries no\n * frontmatter `id` of its own. */\nexport function parseCorpusSkills(corpus: CorpusEntry[]): SkillEntry[] {\n return corpus.map((entry) => skillEntryFromMarkdown(entry.content, entry.id))\n}\n\n/** Project skills onto the typed `resources.skills` channel\n * (`AgentProfileResourceRef[]`), tier-filtered (same `s.tier === tier`\n * semantics as {@link registrySkills}) when `opts.tier` is given, sorted by\n * id for determinism. `ref.name` MUST be the skill id: the platform\n * materializer writes each ref to `${skillDir}/${name}/SKILL.md`. */\nexport function skillRefs(\n skills: SkillEntry[],\n opts: { tier?: string } = {},\n): AgentProfileResourceRef[] {\n const filtered = opts.tier ? skills.filter((s) => s.tier === opts.tier) : skills\n return [...filtered]\n .sort((a, b) => a.id.localeCompare(b.id))\n .map((s) => inlineResource(s.id, s.skillMd))\n}\n\n/** Strip the frontmatter block from a skill's `skillMd` and render it as a\n * prompt subsection. The default `body` renderer {@link renderInlineSkills}\n * passes to itself. */\nfunction renderInlineSkillBody(skill: SkillEntry): string {\n const { body } = parseSkillFrontmatter(skill.skillMd)\n return `### ${skill.name}\\n\\n${body.trim()}`\n}\n\n/** Inputs to {@link renderInlineSkills}. */\nexport interface RenderInlineSkillsInput {\n skills: SkillEntry[]\n /** Section heading. Default `'## Skills'`. */\n heading?: string\n /** Tier filter — same semantics as {@link skillRefs}. */\n tier?: string\n /** Per-skill body renderer. Default strips frontmatter and emits\n * `### <name>\\n\\n<body>`. */\n body?: (skill: SkillEntry) => string\n}\n\n/**\n * Render every (tier-filtered) skill's full body inline into the prompt — the\n * `inline` delivery mode. Returns `''` when no skill survives the filter, else\n * a section starting `\\n\\n<heading>\\n\\n` with each skill's body joined by\n * `\\n\\n` — the same \"already carries its own leading `\\n\\n`\" shape\n * `assembleSystemPrompt` expects from every section it concatenates.\n */\nexport function renderInlineSkills(input: RenderInlineSkillsInput): string {\n const heading = input.heading ?? '## Skills'\n const filtered = input.tier ? input.skills.filter((s) => s.tier === input.tier) : input.skills\n if (filtered.length === 0) return ''\n const renderBody = input.body ?? renderInlineSkillBody\n return `\\n\\n${heading}\\n\\n${filtered.map(renderBody).join('\\n\\n')}`\n}\n\n/** Inputs to {@link renderSkillIndex}. */\nexport interface RenderSkillIndexInput {\n skills: SkillEntry[]\n /** cwd-relative directory the skills are mounted under (e.g.\n * `.opencode/skills`) — named in each index line so the agent knows where\n * to read the full `SKILL.md`. */\n skillDir: string\n /** Section heading. Default `'## Skills'`. */\n heading?: string\n /** Tier filter — same semantics as {@link skillRefs}. */\n tier?: string\n}\n\n/**\n * Render a one-line-per-skill INDEX (name, description, and the path to read\n * the full body) — the `mounted` delivery mode's prompt section, paired with\n * {@link skillRefs} putting the actual files on `resources.skills`. Same\n * empty/section shape as {@link renderInlineSkills}.\n */\nexport function renderSkillIndex(input: RenderSkillIndexInput): string {\n const heading = input.heading ?? '## Skills'\n const filtered = input.tier ? input.skills.filter((s) => s.tier === input.tier) : input.skills\n if (filtered.length === 0) return ''\n const lines = filtered.map(\n (s) => `- ${s.name}: ${s.description} (read ${input.skillDir}/${s.id}/SKILL.md)`,\n )\n return `\\n\\n${heading}\\n\\n${lines.join('\\n')}`\n}\n\n/** How a skill reaches the agent: `inline` renders its full body into the\n * system prompt; `mounted` puts it on the typed `resources.skills` channel\n * and renders only an index line. */\nexport type SkillDeliveryMode = 'inline' | 'mounted'\n\n/** The output of {@link composeSkills}: the refs to attach to\n * `resources.skills` (empty for `inline`) and the prompt section to fold into\n * the system prompt (already carries its own leading `\\n\\n`, or `''`).\n * `inlineIds`/`mountedIds` record which (tier-filtered) skills were delivered\n * in each mode so {@link mergeComposedSkills} can enforce the modes stay\n * disjoint when batches are combined. */\nexport interface ComposedSkills {\n refs: AgentProfileResourceRef[]\n promptSection: string\n inlineIds: string[]\n mountedIds: string[]\n}\n\n/** Inputs to {@link composeSkills}. */\nexport interface ComposeSkillsInput {\n skills: SkillEntry[]\n mode: SkillDeliveryMode\n tier?: string\n heading?: string\n /** REQUIRED (non-null) for `mode: 'mounted'` — the cwd-relative skill dir\n * the harness reads. Resolve it with `@tangle-network/agent-app/skills-placement`\n * rather than hardcoding it; omitted or `null` throws. */\n skillDir?: string | null\n}\n\n/**\n * Build the {@link ComposedSkills} for one delivery mode. `inline` never\n * touches `resources.skills` — `refs` is always `[]`. `mounted` requires a\n * non-null `skillDir` (the harness must have a native skill-discovery\n * directory); a null/absent one throws rather than silently falling back, so\n * the caller resolves the fallback deliberately (see\n * `@tangle-network/agent-app/skills-placement`'s `composeSkillsForHarness`,\n * whose own default is to throw rather than choose `inline` for you — inline\n * is a narrow, explicitly opted-into fallback, not a co-equal mode).\n */\nexport function composeSkills(input: ComposeSkillsInput): ComposedSkills {\n const { skills, mode, tier, heading } = input\n const deliveredIds = (tier ? skills.filter((s) => s.tier === tier) : skills).map((s) => s.id)\n if (mode === 'inline') {\n return {\n refs: [],\n promptSection: renderInlineSkills({ skills, tier, heading }),\n inlineIds: deliveredIds,\n mountedIds: [],\n }\n }\n if (!input.skillDir) {\n throw new Error(\n 'composeSkills: mode \"mounted\" requires a non-null skillDir — this harness cannot receive ' +\n 'skill files at a cwd path; pass mode \"inline\" instead',\n )\n }\n return {\n refs: skillRefs(skills, { tier }),\n promptSection: renderSkillIndex({ skills, skillDir: input.skillDir, tier, heading }),\n inlineIds: [],\n mountedIds: deliveredIds,\n }\n}\n\n/** Throw when the same skill id is delivered both `inline` and `mounted` —\n * the agent would see it twice (once in the prompt body, once as a mounted\n * file it's told to go read), doubling prompt bytes and inviting drift\n * between the two copies. Lists every offending id in the message. */\nexport function assertSkillDeliveryDisjoint(\n inlineIds: Iterable<string>,\n mountedIds: Iterable<string>,\n): void {\n const inline = new Set(inlineIds)\n const overlap = [...new Set(mountedIds)].filter((id) => inline.has(id))\n if (overlap.length > 0) {\n throw new Error(\n `assertSkillDeliveryDisjoint: skill id(s) delivered both inline and mounted: ${overlap.join(', ')}`,\n )\n }\n}\n\n/**\n * Combine multiple {@link ComposedSkills} batches (e.g. a built-in corpus and\n * an installed catalog) into one, concatenating refs and prompt sections in\n * batch order. This is the seam where inline and mounted deliveries can first\n * collide, so it applies {@link assertSkillDeliveryDisjoint} — a skill id\n * delivered inline by one batch and mounted by another throws instead of\n * reaching the agent twice. Merge through this rather than spreading batch\n * fields by hand.\n */\nexport function mergeComposedSkills(batches: ComposedSkills[]): ComposedSkills {\n const merged: ComposedSkills = {\n refs: batches.flatMap((b) => b.refs),\n promptSection: batches.map((b) => b.promptSection).join(''),\n inlineIds: batches.flatMap((b) => b.inlineIds),\n mountedIds: batches.flatMap((b) => b.mountedIds),\n }\n assertSkillDeliveryDisjoint(merged.inlineIds, merged.mountedIds)\n return merged\n}\n"],"mappings":"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","names":["i"]}
|
package/dist/profile/index.d.ts
CHANGED
|
@@ -113,6 +113,14 @@ interface ComposeProfileBudget {
|
|
|
113
113
|
* for a product with a known-big prompt that must still ship (it yells on
|
|
114
114
|
* every compose instead of blocking). */
|
|
115
115
|
warnOnly?: boolean;
|
|
116
|
+
/** Required to raise {@link maxSystemPromptBytes} above
|
|
117
|
+
* {@link DEFAULT_MAX_SYSTEM_PROMPT_BYTES} or to set {@link warnOnly}: a
|
|
118
|
+
* written reason naming what stays inline and why it cannot be mounted.
|
|
119
|
+
* Weakening the cap is a product decision that outlives the person making
|
|
120
|
+
* it, and the usual cause is reference material concatenated into the prompt
|
|
121
|
+
* that belongs in `resources.files`; demanding the sentence here keeps that
|
|
122
|
+
* from happening by accident. */
|
|
123
|
+
overBudgetReason?: string;
|
|
116
124
|
}
|
|
117
125
|
/** Largest markdown-heading-delimited sections of a prompt, by UTF-8 bytes.
|
|
118
126
|
* Cheap heuristic: split on `#`-heading lines; the preamble before the first
|
package/dist/profile/index.js
CHANGED
|
@@ -13,12 +13,21 @@ import {
|
|
|
13
13
|
skillEntryFromMarkdown,
|
|
14
14
|
skillMountPath,
|
|
15
15
|
skillRefs
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-34M7AUWO.js";
|
|
17
17
|
|
|
18
18
|
// src/profile/index.ts
|
|
19
19
|
import { mergeAgentProfiles } from "@tangle-network/sandbox";
|
|
20
20
|
import { profile } from "@tangle-network/agent-eval";
|
|
21
21
|
var DEFAULT_MAX_SYSTEM_PROMPT_BYTES = 4e4;
|
|
22
|
+
function assertBudgetPolicy(budget) {
|
|
23
|
+
const raisedCap = budget.maxSystemPromptBytes !== void 0 && budget.maxSystemPromptBytes > DEFAULT_MAX_SYSTEM_PROMPT_BYTES;
|
|
24
|
+
if (!raisedCap && !budget.warnOnly) return;
|
|
25
|
+
if ((budget.overBudgetReason ?? "").trim() !== "") return;
|
|
26
|
+
const weakened = raisedCap ? `maxSystemPromptBytes ${budget.maxSystemPromptBytes} exceeds the ${DEFAULT_MAX_SYSTEM_PROMPT_BYTES}-byte default` : "warnOnly downgrades the over-budget throw to a warning";
|
|
27
|
+
throw new Error(
|
|
28
|
+
`${weakened} without an overBudgetReason. Oversized system prompts degrade toward empty answers, so the cap is not a formality. Before raising it: rank the prompt with largestPromptSections() \u2014 reference material (playbooks, checklists, corpora) belongs in resources.files via corpusSkills()/userSkillMounts() or composeSkills({ mode: 'mounted' }), which puts the bodies on disk in the sandbox and leaves a short index in the prompt. Only content the agent must obey without a tool call should stay inline. If the prompt is genuinely irreducible, set overBudgetReason to the sentence that says so.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
22
31
|
function largestPromptSections(prompt, top = 3) {
|
|
23
32
|
const encoder = new TextEncoder();
|
|
24
33
|
const sections = [];
|
|
@@ -38,11 +47,12 @@ function largestPromptSections(prompt, top = 3) {
|
|
|
38
47
|
return sections.sort((a, b) => b.bytes - a.bytes).slice(0, top);
|
|
39
48
|
}
|
|
40
49
|
function assertSystemPromptWithinBudget(systemPrompt, budget = {}) {
|
|
50
|
+
assertBudgetPolicy(budget);
|
|
41
51
|
const max = budget.maxSystemPromptBytes ?? DEFAULT_MAX_SYSTEM_PROMPT_BYTES;
|
|
42
52
|
const bytes = new TextEncoder().encode(systemPrompt).byteLength;
|
|
43
53
|
if (bytes <= max) return;
|
|
44
54
|
const sections = largestPromptSections(systemPrompt).map((s) => `"${s.title}" (${s.bytes}B)`).join(", ");
|
|
45
|
-
const message = `composed systemPrompt is ${bytes} bytes \u2014 over the ${max}-byte budget (oversized prompts degrade to empty answers). ` + (sections ? `Largest sections: ${sections}. ` : "") + `
|
|
55
|
+
const message = `composed systemPrompt is ${bytes} bytes \u2014 over the ${max}-byte budget (oversized prompts degrade to empty answers). ` + (sections ? `Largest sections: ${sections}. ` : "") + `Move reference material to resources.files (corpusSkills/userSkillMounts, or composeSkills({ mode: 'mounted' })) so the bodies land on disk in the sandbox and the prompt keeps only an index; keep inline only what the agent must obey without a tool call. Raising maxSystemPromptBytes requires an overBudgetReason.`;
|
|
46
56
|
if (budget.warnOnly) {
|
|
47
57
|
console.warn(`[profile] ${message}`);
|
|
48
58
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/profile/index.ts"],"sourcesContent":["/**\n * Profile composer + evolvable-section seam for agent products.\n *\n * The standard \"load a deployable AgentProfile, including skills, plus the\n * skills the end user added to their own instance\" entry point. A product holds\n * a canonical base `AgentProfile` (role/environment/tool-conventions rendered\n * into `prompt.systemPrompt`, baseline skills, baseline MCP). At deploy/turn\n * time it layers four file-mount channels onto `resources.files` —\n *\n * 1. skills — the always-mounted product skill corpus\n * 2. knowledge — a second always-mounted corpus (domain knowledge pack)\n * 3. registry — the tier-gated installable registry (free -> boot-mounted)\n * 4. userSkills — per-user / per-workspace skills the END USER adds to their\n * own instance, mounted at `~/.claude/skills/<id>/SKILL.md`\n * exactly like the registry's free tier\n *\n * plus an optional MCP overlay (delegation + per-turn app-tool side channel), a\n * per-turn `systemPrompt` override, and a `name` override. The merge is the SDK\n * `mergeAgentProfiles`: `mcp` is last-wins per key (base -> overlay), `resources`\n * arrays are concatenated (base ++ overlay), `prompt` is shallow-merged so an\n * overlay carrying only `systemPrompt` overrides it while keeping base\n * instructions. The compose algebra is DATA — the product injects the base\n * profile, the channel mounts (built with the `skills` subpath primitives), the\n * delegation/app-tool MCP map, and the override strings; nothing here reaches\n * for env, a glob, or a specific product's profile.\n *\n * The evolvable-section seam is the loader closure. A product's single\n * self-improvable domain section (the one `applyDomainPatch` targets) loads its\n * body from a deployed markdown override, falling back to an in-tree baseline.\n * The `import.meta.glob('<lit>', ...)` literal must stay at the CONSUMER call\n * site (Vite static-analyzes it), so `makeEvolvableSection` takes the loader as\n * a closure and a REQUIRED `baseline` — it never constructs a glob and never\n * defaults the baseline, so a product can't render an empty learned-guidance\n * section. `stripComments` is the shared \"is this addendum really empty?\" test.\n */\n\nimport type {\n AgentProfile,\n AgentProfileFileMount,\n AgentProfileMcpServer,\n AgentProfileResourceRef,\n} from '@tangle-network/sandbox'\nimport { mergeAgentProfiles } from '@tangle-network/sandbox'\nimport { profile } from '@tangle-network/agent-eval'\nimport {\n composeShellResources,\n registrySkills,\n skillMountPath,\n type ComposeShellResourcesInput,\n type SkillEntry,\n} from '../skills/index'\n\n/** Re-expose the agent-eval section/render substrate so a product wires the\n * evolvable surface through ONE subpath: `makeEvolvableSection` builds the\n * section, `profile.renderProfile` renders it, `profile.applyDomainPatch` lets\n * the loop patch it by id. The rendering/patching engine stays in agent-eval;\n * reach it through this namespace (re-exporting the bare fns would leak\n * agent-eval's un-nameable AgentProfile type into our generated d.ts). */\nexport { profile }\n\n/** The file-mount channels layered onto `resources.files`. The first three\n * mirror {@link ComposeShellResourcesInput}; `userSkills` is the per-user /\n * per-workspace channel — skills the END USER added to their own instance,\n * mounted at the harness skill-discovery path like the registry's free tier. */\nexport interface ProfileChannels {\n /** Always-mounted skill corpus (pass `corpusSkills(...)`). */\n skills?: AgentProfileFileMount[]\n /** Always-mounted knowledge corpus (pass `corpusSkills(...)` for the pack). */\n knowledge?: AgentProfileFileMount[]\n /** Single-file evolvable / learned-guidance corpora, if mounted as files. */\n evolvable?: AgentProfileFileMount[]\n /** Tier-gated installable registry (pass the registry array; free tier is\n * mounted, paid is install-on-demand). Gated through {@link registrySkills}. */\n registry?: SkillEntry[]\n /** Per-user / per-workspace skills the end user adds to their own instance.\n * Mounted at `~/.claude/skills/<id>/SKILL.md`, the same harness path the\n * registry uses, so a user skill and a registry skill with the same id\n * collide deterministically (the user skill, appended last, wins). */\n userSkills?: UserSkill[]\n /** Final skip filter applied to the composed mount list by mount `path`. */\n filesPredicate?: (mount: AgentProfileFileMount) => boolean\n /** Typed `resources.skills` channel — refs the platform materializer places\n * at the harness-native skill dir (see {@link skillRefs} and\n * `@tangle-network/agent-app/skills-placement`'s `composeSkillsForHarness`).\n * The successor to path-baked mounts: `registry`/`userSkills` above mount\n * files at the hardcoded claude-code path via {@link skillMountPath};\n * `skillRefs` instead rides the provider-neutral `resources.skills` field\n * the platform resolves per harness. */\n skillRefs?: AgentProfileResourceRef[]\n /** Tier passed to {@link registrySkills} for the `registry` channel.\n * Previously hardcoded `'free'`; default unchanged. */\n registryTier?: string\n}\n\n/** A per-user / per-workspace skill: an id and an inline `SKILL.md` body. The\n * user-facing analogue of a registry {@link SkillEntry} with no tier gate —\n * every user skill is mounted (the user opted in by adding it). */\nexport interface UserSkill {\n id: string\n /** Inline `SKILL.md` body mounted at {@link skillMountPath}. */\n skillMd: string\n}\n\n/** Overlay overrides applied on top of the channel mounts. */\nexport interface ProfileOverlay {\n /** Extra MCP servers merged into the profile `mcp` map (last-wins per key over\n * the base servers). The product builds this from its delegation MCP entry\n * and any per-turn app-tool side-channel servers. An absent/`undefined` entry\n * is dropped — pass only the servers that resolved (fail-closed at the seam,\n * not here). */\n mcp?: Record<string, AgentProfileMcpServer>\n /** Per-turn system-prompt override. When set, replaces the base\n * `prompt.systemPrompt` while keeping base `prompt.instructions`. When unset,\n * the base prompt passes through unchanged. */\n systemPrompt?: string\n /** Extra instruction lines merged onto the active prompt (e.g. a per-turn\n * domain/integration directive). Appended to base `prompt.instructions` by\n * the SDK merge. */\n instructions?: string[]\n /** Profile `name` override. When unset, the base name is kept. */\n name?: string\n}\n\n/** Byte budget on the FINAL composed `prompt.systemPrompt`. Past this the\n * model degrades sharply (a 122,659-byte prompt shipped once and the model\n * returned empty answers), so the default gate throws well before that. */\nexport const DEFAULT_MAX_SYSTEM_PROMPT_BYTES = 40_000\n\n/** Budget config for the composed system prompt. */\nexport interface ComposeProfileBudget {\n /** Byte cap on the composed `prompt.systemPrompt`.\n * Default {@link DEFAULT_MAX_SYSTEM_PROMPT_BYTES}. */\n maxSystemPromptBytes?: number\n /** Downgrade the over-budget throw to a `console.warn` — the escape hatch\n * for a product with a known-big prompt that must still ship (it yells on\n * every compose instead of blocking). */\n warnOnly?: boolean\n}\n\n/** Largest markdown-heading-delimited sections of a prompt, by UTF-8 bytes.\n * Cheap heuristic: split on `#`-heading lines; the preamble before the first\n * heading reports as \"(preamble)\". */\nexport function largestPromptSections(\n prompt: string,\n top = 3,\n): Array<{ title: string; bytes: number }> {\n const encoder = new TextEncoder()\n const sections: Array<{ title: string; bytes: number }> = []\n let title = '(preamble)'\n let start = 0\n const flush = (end: number) => {\n const body = prompt.slice(start, end)\n if (body.trim()) sections.push({ title, bytes: encoder.encode(body).byteLength })\n }\n const headingRe = /^#{1,6}\\s+(.+)$/gm\n for (const match of prompt.matchAll(headingRe)) {\n flush(match.index)\n title = (match[1] ?? '').trim() || '(untitled section)'\n start = match.index\n }\n flush(prompt.length)\n return sections.sort((a, b) => b.bytes - a.bytes).slice(0, top)\n}\n\n/** Enforce {@link ComposeProfileBudget} on a composed system prompt: over\n * budget throws (or warns with `warnOnly`) with the actual size and the\n * top-3 largest sections. Exported so a product assembling its prompt\n * outside {@link composeAgentProfile} (e.g. via the `/prompt` assembler) can\n * run the same gate at its own final-composition point. */\nexport function assertSystemPromptWithinBudget(\n systemPrompt: string,\n budget: ComposeProfileBudget = {},\n): void {\n const max = budget.maxSystemPromptBytes ?? DEFAULT_MAX_SYSTEM_PROMPT_BYTES\n const bytes = new TextEncoder().encode(systemPrompt).byteLength\n if (bytes <= max) return\n const sections = largestPromptSections(systemPrompt)\n .map((s) => `\"${s.title}\" (${s.bytes}B)`)\n .join(', ')\n const message =\n `composed systemPrompt is ${bytes} bytes — over the ${max}-byte budget ` +\n `(oversized prompts degrade to empty answers). ` +\n (sections ? `Largest sections: ${sections}. ` : '') +\n `Trim the prompt, move content to skills/knowledge mounts, or raise maxSystemPromptBytes deliberately.`\n if (budget.warnOnly) {\n console.warn(`[profile] ${message}`)\n return\n }\n throw new Error(message)\n}\n\n/** Project per-user skills onto SDK file mounts at the harness skill-discovery\n * path. No tier gate — a user skill is mounted because the user added it.\n * Sorted by path for determinism (matches {@link registrySkills}). */\nexport function userSkillMounts(userSkills: UserSkill[]): AgentProfileFileMount[] {\n return userSkills\n .map(\n (s) =>\n ({\n path: skillMountPath(s.id),\n resource: { kind: 'inline', name: s.id, content: s.skillMd },\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/**\n * Compose a deployable `AgentProfile` from a canonical base plus the four\n * file-mount channels and the overlay overrides.\n *\n * Files: base `resources.files` come first; the four channels follow in\n * `skills -> knowledge -> evolvable -> registry -> userSkills` order (so a\n * userSkill that mounts at the same path as a registry skill is the last write\n * and wins). MCP: base servers first, the overlay `mcp` last (last-wins per\n * key). Prompt: the overlay `systemPrompt`, when set, replaces the base one;\n * base instructions are preserved. Name: the overlay `name`, when set, wins.\n *\n * The merge delegates to the SDK `mergeAgentProfiles` (overlay-wins on records,\n * arrays concatenated) — the deterministic algebra is the overlay we hand it,\n * not a hand-rolled spread. `mergeAgentProfiles(base, overlay)` returns\n * `undefined` only when BOTH are `undefined`; `base` is always defined here, so\n * the result is non-`undefined` by construction and we assert that to the caller.\n *\n * The composed `prompt.systemPrompt` is byte-budgeted here — the single point\n * where the FINAL prompt exists ({@link assertSystemPromptWithinBudget};\n * default {@link DEFAULT_MAX_SYSTEM_PROMPT_BYTES}, `warnOnly` escape hatch).\n */\nexport function composeAgentProfile(\n base: AgentProfile,\n channels: ProfileChannels = {},\n overlay: ProfileOverlay = {},\n budget: ComposeProfileBudget = {},\n): AgentProfile {\n const shellInput: ComposeShellResourcesInput = {\n skills: channels.skills,\n knowledge: channels.knowledge,\n evolvable: channels.evolvable,\n registry: channels.registry\n ? registrySkills(channels.registry, channels.registryTier ?? 'free')\n : undefined,\n predicate: channels.filesPredicate,\n }\n const channelFiles = composeShellResources(shellInput)\n const userFiles = channels.userSkills ? userSkillMounts(channels.userSkills) : []\n const overlayFiles = channels.filesPredicate\n ? userFiles.filter(channels.filesPredicate)\n : userFiles\n const files = [...channelFiles, ...overlayFiles]\n\n const promptOverlay: { systemPrompt?: string; instructions?: string[] } = {}\n if (overlay.systemPrompt) promptOverlay.systemPrompt = overlay.systemPrompt\n if (overlay.instructions && overlay.instructions.length > 0) promptOverlay.instructions = overlay.instructions\n\n const overlayProfile: AgentProfile = {\n ...(overlay.name ? { name: overlay.name } : {}),\n ...(Object.keys(promptOverlay).length > 0 ? { prompt: promptOverlay } : {}),\n ...(overlay.mcp ? { mcp: overlay.mcp } : {}),\n resources: {\n files,\n ...(channels.skillRefs && channels.skillRefs.length > 0 ? { skills: channels.skillRefs } : {}),\n },\n }\n\n const merged = mergeAgentProfiles(base, overlayProfile)\n if (!merged)\n throw new Error('composeAgentProfile: mergeAgentProfiles returned undefined for a defined base')\n // Byte-budget gate on the FINAL composed systemPrompt — this is the single\n // point where every channel and overlay has been merged in.\n const systemPrompt = merged.prompt?.systemPrompt\n if (typeof systemPrompt === 'string') assertSystemPromptWithinBudget(systemPrompt, budget)\n return pruneEmptyResourceChannels(merged)\n}\n\n/** Drop empty resource channels the SDK merge normalizes in (`tools`/`skills`/\n * `agents`/`commands`: `[]`), so the composed profile's wire payload carries\n * only the channels that actually have content — one canonical shape every app\n * emits, instead of a sidecar payload full of empty arrays. */\nfunction pruneEmptyResourceChannels(profile: AgentProfile): AgentProfile {\n if (!profile.resources) return profile\n const kept = Object.fromEntries(\n Object.entries(profile.resources).filter(([, value]) => !(Array.isArray(value) && value.length === 0)),\n ) as AgentProfile['resources']\n const out: AgentProfile = { ...profile, resources: kept }\n if (kept && Object.keys(kept).length === 0) delete out.resources\n return out\n}\n\n/** True body of an addendum file with HTML comments stripped — an all-comment\n * placeholder counts as empty, so the loader falls back to the baseline. */\nexport function stripComments(raw: string): string {\n return raw.replace(/<!--[\\s\\S]*?-->/g, '').trim()\n}\n\n/** Inputs to {@link makeEvolvableSection}. */\nexport interface EvolvableSectionInput {\n /** Section id the self-improvement loop targets with `applyDomainPatch`. */\n id: string\n /** Section title rendered as `### <title>`. */\n title: string\n /**\n * Load the deployed section body. The CONSUMER supplies this closure and runs\n * its own `import.meta.glob('<lit>', { eager: true, query: '?raw', import:\n * 'default' })` inside it — the literal must stay at the call site so Vite can\n * static-analyze it; a glob constructed here would not resolve the product's\n * files. Return the raw markdown (comments and all); `makeEvolvableSection`\n * applies {@link stripComments} to decide whether it is really populated.\n */\n load: () => string\n /**\n * The in-tree fallback body, used when `load()` returns an\n * all-comments/empty placeholder. REQUIRED — no internal default — so a\n * product can never accidentally render an empty evolvable section.\n */\n baseline: string\n}\n\n/**\n * Build the one evolvable (`evolvable: true`) domain section whose body comes\n * from the product's loader, falling back to the required baseline when the\n * loaded body is empty after stripping comments. Returns the agent-eval\n * `AgentProfileSection` shape — drop it straight into `prodProfile`'s shipped\n * sections. The loader is the only seam; the empty-vs-populated rule and the\n * baseline fallback are the lifted algebra.\n */\nexport function makeEvolvableSection(input: EvolvableSectionInput): profile.AgentProfileSection {\n const loaded = input.load()\n const body = stripComments(loaded) ? loaded.trim() : input.baseline\n return { id: input.id, title: input.title, body, evolvable: true }\n}\n\nexport {\n assertSkillDeliveryDisjoint,\n composeShellResources,\n composeSkills,\n corpusSkills,\n loadMarkdownCorpus,\n mergeComposedSkills,\n parseCorpusSkills,\n parseSkillFrontmatter,\n registrySkills,\n renderInlineSkills,\n renderSkillIndex,\n skillEntryFromMarkdown,\n skillMountPath,\n skillRefs,\n} from '../skills/index'\nexport type {\n ComposedSkills,\n ComposeShellResourcesInput,\n CorpusEntry,\n CorpusLoadResult,\n GlobModules,\n LoadCorpusOptions,\n ParsedSkill,\n SkillDeliveryMode,\n SkillEntry,\n SkillFrontmatter,\n} from '../skills/index'\n"],"mappings":"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","names":["profile"]}
|
|
1
|
+
{"version":3,"sources":["../../src/profile/index.ts"],"sourcesContent":["/**\n * Profile composer + evolvable-section seam for agent products.\n *\n * The standard \"load a deployable AgentProfile, including skills, plus the\n * skills the end user added to their own instance\" entry point. A product holds\n * a canonical base `AgentProfile` (role/environment/tool-conventions rendered\n * into `prompt.systemPrompt`, baseline skills, baseline MCP). At deploy/turn\n * time it layers four file-mount channels onto `resources.files` —\n *\n * 1. skills — the always-mounted product skill corpus\n * 2. knowledge — a second always-mounted corpus (domain knowledge pack)\n * 3. registry — the tier-gated installable registry (free -> boot-mounted)\n * 4. userSkills — per-user / per-workspace skills the END USER adds to their\n * own instance, mounted at `~/.claude/skills/<id>/SKILL.md`\n * exactly like the registry's free tier\n *\n * plus an optional MCP overlay (delegation + per-turn app-tool side channel), a\n * per-turn `systemPrompt` override, and a `name` override. The merge is the SDK\n * `mergeAgentProfiles`: `mcp` is last-wins per key (base -> overlay), `resources`\n * arrays are concatenated (base ++ overlay), `prompt` is shallow-merged so an\n * overlay carrying only `systemPrompt` overrides it while keeping base\n * instructions. The compose algebra is DATA — the product injects the base\n * profile, the channel mounts (built with the `skills` subpath primitives), the\n * delegation/app-tool MCP map, and the override strings; nothing here reaches\n * for env, a glob, or a specific product's profile.\n *\n * The evolvable-section seam is the loader closure. A product's single\n * self-improvable domain section (the one `applyDomainPatch` targets) loads its\n * body from a deployed markdown override, falling back to an in-tree baseline.\n * The `import.meta.glob('<lit>', ...)` literal must stay at the CONSUMER call\n * site (Vite static-analyzes it), so `makeEvolvableSection` takes the loader as\n * a closure and a REQUIRED `baseline` — it never constructs a glob and never\n * defaults the baseline, so a product can't render an empty learned-guidance\n * section. `stripComments` is the shared \"is this addendum really empty?\" test.\n */\n\nimport type {\n AgentProfile,\n AgentProfileFileMount,\n AgentProfileMcpServer,\n AgentProfileResourceRef,\n} from '@tangle-network/sandbox'\nimport { mergeAgentProfiles } from '@tangle-network/sandbox'\nimport { profile } from '@tangle-network/agent-eval'\nimport {\n composeShellResources,\n registrySkills,\n skillMountPath,\n type ComposeShellResourcesInput,\n type SkillEntry,\n} from '../skills/index'\n\n/** Re-expose the agent-eval section/render substrate so a product wires the\n * evolvable surface through ONE subpath: `makeEvolvableSection` builds the\n * section, `profile.renderProfile` renders it, `profile.applyDomainPatch` lets\n * the loop patch it by id. The rendering/patching engine stays in agent-eval;\n * reach it through this namespace (re-exporting the bare fns would leak\n * agent-eval's un-nameable AgentProfile type into our generated d.ts). */\nexport { profile }\n\n/** The file-mount channels layered onto `resources.files`. The first three\n * mirror {@link ComposeShellResourcesInput}; `userSkills` is the per-user /\n * per-workspace channel — skills the END USER added to their own instance,\n * mounted at the harness skill-discovery path like the registry's free tier. */\nexport interface ProfileChannels {\n /** Always-mounted skill corpus (pass `corpusSkills(...)`). */\n skills?: AgentProfileFileMount[]\n /** Always-mounted knowledge corpus (pass `corpusSkills(...)` for the pack). */\n knowledge?: AgentProfileFileMount[]\n /** Single-file evolvable / learned-guidance corpora, if mounted as files. */\n evolvable?: AgentProfileFileMount[]\n /** Tier-gated installable registry (pass the registry array; free tier is\n * mounted, paid is install-on-demand). Gated through {@link registrySkills}. */\n registry?: SkillEntry[]\n /** Per-user / per-workspace skills the end user adds to their own instance.\n * Mounted at `~/.claude/skills/<id>/SKILL.md`, the same harness path the\n * registry uses, so a user skill and a registry skill with the same id\n * collide deterministically (the user skill, appended last, wins). */\n userSkills?: UserSkill[]\n /** Final skip filter applied to the composed mount list by mount `path`. */\n filesPredicate?: (mount: AgentProfileFileMount) => boolean\n /** Typed `resources.skills` channel — refs the platform materializer places\n * at the harness-native skill dir (see {@link skillRefs} and\n * `@tangle-network/agent-app/skills-placement`'s `composeSkillsForHarness`).\n * The successor to path-baked mounts: `registry`/`userSkills` above mount\n * files at the hardcoded claude-code path via {@link skillMountPath};\n * `skillRefs` instead rides the provider-neutral `resources.skills` field\n * the platform resolves per harness. */\n skillRefs?: AgentProfileResourceRef[]\n /** Tier passed to {@link registrySkills} for the `registry` channel.\n * Previously hardcoded `'free'`; default unchanged. */\n registryTier?: string\n}\n\n/** A per-user / per-workspace skill: an id and an inline `SKILL.md` body. The\n * user-facing analogue of a registry {@link SkillEntry} with no tier gate —\n * every user skill is mounted (the user opted in by adding it). */\nexport interface UserSkill {\n id: string\n /** Inline `SKILL.md` body mounted at {@link skillMountPath}. */\n skillMd: string\n}\n\n/** Overlay overrides applied on top of the channel mounts. */\nexport interface ProfileOverlay {\n /** Extra MCP servers merged into the profile `mcp` map (last-wins per key over\n * the base servers). The product builds this from its delegation MCP entry\n * and any per-turn app-tool side-channel servers. An absent/`undefined` entry\n * is dropped — pass only the servers that resolved (fail-closed at the seam,\n * not here). */\n mcp?: Record<string, AgentProfileMcpServer>\n /** Per-turn system-prompt override. When set, replaces the base\n * `prompt.systemPrompt` while keeping base `prompt.instructions`. When unset,\n * the base prompt passes through unchanged. */\n systemPrompt?: string\n /** Extra instruction lines merged onto the active prompt (e.g. a per-turn\n * domain/integration directive). Appended to base `prompt.instructions` by\n * the SDK merge. */\n instructions?: string[]\n /** Profile `name` override. When unset, the base name is kept. */\n name?: string\n}\n\n/** Byte budget on the FINAL composed `prompt.systemPrompt`. Past this the\n * model degrades sharply (a 122,659-byte prompt shipped once and the model\n * returned empty answers), so the default gate throws well before that. */\nexport const DEFAULT_MAX_SYSTEM_PROMPT_BYTES = 40_000\n\n/** Budget config for the composed system prompt. */\nexport interface ComposeProfileBudget {\n /** Byte cap on the composed `prompt.systemPrompt`.\n * Default {@link DEFAULT_MAX_SYSTEM_PROMPT_BYTES}. */\n maxSystemPromptBytes?: number\n /** Downgrade the over-budget throw to a `console.warn` — the escape hatch\n * for a product with a known-big prompt that must still ship (it yells on\n * every compose instead of blocking). */\n warnOnly?: boolean\n /** Required to raise {@link maxSystemPromptBytes} above\n * {@link DEFAULT_MAX_SYSTEM_PROMPT_BYTES} or to set {@link warnOnly}: a\n * written reason naming what stays inline and why it cannot be mounted.\n * Weakening the cap is a product decision that outlives the person making\n * it, and the usual cause is reference material concatenated into the prompt\n * that belongs in `resources.files`; demanding the sentence here keeps that\n * from happening by accident. */\n overBudgetReason?: string\n}\n\n/** Reject a budget that weakens the cap without stating why. Runs before the\n * size check so it fires on every compose, not only once a prompt has already\n * grown past the raised ceiling. */\nfunction assertBudgetPolicy(budget: ComposeProfileBudget): void {\n const raisedCap =\n budget.maxSystemPromptBytes !== undefined &&\n budget.maxSystemPromptBytes > DEFAULT_MAX_SYSTEM_PROMPT_BYTES\n if (!raisedCap && !budget.warnOnly) return\n if ((budget.overBudgetReason ?? '').trim() !== '') return\n const weakened = raisedCap\n ? `maxSystemPromptBytes ${budget.maxSystemPromptBytes} exceeds the ${DEFAULT_MAX_SYSTEM_PROMPT_BYTES}-byte default`\n : 'warnOnly downgrades the over-budget throw to a warning'\n throw new Error(\n `${weakened} without an overBudgetReason. Oversized system prompts degrade toward empty answers, so the cap is not a formality. ` +\n 'Before raising it: rank the prompt with largestPromptSections() — reference material (playbooks, checklists, corpora) belongs in resources.files ' +\n \"via corpusSkills()/userSkillMounts() or composeSkills({ mode: 'mounted' }), which puts the bodies on disk in the sandbox and leaves a short index in the prompt. \" +\n 'Only content the agent must obey without a tool call should stay inline. If the prompt is genuinely irreducible, set overBudgetReason to the sentence that says so.',\n )\n}\n\n/** Largest markdown-heading-delimited sections of a prompt, by UTF-8 bytes.\n * Cheap heuristic: split on `#`-heading lines; the preamble before the first\n * heading reports as \"(preamble)\". */\nexport function largestPromptSections(\n prompt: string,\n top = 3,\n): Array<{ title: string; bytes: number }> {\n const encoder = new TextEncoder()\n const sections: Array<{ title: string; bytes: number }> = []\n let title = '(preamble)'\n let start = 0\n const flush = (end: number) => {\n const body = prompt.slice(start, end)\n if (body.trim()) sections.push({ title, bytes: encoder.encode(body).byteLength })\n }\n const headingRe = /^#{1,6}\\s+(.+)$/gm\n for (const match of prompt.matchAll(headingRe)) {\n flush(match.index)\n title = (match[1] ?? '').trim() || '(untitled section)'\n start = match.index\n }\n flush(prompt.length)\n return sections.sort((a, b) => b.bytes - a.bytes).slice(0, top)\n}\n\n/** Enforce {@link ComposeProfileBudget} on a composed system prompt: over\n * budget throws (or warns with `warnOnly`) with the actual size and the\n * top-3 largest sections. Exported so a product assembling its prompt\n * outside {@link composeAgentProfile} (e.g. via the `/prompt` assembler) can\n * run the same gate at its own final-composition point. */\nexport function assertSystemPromptWithinBudget(\n systemPrompt: string,\n budget: ComposeProfileBudget = {},\n): void {\n assertBudgetPolicy(budget)\n const max = budget.maxSystemPromptBytes ?? DEFAULT_MAX_SYSTEM_PROMPT_BYTES\n const bytes = new TextEncoder().encode(systemPrompt).byteLength\n if (bytes <= max) return\n const sections = largestPromptSections(systemPrompt)\n .map((s) => `\"${s.title}\" (${s.bytes}B)`)\n .join(', ')\n const message =\n `composed systemPrompt is ${bytes} bytes — over the ${max}-byte budget ` +\n `(oversized prompts degrade to empty answers). ` +\n (sections ? `Largest sections: ${sections}. ` : '') +\n `Move reference material to resources.files (corpusSkills/userSkillMounts, or composeSkills({ mode: 'mounted' })) so the bodies land on disk in the sandbox ` +\n `and the prompt keeps only an index; keep inline only what the agent must obey without a tool call. Raising maxSystemPromptBytes requires an overBudgetReason.`\n if (budget.warnOnly) {\n console.warn(`[profile] ${message}`)\n return\n }\n throw new Error(message)\n}\n\n/** Project per-user skills onto SDK file mounts at the harness skill-discovery\n * path. No tier gate — a user skill is mounted because the user added it.\n * Sorted by path for determinism (matches {@link registrySkills}). */\nexport function userSkillMounts(userSkills: UserSkill[]): AgentProfileFileMount[] {\n return userSkills\n .map(\n (s) =>\n ({\n path: skillMountPath(s.id),\n resource: { kind: 'inline', name: s.id, content: s.skillMd },\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/**\n * Compose a deployable `AgentProfile` from a canonical base plus the four\n * file-mount channels and the overlay overrides.\n *\n * Files: base `resources.files` come first; the four channels follow in\n * `skills -> knowledge -> evolvable -> registry -> userSkills` order (so a\n * userSkill that mounts at the same path as a registry skill is the last write\n * and wins). MCP: base servers first, the overlay `mcp` last (last-wins per\n * key). Prompt: the overlay `systemPrompt`, when set, replaces the base one;\n * base instructions are preserved. Name: the overlay `name`, when set, wins.\n *\n * The merge delegates to the SDK `mergeAgentProfiles` (overlay-wins on records,\n * arrays concatenated) — the deterministic algebra is the overlay we hand it,\n * not a hand-rolled spread. `mergeAgentProfiles(base, overlay)` returns\n * `undefined` only when BOTH are `undefined`; `base` is always defined here, so\n * the result is non-`undefined` by construction and we assert that to the caller.\n *\n * The composed `prompt.systemPrompt` is byte-budgeted here — the single point\n * where the FINAL prompt exists ({@link assertSystemPromptWithinBudget};\n * default {@link DEFAULT_MAX_SYSTEM_PROMPT_BYTES}, `warnOnly` escape hatch).\n */\nexport function composeAgentProfile(\n base: AgentProfile,\n channels: ProfileChannels = {},\n overlay: ProfileOverlay = {},\n budget: ComposeProfileBudget = {},\n): AgentProfile {\n const shellInput: ComposeShellResourcesInput = {\n skills: channels.skills,\n knowledge: channels.knowledge,\n evolvable: channels.evolvable,\n registry: channels.registry\n ? registrySkills(channels.registry, channels.registryTier ?? 'free')\n : undefined,\n predicate: channels.filesPredicate,\n }\n const channelFiles = composeShellResources(shellInput)\n const userFiles = channels.userSkills ? userSkillMounts(channels.userSkills) : []\n const overlayFiles = channels.filesPredicate\n ? userFiles.filter(channels.filesPredicate)\n : userFiles\n const files = [...channelFiles, ...overlayFiles]\n\n const promptOverlay: { systemPrompt?: string; instructions?: string[] } = {}\n if (overlay.systemPrompt) promptOverlay.systemPrompt = overlay.systemPrompt\n if (overlay.instructions && overlay.instructions.length > 0) promptOverlay.instructions = overlay.instructions\n\n const overlayProfile: AgentProfile = {\n ...(overlay.name ? { name: overlay.name } : {}),\n ...(Object.keys(promptOverlay).length > 0 ? { prompt: promptOverlay } : {}),\n ...(overlay.mcp ? { mcp: overlay.mcp } : {}),\n resources: {\n files,\n ...(channels.skillRefs && channels.skillRefs.length > 0 ? { skills: channels.skillRefs } : {}),\n },\n }\n\n const merged = mergeAgentProfiles(base, overlayProfile)\n if (!merged)\n throw new Error('composeAgentProfile: mergeAgentProfiles returned undefined for a defined base')\n // Byte-budget gate on the FINAL composed systemPrompt — this is the single\n // point where every channel and overlay has been merged in.\n const systemPrompt = merged.prompt?.systemPrompt\n if (typeof systemPrompt === 'string') assertSystemPromptWithinBudget(systemPrompt, budget)\n return pruneEmptyResourceChannels(merged)\n}\n\n/** Drop empty resource channels the SDK merge normalizes in (`tools`/`skills`/\n * `agents`/`commands`: `[]`), so the composed profile's wire payload carries\n * only the channels that actually have content — one canonical shape every app\n * emits, instead of a sidecar payload full of empty arrays. */\nfunction pruneEmptyResourceChannels(profile: AgentProfile): AgentProfile {\n if (!profile.resources) return profile\n const kept = Object.fromEntries(\n Object.entries(profile.resources).filter(([, value]) => !(Array.isArray(value) && value.length === 0)),\n ) as AgentProfile['resources']\n const out: AgentProfile = { ...profile, resources: kept }\n if (kept && Object.keys(kept).length === 0) delete out.resources\n return out\n}\n\n/** True body of an addendum file with HTML comments stripped — an all-comment\n * placeholder counts as empty, so the loader falls back to the baseline. */\nexport function stripComments(raw: string): string {\n return raw.replace(/<!--[\\s\\S]*?-->/g, '').trim()\n}\n\n/** Inputs to {@link makeEvolvableSection}. */\nexport interface EvolvableSectionInput {\n /** Section id the self-improvement loop targets with `applyDomainPatch`. */\n id: string\n /** Section title rendered as `### <title>`. */\n title: string\n /**\n * Load the deployed section body. The CONSUMER supplies this closure and runs\n * its own `import.meta.glob('<lit>', { eager: true, query: '?raw', import:\n * 'default' })` inside it — the literal must stay at the call site so Vite can\n * static-analyze it; a glob constructed here would not resolve the product's\n * files. Return the raw markdown (comments and all); `makeEvolvableSection`\n * applies {@link stripComments} to decide whether it is really populated.\n */\n load: () => string\n /**\n * The in-tree fallback body, used when `load()` returns an\n * all-comments/empty placeholder. REQUIRED — no internal default — so a\n * product can never accidentally render an empty evolvable section.\n */\n baseline: string\n}\n\n/**\n * Build the one evolvable (`evolvable: true`) domain section whose body comes\n * from the product's loader, falling back to the required baseline when the\n * loaded body is empty after stripping comments. Returns the agent-eval\n * `AgentProfileSection` shape — drop it straight into `prodProfile`'s shipped\n * sections. The loader is the only seam; the empty-vs-populated rule and the\n * baseline fallback are the lifted algebra.\n */\nexport function makeEvolvableSection(input: EvolvableSectionInput): profile.AgentProfileSection {\n const loaded = input.load()\n const body = stripComments(loaded) ? loaded.trim() : input.baseline\n return { id: input.id, title: input.title, body, evolvable: true }\n}\n\nexport {\n assertSkillDeliveryDisjoint,\n composeShellResources,\n composeSkills,\n corpusSkills,\n loadMarkdownCorpus,\n mergeComposedSkills,\n parseCorpusSkills,\n parseSkillFrontmatter,\n registrySkills,\n renderInlineSkills,\n renderSkillIndex,\n skillEntryFromMarkdown,\n skillMountPath,\n skillRefs,\n} from '../skills/index'\nexport type {\n ComposedSkills,\n ComposeShellResourcesInput,\n CorpusEntry,\n CorpusLoadResult,\n GlobModules,\n LoadCorpusOptions,\n ParsedSkill,\n SkillDeliveryMode,\n SkillEntry,\n SkillFrontmatter,\n} from '../skills/index'\n"],"mappings":"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","names":["profile"]}
|
package/dist/skills/index.d.ts
CHANGED
|
@@ -289,7 +289,8 @@ interface ComposeSkillsInput {
|
|
|
289
289
|
* directory); a null/absent one throws rather than silently falling back, so
|
|
290
290
|
* the caller resolves the fallback deliberately (see
|
|
291
291
|
* `@tangle-network/agent-app/skills-placement`'s `composeSkillsForHarness`,
|
|
292
|
-
*
|
|
292
|
+
* whose own default is to throw rather than choose `inline` for you — inline
|
|
293
|
+
* is a narrow, explicitly opted-into fallback, not a co-equal mode).
|
|
293
294
|
*/
|
|
294
295
|
declare function composeSkills(input: ComposeSkillsInput): ComposedSkills;
|
|
295
296
|
/** Throw when the same skill id is delivered both `inline` and `mounted` —
|
package/dist/skills/index.js
CHANGED
|
@@ -39,12 +39,42 @@ interface ComposeSkillsForHarnessInput {
|
|
|
39
39
|
harness: Harness;
|
|
40
40
|
tier?: string;
|
|
41
41
|
heading?: string;
|
|
42
|
+
/**
|
|
43
|
+
* What to do when `harness` has no native skill-mount directory (see
|
|
44
|
+
* {@link resolveSkillDir}). Default `'throw'`: refuse rather than silently
|
|
45
|
+
* inlining every skill BODY into the system prompt. Pass `'inline'` only as
|
|
46
|
+
* a deliberate, auditable opt-in — the caller has decided every skill must
|
|
47
|
+
* reach this harness even though it will be concatenated into the prompt.
|
|
48
|
+
*/
|
|
49
|
+
onNoSkillDir?: 'throw' | 'inline';
|
|
42
50
|
}
|
|
43
51
|
/**
|
|
44
|
-
* Compose {@link ComposedSkills} for `harness
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
52
|
+
* Compose {@link ComposedSkills} for `harness`.
|
|
53
|
+
*
|
|
54
|
+
* `mounted` delivery is the standard path: whenever the platform names a cwd
|
|
55
|
+
* skill dir for `harness`, skills ride the typed `resources.skills` channel
|
|
56
|
+
* and the platform materializer writes each one to that directory as a real
|
|
57
|
+
* file — the agent reads it on demand with its own file tools.
|
|
58
|
+
*
|
|
59
|
+
* `inline` delivery — every skill BODY rendered straight into the system
|
|
60
|
+
* prompt — is a NARROW fallback for harnesses with no native skill dir, never
|
|
61
|
+
* a co-equal mode. An `AgentProfile` is a RECIPE that materializes into a
|
|
62
|
+
* sandbox, not a prompt template: skill bodies belong in a file mount, and
|
|
63
|
+
* `prompt.systemPrompt` should carry only what the agent must obey without a
|
|
64
|
+
* tool call (identity, tool-call contract, safety rules, output format) plus
|
|
65
|
+
* a short index of what's mounted. Concatenating skill bodies into the prompt
|
|
66
|
+
* degrades the model toward empty answers once it grows large enough — one
|
|
67
|
+
* product reproduced a 151,882-byte prompt exactly this way, from an
|
|
68
|
+
* unnoticed inline fallback.
|
|
69
|
+
*
|
|
70
|
+
* Because of that, `onNoSkillDir` defaults to `'throw'`: a harness with no
|
|
71
|
+
* skill dir refuses instead of silently inlining. The thrown error names the
|
|
72
|
+
* harness, the skill count, the byte size that would have been inlined, and
|
|
73
|
+
* the harnesses that DO support native mounting. Pass `onNoSkillDir:
|
|
74
|
+
* 'inline'` only as a deliberate, auditable opt-in when every skill genuinely
|
|
75
|
+
* must reach this harness regardless of prompt cost.
|
|
76
|
+
*
|
|
77
|
+
* The one function a product calls instead of hand-checking `resolveSkillDir`
|
|
48
78
|
* and branching between {@link composeSkills}'s two modes itself.
|
|
49
79
|
*/
|
|
50
80
|
declare function composeSkillsForHarness(input: ComposeSkillsForHarnessInput): ComposedSkills;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
composeSkills
|
|
3
|
-
|
|
2
|
+
composeSkills,
|
|
3
|
+
renderInlineSkills
|
|
4
|
+
} from "../chunk-34M7AUWO.js";
|
|
5
|
+
import {
|
|
6
|
+
KNOWN_HARNESSES
|
|
7
|
+
} from "../chunk-CQZSAR77.js";
|
|
4
8
|
|
|
5
9
|
// src/skills-placement/index.ts
|
|
6
10
|
import { skillDirForHarness } from "@tangle-network/agent-profile-materialize";
|
|
@@ -30,11 +34,21 @@ function unsupportedSkillHarnesses(harnesses) {
|
|
|
30
34
|
}
|
|
31
35
|
return out;
|
|
32
36
|
}
|
|
37
|
+
function nativeSkillMountHarnesses() {
|
|
38
|
+
return KNOWN_HARNESSES.filter((harness) => resolveSkillDir(harness) !== null);
|
|
39
|
+
}
|
|
33
40
|
function composeSkillsForHarness(input) {
|
|
34
|
-
const { skills, harness, tier, heading } = input;
|
|
41
|
+
const { skills, harness, tier, heading, onNoSkillDir = "throw" } = input;
|
|
35
42
|
const skillDir = resolveSkillDir(harness);
|
|
36
43
|
if (skillDir) return composeSkills({ skills, mode: "mounted", skillDir, tier, heading });
|
|
37
|
-
return composeSkills({ skills, mode: "inline", tier, heading });
|
|
44
|
+
if (onNoSkillDir === "inline") return composeSkills({ skills, mode: "inline", tier, heading });
|
|
45
|
+
const relevant = tier ? skills.filter((s) => s.tier === tier) : skills;
|
|
46
|
+
const promptSection = renderInlineSkills({ skills, tier, heading });
|
|
47
|
+
const bytes = new TextEncoder().encode(promptSection).byteLength;
|
|
48
|
+
const mountable = nativeSkillMountHarnesses();
|
|
49
|
+
throw new Error(
|
|
50
|
+
`composeSkillsForHarness: "${harness}" has no native skill-mount directory. Inlining ${relevant.length} skill(s) (${bytes} bytes) into its system prompt would silently convert mounted skills into prompt text \u2014 a 151,882-byte prompt shipped this way once, and oversized prompts degrade toward empty answers. Mount the skills instead: pick a harness with native skill support (${mountable.join(", ")}), or pass onNoSkillDir: 'inline' to opt in explicitly and accept the inlined bytes.`
|
|
51
|
+
);
|
|
38
52
|
}
|
|
39
53
|
export {
|
|
40
54
|
composeSkillsForHarness,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/skills-placement/index.ts"],"sourcesContent":["/**\n * Harness-native skill directory resolution — the one place agent-app binds\n * to the platform's authoritative per-harness skill-dir map.\n *\n * `../skills` renders skill CONTENT (parse, tier-filter, `inline`/`mounted`\n * delivery) but deliberately stops short of naming WHICH cwd path a `mounted`\n * skill lands at on a given harness — that mapping is owned by the platform\n * materializer (`@tangle-network/agent-profile-materialize`'s\n * `skillDirForHarness`), not by app-shell. This subpath exists so no product\n * — and no other agent-app module — ever writes a skill path literal\n * (`~/.claude/skills/...`, `.opencode/skills`, ...) of its own; it bridges\n * agent-app's `Harness` taxonomy onto the platform's `HarnessId` and asks the\n * platform for the answer.\n *\n * Requires the OPTIONAL peer `@tangle-network/agent-profile-materialize`.\n * Products that don't install it simply don't import this subpath — every\n * other agent-app skills surface (`../skills`, `ProfileChannels.skillRefs`)\n * works without it, falling back to `inline` delivery.\n */\n\nimport type
|
|
1
|
+
{"version":3,"sources":["../../src/skills-placement/index.ts"],"sourcesContent":["/**\n * Harness-native skill directory resolution — the one place agent-app binds\n * to the platform's authoritative per-harness skill-dir map.\n *\n * `../skills` renders skill CONTENT (parse, tier-filter, `inline`/`mounted`\n * delivery) but deliberately stops short of naming WHICH cwd path a `mounted`\n * skill lands at on a given harness — that mapping is owned by the platform\n * materializer (`@tangle-network/agent-profile-materialize`'s\n * `skillDirForHarness`), not by app-shell. This subpath exists so no product\n * — and no other agent-app module — ever writes a skill path literal\n * (`~/.claude/skills/...`, `.opencode/skills`, ...) of its own; it bridges\n * agent-app's `Harness` taxonomy onto the platform's `HarnessId` and asks the\n * platform for the answer.\n *\n * Requires the OPTIONAL peer `@tangle-network/agent-profile-materialize`.\n * Products that don't install it simply don't import this subpath — every\n * other agent-app skills surface (`../skills`, `ProfileChannels.skillRefs`)\n * works without it, falling back to `inline` delivery.\n */\n\nimport { KNOWN_HARNESSES, type Harness } from '../harness/index'\nimport { skillDirForHarness, type HarnessId } from '@tangle-network/agent-profile-materialize'\nimport { composeSkills, renderInlineSkills, type ComposedSkills, type SkillEntry } from '../skills/index'\n\n/** agent-app `Harness` -> platform `HarnessId`, identity-mapped for exactly\n * the harnesses the platform map covers. Harnesses absent here (`amp`,\n * `factory-droids`, `forge`, `acp`, `cursor`, `cli-base`) resolve to `null` —\n * {@link composeSkillsForHarness} refuses by default rather than silently\n * delivering `inline`; a caller that genuinely needs `inline` on one of\n * these opts in explicitly (`onNoSkillDir: 'inline'`). `cursor`'s adapter\n * supports `resources.skills` bespokely but isn't in the platform map yet;\n * treating it as unbridged is the safe posture until the map covers it,\n * rather than guessing its cwd skill dir here. */\nconst HARNESS_BRIDGE: Partial<Record<Harness, HarnessId>> = {\n opencode: 'opencode',\n 'claude-code': 'claude-code',\n nanoclaw: 'nanoclaw',\n 'kimi-code': 'kimi-code',\n codex: 'codex',\n pi: 'pi',\n hermes: 'hermes',\n openclaw: 'openclaw',\n}\n\n/** Resolve the cwd-relative skill dir `resources.skills` refs materialize\n * into on `harness` — via the platform's `skillDirForHarness`. `null` when\n * `harness` isn't bridged (see {@link HARNESS_BRIDGE}) or when the platform\n * itself has no cwd skill primitive for it (e.g. `hermes`, user-dir-only). */\nexport function resolveSkillDir(harness: Harness): string | null {\n const bridged = HARNESS_BRIDGE[harness]\n if (!bridged) return null\n return skillDirForHarness(bridged)\n}\n\n/** Filter `harnesses` down to those with no mounted skill dir (deduped,\n * first-seen order preserved) — the set that must fall back to `inline`\n * delivery, or that a caller should warn about before offering \"mounted\"\n * install UX. */\nexport function unsupportedSkillHarnesses(harnesses: Iterable<Harness>): Harness[] {\n const seen = new Set<Harness>()\n const out: Harness[] = []\n for (const harness of harnesses) {\n if (resolveSkillDir(harness) !== null) continue\n if (seen.has(harness)) continue\n seen.add(harness)\n out.push(harness)\n }\n return out\n}\n\n/** Inputs to {@link composeSkillsForHarness}. */\nexport interface ComposeSkillsForHarnessInput {\n skills: SkillEntry[]\n harness: Harness\n tier?: string\n heading?: string\n /**\n * What to do when `harness` has no native skill-mount directory (see\n * {@link resolveSkillDir}). Default `'throw'`: refuse rather than silently\n * inlining every skill BODY into the system prompt. Pass `'inline'` only as\n * a deliberate, auditable opt-in — the caller has decided every skill must\n * reach this harness even though it will be concatenated into the prompt.\n */\n onNoSkillDir?: 'throw' | 'inline'\n}\n\n/** `KNOWN_HARNESSES` filtered to those the platform materializer can mount\n * skill files onto natively (a non-null {@link resolveSkillDir}). Computed\n * fresh rather than cached — the platform map can grow without a release\n * here. Used only to build the actionable error in\n * {@link composeSkillsForHarness}; not exported because it duplicates\n * {@link unsupportedSkillHarnesses}'s complement and callers should reach\n * for that instead when they need the harness list at runtime. */\nfunction nativeSkillMountHarnesses(): Harness[] {\n return KNOWN_HARNESSES.filter((harness) => resolveSkillDir(harness) !== null)\n}\n\n/**\n * Compose {@link ComposedSkills} for `harness`.\n *\n * `mounted` delivery is the standard path: whenever the platform names a cwd\n * skill dir for `harness`, skills ride the typed `resources.skills` channel\n * and the platform materializer writes each one to that directory as a real\n * file — the agent reads it on demand with its own file tools.\n *\n * `inline` delivery — every skill BODY rendered straight into the system\n * prompt — is a NARROW fallback for harnesses with no native skill dir, never\n * a co-equal mode. An `AgentProfile` is a RECIPE that materializes into a\n * sandbox, not a prompt template: skill bodies belong in a file mount, and\n * `prompt.systemPrompt` should carry only what the agent must obey without a\n * tool call (identity, tool-call contract, safety rules, output format) plus\n * a short index of what's mounted. Concatenating skill bodies into the prompt\n * degrades the model toward empty answers once it grows large enough — one\n * product reproduced a 151,882-byte prompt exactly this way, from an\n * unnoticed inline fallback.\n *\n * Because of that, `onNoSkillDir` defaults to `'throw'`: a harness with no\n * skill dir refuses instead of silently inlining. The thrown error names the\n * harness, the skill count, the byte size that would have been inlined, and\n * the harnesses that DO support native mounting. Pass `onNoSkillDir:\n * 'inline'` only as a deliberate, auditable opt-in when every skill genuinely\n * must reach this harness regardless of prompt cost.\n *\n * The one function a product calls instead of hand-checking `resolveSkillDir`\n * and branching between {@link composeSkills}'s two modes itself.\n */\nexport function composeSkillsForHarness(input: ComposeSkillsForHarnessInput): ComposedSkills {\n const { skills, harness, tier, heading, onNoSkillDir = 'throw' } = input\n const skillDir = resolveSkillDir(harness)\n if (skillDir) return composeSkills({ skills, mode: 'mounted', skillDir, tier, heading })\n if (onNoSkillDir === 'inline') return composeSkills({ skills, mode: 'inline', tier, heading })\n\n const relevant = tier ? skills.filter((s) => s.tier === tier) : skills\n const promptSection = renderInlineSkills({ skills, tier, heading })\n const bytes = new TextEncoder().encode(promptSection).byteLength\n const mountable = nativeSkillMountHarnesses()\n throw new Error(\n `composeSkillsForHarness: \"${harness}\" has no native skill-mount directory. ` +\n `Inlining ${relevant.length} skill(s) (${bytes} bytes) into its system prompt would silently ` +\n `convert mounted skills into prompt text — a 151,882-byte prompt shipped this way once, and ` +\n `oversized prompts degrade toward empty answers. Mount the skills instead: pick a harness with ` +\n `native skill support (${mountable.join(', ')}), or pass onNoSkillDir: 'inline' to opt in ` +\n `explicitly and accept the inlined bytes.`,\n )\n}\n\nexport type { ComposedSkills, SkillEntry } from '../skills/index'\n"],"mappings":";;;;;;;;;AAqBA,SAAS,0BAA0C;AAYnD,IAAM,iBAAsD;AAAA,EAC1D,UAAU;AAAA,EACV,eAAe;AAAA,EACf,UAAU;AAAA,EACV,aAAa;AAAA,EACb,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,UAAU;AACZ;AAMO,SAAS,gBAAgB,SAAiC;AAC/D,QAAM,UAAU,eAAe,OAAO;AACtC,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,mBAAmB,OAAO;AACnC;AAMO,SAAS,0BAA0B,WAAyC;AACjF,QAAM,OAAO,oBAAI,IAAa;AAC9B,QAAM,MAAiB,CAAC;AACxB,aAAW,WAAW,WAAW;AAC/B,QAAI,gBAAgB,OAAO,MAAM,KAAM;AACvC,QAAI,KAAK,IAAI,OAAO,EAAG;AACvB,SAAK,IAAI,OAAO;AAChB,QAAI,KAAK,OAAO;AAAA,EAClB;AACA,SAAO;AACT;AAyBA,SAAS,4BAAuC;AAC9C,SAAO,gBAAgB,OAAO,CAAC,YAAY,gBAAgB,OAAO,MAAM,IAAI;AAC9E;AA+BO,SAAS,wBAAwB,OAAqD;AAC3F,QAAM,EAAE,QAAQ,SAAS,MAAM,SAAS,eAAe,QAAQ,IAAI;AACnE,QAAM,WAAW,gBAAgB,OAAO;AACxC,MAAI,SAAU,QAAO,cAAc,EAAE,QAAQ,MAAM,WAAW,UAAU,MAAM,QAAQ,CAAC;AACvF,MAAI,iBAAiB,SAAU,QAAO,cAAc,EAAE,QAAQ,MAAM,UAAU,MAAM,QAAQ,CAAC;AAE7F,QAAM,WAAW,OAAO,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,IAAI,IAAI;AAChE,QAAM,gBAAgB,mBAAmB,EAAE,QAAQ,MAAM,QAAQ,CAAC;AAClE,QAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,aAAa,EAAE;AACtD,QAAM,YAAY,0BAA0B;AAC5C,QAAM,IAAI;AAAA,IACR,6BAA6B,OAAO,mDACtB,SAAS,MAAM,cAAc,KAAK,qQAGrB,UAAU,KAAK,IAAI,CAAC;AAAA,EAEjD;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-app",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.60",
|
|
4
4
|
"packageManager": "pnpm@10.33.4",
|
|
5
5
|
"description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.",
|
|
6
6
|
"keywords": [
|