@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.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/agent.d.ts +66 -0
- package/dist/agent.js +97 -0
- package/dist/architecture.d.ts +74 -0
- package/dist/architecture.js +105 -0
- package/dist/embedding.d.ts +60 -0
- package/dist/embedding.js +21 -0
- package/dist/embeddings/gemini.d.ts +24 -0
- package/dist/embeddings/gemini.js +71 -0
- package/dist/embeddings/index.d.ts +4 -0
- package/dist/embeddings/index.js +4 -0
- package/dist/embeddings/openai.d.ts +18 -0
- package/dist/embeddings/openai.js +27 -0
- package/dist/embeddings/openrouter.d.ts +21 -0
- package/dist/embeddings/openrouter.js +54 -0
- package/dist/events.d.ts +144 -0
- package/dist/events.js +110 -0
- package/dist/ids.d.ts +14 -0
- package/dist/ids.js +33 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/inspect/client.d.ts +2 -0
- package/dist/inspect/client.js +1326 -0
- package/dist/inspect/css.d.ts +2 -0
- package/dist/inspect/css.js +164 -0
- package/dist/inspect/index.d.ts +9 -0
- package/dist/inspect/index.js +12 -0
- package/dist/inspect/page.d.ts +3 -0
- package/dist/inspect/page.js +93 -0
- package/dist/inspect/report.d.ts +54 -0
- package/dist/inspect/report.js +62 -0
- package/dist/kernel.d.ts +178 -0
- package/dist/kernel.js +929 -0
- package/dist/memory-stores/factory.d.ts +15 -0
- package/dist/memory-stores/factory.js +29 -0
- package/dist/memory-stores/file.d.ts +37 -0
- package/dist/memory-stores/file.js +174 -0
- package/dist/memory-stores/in-memory.d.ts +17 -0
- package/dist/memory-stores/in-memory.js +80 -0
- package/dist/memory-stores/index.d.ts +4 -0
- package/dist/memory-stores/index.js +5 -0
- package/dist/memory.d.ts +115 -0
- package/dist/memory.js +213 -0
- package/dist/model.d.ts +25 -0
- package/dist/model.js +2 -0
- package/dist/models/anthropic.d.ts +36 -0
- package/dist/models/anthropic.js +293 -0
- package/dist/models/factory.d.ts +202 -0
- package/dist/models/factory.js +533 -0
- package/dist/models/gemini.d.ts +45 -0
- package/dist/models/gemini.js +368 -0
- package/dist/models/index.d.ts +7 -0
- package/dist/models/index.js +7 -0
- package/dist/models/openai-chat.d.ts +19 -0
- package/dist/models/openai-chat.js +203 -0
- package/dist/models/openai-responses.d.ts +31 -0
- package/dist/models/openai-responses.js +258 -0
- package/dist/models/openrouter.d.ts +44 -0
- package/dist/models/openrouter.js +230 -0
- package/dist/payload-stores/factory.d.ts +15 -0
- package/dist/payload-stores/factory.js +30 -0
- package/dist/payload-stores/file.d.ts +29 -0
- package/dist/payload-stores/file.js +112 -0
- package/dist/payload-stores/in-memory.d.ts +17 -0
- package/dist/payload-stores/in-memory.js +40 -0
- package/dist/payload-stores/index.d.ts +4 -0
- package/dist/payload-stores/index.js +5 -0
- package/dist/payload.d.ts +89 -0
- package/dist/payload.js +181 -0
- package/dist/project/config.d.ts +302 -0
- package/dist/project/config.js +290 -0
- package/dist/project/index.d.ts +4 -0
- package/dist/project/index.js +4 -0
- package/dist/project/load.d.ts +142 -0
- package/dist/project/load.js +430 -0
- package/dist/project/refs.d.ts +23 -0
- package/dist/project/refs.js +70 -0
- package/dist/prompt.d.ts +66 -0
- package/dist/prompt.js +62 -0
- package/dist/runner.d.ts +103 -0
- package/dist/runner.js +537 -0
- package/dist/services.d.ts +23 -0
- package/dist/services.js +44 -0
- package/dist/skill-providers/factory.d.ts +16 -0
- package/dist/skill-providers/factory.js +26 -0
- package/dist/skill-providers/file.d.ts +44 -0
- package/dist/skill-providers/file.js +209 -0
- package/dist/skill-providers/index.d.ts +4 -0
- package/dist/skill-providers/index.js +5 -0
- package/dist/skill-providers/static.d.ts +13 -0
- package/dist/skill-providers/static.js +47 -0
- package/dist/skills.d.ts +102 -0
- package/dist/skills.js +232 -0
- package/dist/state.d.ts +86 -0
- package/dist/state.js +49 -0
- package/dist/tools/exa.d.ts +23 -0
- package/dist/tools/exa.js +389 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/sandbox.d.ts +172 -0
- package/dist/tools/sandbox.js +602 -0
- package/dist/tools/workspace.d.ts +53 -0
- package/dist/tools/workspace.js +1034 -0
- package/dist/trajectory.d.ts +213 -0
- package/dist/trajectory.js +232 -0
- package/dist/types.d.ts +195 -0
- package/dist/types.js +134 -0
- package/package.json +98 -0
package/dist/kernel.js
ADDED
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Agent, AgentRegistry, handoffTarget, handoffTool } from "./agent.js";
|
|
3
|
+
import { systemClock } from "./ids.js";
|
|
4
|
+
import { memoryTools } from "./memory.js";
|
|
5
|
+
import { hash } from "./payload.js";
|
|
6
|
+
import { composePrompt } from "./prompt.js";
|
|
7
|
+
import { allows, lockedSkillTools, renderSkillIndex, renderSkills, skillContentHash, skillTools, } from "./skills.js";
|
|
8
|
+
import { lastOfType, projectMessages, projected, totalUsage, } from "./trajectory.js";
|
|
9
|
+
import { FINAL_OUTPUT_TOOL, FORK_TOOL, addUsage, parseArgs, toContent, zeroUsage, } from "./types.js";
|
|
10
|
+
const FINAL_OUTPUT_INSTRUCTIONS = `When you have the complete answer, deliver it by calling the \`${FINAL_OUTPUT_TOOL}\` ` +
|
|
11
|
+
`tool with arguments matching its schema. Plain prose is not accepted as the final answer.`;
|
|
12
|
+
/**
|
|
13
|
+
* Every `apply*` returns a *new* state object; old snapshots stay valid, which
|
|
14
|
+
* is exactly what a checkpointing caller needs. Nodes carry no index: the array
|
|
15
|
+
* *is* the order.
|
|
16
|
+
*/
|
|
17
|
+
function begin(state, env) {
|
|
18
|
+
const clock = env.clock ?? systemClock;
|
|
19
|
+
const nodes = [];
|
|
20
|
+
return {
|
|
21
|
+
add(body, agent = state.agentName) {
|
|
22
|
+
const node = {
|
|
23
|
+
id: clock.newId(),
|
|
24
|
+
ts: clock.now(),
|
|
25
|
+
agent,
|
|
26
|
+
...body,
|
|
27
|
+
};
|
|
28
|
+
nodes.push(node);
|
|
29
|
+
return node;
|
|
30
|
+
},
|
|
31
|
+
commit(patch) {
|
|
32
|
+
const trajectory = [...state.trajectory, ...nodes];
|
|
33
|
+
return { ...state, ...patch, trajectory, usage: ownUsage(state, trajectory) };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What *this* run consumed. A branch starts from a copy of the parent's prefix,
|
|
39
|
+
* whose `llm_call` nodes were paid for by the parent, so they are excluded.
|
|
40
|
+
*/
|
|
41
|
+
function ownUsage(state, trajectory) {
|
|
42
|
+
return totalUsage(trajectory.slice(state.spec.prefixLength ?? 0));
|
|
43
|
+
}
|
|
44
|
+
function put(env, value) {
|
|
45
|
+
return env.services.payloads.put(value);
|
|
46
|
+
}
|
|
47
|
+
function get(env, payload) {
|
|
48
|
+
return env.services.payloads.get(payload);
|
|
49
|
+
}
|
|
50
|
+
async function toPayloadParts(env, input) {
|
|
51
|
+
return Promise.all(toContent(input).map(async (p) => p.type === 'text'
|
|
52
|
+
? { type: 'text', text: await put(env, p.text) }
|
|
53
|
+
: { type: p.type, url: p.url, mimeType: p.mimeType }));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The only way to obtain a state. Starting a run is explicit; the loop then
|
|
57
|
+
* only ever *advances* an existing state.
|
|
58
|
+
*/
|
|
59
|
+
export async function createState(opts, env) {
|
|
60
|
+
const clock = env.clock ?? systemClock;
|
|
61
|
+
const spec = {
|
|
62
|
+
startAgent: opts.agent,
|
|
63
|
+
forkDepth: 0,
|
|
64
|
+
maxForkDepth: opts.maxForkDepth ?? 2,
|
|
65
|
+
};
|
|
66
|
+
if (opts.output) {
|
|
67
|
+
const { schema, wrapped } = outputJsonSchema(opts.output);
|
|
68
|
+
spec.outputSchema = schema;
|
|
69
|
+
spec.outputSchemaHash = hash(JSON.stringify(schema));
|
|
70
|
+
spec.outputWrapped = wrapped;
|
|
71
|
+
}
|
|
72
|
+
const state = {
|
|
73
|
+
version: 1,
|
|
74
|
+
runId: opts.runId ?? clock.newId(),
|
|
75
|
+
spec,
|
|
76
|
+
agentName: opts.agent,
|
|
77
|
+
phase: 'created',
|
|
78
|
+
trajectory: [],
|
|
79
|
+
pendingToolCalls: [],
|
|
80
|
+
usage: zeroUsage(),
|
|
81
|
+
context: opts.context,
|
|
82
|
+
};
|
|
83
|
+
const b = begin(state, env);
|
|
84
|
+
if (opts.systemPrompt !== undefined) {
|
|
85
|
+
b.add({
|
|
86
|
+
type: 'system_prompt',
|
|
87
|
+
prompt: await put(env, opts.systemPrompt),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (opts.input !== undefined) {
|
|
91
|
+
b.add({
|
|
92
|
+
type: 'user_input',
|
|
93
|
+
content: await toPayloadParts(env, opts.input),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return b.commit({ phase: opts.input === undefined ? 'created' : 'awaiting_llm' });
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A Zod schema is a live object with functions, so it cannot live in a
|
|
100
|
+
* JSON-serializable state; its JSON Schema projection can, and must, because it
|
|
101
|
+
* is part of the prompt (the `final_output` tool's parameters).
|
|
102
|
+
*/
|
|
103
|
+
function outputJsonSchema(schema) {
|
|
104
|
+
let json;
|
|
105
|
+
try {
|
|
106
|
+
json = z.toJSONSchema(schema, { target: 'draft-2020-12', io: 'input' });
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
// Transforms, refinements and z.custom have no JSON Schema equivalent:
|
|
110
|
+
// the model sees the looser shape and safeParse enforces the rest.
|
|
111
|
+
console.warn(`output schema is only partly representable as JSON Schema: ${String(e)}`);
|
|
112
|
+
json = z.toJSONSchema(schema, {
|
|
113
|
+
target: 'draft-2020-12',
|
|
114
|
+
io: 'input',
|
|
115
|
+
unrepresentable: 'any',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const obj = json;
|
|
119
|
+
if (obj.type === 'object') {
|
|
120
|
+
return { schema: obj, wrapped: false };
|
|
121
|
+
}
|
|
122
|
+
// Non-object roots (a bare string, array, union …) cannot be tool
|
|
123
|
+
// parameters, so they travel in a single `value` property.
|
|
124
|
+
return {
|
|
125
|
+
schema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: { value: obj },
|
|
128
|
+
required: ['value'],
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
},
|
|
131
|
+
wrapped: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** A persisted run must not silently change its contract on resume. */
|
|
135
|
+
export function checkOutputSchema(state, env) {
|
|
136
|
+
if (!env.output) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const { schema } = outputJsonSchema(env.output);
|
|
140
|
+
const given = hash(JSON.stringify(schema));
|
|
141
|
+
if (state.spec.outputSchemaHash && given !== state.spec.outputSchemaHash) {
|
|
142
|
+
throw new Error(`output schema mismatch: run ${state.runId} was created with a different schema`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Pure inspection: what must happen next. No I/O, no payload resolution. */
|
|
146
|
+
export function nextAction(state) {
|
|
147
|
+
if (state.phase === 'failed') {
|
|
148
|
+
throw new Error(`run ${state.runId} failed: ${state.error ?? 'unknown error'}`);
|
|
149
|
+
}
|
|
150
|
+
if (state.phase === 'done') {
|
|
151
|
+
const output = lastOfType(state.trajectory, 'final_output');
|
|
152
|
+
if (!output) {
|
|
153
|
+
throw new Error(`run ${state.runId} is done but has no final output`);
|
|
154
|
+
}
|
|
155
|
+
return { kind: 'done', output };
|
|
156
|
+
}
|
|
157
|
+
// Tool results are what unblock the next model call, so they come first even
|
|
158
|
+
// when a fork is also pending.
|
|
159
|
+
if (state.pendingToolCalls.length) {
|
|
160
|
+
const byId = new Map(state.trajectory.flatMap((n) => n.type === 'tool_call' ? [[n.callId, n]] : []));
|
|
161
|
+
const calls = state.pendingToolCalls.flatMap((id) => {
|
|
162
|
+
const n = byId.get(id);
|
|
163
|
+
return n ? [{ callId: n.callId, name: n.name, args: n.args }] : [];
|
|
164
|
+
});
|
|
165
|
+
if (calls.length) {
|
|
166
|
+
return { kind: 'tools', calls };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const pendingFork = state.pendingFork;
|
|
170
|
+
if (pendingFork?.branches.length) {
|
|
171
|
+
// Looked up by call id, not by "the last fork node": with two forks in
|
|
172
|
+
// one run the most recent one is not necessarily the pending one.
|
|
173
|
+
const fork = state.trajectory.find((n) => n.type === 'fork' && n.callId === pendingFork.callId);
|
|
174
|
+
if (fork) {
|
|
175
|
+
const branches = fork.branches
|
|
176
|
+
.filter((b) => pendingFork.branches.includes(b.name))
|
|
177
|
+
.map(({ name, agent, childRunId }) => ({ name, agent, childRunId }));
|
|
178
|
+
return { kind: 'fork', forkId: fork.callId, branches };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { kind: 'llm' };
|
|
182
|
+
}
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Tool set — derived from the state, not a static agent property
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
/** Agents a branch of this fork may run, in registration order. */
|
|
187
|
+
function forkAgents(state, reg) {
|
|
188
|
+
const allowed = reg.find(state.agentName)?.fork?.agents;
|
|
189
|
+
if (!allowed) {
|
|
190
|
+
return reg.names();
|
|
191
|
+
}
|
|
192
|
+
const known = allowed.filter((n) => reg.find(n));
|
|
193
|
+
return known.length ? known : [state.agentName];
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Derived from the state rather than a constant, because the one thing a weak
|
|
197
|
+
* model reliably gets wrong here is inventing an agent name. An `enum` of the
|
|
198
|
+
* agents that actually exist is a constraint the provider enforces during
|
|
199
|
+
* decoding, which no amount of prose in the description can match.
|
|
200
|
+
*
|
|
201
|
+
* `minItems`/`maxItems` are stated too, but they are advisory — most providers
|
|
202
|
+
* ignore them under strict decoding — so `parseForkArgs` and `forkProblem`
|
|
203
|
+
* still check both, and their messages are written to be read by the model.
|
|
204
|
+
*/
|
|
205
|
+
function forkParameters(agents, self, maxBranches) {
|
|
206
|
+
const max = maxBranches ?? 0;
|
|
207
|
+
// When the author's list excludes the current agent there is no sane
|
|
208
|
+
// default, so the model has to name one.
|
|
209
|
+
const selfAllowed = agents.includes(self);
|
|
210
|
+
return {
|
|
211
|
+
type: 'object',
|
|
212
|
+
properties: {
|
|
213
|
+
branches: {
|
|
214
|
+
type: 'array',
|
|
215
|
+
minItems: 2,
|
|
216
|
+
...(max >= 2 ? { maxItems: max } : {}),
|
|
217
|
+
description: `At least two branches${max >= 2 ? `, at most ${max}` : ''}. ` +
|
|
218
|
+
'One branch is never valid: if the work does not split, do it yourself ' +
|
|
219
|
+
'instead of calling this tool.',
|
|
220
|
+
items: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: {
|
|
223
|
+
name: {
|
|
224
|
+
type: 'string',
|
|
225
|
+
description: 'Short, unique label for this branch, e.g. "eu-tariffs".',
|
|
226
|
+
},
|
|
227
|
+
instructions: {
|
|
228
|
+
type: 'string',
|
|
229
|
+
description: 'The complete assignment for this branch. It runs in a separate ' +
|
|
230
|
+
'conversation and cannot ask questions, so state everything it ' +
|
|
231
|
+
'needs and exactly what it must return.',
|
|
232
|
+
},
|
|
233
|
+
agent: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
enum: agents,
|
|
236
|
+
description: 'Which agent runs this branch. Must be one of: ' +
|
|
237
|
+
agents.join(', ') +
|
|
238
|
+
(selfAllowed ? `. Omit to use "${self}".` : '.'),
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
required: selfAllowed
|
|
242
|
+
? ['name', 'instructions']
|
|
243
|
+
: ['name', 'instructions', 'agent'],
|
|
244
|
+
additionalProperties: false,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
context: {
|
|
248
|
+
type: 'string',
|
|
249
|
+
enum: ['inherit', 'compact', 'none'],
|
|
250
|
+
description: 'How much of this conversation each branch starts with: "inherit" (all of ' +
|
|
251
|
+
'it, the default), "compact" (messages only, no tool traffic), "none" ' +
|
|
252
|
+
'(nothing but its own instructions).',
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
required: ['branches'],
|
|
256
|
+
additionalProperties: false,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const FORK_DESCRIPTION = [
|
|
260
|
+
'Split the work into independent branches that run in parallel and rejoin into one result.',
|
|
261
|
+
'',
|
|
262
|
+
'Call it when two or more parts of the task can be worked on without seeing each ' +
|
|
263
|
+
"other's findings — separate documents, regions, candidates, hypotheses. Do not call it " +
|
|
264
|
+
'to break one line of reasoning into steps: branches cannot talk to each other, and a ' +
|
|
265
|
+
"step that needs the previous step's answer must stay in this conversation.",
|
|
266
|
+
'',
|
|
267
|
+
'Every call needs at least two branches, each with a unique name and self-contained ' +
|
|
268
|
+
'instructions. Their answers come back to you as the result of this call, and you ' +
|
|
269
|
+
'decide what the final answer is.',
|
|
270
|
+
].join('\n');
|
|
271
|
+
/**
|
|
272
|
+
* tools(state) = agent tools + handoffs + memory + skills + fork +
|
|
273
|
+
* final_output. Computed from the state so a rehydrated run offers exactly the
|
|
274
|
+
* tools it had before the crash.
|
|
275
|
+
*
|
|
276
|
+
* Skill-owned tools are declared here whether or not their skill is active: the
|
|
277
|
+
* gate `lockedSkillTools` installs reads the trajectory when the tool is called,
|
|
278
|
+
* not when it is declared, so the array the model is sent never mutates
|
|
279
|
+
* mid-conversation and the provider's prompt cache survives a `skill_load`.
|
|
280
|
+
*/
|
|
281
|
+
export async function resolveTools(state, reg, env) {
|
|
282
|
+
const agent = reg.get(state.agentName);
|
|
283
|
+
const ctx = state.context;
|
|
284
|
+
const tools = [...agent.tools];
|
|
285
|
+
for (const name of agent.handoffs) {
|
|
286
|
+
tools.push(handoffTool(name, reg.find(name)?.description));
|
|
287
|
+
}
|
|
288
|
+
tools.push(...memoryTools(agent.memoryBindings(ctx)));
|
|
289
|
+
if (agent.skills) {
|
|
290
|
+
const binding = agent.skills;
|
|
291
|
+
tools.push(...skillTools(binding));
|
|
292
|
+
const provider = env.services.skillProvider(binding.provider);
|
|
293
|
+
// An agent-level tool of the same name wins, and is never gated.
|
|
294
|
+
const own = new Set(agent.tools.map((t) => t.name));
|
|
295
|
+
tools.push(...(await lockedSkillTools(binding, provider, own)));
|
|
296
|
+
}
|
|
297
|
+
if (agent.fork && state.spec.forkDepth < state.spec.maxForkDepth) {
|
|
298
|
+
tools.push({
|
|
299
|
+
name: FORK_TOOL,
|
|
300
|
+
description: FORK_DESCRIPTION,
|
|
301
|
+
parameters: forkParameters(forkAgents(state, reg), state.agentName, agent.fork.maxBranches),
|
|
302
|
+
// Never executed as a tool: the runner drives branches and the join
|
|
303
|
+
// becomes the tool result.
|
|
304
|
+
execute: () => 'fork scheduled',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (state.spec.outputSchema) {
|
|
308
|
+
tools.push({
|
|
309
|
+
name: FINAL_OUTPUT_TOOL,
|
|
310
|
+
description: 'Deliver the final answer. Ends the run.',
|
|
311
|
+
parameters: state.spec.outputSchema,
|
|
312
|
+
execute: () => 'final output accepted',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return tools;
|
|
316
|
+
}
|
|
317
|
+
function toSchema(t) {
|
|
318
|
+
return { name: t.name, description: t.description, parameters: t.parameters };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Prompt text the runtime owns rather than the author: the skill index and the
|
|
322
|
+
* `final_output` instruction. Both used to be appended inside `buildRequest`,
|
|
323
|
+
* which meant part of the system prompt existed in no node and showed up in no
|
|
324
|
+
* audit. Composed in now, always last, so the volatile tail never invalidates
|
|
325
|
+
* the cacheable prefix.
|
|
326
|
+
*/
|
|
327
|
+
async function derivedPrompt(agent, state, env) {
|
|
328
|
+
const out = [];
|
|
329
|
+
if (agent.skills?.discovery === 'index') {
|
|
330
|
+
const binding = agent.skills;
|
|
331
|
+
const provider = env.services.skillProvider(binding.provider);
|
|
332
|
+
// Preloaded skills are left out: the index is a menu of what can be
|
|
333
|
+
// fetched, and offering something the agent already has invites a
|
|
334
|
+
// wasted `skill_load`. Filtering on the binding rather than on the
|
|
335
|
+
// trajectory keeps the prompt a function of config, so it stays
|
|
336
|
+
// byte-stable across turns.
|
|
337
|
+
const preloaded = new Set(binding.preload ?? []);
|
|
338
|
+
const index = (await provider.list()).filter((s) => allows(binding, s) && !preloaded.has(s.name));
|
|
339
|
+
const rendered = renderSkillIndex(index, binding.maxIndexEntries ?? 50);
|
|
340
|
+
if (rendered) {
|
|
341
|
+
out.push(rendered);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (state.spec.outputSchema) {
|
|
345
|
+
out.push(FINAL_OUTPUT_INSTRUCTIONS);
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Projection + tool schemas + system prompt for the active agent. The only
|
|
351
|
+
* place in the kernel that dereferences payloads — everything else works on
|
|
352
|
+
* refs alone.
|
|
353
|
+
*
|
|
354
|
+
* Nothing is appended to the system prompt here: what the model reads is
|
|
355
|
+
* exactly the `system_prompt` node `applySystemPrompt` recorded, which is what
|
|
356
|
+
* makes `requestDigest` a meaningful replay check for the prompt.
|
|
357
|
+
*/
|
|
358
|
+
export async function buildRequest(state, reg, env) {
|
|
359
|
+
const { system, messages } = await projectMessages(state.trajectory, env.services.payloads);
|
|
360
|
+
const tools = await resolveTools(state, reg, env);
|
|
361
|
+
return {
|
|
362
|
+
system,
|
|
363
|
+
messages,
|
|
364
|
+
tools: tools.map(toSchema),
|
|
365
|
+
toolChoice: 'auto',
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/** Stable fingerprint of a request, for replay divergence detection. */
|
|
369
|
+
export function requestDigest(req) {
|
|
370
|
+
return hash(JSON.stringify({
|
|
371
|
+
system: req.system ?? '',
|
|
372
|
+
messages: req.messages,
|
|
373
|
+
tools: req.tools.map((t) => t.name),
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* A request as it goes onto an `llm_call` node: everything that shaped the
|
|
378
|
+
* call, minus the `signal`, which is machinery rather than content and is not
|
|
379
|
+
* JSON anyway. Pretty-printed, since the whole point of keeping it is that a
|
|
380
|
+
* human reads it later.
|
|
381
|
+
*/
|
|
382
|
+
export function serializeRequest(req) {
|
|
383
|
+
const { signal: _signal, ...rest } = req;
|
|
384
|
+
return JSON.stringify(rest, null, 2);
|
|
385
|
+
}
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// Advancing the state
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
export async function applyUserInput(state, input, env, synthetic = false) {
|
|
390
|
+
const b = begin(state, env);
|
|
391
|
+
b.add({
|
|
392
|
+
type: 'user_input',
|
|
393
|
+
content: await toPayloadParts(env, input),
|
|
394
|
+
synthetic: synthetic || undefined,
|
|
395
|
+
});
|
|
396
|
+
return b.commit({ phase: 'awaiting_llm' });
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Renders the active agent's instructions into the trajectory. Idempotent: a
|
|
400
|
+
* matching prompt already in force produces no node, so re-entering the loop
|
|
401
|
+
* (or resuming) does not litter the history.
|
|
402
|
+
*
|
|
403
|
+
* The rendered bytes are the identity. Two compositions that render the same
|
|
404
|
+
* string *are* the same prompt to the model, the provider cache and replay, so
|
|
405
|
+
* there is nothing else to hash and no renderer version to pin: a change that
|
|
406
|
+
* matters changes the bytes, and one that does not, does not.
|
|
407
|
+
*/
|
|
408
|
+
export async function applySystemPrompt(state, reg, env) {
|
|
409
|
+
const agent = reg.get(state.agentName);
|
|
410
|
+
const { text, sources } = await composePrompt(agent.instructions, state.context, state.spec, (value) => put(env, value), await derivedPrompt(agent, state, env));
|
|
411
|
+
if (!text) {
|
|
412
|
+
return state;
|
|
413
|
+
}
|
|
414
|
+
const current = lastOfType(projected(state.trajectory), 'system_prompt');
|
|
415
|
+
if (current && current.agent === state.agentName && current.prompt.sha256 === hash(text)) {
|
|
416
|
+
return state;
|
|
417
|
+
}
|
|
418
|
+
const b = begin(state, env);
|
|
419
|
+
b.add({
|
|
420
|
+
type: 'system_prompt',
|
|
421
|
+
prompt: await put(env, text),
|
|
422
|
+
...(sources.length ? { sources } : {}),
|
|
423
|
+
});
|
|
424
|
+
return b.commit({});
|
|
425
|
+
}
|
|
426
|
+
function parseForkArgs(raw) {
|
|
427
|
+
const args = parseArgs(raw);
|
|
428
|
+
const branches = args.branches;
|
|
429
|
+
// Spelled out rather than "invalid arguments": this string is fed back to
|
|
430
|
+
// the model as the tool result, and it is the only chance it gets to work
|
|
431
|
+
// out what to do differently.
|
|
432
|
+
if (!Array.isArray(branches) || branches.length === 0) {
|
|
433
|
+
return 'the "branches" argument is missing or empty; it must be an array of at least two branches';
|
|
434
|
+
}
|
|
435
|
+
if (branches.length < 2) {
|
|
436
|
+
return ('a fork needs at least two branches, and this call has one. Either add the other ' +
|
|
437
|
+
'independent branches, or do not fork at all and carry out the work in this ' +
|
|
438
|
+
'conversation.');
|
|
439
|
+
}
|
|
440
|
+
const parsed = [];
|
|
441
|
+
for (const b of branches) {
|
|
442
|
+
if (typeof b?.name !== 'string' || !b.name.trim()) {
|
|
443
|
+
return 'every branch needs a non-empty "name"';
|
|
444
|
+
}
|
|
445
|
+
if (typeof b?.instructions !== 'string' || !b.instructions.trim()) {
|
|
446
|
+
return `branch "${b.name}" needs non-empty "instructions" saying what it must do`;
|
|
447
|
+
}
|
|
448
|
+
parsed.push({
|
|
449
|
+
name: b.name,
|
|
450
|
+
instructions: b.instructions,
|
|
451
|
+
agent: typeof b.agent === 'string' ? b.agent : undefined,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (new Set(parsed.map((b) => b.name)).size !== parsed.length) {
|
|
455
|
+
return 'branch names must be unique';
|
|
456
|
+
}
|
|
457
|
+
const mode = args.context;
|
|
458
|
+
return {
|
|
459
|
+
branches: parsed,
|
|
460
|
+
context: mode === 'inherit' || mode === 'compact' || mode === 'none' ? mode : 'inherit',
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Appends the `llm_call` node (plus the nodes its tool calls imply) and moves
|
|
465
|
+
* the phase on. Typed runs are resolved here too: `final_output` never reaches
|
|
466
|
+
* tool execution, it is validated against the run's schema on the spot.
|
|
467
|
+
*/
|
|
468
|
+
export async function applyLlmResponse(state, res, model, env, opts = {}) {
|
|
469
|
+
const reg = opts.registry;
|
|
470
|
+
const b = begin(state, env);
|
|
471
|
+
const toolCalls = await Promise.all(res.toolCalls.map(async (c) => ({
|
|
472
|
+
callId: c.id,
|
|
473
|
+
name: c.name,
|
|
474
|
+
args: await put(env, c.args),
|
|
475
|
+
signature: c.signature,
|
|
476
|
+
})));
|
|
477
|
+
b.add({
|
|
478
|
+
type: 'llm_call',
|
|
479
|
+
model,
|
|
480
|
+
requestDigest: opts.digest ?? (opts.request ? requestDigest(opts.request) : ''),
|
|
481
|
+
request: opts.request ? await put(env, serializeRequest(opts.request)) : undefined,
|
|
482
|
+
text: await put(env, res.text),
|
|
483
|
+
thinking: res.thinking ? await put(env, res.thinking) : undefined,
|
|
484
|
+
toolCalls,
|
|
485
|
+
usage: res.usage ?? zeroUsage(),
|
|
486
|
+
stopReason: res.stopReason ?? (res.toolCalls.length ? 'tool_calls' : 'stop'),
|
|
487
|
+
});
|
|
488
|
+
// --- no tool calls: either the run is over, or the model owes us a call ---
|
|
489
|
+
if (!res.toolCalls.length) {
|
|
490
|
+
if (!state.spec.outputSchema) {
|
|
491
|
+
b.add({ type: 'final_output', output: await put(env, res.text) });
|
|
492
|
+
return b.commit({ phase: 'done', pendingToolCalls: [] });
|
|
493
|
+
}
|
|
494
|
+
b.add({
|
|
495
|
+
type: 'user_input',
|
|
496
|
+
content: [
|
|
497
|
+
{
|
|
498
|
+
type: 'text',
|
|
499
|
+
text: await put(env, `Your answer must be delivered through the \`${FINAL_OUTPUT_TOOL}\` tool.`),
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
synthetic: true,
|
|
503
|
+
});
|
|
504
|
+
return b.commit({ phase: 'awaiting_llm', pendingToolCalls: [] });
|
|
505
|
+
}
|
|
506
|
+
const pending = [];
|
|
507
|
+
let pendingFork = state.pendingFork;
|
|
508
|
+
let done = false;
|
|
509
|
+
for (let i = 0; i < res.toolCalls.length; i++) {
|
|
510
|
+
const call = res.toolCalls[i];
|
|
511
|
+
const ref = toolCalls[i];
|
|
512
|
+
if (call.name === FINAL_OUTPUT_TOOL && state.spec.outputSchema) {
|
|
513
|
+
const parsed = validateOutput(state, env, call.args);
|
|
514
|
+
if (parsed.ok) {
|
|
515
|
+
b.add({
|
|
516
|
+
type: 'final_output',
|
|
517
|
+
output: await put(env, stringifyOutput(parsed.value)),
|
|
518
|
+
parsed: parsed.value,
|
|
519
|
+
});
|
|
520
|
+
done = true;
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
// Repairable: the model sees its own mistake and tries again.
|
|
524
|
+
b.add({
|
|
525
|
+
type: 'tool_result',
|
|
526
|
+
callId: call.id,
|
|
527
|
+
name: call.name,
|
|
528
|
+
result: await put(env, parsed.message),
|
|
529
|
+
isError: true,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (call.name === FORK_TOOL) {
|
|
535
|
+
const args = parseForkArgs(call.args);
|
|
536
|
+
const problem = typeof args === 'string' ? args : forkProblem(state, args, reg);
|
|
537
|
+
if (typeof args === 'string' || problem) {
|
|
538
|
+
b.add({
|
|
539
|
+
type: 'tool_result',
|
|
540
|
+
callId: call.id,
|
|
541
|
+
name: call.name,
|
|
542
|
+
result: await put(env, `error: ${problem ?? args}`),
|
|
543
|
+
isError: true,
|
|
544
|
+
});
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const clock = env.clock ?? systemClock;
|
|
548
|
+
const branches = await Promise.all(args.branches.map(async (br) => ({
|
|
549
|
+
name: br.name,
|
|
550
|
+
agent: br.agent ?? state.agentName,
|
|
551
|
+
instructions: await put(env, br.instructions),
|
|
552
|
+
childRunId: clock.newId(),
|
|
553
|
+
})));
|
|
554
|
+
b.add({
|
|
555
|
+
type: 'fork',
|
|
556
|
+
callId: call.id,
|
|
557
|
+
contextMode: args.context ?? 'inherit',
|
|
558
|
+
branches,
|
|
559
|
+
});
|
|
560
|
+
pendingFork = { callId: call.id, branches: branches.map((br) => br.name) };
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
b.add({
|
|
564
|
+
type: 'tool_call',
|
|
565
|
+
callId: call.id,
|
|
566
|
+
name: call.name,
|
|
567
|
+
args: ref.args,
|
|
568
|
+
});
|
|
569
|
+
pending.push(call.id);
|
|
570
|
+
}
|
|
571
|
+
if (done) {
|
|
572
|
+
return b.commit({ phase: 'done', pendingToolCalls: [], pendingFork: undefined });
|
|
573
|
+
}
|
|
574
|
+
const phase = pending.length
|
|
575
|
+
? 'awaiting_tools'
|
|
576
|
+
: pendingFork?.branches.length
|
|
577
|
+
? 'awaiting_branches'
|
|
578
|
+
: 'awaiting_llm';
|
|
579
|
+
return b.commit({ phase, pendingToolCalls: pending, pendingFork });
|
|
580
|
+
}
|
|
581
|
+
function forkProblem(state, args, reg) {
|
|
582
|
+
const agent = reg?.find(state.agentName);
|
|
583
|
+
if (agent && !agent.fork) {
|
|
584
|
+
return 'this agent cannot fork';
|
|
585
|
+
}
|
|
586
|
+
const max = agent?.fork?.maxBranches;
|
|
587
|
+
if (max && args.branches.length > max) {
|
|
588
|
+
return `at most ${max} branches are allowed, and this call has ${args.branches.length}`;
|
|
589
|
+
}
|
|
590
|
+
// Hallucinated agent names are the common failure, so the answer always
|
|
591
|
+
// carries the list of real ones — a retry then has nothing left to guess.
|
|
592
|
+
const allowed = reg ? forkAgents(state, reg) : undefined;
|
|
593
|
+
for (const b of args.branches) {
|
|
594
|
+
const name = b.agent ?? state.agentName;
|
|
595
|
+
if (allowed && !allowed.includes(name)) {
|
|
596
|
+
return (`branch "${b.name}" names an agent that cannot run it: "${name}". ` +
|
|
597
|
+
`Use one of: ${allowed.join(', ')} — or omit "agent" to use ${state.agentName}.`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
function stringifyOutput(v) {
|
|
603
|
+
return typeof v === 'string' ? v : JSON.stringify(v, null, 2);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Validation uses the Zod schema when the caller supplied one — transforms,
|
|
607
|
+
* defaults and refinements included. A run resumed by generic tooling without
|
|
608
|
+
* the original schema degrades to a structural JSON Schema check.
|
|
609
|
+
*/
|
|
610
|
+
function validateOutput(state, env, raw) {
|
|
611
|
+
let args;
|
|
612
|
+
try {
|
|
613
|
+
args = JSON.parse(raw || '{}');
|
|
614
|
+
}
|
|
615
|
+
catch (e) {
|
|
616
|
+
return { ok: false, message: `error: arguments are not valid JSON: ${String(e)}` };
|
|
617
|
+
}
|
|
618
|
+
const payload = state.spec.outputWrapped ? args?.value : args;
|
|
619
|
+
if (env.output) {
|
|
620
|
+
const parsed = env.output.safeParse(payload);
|
|
621
|
+
return parsed.success
|
|
622
|
+
? { ok: true, value: parsed.data }
|
|
623
|
+
: {
|
|
624
|
+
ok: false,
|
|
625
|
+
message: `error: output does not match the schema:\n${z.prettifyError(parsed.error)}`,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const schema = state.spec.outputSchema;
|
|
629
|
+
if (!state.spec.outputWrapped && schema?.required?.length) {
|
|
630
|
+
const obj = (payload ?? {});
|
|
631
|
+
const missing = schema.required.filter((k) => obj[k] === undefined);
|
|
632
|
+
if (missing.length) {
|
|
633
|
+
return { ok: false, message: `error: missing required fields: ${missing.join(', ')}` };
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return { ok: true, value: payload };
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Appends the tool result, folds in whatever the tool did to memory or skills,
|
|
640
|
+
* and performs a hand-off when the call was a `transfer_to_*`.
|
|
641
|
+
*/
|
|
642
|
+
export async function applyToolResult(state, callId, outcome, env) {
|
|
643
|
+
const call = state.trajectory.find((n) => n.type === 'tool_call' && n.callId === callId);
|
|
644
|
+
if (!call) {
|
|
645
|
+
throw new Error(`no pending tool call ${callId} in run ${state.runId}`);
|
|
646
|
+
}
|
|
647
|
+
const b = begin(state, env);
|
|
648
|
+
b.add({
|
|
649
|
+
type: 'tool_result',
|
|
650
|
+
callId,
|
|
651
|
+
name: call.name,
|
|
652
|
+
result: await put(env, outcome.output),
|
|
653
|
+
isError: outcome.isError,
|
|
654
|
+
durationMs: outcome.durationMs,
|
|
655
|
+
});
|
|
656
|
+
for (const effect of outcome.effects ?? []) {
|
|
657
|
+
if (effect.kind === 'memory_op') {
|
|
658
|
+
await addMemoryOp(b, env, effect.spec);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
await addSkillLoad(b, env, effect.spec);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const target = outcome.isError ? undefined : handoffTarget(call.name);
|
|
665
|
+
let agentName = state.agentName;
|
|
666
|
+
if (target) {
|
|
667
|
+
b.add({
|
|
668
|
+
type: 'handoff',
|
|
669
|
+
from: state.agentName,
|
|
670
|
+
to: target,
|
|
671
|
+
reason: asReason(call),
|
|
672
|
+
});
|
|
673
|
+
agentName = target;
|
|
674
|
+
}
|
|
675
|
+
const pendingToolCalls = state.pendingToolCalls.filter((id) => id !== callId);
|
|
676
|
+
const phase = pendingToolCalls.length
|
|
677
|
+
? 'awaiting_tools'
|
|
678
|
+
: state.pendingFork?.branches.length
|
|
679
|
+
? 'awaiting_branches'
|
|
680
|
+
: 'awaiting_llm';
|
|
681
|
+
return b.commit({ phase, pendingToolCalls, agentName });
|
|
682
|
+
}
|
|
683
|
+
function asReason(call) {
|
|
684
|
+
// The reason travels in the call arguments, which are behind a payload; the
|
|
685
|
+
// preview is enough for a human-readable trace and costs no I/O.
|
|
686
|
+
const preview = call.args.preview;
|
|
687
|
+
if (!preview) {
|
|
688
|
+
return undefined;
|
|
689
|
+
}
|
|
690
|
+
const reason = parseArgs(preview).reason;
|
|
691
|
+
return typeof reason === 'string' ? reason : undefined;
|
|
692
|
+
}
|
|
693
|
+
async function addMemoryOp(b, env, spec) {
|
|
694
|
+
b.add({
|
|
695
|
+
type: 'memory_op',
|
|
696
|
+
op: spec.op,
|
|
697
|
+
store: spec.store,
|
|
698
|
+
scope: spec.scope,
|
|
699
|
+
opId: spec.opId,
|
|
700
|
+
recordId: spec.recordId,
|
|
701
|
+
revision: spec.revision,
|
|
702
|
+
before: spec.before === undefined ? undefined : await put(env, spec.before),
|
|
703
|
+
after: spec.after === undefined ? undefined : await put(env, spec.after),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
async function addSkillLoad(b, env, spec) {
|
|
707
|
+
b.add({
|
|
708
|
+
type: 'load_skills',
|
|
709
|
+
provider: spec.provider,
|
|
710
|
+
skills: spec.skills.map((s) => ({
|
|
711
|
+
name: s.name,
|
|
712
|
+
version: s.version,
|
|
713
|
+
contentHash: skillContentHash(s),
|
|
714
|
+
file: s.file,
|
|
715
|
+
})),
|
|
716
|
+
content: await put(env, renderSkills(spec.skills)),
|
|
717
|
+
toolNames: [...new Set(spec.skills.flatMap((s) => (s.tools ?? []).map((t) => t.name)))],
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
/** Records a memory recall or mutation performed by the driver. */
|
|
721
|
+
export async function applyMemoryEffect(state, effect, env) {
|
|
722
|
+
const b = begin(state, env);
|
|
723
|
+
if (effect.kind === 'op') {
|
|
724
|
+
await addMemoryOp(b, env, effect);
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
b.add({
|
|
728
|
+
type: 'memory_recall',
|
|
729
|
+
store: effect.store,
|
|
730
|
+
scope: effect.scope,
|
|
731
|
+
query: effect.query,
|
|
732
|
+
hits: effect.hits,
|
|
733
|
+
content: await put(env, effect.content),
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return b.commit({});
|
|
737
|
+
}
|
|
738
|
+
/** Records a skill activation and the tools it unlocked. */
|
|
739
|
+
export async function applySkillLoad(state, provider, skills, env) {
|
|
740
|
+
const b = begin(state, env);
|
|
741
|
+
await addSkillLoad(b, env, { kind: 'skill_load', provider, skills });
|
|
742
|
+
return b.commit({});
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Containment, not deletion: the covered nodes stay in the trajectory for audit
|
|
746
|
+
* and replay, and only the projection skips them. The log stays append-only.
|
|
747
|
+
*/
|
|
748
|
+
export async function applyCompaction(state, spec, env) {
|
|
749
|
+
const clock = env.clock ?? systemClock;
|
|
750
|
+
const b = begin(state, env);
|
|
751
|
+
b.add({
|
|
752
|
+
type: 'compaction',
|
|
753
|
+
callId: spec.callId ?? `compact_${clock.newId()}`,
|
|
754
|
+
covers: spec.covers,
|
|
755
|
+
summary: await put(env, spec.summary),
|
|
756
|
+
reason: spec.reason,
|
|
757
|
+
usage: spec.usage ?? zeroUsage(),
|
|
758
|
+
});
|
|
759
|
+
return b.commit({});
|
|
760
|
+
}
|
|
761
|
+
export function applyFailure(state, error) {
|
|
762
|
+
return { ...state, phase: 'failed', error };
|
|
763
|
+
}
|
|
764
|
+
// ---------------------------------------------------------------------------
|
|
765
|
+
// Fork / join
|
|
766
|
+
// ---------------------------------------------------------------------------
|
|
767
|
+
/**
|
|
768
|
+
* Derives a child state seeded with *real nodes* copied from the parent, not a
|
|
769
|
+
* frozen `Message[]` blob: the branch can compact its own inherited context,
|
|
770
|
+
* and at join those nodes splice back into one trajectory. The copy is a
|
|
771
|
+
* snapshot, so the branch can execute in another process.
|
|
772
|
+
*/
|
|
773
|
+
export async function createChildState(state, forkId, branch, env) {
|
|
774
|
+
const fork = state.trajectory.find((n) => n.type === 'fork' && n.callId === forkId);
|
|
775
|
+
const plan = fork?.branches.find((b) => b.name === branch);
|
|
776
|
+
if (!fork || !plan) {
|
|
777
|
+
throw new Error(`unknown branch ${branch} of fork ${forkId}`);
|
|
778
|
+
}
|
|
779
|
+
const prefix = branchPrefix(state, fork, plan.agent);
|
|
780
|
+
const child = {
|
|
781
|
+
version: 1,
|
|
782
|
+
runId: plan.childRunId,
|
|
783
|
+
spec: {
|
|
784
|
+
startAgent: plan.agent,
|
|
785
|
+
parent: { runId: state.runId, forkId, branch },
|
|
786
|
+
forkDepth: state.spec.forkDepth + 1,
|
|
787
|
+
maxForkDepth: state.spec.maxForkDepth,
|
|
788
|
+
// Where the branch's own history begins: the parent already holds
|
|
789
|
+
// the prefix, so only what follows is spliced back at join.
|
|
790
|
+
prefixLength: prefix.length,
|
|
791
|
+
},
|
|
792
|
+
agentName: plan.agent,
|
|
793
|
+
phase: 'created',
|
|
794
|
+
trajectory: prefix,
|
|
795
|
+
pendingToolCalls: [],
|
|
796
|
+
usage: zeroUsage(),
|
|
797
|
+
context: state.context,
|
|
798
|
+
};
|
|
799
|
+
const b = begin(child, env);
|
|
800
|
+
if (prefix.length) {
|
|
801
|
+
// The branch inherited the turn that called `fork`, so the honest way to
|
|
802
|
+
// hand it its assignment is to answer that call: the model sees a tool
|
|
803
|
+
// result for a tool it just called, instead of a second user message
|
|
804
|
+
// appearing after its own. It also keeps the fork call itself in the
|
|
805
|
+
// prompt — otherwise `repairToolCalls` strips it as unanswered and the
|
|
806
|
+
// branch never learns that it fanned out at all.
|
|
807
|
+
b.add({
|
|
808
|
+
type: 'tool_result',
|
|
809
|
+
callId: fork.callId,
|
|
810
|
+
name: FORK_TOOL,
|
|
811
|
+
result: await put(env, branchBrief(fork, plan, await get(env, plan.instructions))),
|
|
812
|
+
isError: false,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
// Nothing to answer: a `context: 'none'` branch has no history, so its
|
|
817
|
+
// assignment arrives the only way it can, as the opening message.
|
|
818
|
+
b.add({
|
|
819
|
+
type: 'user_input',
|
|
820
|
+
// Reuses the instructions payload already written by the fork node.
|
|
821
|
+
content: [{ type: 'text', text: plan.instructions }],
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
return b.commit({ phase: 'awaiting_llm' });
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* What an inheriting branch is told when its own `fork` call comes back. The
|
|
828
|
+
* parent's fork arguments are already in the prompt above it, so the siblings'
|
|
829
|
+
* assignments need no repeating — but which of the branches this run *is*, and
|
|
830
|
+
* what happens to what it produces, exist nowhere else. Without them a branch
|
|
831
|
+
* re-derives work a sibling already owns and writes an answer shaped for a user
|
|
832
|
+
* rather than for a merge.
|
|
833
|
+
*/
|
|
834
|
+
function branchBrief(fork, plan, instructions) {
|
|
835
|
+
const siblings = fork.branches.filter((b) => b.name !== plan.name).map((b) => `"${b.name}"`);
|
|
836
|
+
return [
|
|
837
|
+
`You are branch "${plan.name}" of the fork above. Your assignment, and your only one:`,
|
|
838
|
+
'',
|
|
839
|
+
instructions,
|
|
840
|
+
'',
|
|
841
|
+
`Running in parallel, on the assignments the call above gave them: ${siblings.join(', ')}. ` +
|
|
842
|
+
'You cannot see their work and they cannot see yours, so anything you do on their ' +
|
|
843
|
+
'part is duplicated effort, and anything you leave to them will be there. This branch ' +
|
|
844
|
+
'ends with your final answer; that answer alone rejoins the conversation above, ' +
|
|
845
|
+
'merged with theirs.',
|
|
846
|
+
].join('\n');
|
|
847
|
+
}
|
|
848
|
+
/** The parent history a branch starts from, as decided by `contextMode`. */
|
|
849
|
+
function branchPrefix(state, fork, agent) {
|
|
850
|
+
if (fork.contextMode === 'none') {
|
|
851
|
+
return [];
|
|
852
|
+
}
|
|
853
|
+
const visible = projected(state.trajectory);
|
|
854
|
+
const at = visible.findIndex((n) => n.id === fork.id);
|
|
855
|
+
const before = at === -1 ? visible : visible.slice(0, at);
|
|
856
|
+
return before.filter((n) => {
|
|
857
|
+
// Skills are agent-scoped. Carrying the instructions text across an
|
|
858
|
+
// agent change would leave the branch reading about tools it cannot
|
|
859
|
+
// call, so text and tools have to disappear together.
|
|
860
|
+
if (n.type === 'load_skills') {
|
|
861
|
+
return n.agent === agent;
|
|
862
|
+
}
|
|
863
|
+
// 'compact': start from the conversation, not the parent's tool chatter.
|
|
864
|
+
// The now-unanswered tool calls are dropped by `repairToolCalls`.
|
|
865
|
+
if (fork.contextMode === 'compact') {
|
|
866
|
+
return n.type !== 'tool_call' && n.type !== 'tool_result' && n.type !== 'memory_recall';
|
|
867
|
+
}
|
|
868
|
+
return true;
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Folds branches back into the parent. Each branch's history is nested inside
|
|
873
|
+
* its row of the join, in *declared* branch order — so the result is identical
|
|
874
|
+
* regardless of completion order, and a walk of the parent array never trips
|
|
875
|
+
* over someone else's nodes.
|
|
876
|
+
*/
|
|
877
|
+
export async function applyJoin(state, forkId, results, env) {
|
|
878
|
+
const fork = state.trajectory.find((n) => n.type === 'fork' && n.callId === forkId);
|
|
879
|
+
if (!fork) {
|
|
880
|
+
throw new Error(`unknown fork ${forkId} in run ${state.runId}`);
|
|
881
|
+
}
|
|
882
|
+
const byName = new Map(results.map((r) => [r.name, r]));
|
|
883
|
+
const b = begin(state, env);
|
|
884
|
+
const rows = [];
|
|
885
|
+
let usage = zeroUsage();
|
|
886
|
+
for (const plan of fork.branches) {
|
|
887
|
+
const r = byName.get(plan.name);
|
|
888
|
+
if (!r) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
rows.push({
|
|
892
|
+
name: plan.name,
|
|
893
|
+
agent: plan.agent,
|
|
894
|
+
status: r.status,
|
|
895
|
+
output: await put(env, r.output),
|
|
896
|
+
error: r.error,
|
|
897
|
+
usage: r.usage,
|
|
898
|
+
// Only what the branch itself did: the inherited prefix is the
|
|
899
|
+
// parent's own history and is already right there in the array.
|
|
900
|
+
nodes: r.childState.trajectory.slice(r.childState.spec.prefixLength ?? 0),
|
|
901
|
+
});
|
|
902
|
+
usage = addUsage(usage, r.usage);
|
|
903
|
+
}
|
|
904
|
+
b.add({ type: 'join', callId: forkId, branches: rows, usage });
|
|
905
|
+
const remaining = (state.pendingFork?.branches ?? []).filter((n) => !byName.has(n));
|
|
906
|
+
return b.commit({
|
|
907
|
+
pendingFork: remaining.length ? { callId: forkId, branches: remaining } : undefined,
|
|
908
|
+
phase: remaining.length
|
|
909
|
+
? 'awaiting_branches'
|
|
910
|
+
: state.pendingToolCalls.length
|
|
911
|
+
? 'awaiting_tools'
|
|
912
|
+
: 'awaiting_llm',
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
// ---------------------------------------------------------------------------
|
|
916
|
+
// Small read helpers used by drivers
|
|
917
|
+
// ---------------------------------------------------------------------------
|
|
918
|
+
export function lastUserInput(state) {
|
|
919
|
+
const nodes = state.trajectory;
|
|
920
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
921
|
+
const n = nodes[i];
|
|
922
|
+
if (n.type === 'user_input' && !n.synthetic) {
|
|
923
|
+
return n;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return undefined;
|
|
927
|
+
}
|
|
928
|
+
export { Agent, AgentRegistry };
|
|
929
|
+
//# sourceMappingURL=kernel.js.map
|