pi-subagents 0.50.0 → 0.52.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/CHANGELOG.md +103 -0
  2. package/agents/oracle.md +3 -1
  3. package/agents/reviewer.md +1 -0
  4. package/agents/scout.md +2 -2
  5. package/agents/worker.md +2 -1
  6. package/async-retention-discovery-worker.mjs +180 -0
  7. package/docs/agents.md +40 -2
  8. package/docs/configuration.md +30 -12
  9. package/docs/extension-api.md +42 -1
  10. package/docs/models.md +2 -0
  11. package/docs/observability.md +41 -5
  12. package/docs/tool-reference.md +38 -39
  13. package/docs/workflows.md +171 -3
  14. package/package.json +4 -2
  15. package/skills/pi-subagents/SKILL.md +6 -4
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
  17. package/skills/pi-subagents/references/execution-controls.md +29 -15
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
  19. package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
  20. package/src/agents/agent-management.ts +124 -351
  21. package/src/agents/agents.ts +157 -31
  22. package/src/agents/skills.ts +1 -1
  23. package/src/api/external-job-provider.ts +185 -0
  24. package/src/api/preflight.ts +36 -8
  25. package/src/api/shared-types.ts +2 -0
  26. package/src/extension/config.ts +3 -3
  27. package/src/extension/doctor.ts +3 -6
  28. package/src/extension/fanout-child.ts +2 -2
  29. package/src/extension/index.ts +166 -88
  30. package/src/extension/public-execution.ts +31 -2
  31. package/src/extension/schemas.ts +12 -35
  32. package/src/extension/tool-description.ts +35 -24
  33. package/src/inspectors/herdr/actions.ts +2 -2
  34. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  35. package/src/inspectors/herdr/project-panes.ts +2 -2
  36. package/src/intercom/native-supervisor-channel.ts +32 -10
  37. package/src/missions/lifecycle.ts +6 -1
  38. package/src/missions/store.ts +4 -9
  39. package/src/missions/workflow-state.ts +2 -2
  40. package/src/profiles/profiles.ts +3 -1
  41. package/src/runs/background/active-run-index.ts +31 -8
  42. package/src/runs/background/async-execution.ts +68 -40
  43. package/src/runs/background/async-job-tracker.ts +20 -4
  44. package/src/runs/background/async-resume.ts +47 -15
  45. package/src/runs/background/async-retention.ts +886 -0
  46. package/src/runs/background/async-status.ts +39 -53
  47. package/src/runs/background/chain-append.ts +3 -33
  48. package/src/runs/background/completion-dedupe.ts +5 -1
  49. package/src/runs/background/completion-replay.ts +22 -12
  50. package/src/runs/background/control-channel.ts +14 -68
  51. package/src/runs/background/fleet-view.ts +68 -19
  52. package/src/runs/background/index-segment.ts +59 -0
  53. package/src/runs/background/inspect-rpc.ts +443 -0
  54. package/src/runs/background/notify.ts +31 -5
  55. package/src/runs/background/result-files.ts +158 -90
  56. package/src/runs/background/result-watcher.ts +116 -20
  57. package/src/runs/background/resume-guidance.ts +8 -5
  58. package/src/runs/background/retained-children.ts +13 -3
  59. package/src/runs/background/run-id-query.ts +7 -0
  60. package/src/runs/background/run-id-resolver.ts +11 -9
  61. package/src/runs/background/run-status.ts +27 -18
  62. package/src/runs/background/scheduled-runs.ts +24 -4
  63. package/src/runs/background/stale-run-reconciler.ts +6 -3
  64. package/src/runs/background/steering.ts +11 -1
  65. package/src/runs/background/subagent-runner.ts +253 -140
  66. package/src/runs/background/subagent-wait.ts +8 -8
  67. package/src/runs/background/terminal-run-index.ts +129 -0
  68. package/src/runs/background/wait-completions.ts +21 -4
  69. package/src/runs/background/wait-subscriptions.ts +81 -2
  70. package/src/runs/foreground/async-steering-action.ts +21 -14
  71. package/src/runs/foreground/execution.ts +3 -1
  72. package/src/runs/foreground/subagent-executor.ts +666 -1579
  73. package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
  74. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  75. package/src/runs/shared/acceptance.ts +4 -4
  76. package/src/runs/shared/chain-outputs.ts +1 -3
  77. package/src/runs/shared/external-job-bridge.ts +444 -0
  78. package/src/runs/shared/external-job-runner.ts +286 -0
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  80. package/src/runs/shared/model-fallback.ts +98 -38
  81. package/src/runs/shared/orca-progress-tabs.ts +84 -22
  82. package/src/runs/shared/parallel-handoff.ts +46 -4
  83. package/src/runs/shared/parallel-utils.ts +4 -15
  84. package/src/runs/shared/permissions.ts +5 -1
  85. package/src/runs/shared/pi-args.ts +8 -1
  86. package/src/runs/shared/session-lease.ts +0 -6
  87. package/src/runs/shared/subagent-control.ts +26 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
  89. package/src/runs/shared/workflow-graph.ts +1 -23
  90. package/src/runs/shared/worktree.ts +14 -2
  91. package/src/shared/atomic-json.ts +22 -2
  92. package/src/shared/capacity-resilient-json.ts +102 -0
  93. package/src/shared/completion-owner.ts +14 -0
  94. package/src/shared/file-system-retry.ts +49 -1
  95. package/src/shared/fork-context.ts +42 -0
  96. package/src/shared/prompt-resources.ts +0 -40
  97. package/src/shared/settings.ts +3 -27
  98. package/src/shared/types.ts +71 -26
  99. package/src/shared/utils.ts +8 -0
  100. package/src/shared/watch-strategy.ts +10 -0
  101. package/src/slash/slash-bridge.ts +2 -1
  102. package/src/slash/slash-commands.ts +29 -3
  103. package/src/tui/fleet.ts +63 -15
  104. package/src/watchdog/change-signature.ts +1 -1
  105. package/src/watchdog/lsp-diagnostics.ts +1 -0
  106. package/src/workflows/chat-progress.ts +2 -2
  107. package/src/workflows/scripted-workflow.ts +220 -67
  108. package/src/runs/foreground/chain-clarify.ts +0 -1354
  109. package/src/runs/foreground/chain-execution.ts +0 -1581
