@juno-ai/bind 10.0.0 → 12.0.0

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.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * SHA-256 over a UTF-8 string, in plain TypeScript.
3
+ *
4
+ * **Why the package carries an implementation instead of taking a port**, when
5
+ * `run/receipts.ts` deliberately does the opposite. Two differences decide it.
6
+ *
7
+ * The first is *when* the digest is needed. A receipt's hash is taken inside an
8
+ * already-async tool dispatch, so `crypto.subtle.digest` — which is async
9
+ * everywhere, and is the only digest workerd has — fits without changing any
10
+ * signature. A skill's `contentSha` is computed at **registration**, which is
11
+ * the synchronous act of a module declaring what it contributes. Taking an
12
+ * async digest there forces either an async `register()` (so a plugin's
13
+ * registration becomes something a host must await and order) or a lazily
14
+ * resolved hash on every read path that wants one. Both cost more than this
15
+ * file.
16
+ *
17
+ * The second is *what the value means*. A receipt hash is persisted by the host
18
+ * forever and compared against rows written by earlier versions, so the host
19
+ * must own the algorithm choice — and its input is tool arguments, routinely
20
+ * user data, which is why that doc argues preimage resistance. A `contentSha`
21
+ * identifies build content within one deployment: it is derived from source the
22
+ * deployment already ships, and nothing reads it that did not just compute it.
23
+ *
24
+ * A host with a native digest can still supply one — {@link Sha256Hex} is a
25
+ * parameter everywhere this is used — but it must be synchronous and it must be
26
+ * SHA-256, or the hashes two hosts compute for the same skill diverge.
27
+ *
28
+ * The implementation is the FIPS 180-4 reference algorithm with no shortcuts.
29
+ * `__tests__/sha256.test.ts` pins it against the published vectors, plus a
30
+ * multi-block and a multi-byte-UTF-8 case, because a hash that is subtly wrong
31
+ * fails silently: every value still looks like a hash.
32
+ */
33
+ /** A synchronous SHA-256 that returns lowercase hex. */
34
+ export type Sha256Hex = (text: string) => string;
35
+ /** SHA-256 (lowercase hex) of a UTF-8 string. */
36
+ export declare const sha256Hex: Sha256Hex;
37
+ /** Byte length of a string in UTF-8 — what a resource listing reports. */
38
+ export declare function utf8ByteLength(text: string): number;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * SHA-256 over a UTF-8 string, in plain TypeScript.
3
+ *
4
+ * **Why the package carries an implementation instead of taking a port**, when
5
+ * `run/receipts.ts` deliberately does the opposite. Two differences decide it.
6
+ *
7
+ * The first is *when* the digest is needed. A receipt's hash is taken inside an
8
+ * already-async tool dispatch, so `crypto.subtle.digest` — which is async
9
+ * everywhere, and is the only digest workerd has — fits without changing any
10
+ * signature. A skill's `contentSha` is computed at **registration**, which is
11
+ * the synchronous act of a module declaring what it contributes. Taking an
12
+ * async digest there forces either an async `register()` (so a plugin's
13
+ * registration becomes something a host must await and order) or a lazily
14
+ * resolved hash on every read path that wants one. Both cost more than this
15
+ * file.
16
+ *
17
+ * The second is *what the value means*. A receipt hash is persisted by the host
18
+ * forever and compared against rows written by earlier versions, so the host
19
+ * must own the algorithm choice — and its input is tool arguments, routinely
20
+ * user data, which is why that doc argues preimage resistance. A `contentSha`
21
+ * identifies build content within one deployment: it is derived from source the
22
+ * deployment already ships, and nothing reads it that did not just compute it.
23
+ *
24
+ * A host with a native digest can still supply one — {@link Sha256Hex} is a
25
+ * parameter everywhere this is used — but it must be synchronous and it must be
26
+ * SHA-256, or the hashes two hosts compute for the same skill diverge.
27
+ *
28
+ * The implementation is the FIPS 180-4 reference algorithm with no shortcuts.
29
+ * `__tests__/sha256.test.ts` pins it against the published vectors, plus a
30
+ * multi-block and a multi-byte-UTF-8 case, because a hash that is subtly wrong
31
+ * fails silently: every value still looks like a hash.
32
+ */
33
+ /** Round constants: the first 32 bits of the fractional parts of the cube roots of the first 64 primes. */
34
+ // prettier-ignore
35
+ const K = new Uint32Array([
36
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
37
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
38
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
39
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
40
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
41
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
42
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
43
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
44
+ ]);
45
+ function rotr(x, n) {
46
+ return (x >>> n) | (x << (32 - n));
47
+ }
48
+ const encoder = new TextEncoder();
49
+ /** SHA-256 (lowercase hex) of a UTF-8 string. */
50
+ export const sha256Hex = (text) => {
51
+ const data = encoder.encode(text);
52
+ // Pad to a whole number of 512-bit blocks: the 0x80 terminator, zeroes, then
53
+ // the message length in bits as a 64-bit big-endian integer.
54
+ const total = (((data.length + 9 + 63) / 64) | 0) * 64;
55
+ const buf = new Uint8Array(total);
56
+ buf.set(data);
57
+ buf[data.length] = 0x80;
58
+ const view = new DataView(buf.buffer);
59
+ // The bit length can exceed 32 bits for inputs over 512 MiB. Split it rather
60
+ // than relying on a single `setUint32`, which would silently truncate.
61
+ const bitLength = data.length * 8;
62
+ view.setUint32(total - 8, Math.floor(bitLength / 0x1_0000_0000));
63
+ view.setUint32(total - 4, bitLength >>> 0);
64
+ // Initial hash values: the first 32 bits of the fractional parts of the
65
+ // square roots of the first eight primes.
66
+ const h = new Uint32Array([
67
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
68
+ 0x1f83d9ab, 0x5be0cd19,
69
+ ]);
70
+ const w = new Uint32Array(64);
71
+ for (let offset = 0; offset < total; offset += 64) {
72
+ for (let i = 0; i < 16; i += 1)
73
+ w[i] = view.getUint32(offset + i * 4);
74
+ for (let i = 16; i < 64; i += 1) {
75
+ const x = w[i - 15];
76
+ const y = w[i - 2];
77
+ const s0 = rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3);
78
+ const s1 = rotr(y, 17) ^ rotr(y, 19) ^ (y >>> 10);
79
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1;
80
+ }
81
+ let a = h[0];
82
+ let b = h[1];
83
+ let c = h[2];
84
+ let d = h[3];
85
+ let e = h[4];
86
+ let f = h[5];
87
+ let g = h[6];
88
+ let hh = h[7];
89
+ for (let i = 0; i < 64; i += 1) {
90
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
91
+ const ch = (e & f) ^ (~e & g);
92
+ const t1 = (hh + s1 + ch + K[i] + w[i]) | 0;
93
+ const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
94
+ const maj = (a & b) ^ (a & c) ^ (b & c);
95
+ const t2 = (s0 + maj) | 0;
96
+ hh = g;
97
+ g = f;
98
+ f = e;
99
+ e = (d + t1) | 0;
100
+ d = c;
101
+ c = b;
102
+ b = a;
103
+ a = (t1 + t2) | 0;
104
+ }
105
+ h[0] += a;
106
+ h[1] += b;
107
+ h[2] += c;
108
+ h[3] += d;
109
+ h[4] += e;
110
+ h[5] += f;
111
+ h[6] += g;
112
+ h[7] += hh;
113
+ }
114
+ let hex = "";
115
+ for (let i = 0; i < 8; i += 1)
116
+ hex += h[i].toString(16).padStart(8, "0");
117
+ return hex;
118
+ };
119
+ /** Byte length of a string in UTF-8 — what a resource listing reports. */
120
+ export function utf8ByteLength(text) {
121
+ return encoder.encode(text).length;
122
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `SKILL.md` — the interchange format.
3
+ *
4
+ * A deployment's canonical storage is its own (a database row, a directory of
5
+ * files), but the portable form is the de-facto `SKILL.md` bundle: a `---`
6
+ * fenced YAML frontmatter block followed by a markdown body, as used by
7
+ * Anthropic's skills and agentskills.io. It is how a skill authored in one
8
+ * place is read in another, which makes both directions of this file a
9
+ * compatibility surface rather than an internal convenience.
10
+ *
11
+ * **Import is deliberately lenient and export is deliberately strict.** The
12
+ * input is a document a person wrote, often in another tool, and the two
13
+ * outcomes are not symmetric: refusing a skill over a cosmetic frontmatter
14
+ * mistake loses knowledge the author already wrote down, while accepting a
15
+ * malformed one costs a warning. So parsing repairs what it can, warns about
16
+ * what it repaired, and fails on exactly two things — frontmatter that is not
17
+ * YAML at all, and a missing `description`, which is the one field with no
18
+ * sensible default because it is the entire Tier-1 catalog line.
19
+ *
20
+ * **YAML arrives as a port.** The package takes peer dependencies only, and a
21
+ * YAML implementation is neither a peer the harness can assume nor something
22
+ * it should bundle. Every host that reads `SKILL.md` already has one — the
23
+ * seam is two functions wide.
24
+ */
25
+ /**
26
+ * The host's YAML implementation. `js-yaml`'s `load`/`dump` and the `yaml`
27
+ * package's `parse`/`stringify` both satisfy it directly.
28
+ *
29
+ * `parse` must **throw** on invalid YAML rather than returning a sentinel: the
30
+ * repair pass below is driven by the throw, so a parser that returns
31
+ * `undefined` instead would skip the repair and reject documents this module
32
+ * is meant to accept.
33
+ */
34
+ export interface SkillYamlCodec {
35
+ parse: (text: string) => unknown;
36
+ /** Must emit block-style YAML and must not wrap long lines. */
37
+ stringify: (value: Record<string, unknown>) => string;
38
+ }
39
+ export interface ParsedSkillMd {
40
+ /** `null` when frontmatter omits it — the caller supplies a fallback (usually the filename). */
41
+ name: string | null;
42
+ description: string;
43
+ whenToUse: string | null;
44
+ body: string;
45
+ /** Unknown frontmatter keys, preserved for round-trip and read-only display. */
46
+ metadata: Record<string, unknown> | null;
47
+ }
48
+ export type SkillMdParseResult = {
49
+ ok: true;
50
+ skill: ParsedSkillMd;
51
+ warnings: string[];
52
+ } | {
53
+ ok: false;
54
+ error: string;
55
+ warnings: string[];
56
+ };
57
+ /** Parse a raw `SKILL.md` (frontmatter + body) into importable fields. */
58
+ export declare function parseSkillMarkdown(raw: string, yaml: SkillYamlCodec): SkillMdParseResult;
59
+ /**
60
+ * Serialize a stored skill back to canonical `SKILL.md` text.
61
+ *
62
+ * Preserved unknown keys are written first and the canonical keys last, so a
63
+ * stray `name` / `description` / `when_to_use` that ended up inside `metadata`
64
+ * — which an earlier lenient import makes possible — cannot clobber the real
65
+ * value on the way out.
66
+ */
67
+ export declare function serializeSkillMarkdown(skill: {
68
+ name: string;
69
+ description: string;
70
+ whenToUse: string | null;
71
+ body: string;
72
+ metadata: Record<string, unknown> | null;
73
+ }, yaml: SkillYamlCodec): string;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * `SKILL.md` — the interchange format.
3
+ *
4
+ * A deployment's canonical storage is its own (a database row, a directory of
5
+ * files), but the portable form is the de-facto `SKILL.md` bundle: a `---`
6
+ * fenced YAML frontmatter block followed by a markdown body, as used by
7
+ * Anthropic's skills and agentskills.io. It is how a skill authored in one
8
+ * place is read in another, which makes both directions of this file a
9
+ * compatibility surface rather than an internal convenience.
10
+ *
11
+ * **Import is deliberately lenient and export is deliberately strict.** The
12
+ * input is a document a person wrote, often in another tool, and the two
13
+ * outcomes are not symmetric: refusing a skill over a cosmetic frontmatter
14
+ * mistake loses knowledge the author already wrote down, while accepting a
15
+ * malformed one costs a warning. So parsing repairs what it can, warns about
16
+ * what it repaired, and fails on exactly two things — frontmatter that is not
17
+ * YAML at all, and a missing `description`, which is the one field with no
18
+ * sensible default because it is the entire Tier-1 catalog line.
19
+ *
20
+ * **YAML arrives as a port.** The package takes peer dependencies only, and a
21
+ * YAML implementation is neither a peer the harness can assume nor something
22
+ * it should bundle. Every host that reads `SKILL.md` already has one — the
23
+ * seam is two functions wide.
24
+ */
25
+ /**
26
+ * Frontmatter keys this module owns. Everything else is preserved verbatim in
27
+ * `metadata` — a host or another tool may put anything here, and dropping it
28
+ * would make an import/export round-trip lossy.
29
+ */
30
+ const KNOWN_KEYS = new Set(["name", "description", "when_to_use", "whenToUse", "when-to-use"]);
31
+ /** Lower-kebab, at most 64 characters — the shape a skill name must canonicalize to. */
32
+ const NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
33
+ const NAME_MAX = 64;
34
+ function splitFrontmatter(raw) {
35
+ const text = raw.replace(/^/, ""); // a BOM before the fence is common from Windows editors
36
+ if (!text.startsWith("---"))
37
+ return null;
38
+ const rest = text.slice(3).replace(/^\r?\n/, "");
39
+ const endIndex = rest.search(/\r?\n---\s*(\r?\n|$)/);
40
+ if (endIndex === -1)
41
+ return null;
42
+ return {
43
+ frontmatter: rest.slice(0, endIndex),
44
+ body: rest.slice(endIndex).replace(/^\r?\n---\s*(\r?\n)?/, ""),
45
+ };
46
+ }
47
+ /**
48
+ * Repair the one frontmatter mistake that dominates real imports: an unquoted
49
+ * value containing a colon (`description: Use when X: do Y`), which YAML reads
50
+ * as a nested mapping and rejects. Quote any `key: value` whose value has an
51
+ * unquoted inner colon.
52
+ *
53
+ * A value opening with `[` or `{` is a flow collection — valid YAML where the
54
+ * colons are structural — and is skipped. Without that exception a single
55
+ * unrelated bad line, which forces this whole-block pass, would collateral-
56
+ * quote a legitimate inline array or object into a plain string.
57
+ */
58
+ function repairUnquotedColons(frontmatter) {
59
+ return frontmatter
60
+ .split(/\r?\n/)
61
+ .map((line) => {
62
+ const match = line.match(/^(\s*[A-Za-z0-9_-]+:\s+)(.*)$/);
63
+ if (!match)
64
+ return line;
65
+ const value = match[2];
66
+ const trimmed = value.trim();
67
+ const structured = /^["'[{]/.test(trimmed) || trimmed.length === 0;
68
+ if (structured || !value.includes(":"))
69
+ return line;
70
+ return `${match[1]}"${value.replace(/"/g, '\\"')}"`;
71
+ })
72
+ .join("\n");
73
+ }
74
+ function loadYaml(frontmatter, yaml) {
75
+ const warnings = [];
76
+ let parsed;
77
+ try {
78
+ parsed = yaml.parse(frontmatter);
79
+ }
80
+ catch {
81
+ try {
82
+ parsed = yaml.parse(repairUnquotedColons(frontmatter));
83
+ warnings.push("Frontmatter had unquoted colon values; auto-quoted them on import.");
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
90
+ return null;
91
+ return { object: parsed, warnings };
92
+ }
93
+ /** Parse a raw `SKILL.md` (frontmatter + body) into importable fields. */
94
+ export function parseSkillMarkdown(raw, yaml) {
95
+ const split = splitFrontmatter(raw);
96
+ if (!split) {
97
+ return {
98
+ ok: false,
99
+ error: "Not a SKILL.md — expected a `---` YAML frontmatter block.",
100
+ warnings: [],
101
+ };
102
+ }
103
+ const loaded = loadYaml(split.frontmatter, yaml);
104
+ if (!loaded) {
105
+ return { ok: false, error: "Frontmatter is not valid YAML.", warnings: [] };
106
+ }
107
+ const { object, warnings } = loaded;
108
+ const description = typeof object.description === "string" ? object.description.trim() : "";
109
+ if (!description) {
110
+ return {
111
+ ok: false,
112
+ error: "SKILL.md is missing a required `description`.",
113
+ warnings,
114
+ };
115
+ }
116
+ const rawName = typeof object.name === "string" ? object.name.trim() : "";
117
+ const name = rawName.length > 0 ? rawName : null;
118
+ if (name && (name.length > NAME_MAX || !NAME_PATTERN.test(name))) {
119
+ warnings.push(`Frontmatter name "${name}" is not lower-kebab-case ≤ ${NAME_MAX} chars; it will be normalized.`);
120
+ }
121
+ // Three spellings, because all three appear in the wild.
122
+ const hintRaw = object.when_to_use ?? object.whenToUse ?? object["when-to-use"] ?? null;
123
+ const whenToUse = typeof hintRaw === "string" && hintRaw.trim() ? hintRaw.trim() : null;
124
+ const extra = Object.entries(object).filter(([key]) => !KNOWN_KEYS.has(key));
125
+ const metadata = extra.length > 0 ? Object.fromEntries(extra) : null;
126
+ return {
127
+ ok: true,
128
+ skill: { name, description, whenToUse, body: split.body.trim(), metadata },
129
+ warnings,
130
+ };
131
+ }
132
+ /**
133
+ * Serialize a stored skill back to canonical `SKILL.md` text.
134
+ *
135
+ * Preserved unknown keys are written first and the canonical keys last, so a
136
+ * stray `name` / `description` / `when_to_use` that ended up inside `metadata`
137
+ * — which an earlier lenient import makes possible — cannot clobber the real
138
+ * value on the way out.
139
+ */
140
+ export function serializeSkillMarkdown(skill, yaml) {
141
+ const front = { ...(skill.metadata ?? {}) };
142
+ front.name = skill.name;
143
+ front.description = skill.description;
144
+ if (skill.whenToUse)
145
+ front.when_to_use = skill.whenToUse;
146
+ else
147
+ delete front.when_to_use;
148
+ return `---\n${yaml.stringify(front).trimEnd()}\n---\n\n${skill.body.trim()}\n`;
149
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The skill vocabulary — the *knowledge* sibling of `src/plugins/tool.ts`.
3
+ *
4
+ * A skill is a markdown-shaped procedure or reference module, disclosed to a
5
+ * model in three tiers: a one-line catalog entry it always sees (Tier 1), a
6
+ * body injected into the system prompt once loaded (Tier 2), and bundled
7
+ * resources it may read after loading (Tier 3). That progression is the whole
8
+ * point: a workspace's accumulated know-how does not fit in a context window,
9
+ * and a catalog line costs ~50 tokens where a body costs thousands.
10
+ *
11
+ * Two sources of skill exist and they differ in exactly one way — where the
12
+ * body comes from. A **code skill** is registered from the deployment's own
13
+ * source (see {@link SkillDef}) and its body is a build artifact; an
14
+ * **external skill** is data the host stores and can edit at runtime. The
15
+ * harness owns the first entirely and knows nothing about the second beyond
16
+ * the shape it resolves to, which is why {@link SkillSummary} is the only type
17
+ * both sides share.
18
+ */
19
+ /**
20
+ * Where a resolved skill came from.
21
+ *
22
+ * - `platform` — code-registered and unowned: available to every agent.
23
+ * - `plugin` — code-registered and contributed by a tool plugin: available
24
+ * only to an agent that can load that plugin, since a recipe for tools the
25
+ * agent cannot call is noise.
26
+ * - `tenant` — authored in the **host's data store** rather than in code. The
27
+ * name is Monad's (a tenant is its workspace unit) and is kept because it is
28
+ * the value hosts already persist and render; read it as "host-managed".
29
+ *
30
+ * The order is also the precedence order — see `compareSkillSummaries`.
31
+ */
32
+ export type SkillOrigin = "platform" | "plugin" | "tenant";
33
+ /** `reference` = inline text the model reads; `asset` = an opaque blob, summarised rather than inlined. */
34
+ export type SkillResourceKind = "reference" | "asset";
35
+ /**
36
+ * A Tier-3 resource bundled with a code skill. Code-skill resources are static
37
+ * assets of the deploy artifact, so their text is a function rather than a
38
+ * handle: `render()` is called to hash the resource at registration and again
39
+ * to read it. Binary `asset` resources belong to host-stored skills, where
40
+ * there is somewhere to put the bytes.
41
+ */
42
+ export interface SkillResourceDef {
43
+ /** Relative path, e.g. `references/spec.md`. Unique within the skill. */
44
+ path: string;
45
+ /** Always `reference` for a code skill — there is no blob store in a build artifact. */
46
+ kind: Extract<SkillResourceKind, "reference">;
47
+ /** MIME type, e.g. `text/markdown`. */
48
+ contentType: string;
49
+ /** The inline text body. Must be deterministic: it feeds the content hash. */
50
+ render: () => string;
51
+ }
52
+ /**
53
+ * A code-registered skill — what a plugin contributes or a standalone module
54
+ * registers. `render()` must be deterministic for a given build: its output is
55
+ * hashed into the skill's `contentSha`, which is what pins an eval or a replay
56
+ * to the exact instructions a run actually saw.
57
+ */
58
+ export interface SkillDef {
59
+ /** Lower-kebab; unique across a registry. */
60
+ name: string;
61
+ /** Tier-1 catalog line: what the skill does. */
62
+ description: string;
63
+ /**
64
+ * Activation hint appended to the catalog line — when to reach for this.
65
+ * Required for a code skill: without it the catalog line says what the skill
66
+ * is but not when it applies, which is the half that decides routing.
67
+ */
68
+ whenToUse: string;
69
+ /**
70
+ * Set for a plugin-contributed skill. Filled by the registration path from
71
+ * the owning plugin's name rather than read from the def — see
72
+ * `createSkillRegistry`.
73
+ */
74
+ ownerPlugin?: string;
75
+ /** The Tier-2 markdown body. */
76
+ render: () => string;
77
+ /** Tier-3 bundled reference resources. */
78
+ resources?: SkillResourceDef[];
79
+ }
80
+ /**
81
+ * The Tier-1 projection every source resolves to — the analogue of
82
+ * `PluginSummary`. The catalog and the activation set are both built from
83
+ * these, so neither has to know whether a skill came from code or from the
84
+ * host's store.
85
+ */
86
+ export interface SkillSummary {
87
+ /**
88
+ * The activation ref: what a host persists as the run's active-skill set.
89
+ * `platform:<name>` for a code skill (see `refs.ts`), the host's own opaque
90
+ * identifier for an external one. One namespaced form spans both sources
91
+ * without ambiguity; the catalog still renders the plain `name`.
92
+ */
93
+ ref: string;
94
+ /** Lower-kebab identifier, unique within the resolved catalog. */
95
+ name: string;
96
+ /** Tier-1 catalog line. */
97
+ description: string;
98
+ /** Optional activation hint, appended to the catalog line. */
99
+ whenToUse: string | null;
100
+ /** Set for a plugin-contributed skill (and for a host skill that shadows one). */
101
+ ownerPlugin: string | null;
102
+ /** Which of the three sources this came from. */
103
+ origin: SkillOrigin;
104
+ }
105
+ /** A registered code skill, as the registry hands it back. */
106
+ export interface RegisteredSkill {
107
+ def: SkillDef;
108
+ /** `platform` (standalone) or `plugin` (contributed). Never `tenant`: code only. */
109
+ origin: Extract<SkillOrigin, "platform" | "plugin">;
110
+ /** The owning plugin's name for a `plugin` skill; null for `platform`. */
111
+ ownerPlugin: string | null;
112
+ /** Content hash over every version-defining input, computed at registration. */
113
+ contentSha: string;
114
+ }
115
+ /** A Tier-3 resource as the catalog and the wrapper see it — metadata, not bytes. */
116
+ export interface SkillResourceRef {
117
+ path: string;
118
+ kind: SkillResourceKind;
119
+ /** Size in bytes. Rendered only for `asset`, which is never inlined. */
120
+ bytes: number;
121
+ }
122
+ /**
123
+ * A skill body resolved for injection, whatever its source. The host's
124
+ * external source returns these; the registry-backed code path produces them
125
+ * internally.
126
+ */
127
+ export interface ResolvedSkillBody {
128
+ name: string;
129
+ ownerPlugin: string | null;
130
+ /**
131
+ * Monotonic revision for a host-stored skill, rendered into the wrapper so
132
+ * the model can cite what it read. `null` for a code skill, whose version
133
+ * history is the deployment's source control rather than a number.
134
+ */
135
+ version: number | null;
136
+ body: string;
137
+ /** The content hash pinned into the run's `loadedSkillShas`. */
138
+ contentSha: string;
139
+ resources: SkillResourceRef[];
140
+ }
141
+ /**
142
+ * Rough token budget for the Tier-1 catalog. Past it the catalog demotes its
143
+ * overflow to names only rather than dropping entries: a name the model can
144
+ * still pass to `load_skill` beats a skill it cannot discover at all.
145
+ */
146
+ export declare const SKILL_CATALOG_TOKEN_BUDGET = 2500;
147
+ /**
148
+ * Max skills one session may hold loaded at once. The count half of the active
149
+ * bound — cheap to check before anything is resolved.
150
+ */
151
+ export declare const SKILL_MAX_ACTIVE_PER_SESSION = 12;
152
+ /**
153
+ * Combined token budget for all loaded bodies. The other half of the active
154
+ * bound, and the one that actually protects the context window: twelve small
155
+ * skills are fine and three large ones are not, so a count cap alone does not
156
+ * bound the prompt.
157
+ */
158
+ export declare const SKILL_MAX_ACTIVE_BODY_TOKENS = 40000;
159
+ /**
160
+ * Characters per token, for every estimate in this module. Deliberately crude:
161
+ * these budgets choose between "render the line" and "render the name", and a
162
+ * real tokenizer would cost more than the decision is worth.
163
+ */
164
+ export declare const SKILL_CHARS_PER_TOKEN = 4;
165
+ /** The module's shared estimator. */
166
+ export declare function estimateSkillTokens(text: string): number;
167
+ /**
168
+ * Observe something the harness will otherwise carry on past — a name
169
+ * collision, a pinned body that has drifted, an activation of a skill that is
170
+ * not in the catalog. A port rather than a logger because the package has no
171
+ * opinion about where a host's warnings go, and because a test needs to see
172
+ * them without scraping stdout.
173
+ */
174
+ export type SkillWarn = (message: string, fields: Record<string, unknown>) => void;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The skill vocabulary — the *knowledge* sibling of `src/plugins/tool.ts`.
3
+ *
4
+ * A skill is a markdown-shaped procedure or reference module, disclosed to a
5
+ * model in three tiers: a one-line catalog entry it always sees (Tier 1), a
6
+ * body injected into the system prompt once loaded (Tier 2), and bundled
7
+ * resources it may read after loading (Tier 3). That progression is the whole
8
+ * point: a workspace's accumulated know-how does not fit in a context window,
9
+ * and a catalog line costs ~50 tokens where a body costs thousands.
10
+ *
11
+ * Two sources of skill exist and they differ in exactly one way — where the
12
+ * body comes from. A **code skill** is registered from the deployment's own
13
+ * source (see {@link SkillDef}) and its body is a build artifact; an
14
+ * **external skill** is data the host stores and can edit at runtime. The
15
+ * harness owns the first entirely and knows nothing about the second beyond
16
+ * the shape it resolves to, which is why {@link SkillSummary} is the only type
17
+ * both sides share.
18
+ */
19
+ // ---------------------------------------------------------------------------
20
+ // Bounds
21
+ //
22
+ // Every one of these bounds the *prompt*, not a database. They are defaults: a
23
+ // host passes its own where the call takes one. What they must not be is
24
+ // absent — an unbounded catalog and an unbounded active set are the two ways a
25
+ // skill library silently degrades every run in the deployment rather than
26
+ // failing one of them.
27
+ // ---------------------------------------------------------------------------
28
+ /**
29
+ * Rough token budget for the Tier-1 catalog. Past it the catalog demotes its
30
+ * overflow to names only rather than dropping entries: a name the model can
31
+ * still pass to `load_skill` beats a skill it cannot discover at all.
32
+ */
33
+ export const SKILL_CATALOG_TOKEN_BUDGET = 2500;
34
+ /**
35
+ * Max skills one session may hold loaded at once. The count half of the active
36
+ * bound — cheap to check before anything is resolved.
37
+ */
38
+ export const SKILL_MAX_ACTIVE_PER_SESSION = 12;
39
+ /**
40
+ * Combined token budget for all loaded bodies. The other half of the active
41
+ * bound, and the one that actually protects the context window: twelve small
42
+ * skills are fine and three large ones are not, so a count cap alone does not
43
+ * bound the prompt.
44
+ */
45
+ export const SKILL_MAX_ACTIVE_BODY_TOKENS = 40_000;
46
+ /**
47
+ * Characters per token, for every estimate in this module. Deliberately crude:
48
+ * these budgets choose between "render the line" and "render the name", and a
49
+ * real tokenizer would cost more than the decision is worth.
50
+ */
51
+ export const SKILL_CHARS_PER_TOKEN = 4;
52
+ /** The module's shared estimator. */
53
+ export function estimateSkillTokens(text) {
54
+ return Math.ceil(text.length / SKILL_CHARS_PER_TOKEN);
55
+ }