@zenera/neo 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/dist/agent.d.ts +66 -0
  4. package/dist/agent.js +97 -0
  5. package/dist/architecture.d.ts +74 -0
  6. package/dist/architecture.js +105 -0
  7. package/dist/embedding.d.ts +60 -0
  8. package/dist/embedding.js +21 -0
  9. package/dist/embeddings/gemini.d.ts +24 -0
  10. package/dist/embeddings/gemini.js +71 -0
  11. package/dist/embeddings/index.d.ts +4 -0
  12. package/dist/embeddings/index.js +4 -0
  13. package/dist/embeddings/openai.d.ts +18 -0
  14. package/dist/embeddings/openai.js +27 -0
  15. package/dist/embeddings/openrouter.d.ts +21 -0
  16. package/dist/embeddings/openrouter.js +54 -0
  17. package/dist/events.d.ts +144 -0
  18. package/dist/events.js +110 -0
  19. package/dist/ids.d.ts +14 -0
  20. package/dist/ids.js +33 -0
  21. package/dist/index.d.ts +25 -0
  22. package/dist/index.js +25 -0
  23. package/dist/inspect/client.d.ts +2 -0
  24. package/dist/inspect/client.js +1326 -0
  25. package/dist/inspect/css.d.ts +2 -0
  26. package/dist/inspect/css.js +164 -0
  27. package/dist/inspect/index.d.ts +9 -0
  28. package/dist/inspect/index.js +12 -0
  29. package/dist/inspect/page.d.ts +3 -0
  30. package/dist/inspect/page.js +93 -0
  31. package/dist/inspect/report.d.ts +54 -0
  32. package/dist/inspect/report.js +62 -0
  33. package/dist/kernel.d.ts +178 -0
  34. package/dist/kernel.js +929 -0
  35. package/dist/memory-stores/factory.d.ts +15 -0
  36. package/dist/memory-stores/factory.js +29 -0
  37. package/dist/memory-stores/file.d.ts +37 -0
  38. package/dist/memory-stores/file.js +174 -0
  39. package/dist/memory-stores/in-memory.d.ts +17 -0
  40. package/dist/memory-stores/in-memory.js +80 -0
  41. package/dist/memory-stores/index.d.ts +4 -0
  42. package/dist/memory-stores/index.js +5 -0
  43. package/dist/memory.d.ts +115 -0
  44. package/dist/memory.js +213 -0
  45. package/dist/model.d.ts +25 -0
  46. package/dist/model.js +2 -0
  47. package/dist/models/anthropic.d.ts +36 -0
  48. package/dist/models/anthropic.js +293 -0
  49. package/dist/models/factory.d.ts +202 -0
  50. package/dist/models/factory.js +533 -0
  51. package/dist/models/gemini.d.ts +45 -0
  52. package/dist/models/gemini.js +368 -0
  53. package/dist/models/index.d.ts +7 -0
  54. package/dist/models/index.js +7 -0
  55. package/dist/models/openai-chat.d.ts +19 -0
  56. package/dist/models/openai-chat.js +203 -0
  57. package/dist/models/openai-responses.d.ts +31 -0
  58. package/dist/models/openai-responses.js +258 -0
  59. package/dist/models/openrouter.d.ts +44 -0
  60. package/dist/models/openrouter.js +230 -0
  61. package/dist/payload-stores/factory.d.ts +15 -0
  62. package/dist/payload-stores/factory.js +30 -0
  63. package/dist/payload-stores/file.d.ts +29 -0
  64. package/dist/payload-stores/file.js +112 -0
  65. package/dist/payload-stores/in-memory.d.ts +17 -0
  66. package/dist/payload-stores/in-memory.js +40 -0
  67. package/dist/payload-stores/index.d.ts +4 -0
  68. package/dist/payload-stores/index.js +5 -0
  69. package/dist/payload.d.ts +89 -0
  70. package/dist/payload.js +181 -0
  71. package/dist/project/config.d.ts +302 -0
  72. package/dist/project/config.js +290 -0
  73. package/dist/project/index.d.ts +4 -0
  74. package/dist/project/index.js +4 -0
  75. package/dist/project/load.d.ts +142 -0
  76. package/dist/project/load.js +430 -0
  77. package/dist/project/refs.d.ts +23 -0
  78. package/dist/project/refs.js +70 -0
  79. package/dist/prompt.d.ts +66 -0
  80. package/dist/prompt.js +62 -0
  81. package/dist/runner.d.ts +103 -0
  82. package/dist/runner.js +537 -0
  83. package/dist/services.d.ts +23 -0
  84. package/dist/services.js +44 -0
  85. package/dist/skill-providers/factory.d.ts +16 -0
  86. package/dist/skill-providers/factory.js +26 -0
  87. package/dist/skill-providers/file.d.ts +44 -0
  88. package/dist/skill-providers/file.js +209 -0
  89. package/dist/skill-providers/index.d.ts +4 -0
  90. package/dist/skill-providers/index.js +5 -0
  91. package/dist/skill-providers/static.d.ts +13 -0
  92. package/dist/skill-providers/static.js +47 -0
  93. package/dist/skills.d.ts +102 -0
  94. package/dist/skills.js +232 -0
  95. package/dist/state.d.ts +86 -0
  96. package/dist/state.js +49 -0
  97. package/dist/tools/exa.d.ts +23 -0
  98. package/dist/tools/exa.js +389 -0
  99. package/dist/tools/index.d.ts +4 -0
  100. package/dist/tools/index.js +6 -0
  101. package/dist/tools/sandbox.d.ts +172 -0
  102. package/dist/tools/sandbox.js +602 -0
  103. package/dist/tools/workspace.d.ts +53 -0
  104. package/dist/tools/workspace.js +1034 -0
  105. package/dist/trajectory.d.ts +213 -0
  106. package/dist/trajectory.js +232 -0
  107. package/dist/types.d.ts +195 -0
  108. package/dist/types.js +134 -0
  109. package/package.json +98 -0
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