@@ -1,14 +1,31 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, resolve as resolvePath } from "node:path";
1
4
  import { Worker } from "node:worker_threads";
2
5
 
3
6
  const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
7
+ const requireFromPackage = createRequire(import.meta.url);
4
8
 
5
9
  const WORKER_SOURCE = String.raw`
6
- const { parentPort } = require("node:worker_threads");
7
- const { promiseHooks } = require("node:v8");
10
+ const { parentPort, workerData } = require("node:worker_threads");
8
11
  const vm = require("node:vm");
9
12
  const { inspect } = require("node:util");
13
+ const { parse } = require(workerData.acornPath);
10
14
 
11
- if (!promiseHooks || typeof promiseHooks.createHook !== "function") throw new Error("workflowScript requires node:v8 promiseHooks.createHook support.");
15
+ let promiseHooks;
16
+ try {
17
+ ({ promiseHooks } = require("node:v8"));
18
+ } catch {}
19
+
20
+ function createWorkflowPromiseHook(callbacks) {
21
+ if (!promiseHooks || typeof promiseHooks.createHook !== "function") return () => {};
22
+ try {
23
+ return promiseHooks.createHook(callbacks);
24
+ } catch (error) {
25
+ if (error?.name !== "NotImplementedError") throw error;
26
+ return () => {};
27
+ }
28
+ }
12
29
 
13
30
  let nextCallId = 0;
14
31
  let topLevelWorkflowPromise;
@@ -20,7 +37,7 @@ const trackedPromiseTrackers = new WeakMap();
20
37
  const trackedPromiseTargets = new WeakMap();
21
38
  let nativePromiseTrackers = new WeakMap();
22
39
  let nativePromiseParents = new WeakMap();
23
- const observedRunCallIds = new Set();
40
+ const observedCallIds = new Set();
24
41
 
25
42
  function stableRunJson(value) {
26
43
  if (Array.isArray(value)) return "[" + value.map(stableRunJson).join(",") + "]";
@@ -79,7 +96,7 @@ function mergeObservations(...groups) {
79
96
  const merged = [];
80
97
  for (const group of groups) {
81
98
  for (const observation of group) {
82
- if (!observation || typeof observation.callId !== "number" || typeof observation.key !== "string" || seen.has(observation.callId)) continue;
99
+ if (!observation || typeof observation.callId !== "number" || typeof observation.key !== "string" || typeof observation.operation !== "string" || seen.has(observation.callId)) continue;
83
100
  seen.add(observation.callId);
84
101
  merged.push(observation);
85
102
  }
@@ -99,9 +116,9 @@ function addTrackedObservations(tracker, observations) {
99
116
  tracker.observations = mergeObservations(tracker.observations, observations);
100
117
  if (!tracker.consumed) return;
101
118
  for (const observation of tracker.observations) {
102
- if (observedRunCallIds.has(observation.callId)) continue;
103
- observedRunCallIds.add(observation.callId);
104
- parentPort.postMessage({ type: "runObserved", callId: observation.callId, key: observation.key });
119
+ if (observedCallIds.has(observation.callId)) continue;
120
+ observedCallIds.add(observation.callId);
121
+ parentPort.postMessage({ type: "callObserved", callId: observation.callId, key: observation.key, operation: observation.operation });
105
122
  }
106
123
  }
107
124
 
@@ -211,8 +228,8 @@ function hostCall(method, args, observation) {
211
228
  pending.set(callId, { resolve, reject });
212
229
  parentPort.postMessage({ type: "call", callId, method, args });
213
230
  });
214
- return observation && typeof observation.key === "string"
215
- ? trackRunObservation([{ key: observation.key, callId }], promise)
231
+ return observation && typeof observation.key === "string" && typeof observation.operation === "string"
232
+ ? trackRunObservation([{ key: observation.key, operation: observation.operation, callId }], promise)
216
233
  : promise;
217
234
  }
218
235
 
@@ -241,6 +258,7 @@ function validateRunCall(key, params, label, fingerprints) {
241
258
  const hint = label === "runs.run" ? "; use runs.all(...) and JavaScript control flow for orchestration." : ".";
242
259
  throw new Error(label + " accepts one child via { agent, task } and execution controls only" + hint);
243
260
  }
261
+ if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
244
262
  if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
245
263
  if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
246
264
  if (params.gate !== undefined && params.acceptance !== undefined) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify.");
@@ -258,7 +276,7 @@ function validateRunCall(key, params, label, fingerprints) {
258
276
  const runs = Object.freeze({
259
277
  run(key, params) {
260
278
  validateRunCall(key, params, "runs.run", runFingerprints);
261
- return hostCall("run", { key, params }, { key });
279
+ return hostCall("run", { key, params }, { key, operation: "run" });
262
280
  },
263
281
  all(items) {
264
282
  if (!Array.isArray(items)) throw new Error("runs.all(items) requires an array.");
@@ -275,7 +293,18 @@ const runs = Object.freeze({
275
293
  for (const { key, params } of calls) runFingerprints.set(key, stableRunJson(params));
276
294
  const batch = { id: "batch-" + (++nextCallId), calls };
277
295
  const launched = calls.map(({ key, params }) => runHostCall(key, params, true, batch));
278
- return trackRunObservation(launched.map(({ key, callId }) => ({ key, callId })), Promise.all(launched.map(({ promise }) => promise)));
296
+ return trackRunObservation(launched.map(({ key, callId }) => ({ key, operation: "run", callId })), Promise.all(launched.map(({ promise }) => promise)));
297
+ },
298
+ steer(key, message, options = {}) {
299
+ if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error("runs.steer has an invalid key.");
300
+ if (typeof message !== "string" || !message.trim()) throw new Error("runs.steer message must be a non-empty string.");
301
+ if (!options || typeof options !== "object" || Array.isArray(options)) throw new Error("runs.steer options must be an object.");
302
+ const allowed = new Set(["mode", "index", "ackTimeoutMs"]);
303
+ for (const option of Object.keys(options)) if (!allowed.has(option)) throw new Error("runs.steer options contain unsupported field '" + option + "'.");
304
+ if (options.mode !== undefined && options.mode !== "steer" && options.mode !== "follow_up" && options.mode !== "auto") throw new Error("runs.steer mode must be 'steer', 'follow_up', or 'auto'.");
305
+ if (options.index !== undefined && (!Number.isInteger(options.index) || options.index < 0 || options.index > 1000000)) throw new Error("runs.steer index must be an integer between 0 and 1000000.");
306
+ if (options.ackTimeoutMs !== undefined && (!Number.isInteger(options.ackTimeoutMs) || options.ackTimeoutMs < 1)) throw new Error("runs.steer ackTimeoutMs must be a positive integer.");
307
+ return hostCall("steer", { key, message: message.trim(), options }, { key, operation: "steer" });
279
308
  },
280
309
  status(keyOrRunId) { return hostCall("status", { keyOrRunId }); },
281
310
  ref: formatRef,
@@ -299,14 +328,6 @@ const state = Object.freeze({
299
328
  },
300
329
  });
301
330
 
302
- const prompts = Object.freeze({
303
- render(ref, vars) {
304
- if (typeof ref !== "string" || !ref.trim()) throw new Error("prompts.render(ref, vars) requires a non-empty ref string.");
305
- if (vars !== undefined) assertJsonValue(vars, "prompts.render vars");
306
- return hostCall("prompts.render", { ref, vars });
307
- },
308
- });
309
-
310
331
  let contextObjectPrototype;
311
332
 
312
333
  const capturedConsole = Object.freeze(Object.fromEntries(
@@ -316,7 +337,7 @@ const capturedConsole = Object.freeze(Object.fromEntries(
316
337
  ));
317
338
 
318
339
  function formatWorkflowScriptSyntaxError(error) {
319
- const details = error && error.stack ? error.stack : String(error);
340
+ const details = formatWorkflowScriptError(error);
320
341
  return [
321
342
  "workflowScript must be valid JavaScript.",
322
343
  "If task text contains Markdown fences or backticks, use an array joined with \"\\n\" or escaped strings instead of a raw backtick template literal.",
@@ -326,6 +347,52 @@ function formatWorkflowScriptSyntaxError(error) {
326
347
  ].join("\n");
327
348
  }
328
349
 
350
+ function formatWorkflowScriptError(error) {
351
+ const message = error && typeof error.message === "string" ? error.message : String(error);
352
+ const stack = error && typeof error.stack === "string" ? error.stack : "";
353
+ if (!stack) return message;
354
+ return stack.includes(message) ? stack : message + "\n" + stack;
355
+ }
356
+
357
+ function isSyntaxError(error) {
358
+ return error instanceof SyntaxError || error?.name === "SyntaxError";
359
+ }
360
+
361
+ const NESTED_ASYNC_WORKFLOW_ERROR = "workflowScript does not support nested async functions. Use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains so workflows stay portable across Node and Bun.";
362
+ const AST_SCALAR_KEYS = new Set(["type", "start", "end"]);
363
+
364
+ function assertPortableWorkflowScript(source) {
365
+ const wrapped = "(async () => {\n" + source + "\n})()";
366
+ const ast = parse(wrapped, { ecmaVersion: "latest", sourceType: "script" });
367
+ const wrapper = workflowWrapperFunction(ast);
368
+ walkWorkflowAst(wrapper.body, wrapper);
369
+ }
370
+
371
+ function workflowWrapperFunction(ast) {
372
+ const wrapper = ast.body?.[0]?.expression?.callee;
373
+ if (!wrapper || wrapper.type !== "ArrowFunctionExpression") throw new Error("workflowScript wrapper parse invariant failed.");
374
+ return wrapper;
375
+ }
376
+
377
+ function isAsyncFunctionNode(node) {
378
+ return node.async === true && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression");
379
+ }
380
+
381
+ function walkWorkflowAst(node, allowedAsyncFunction) {
382
+ if (!node || typeof node !== "object") return;
383
+ if (Array.isArray(node)) {
384
+ for (const item of node) walkWorkflowAst(item, allowedAsyncFunction);
385
+ return;
386
+ }
387
+ if (node !== allowedAsyncFunction && isAsyncFunctionNode(node)) {
388
+ throw new Error(NESTED_ASYNC_WORKFLOW_ERROR);
389
+ }
390
+ for (const [key, child] of Object.entries(node)) {
391
+ if (AST_SCALAR_KEYS.has(key)) continue;
392
+ walkWorkflowAst(child, allowedAsyncFunction);
393
+ }
394
+ }
395
+
329
396
  function assertJsonValue(value, path = "emit", seen = new Set()) {
330
397
  if (value === null || typeof value === "string" || typeof value === "boolean") return;
331
398
  if (typeof value === "number") {
@@ -383,16 +450,16 @@ parentPort.on("message", async (message) => {
383
450
  }
384
451
  if (message.type !== "start") return;
385
452
  try {
386
- const sandbox = { runs, prompts, Promise: workflowPromise, emit(value) { assertJsonValue(value); parentPort.postMessage({ type: "emit", value }); }, console: capturedConsole };
453
+ const sandbox = { runs, Promise: workflowPromise, emit(value) { assertJsonValue(value); parentPort.postMessage({ type: "emit", value }); }, console: capturedConsole };
387
454
  if (message.stateEnabled) sandbox.state = state;
388
455
  const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
389
456
  contextObjectPrototype = vm.runInContext("Object.prototype", context);
390
457
  let compiled;
391
458
  try {
459
+ assertPortableWorkflowScript(message.script);
392
460
  compiled = new vm.Script("(async () => {\n" + message.script + "\n})()", { filename: "workflow-script.js" });
393
461
  } catch (error) {
394
- if (!(error instanceof SyntaxError)) throw error;
395
- parentPort.postMessage({ type: "error", error: formatWorkflowScriptSyntaxError(error) });
462
+ parentPort.postMessage({ type: "error", error: isSyntaxError(error) ? formatWorkflowScriptSyntaxError(error) : formatWorkflowScriptError(error) });
396
463
  return;
397
464
  }
398
465
  const nativePromisePrototype = vm.runInContext("(async () => {})().constructor.prototype", context);
@@ -411,7 +478,7 @@ parentPort.on("message", async (message) => {
411
478
  return Reflect.apply(nativeThen, this, args);
412
479
  },
413
480
  });
414
- stopWorkflowPromiseHook = promiseHooks.createHook({
481
+ stopWorkflowPromiseHook = createWorkflowPromiseHook({
415
482
  before(promise) {
416
483
  activeNativePromises.push(promise);
417
484
  },
@@ -456,7 +523,7 @@ parentPort.on("message", async (message) => {
456
523
  assertJsonValue(persistedValue, "return");
457
524
  parentPort.postMessage({ type: "complete", value: persistedValue });
458
525
  } catch (error) {
459
- parentPort.postMessage({ type: "error", error: error && error.stack ? error.stack : String(error), ...(error && error.workflowErrorKind === "detached-child" ? { errorKind: "detached-child" } : {}) });
526
+ parentPort.postMessage({ type: "error", error: isSyntaxError(error) ? formatWorkflowScriptSyntaxError(error) : formatWorkflowScriptError(error), ...(error && error.workflowErrorKind === "detached-child" ? { errorKind: "detached-child" } : {}) });
460
527
  }
461
528
  });
462
529
  `;
