@zenera/neo 1.1.16 → 1.1.18
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 +1 -1
- package/dist/embeddings/limiter.js +20 -1
- package/dist/fork.d.ts +24 -0
- package/dist/fork.js +137 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/kernel.js +23 -88
- package/dist/memory/store.js +2 -2
- package/dist/project/config.js +5 -3
- package/dist/project/load.d.ts +16 -3
- package/dist/project/load.js +72 -26
- package/dist/prompt.d.ts +10 -1
- package/dist/prompt.js +20 -3
- package/dist/skill-providers/file.js +5 -3
- package/dist/skills.d.ts +7 -0
- package/dist/skills.js +11 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ of surfacing three turns into a run as a confused model.
|
|
|
166
166
|
[docs/projects.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/projects.md) ·
|
|
167
167
|
[docs/agents-yaml.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/agents-yaml.md)
|
|
168
168
|
· and
|
|
169
|
-
[`examples/`](https://github.com/andreyryabov/ZeneraNeo/tree/main/examples) for
|
|
169
|
+
[`examples/sdk/`](https://github.com/andreyryabov/ZeneraNeo/tree/main/examples/sdk) for
|
|
170
170
|
eight worked demos, from a single agent to a project loaded entirely from a
|
|
171
171
|
folder.
|
|
172
172
|
|
|
@@ -99,10 +99,12 @@ export class RateLimiter {
|
|
|
99
99
|
if (at >= 0) {
|
|
100
100
|
this.#waiting.splice(at, 1);
|
|
101
101
|
}
|
|
102
|
+
this.#hold();
|
|
102
103
|
reject(signal?.reason);
|
|
103
104
|
};
|
|
104
105
|
signal?.addEventListener('abort', abort, { once: true });
|
|
105
106
|
this.#waiting.push(waiter);
|
|
107
|
+
this.#hold();
|
|
106
108
|
});
|
|
107
109
|
}
|
|
108
110
|
#free() {
|
|
@@ -121,6 +123,7 @@ export class RateLimiter {
|
|
|
121
123
|
this.#active++;
|
|
122
124
|
this.#waiting.shift().grant(this.#epoch);
|
|
123
125
|
}
|
|
126
|
+
this.#hold();
|
|
124
127
|
}
|
|
125
128
|
#succeeded() {
|
|
126
129
|
this.#penalty = this.#initial;
|
|
@@ -168,7 +171,23 @@ export class RateLimiter {
|
|
|
168
171
|
return;
|
|
169
172
|
}
|
|
170
173
|
this.#timer = setTimeout(() => this.#arm(), remaining);
|
|
171
|
-
this.#
|
|
174
|
+
this.#hold();
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* A parked task is a bare promise, which keeps nothing alive, and while the
|
|
178
|
+
* pause is in force this timer is the only thing that can ever wake it — so
|
|
179
|
+
* it has to hold the event loop open, or the last in-flight request settling
|
|
180
|
+
* empties the loop and Node exits 13 with the whole queue unsettled. With
|
|
181
|
+
* nobody waiting it must NOT hold it, or a run that ended on a refusal sits
|
|
182
|
+
* out the rest of the backoff before the process can exit.
|
|
183
|
+
*/
|
|
184
|
+
#hold() {
|
|
185
|
+
if (this.#waiting.length > 0) {
|
|
186
|
+
this.#timer?.ref?.();
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
this.#timer?.unref?.();
|
|
190
|
+
}
|
|
172
191
|
}
|
|
173
192
|
}
|
|
174
193
|
const clamp = (n, low, high) => Math.min(high, Math.max(low, n));
|
package/dist/fork.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { JsonSchema } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Deliberately not a function of the agent list or the branch cap: both are
|
|
4
|
+
* already on the tool schema, where the provider enforces them during decoding,
|
|
5
|
+
* and naming them twice buys nothing but two places to drift.
|
|
6
|
+
*/
|
|
7
|
+
export declare function forkInstructions(): string;
|
|
8
|
+
/**
|
|
9
|
+
* Mechanics only — the policy above is in the system prompt of every agent that
|
|
10
|
+
* can call this, so repeating it here would cost tokens in two places at once.
|
|
11
|
+
*/
|
|
12
|
+
export declare const FORK_DESCRIPTION: string;
|
|
13
|
+
/**
|
|
14
|
+
* Derived from the registry rather than a constant, because the one thing a
|
|
15
|
+
* weak model reliably gets wrong here is inventing an agent name. An `enum` of
|
|
16
|
+
* the agents that actually exist is a constraint the provider enforces during
|
|
17
|
+
* decoding, which no amount of prose in the description can match.
|
|
18
|
+
*
|
|
19
|
+
* `minItems`/`maxItems` are stated too, but they are advisory — most providers
|
|
20
|
+
* ignore them under strict decoding — so `parseForkArgs` and `forkProblem`
|
|
21
|
+
* still check both, and their messages are written to be read by the model.
|
|
22
|
+
*/
|
|
23
|
+
export declare function forkParameters(agents: string[], self: string, maxBranches?: number): JsonSchema;
|
|
24
|
+
//# sourceMappingURL=fork.d.ts.map
|
package/dist/fork.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Teaching the model to fan out and to delegate
|
|
3
|
+
//
|
|
4
|
+
// The tool schema says what the arguments are; none of it says what survives a
|
|
5
|
+
// join, that a branch cannot be corrected once it starts, or which `context`
|
|
6
|
+
// mode fits which shape of work. That is policy, it is the same for every
|
|
7
|
+
// agent, and it belongs in the system prompt — the only place it can be stated
|
|
8
|
+
// once per run instead of once per call.
|
|
9
|
+
//
|
|
10
|
+
// It goes in through `composePrompt`'s `derived` array, so it sits in the
|
|
11
|
+
// cacheable prefix and is recorded on the `system_prompt` node like every other
|
|
12
|
+
// part of what the model read. The text is gated on the agent holding a fork
|
|
13
|
+
// binding *and* being below the depth cap — the same condition that decides
|
|
14
|
+
// whether the tool is offered at all, because a rule about a tool the model
|
|
15
|
+
// does not have is a rule it can only be confused by.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Deliberately not a function of the agent list or the branch cap: both are
|
|
19
|
+
* already on the tool schema, where the provider enforces them during decoding,
|
|
20
|
+
* and naming them twice buys nothing but two places to drift.
|
|
21
|
+
*/
|
|
22
|
+
export function forkInstructions() {
|
|
23
|
+
return FORKING;
|
|
24
|
+
}
|
|
25
|
+
const FORKING = `Forking runs work in separate conversations and brings back only the answers.
|
|
26
|
+
|
|
27
|
+
A branch is a run of its own: its own conversation, its own tool calls, its own
|
|
28
|
+
reasoning. None of that returns. What returns is each branch's final answer, as
|
|
29
|
+
the result of your \`fork\` call — so whatever you will need afterwards has to be
|
|
30
|
+
*in* that answer, and the branch only knows it if your instructions say so. Work
|
|
31
|
+
whose value is the trace rather than the conclusion should not be forked at all.
|
|
32
|
+
|
|
33
|
+
The instructions you write are the entire assignment. A branch cannot ask you a
|
|
34
|
+
question, cannot see what its siblings are doing, and cannot be corrected once
|
|
35
|
+
it starts. Say what it must do, what it must leave alone, and the exact shape of
|
|
36
|
+
what it must hand back.
|
|
37
|
+
|
|
38
|
+
**One branch is delegation**: the work happens elsewhere and you get the
|
|
39
|
+
conclusion instead of the transcript. Use it when another agent is better suited
|
|
40
|
+
to the job, or when the job would otherwise fill this conversation with material
|
|
41
|
+
you have no use for afterwards — a long file survey, a noisy build loop, an
|
|
42
|
+
exploration down a path that may go nowhere. **Several branches are a fan-out**:
|
|
43
|
+
independent parts of one task, worked at the same time, merged by you. It is the
|
|
44
|
+
same call either way; the number of branches is the only difference.
|
|
45
|
+
|
|
46
|
+
Do not fork a sequence. Branches run at once and can exchange nothing, so a step
|
|
47
|
+
that needs the step before it has to stay in this conversation.
|
|
48
|
+
|
|
49
|
+
\`context\` decides what each branch starts from, and you choose it per call:
|
|
50
|
+
- \`inherit\` — everything said here so far. For work that only makes sense
|
|
51
|
+
against the case as it stands.
|
|
52
|
+
- \`compact\` — the same, without the tool traffic: what was decided, not how it
|
|
53
|
+
was found out. The usual choice for a wide fan-out.
|
|
54
|
+
- \`none\` — nothing but its own instructions. The cheapest, and the honest one
|
|
55
|
+
when the assignment is self-contained.
|
|
56
|
+
|
|
57
|
+
A branch can fail, or answer badly. Its row says which, and what the final
|
|
58
|
+
answer is remains your decision, not the join's.`;
|
|
59
|
+
/**
|
|
60
|
+
* Mechanics only — the policy above is in the system prompt of every agent that
|
|
61
|
+
* can call this, so repeating it here would cost tokens in two places at once.
|
|
62
|
+
*/
|
|
63
|
+
export const FORK_DESCRIPTION = [
|
|
64
|
+
'Run work in separate conversations and get back only the answers.',
|
|
65
|
+
'',
|
|
66
|
+
'Each branch needs a unique name and complete, self-contained instructions: it runs on ' +
|
|
67
|
+
'its own, cannot ask questions, and cannot see the other branches. One branch delegates ' +
|
|
68
|
+
'a job; several run in parallel and rejoin together.',
|
|
69
|
+
'',
|
|
70
|
+
'The branches\u2019 answers come back as the result of this call, and what the final answer ' +
|
|
71
|
+
'is remains yours to decide.',
|
|
72
|
+
].join('\n');
|
|
73
|
+
/**
|
|
74
|
+
* Derived from the registry rather than a constant, because the one thing a
|
|
75
|
+
* weak model reliably gets wrong here is inventing an agent name. An `enum` of
|
|
76
|
+
* the agents that actually exist is a constraint the provider enforces during
|
|
77
|
+
* decoding, which no amount of prose in the description can match.
|
|
78
|
+
*
|
|
79
|
+
* `minItems`/`maxItems` are stated too, but they are advisory — most providers
|
|
80
|
+
* ignore them under strict decoding — so `parseForkArgs` and `forkProblem`
|
|
81
|
+
* still check both, and their messages are written to be read by the model.
|
|
82
|
+
*/
|
|
83
|
+
export function forkParameters(agents, self, maxBranches) {
|
|
84
|
+
const max = maxBranches ?? 0;
|
|
85
|
+
// When the author's list excludes the current agent there is no sane
|
|
86
|
+
// default, so the model has to name one.
|
|
87
|
+
const selfAllowed = agents.includes(self);
|
|
88
|
+
return {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
branches: {
|
|
92
|
+
type: 'array',
|
|
93
|
+
minItems: 1,
|
|
94
|
+
...(max >= 1 ? { maxItems: max } : {}),
|
|
95
|
+
description: `One branch per job that can be carried out on its own${max >= 1 ? `, at most ${max}` : ''}. ` +
|
|
96
|
+
'One delegates that job elsewhere; several work independent parts of the ' +
|
|
97
|
+
'task at the same time.',
|
|
98
|
+
items: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
name: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'Short, unique label for this branch, e.g. "eu-tariffs".',
|
|
104
|
+
},
|
|
105
|
+
instructions: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'The complete assignment for this branch. It runs in a separate ' +
|
|
108
|
+
'conversation and cannot ask questions, so state everything it ' +
|
|
109
|
+
'needs and exactly what it must return.',
|
|
110
|
+
},
|
|
111
|
+
agent: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
enum: agents,
|
|
114
|
+
description: 'Which agent runs this branch. Must be one of: ' +
|
|
115
|
+
agents.join(', ') +
|
|
116
|
+
(selfAllowed ? `. Omit to use "${self}".` : '.'),
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
required: selfAllowed
|
|
120
|
+
? ['name', 'instructions']
|
|
121
|
+
: ['name', 'instructions', 'agent'],
|
|
122
|
+
additionalProperties: false,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
context: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
enum: ['inherit', 'compact', 'none'],
|
|
128
|
+
description: 'How much of this conversation each branch starts with: "inherit" (all of ' +
|
|
129
|
+
'it, the default), "compact" (messages only, no tool traffic), "none" ' +
|
|
130
|
+
'(nothing but its own instructions).',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
required: ['branches'],
|
|
134
|
+
additionalProperties: false,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=fork.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from './embedding.ts';
|
|
|
4
4
|
export * from './embeddings/index.ts';
|
|
5
5
|
export * from './events.ts';
|
|
6
6
|
export * from './failure.ts';
|
|
7
|
+
export * from './fork.ts';
|
|
7
8
|
export * from './ids.ts';
|
|
8
9
|
export * from './inspect/index.ts';
|
|
9
10
|
export * as Kernel from './kernel.ts';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./embedding.js";
|
|
|
4
4
|
export * from "./embeddings/index.js";
|
|
5
5
|
export * from "./events.js";
|
|
6
6
|
export * from "./failure.js";
|
|
7
|
+
export * from "./fork.js";
|
|
7
8
|
export * from "./ids.js";
|
|
8
9
|
export * from "./inspect/index.js";
|
|
9
10
|
export * as Kernel from "./kernel.js";
|
package/dist/kernel.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { Agent, AgentRegistry, handoffTarget, handoffTool } from "./agent.js";
|
|
3
|
+
import { FORK_DESCRIPTION, forkInstructions, forkParameters } from "./fork.js";
|
|
3
4
|
import { systemClock } from "./ids.js";
|
|
4
5
|
import { memoryInstructions, renderPreferences } from "./memory/instructions.js";
|
|
5
6
|
import { memoryTools } from "./memory/tools.js";
|
|
@@ -193,82 +194,10 @@ function forkAgents(state, reg) {
|
|
|
193
194
|
const known = allowed.filter((n) => reg.find(n));
|
|
194
195
|
return known.length ? known : [state.agentName];
|
|
195
196
|
}
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
* agents that actually exist is a constraint the provider enforces during
|
|
200
|
-
* decoding, which no amount of prose in the description can match.
|
|
201
|
-
*
|
|
202
|
-
* `minItems`/`maxItems` are stated too, but they are advisory — most providers
|
|
203
|
-
* ignore them under strict decoding — so `parseForkArgs` and `forkProblem`
|
|
204
|
-
* still check both, and their messages are written to be read by the model.
|
|
205
|
-
*/
|
|
206
|
-
function forkParameters(agents, self, maxBranches) {
|
|
207
|
-
const max = maxBranches ?? 0;
|
|
208
|
-
// When the author's list excludes the current agent there is no sane
|
|
209
|
-
// default, so the model has to name one.
|
|
210
|
-
const selfAllowed = agents.includes(self);
|
|
211
|
-
return {
|
|
212
|
-
type: 'object',
|
|
213
|
-
properties: {
|
|
214
|
-
branches: {
|
|
215
|
-
type: 'array',
|
|
216
|
-
minItems: 2,
|
|
217
|
-
...(max >= 2 ? { maxItems: max } : {}),
|
|
218
|
-
description: `At least two branches${max >= 2 ? `, at most ${max}` : ''}. ` +
|
|
219
|
-
'One branch is never valid: if the work does not split, do it yourself ' +
|
|
220
|
-
'instead of calling this tool.',
|
|
221
|
-
items: {
|
|
222
|
-
type: 'object',
|
|
223
|
-
properties: {
|
|
224
|
-
name: {
|
|
225
|
-
type: 'string',
|
|
226
|
-
description: 'Short, unique label for this branch, e.g. "eu-tariffs".',
|
|
227
|
-
},
|
|
228
|
-
instructions: {
|
|
229
|
-
type: 'string',
|
|
230
|
-
description: 'The complete assignment for this branch. It runs in a separate ' +
|
|
231
|
-
'conversation and cannot ask questions, so state everything it ' +
|
|
232
|
-
'needs and exactly what it must return.',
|
|
233
|
-
},
|
|
234
|
-
agent: {
|
|
235
|
-
type: 'string',
|
|
236
|
-
enum: agents,
|
|
237
|
-
description: 'Which agent runs this branch. Must be one of: ' +
|
|
238
|
-
agents.join(', ') +
|
|
239
|
-
(selfAllowed ? `. Omit to use "${self}".` : '.'),
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
required: selfAllowed
|
|
243
|
-
? ['name', 'instructions']
|
|
244
|
-
: ['name', 'instructions', 'agent'],
|
|
245
|
-
additionalProperties: false,
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
context: {
|
|
249
|
-
type: 'string',
|
|
250
|
-
enum: ['inherit', 'compact', 'none'],
|
|
251
|
-
description: 'How much of this conversation each branch starts with: "inherit" (all of ' +
|
|
252
|
-
'it, the default), "compact" (messages only, no tool traffic), "none" ' +
|
|
253
|
-
'(nothing but its own instructions).',
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
required: ['branches'],
|
|
257
|
-
additionalProperties: false,
|
|
258
|
-
};
|
|
197
|
+
/** One condition for the tool and for the prompt block that explains it. */
|
|
198
|
+
function canFork(agent, state) {
|
|
199
|
+
return Boolean(agent.fork) && state.spec.forkDepth < state.spec.maxForkDepth;
|
|
259
200
|
}
|
|
260
|
-
const FORK_DESCRIPTION = [
|
|
261
|
-
'Split the work into independent branches that run in parallel and rejoin into one result.',
|
|
262
|
-
'',
|
|
263
|
-
'Call it when two or more parts of the task can be worked on without seeing each ' +
|
|
264
|
-
"other's findings — separate documents, regions, candidates, hypotheses. Do not call it " +
|
|
265
|
-
'to break one line of reasoning into steps: branches cannot talk to each other, and a ' +
|
|
266
|
-
"step that needs the previous step's answer must stay in this conversation.",
|
|
267
|
-
'',
|
|
268
|
-
'Every call needs at least two branches, each with a unique name and self-contained ' +
|
|
269
|
-
'instructions. Their answers come back to you as the result of this call, and you ' +
|
|
270
|
-
'decide what the final answer is.',
|
|
271
|
-
].join('\n');
|
|
272
201
|
/**
|
|
273
202
|
* tools(state) = agent tools + handoffs + memory + skills + fork +
|
|
274
203
|
* final_output. Computed from the state so a rehydrated run offers exactly the
|
|
@@ -298,11 +227,11 @@ export async function resolveTools(state, reg, env) {
|
|
|
298
227
|
const own = new Set(agent.tools.map((t) => t.name));
|
|
299
228
|
tools.push(...(await lockedSkillTools(binding, provider, own)));
|
|
300
229
|
}
|
|
301
|
-
if (agent
|
|
230
|
+
if (canFork(agent, state)) {
|
|
302
231
|
tools.push({
|
|
303
232
|
name: FORK_TOOL,
|
|
304
233
|
description: FORK_DESCRIPTION,
|
|
305
|
-
parameters: forkParameters(forkAgents(state, reg), state.agentName, agent.fork
|
|
234
|
+
parameters: forkParameters(forkAgents(state, reg), state.agentName, agent.fork?.maxBranches),
|
|
306
235
|
// Never executed as a tool: the runner drives branches and the join
|
|
307
236
|
// becomes the tool result.
|
|
308
237
|
execute: () => 'fork scheduled',
|
|
@@ -355,6 +284,9 @@ async function derivedPrompt(agent, state, env) {
|
|
|
355
284
|
out.push(rendered);
|
|
356
285
|
}
|
|
357
286
|
}
|
|
287
|
+
if (canFork(agent, state)) {
|
|
288
|
+
out.push(forkInstructions());
|
|
289
|
+
}
|
|
358
290
|
if (state.spec.outputSchema) {
|
|
359
291
|
out.push(FINAL_OUTPUT_INSTRUCTIONS);
|
|
360
292
|
}
|
|
@@ -444,12 +376,7 @@ function parseForkArgs(raw) {
|
|
|
444
376
|
// the model as the tool result, and it is the only chance it gets to work
|
|
445
377
|
// out what to do differently.
|
|
446
378
|
if (!Array.isArray(branches) || branches.length === 0) {
|
|
447
|
-
return 'the "branches" argument is missing or empty; it must be an array of at least
|
|
448
|
-
}
|
|
449
|
-
if (branches.length < 2) {
|
|
450
|
-
return ('a fork needs at least two branches, and this call has one. Either add the other ' +
|
|
451
|
-
'independent branches, or do not fork at all and carry out the work in this ' +
|
|
452
|
-
'conversation.');
|
|
379
|
+
return 'the "branches" argument is missing or empty; it must be an array of at least one branch';
|
|
453
380
|
}
|
|
454
381
|
const parsed = [];
|
|
455
382
|
for (const b of branches) {
|
|
@@ -847,6 +774,9 @@ export async function createChildState(state, forkId, branch, env) {
|
|
|
847
774
|
* what happens to what it produces, exist nowhere else. Without them a branch
|
|
848
775
|
* re-derives work a sibling already owns and writes an answer shaped for a user
|
|
849
776
|
* rather than for a merge.
|
|
777
|
+
*
|
|
778
|
+
* A lone branch has no siblings to be told about, and the thing it does need to
|
|
779
|
+
* know is the opposite one: nobody else is working on any of this.
|
|
850
780
|
*/
|
|
851
781
|
function branchBrief(fork, plan, instructions) {
|
|
852
782
|
const siblings = fork.branches.filter((b) => b.name !== plan.name).map((b) => `"${b.name}"`);
|
|
@@ -855,11 +785,16 @@ function branchBrief(fork, plan, instructions) {
|
|
|
855
785
|
'',
|
|
856
786
|
instructions,
|
|
857
787
|
'',
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
788
|
+
siblings.length
|
|
789
|
+
? `Running in parallel, on the assignments the call above gave them: ${siblings.join(', ')}. ` +
|
|
790
|
+
'You cannot see their work and they cannot see yours, so anything you do on their ' +
|
|
791
|
+
'part is duplicated effort, and anything you leave to them will be there. This branch ' +
|
|
792
|
+
'ends with your final answer; that answer alone rejoins the conversation above, ' +
|
|
793
|
+
'merged with theirs.'
|
|
794
|
+
: 'No other branch was opened, so the whole assignment is yours and nothing you ' +
|
|
795
|
+
'leave undone will be picked up elsewhere. This branch ends with your final ' +
|
|
796
|
+
'answer; that answer alone rejoins the conversation above, and none of the steps ' +
|
|
797
|
+
'you take to reach it go with it.',
|
|
863
798
|
].join('\n');
|
|
864
799
|
}
|
|
865
800
|
/** The parent history a branch starts from, as decided by `contextMode`. */
|
package/dist/memory/store.js
CHANGED
|
@@ -51,7 +51,7 @@ export class MemoryStore {
|
|
|
51
51
|
const b = opts.embedding;
|
|
52
52
|
if (a.model !== b.model || a.dimensions !== b.dimensions) {
|
|
53
53
|
throw new MemoryError(`this memory was embedded with ${a.model} (${a.dimensions}d), ` +
|
|
54
|
-
`but ${b.model} (${b.dimensions}d) is configured`, '
|
|
54
|
+
`but ${b.model} (${b.dimensions}d) is configured`, 'restore the original model, or start a new memory');
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
const graph = manifest
|
|
@@ -79,7 +79,7 @@ export class MemoryStore {
|
|
|
79
79
|
if (held) {
|
|
80
80
|
if (held.model !== embedding.model || held.dimensions !== embedding.dimensions) {
|
|
81
81
|
throw new MemoryError(`this memory was embedded with ${held.model} (${held.dimensions}d), ` +
|
|
82
|
-
`but ${embedding.model} (${embedding.dimensions}d) is configured`, '
|
|
82
|
+
`but ${embedding.model} (${embedding.dimensions}d) is configured`, 'restore the original model, or start a new memory');
|
|
83
83
|
}
|
|
84
84
|
return;
|
|
85
85
|
}
|
package/dist/project/config.js
CHANGED
|
@@ -157,15 +157,17 @@ const skillsBinding = z
|
|
|
157
157
|
* `fork:` is never offered the tool.
|
|
158
158
|
*
|
|
159
159
|
* Both limits are stated as errors rather than as values that get clamped
|
|
160
|
-
* later: `maxBranches:
|
|
160
|
+
* later: `maxBranches: 0` would make every call the model could write fail, and
|
|
161
161
|
* `agents: []` reads as "no agents" while actually meaning "only itself".
|
|
162
|
+
* `maxBranches: 1` is not a contradiction — it says this agent may delegate a
|
|
163
|
+
* job but may not fan out.
|
|
162
164
|
*/
|
|
163
165
|
const forkBinding = z
|
|
164
166
|
.object({
|
|
165
167
|
/** agents a branch may run; absent means any registered agent */
|
|
166
168
|
agents: z.array(name).min(1).optional(),
|
|
167
|
-
/** a
|
|
168
|
-
maxBranches: z.int().min(
|
|
169
|
+
/** a cap of 1 allows delegation only; 0 would refuse every possible call */
|
|
170
|
+
maxBranches: z.int().min(1).optional(),
|
|
169
171
|
})
|
|
170
172
|
.strict();
|
|
171
173
|
/**
|
package/dist/project/load.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { RunStream } from '../events.ts';
|
|
|
4
4
|
import { MemoryIndex } from '../memory/index.ts';
|
|
5
5
|
import { ModelRegistry, type EmbeddingRef, type ModelRef, type ProviderSpec } from '../models/factory.ts';
|
|
6
6
|
import type { PayloadStore } from '../payload.ts';
|
|
7
|
-
import { AgentRunner, type
|
|
7
|
+
import { AgentRunner, type RunnerOptions, type RunOptions } from '../runner.ts';
|
|
8
8
|
import { type SkillDir } from '../skill-providers/file.ts';
|
|
9
9
|
import type { SkillProvider } from '../skills.ts';
|
|
10
10
|
import { type AnyTool, type Input } from '../types.ts';
|
|
@@ -13,8 +13,9 @@ import { type ProjectConfig } from './config.ts';
|
|
|
13
13
|
* A folder is an agentic system:
|
|
14
14
|
*
|
|
15
15
|
* ```
|
|
16
|
-
* INSTRUCTIONS.md house rules, prepended to every agent
|
|
17
16
|
* agents.yaml who exists, what they may reach for
|
|
17
|
+
* agents/instructions.md house rules, prepended to every agent
|
|
18
|
+
* agents/<topic>-instructions.md more of them, added as the project grows
|
|
18
19
|
* agents/prompts/<role>.md one agent's own instructions
|
|
19
20
|
* agents/skills/<name>/SKILL.md
|
|
20
21
|
* assets/ reference material, mounted read-only at /assets
|
|
@@ -55,6 +56,13 @@ export interface ProjectOptions<TCtx = unknown> {
|
|
|
55
56
|
*/
|
|
56
57
|
registry?: ModelRegistry;
|
|
57
58
|
memory?: MemoryIndex;
|
|
59
|
+
/**
|
|
60
|
+
* Another directory to keep the memory graph in, relative to the project
|
|
61
|
+
* root when it is not absolute. Whoever sets it must mount that directory
|
|
62
|
+
* as `/memory` too, or the agent reads its remembered files from one graph
|
|
63
|
+
* and writes to another.
|
|
64
|
+
*/
|
|
65
|
+
memoryDir?: string;
|
|
58
66
|
/** turns an agent-visible path into a host path, so files can be remembered */
|
|
59
67
|
resolveFile?: (path: string) => string;
|
|
60
68
|
payloads?: PayloadStore;
|
|
@@ -165,8 +173,13 @@ export declare function projectRegistry<TCtx>(config: ProjectConfig, opts?: Proj
|
|
|
165
173
|
* with the front end, which has to mount the same directory into the sandbox:
|
|
166
174
|
* two answers to this question would mean `/memory` pointing somewhere the
|
|
167
175
|
* graph is not.
|
|
176
|
+
*
|
|
177
|
+
* `override` is the host pointing the run at another graph — `zen run
|
|
178
|
+
* --memory`. It answers for a project that declares no memory at all, because
|
|
179
|
+
* naming a directory is the declaration; the agents bound to it are still the
|
|
180
|
+
* ones that said `memory: true`.
|
|
168
181
|
*/
|
|
169
|
-
export declare function memoryDir(root: string, config: ProjectConfig): string | undefined;
|
|
182
|
+
export declare function memoryDir(root: string, config: ProjectConfig, override?: string): string | undefined;
|
|
170
183
|
/**
|
|
171
184
|
* Where each skill directory appears in the agent's namespace.
|
|
172
185
|
*
|
package/dist/project/load.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, join, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { Agent, AgentRegistry } from "../agent.js";
|
|
4
4
|
import { RunStream } from "../events.js";
|
|
5
5
|
import { MemoryIndex } from "../memory/index.js";
|
|
@@ -11,6 +11,7 @@ import { FileSkillProvider } from "../skill-providers/file.js";
|
|
|
11
11
|
import { selectTools } from "../types.js";
|
|
12
12
|
import { parseConfig } from "./config.js";
|
|
13
13
|
import { projectDir, projectFile, projectRoot } from "./refs.js";
|
|
14
|
+
const AGENTS_DIR = 'agents';
|
|
14
15
|
const PROMPTS_DIR = 'agents/prompts';
|
|
15
16
|
const SKILLS_DIR = 'agents/skills';
|
|
16
17
|
/** Reference material, by convention, when `assets:` says nothing. */
|
|
@@ -19,7 +20,11 @@ const CONFIG_NAMES = ['agents.yaml', 'agents.yml', 'agents/agents.yaml', 'agents
|
|
|
19
20
|
// Deliberately not `AGENTS.md`: every coding assistant now reads that name out
|
|
20
21
|
// of an open folder, and `zen open` opens exactly this directory. The house
|
|
21
22
|
// rules here address *this project's* agents, not the editor's.
|
|
22
|
-
const HOUSE_RULES = '
|
|
23
|
+
const HOUSE_RULES = 'agents/instructions.md';
|
|
24
|
+
/** Every other `agents/<topic>-instructions.md`, in filename order. */
|
|
25
|
+
const INSTRUCTIONS_SUFFIX = '-instructions.md';
|
|
26
|
+
/** Where the house rules used to live. Still read, so an old project still runs. */
|
|
27
|
+
const LEGACY_HOUSE_RULES = 'INSTRUCTIONS.md';
|
|
23
28
|
/**
|
|
24
29
|
* Finds and validates `agents.yaml`, and stops there. Loading a project builds
|
|
25
30
|
* its model clients, which needs credentials — so anything that wants to *look*
|
|
@@ -39,8 +44,8 @@ export function readProjectConfig(dir) {
|
|
|
39
44
|
*/
|
|
40
45
|
export async function loadProject(dir, opts = {}) {
|
|
41
46
|
const { root, source, config } = readProjectConfig(dir);
|
|
42
|
-
// Read once and share the
|
|
43
|
-
// same
|
|
47
|
+
// Read once and share the objects: every agent's prompt then reports the
|
|
48
|
+
// same paths and the same content hashes, so the report says "one document,
|
|
44
49
|
// five prompts" instead of showing five identical blobs.
|
|
45
50
|
const houseRules = readHouseRules(root);
|
|
46
51
|
const providers = [...skillProviders(root, config, opts), ...(opts.skills ?? [])];
|
|
@@ -69,7 +74,7 @@ export async function loadProject(dir, opts = {}) {
|
|
|
69
74
|
// The default is resolved here rather than on first use, so a broken
|
|
70
75
|
// `embedding:` fails at load like a broken `model:` does.
|
|
71
76
|
embedders(config.embedding, 'embedding');
|
|
72
|
-
const memory = opts.memory ?? (await openMemory(root, config, embedders));
|
|
77
|
+
const memory = opts.memory ?? (await openMemory(root, config, embedders, opts.memoryDir));
|
|
73
78
|
return new AgentProject({
|
|
74
79
|
root,
|
|
75
80
|
source,
|
|
@@ -184,30 +189,63 @@ function findConfig(root) {
|
|
|
184
189
|
}
|
|
185
190
|
throw new Error(`no project configuration in ${root} (looked for ${CONFIG_NAMES.join(', ')})`);
|
|
186
191
|
}
|
|
192
|
+
/** The project-relative name of a file, which is the name the model is given. */
|
|
193
|
+
function within(root, path) {
|
|
194
|
+
return relative(root, path).split(sep).join('/');
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Every house-rules document, in the order they are prepended.
|
|
198
|
+
*
|
|
199
|
+
* The set is a glob rather than a list in `agents.yaml` because these are read
|
|
200
|
+
* by every agent without exception — there is nothing to decide, so there is
|
|
201
|
+
* nothing to declare. Order is the filename's, which is the only ordering that
|
|
202
|
+
* is the same on every machine and is visible without opening another file.
|
|
203
|
+
*
|
|
204
|
+
* All of it is optional: a one-agent project whose whole prompt is its role
|
|
205
|
+
* file should not need a second, empty document.
|
|
206
|
+
*/
|
|
187
207
|
function readHouseRules(root) {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
208
|
+
const rules = [];
|
|
209
|
+
const take = (rel) => {
|
|
210
|
+
const path = join(root, rel);
|
|
211
|
+
if (existsSync(path)) {
|
|
212
|
+
rules.push(promptFile(path, 'house_rules', rel));
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
// First, because it is the oldest and most general thing the project says:
|
|
216
|
+
// anything added under `agents/` during a migration layers on top of it.
|
|
217
|
+
take(LEGACY_HOUSE_RULES);
|
|
218
|
+
take(HOUSE_RULES);
|
|
219
|
+
for (const name of topics(root)) {
|
|
220
|
+
take(`${AGENTS_DIR}/${name}`);
|
|
221
|
+
}
|
|
222
|
+
return rules;
|
|
223
|
+
}
|
|
224
|
+
/** `agents/<topic>-instructions.md`, sorted. Subdirectories are not searched. */
|
|
225
|
+
function topics(root) {
|
|
226
|
+
const dir = join(root, AGENTS_DIR);
|
|
227
|
+
if (!existsSync(dir)) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
231
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(INSTRUCTIONS_SUFFIX))
|
|
232
|
+
.map((entry) => entry.name)
|
|
233
|
+
.sort((a, b) => a.localeCompare(b));
|
|
192
234
|
}
|
|
193
235
|
/**
|
|
194
|
-
*
|
|
236
|
+
* The house rules first, the agent's own file second — shared context before
|
|
195
237
|
* the specific job, and the stable half of the prompt in front where a cache
|
|
196
238
|
* can reuse it.
|
|
197
239
|
*/
|
|
198
240
|
function instructionsFor(root, spec, houseRules) {
|
|
199
|
-
const parts = [];
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const conventional = join(root, PROMPTS_DIR, `${spec.name}.md`);
|
|
208
|
-
if (existsSync(conventional)) {
|
|
209
|
-
parts.push(promptFile(conventional, 'role'));
|
|
210
|
-
}
|
|
241
|
+
const parts = [...houseRules];
|
|
242
|
+
const role = spec.system
|
|
243
|
+
? projectFile(root, spec.system, `agents.${spec.name}.system`)
|
|
244
|
+
: join(root, PROMPTS_DIR, `${spec.name}.md`);
|
|
245
|
+
// A named prompt that is missing is `projectFile`'s to refuse; a
|
|
246
|
+
// conventional one that is missing is simply an agent without a role file.
|
|
247
|
+
if (spec.system || existsSync(role)) {
|
|
248
|
+
parts.push(promptFile(role, 'role', within(root, role)));
|
|
211
249
|
}
|
|
212
250
|
return parts;
|
|
213
251
|
}
|
|
@@ -354,8 +392,16 @@ function memoryFor(spec) {
|
|
|
354
392
|
* with the front end, which has to mount the same directory into the sandbox:
|
|
355
393
|
* two answers to this question would mean `/memory` pointing somewhere the
|
|
356
394
|
* graph is not.
|
|
395
|
+
*
|
|
396
|
+
* `override` is the host pointing the run at another graph — `zen run
|
|
397
|
+
* --memory`. It answers for a project that declares no memory at all, because
|
|
398
|
+
* naming a directory is the declaration; the agents bound to it are still the
|
|
399
|
+
* ones that said `memory: true`.
|
|
357
400
|
*/
|
|
358
|
-
export function memoryDir(root, config) {
|
|
401
|
+
export function memoryDir(root, config, override) {
|
|
402
|
+
if (override) {
|
|
403
|
+
return resolve(root, override);
|
|
404
|
+
}
|
|
359
405
|
if (!config.memory && !config.agents.some((a) => a.memory)) {
|
|
360
406
|
return undefined;
|
|
361
407
|
}
|
|
@@ -367,8 +413,8 @@ export function memoryDir(root, config) {
|
|
|
367
413
|
* to it still counts: it is how a project declares the graph `zen memory`
|
|
368
414
|
* inspects between runs.
|
|
369
415
|
*/
|
|
370
|
-
async function openMemory(root, config, embedders) {
|
|
371
|
-
const dir = memoryDir(root, config);
|
|
416
|
+
async function openMemory(root, config, embedders, override) {
|
|
417
|
+
const dir = memoryDir(root, config, override);
|
|
372
418
|
if (!dir) {
|
|
373
419
|
return undefined;
|
|
374
420
|
}
|
package/dist/prompt.d.ts
CHANGED
|
@@ -13,6 +13,13 @@ export interface PromptText {
|
|
|
13
13
|
path?: string;
|
|
14
14
|
/** opt-in wrapper tag, e.g. `intent` → `<intent>…</intent>` */
|
|
15
15
|
section?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The name of that document *as the model is told it*, written onto the
|
|
18
|
+
* wrapper tag as `src`. Deliberately not `path`: that one is absolute, so
|
|
19
|
+
* it would put a home directory in the prompt and give two machines two
|
|
20
|
+
* different cache prefixes for the same project.
|
|
21
|
+
*/
|
|
22
|
+
src?: string;
|
|
16
23
|
}
|
|
17
24
|
/**
|
|
18
25
|
* One contributor to a system prompt: literal text, text computed from the run
|
|
@@ -39,7 +46,7 @@ export type Instructions<TCtx = unknown> = string | ((ctx: TCtx, spec: RunSpec)
|
|
|
39
46
|
* A prompt that must change without a restart is a different feature: rebuild
|
|
40
47
|
* the agent, or use the function form.
|
|
41
48
|
*/
|
|
42
|
-
export declare function promptFile(path: string, section?: string): PromptText;
|
|
49
|
+
export declare function promptFile(path: string, section?: string, src?: string): PromptText;
|
|
43
50
|
/** A document that fed a prompt, with the exact bytes it contributed. */
|
|
44
51
|
export interface PromptSource {
|
|
45
52
|
path: string;
|
|
@@ -51,6 +58,8 @@ export interface ComposedPrompt {
|
|
|
51
58
|
/** the editable contributors, in the order they were rendered */
|
|
52
59
|
sources: PromptSource[];
|
|
53
60
|
}
|
|
61
|
+
/** `&`, `<` and `"` are the three that would end the attribute or the tag. */
|
|
62
|
+
export declare function attribute(value: string): string;
|
|
54
63
|
/**
|
|
55
64
|
* Renders the authored parts, then the kernel's derived text, into one string —
|
|
56
65
|
* and reports which documents went into it.
|
package/dist/prompt.js
CHANGED
|
@@ -11,17 +11,34 @@ import { readFileSync } from 'node:fs';
|
|
|
11
11
|
* A prompt that must change without a restart is a different feature: rebuild
|
|
12
12
|
* the agent, or use the function form.
|
|
13
13
|
*/
|
|
14
|
-
export function promptFile(path, section) {
|
|
14
|
+
export function promptFile(path, section, src) {
|
|
15
15
|
const text = readFileSync(path, 'utf8');
|
|
16
|
-
return
|
|
16
|
+
return {
|
|
17
|
+
text,
|
|
18
|
+
path,
|
|
19
|
+
...(section === undefined ? {} : { section }),
|
|
20
|
+
...(src === undefined ? {} : { src }),
|
|
21
|
+
};
|
|
17
22
|
}
|
|
18
23
|
const SEPARATOR = '\n\n';
|
|
24
|
+
/** `&`, `<` and `"` are the three that would end the attribute or the tag. */
|
|
25
|
+
export function attribute(value) {
|
|
26
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
|
|
27
|
+
}
|
|
19
28
|
/**
|
|
20
29
|
* Markers are opt-in because they are tokens the model reads: a wrapper that
|
|
21
30
|
* "does not influence the content" does not exist for an LLM.
|
|
31
|
+
*
|
|
32
|
+
* `src` rides on the marker rather than being a line of prose above it for the
|
|
33
|
+
* same reason the marker exists at all — it is structure, and a model that can
|
|
34
|
+
* see where a block ends can see where it came from.
|
|
22
35
|
*/
|
|
23
36
|
function wrap(part) {
|
|
24
|
-
|
|
37
|
+
if (!part.section) {
|
|
38
|
+
return part.text;
|
|
39
|
+
}
|
|
40
|
+
const src = part.src ? ` src="${attribute(part.src)}"` : '';
|
|
41
|
+
return `<${part.section}${src}>\n${part.text}\n</${part.section}>`;
|
|
25
42
|
}
|
|
26
43
|
/**
|
|
27
44
|
* Renders the authored parts, then the kernel's derived text, into one string —
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
-
import { basename, join, resolve } from 'node:path';
|
|
2
|
+
import { basename, join, relative, resolve } from 'node:path';
|
|
3
3
|
import { selectTools } from "../types.js";
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// Filesystem-backed skill provider
|
|
@@ -83,6 +83,7 @@ export class FileSkillProvider {
|
|
|
83
83
|
...entry.summary,
|
|
84
84
|
content: entry.content,
|
|
85
85
|
file: entry.file,
|
|
86
|
+
src: entry.src,
|
|
86
87
|
...(tools.length ? { tools } : {}),
|
|
87
88
|
...(entry.at ? { path: entry.at } : {}),
|
|
88
89
|
};
|
|
@@ -122,19 +123,20 @@ export class FileSkillProvider {
|
|
|
122
123
|
// Only a folder skill has files of its own to point at, and
|
|
123
124
|
// only when the host said where the directory can be reached.
|
|
124
125
|
const at = folder && dir.at ? `${dir.at}/${basename(folder)}` : undefined;
|
|
125
|
-
const entry = parse(raw, base, file, folder, at);
|
|
126
|
+
const entry = parse(raw, base, file, relative(dir.path, file), folder, at);
|
|
126
127
|
index.set(entry.summary.name, entry);
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
return index;
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
|
-
function parse(raw, base, file, folder, at) {
|
|
133
|
+
function parse(raw, base, file, src, folder, at) {
|
|
133
134
|
const { data, body } = frontmatter(raw);
|
|
134
135
|
const tags = toList(data.tags);
|
|
135
136
|
const tools = toList(data.tools);
|
|
136
137
|
return {
|
|
137
138
|
file,
|
|
139
|
+
src,
|
|
138
140
|
folder,
|
|
139
141
|
...(at ? { at } : {}),
|
|
140
142
|
content: body,
|
package/dist/skills.d.ts
CHANGED
|
@@ -33,6 +33,13 @@ export interface Skill extends SkillSummary {
|
|
|
33
33
|
path?: string;
|
|
34
34
|
/** absolute path of the source file, if known (set by file-backed providers) */
|
|
35
35
|
file?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The source document *as the model is told it*, written onto the block as
|
|
38
|
+
* `src`. Deliberately not `file`: that one is absolute, so it would put a
|
|
39
|
+
* home directory in the prompt and give two machines two different cache
|
|
40
|
+
* prefixes for the same catalog.
|
|
41
|
+
*/
|
|
42
|
+
src?: string;
|
|
36
43
|
}
|
|
37
44
|
export interface SkillProvider {
|
|
38
45
|
readonly id: string;
|
package/dist/skills.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hash } from "./payload.js";
|
|
2
|
+
import { attribute } from "./prompt.js";
|
|
2
3
|
import { projected } from "./trajectory.js";
|
|
3
4
|
import { SKILL_LOAD_TOOL, SKILL_SEARCH_TOOL, tool, withEffects, } from "./types.js";
|
|
4
5
|
export function skillContentHash(s) {
|
|
@@ -7,15 +8,21 @@ export function skillContentHash(s) {
|
|
|
7
8
|
export function renderSkills(skills) {
|
|
8
9
|
return skills
|
|
9
10
|
.map((s) => {
|
|
10
|
-
const
|
|
11
|
+
const attrs = [
|
|
12
|
+
` name="${attribute(s.name)}"`,
|
|
13
|
+
s.version ? ` version="${attribute(s.version)}"` : '',
|
|
14
|
+
// Same contract as a prompt block's `src`: the document to edit,
|
|
15
|
+
// under a name that is the same on every machine.
|
|
16
|
+
s.src ? ` src="${attribute(s.src)}"` : '',
|
|
17
|
+
].join('');
|
|
11
18
|
// One line, and only when the files are reachable. Without it the
|
|
12
19
|
// model has the instructions and no way to act on the half of them
|
|
13
20
|
// that names a file.
|
|
14
21
|
const where = s.path
|
|
15
|
-
?
|
|
16
|
-
'relative to that directory.\n'
|
|
22
|
+
? `This skill's files are at ${s.path}, read-only. Paths written in it are ` +
|
|
23
|
+
'relative to that directory.\n\n'
|
|
17
24
|
: '';
|
|
18
|
-
return
|
|
25
|
+
return `<skill${attrs}>\n${where}${s.content}\n</skill>`;
|
|
19
26
|
})
|
|
20
27
|
.join('\n\n');
|
|
21
28
|
}
|