@nredd/pi-subagents 0.0.0-stage → 0.22.0-nredd.10

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1444 -2
  3. package/dist/chunks/chunk-MSGGFB5Q.ts +6755 -0
  4. package/dist/chunks/chunk-MSGGFB5Q.ts.map +7 -0
  5. package/dist/chunks/conversation-viewer-A7AMRDVY.ts +15 -0
  6. package/dist/chunks/conversation-viewer-A7AMRDVY.ts.map +7 -0
  7. package/dist/index.ts +9874 -0
  8. package/dist/index.ts.map +7 -0
  9. package/package.json +82 -4
  10. package/src/abortable.ts +43 -0
  11. package/src/agent-color.ts +161 -0
  12. package/src/agent-file-toggle.ts +298 -0
  13. package/src/agent-manager.ts +2431 -0
  14. package/src/agent-runner.ts +1703 -0
  15. package/src/agent-source-badge.ts +75 -0
  16. package/src/agent-types.ts +346 -0
  17. package/src/child-context.ts +33 -0
  18. package/src/clock.ts +25 -0
  19. package/src/completion-outbox.ts +144 -0
  20. package/src/context.ts +58 -0
  21. package/src/cross-extension-rpc.ts +200 -0
  22. package/src/custom-agents.ts +441 -0
  23. package/src/default-agents.ts +126 -0
  24. package/src/detached-results.ts +150 -0
  25. package/src/enabled-models.ts +181 -0
  26. package/src/enterprise-local.ts +67 -0
  27. package/src/env.ts +33 -0
  28. package/src/error-message.ts +11 -0
  29. package/src/git-path.ts +66 -0
  30. package/src/group-join.ts +142 -0
  31. package/src/home-paths.ts +27 -0
  32. package/src/index.ts +5047 -0
  33. package/src/invocation-config.ts +158 -0
  34. package/src/memory.ts +179 -0
  35. package/src/mention-clone.ts +200 -0
  36. package/src/mention.ts +141 -0
  37. package/src/model-call.ts +83 -0
  38. package/src/model-resolver.ts +333 -0
  39. package/src/model-scope.ts +71 -0
  40. package/src/model-selection.ts +170 -0
  41. package/src/model-tiers.ts +176 -0
  42. package/src/nested-tools.ts +512 -0
  43. package/src/notification-result.ts +118 -0
  44. package/src/output-file.ts +186 -0
  45. package/src/package-resources.ts +639 -0
  46. package/src/plan-mode-children.ts +176 -0
  47. package/src/process-budget.ts +346 -0
  48. package/src/prompts.ts +161 -0
  49. package/src/quota-admission.ts +121 -0
  50. package/src/resource-loader.ts +65 -0
  51. package/src/schedule-store.ts +153 -0
  52. package/src/schedule.ts +514 -0
  53. package/src/settings.ts +851 -0
  54. package/src/skill-loader.ts +288 -0
  55. package/src/stale-ctx.ts +16 -0
  56. package/src/startup-error.ts +29 -0
  57. package/src/status-note.ts +113 -0
  58. package/src/structured-output.ts +130 -0
  59. package/src/types.ts +487 -0
  60. package/src/ui/agent-mention.ts +219 -0
  61. package/src/ui/agent-row.ts +245 -0
  62. package/src/ui/agent-widget.ts +722 -0
  63. package/src/ui/conversation-viewer.ts +837 -0
  64. package/src/ui/fallback-flag.ts +114 -0
  65. package/src/ui/fleet-list.ts +1020 -0
  66. package/src/ui/fleet-state.ts +154 -0
  67. package/src/ui/notification-view.ts +139 -0
  68. package/src/ui/schedule-menu.ts +105 -0
  69. package/src/ui/select-item.ts +45 -0
  70. package/src/ui/viewer-keys.ts +39 -0
  71. package/src/ui/workflow-card.ts +459 -0
  72. package/src/ui/workflow-dialog.ts +1138 -0
  73. package/src/ui/workflow-menu.ts +193 -0
  74. package/src/usage.ts +167 -0
  75. package/src/workflow/collisions.ts +123 -0
  76. package/src/workflow/entry.ts +47 -0
  77. package/src/workflow/host.ts +398 -0
  78. package/src/workflow/journal.ts +176 -0
  79. package/src/workflow/json-schema.ts +129 -0
  80. package/src/workflow/meta.ts +329 -0
  81. package/src/workflow/progress.ts +554 -0
  82. package/src/workflow/runtime.ts +1249 -0
  83. package/src/workflow/saved.ts +259 -0
  84. package/src/workflow/task.ts +317 -0
  85. package/src/workflow/tool-description.ts +200 -0
  86. package/src/workflow/worker-source.ts +903 -0
  87. package/src/worktree.ts +520 -0
  88. package/src/xml.ts +13 -0