@@ -476,9 +543,9 @@ export interface WorkflowScriptChildResult {
476
543
  }
477
544
 
478
545
  export interface WorkflowScriptTraceEntry {
479
- operation: "run" | "status";
546
+ operation: "run" | "status" | "steer";
480
547
  key: string;
481
- state: "started" | "completed" | "failed" | "detached" | "stopped" | "reused";
548
+ state: "started" | "completed" | "failed" | "detached" | "stopped" | "reused" | "queued" | "delivered" | "missed";
482
549
  /** Canonical child agent name when resolved launch or result data is available. */
483
550
  agent?: string;
484
551
  runId?: string;
@@ -488,6 +555,21 @@ export interface WorkflowScriptTraceEntry {
488
555
  error?: string;
489
556
  }
490
557
 
558
+ export interface WorkflowSteerOptions {
559
+ mode?: "steer" | "follow_up" | "auto";
560
+ index?: number;
561
+ ackTimeoutMs?: number;
562
+ }
563
+
564
+ export interface WorkflowSteerResult {
565
+ key: string;
566
+ state: "queued" | "delivered" | "missed" | "failed";
567
+ requestId?: string;
568
+ deliveryStatus?: "queued" | "delivered";
569
+ targets?: Array<{ index: number; state: string; reason?: string }>;
570
+ error?: string;
571
+ }
572
+
491
573
  export interface WorkflowScriptResult {
492
574
  value: unknown;
493
575
  emits: unknown[];
@@ -515,13 +597,11 @@ export interface RunWorkflowScriptOptions {
515
597
  admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
516
598
  launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean }) => Promise<WorkflowScriptChildResult>;
517
599
  status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
600
+ steer?: (key: string, message: string, options: WorkflowSteerOptions, signal: AbortSignal) => Promise<WorkflowSteerResult>;
518
601
  state?: {
519
602
  get: (key: string) => unknown | Promise<unknown>;
520
603
  set: (key: string, value: unknown) => void | Promise<void>;
521
604
  };
522
- prompts?: {
523
- render: (ref: string, vars?: unknown) => string | Promise<string>;
524
- };
525
605
  onTrace?: (trace: WorkflowScriptTraceEntry[]) => void;
526
606
  onEmit?: (emits: unknown[]) => void;
527
607
  }
