@zenera/neo 1.1.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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/dist/agent.d.ts +66 -0
  4. package/dist/agent.js +97 -0
  5. package/dist/architecture.d.ts +74 -0
  6. package/dist/architecture.js +105 -0
  7. package/dist/embedding.d.ts +60 -0
  8. package/dist/embedding.js +21 -0
  9. package/dist/embeddings/gemini.d.ts +24 -0
  10. package/dist/embeddings/gemini.js +71 -0
  11. package/dist/embeddings/index.d.ts +4 -0
  12. package/dist/embeddings/index.js +4 -0
  13. package/dist/embeddings/openai.d.ts +18 -0
  14. package/dist/embeddings/openai.js +27 -0
  15. package/dist/embeddings/openrouter.d.ts +21 -0
  16. package/dist/embeddings/openrouter.js +54 -0
  17. package/dist/events.d.ts +144 -0
  18. package/dist/events.js +110 -0
  19. package/dist/ids.d.ts +14 -0
  20. package/dist/ids.js +33 -0
  21. package/dist/index.d.ts +25 -0
  22. package/dist/index.js +25 -0
  23. package/dist/inspect/client.d.ts +2 -0
  24. package/dist/inspect/client.js +1326 -0
  25. package/dist/inspect/css.d.ts +2 -0
  26. package/dist/inspect/css.js +164 -0
  27. package/dist/inspect/index.d.ts +9 -0
  28. package/dist/inspect/index.js +12 -0
  29. package/dist/inspect/page.d.ts +3 -0
  30. package/dist/inspect/page.js +93 -0
  31. package/dist/inspect/report.d.ts +54 -0
  32. package/dist/inspect/report.js +62 -0
  33. package/dist/kernel.d.ts +178 -0
  34. package/dist/kernel.js +929 -0
  35. package/dist/memory-stores/factory.d.ts +15 -0
  36. package/dist/memory-stores/factory.js +29 -0
  37. package/dist/memory-stores/file.d.ts +37 -0
  38. package/dist/memory-stores/file.js +174 -0
  39. package/dist/memory-stores/in-memory.d.ts +17 -0
  40. package/dist/memory-stores/in-memory.js +80 -0
  41. package/dist/memory-stores/index.d.ts +4 -0
  42. package/dist/memory-stores/index.js +5 -0
  43. package/dist/memory.d.ts +115 -0
  44. package/dist/memory.js +213 -0
  45. package/dist/model.d.ts +25 -0
  46. package/dist/model.js +2 -0
  47. package/dist/models/anthropic.d.ts +36 -0
  48. package/dist/models/anthropic.js +293 -0
  49. package/dist/models/factory.d.ts +202 -0
  50. package/dist/models/factory.js +533 -0
  51. package/dist/models/gemini.d.ts +45 -0
  52. package/dist/models/gemini.js +368 -0
  53. package/dist/models/index.d.ts +7 -0
  54. package/dist/models/index.js +7 -0
  55. package/dist/models/openai-chat.d.ts +19 -0
  56. package/dist/models/openai-chat.js +203 -0
  57. package/dist/models/openai-responses.d.ts +31 -0
  58. package/dist/models/openai-responses.js +258 -0
  59. package/dist/models/openrouter.d.ts +44 -0
  60. package/dist/models/openrouter.js +230 -0
  61. package/dist/payload-stores/factory.d.ts +15 -0
  62. package/dist/payload-stores/factory.js +30 -0
  63. package/dist/payload-stores/file.d.ts +29 -0
  64. package/dist/payload-stores/file.js +112 -0
  65. package/dist/payload-stores/in-memory.d.ts +17 -0
  66. package/dist/payload-stores/in-memory.js +40 -0
  67. package/dist/payload-stores/index.d.ts +4 -0
  68. package/dist/payload-stores/index.js +5 -0
  69. package/dist/payload.d.ts +89 -0
  70. package/dist/payload.js +181 -0
  71. package/dist/project/config.d.ts +302 -0
  72. package/dist/project/config.js +290 -0
  73. package/dist/project/index.d.ts +4 -0
  74. package/dist/project/index.js +4 -0
  75. package/dist/project/load.d.ts +142 -0
  76. package/dist/project/load.js +430 -0
  77. package/dist/project/refs.d.ts +23 -0
  78. package/dist/project/refs.js +70 -0
  79. package/dist/prompt.d.ts +66 -0
  80. package/dist/prompt.js +62 -0
  81. package/dist/runner.d.ts +103 -0
  82. package/dist/runner.js +537 -0
  83. package/dist/services.d.ts +23 -0
  84. package/dist/services.js +44 -0
  85. package/dist/skill-providers/factory.d.ts +16 -0
  86. package/dist/skill-providers/factory.js +26 -0
  87. package/dist/skill-providers/file.d.ts +44 -0
  88. package/dist/skill-providers/file.js +209 -0
  89. package/dist/skill-providers/index.d.ts +4 -0
  90. package/dist/skill-providers/index.js +5 -0
  91. package/dist/skill-providers/static.d.ts +13 -0
  92. package/dist/skill-providers/static.js +47 -0
  93. package/dist/skills.d.ts +102 -0
  94. package/dist/skills.js +232 -0
  95. package/dist/state.d.ts +86 -0
  96. package/dist/state.js +49 -0
  97. package/dist/tools/exa.d.ts +23 -0
  98. package/dist/tools/exa.js +389 -0
  99. package/dist/tools/index.d.ts +4 -0
  100. package/dist/tools/index.js +6 -0
  101. package/dist/tools/sandbox.d.ts +172 -0
  102. package/dist/tools/sandbox.js +602 -0
  103. package/dist/tools/workspace.d.ts +53 -0
  104. package/dist/tools/workspace.js +1034 -0
  105. package/dist/trajectory.d.ts +213 -0
  106. package/dist/trajectory.js +232 -0
  107. package/dist/types.d.ts +195 -0
  108. package/dist/types.js +134 -0
  109. package/package.json +98 -0
