@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.
- package/README.md +375 -15
- package/contracts/index.d.ts +1 -1
- package/contracts/index.js +1 -1
- package/contracts/turn.d.ts +77 -2
- package/contracts/turn.js +35 -2
- package/index.d.ts +6 -2
- package/index.js +6 -2
- package/loop/index.d.ts +2 -1
- package/loop/index.js +1 -1
- package/loop/tool-loop.d.ts +117 -12
- package/loop/tool-loop.js +242 -67
- package/package.json +10 -2
- package/plugins/dispatch.d.ts +130 -0
- package/plugins/dispatch.js +241 -0
- package/plugins/index.d.ts +2 -0
- package/plugins/index.js +2 -0
- package/plugins/tool-message.d.ts +23 -0
- package/plugins/tool-message.js +31 -0
- package/skills/activation.d.ts +64 -0
- package/skills/activation.js +39 -0
- package/skills/admission.d.ts +61 -0
- package/skills/admission.js +41 -0
- package/skills/catalog.d.ts +54 -0
- package/skills/catalog.js +77 -0
- package/skills/discovery.d.ts +82 -0
- package/skills/discovery.js +91 -0
- package/skills/index.d.ts +19 -0
- package/skills/index.js +19 -0
- package/skills/refs.d.ts +21 -0
- package/skills/refs.js +27 -0
- package/skills/registry.d.ts +57 -0
- package/skills/registry.js +94 -0
- package/skills/resolve.d.ts +89 -0
- package/skills/resolve.js +124 -0
- package/skills/sha.d.ts +53 -0
- package/skills/sha.js +60 -0
- package/skills/sha256.d.ts +38 -0
- package/skills/sha256.js +122 -0
- package/skills/skill-md.d.ts +73 -0
- package/skills/skill-md.js +149 -0
- package/skills/types.d.ts +174 -0
- package/skills/types.js +55 -0
- package/testing/index.d.ts +153 -0
- package/testing/index.js +188 -0
- package/tools/control-chars.d.ts +23 -0
- 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";
|
package/skills/index.js
ADDED
|
@@ -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";
|
package/skills/refs.d.ts
ADDED
|
@@ -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, "&")
|
|
21
|
+
.replace(/</g, "<")
|
|
22
|
+
.replace(/>/g, ">")
|
|
23
|
+
.replace(/"/g, """);
|
|
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
|
+
}
|