@@ -629,44 +709,92 @@ function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowS
629
709
  };
630
710
  }
631
711
 
712
+ function resolveWorkflowParserEntry(): string {
713
+ try {
714
+ return requireFromPackage.resolve("acorn");
715
+ } catch (primaryError) {
716
+ // Some runtimes (e.g. Bun-compiled single-file binaries) fail bare
717
+ // package-specifier resolution through createRequire while subpath
718
+ // resolution still works. Resolve the manifest and derive the
719
+ // CommonJS entry from its "main" field instead.
720
+ try {
721
+ const manifestPath = requireFromPackage.resolve("acorn/package.json");
722
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { main?: unknown };
723
+ const entry = typeof manifest.main === "string" && manifest.main ? manifest.main : "./dist/acorn.js";
724
+ return resolvePath(dirname(manifestPath), entry);
725
+ } catch {
726
+ throw primaryError;
727
+ }
728
+ }
729
+ }
730
+
632
731
  export async function runWorkflowScript(options: RunWorkflowScriptOptions): Promise<WorkflowScriptResult> {
633
732
  if (!options.script.trim()) throw new Error("workflowScript must not be empty.");
634
733
  if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error("workflow script timeout must be a positive integer.");
635
734
 
636
- const worker = new Worker(WORKER_SOURCE, { eval: true });
735
+ let acornPath: string;
736
+ try {
737
+ acornPath = resolveWorkflowParserEntry();
738
+ } catch (error) {
739
+ throw new Error("Workflow parser dependency 'acorn' is unavailable from pi-subagents. Reinstall pi-subagents dependencies before launching workflowScript.", { cause: error });
740
+ }
741
+ const worker = new Worker(WORKER_SOURCE, { eval: true, workerData: { acornPath } });
637
742
  const emits: unknown[] = [];
638
743
  const consoleEntries: WorkflowScriptResult["console"] = [];
639
744
  const trace: WorkflowScriptTraceEntry[] = [];
640
745
  const children = new Map<string, WorkflowScriptChildResult>();
641
746
  const childOrder: string[] = [];
642
747
  const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult>; observed: boolean }>();
