@juno-ai/bind 9.0.0 → 11.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.
Files changed (46) hide show
  1. package/README.md +375 -15
  2. package/contracts/index.d.ts +1 -1
  3. package/contracts/index.js +1 -1
  4. package/contracts/turn.d.ts +77 -2
  5. package/contracts/turn.js +35 -2
  6. package/index.d.ts +6 -2
  7. package/index.js +6 -2
  8. package/loop/index.d.ts +2 -1
  9. package/loop/index.js +1 -1
  10. package/loop/tool-loop.d.ts +117 -12
  11. package/loop/tool-loop.js +242 -67
  12. package/package.json +10 -2
  13. package/plugins/dispatch.d.ts +130 -0
  14. package/plugins/dispatch.js +241 -0
  15. package/plugins/index.d.ts +2 -0
  16. package/plugins/index.js +2 -0
  17. package/plugins/tool-message.d.ts +23 -0
  18. package/plugins/tool-message.js +31 -0
  19. package/skills/activation.d.ts +64 -0
  20. package/skills/activation.js +39 -0
  21. package/skills/admission.d.ts +61 -0
  22. package/skills/admission.js +41 -0
  23. package/skills/catalog.d.ts +54 -0
  24. package/skills/catalog.js +77 -0
  25. package/skills/discovery.d.ts +82 -0
  26. package/skills/discovery.js +91 -0
  27. package/skills/index.d.ts +19 -0
  28. package/skills/index.js +19 -0
  29. package/skills/refs.d.ts +21 -0
  30. package/skills/refs.js +27 -0
  31. package/skills/registry.d.ts +57 -0
  32. package/skills/registry.js +94 -0
  33. package/skills/resolve.d.ts +89 -0
  34. package/skills/resolve.js +124 -0
  35. package/skills/sha.d.ts +53 -0
  36. package/skills/sha.js +60 -0
  37. package/skills/sha256.d.ts +38 -0
  38. package/skills/sha256.js +122 -0
  39. package/skills/skill-md.d.ts +73 -0
  40. package/skills/skill-md.js +149 -0
  41. package/skills/types.d.ts +174 -0
  42. package/skills/types.js +55 -0
  43. package/testing/index.d.ts +153 -0
  44. package/testing/index.js +188 -0
  45. package/tools/control-chars.d.ts +23 -0
  46. package/tools/control-chars.js +35 -0
