@zenera/neo 1.1.15 → 1.1.17
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 +16 -16
- 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/project/config.js +5 -3
- package/dist/project/load.d.ts +3 -2
- package/dist/project/load.js +60 -22
- package/dist/prompt.d.ts +8 -1
- package/dist/prompt.js +20 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
an append-only trajectory.**
|
|
17
17
|
|
|
18
18
|
> Part of [ZeneraNeo](https://github.com/andreyryabov/ZeneraNeo). This is an
|
|
19
|
-
> open-source side project for experimentation and chore work
|
|
19
|
+
> open-source side project for experimentation and chore work - **not** the
|
|
20
20
|
> official Zenera AI Platform. It carries no support or stability promises.
|
|
21
21
|
|
|
22
22
|
## Install
|
|
@@ -41,7 +41,7 @@ or load them from a folder.
|
|
|
41
41
|
### In code
|
|
42
42
|
|
|
43
43
|
An agent is an instruction, a model, some tools, and who it may hand work to.
|
|
44
|
-
That is the whole list
|
|
44
|
+
That is the whole list - there is no orchestration layer underneath.
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
47
|
import { AgentRunner, createModel, tool } from '@zenera/neo';
|
|
@@ -72,7 +72,7 @@ console.log(result.output);
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
`run()` returns a handle: `await` it for the answer, or iterate it for the
|
|
75
|
-
stream
|
|
75
|
+
stream - token deltas plus a checkpoint for every model call, tool call,
|
|
76
76
|
handoff, fork and join.
|
|
77
77
|
|
|
78
78
|
```ts
|
|
@@ -88,7 +88,7 @@ them.
|
|
|
88
88
|
|
|
89
89
|
### From a folder
|
|
90
90
|
|
|
91
|
-
An agentic project is a folder
|
|
91
|
+
An agentic project is a folder - prompts, agent wiring, skills and tool
|
|
92
92
|
selections are Markdown and YAML files, not code buried inside an application.
|
|
93
93
|
Load one and run it:
|
|
94
94
|
|
|
@@ -127,29 +127,29 @@ agents:
|
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
It is validated strictly at load: an unknown tool, a handoff to nobody, a
|
|
130
|
-
missing prompt file
|
|
130
|
+
missing prompt file - each fails immediately, naming the offending key, instead
|
|
131
131
|
of surfacing three turns into a run as a confused model.
|
|
132
132
|
|
|
133
133
|
## What it gives you
|
|
134
134
|
|
|
135
|
-
- **Agents**
|
|
135
|
+
- **Agents** - an instruction, a model, tools, skills, who it may hand the work
|
|
136
136
|
to, and how it splits into parallel branches. There is no hidden orchestration
|
|
137
137
|
layer.
|
|
138
|
-
- **Models**
|
|
138
|
+
- **Models** - OpenAI, Anthropic, Google/Vertex and OpenRouter behind one
|
|
139
139
|
interface, each through its own SDK, plus any OpenAI-compatible endpoint.
|
|
140
140
|
Named providers and model aliases resolve from config or from the host.
|
|
141
|
-
- **Tools**
|
|
141
|
+
- **Tools** - plain typed functions, selectable per agent by name, group or
|
|
142
142
|
wildcard. Workspace file tools and a Podman sandbox ship with the library.
|
|
143
|
-
- **Skills**
|
|
143
|
+
- **Skills** - instruction bundles discovered and loaded on demand instead of
|
|
144
144
|
permanently occupying the prompt, optionally owning their own tools.
|
|
145
|
-
- **Memory**
|
|
145
|
+
- **Memory** - a timed, masked knowledge graph that outlives a run: recall
|
|
146
146
|
returns a stitched subgraph rather than a ranked list, remembered files are
|
|
147
147
|
mounted at `/memory`, and standing preferences go into the system prompt.
|
|
148
|
-
- **Trajectory**
|
|
148
|
+
- **Trajectory** - an append-only log of everything that happened. What is sent
|
|
149
149
|
to a provider is derived from it, and compaction _covers_ nodes rather than
|
|
150
150
|
deleting them, so the full record survives even when older turns have to be
|
|
151
151
|
pushed out of the context window.
|
|
152
|
-
- **Inspection**
|
|
152
|
+
- **Inspection** - `renderReportHtml` turns any run into one self-contained HTML
|
|
153
153
|
file: the graph, every request, every token.
|
|
154
154
|
|
|
155
155
|
## Entry points
|
|
@@ -174,14 +174,14 @@ folder.
|
|
|
174
174
|
|
|
175
175
|
| Package | What it is |
|
|
176
176
|
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
177
|
-
| [`@zenera/cli`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/cli/README.md) | `zen`
|
|
178
|
-
| [`@zenera/faker`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/faker/README.md) | `zen faker`
|
|
179
|
-
| [`@zenera/rag`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/rag/README.md) | `zen rag`
|
|
177
|
+
| [`@zenera/cli`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/cli/README.md) | `zen` - this runtime on the command line, with a TUI |
|
|
178
|
+
| [`@zenera/faker`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/faker/README.md) | `zen faker` - a mock API from an openapi/swagger document |
|
|
179
|
+
| [`@zenera/rag`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/rag/README.md) | `zen rag` - an API description as a searchable graph, + tools |
|
|
180
180
|
|
|
181
181
|
`@zenera/rag` also exports tools you can hand straight to `loadProject`, so an
|
|
182
182
|
agent can search an API it has never read.
|
|
183
183
|
|
|
184
184
|
## License
|
|
185
185
|
|
|
186
|
-
Early days and moving fast
|
|
186
|
+
Early days and moving fast - issues, questions and pull requests are welcome.
|
|
187
187
|
[MIT](LICENSE).
|
|
@@ -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/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
|
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, 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 ?? [])];
|
|
@@ -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
|
}
|
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;
|
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
|
+
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 —
|