748
+ const steers = new Map<number, { key: string; promise: Promise<WorkflowSteerResult>; observed: boolean }>();
643
749
  const stoppedLaunches = new Set<string>();
644
750
  const batchAdmissions = new Map<string, Promise<void>>();
645
751
  const observedRunCalls = new Set<number>();
752
+ const observedSteerCalls = new Set<number>();
646
753
  const childController = new AbortController();
647
754
  let settled = false;
755
+ let finishing = false;
648
756
 
649
757
  const partial = (): Omit<WorkflowScriptResult, "value"> => ({ emits, console: consoleEntries, trace, children: childOrder.flatMap((key) => {
650
758
  const child = children.get(key);
651
759
  return child ? [child] : [];
652
760
  }) });
653
- const traceChanged = () => options.onTrace?.([...trace]);
761
+ // Hosts use onTrace to persist a progress journal, and it is invoked from inside
762
+ // the run-promise handlers below. A throw here would reject the child promise the
763
+ // script is awaiting, so a single failed status write could mark a completed child
764
+ // failed and abort its siblings through Promise.all. Telemetry must not decide
765
+ // workflow outcomes, so a failing callback is reported and the run continues.
766
+ const traceChanged = () => {
767
+ try {
768
+ options.onTrace?.([...trace]);
769
+ } catch (error) {
770
+ console.error("Workflow onTrace callback failed:", error);
771
+ }
772
+ };
654
773
 