@@ -0,0 +1,903 @@
1
+ /**
2
+ * worker-source.ts — the JavaScript that runs inside the workflow worker thread.
3
+ *
4
+ * The host spawns this with `new Worker(WORKER_SOURCE, { eval: true })`, so the
5
+ * source has to be an inlined string: `AGENTS.md` forbids dynamic `import()`,
6
+ * and a file path would have to survive bundling. Keeping it as a template
7
+ * literal costs editor tooling but nothing else — the worker is plain CommonJS
8
+ * JavaScript and never sees the TypeScript pipeline.
9
+ *
10
+ * Two boundaries stack here, and they are not the same boundary:
11
+ *
12
+ * host thread ←postMessage→ worker thread ←vm context→ workflow script
13
+ *
14
+ * The worker/host split exists for *killability*: `worker.terminate()` stops a
15
+ * runaway script mid-loop, which an in-process `vm` timeout cannot do once the
16
+ * script is inside an `await`. The vm context exists for *determinism and
17
+ * accident-avoidance*, not security — see the note on `codeGeneration` below.
18
+ *
19
+ * ## Why the context gets no host built-ins
20
+ *
21
+ * `vm.createContext(sandbox)` gives the script a fresh realm that already owns
22
+ * `Object`, `Array`, `JSON`, `Math`, `Date`, `Promise`, `Map`, `Set`. We inject
23
+ * *only* our own globals on top. Injecting host built-ins instead would hand the
24
+ * script `Object.constructor` → the **host** `Function`, i.e. a compiler for
25
+ * arbitrary host-realm code.
26
+ *
27
+ * That said: our injected globals are themselves host closures, so
28
+ * `agent.constructor` is still the host `Function`. The hygiene shrinks the
29
+ * surface; it does not close the hole. `codeGeneration: { strings: false }`
30
+ * makes realm-local `Function` and `eval` throw `EvalError`, but does not stop a
31
+ * captured host `Function`. The stack formatter stays realm-native rather than
32
+ * adding another host callback. Treat this as a determinism boundary, not a
33
+ * security boundary against a hostile script.
34
+ *
35
+ * ## Why determinism is a prelude and not a stub
36
+ *
37
+ * Because `Date` and `Math` come *from the realm*, they cannot be neutered by
38
+ * injection — there is nothing to inject over. So the compiled source is
39
+ * prefixed with a prelude that runs inside the realm and reassigns `Date.now`
40
+ * and `Math.random` in place, then lexically shadows `Date` with a subclass
41
+ * whose zero-argument constructor throws. Lexical shadowing rather than a global
42
+ * assignment because a `const` in the IIFE scope cannot be reached around.
43
+ *
44
+ * Determinism is enforced because a workflow's journal is replayed by prefix on
45
+ * resume: a script that reads the clock produces a different prefix on the
46
+ * second run and the replay silently diverges.
47
+ */
48
+
49
+ /**
50
+ * Runs inside the realm, ahead of the script body, on a single line.
51
+ *
52
+ * The shared compiler puts this and the wrapper on the body's first line.
53
+ * No lineOffset is needed (Bun ignores it); only first-line columns need mapping.
54
+ */
55
+ const DETERMINISM_PRELUDE =
56
+ "const Date = (function () {" +
57
+ " const RealDate = globalThis.Date;" +
58
+ " const die = function (what) {" +
59
+ " throw new Error(what + \" is unavailable in workflow scripts (breaks resume)." +
60
+ " Stamp results after the workflow returns, or pass timestamps via `args`.\");" +
61
+ " };" +
62
+ " RealDate.now = function () { return die(\"Date.now()\"); };" +
63
+ " Math.random = function () { return die(\"Math.random()\"); };" +
64
+ " return class WorkflowDate extends RealDate {" +
65
+ " constructor() { if (arguments.length === 0) die(\"new Date()\"); super(...arguments); }" +
66
+ " };" +
67
+ "})();";
68
+
69
+ export const WORKER_SOURCE = `"use strict";
70
+
71
+ const { parentPort, workerData } = require("node:worker_threads");
72
+ const vm = require("node:vm");
73
+
74
+ const port = parentPort;
75
+ const ITEM_CAP = workerData.itemCap;
76
+ const PRELUDE = ${JSON.stringify(DETERMINISM_PRELUDE)};
77
+
78
+ /* ------------------------------------------------------------------ *
79
+ * RPC to the host
80
+ *
81
+ * The script never touches the agent manager. Every effect leaves as a
82
+ * "call" message and comes back as a "response", so the host owns the
83
+ * semaphore, the caps, and the abort story.
84
+ * ------------------------------------------------------------------ */
85
+
86
+ let nextCallId = 1;
87
+ const pendingCalls = new Map();
88
+
89
+ /**
90
+ * Output tokens this run has spent, as last reported by the host.
91
+ *
92
+ * A mirror, not a tally: every response carries the host's current total, so
93
+ * there is exactly one counter and it cannot drift. Between responses it cannot
94
+ * be stale in any way the script could observe — tokens only accrue through
95
+ * agents, and an agent's response is the only thing the script waits on.
96
+ */
97
+ let spentOutput = 0;
98
+
99
+ function callHost(method, payload, onModelNote) {
100
+ // Drain first, so the phase() that named this agent reaches the host ahead of
101
+ // the agent entry rather than a tick behind it.
102
+ flushProgress();
103
+ return new Promise(function (resolve, reject) {
104
+ const callId = nextCallId++;
105
+ pendingCalls.set(callId, { resolve: resolve, reject: reject, onModelNote: onModelNote });
106
+ port.postMessage({ type: "call", callId: callId, method: method, payload: payload });
107
+ });
108
+ }
109
+
110
+ port.on("message", function (message) {
111
+ if (!message || message.type !== "response") return;
112
+ if (typeof message.spent === "number") spentOutput = message.spent;
113
+ const waiter = pendingCalls.get(message.callId);
114
+ if (!waiter) return;
115
+ pendingCalls.delete(message.callId);
116
+ if (message.ok) {
117
+ if (message.modelNote !== undefined && waiter.onModelNote) waiter.onModelNote(message.modelNote);
118
+ waiter.resolve(message.value);
119
+ return;
120
+ }
121
+ const error = new Error(message.error || "The workflow host rejected the call.");
122
+ // Fatal errors are the run's, not the item's: parallel() and pipeline()
123
+ // swallow ordinary failures into null, and a cap breach must not be
124
+ // silently absorbed that way.
125
+ if (message.fatal) error.workflowFatal = true;
126
+ waiter.reject(error);
127
+ });
128
+
129
+ function isFatal(error) {
130
+ return !!(error && typeof error === "object" && error.workflowFatal === true);
131
+ }
132
+
133
+ /* ------------------------------------------------------------------ *
134
+ * Progress entries
135
+ * ------------------------------------------------------------------ */
136
+
137
+ let progressQueue = [];
138
+ let flushTimer = null;
139
+
140
+ function emit(entry) {
141
+ progressQueue.push(entry);
142
+ // Batched on a macrotask: a fan-out emits a burst of phase/log entries in one
143
+ // turn, and the host renders once per batch rather than once per entry.
144
+ if (flushTimer === null) flushTimer = setTimeout(flushProgress, 0);
145
+ }
146
+
147
+ function flushProgress() {
148
+ if (flushTimer !== null) {
149
+ clearTimeout(flushTimer);
150
+ flushTimer = null;
151
+ }
152
+ if (progressQueue.length === 0) return;
153
+ const batch = progressQueue;
154
+ progressQueue = [];
155
+ port.postMessage({ type: "progress", entries: batch });
156
+ }
157
+
158
+ /* ------------------------------------------------------------------ *
159
+ * The JSON boundary
160
+ *
161
+ * Checked here rather than relying on structured clone, which happily
162
+ * carries cycles, BigInt and Maps that the progress log and the resume
163
+ * journal cannot represent. Rejecting loudly beats writing a journal
164
+ * that will not replay.
165
+ * ------------------------------------------------------------------ */
166
+
167
+ let realmObjectPrototype = null;
168
+ /**
169
+ * The realm's own \`JSON.parse\`.
170
+ *
171
+ * Module-scope, not local to main(), because \`agent({ schema })\` parses its
172
+ * result here — outside main's closure — and the object has to carry the
173
+ * *script's* Object.prototype, not the worker's, or \`instanceof Object\` fails
174
+ * inside the script it was handed to.
175
+ */
176
+ let realmParse = null;
177
+
178
+ /**
179
+ * The top-level script's scope.
180
+ *
181
+ * Module-scope because a nested \`workflow()\` needs the realm-native function
182
+ * compiler that \`main()\` builds, and because the compiled child function is
183
+ * cached per body — see {@link workflowIn}.
184
+ */
185
+ let rootScope = null;
186
+ /**
187
+ * The vm context every script runs in.
188
+ *
189
+ * Held so a nested \`workflow()\` can compile its child there. Compiled from
190
+ * *outside* the realm, with \`vm.Script\`, because the context itself has
191
+ * \`codeGeneration.strings\` off — the script cannot build code, but the worker
192
+ * that owns it still can.
193
+ */
194
+ let realmContext = null;
195
+ /** Nested invocations made so far, against \`workerData.nestedCap\`. */
196
+ let nestedCount = 0;
197
+
198
+ function boundaryError(what, path) {
199
+ return new Error(
200
+ "Cannot pass " + what + " across the workflow VM boundary (at " + path + ")."
201
+ );
202
+ }
203
+
204
+ function assertBoundary(value, path, seen) {
205
+ if (value === null) return;
206
+ const kind = typeof value;
207
+ if (kind === "string" || kind === "boolean") return;
208
+ if (kind === "number") {
209
+ if (!Number.isFinite(value)) throw boundaryError("a non-finite number", path);
210
+ return;
211
+ }
212
+ if (kind === "undefined") {
213
+ if (path === "the workflow result") return;
214
+ throw boundaryError("undefined", path);
215
+ }
216
+ if (kind === "bigint") throw boundaryError("a BigInt", path);
217
+ if (kind === "symbol") throw boundaryError("a symbol", path);
218
+ if (kind === "function") throw boundaryError("a function", path);
219
+ if (kind !== "object") throw boundaryError("a " + kind, path);
220
+
221
+ if (seen.has(value)) throw boundaryError("a circular structure", path);
222
+ seen.add(value);
223
+
224
+ if (Object.getOwnPropertySymbols(value).length > 0) {
225
+ throw boundaryError("an object with symbol keys", path);
226
+ }
227
+
228
+ if (Array.isArray(value)) {
229
+ const length = value.length;
230
+ for (let i = 0; i < length; i++) {
231
+ // A sparse array round-trips through JSON as nulls, which silently
232
+ // changes the data. Reject instead.
233
+ if (!Object.prototype.hasOwnProperty.call(value, i)) {
234
+ throw boundaryError("a sparse array", path + "[" + i + "]");
235
+ }
236
+ assertBoundary(value[i], path + "[" + i + "]", seen);
237
+ }
238
+ seen.delete(value);
239
+ return;
240
+ }
241
+
242
+ const prototype = Object.getPrototypeOf(value);
243
+ // Two prototypes are legitimate: the realm's own Object.prototype (anything
244
+ // the script built) and the worker's (arrays we hand back from parallel).
245
+ // Everything else — Map, Set, Date, a class instance — loses meaning here.
246
+ if (prototype !== null && prototype !== realmObjectPrototype && prototype !== Object.prototype) {
247
+ throw boundaryError("a non-plain object", path);
248
+ }
249
+
250
+ const keys = Object.keys(value);
251
+ for (let i = 0; i < keys.length; i++) {
252
+ assertBoundary(value[keys[i]], path + "." + keys[i], seen);
253
+ }
254
+ seen.delete(value);
255
+ }
256
+
257
+ function checkBoundary(value, path) {
258
+ assertBoundary(value, path, new Set());
259
+ return value;
260
+ }
261
+
262
+ /* ------------------------------------------------------------------ *
263
+ * Realm helpers
264
+ *
265
+ * parallel() and pipeline() build their result arrays in worker code, but
266
+ * the script should get an array its own realm recognises — otherwise
267
+ * \`result instanceof Array\` is false and \`Array.isArray\` is the only thing
268
+ * that works. Item values are moved across untouched.
269
+ * ------------------------------------------------------------------ */
270
+
271
+ let realmNewArray = null;
272
+ let realmPush = null;
273
+
274
+ function toRealmArray(items) {
275
+ const array = realmNewArray();
276
+ for (let i = 0; i < items.length; i++) realmPush(array, items[i]);
277
+ return array;
278
+ }
279
+
280
+ function toList(value, what) {
281
+ if (!Array.isArray(value)) throw new Error(what + " expects an array.");
282
+ const length = value.length >>> 0;
283
+ if (length > ITEM_CAP) {
284
+ throw new Error(
285
+ what + " was given " + length + " items, over the limit of " + ITEM_CAP + "."
286
+ );
287
+ }
288
+ const out = [];
289
+ for (let i = 0; i < length; i++) out.push(value[i]);
290
+ return out;
291
+ }
292
+
293
+ function requireText(value, what) {
294
+ if (typeof value !== "string" || value.trim() === "") {
295
+ throw new Error(what + " requires a non-empty string.");
296
+ }
297
+ return value;
298
+ }
299
+
300
+ function optionalText(value, what) {
301
+ if (value === undefined || value === null) return undefined;
302
+ return requireText(value, what);
303
+ }
304
+
305
+ /**
306
+ * Reasoning effort a child may be spawned under — pi's \`ThinkingLevel\`.
307
+ *
308
+ * A superset of Claude Code's five, so a script written there runs here; the
309
+ * extra \`minimal\` is pi's own. Validated in the worker rather than the host
310
+ * because a typo should stop the script at the call that made it, not surface
311
+ * later as an agent that quietly ran at the wrong depth.
312
+ */
313
+ const EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"];
314
+
315
+ /**
316
+ * Every option \`agent()\` understands.
317
+ *
318
+ * Checked rather than ignored, because the alternative is the worst failure a
319
+ * ported script can have. Claude Code's \`agent()\` also takes \`schema\`, and its
320
+ * own canonical example uses it; quietly dropping it hands the script the
321
+ * agent's raw text where it expected a validated object, and the run then dies
322
+ * several lines later reading a field off a string. A typo behaves the same
323
+ * way. Naming the option costs one error message and no model calls.
324
+ */
325
+ const AGENT_OPTIONS = [
326
+ "label",
327
+ "phase",
328
+ "model",
329
+ "agentType",
330
+ "isolation",
331
+ "gate",
332
+ "resume",
333
+ "effort",
334
+ "schema",
335
+ ];
336
+
337
+ /** Claude Code options this runtime does not have, and why. */
338
+ const UNSUPPORTED_AGENT_OPTIONS = {};
339
+
340
+ /* ------------------------------------------------------------------ *
341
+ * Script globals
342
+ * ------------------------------------------------------------------ */
343
+
344
+ /**
345
+ * Phase indices are allocated once for the whole run, so parent and child never
346
+ * collide. What is per-scope is the *title to index* map: a child's
347
+ * \`phase("Scan")\` must not resolve to the parent's "Scan".
348
+ */
349
+ let nextPhaseIndex = 0;
350
+
351
+ /**
352
+ * One script's view of the world.
353
+ *
354
+ * A nested \`workflow()\` runs in this same worker and this same vm context —
355
+ * which is what makes it share the run's semaphore, agent counter, journal,
356
+ * abort signal and budget without any of them being plumbed anywhere. What it
357
+ * must NOT share is ambient phase state, so that lives here and the child's
358
+ * globals are closures over its own scope.
359
+ */
360
+ function makeScope(name, depth) {
361
+ const scope = {
362
+ name: name,
363
+ depth: depth,
364
+ // Prefixed into every phase title the child defines, which is the whole of
365
+ // how a nested run reads as its own group in the progress tree — no new
366
+ // entry type, no renderer change.
367
+ prefix: name === undefined ? "" : "\u25b8 " + name,
368
+ ambientPhaseIndex: undefined,
369
+ ambientPhaseTitle: undefined,
370
+ phaseIndexByTitle: new Map(),
371
+ };
372
+ scope.agent = function (prompt, opts) {
373
+ return agentIn(scope, prompt, opts);
374
+ };
375
+ scope.phase = function (title) {
376
+ return phaseIn(scope, title);
377
+ };
378
+ scope.log = function (message) {
379
+ return logIn(scope, message);
380
+ };
381
+ scope.workflow = function (ref, args) {
382
+ return workflowIn(scope, ref, args);
383
+ };
384
+ scope.console = makeConsole(scope);
385
+ return scope;
386
+ }
387
+
388
+ /** A scope's title for a phase: the child's own group, or the parent's bare title. */
389
+ function scopedTitle(scope, title) {
390
+ if (scope.prefix === "") return title;
391
+ return title === undefined ? scope.prefix : scope.prefix + " \u203a " + title;
392
+ }
393
+
394
+ function definePhaseIn(scope, title) {
395
+ let index = scope.phaseIndexByTitle.get(title);
396
+ if (index !== undefined) return index;
397
+ index = nextPhaseIndex++;
398
+ scope.phaseIndexByTitle.set(title, index);
399
+ emit({ type: "workflow_phase", index: index, title: scopedTitle(scope, title) });
400
+ return index;
401
+ }
402
+
403
+ function phaseIn(scope, title) {
404
+ const text = requireText(title, "phase(title)");
405
+ scope.ambientPhaseIndex = definePhaseIn(scope, text);
406
+ scope.ambientPhaseTitle = scopedTitle(scope, text);
407
+ }
408
+
409
+ function describe(value) {
410
+ if (typeof value === "string") return value;
411
+ // Duck-typed, not \`instanceof Error\`: an error thrown by the script belongs
412
+ // to the vm realm, so it fails an instanceof check against the worker's.
413
+ if (value && typeof value === "object" && typeof value.message === "string" && typeof value.stack === "string") {
414
+ return value.message;
415
+ }
416
+ try {
417
+ const json = JSON.stringify(value);
418
+ if (json !== undefined) return json;
419
+ } catch {
420
+ /* cycles and BigInt fall through to String() */
421
+ }
422
+ return String(value);
423
+ }
424
+
425
+ /** Attribute a line to the child that wrote it; logs carry no phase of their own. */
426
+ function logPrefix(scope) {
427
+ return scope.prefix === "" ? "" : scope.prefix + ": ";
428
+ }
429
+
430
+ function logIn(scope, message) {
431
+ emit({ type: "workflow_log", message: logPrefix(scope) + describe(message) });
432
+ }
433
+
434
+ function makeConsole(scope) {
435
+ const write = function () {
436
+ const parts = [];
437
+ for (let i = 0; i < arguments.length; i++) parts.push(describe(arguments[i]));
438
+ emit({ type: "workflow_log", message: logPrefix(scope) + parts.join(" ") });
439
+ };
440
+ return { log: write, info: write, warn: write, error: write, debug: write };
441
+ }
442
+
443
+ async function agentIn(scope, prompt, opts) {
444
+ const text = requireText(prompt, "agent(prompt)");
445
+ const options = opts === undefined || opts === null ? {} : opts;
446
+ if (typeof options !== "object" || Array.isArray(options)) {
447
+ throw new Error("agent(prompt, opts) expects opts to be an object.");
448
+ }
449
+
450
+ for (const key of Object.keys(options)) {
451
+ if (AGENT_OPTIONS.indexOf(key) !== -1) continue;
452
+ const why = UNSUPPORTED_AGENT_OPTIONS[key];
453
+ throw new Error(
454
+ why !== undefined
455
+ ? "agent() opts." + key + " is not supported here: " + why
456
+ : "agent() opts." + key + " is not a recognised option. Supported: " + AGENT_OPTIONS.join(", ") + "."
457
+ );
458
+ }
459
+
460
+ const label = optionalText(options.label, "agent() opts.label");
461
+ const phaseName = optionalText(options.phase, "agent() opts.phase");
462
+ const model = optionalText(options.model, "agent() opts.model");
463
+ const agentType = optionalText(options.agentType, "agent() opts.agentType");
464
+ const isolation = optionalText(options.isolation, "agent() opts.isolation");
465
+ if (isolation !== undefined && isolation !== "worktree") {
466
+ throw new Error("agent() opts.isolation must be \\"worktree\\".");
467
+ }
468
+ const gate = optionalText(options.gate, "agent() opts.gate");
469
+ const resume = optionalText(options.resume, "agent() opts.resume");
470
+ const effort = optionalText(options.effort, "agent() opts.effort");
471
+ const schema = options.schema;
472
+ if (schema !== undefined) {
473
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
474
+ throw new Error("agent() opts.schema must be a JSON Schema object.");
475
+ }
476
+ // Structured clone would happily carry a Map or a cycle that neither the
477
+ // journal key nor the tool's parameters can survive. Same check the return
478
+ // value gets.
479
+ checkBoundary(schema, "agent() opts.schema");
480
+ }
481
+ if (effort !== undefined && EFFORT_LEVELS.indexOf(effort) === -1) {
482
+ throw new Error("agent() opts.effort must be one of: " + EFFORT_LEVELS.join(", ") + ".");
483
+ }
484
+
485
+ // resume revives a child that already exists, so anything describing how to
486
+ // *start* one is not a thing this call gets to decide — the revived child
487
+ // keeps the agent, model and tool contract it was started with. Rejecting is
488
+ // the point: silently ignoring these opts would look like they applied.
489
+ if (resume !== undefined) {
490
+ if (agentType !== undefined) {
491
+ throw new Error(
492
+ "agent() opts.resume and opts.agentType are mutually exclusive: a resumed agent keeps the agent type it was started with."
493
+ );
494
+ }
495
+ if (model !== undefined) {
496
+ throw new Error(
497
+ "agent() opts.resume and opts.model are mutually exclusive: a resumed agent keeps the model it was started with."
498
+ );
499
+ }
500
+ if (isolation !== undefined) {
501
+ throw new Error(
502
+ "agent() opts.resume and opts.isolation are mutually exclusive: a resumed agent keeps the working tree it was started in."
503
+ );
504
+ }
505
+ if (effort !== undefined) {
506
+ throw new Error(
507
+ "agent() opts.resume and opts.effort are mutually exclusive: a resumed agent keeps the reasoning effort it was started with."
508
+ );
509
+ }
510
+ if (schema !== undefined) {
511
+ throw new Error(
512
+ "agent() opts.resume and opts.schema are mutually exclusive: a resumed child re-prompts the session it "
513
+ + "already had, whose tool set was fixed when it started — it has no StructuredOutput tool to answer through."
514
+ );
515
+ }
516
+ if (gate !== undefined) {
517
+ throw new Error("agent() opts.gate cannot be combined with opts.resume.");
518
+ }
519
+ }
520
+
521
+ // An explicit opts.phase files this agent under that phase without moving
522
+ // the ambient one, so a stray verify step does not re-point the phases that
523
+ // follow it.
524
+ const phaseIndex = phaseName !== undefined ? definePhaseIn(scope, phaseName) : scope.ambientPhaseIndex;
525
+ const phaseTitle = phaseName !== undefined ? scopedTitle(scope, phaseName) : scope.ambientPhaseTitle;
526
+
527
+ // Set when the host ran the child on another model than configured.
528
+ let modelNote;
529
+ const result = await callHost("agent", {
530
+ prompt: text,
531
+ label: label,
532
+ model: model,
533
+ agentType: agentType,
534
+ isolation: isolation,
535
+ phaseIndex: phaseIndex,
536
+ phaseTitle: phaseTitle,
537
+ gate: gate,
538
+ resume: resume,
539
+ effort: effort,
540
+ schema: schema,
541
+ }, function (note) { modelNote = note; });
542
+ if (result === undefined || result === null) {
543
+ // A failed child is a null; its note would otherwise be lost with it.
544
+ if (modelNote !== undefined) logIn(scope, modelNote);
545
+ return null;
546
+ }
547
+ if (schema === undefined) return result;
548
+ // Parsed with the realm's own JSON.parse so the script gets an object whose
549
+ // prototype is its own — \`x instanceof Object\` and \`x.list instanceof Array\`
550
+ // both hold, and it survives assertBoundary if the script returns it.
551
+ try {
552
+ const parsed = realmParse(result);
553
+ // A schema'd payload must stay exactly what the schema describes, so the
554
+ // note cannot lead it as text. It goes to the run log, and onto a plain
555
+ // object payload as a sibling \`_modelNote\` the script can read.
556
+ if (modelNote !== undefined) {
557
+ logIn(scope, modelNote);
558
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && canCarryModelNote(schema) && !Object.prototype.hasOwnProperty.call(parsed, "_modelNote")) parsed._modelNote = modelNote;
559
+ }
560
+ return parsed;
561
+ } catch (error) {
562
+ logIn(scope, "agent(): the host returned a structured result that is not JSON");
563
+ return null;
564
+ }
565
+ }
566
+
567
+ /**
568
+ * Whether the host may add a sibling \`_modelNote\` to a schema'd payload.
569
+ *
570
+ * Deliberately conservative: only a PLAIN object-with-properties schema, using
571
+ * nothing but the keywords below, qualifies. Anything that could declare the
572
+ * field somewhere we do not look (allOf/anyOf/oneOf/$ref/patternProperties/
573
+ * if-then-else/dependent schemas/a schema-valued additionalProperties) or forbid
574
+ * it (additionalProperties: false) never gets an injected field, and the note
575
+ * goes to the run log instead.
576
+ */
577
+ var PLAIN_SCHEMA_KEYS = ["$schema", "type", "properties", "required", "description", "title"];
578
+ function canCarryModelNote(schema) {
579
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) return false;
580
+ if (schema.type !== "object") return false;
581
+ const props = schema.properties;
582
+ if (!props || typeof props !== "object" || Array.isArray(props)) return false;
583
+ if (Object.prototype.hasOwnProperty.call(props, "_modelNote")) return false;
584
+ const keys = Object.keys(schema);
585
+ for (let i = 0; i < keys.length; i++) {
586
+ if (PLAIN_SCHEMA_KEYS.indexOf(keys[i]) === -1) {
587
+ // \`additionalProperties: true\` changes nothing, so it stays plain.
588
+ if (keys[i] === "additionalProperties" && schema.additionalProperties === true) continue;
589
+ return false;
590
+ }
591
+ }
592
+ return true;
593
+ }
594
+
595
+ /**
596
+ * A barrier: every thunk starts now, and nothing past the await runs until all
597
+ * of them have settled. A thunk that throws resolves to null rather than
598
+ * failing its siblings — the script filters, it does not try/catch.
599
+ */
600
+ async function parallel(thunks) {
601
+ const list = toList(thunks, "parallel(thunks)");
602
+ for (let i = 0; i < list.length; i++) {
603
+ if (typeof list[i] !== "function") {
604
+ throw new Error("parallel(thunks) expects an array of functions; item " + i + " is not one.");
605
+ }
606
+ }
607
+ const settled = await Promise.all(
608
+ list.map(async function (thunk) {
609
+ try {
610
+ return await thunk();
611
+ } catch (error) {
612
+ if (isFatal(error)) throw error;
613
+ return null;
614
+ }
615
+ })
616
+ );
617
+ return toRealmArray(settled);
618
+ }
619
+
620
+ /**
621
+ * No barrier between stages. Each item walks its own chain, so item A can be
622
+ * in stage 3 while item B is still in stage 1 — which is the whole point:
623
+ * a barrier makes every stage wait on its slowest sibling, and with agents in
624
+ * the stages that latency is measured in minutes.
625
+ *
626
+ * A stage that throws drops that item to null and skips its remaining stages.
627
+ * Every stage sees (previousResult, originalItem, index).
628
+ */
629
+ async function pipeline(items, ...stages) {
630
+ const list = toList(items, "pipeline(items, ...stages)");
631
+ for (let i = 0; i < stages.length; i++) {
632
+ if (typeof stages[i] !== "function") {
633
+ throw new Error("pipeline(items, ...stages) expects stages to be functions; stage " + i + " is not one.");
634
+ }
635
+ }
636
+ const settled = await Promise.all(
637
+ list.map(async function (item, index) {
638
+ let value = item;
639
+ for (let s = 0; s < stages.length; s++) {
640
+ try {
641
+ value = await stages[s](value, item, index);
642
+ } catch (error) {
643
+ if (isFatal(error)) throw error;
644
+ return null;
645
+ }
646
+ }
647
+ return value;
648
+ })
649
+ );
650
+ return toRealmArray(settled);
651
+ }
652
+
653
+ /**
654
+ * The \`workflow(nameOrRef, args?)\` global.
655
+ *
656
+ * Runs another workflow inline. The child executes in *this* worker and *this*
657
+ * vm context, as a function whose parameters shadow the globals — which is why
658
+ * it shares the run's concurrency cap, agent counter, abort signal, journal and
659
+ * budget without any of them being passed anywhere: there is only ever one of
660
+ * each. What it does not share is ambient phase state, which lives on the scope.
661
+ *
662
+ * One level only, as in Claude Code. The child's \`workflow\` is present and
663
+ * throws rather than absent, so the error names the limit instead of reading
664
+ * \`workflow is not defined\`.
665
+ */
666
+ async function workflowIn(scope, nameOrRef, args) {
667
+ if (scope.depth > 0) {
668
+ throw new Error(
669
+ "workflow() cannot be nested more than one level deep — you are already inside the workflow '" +
670
+ scope.name + "'. Call the agents inline instead."
671
+ );
672
+ }
673
+
674
+ let ref;
675
+ if (typeof nameOrRef === "string") {
676
+ if (nameOrRef.trim() === "") throw new Error("workflow(nameOrRef) expects a non-empty name.");
677
+ ref = { name: nameOrRef };
678
+ } else if (nameOrRef && typeof nameOrRef === "object" && !Array.isArray(nameOrRef)) {
679
+ const scriptPath = optionalText(nameOrRef.scriptPath, "workflow() scriptPath");
680
+ const name = optionalText(nameOrRef.name, "workflow() name");
681
+ if (scriptPath === undefined && name === undefined) {
682
+ throw new Error("workflow({ ... }) expects a \`name\` or a \`scriptPath\`.");
683
+ }
684
+ ref = { name: name, scriptPath: scriptPath };
685
+ } else {
686
+ throw new Error("workflow(nameOrRef) expects a saved workflow name or { scriptPath }.");
687
+ }
688
+
689
+ const label = ref.name !== undefined ? ref.name : ref.scriptPath;
690
+ if (args !== undefined) checkBoundary(args, 'workflow("' + label + '") args');
691
+
692
+ if (nestedCount >= workerData.nestedCap) {
693
+ // Fatal, like the agent cap: a limit that silently drops work would be
694
+ // worse than no limit.
695
+ const error = new Error(
696
+ "Workflow exceeded its cap of " + workerData.nestedCap + " nested workflow() calls."
697
+ );
698
+ error.workflowFatal = true;
699
+ throw error;
700
+ }
701
+ nestedCount++;
702
+
703
+ let loaded;
704
+ try {
705
+ loaded = await callHost("workflow", ref);
706
+ } catch (error) {
707
+ // Resolution failures are the script's to handle — Claude Code documents
708
+ // workflow() as throwing on an unknown name so a script can catch it.
709
+ // Attributed, so a caught error says which reference failed.
710
+ if (isFatal(error)) throw error;
711
+ throw new Error('workflow("' + label + '"): ' + describe(error));
712
+ }
713
+
714
+ const child = makeScope(loaded.name, scope.depth + 1);
715
+ // The child's own group, defined before its first agent so a child that never
716
+ // calls phase() still reads as its own section rather than falling into the
717
+ // parent's un-phased bucket.
718
+ child.ambientPhaseIndex = definePhaseIn(child, undefined);
719
+ child.ambientPhaseTitle = scopedTitle(child, undefined);
720
+
721
+ const run = compileWorkflow(loaded.body, "workflow:" + loaded.name + ".js");
722
+
723
+ const value = await run(child.agent, child.phase, child.log, child.workflow, child.console, args);
724
+ checkBoundary(value, 'the result of workflow("' + label + '")');
725
+ return value;
726
+ }
727
+
728
+ /**
729
+ * The \`budget\` global.
730
+ *
731
+ * \`total\` is permanently null, and that is the honest answer rather than a
732
+ * stub: Claude Code fills it from the user's "+500k" directive and pi has no
733
+ * such directive, so "no target set" is the state this runtime is always in.
734
+ * Every pattern Claude Code documents guards on exactly that — \`while
735
+ * (budget.total && ...)\`, \`budget.total ? ... : 5\` — so those scripts run here
736
+ * unchanged and take the branch they were written for. Leaving \`budget\`
737
+ * undefined instead would turn a graceful guard into a ReferenceError.
738
+ *
739
+ * \`spent()\` is real. It differs from Claude Code's in scope: theirs pools the
740
+ * main loop and every workflow in the turn, ours counts this run's agents.
741
+ */
742
+ function makeBudget() {
743
+ return {
744
+ total: null,
745
+ spent: function () {
746
+ return spentOutput;
747
+ },
748
+ remaining: function () {
749
+ // Infinity, not a number, because there is no target to subtract from.
750
+ return Infinity;
751
+ },
752
+ };
753
+ }
754
+
755
+ /* ------------------------------------------------------------------ *
756
+ * Authored source locations
757
+ * ------------------------------------------------------------------ */
758
+
759
+ // Neither lineOffset nor columnOffset is implemented by Bun 1.3/1.4. Keep
760
+ // authored lines intact and map only the prefix on line 1. Both entry points
761
+ // compile the same function; calling it outside the VM adds no synthetic frame.
762
+ const WORKFLOW_PREFIX = "(async (agent, phase, log, workflow, console, args) => {" + PRELUDE;
763
+ let registerWorkflowFile = null;
764
+
765
+ // Compiled from source in the realm, not installed as a host callback. Only
766
+ // primitive prefix length and filenames cross into its private realm-native Set.
767
+ function installAuthoredStacks(prefixLength) {
768
+ const workflowFiles = new Set();
769
+ function authoredFrame(frame) {
770
+ const text = String(frame);
771
+ const file = frame.getFileName();
772
+ if (!workflowFiles.has(file) || frame.getLineNumber() !== 1) return text;
773
+ const location = file + ":1:";
774
+ const at = text.lastIndexOf(location);
775
+ if (at === -1) return text;
776
+ const start = at + location.length;
777
+ // Bun 1.3's getColumnNumber() is zero-based, but CallSite.toString()
778
+ // is one-based on all supported engines.
779
+ const column = Number.parseInt(text.slice(start), 10);
780
+ if (column <= prefixLength) return text;
781
+ const digits = String(column).length;
782
+ return text.slice(0, start) + (column - prefixLength) + text.slice(start + digits);
783
+ }
784
+ Error.prepareStackTrace = function (error, frames) {
785
+ return Error.prototype.toString.call(error) + frames.map(function (frame) {
786
+ return "\\n at " + authoredFrame(frame);
787
+ }).join("");
788
+ };
789
+ return function (filename) { workflowFiles.add(filename); };
790
+ }
791
+
792
+ function compileWorkflow(body, filename) {
793
+ registerWorkflowFile(filename);
794
+ try {
795
+ return new vm.Script(WORKFLOW_PREFIX + body + "\\n})", { filename: filename }).runInContext(realmContext);
796
+ } catch (error) {
797
+ // Parser errors are created outside the realm. Retain the actual error,
798
+ // but remove the generated prefix from a first-line source/caret excerpt.
799
+ if (error && typeof error.stack === "string") {
800
+ if (error.stack.startsWith(filename + ":1\\n")) {
801
+ const lines = error.stack.split("\\n");
802
+ if (lines[1]?.startsWith(WORKFLOW_PREFIX)) {
803
+ lines[1] = lines[1].slice(WORKFLOW_PREFIX.length);
804
+ if (lines[2] !== undefined) lines[2] = lines[2].slice(WORKFLOW_PREFIX.length);
805
+ error.stack = lines.join("\\n");
806
+ }
807
+ } else if (error.sourceURL === filename && error.stack.startsWith("[native code]:0\\n")) {
808
+ // Bun 1.3 defers parsing until runInContext and loses the source
809
+ // excerpt/column. It still supplies the authored line; don't invent
810
+ // a caret for a column the engine did not report.
811
+ const line = error.line;
812
+ const sourceLines = body.split(/\\r\\n|[\\n\\r\\u2028\\u2029]/);
813
+ if (Number.isInteger(line) && line > 0 && line <= sourceLines.length) {
814
+ error.stack = filename + ":" + line + "\\n" + sourceLines[line - 1] + "\\n" + error.stack.slice(error.stack.indexOf("\\n") + 1);
815
+ }
816
+ }
817
+ }
818
+ throw error;
819
+ }
820
+ }
821
+
822
+ /* ------------------------------------------------------------------ *
823
+ * Run
824
+ * ------------------------------------------------------------------ */
825
+
826
+ async function main() {
827
+ rootScope = makeScope(undefined, 0);
828
+ const sandbox = {
829
+ agent: rootScope.agent,
830
+ parallel: parallel,
831
+ pipeline: pipeline,
832
+ phase: rootScope.phase,
833
+ log: rootScope.log,
834
+ workflow: rootScope.workflow,
835
+ budget: makeBudget(),
836
+ console: rootScope.console,
837
+ };
838
+ const context = vm.createContext(sandbox, {
839
+ name: "workflow",
840
+ codeGeneration: { strings: false, wasm: false },
841
+ });
842
+
843
+ realmObjectPrototype = vm.runInContext("Object.prototype", context);
844
+ realmNewArray = vm.runInContext("(function () { return []; })", context);
845
+ realmPush = vm.runInContext("(function (array, value) { array.push(value); })", context);
846
+ realmParse = vm.runInContext("JSON.parse", context);
847
+ realmContext = context;
848
+
849
+ // meta and args are materialised *inside* the realm rather than injected, so
850
+ // the script sees objects whose prototype is its own Object.prototype and
851
+ // whose .constructor is its own Function.
852
+ sandbox.meta = realmParse(workerData.metaJson);
853
+ sandbox.args = workerData.argsJson === undefined ? undefined : realmParse(workerData.argsJson);
854
+
855
+ // Structured CallSites retain constructor-expression columns on both V8 and
856
+ // JSC. Formatting changes no Error constructor, prototype, identity or cause.
857
+ registerWorkflowFile = vm.runInContext(
858
+ "(" + installAuthoredStacks.toString() + ")(" + WORKFLOW_PREFIX.length + ")",
859
+ context,
860
+ { filename: "workflow-stack.js" }
861
+ );
862
+ const run = compileWorkflow(workerData.body, "workflow.js");
863
+ const value = await run(rootScope.agent, rootScope.phase, rootScope.log, rootScope.workflow, rootScope.console, sandbox.args);
864
+ checkBoundary(value, "the workflow result");
865
+ flushProgress();
866
+ const result = {
867
+ type: "complete",
868
+ resultJson: value === undefined ? undefined : JSON.stringify(value),
869
+ };
870
+ if (workerData.collectCoverage) {
871
+ result.coverage = vm.runInContext("globalThis.__coverage__", context);
872
+ }
873
+ port.postMessage(result);
874
+ }
875
+
876
+ /** Serialize diagnostics only: keep realm errors untouched and bound cause traversal/output. */
877
+ function failureDetails(error) {
878
+ const limit = 16384;
879
+ let message = "Workflow threw an unreadable error.";
880
+ let stack = "";
881
+ const seen = new Set();
882
+ try {
883
+ message = String(error && error.message ? error.message : error).slice(0, 4096);
884
+ let current = error;
885
+ for (let depth = 0; depth < 8 && stack.length < limit; depth++) {
886
+ if (seen.has(current)) break;
887
+ seen.add(current);
888
+ const text = current && typeof current.stack === "string" ? current.stack : String(current);
889
+ stack += (depth === 0 ? "" : "\\nCaused by: ") + text.slice(0, limit - stack.length);
890
+ if (!current || current.cause === undefined) break;
891
+ current = current.cause;
892
+ }
893
+ } catch {
894
+ // A diagnostic getter must not prevent the failed run from settling.
895
+ }
896
+ return { type: "error", message: message, stack: stack.slice(0, limit) || undefined };
897
+ }
898
+
899
+ main().catch(function (error) {
900
+ flushProgress();
901
+ port.postMessage(failureDetails(error));
902
+ });
903
+ `;