@@ -0,0 +1,209 @@
1
+ import { readFile, readdir } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { selectTools } from "../types.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Filesystem-backed skill provider
6
+ // ---------------------------------------------------------------------------
7
+ const SKILL_FILE = 'SKILL.md';
8
+ /**
9
+ * Reads skills from disk. Two layouts, both discovered in one scan:
10
+ *
11
+ * ```
12
+ * <dir>/budget_travel.md flat: frontmatter + body
13
+ * <dir>/budget_travel/SKILL.md folder: sibling files become `resources`
14
+ * ```
15
+ *
16
+ * Frontmatter is a small subset of YAML — `key: value`, plus `[a, b]` lists
17
+ * for `tags` and `tools`. `name` defaults to the file or folder name and
18
+ * `description` to the first line of the body, so the minimum viable skill is
19
+ * a markdown file with no frontmatter at all.
20
+ */
21
+ export class FileSkillProvider {
22
+ id;
23
+ #dirs;
24
+ #tools = new Map();
25
+ #index;
26
+ constructor(opts) {
27
+ const o = typeof opts === 'string' ? { dir: opts } : opts;
28
+ const raw = Array.isArray(o.dir) ? o.dir : [o.dir];
29
+ this.#dirs = raw.map((d) => resolve(d));
30
+ this.id = o.id ?? 'file';
31
+ for (const t of o.tools ?? []) {
32
+ this.#tools.set(t.name, t);
33
+ }
34
+ }
35
+ /** The first configured directory (kept for backwards compatibility). */
36
+ get dir() {
37
+ return this.#dirs[0];
38
+ }
39
+ /** All configured directories. */
40
+ get dirs() {
41
+ return [...this.#dirs];
42
+ }
43
+ /** Drops the cached scan; call after editing skills on disk. */
44
+ refresh() {
45
+ this.#index = undefined;
46
+ }
47
+ async list() {
48
+ const index = await this.#scan();
49
+ return [...index.values()]
50
+ .map((e) => e.summary)
51
+ .sort((a, b) => a.name.localeCompare(b.name));
52
+ }
53
+ async search(query, limit = 8) {
54
+ const q = query.toLowerCase();
55
+ const all = await this.list();
56
+ return all
57
+ .filter((s) => s.name.toLowerCase().includes(q) ||
58
+ s.description.toLowerCase().includes(q) ||
59
+ (s.tags ?? []).some((t) => t.toLowerCase().includes(q)))
60
+ .slice(0, limit);
61
+ }
62
+ async load(name, version) {
63
+ const entry = (await this.#scan()).get(name);
64
+ if (!entry) {
65
+ throw new Error(`unknown skill: ${name}`);
66
+ }
67
+ const { version: found } = entry.summary;
68
+ if (version && found && found !== version) {
69
+ throw new Error(`skill ${name} is at ${found}, ${version} was requested`);
70
+ }
71
+ const tools = selectTools([...this.#tools.values()], entry.summary.toolNames ?? [], {
72
+ where: `skill "${name}"`,
73
+ hint: `register it on provider "${this.id}"`,
74
+ });
75
+ return {
76
+ ...entry.summary,
77
+ content: entry.content,
78
+ file: entry.file,
79
+ ...(tools.length ? { tools } : {}),
80
+ ...(entry.folder ? { resources: await readResources(entry.folder) } : {}),
81
+ };
82
+ }
83
+ tool(name) {
84
+ return this.#tools.get(name);
85
+ }
86
+ /** One scan, memoized: `list` runs on every `skill_load`. */
87
+ #scan() {
88
+ this.#index ??= this.#read();
89
+ return this.#index;
90
+ }
91
+ async #read() {
92
+ const index = new Map();
93
+ for (const dir of this.#dirs) {
94
+ let items;
95
+ try {
96
+ items = await readdir(dir, { withFileTypes: true });
97
+ }
98
+ catch (err) {
99
+ if (err.code === 'ENOENT') {
100
+ continue;
101
+ }
102
+ throw err;
103
+ }
104
+ for (const item of items) {
105
+ const folder = item.isDirectory() ? join(dir, item.name) : undefined;
106
+ const file = folder ? join(folder, SKILL_FILE) : join(dir, item.name);
107
+ if (!folder && !(item.isFile() && item.name.endsWith('.md'))) {
108
+ continue;
109
+ }
110
+ const raw = await readOptional(file);
111
+ if (raw === undefined) {
112
+ continue;
113
+ }
114
+ const base = folder ? item.name : item.name.slice(0, -'.md'.length);
115
+ const entry = parse(raw, base, file, folder);
116
+ index.set(entry.summary.name, entry);
117
+ }
118
+ }
119
+ return index;
120
+ }
121
+ }
122
+ function parse(raw, base, file, folder) {
123
+ const { data, body } = frontmatter(raw);
124
+ const tags = toList(data.tags);
125
+ const tools = toList(data.tools);
126
+ return {
127
+ file,
128
+ folder,
129
+ content: body,
130
+ summary: {
131
+ name: data.name || base,
132
+ description: data.description || firstLine(body),
133
+ ...(tags.length ? { tags } : {}),
134
+ ...(data.version ? { version: data.version } : {}),
135
+ ...(tools.length ? { toolNames: tools } : {}),
136
+ },
137
+ };
138
+ }
139
+ /** Sibling files of a `SKILL.md`, top level only, keyed by filename. */
140
+ async function readResources(folder) {
141
+ const out = {};
142
+ for (const item of await readdir(folder, { withFileTypes: true })) {
143
+ if (!item.isFile() || item.name === SKILL_FILE) {
144
+ continue;
145
+ }
146
+ const value = await readOptional(join(folder, item.name));
147
+ if (value !== undefined) {
148
+ out[item.name] = value;
149
+ }
150
+ }
151
+ return out;
152
+ }
153
+ async function readOptional(path) {
154
+ try {
155
+ return await readFile(path, 'utf8');
156
+ }
157
+ catch (err) {
158
+ const code = err.code;
159
+ // ENOENT: a folder without a SKILL.md is simply not a skill.
160
+ // EISDIR: a `<name>.md` directory is not one either.
161
+ if (code === 'ENOENT' || code === 'EISDIR') {
162
+ return undefined;
163
+ }
164
+ throw err;
165
+ }
166
+ }
167
+ /**
168
+ * Deliberately not a YAML parser: `key: value` and flow lists cover what a
169
+ * skill header needs, and a real dependency would buy nothing.
170
+ */
171
+ function frontmatter(raw) {
172
+ const m = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(raw);
173
+ if (!m) {
174
+ return { data: {}, body: raw.trim() };
175
+ }
176
+ const data = {};
177
+ for (const line of m[1].split(/\r?\n/)) {
178
+ const i = line.indexOf(':');
179
+ const key = i < 0 ? '' : line.slice(0, i).trim();
180
+ if (!key || key.startsWith('#')) {
181
+ continue;
182
+ }
183
+ data[key] = unquote(line.slice(i + 1).trim());
184
+ }
185
+ return { data, body: raw.slice(m[0].length).trim() };
186
+ }
187
+ function toList(value) {
188
+ if (!value) {
189
+ return [];
190
+ }
191
+ const inner = value.startsWith('[') && value.endsWith(']') ? value.slice(1, -1) : value;
192
+ return inner
193
+ .split(',')
194
+ .map((v) => unquote(v.trim()))
195
+ .filter(Boolean);
196
+ }
197
+ function unquote(value) {
198
+ const quoted = (value.startsWith('"') && value.endsWith('"')) ||
199
+ (value.startsWith("'") && value.endsWith("'"));
200
+ return quoted && value.length > 1 ? value.slice(1, -1) : value;
201
+ }
202
+ function firstLine(body) {
203
+ return (body
204
+ .split(/\r?\n/)
205
+ .find((l) => l.trim())
206
+ ?.replace(/^#+\s*/, '')
207
+ .trim() ?? '');
208
+ }
209
+ //# sourceMappingURL=file.js.map
@@ -0,0 +1,4 @@
1
+ export * from './factory.ts';
2
+ export * from './file.ts';
3
+ export * from './static.ts';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ // Sources for the `SkillProvider` contract in `../skills.ts`.
2
+ export * from "./factory.js";
3
+ export * from "./file.js";
4
+ export * from "./static.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ import type { Skill, SkillProvider, SkillSummary } from '../skills.ts';
2
+ import type { AnyTool } from '../types.ts';
3
+ /** Skills handed over in code, held in a map. No I/O, no catalog to scan. */
4
+ export declare class StaticSkillProvider implements SkillProvider {
5
+ #private;
6
+ readonly id: string;
7
+ constructor(skills?: Skill[], id?: string);
8
+ list(): Promise<SkillSummary[]>;
9
+ search(query: string, limit?: number): Promise<SkillSummary[]>;
10
+ load(name: string, version?: string): Promise<Skill>;
11
+ tool(name: string): AnyTool<any> | undefined;
12
+ }
13
+ //# sourceMappingURL=static.d.ts.map
@@ -0,0 +1,47 @@
1
+ /** Skills handed over in code, held in a map. No I/O, no catalog to scan. */
2
+ export class StaticSkillProvider {
3
+ id;
4
+ #skills = new Map();
5
+ #tools = new Map();
6
+ constructor(skills = [], id = 'static') {
7
+ this.id = id;
8
+ for (const s of skills) {
9
+ this.#skills.set(s.name, s);
10
+ for (const t of s.tools ?? []) {
11
+ this.#tools.set(t.name, t);
12
+ }
13
+ }
14
+ }
15
+ async list() {
16
+ return [...this.#skills.values()].map(({ name, description, tags, version, tools }) => ({
17
+ name,
18
+ description,
19
+ tags,
20
+ version,
21
+ toolNames: tools?.length ? tools.map((t) => t.name) : undefined,
22
+ }));
23
+ }
24
+ async search(query, limit = 8) {
25
+ const q = query.toLowerCase();
26
+ const all = await this.list();
27
+ return all
28
+ .filter((s) => s.name.toLowerCase().includes(q) ||
29
+ s.description.toLowerCase().includes(q) ||
30
+ (s.tags ?? []).some((t) => t.toLowerCase().includes(q)))
31
+ .slice(0, limit);
32
+ }
33
+ async load(name, version) {
34
+ const s = this.#skills.get(name);
35
+ if (!s) {
36
+ throw new Error(`unknown skill: ${name}`);
37
+ }
38
+ if (version && s.version && s.version !== version) {
39
+ throw new Error(`skill ${name} is at ${s.version}, ${version} was requested`);
40
+ }
41
+ return s;
42
+ }
43
+ tool(name) {
44
+ return this.#tools.get(name);
45
+ }
46
+ }
47
+ //# sourceMappingURL=static.js.map
@@ -0,0 +1,102 @@
1
+ import type { AgentState } from './state.ts';
2
+ import { type AnyTool } from './types.ts';
3
+ export interface SkillSummary {
4
+ name: string;
5
+ /** one line — this is what search and the prompt index see */
6
+ description: string;
7
+ tags?: string[];
8
+ version?: string;
9
+ /**
10
+ * Tools this skill unlocks, by name. On the *summary* rather than only on
11
+ * the loaded skill, because the declared tool set has to be known before
12
+ * anything is loaded — see `lockedSkillTools`. Names only: a summary comes
13
+ * from a cheap index, and code cannot live in one.
14
+ */
15
+ toolNames?: string[];
16
+ }
17
+ /**
18
+ * Note on the spec: `content` is a plain string here rather than a `Payload`,
19
+ * because a provider is a content source and must not need a payload store.
20
+ * The kernel offloads it when the activation is recorded in the trajectory.
21
+ */
22
+ export interface Skill extends SkillSummary {
23
+ content: string;
24
+ /** tools unlocked while the skill is active; the context type is the host agent's */
25
+ tools?: AnyTool<any>[];
26
+ resources?: Record<string, string>;
27
+ /** absolute path of the source file, if known (set by file-backed providers) */
28
+ file?: string;
29
+ }
30
+ export interface SkillProvider {
31
+ readonly id: string;
32
+ /** cheap index */
33
+ list(): Promise<SkillSummary[]>;
34
+ search(query: string, limit?: number): Promise<SkillSummary[]>;
35
+ /** full content */
36
+ load(name: string, version?: string): Promise<Skill>;
37
+ /**
38
+ * Resolves a tool name declared by some skill in this catalog to its
39
+ * implementation, *without* loading the skill. The declared tool set is
40
+ * built from this, so it must not depend on what is currently active.
41
+ */
42
+ tool(name: string): AnyTool<any> | undefined;
43
+ }
44
+ export interface SkillBinding {
45
+ /** SkillProvider id */
46
+ provider: string;
47
+ /**
48
+ * 'index' — names + descriptions rendered into the system prompt.
49
+ * 'search' — only a `skill_search` tool; nothing pre-rendered.
50
+ * 'none' — preloads only.
51
+ */
52
+ discovery: 'index' | 'search' | 'none';
53
+ /** always loaded at run start, before the first LLM call */
54
+ preload?: string[];
55
+ maxIndexEntries?: number;
56
+ allow?: string[] | ((s: SkillSummary) => boolean);
57
+ }
58
+ export interface SkillLoadSpec {
59
+ kind: 'skill_load';
60
+ provider: string;
61
+ skills: Skill[];
62
+ }
63
+ export declare function skillContentHash(s: Skill): string;
64
+ export declare function renderSkills(skills: Skill[]): string;
65
+ export declare function renderSkillIndex(summaries: SkillSummary[], max: number): string;
66
+ export declare function allows(binding: SkillBinding, s: SkillSummary): boolean;
67
+ /**
68
+ * Skills activated by still-projected `load_skills` nodes of the active agent.
69
+ *
70
+ * Reads the names straight off the nodes rather than re-loading the skills: an
71
+ * activation records what it activated, so the provider has nothing left to
72
+ * say, and a gate that did I/O could not be a plain predicate.
73
+ *
74
+ * Exported because `preload` needs exactly the same answer: "what is active for
75
+ * this agent, right now?" One derivation, so a preloaded skill and a
76
+ * model-loaded one are indistinguishable to the gate.
77
+ */
78
+ export declare function activeSkillNames(state: AgentState): Set<string>;
79
+ /** Thrown by a locked tool whose skill is not active. Reaches the model as an ordinary error result. */
80
+ export declare class SkillRequiredError extends Error {
81
+ readonly code = "SKILL_REQUIRED";
82
+ readonly tool: string;
83
+ /** every skill that unlocks this tool, in catalog order */
84
+ readonly candidates: SkillSummary[];
85
+ constructor(tool: string, candidates: SkillSummary[]);
86
+ }
87
+ /**
88
+ * Every tool the binding's catalog can unlock, each gated on its own skills.
89
+ * Depends on `binding` and the catalog, never on the trajectory \u2014 which is what
90
+ * keeps the declared set stable across turns.
91
+ *
92
+ * `exclude` is the host agent's own tool names: an agent-level tool of the same
93
+ * name wins and is never gated.
94
+ */
95
+ export declare function lockedSkillTools<TCtx>(binding: SkillBinding, provider: SkillProvider, exclude?: ReadonlySet<string>): Promise<AnyTool<TCtx>[]>;
96
+ /**
97
+ * `skill_load` deliberately returns a short acknowledgement: the instructions
98
+ * themselves reach the model through the `LoadSkillsNode` projection, so they
99
+ * are not duplicated in the tool result and can be compacted independently.
100
+ */
101
+ export declare function skillTools<TCtx>(binding: SkillBinding): AnyTool<TCtx>[];
102
+ //# sourceMappingURL=skills.d.ts.map
package/dist/skills.js ADDED
@@ -0,0 +1,232 @@
1
+ import { hash } from "./payload.js";
2
+ import { projected } from "./trajectory.js";
3
+ import { SKILL_LOAD_TOOL, SKILL_SEARCH_TOOL, tool, withEffects, } from "./types.js";
4
+ export function skillContentHash(s) {
5
+ return hash(s.content);
6
+ }
7
+ export function renderSkills(skills) {
8
+ return skills
9
+ .map((s) => `## Skill: ${s.name}${s.version ? ` (${s.version})` : ''}\n${s.content}`)
10
+ .join('\n\n');
11
+ }
12
+ export function renderSkillIndex(summaries, max) {
13
+ const shown = summaries.slice(0, max);
14
+ if (!shown.length) {
15
+ return '';
16
+ }
17
+ const lines = shown.map((s) => `- ${s.name}: ${s.description}`);
18
+ return (`Available skills (load with the \`${SKILL_LOAD_TOOL}\` tool before ` +
19
+ `relying on them):\n${lines.join('\n')}`);
20
+ }
21
+ export function allows(binding, s) {
22
+ if (!binding.allow) {
23
+ return true;
24
+ }
25
+ return typeof binding.allow === 'function' ? binding.allow(s) : binding.allow.includes(s.name);
26
+ }
27
+ // ---------------------------------------------------------------------------
28
+ // Locked tools
29
+ //
30
+ // A tool a skill unlocks is declared to the model from the first turn and never
31
+ // withdrawn; only its *execution* is gated. The alternative \u2014 appending the
32
+ // tool once its skill activates \u2014 mutates the `tools` array mid-conversation,
33
+ // and since tool definitions are serialized ahead of the system prompt and the
34
+ // transcript, that puts the first differing token near position 0 and costs a
35
+ // full re-read of the entire context at exactly the point where it is largest.
36
+ //
37
+ // So the gate lives in the closure, never in the schema: `name`, `description`
38
+ // and `parameters` are derived from the catalog alone and are byte-identical on
39
+ // every turn.
40
+ //
41
+ // The gate itself captures nothing. It reads the trajectory it is handed at the
42
+ // moment of the call, which makes `lockedSkillTools` a pure function of the
43
+ // binding and the catalog — the stability the cache depends on is then a
44
+ // property of the code rather than a discipline callers have to keep.
45
+ // ---------------------------------------------------------------------------
46
+ /**
47
+ * Skills activated by still-projected `load_skills` nodes of the active agent.
48
+ *
49
+ * Reads the names straight off the nodes rather than re-loading the skills: an
50
+ * activation records what it activated, so the provider has nothing left to
51
+ * say, and a gate that did I/O could not be a plain predicate.
52
+ *
53
+ * Exported because `preload` needs exactly the same answer: "what is active for
54
+ * this agent, right now?" One derivation, so a preloaded skill and a
55
+ * model-loaded one are indistinguishable to the gate.
56
+ */
57
+ export function activeSkillNames(state) {
58
+ const out = new Set();
59
+ for (const n of projected(state.trajectory)) {
60
+ if (n.type === 'load_skills' && n.agent === state.agentName) {
61
+ for (const s of n.skills) {
62
+ out.add(s.name);
63
+ }
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+ /** Thrown by a locked tool whose skill is not active. Reaches the model as an ordinary error result. */
69
+ export class SkillRequiredError extends Error {
70
+ code = 'SKILL_REQUIRED';
71
+ tool;
72
+ /** every skill that unlocks this tool, in catalog order */
73
+ candidates;
74
+ constructor(tool, candidates) {
75
+ super(skillRequiredMessage(tool, candidates));
76
+ this.name = 'SkillRequiredError';
77
+ this.tool = tool;
78
+ this.candidates = candidates;
79
+ }
80
+ }
81
+ /**
82
+ * With one candidate the model has nothing to decide, so the message is an
83
+ * instruction. With several it does, so the message states the choice and
84
+ * carries each description \u2014 under `discovery: 'search'` there is no index in
85
+ * the prompt, and this error is the only place those descriptions appear.
86
+ */
87
+ function skillRequiredMessage(tool, candidates) {
88
+ if (candidates.length === 1) {
89
+ const { name } = candidates[0];
90
+ return (`PREREQUISITE_MISSING: "${tool}" requires the "${name}" skill. ` +
91
+ `Call ${SKILL_LOAD_TOOL}({"names":["${name}"]}), then retry this call.`);
92
+ }
93
+ const lines = candidates.map((c) => ` - ${c.name}: ${c.description}`);
94
+ return (`PREREQUISITE_MISSING: "${tool}" requires at least one of these skills to be ` +
95
+ `loaded first:\n${lines.join('\n')}\n` +
96
+ 'Decide which one applies to this call. If the case spans more than one, load ' +
97
+ `them together. Then call ${SKILL_LOAD_TOOL}({"names":[...]}) and retry this call.`);
98
+ }
99
+ function lockedDescription(inner, candidates) {
100
+ const names = candidates.map((c) => `"${c.name}"`);
101
+ const note = names.length === 1
102
+ ? `Unlocked by the ${names[0]} skill; load it with \`${SKILL_LOAD_TOOL}\` first.`
103
+ : `Unlocked by any of: ${names.join(', ')}; ` +
104
+ `load whichever applies with \`${SKILL_LOAD_TOOL}\` first.`;
105
+ return inner.description ? `${inner.description} (${note})` : note;
106
+ }
107
+ function lock(inner, candidates) {
108
+ return {
109
+ name: inner.name,
110
+ description: lockedDescription(inner, candidates),
111
+ parameters: inner.parameters,
112
+ // The three fields above are all the model is sent, and none of them can
113
+ // see the run. The state enters here and only here, per call.
114
+ execute: (args, tc) => {
115
+ const active = activeSkillNames(tc.state);
116
+ if (!candidates.some((c) => active.has(c.name))) {
117
+ throw new SkillRequiredError(inner.name, candidates);
118
+ }
119
+ return inner.execute(args, tc);
120
+ },
121
+ };
122
+ }
123
+ /**
124
+ * Every tool the binding's catalog can unlock, each gated on its own skills.
125
+ * Depends on `binding` and the catalog, never on the trajectory \u2014 which is what
126
+ * keeps the declared set stable across turns.
127
+ *
128
+ * `exclude` is the host agent's own tool names: an agent-level tool of the same
129
+ * name wins and is never gated.
130
+ */
131
+ export async function lockedSkillTools(binding, provider, exclude) {
132
+ const index = (await provider.list())
133
+ .filter((s) => allows(binding, s))
134
+ // Not every provider promises an order; the declared tool list must not
135
+ // depend on one that varies.
136
+ .sort((a, b) => a.name.localeCompare(b.name));
137
+ // name -> every skill declaring it. Two owners need no tie-break: either one
138
+ // being active is enough, and both are offered when neither is.
139
+ const owners = new Map();
140
+ for (const summary of index) {
141
+ for (const name of summary.toolNames ?? []) {
142
+ if (exclude?.has(name)) {
143
+ continue;
144
+ }
145
+ const known = owners.get(name);
146
+ if (known) {
147
+ known.push(summary);
148
+ }
149
+ else {
150
+ owners.set(name, [summary]);
151
+ }
152
+ }
153
+ }
154
+ const out = [];
155
+ for (const [name, candidates] of owners) {
156
+ const inner = provider.tool(name);
157
+ if (!inner) {
158
+ // A catalog naming a tool nobody implements is a configuration
159
+ // error, but throwing here would fail every turn of every run that
160
+ // touches the provider, which is worse than leaving it undeclared.
161
+ console.warn(`skill provider "${provider.id}": no implementation for tool "${name}" ` +
162
+ `(declared by ${candidates.map((c) => c.name).join(', ')})`);
163
+ continue;
164
+ }
165
+ out.push(lock(inner, candidates));
166
+ }
167
+ return out;
168
+ }
169
+ // ---------------------------------------------------------------------------
170
+ // Built-in skill tools
171
+ // ---------------------------------------------------------------------------
172
+ /**
173
+ * `skill_load` deliberately returns a short acknowledgement: the instructions
174
+ * themselves reach the model through the `LoadSkillsNode` projection, so they
175
+ * are not duplicated in the tool result and can be compacted independently.
176
+ */
177
+ export function skillTools(binding) {
178
+ const tools = [
179
+ tool({
180
+ name: SKILL_LOAD_TOOL,
181
+ description: 'Load skill instructions (and any tools they unlock) into the context.',
182
+ parameters: {
183
+ type: 'object',
184
+ properties: {
185
+ names: { type: 'array', items: { type: 'string' }, minItems: 1 },
186
+ },
187
+ required: ['names'],
188
+ additionalProperties: false,
189
+ },
190
+ execute: async (args, tc) => {
191
+ const provider = tc.services.skillProvider(binding.provider);
192
+ const index = await provider.list();
193
+ const loaded = [];
194
+ for (const name of args.names) {
195
+ const summary = index.find((s) => s.name === name);
196
+ if (summary && !allows(binding, summary)) {
197
+ throw new Error(`skill "${name}" is not available to this agent`);
198
+ }
199
+ loaded.push(await provider.load(name, summary?.version));
200
+ }
201
+ // Two skills may declare the same tool; it is unlocked once.
202
+ const unlocked = [
203
+ ...new Set(loaded.flatMap((s) => (s.tools ?? []).map((t) => t.name))),
204
+ ];
205
+ return withEffects(`loaded: ${loaded.map((s) => s.name).join(', ')}` +
206
+ (unlocked.length ? `; tools unlocked: ${unlocked.join(', ')}` : ''), {
207
+ kind: 'skill_load',
208
+ spec: { kind: 'skill_load', provider: provider.id, skills: loaded },
209
+ });
210
+ },
211
+ }),
212
+ ];
213
+ if (binding.discovery === 'search') {
214
+ tools.push(tool({
215
+ name: SKILL_SEARCH_TOOL,
216
+ description: 'Find skills by topic before loading them.',
217
+ parameters: {
218
+ type: 'object',
219
+ properties: { query: { type: 'string' }, limit: { type: 'integer' } },
220
+ required: ['query'],
221
+ additionalProperties: false,
222
+ },
223
+ execute: async (args, tc) => {
224
+ const provider = tc.services.skillProvider(binding.provider);
225
+ const found = await provider.search(args.query, args.limit);
226
+ return found.filter((s) => allows(binding, s));
227
+ },
228
+ }));
229
+ }
230
+ return tools;
231
+ }
232
+ //# sourceMappingURL=skills.js.map