655
774
  return await new Promise<WorkflowScriptResult>((resolve, reject) => {
656
775
  const finish = (outcome: { value: unknown } | { error: Error & { workflowErrorKind?: unknown } }) => {
657
- if (settled) return;
658
- settled = true;
659
- if (timer) clearTimeout(timer);
660
- options.signal?.removeEventListener("abort", onAbort);
661
- void worker.terminate();
662
- const unobservedKeys = "value" in outcome ? [...launches].filter(([, launch]) => !launch.observed).map(([key]) => key) : [];
663
- const completionError = unobservedKeys.length > 0
664
- ? new Error(`workflowScript completed with unawaited runs.run launch(es): ${unobservedKeys.map((key) => `'${key}'`).join(", ")}. Await or return each launch.`)
665
- : undefined;
666
- childController.abort("error" in outcome ? outcome.error : completionError ?? new Error("Workflow script completed."));
667
- if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child" ? "detached-child" : undefined));
668
- else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
669
- else resolve({ value: outcome.value, ...partial() });
776
+ if (settled || finishing) return;
777
+ finishing = true;
778
+ childController.abort("error" in outcome ? outcome.error : new Error("Workflow script completed."));
779
+ void Promise.allSettled([...steers.values()].map(({ promise }) => promise)).then(() => {
780
+ if (settled) return;
781
+ settled = true;
782
+ if (timer) clearTimeout(timer);
783
+ options.signal?.removeEventListener("abort", onAbort);
784
+ void worker.terminate();
785
+ const unobservedKeys = "value" in outcome ? [...launches].filter(([, launch]) => !launch.observed).map(([key]) => key) : [];
786
+ const completionError = unobservedKeys.length > 0
787
+ ? new Error(`workflowScript completed with unawaited runs.run launch(es): ${unobservedKeys.map((key) => `'${key}'`).join(", ")}. For ordinary parallel fanout use await runs.all([{key, agent, task}, ...]); do not read .output from unawaited launches.`)
788
+ : "value" in outcome
789
+ ? (() => {
790
+ const unobservedSteers = [...steers.values()].filter((steer) => !steer.observed).map((steer) => steer.key);
791
+ return unobservedSteers.length > 0 ? new Error(`workflowScript completed with unawaited runs.steer call(s): ${unobservedSteers.map((key) => `'${key}'`).join(", ")}. Await or return each call.`) : undefined;
792
+ })()
793
+ : undefined;
794
+ if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child" ? "detached-child" : undefined));
795
+ else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
796
+ else resolve({ value: outcome.value, ...partial() });
797
+ });
670
798
  };
671
799
  const onAbort = () => {
672
800
  const signalReason = options.signal?.reason;
@@ -738,11 +866,17 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
738
866
  if (message.errorKind === "detached-child") workflowError.workflowErrorKind = "detached-child";
739
867
  return finish({ error: workflowError });
740
868
  }
741
- if (message.type === "runObserved" && typeof message.callId === "number") {
869
+ if (message.type === "callObserved" && typeof message.callId === "number") {
742
870
  const key = typeof message.key === "string" ? message.key : undefined;
743
- const launch = key ? launches.get(key) : undefined;
744
- if (launch) launch.observed = true;
745
- else observedRunCalls.add(message.callId);
871
+ if (message.operation === "run") {
872
+ const launch = key ? launches.get(key) : undefined;
873
+ if (launch) launch.observed = true;
874
+ else observedRunCalls.add(message.callId);
875
+ } else if (message.operation === "steer") {
876
+ const steer = steers.get(message.callId);
877
+ if (steer) steer.observed = true;
878
+ else observedSteerCalls.add(message.callId);
879
+ }
746
880
  return;
747
881
  }
748
882
  if (message.type !== "call" || typeof message.callId !== "number" || typeof message.method !== "string" || !isRecord(message.args)) return;
@@ -757,18 +891,6 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
757
891
  },
758
892
  );
759
893
  };