@@ -0,0 +1,82 @@
1
+ import { type SkillYamlCodec } from "./skill-md.js";
2
+ import { type Sha256Hex } from "./sha256.js";
3
+ import type { RegisteredSkill } from "./types.js";
4
+ /**
5
+ * The Agent Skills **discovery index** — how a deployment advertises skills to
6
+ * agents that are not its own.
7
+ *
8
+ * Per the Agent Skills Discovery RFC v0.2.0
9
+ * (https://github.com/cloudflare/agent-skills-discovery-rfc,
10
+ * https://agentskills.io/), an external agent fetches
11
+ * `/.well-known/agent-skills/index.json`, reads the `skills[]` catalogue, pulls
12
+ * each `SKILL.md` from its advertised `url`, and verifies the bytes against the
13
+ * published `digest`. Both documents are generated from the same registry here,
14
+ * so the digest a reader sees always matches the bytes served.
15
+ *
16
+ * **What to publish is a product decision and stays with the host** — hence the
17
+ * explicit `include` allowlist rather than "every platform skill". The
18
+ * criterion that matters is whether a skill helps an outside agent drive
19
+ * *something the deployment actually exposes externally*. A skill about the
20
+ * host's internal agent runtime cannot help an external client and does not
21
+ * belong in a public document; a plugin-contributed skill is tied to tools that
22
+ * are not on the external surface; a workspace's own skills are private data.
23
+ * Making the list an argument also means dropping a new file into a skills
24
+ * folder never publishes it by accident.
25
+ */
26
+ /** The RFC v0.2.0 schema URL advertised in the index's `$schema` field. */
27
+ export declare const AGENT_SKILLS_DISCOVERY_SCHEMA_URL = "https://schemas.agentskills.io/discovery/0.2.0/schema.json";
28
+ /** Root of the well-known discovery namespace (host-relative per RFC 3986). */
29
+ export declare const AGENT_SKILLS_WELL_KNOWN_BASE = "/.well-known/agent-skills";
30
+ /** Host-relative URL of the `SKILL.md` artifact for a skill name. */
31
+ export declare function skillMarkdownUrl(name: string, base?: string): string;
32
+ /** One entry in the discovery index's `skills[]` array. */
33
+ export interface DiscoverySkillEntry {
34
+ /** Lower-kebab skill identifier. */
35
+ name: string;
36
+ /** Distribution format. Single-file `SKILL.md` skills only. */
37
+ type: "skill-md";
38
+ /** Tier-1 catalogue line (≤ 1024 chars per the RFC). */
39
+ description: string;
40
+ /** Location of the `SKILL.md`, resolved per RFC 3986. */
41
+ url: string;
42
+ /** SHA-256 of the served `SKILL.md` bytes, `sha256:{64-hex}`. */
43
+ digest: string;
44
+ }
45
+ /** The full `/.well-known/agent-skills/index.json` document. */
46
+ export interface AgentSkillsDiscoveryIndex {
47
+ $schema: string;
48
+ skills: DiscoverySkillEntry[];
49
+ }
50
+ export interface SkillDiscoveryOptions {
51
+ /**
52
+ * Names to publish. Anything not registered as a `platform` skill is skipped.
53
+ *
54
+ * Deliberately **not** `Iterable<string>`: this options object is built once
55
+ * and read by both functions below, and a one-shot iterable (a generator, a
56
+ * `Map.keys()`) would be drained by the first call — so the index would
57
+ * publish the allowlist and every subsequent `SKILL.md` fetch would 404, with
58
+ * nothing in either signature to suggest why. Both accepted forms are
59
+ * re-iterable, which makes that failure unrepresentable rather than merely
60
+ * documented.
61
+ */
62
+ include: readonly string[] | ReadonlySet<string>;
63
+ /** The registry to read from; the host bootstraps it before calling. */
64
+ registry: {
65
+ get(name: string): RegisteredSkill | undefined;
66
+ };
67
+ yaml: SkillYamlCodec;
68
+ /** Base for the advertised `url`. Host-relative by default. */
69
+ baseUrl?: string;
70
+ sha256Hex?: Sha256Hex;
71
+ }
72
+ /** Build the discovery index from the allowlisted, registered platform skills. */
73
+ export declare function buildAgentSkillsDiscoveryIndex(options: SkillDiscoveryOptions): AgentSkillsDiscoveryIndex;
74
+ /**
75
+ * The published `SKILL.md` text for a name, or `null` when it is not published.
76
+ *
77
+ * One return value for "not on the allowlist", "not registered", and
78
+ * "registered but not a platform skill": the caller serves a 404 for all three,
79
+ * and distinguishing them would tell an anonymous reader which internal skills
80
+ * exist.
81
+ */
82
+ export declare function getPublishedSkillMarkdown(name: string, options: SkillDiscoveryOptions): string | null;
@@ -0,0 +1,91 @@
1
+ import { serializeSkillMarkdown } from "./skill-md.js";
2
+ import { sha256Hex as defaultSha256Hex } from "./sha256.js";
3
+ /**
4
+ * The Agent Skills **discovery index** — how a deployment advertises skills to
5
+ * agents that are not its own.
6
+ *
7
+ * Per the Agent Skills Discovery RFC v0.2.0
8
+ * (https://github.com/cloudflare/agent-skills-discovery-rfc,
9
+ * https://agentskills.io/), an external agent fetches
10
+ * `/.well-known/agent-skills/index.json`, reads the `skills[]` catalogue, pulls
11
+ * each `SKILL.md` from its advertised `url`, and verifies the bytes against the
12
+ * published `digest`. Both documents are generated from the same registry here,
13
+ * so the digest a reader sees always matches the bytes served.
14
+ *
15
+ * **What to publish is a product decision and stays with the host** — hence the
16
+ * explicit `include` allowlist rather than "every platform skill". The
17
+ * criterion that matters is whether a skill helps an outside agent drive
18
+ * *something the deployment actually exposes externally*. A skill about the
19
+ * host's internal agent runtime cannot help an external client and does not
20
+ * belong in a public document; a plugin-contributed skill is tied to tools that
21
+ * are not on the external surface; a workspace's own skills are private data.
22
+ * Making the list an argument also means dropping a new file into a skills
23
+ * folder never publishes it by accident.
24
+ */
25
+ /** The RFC v0.2.0 schema URL advertised in the index's `$schema` field. */
26
+ export const AGENT_SKILLS_DISCOVERY_SCHEMA_URL = "https://schemas.agentskills.io/discovery/0.2.0/schema.json";
27
+ /** Root of the well-known discovery namespace (host-relative per RFC 3986). */
28
+ export const AGENT_SKILLS_WELL_KNOWN_BASE = "/.well-known/agent-skills";
29
+ /** Host-relative URL of the `SKILL.md` artifact for a skill name. */
30
+ export function skillMarkdownUrl(name, base = AGENT_SKILLS_WELL_KNOWN_BASE) {
31
+ return `${base}/${name}/SKILL.md`;
32
+ }
33
+ /**
34
+ * Canonical `SKILL.md` bytes for a published skill — exactly the text served at
35
+ * its `url` and hashed for its `digest`.
36
+ *
37
+ * `metadata` is null because a code skill has none to preserve: it is
38
+ * registered from name, description, hint and body, so anything else in the
39
+ * document it was authored from was already dropped at registration. Emitting
40
+ * it here would put bytes in the artifact that the digest of a re-registered
41
+ * skill could not reproduce.
42
+ */
43
+ function skillMarkdownFor(skill, yaml) {
44
+ return serializeSkillMarkdown({
45
+ name: skill.def.name,
46
+ description: skill.def.description,
47
+ whenToUse: skill.def.whenToUse ?? null,
48
+ body: skill.def.render(),
49
+ metadata: null,
50
+ }, yaml);
51
+ }
52
+ /** Only `platform`-origin skills are publishable; a name that is not one is not published. */
53
+ function publishable(name, registry) {
54
+ const skill = registry.get(name);
55
+ return skill && skill.origin === "platform" ? skill : null;
56
+ }
57
+ /** The allowlist as a set — de-duplicated, and O(1) to test. */
58
+ function allowlist(include) {
59
+ return include instanceof Set ? include : new Set(include);
60
+ }
61
+ /** Build the discovery index from the allowlisted, registered platform skills. */
62
+ export function buildAgentSkillsDiscoveryIndex(options) {
63
+ const sha256Hex = options.sha256Hex ?? defaultSha256Hex;
64
+ const skills = [...allowlist(options.include)]
65
+ .map((name) => publishable(name, options.registry))
66
+ .filter((skill) => skill !== null)
67
+ .sort((a, b) => a.def.name.localeCompare(b.def.name))
68
+ .map((skill) => ({
69
+ name: skill.def.name,
70
+ type: "skill-md",
71
+ description: skill.def.description,
72
+ url: skillMarkdownUrl(skill.def.name, options.baseUrl),
73
+ digest: `sha256:${sha256Hex(skillMarkdownFor(skill, options.yaml))}`,
74
+ }));
75
+ return { $schema: AGENT_SKILLS_DISCOVERY_SCHEMA_URL, skills };
76
+ }
77
+ /**
78
+ * The published `SKILL.md` text for a name, or `null` when it is not published.
79
+ *
80
+ * One return value for "not on the allowlist", "not registered", and
81
+ * "registered but not a platform skill": the caller serves a 404 for all three,
82
+ * and distinguishing them would tell an anonymous reader which internal skills
83
+ * exist.
84
+ */
85
+ export function getPublishedSkillMarkdown(name, options) {
86
+ // Allowlist first: an unknown name short-circuits without touching the registry.
87
+ if (!allowlist(options.include).has(name))
88
+ return null;
89
+ const skill = publishable(name, options.registry);
90
+ return skill ? skillMarkdownFor(skill, options.yaml) : null;
91
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `@juno-ai/bind/skills` — progressive knowledge disclosure.
3
+ *
4
+ * The knowledge sibling of `@juno-ai/bind/plugins`: the same three-tier
5
+ * disclosure applied to instructions rather than tools. See `docs/skills.md`
6
+ * for the model and `docs/bind.md` for why each piece is on this side of the
7
+ * boundary.
8
+ */
9
+ export { SKILL_CATALOG_TOKEN_BUDGET, SKILL_CHARS_PER_TOKEN, SKILL_MAX_ACTIVE_BODY_TOKENS, SKILL_MAX_ACTIVE_PER_SESSION, estimateSkillTokens, type RegisteredSkill, type ResolvedSkillBody, type SkillDef, type SkillOrigin, type SkillResourceDef, type SkillResourceKind, type SkillResourceRef, type SkillSummary, type SkillWarn, } from "./types.js";
10
+ export { PLATFORM_SKILL_REF_PREFIX, codeSkillNameFromRef, codeSkillRef, isCodeSkillRef, } from "./refs.js";
11
+ export { sha256Hex, utf8ByteLength, type Sha256Hex } from "./sha256.js";
12
+ export { computeSkillContentSha, stableSkillJson, type SkillShaInput } from "./sha.js";
13
+ export { parseSkillMarkdown, serializeSkillMarkdown, type ParsedSkillMd, type SkillMdParseResult, type SkillYamlCodec, } from "./skill-md.js";
14
+ export { codeSkillResourceRefs, createSkillRegistry, toCodeSkillSummary, type SkillRegistry, type SkillRegistryOptions, } from "./registry.js";
15
+ export { compareSkillSummaries, partitionSkillCatalog, type SkillCatalogDetail, type SkillCatalogEntry, type SkillCatalogOptions, type SkillCatalogPartition, } from "./catalog.js";
16
+ export { resolveActiveSkillInstructions, wrapSkillContent, type ExternalSkillSource, type ResolveActiveSkillsParams, type ResolvedSkillInstructions, } from "./resolve.js";
17
+ export { createSkillActivation, type SkillActivation, type SkillActivationParams, type SkillStore, } from "./activation.js";
18
+ export { admitSkillLoad, estimateSkillBodyTokens, type SkillLoadBounds, type SkillLoadDecision, type SkillLoadRefusal, } from "./admission.js";
19
+ export { AGENT_SKILLS_DISCOVERY_SCHEMA_URL, AGENT_SKILLS_WELL_KNOWN_BASE, buildAgentSkillsDiscoveryIndex, getPublishedSkillMarkdown, skillMarkdownUrl, type AgentSkillsDiscoveryIndex, type DiscoverySkillEntry, type SkillDiscoveryOptions, } from "./discovery.js";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `@juno-ai/bind/skills` — progressive knowledge disclosure.
3
+ *
4
+ * The knowledge sibling of `@juno-ai/bind/plugins`: the same three-tier
5
+ * disclosure applied to instructions rather than tools. See `docs/skills.md`
6
+ * for the model and `docs/bind.md` for why each piece is on this side of the
7
+ * boundary.
8
+ */
9
+ export { SKILL_CATALOG_TOKEN_BUDGET, SKILL_CHARS_PER_TOKEN, SKILL_MAX_ACTIVE_BODY_TOKENS, SKILL_MAX_ACTIVE_PER_SESSION, estimateSkillTokens, } from "./types.js";
10
+ export { PLATFORM_SKILL_REF_PREFIX, codeSkillNameFromRef, codeSkillRef, isCodeSkillRef, } from "./refs.js";
11
+ export { sha256Hex, utf8ByteLength } from "./sha256.js";
12
+ export { computeSkillContentSha, stableSkillJson } from "./sha.js";
13
+ export { parseSkillMarkdown, serializeSkillMarkdown, } from "./skill-md.js";
14
+ export { codeSkillResourceRefs, createSkillRegistry, toCodeSkillSummary, } from "./registry.js";
15
+ export { compareSkillSummaries, partitionSkillCatalog, } from "./catalog.js";
16
+ export { resolveActiveSkillInstructions, wrapSkillContent, } from "./resolve.js";
17
+ export { createSkillActivation, } from "./activation.js";
18
+ export { admitSkillLoad, estimateSkillBodyTokens, } from "./admission.js";
19
+ export { AGENT_SKILLS_DISCOVERY_SCHEMA_URL, AGENT_SKILLS_WELL_KNOWN_BASE, buildAgentSkillsDiscoveryIndex, getPublishedSkillMarkdown, skillMarkdownUrl, } from "./discovery.js";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Activation refs.
3
+ *
4
+ * A run persists which skills are loaded, and the set spans two sources whose
5
+ * identifiers cannot be told apart by shape — a code skill has a name, a
6
+ * host-stored skill has whatever id the host mints. Namespacing the code side
7
+ * makes the union unambiguous with one string comparison and no lookup, which
8
+ * matters because the resolver has to route every ref before it knows whether
9
+ * either source has it.
10
+ *
11
+ * The prefix is part of the persisted format. Changing it strands every stored
12
+ * active-skill set and every `loadedSkillShas` pin recorded against one.
13
+ */
14
+ /** The namespace every code-registered skill's ref carries. */
15
+ export declare const PLATFORM_SKILL_REF_PREFIX = "platform:";
16
+ /** Build the activation ref for a code-registered skill name. */
17
+ export declare function codeSkillRef(name: string): string;
18
+ /** True if a ref points at a code-registered skill rather than a host-stored one. */
19
+ export declare function isCodeSkillRef(ref: string): boolean;
20
+ /** Extract the skill name from a code-skill ref (`platform:foo` → `foo`). */
21
+ export declare function codeSkillNameFromRef(ref: string): string;
package/skills/refs.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Activation refs.
3
+ *
4
+ * A run persists which skills are loaded, and the set spans two sources whose
5
+ * identifiers cannot be told apart by shape — a code skill has a name, a
6
+ * host-stored skill has whatever id the host mints. Namespacing the code side
7
+ * makes the union unambiguous with one string comparison and no lookup, which
8
+ * matters because the resolver has to route every ref before it knows whether
9
+ * either source has it.
10
+ *
11
+ * The prefix is part of the persisted format. Changing it strands every stored
12
+ * active-skill set and every `loadedSkillShas` pin recorded against one.
13
+ */
14
+ /** The namespace every code-registered skill's ref carries. */
15
+ export const PLATFORM_SKILL_REF_PREFIX = "platform:";
16
+ /** Build the activation ref for a code-registered skill name. */
17
+ export function codeSkillRef(name) {
18
+ return `${PLATFORM_SKILL_REF_PREFIX}${name}`;
19
+ }
20
+ /** True if a ref points at a code-registered skill rather than a host-stored one. */
21
+ export function isCodeSkillRef(ref) {
22
+ return ref.startsWith(PLATFORM_SKILL_REF_PREFIX);
23
+ }
24
+ /** Extract the skill name from a code-skill ref (`platform:foo` → `foo`). */
25
+ export function codeSkillNameFromRef(ref) {
26
+ return ref.slice(PLATFORM_SKILL_REF_PREFIX.length);
27
+ }
@@ -0,0 +1,57 @@
1
+ import { type Sha256Hex } from "./sha256.js";
2
+ import type { RegisteredSkill, SkillDef, SkillResourceRef, SkillSummary, SkillWarn } from "./types.js";
3
+ /**
4
+ * The code-skill registry — what a deployment's own source contributes, as
5
+ * against what its users author.
6
+ *
7
+ * **A factory, not a module singleton**, for the same two reasons the tool
8
+ * registry is one: module state in a workerd isolate has a lifetime the host
9
+ * does not control, and an implicit global makes every test share a registry
10
+ * with every other. A host that wants singleton ergonomics wraps one instance
11
+ * in a module of its own — that decision belongs to the host.
12
+ *
13
+ * The registry is deliberately **not** the whole skill library. It holds code
14
+ * skills, whose bodies are build artifacts and whose version history is source
15
+ * control. Host-stored skills never enter it; they meet code skills only in the
16
+ * resolved {@link SkillSummary} catalog, which is source-agnostic by design.
17
+ */
18
+ export interface SkillRegistry {
19
+ /**
20
+ * Register a standalone skill with no owning plugin — available to every
21
+ * agent.
22
+ *
23
+ * `ownerPlugin` on the def is **ignored** and warned about. A platform skill
24
+ * is unowned by definition, and honouring a stray field here would make the
25
+ * loader auto-activate a plugin and mislabel the skill as that plugin's.
26
+ */
27
+ registerPlatform(def: SkillDef): void;
28
+ /**
29
+ * Register a skill contributed by a tool plugin. `ownerPlugin` is filled from
30
+ * `pluginName` rather than the def, so a recipe is only ever offered to an
31
+ * agent that can load the tools it describes.
32
+ */
33
+ registerPlugin(pluginName: string, def: SkillDef): void;
34
+ get(name: string): RegisteredSkill | undefined;
35
+ all(): RegisteredSkill[];
36
+ /** Tier-1 summaries, gated to the plugins available on this run. */
37
+ summaries(options?: {
38
+ availablePlugins?: Iterable<string>;
39
+ }): SkillSummary[];
40
+ /** Drop every registration. For a test that wants a controlled set. */
41
+ clear(): void;
42
+ }
43
+ export interface SkillRegistryOptions {
44
+ /**
45
+ * Observe a likely author bug: a name collision, or a platform registration
46
+ * carrying an owner. Both overwrite/ignore and continue — a bad skill must
47
+ * not take down agent startup — so this is the only signal they happened.
48
+ */
49
+ onWarn?: SkillWarn;
50
+ /** Override the content digest. Must be synchronous SHA-256 (see `sha256.ts`). */
51
+ sha256Hex?: Sha256Hex;
52
+ }
53
+ /** The Tier-3 listing for a code skill: metadata, with sizes measured in UTF-8 bytes. */
54
+ export declare function codeSkillResourceRefs(def: SkillDef): SkillResourceRef[];
55
+ /** Project a registered code skill into the source-agnostic Tier-1 summary. */
56
+ export declare function toCodeSkillSummary(skill: RegisteredSkill): SkillSummary;
57
+ export declare function createSkillRegistry(options?: SkillRegistryOptions): SkillRegistry;
@@ -0,0 +1,94 @@
1
+ import { codeSkillRef } from "./refs.js";
2
+ import { computeSkillContentSha } from "./sha.js";
3
+ import { sha256Hex as defaultSha256Hex, utf8ByteLength } from "./sha256.js";
4
+ /** The Tier-3 listing for a code skill: metadata, with sizes measured in UTF-8 bytes. */
5
+ export function codeSkillResourceRefs(def) {
6
+ return (def.resources ?? []).map((resource) => ({
7
+ path: resource.path,
8
+ kind: resource.kind,
9
+ bytes: utf8ByteLength(resource.render()),
10
+ }));
11
+ }
12
+ /** Project a registered code skill into the source-agnostic Tier-1 summary. */
13
+ export function toCodeSkillSummary(skill) {
14
+ return {
15
+ ref: codeSkillRef(skill.def.name),
16
+ name: skill.def.name,
17
+ description: skill.def.description,
18
+ whenToUse: skill.def.whenToUse ?? null,
19
+ ownerPlugin: skill.ownerPlugin,
20
+ origin: skill.origin,
21
+ };
22
+ }
23
+ export function createSkillRegistry(options = {}) {
24
+ const skills = new Map();
25
+ const sha256Hex = options.sha256Hex ?? defaultSha256Hex;
26
+ const warn = options.onWarn ?? (() => { });
27
+ const digestResources = (resources) => (resources ?? []).map((resource) => ({
28
+ path: resource.path,
29
+ contentType: resource.contentType,
30
+ contentHash: sha256Hex(resource.render()),
31
+ }));
32
+ const register = (def, origin, ownerPlugin) => {
33
+ const existing = skills.get(def.name);
34
+ // Re-registering the *same* def is ordinary — a module imported twice, a
35
+ // host that re-runs its bootstrap — and is silent. Two distinct defs under
36
+ // one name is a copy-paste or a cross-plugin clash: last wins, loudly.
37
+ if (existing && existing.def !== def) {
38
+ warn("code skill name collision — overwriting", {
39
+ name: def.name,
40
+ previousOrigin: existing.origin,
41
+ previousOwnerPlugin: existing.ownerPlugin,
42
+ newOrigin: origin,
43
+ newOwnerPlugin: ownerPlugin,
44
+ });
45
+ }
46
+ const contentSha = computeSkillContentSha({
47
+ name: def.name,
48
+ description: def.description,
49
+ whenToUse: def.whenToUse ?? null,
50
+ body: def.render(),
51
+ ownerPlugin,
52
+ metadata: null,
53
+ resources: digestResources(def.resources),
54
+ }, sha256Hex);
55
+ skills.set(def.name, { def, origin, ownerPlugin, contentSha });
56
+ };
57
+ return {
58
+ registerPlatform(def) {
59
+ if (def.ownerPlugin != null) {
60
+ warn("platform skill declared an ownerPlugin; ignoring it", {
61
+ name: def.name,
62
+ ignoredOwnerPlugin: def.ownerPlugin,
63
+ });
64
+ }
65
+ register(def, "platform", null);
66
+ },
67
+ registerPlugin(pluginName, def) {
68
+ register({ ...def, ownerPlugin: pluginName }, "plugin", pluginName);
69
+ },
70
+ get(name) {
71
+ return skills.get(name);
72
+ },
73
+ all() {
74
+ return [...skills.values()];
75
+ },
76
+ summaries(summaryOptions) {
77
+ const availablePlugins = summaryOptions?.availablePlugins === undefined
78
+ ? undefined
79
+ : new Set(summaryOptions.availablePlugins);
80
+ const out = [];
81
+ for (const skill of skills.values()) {
82
+ if (skill.origin === "plugin" && availablePlugins) {
83
+ if (!skill.ownerPlugin || !availablePlugins.has(skill.ownerPlugin))
84
+ continue;
85
+ }
86
+ out.push(toCodeSkillSummary(skill));
87
+ }
88
+ return out;
89
+ },
90
+ clear() {
91
+ skills.clear();
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,89 @@
1
+ import type { RegisteredSkill, ResolvedSkillBody, SkillResourceRef, SkillSummary, SkillWarn } from "./types.js";
2
+ /**
3
+ * Tier 2 — resolving the bodies of the skills a run has loaded.
4
+ *
5
+ * The bodies land in the **system prompt**, not the conversation tail, which is
6
+ * the decision the rest of this module follows from. It makes them immune to
7
+ * compaction (an agent cannot forget an instruction it was given the way it
8
+ * forgets a message), and it puts them inside the provider's cacheable prefix —
9
+ * so their order and their bytes have to be a pure function of the run's state,
10
+ * or every turn pays for a cache miss on everything below them.
11
+ */
12
+ /**
13
+ * Resolve the bodies of the host's own skills — the ones the registry does not
14
+ * hold. Batched rather than per-ref because the natural implementation is a
15
+ * query, and a per-ref port turns one query into N.
16
+ *
17
+ * `pinnedShas` is a subset of the caller's pin map covering only these refs. A
18
+ * source that can reproduce a historical body should return it; one that cannot
19
+ * should return its current body and leave the drift for the caller to notice
20
+ * (the resolver records what it actually resolved, never what was asked for).
21
+ * Refs the source omits are dropped from the result, which is the right answer
22
+ * for a skill that was deleted mid-session.
23
+ */
24
+ export type ExternalSkillSource = (refs: string[], pinnedShas: Record<string, string> | undefined) => Promise<ReadonlyMap<string, ResolvedSkillBody>>;
25
+ export interface ResolveActiveSkillsParams {
26
+ /** The run's active refs, in whatever order the host stored them. */
27
+ activeRefs: readonly string[];
28
+ /** This run's Tier-1 catalog. A ref absent from it resolves to nothing. */
29
+ available: readonly SkillSummary[];
30
+ /** Where code refs (`platform:<name>`) resolve from. */
31
+ registry: {
32
+ get(name: string): RegisteredSkill | undefined;
33
+ };
34
+ /** Where every other ref resolves from. Omit for a code-only deployment. */
35
+ externalSource?: ExternalSkillSource;
36
+ /**
37
+ * Ref → `contentSha` recorded by a completed run. Present = **pinned mode**:
38
+ * reproduce what that run saw, for a replay, an eval, or an optimizer.
39
+ * Absent = live head, which is what an ordinary turn wants — a skill edited
40
+ * mid-session should take effect on the next turn.
41
+ */
42
+ pinnedShas?: Record<string, string>;
43
+ /**
44
+ * Skills resolvable by name that are **not** in the registry, consulted
45
+ * before it.
46
+ *
47
+ * This is the isolation seam. A host may want an always-on instruction module
48
+ * — an onboarding persona, a per-agent operating manual — that flows through
49
+ * this one resolver alongside the standard library but must never appear in
50
+ * any catalog and must never be loadable by name from a model's tool call.
51
+ * Registering it globally would do both. Passing it here does neither.
52
+ */
53
+ extraSkills?: ReadonlyMap<string, RegisteredSkill>;
54
+ onWarn?: SkillWarn;
55
+ }
56
+ export interface ResolvedSkillInstructions {
57
+ /** One wrapped body per resolved ref, in catalog order. */
58
+ instructions: string[];
59
+ /** Ref → the `contentSha` actually resolved. The run's pin. */
60
+ shas: Record<string, string>;
61
+ }
62
+ /**
63
+ * Wrap a resolved body for in-prompt identification.
64
+ *
65
+ * The wrapper is not decoration: several bodies are concatenated into one
66
+ * section, and without a delimiter a model attributes an instruction from one
67
+ * skill to another — or to the host's own system prompt, which is worse,
68
+ * because it then applies workspace-authored text with the authority of the
69
+ * platform. The attributes are what let it cite which skill it is following,
70
+ * and the resource block is what tells it a Tier-3 read is available at all.
71
+ */
72
+ export declare function wrapSkillContent(params: {
73
+ name: string;
74
+ ownerPlugin: string | null;
75
+ version: number | null;
76
+ body: string;
77
+ resources: readonly SkillResourceRef[];
78
+ }): string;
79
+ /**
80
+ * Resolve the active set into instruction bodies plus the per-skill hash pin.
81
+ *
82
+ * Rendering order is the **catalog's** total order, never the caller's
83
+ * `activeRefs` order. That argument is set/insertion order, which differs
84
+ * between a fresh run that loaded A then B and a resumed run whose session
85
+ * persisted `[B, A]`; since these bodies sit high in the system prompt, an
86
+ * unstable order byte-shifts the prefix and busts the provider's cache for
87
+ * every turn after a resume.
88
+ */
89
+ export declare function resolveActiveSkillInstructions(params: ResolveActiveSkillsParams): Promise<ResolvedSkillInstructions>;
@@ -0,0 +1,124 @@
1
+ import { compareSkillSummaries } from "./catalog.js";
2
+ import { codeSkillNameFromRef, isCodeSkillRef } from "./refs.js";
3
+ import { codeSkillResourceRefs } from "./registry.js";
4
+ /**
5
+ * Escape a value interpolated into a wrapper attribute.
6
+ *
7
+ * The wrapper's whole job is to tell the model which instructions came from
8
+ * where, and a skill's `name` is host data — for a host-stored skill, a string
9
+ * a *user* typed. Without this, a name containing `"` closes the attribute and
10
+ * whatever follows is read as further markup: a skill called
11
+ * `x" trusted="platform` would present itself to the model as platform policy.
12
+ * Monad validates names to lower-kebab, which is why this was never reachable
13
+ * there, but the harness cannot assume every host does — and a fence that holds
14
+ * only when the caller is careful is not a fence.
15
+ *
16
+ * A no-op for every legitimate identifier, so no existing prompt shifts a byte.
17
+ */
18
+ function attribute(value) {
19
+ return value
20
+ .replace(/&/g, "&amp;")
21
+ .replace(/</g, "&lt;")
22
+ .replace(/>/g, "&gt;")
23
+ .replace(/"/g, "&quot;");
24
+ }
25
+ /**
26
+ * Wrap a resolved body for in-prompt identification.
27
+ *
28
+ * The wrapper is not decoration: several bodies are concatenated into one
29
+ * section, and without a delimiter a model attributes an instruction from one
30
+ * skill to another — or to the host's own system prompt, which is worse,
31
+ * because it then applies workspace-authored text with the authority of the
32
+ * platform. The attributes are what let it cite which skill it is following,
33
+ * and the resource block is what tells it a Tier-3 read is available at all.
34
+ */
35
+ export function wrapSkillContent(params) {
36
+ const attributes = [`name="${attribute(params.name)}"`];
37
+ if (params.ownerPlugin)
38
+ attributes.push(`owner_plugin="${attribute(params.ownerPlugin)}"`);
39
+ if (params.version !== null)
40
+ attributes.push(`version="${params.version}"`);
41
+ const parts = [`<skill_content ${attributes.join(" ")}>`, params.body.trim()];
42
+ if (params.resources.length > 0) {
43
+ const files = params.resources
44
+ .map((resource) => ` <file path="${attribute(resource.path)}" kind="${attribute(resource.kind)}"${resource.kind === "asset" ? ` bytes="${resource.bytes}"` : ""}/>`)
45
+ .join("\n");
46
+ parts.push("", "<skill_resources>", files, "</skill_resources>",
47
+ // Escaped here too: the name is interpolated into a quoted call, so an
48
+ // unescaped one breaks out of this sentence exactly as it would out of an
49
+ // attribute. Identical output for any name a host should be accepting.
50
+ `Read a resource with read_skill_resource("${attribute(params.name)}", path).`);
51
+ }
52
+ parts.push("</skill_content>");
53
+ return parts.join("\n");
54
+ }
55
+ /** Resolve a code ref against the extra map first, then the registry. */
56
+ function resolveCodeSkill(ref, params) {
57
+ const name = codeSkillNameFromRef(ref);
58
+ const registered = params.extraSkills?.get(name) ?? params.registry.get(name);
59
+ if (!registered)
60
+ return null;
61
+ const pinned = params.pinnedShas?.[ref];
62
+ if (pinned && registered.contentSha !== pinned) {
63
+ // A code skill's history is the deployment's source control, and the
64
+ // running build has only its own copy — the body at the pinned hash is not
65
+ // recoverable here. Reproduce best-effort and flag it, so a consumer
66
+ // comparing runs can see that this one is not a faithful replay.
67
+ params.onWarn?.("pinned code skill drifted since the run; using current body", {
68
+ ref,
69
+ pinnedSha: pinned,
70
+ currentSha: registered.contentSha,
71
+ });
72
+ }
73
+ return {
74
+ name: registered.def.name,
75
+ ownerPlugin: registered.ownerPlugin,
76
+ version: null,
77
+ body: registered.def.render(),
78
+ contentSha: registered.contentSha,
79
+ resources: codeSkillResourceRefs(registered.def),
80
+ };
81
+ }
82
+ /**
83
+ * Resolve the active set into instruction bodies plus the per-skill hash pin.
84
+ *
85
+ * Rendering order is the **catalog's** total order, never the caller's
86
+ * `activeRefs` order. That argument is set/insertion order, which differs
87
+ * between a fresh run that loaded A then B and a resumed run whose session
88
+ * persisted `[B, A]`; since these bodies sit high in the system prompt, an
89
+ * unstable order byte-shifts the prefix and busts the provider's cache for
90
+ * every turn after a resume.
91
+ */
92
+ export async function resolveActiveSkillInstructions(params) {
93
+ const byRef = new Map(params.available.map((summary) => [summary.ref, summary]));
94
+ const present = params.activeRefs
95
+ .map((ref) => byRef.get(ref))
96
+ .filter((summary) => summary !== undefined)
97
+ .sort(compareSkillSummaries)
98
+ .map((summary) => summary.ref);
99
+ const externalRefs = present.filter((ref) => !isCodeSkillRef(ref));
100
+ const external = externalRefs.length > 0 && params.externalSource
101
+ ? await params.externalSource(externalRefs, params.pinnedShas)
102
+ : new Map();
103
+ const instructions = [];
104
+ const shas = {};
105
+ for (const ref of present) {
106
+ const resolved = isCodeSkillRef(ref)
107
+ ? resolveCodeSkill(ref, params)
108
+ : (external.get(ref) ?? null);
109
+ // A ref that resolves to nothing is skipped rather than reported: it means
110
+ // the skill was deleted, renamed, or gated off since it was loaded, and a
111
+ // run should carry on without the instruction rather than fail on it.
112
+ if (!resolved)
113
+ continue;
114
+ instructions.push(wrapSkillContent({
115
+ name: resolved.name,
116
+ ownerPlugin: resolved.ownerPlugin,
117
+ version: resolved.version,
118
+ body: resolved.body,
119
+ resources: resolved.resources,
120
+ }));
121
+ shas[ref] = resolved.contentSha;
122
+ }
123
+ return { instructions, shas };
124
+ }