760
-
761
- if (message.method === "prompts.render") {
762
- if (!options.prompts) return respond(Promise.reject(new Error("Workflow prompt rendering is unavailable.")));
763
- const ref = message.args.ref;
764
- const vars = message.args.vars;
765
- if (typeof ref !== "string" || !ref.trim()) return respond(Promise.reject(new Error("prompts.render(ref, vars) requires a non-empty ref string.")));
766
- return respond(Promise.resolve().then(() => options.prompts!.render(ref, vars)).then((rendered) => {
767
- if (typeof rendered !== "string") throw new Error("prompts.render must return task text.");
768
- return rendered;
769
- }));
770
- }
771
-
772
894
  if (message.method === "state.get" || message.method === "state.set") {
773
895
  if (!options.state) return respond(Promise.reject(new Error("Workflow state is unavailable without a mission.")));
774
896
  let key: string;
@@ -794,9 +916,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
794
916
  const target = known?.runId ?? keyOrRunId;
795
917
  trace.push({ operation: "status", key: keyOrRunId, state: "started", ...(known?.runId ? { runId: known.runId } : {}) });
796
918
  traceChanged();
797
- if (settled) return;
919
+ if (settled || finishing) return;
798
920
  respond(options.status(target, childController.signal).then((result) => {
799
- if (settled) return result;
921
+ if (settled || finishing) return result;
800
922
  trace.push({ operation: "status", key: keyOrRunId, state: result.ok ? "completed" : "failed", ...(result.runId ? { runId: result.runId } : {}), ...(!result.ok ? { error: result.output } : {}) });
801
923
  traceChanged();
802
924
  if (!result.ok) throw new Error(`Status '${keyOrRunId}' failed: ${result.output}`);
@@ -804,6 +926,37 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
804
926
  }));
805
927
  return;
806
928
  }
929
+ if (message.method === "steer") {
930
+ let key: string;
931
+ try {
932
+ key = validateKey(message.args.key, "runs.steer");
933
+ } catch (error) {
934
+ return respond(Promise.reject(error));
935
+ }
936
+ const steerMessage = message.args.message;
937
+ if (typeof steerMessage !== "string" || !steerMessage.trim()) return respond(Promise.reject(new Error(`runs.steer('${key}') requires a non-empty message.`)));
938
+ const steerOptions = isRecord(message.args.options) ? message.args.options as WorkflowSteerOptions : {};
939
+ const startedAt = Date.now();
940
+ trace.push({ operation: "steer", key, state: "started" });
941
+ traceChanged();
942
+ const promise = Promise.resolve().then(() => {
943
+ if (!launches.has(key)) throw new Error(`runs.steer('${key}') requires a prior runs.run/runs.all launch with that key.`);
944
+ if (!options.steer) throw new Error("Workflow steering is unavailable in this host.");
945
+ return options.steer(key, steerMessage.trim(), steerOptions, childController.signal);
946
+ }).then((receipt) => {
947
+ trace.push({ operation: "steer", key, state: receipt.state, durationMs: Date.now() - startedAt, ...(receipt.error ? { error: receipt.error } : {}) });
948
+ traceChanged();
949
+ return receipt;
950
+ }, (error: unknown) => {
951
+ const text = error instanceof Error ? error.message : String(error);
952
+ trace.push({ operation: "steer", key, state: "failed", durationMs: Date.now() - startedAt, error: text });
953
+ traceChanged();
954
+ throw error;
955
+ });
956
+ steers.set(message.callId, { key, promise, observed: observedSteerCalls.delete(message.callId) });
957
+ respond(promise);
958
+ return;
959
+ }
807
960
  if (message.method !== "run") return respond(Promise.reject(new Error(`Unknown runs API method '${message.method}'.`)));
808
961
 
809
962
  let key: string;
@@ -870,18 +1023,18 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
870
1023
  if (!admission) {
871
1024
  const seenKeys = new Set<string>();
872
1025
  const calls = (batch?.calls ?? [{ key, params }]).filter((call) => {
873
- if (seenKeys.has(call.key) || launches.has(call.key) || call.params.resume !== undefined) return false;
1026
+ if (seenKeys.has(call.key) || launches.has(call.key)) return false;
874
1027
  seenKeys.add(call.key);
875
1028
  return true;
876
1029
  });
877
1030
  admission = Promise.resolve().then(() => {
878
- if (settled) return;
1031
+ if (settled || finishing) return;
879
1032
  return options.admit?.(calls);
880
1033
  });
881
1034
  if (batch) batchAdmissions.set(batch.id, admission);
882
1035
  }
883
1036
  const promise = admission.then(() => {
884
- if (settled || stoppedLaunches.has(key)) {
1037
+ if (settled || finishing || stoppedLaunches.has(key)) {
885
1038
  const reason = childController.signal.reason;
886
1039
  const text = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "Workflow script aborted.";
887
1040
  return { key, ok: false, output: text, error: text, artifactPaths: [] };