pi-subagents 0.49.0 → 0.51.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 (117) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/agents/gpt-pro.md +17 -0
  3. package/agents/oracle.md +7 -5
  4. package/agents/researcher.md +1 -1
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +1 -1
  7. package/agents/worker.md +1 -1
  8. package/async-retention-discovery-worker.mjs +180 -0
  9. package/docs/agents.md +37 -2
  10. package/docs/configuration.md +76 -14
  11. package/docs/extension-api.md +78 -1
  12. package/docs/missions.md +1 -1
  13. package/docs/observability.md +20 -4
  14. package/docs/tool-reference.md +55 -39
  15. package/docs/workflows.md +171 -5
  16. package/package.json +4 -2
  17. package/skills/pi-subagents/SKILL.md +5 -4
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
  19. package/skills/pi-subagents/references/execution-controls.md +22 -18
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
  22. package/src/agents/agent-management.ts +100 -345
  23. package/src/agents/agent-serializer.ts +2 -0
  24. package/src/agents/agents.ts +135 -25
  25. package/src/api/external-job-provider.ts +185 -0
  26. package/src/api/external-runs.ts +174 -84
  27. package/src/api/preflight.ts +42 -11
  28. package/src/api/shared-types.ts +2 -0
  29. package/src/extension/config.ts +36 -3
  30. package/src/extension/doctor.ts +3 -6
  31. package/src/extension/fanout-child.ts +2 -2
  32. package/src/extension/index.ts +210 -90
  33. package/src/extension/public-execution.ts +31 -2
  34. package/src/extension/rpc.ts +5 -1
  35. package/src/extension/schemas.ts +14 -36
  36. package/src/extension/tool-description.ts +37 -24
  37. package/src/inspectors/herdr/actions.ts +5 -9
  38. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  39. package/src/inspectors/herdr/project-panes.ts +4 -8
  40. package/src/inspectors/herdr/shell-command.ts +16 -0
  41. package/src/intercom/intercom-bridge.ts +2 -3
  42. package/src/intercom/native-supervisor-channel.ts +49 -51
  43. package/src/missions/goal-driver.ts +3 -1
  44. package/src/missions/lifecycle.ts +6 -1
  45. package/src/missions/store.ts +4 -9
  46. package/src/profiles/profiles.ts +3 -1
  47. package/src/runs/background/active-run-index.ts +94 -1
  48. package/src/runs/background/async-execution.ts +98 -38
  49. package/src/runs/background/async-job-tracker.ts +21 -4
  50. package/src/runs/background/async-resume.ts +30 -17
  51. package/src/runs/background/async-retention.ts +888 -0
  52. package/src/runs/background/async-status-snapshot.ts +277 -0
  53. package/src/runs/background/async-status.ts +47 -56
  54. package/src/runs/background/chain-append.ts +3 -33
  55. package/src/runs/background/chain-root-attachment.ts +2 -2
  56. package/src/runs/background/completion-replay.ts +11 -1
  57. package/src/runs/background/control-channel.ts +14 -68
  58. package/src/runs/background/fleet-view.ts +3 -1
  59. package/src/runs/background/index-segment.ts +59 -0
  60. package/src/runs/background/notify.ts +3 -1
  61. package/src/runs/background/result-files.ts +505 -0
  62. package/src/runs/background/result-watcher.ts +250 -51
  63. package/src/runs/background/retained-children.ts +79 -20
  64. package/src/runs/background/run-id-query.ts +7 -0
  65. package/src/runs/background/run-id-resolver.ts +37 -29
  66. package/src/runs/background/run-status.ts +34 -20
  67. package/src/runs/background/scheduled-runs.ts +71 -27
  68. package/src/runs/background/stale-run-reconciler.ts +33 -16
  69. package/src/runs/background/steering.ts +11 -1
  70. package/src/runs/background/subagent-runner.ts +535 -161
  71. package/src/runs/background/subagent-wait.ts +9 -7
  72. package/src/runs/background/terminal-run-index.ts +129 -0
  73. package/src/runs/background/wait-completions.ts +22 -2
  74. package/src/runs/background/wait-subscriptions.ts +80 -1
  75. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  76. package/src/runs/foreground/async-steering-action.ts +21 -14
  77. package/src/runs/foreground/execution.ts +221 -15
  78. package/src/runs/foreground/subagent-executor.ts +529 -1519
  79. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  80. package/src/runs/shared/chain-outputs.ts +1 -3
  81. package/src/runs/shared/completion-guard.ts +96 -6
  82. package/src/runs/shared/external-cli-runner.ts +4 -0
  83. package/src/runs/shared/external-job-bridge.ts +450 -0
  84. package/src/runs/shared/external-job-runner.ts +286 -0
  85. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  86. package/src/runs/shared/model-fallback.ts +34 -3
  87. package/src/runs/shared/nested-events.ts +66 -62
  88. package/src/runs/shared/orca-progress-tabs.ts +437 -0
  89. package/src/runs/shared/parallel-handoff.ts +46 -4
  90. package/src/runs/shared/parallel-utils.ts +6 -15
  91. package/src/runs/shared/permissions.ts +5 -1
  92. package/src/runs/shared/pi-args.ts +8 -1
  93. package/src/runs/shared/subagent-control.ts +41 -4
  94. package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
  95. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  96. package/src/runs/shared/tool-timeout.ts +93 -0
  97. package/src/runs/shared/workflow-graph.ts +1 -23
  98. package/src/runs/shared/worktree.ts +12 -1
  99. package/src/shared/atomic-json.ts +22 -2
  100. package/src/shared/capacity-resilient-json.ts +102 -0
  101. package/src/shared/completion-owner.ts +14 -0
  102. package/src/shared/file-system-retry.ts +49 -1
  103. package/src/shared/fork-context.ts +42 -0
  104. package/src/shared/prompt-resources.ts +0 -40
  105. package/src/shared/settings.ts +3 -27
  106. package/src/shared/types.ts +88 -27
  107. package/src/shared/utils.ts +8 -0
  108. package/src/shared/watch-strategy.ts +10 -0
  109. package/src/slash/slash-commands.ts +45 -28
  110. package/src/slash/slash-live-state.ts +3 -0
  111. package/src/tui/fleet-status.ts +160 -45
  112. package/src/tui/fleet.ts +186 -28
  113. package/src/tui/render.ts +41 -10
  114. package/src/workflows/chat-progress.ts +7 -5
  115. package/src/workflows/scripted-workflow.ts +424 -125
  116. package/src/runs/foreground/chain-clarify.ts +0 -1354
  117. package/src/runs/foreground/chain-execution.ts +0 -1565
@@ -1,18 +1,43 @@
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");
10
+ const { parentPort, workerData } = require("node:worker_threads");
7
11
  const vm = require("node:vm");
8
12
  const { inspect } = require("node:util");
13
+ const { parse } = require(workerData.acornPath);
14
+
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
+ }
9
29
 
10
30
  let nextCallId = 0;
31
+ let topLevelWorkflowPromise;
32
+ let suppressNativePromiseConsumption = 0;
33
+ const activeNativePromises = [];
11
34
  const pending = new Map();
12
35
  const runKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
13
- const trackedPromisePatch = Symbol("trackedPromisePatch");
14
- const trackedPromiseObservations = new WeakMap();
15
- let suppressRunObservation = 0;
36
+ const trackedPromiseTrackers = new WeakMap();
37
+ const trackedPromiseTargets = new WeakMap();
38
+ let nativePromiseTrackers = new WeakMap();
39
+ let nativePromiseParents = new WeakMap();
40
+ const observedCallIds = new Set();
16
41
 
17
42
  function stableRunJson(value) {
18
43
  if (Array.isArray(value)) return "[" + value.map(stableRunJson).join(",") + "]";
@@ -23,15 +48,47 @@ function stableRunJson(value) {
23
48
  function isDirectWorkflowScriptPromiseHandlerCall() {
24
49
  const stack = new Error().stack;
25
50
  if (typeof stack !== "string") return false;
26
- const frame = stack.split("\n").slice(1).map((line) => line.trim()).find((line) =>
27
- line &&
28
- !line.includes("isDirectWorkflowScriptPromiseHandlerCall") &&
29
- !line.includes("markObserved") &&
30
- !line.includes("promise.then") &&
31
- !line.includes("promise.catch") &&
32
- !line.includes("promise.finally")
33
- );
34
- return frame ? frame.includes("workflow-script.js") && !frame.includes("at async ") : false;
51
+ return stack.split("\n").some((line) => line.includes("workflow-script.js") && !line.includes("at async "));
52
+ }
53
+
54
+ function nativePromiseTracker(promise) {
55
+ if (!promise || (typeof promise !== "object" && typeof promise !== "function")) return undefined;
56
+ let tracker = nativePromiseTrackers.get(promise);
57
+ if (!tracker) {
58
+ tracker = { observations: [], consumed: false, dependencies: [] };
59
+ nativePromiseTrackers.set(promise, tracker);
60
+ }
61
+ return tracker;
62
+ }
63
+
64
+ function promiseObservationTracker(value) {
65
+ if (!value || (typeof value !== "object" && typeof value !== "function")) return undefined;
66
+ return trackedPromiseTrackers.get(value) ?? nativePromiseTrackers.get(value);
67
+ }
68
+
69
+ function addTrackerDependency(tracker, dependency) {
70
+ if (!dependency) return;
71
+ tracker.dependencies ??= [];
72
+ if (!tracker.dependencies.includes(dependency)) tracker.dependencies.push(dependency);
73
+ if (tracker.consumed) markTrackedObservationsConsumed(dependency);
74
+ }
75
+
76
+ function descendsFromTopLevelWorkflow(promise) {
77
+ const seen = new Set();
78
+ for (let current = promise; current && !seen.has(current); current = nativePromiseParents.get(current)) {
79
+ if (current === topLevelWorkflowPromise) return true;
80
+ seen.add(current);
81
+ }
82
+ return false;
83
+ }
84
+
85
+ function withSuppressedNativePromiseConsumption(callback) {
86
+ suppressNativePromiseConsumption++;
87
+ try {
88
+ return callback();
89
+ } finally {
90
+ suppressNativePromiseConsumption--;
91
+ }
35
92
  }
36
93
 
37
94
  function mergeObservations(...groups) {
@@ -39,7 +96,7 @@ function mergeObservations(...groups) {
39
96
  const merged = [];
40
97
  for (const group of groups) {
41
98
  for (const observation of group) {
42
- 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;
43
100
  seen.add(observation.callId);
44
101
  merged.push(observation);
45
102
  }
@@ -47,63 +104,107 @@ function mergeObservations(...groups) {
47
104
  return merged;
48
105
  }
49
106
 
50
- function trackedObservations(value) {
51
- return value && (typeof value === "object" || typeof value === "function") ? trackedPromiseObservations.get(value) ?? [] : [];
107
+ function trackedObservationTracker(value) {
108
+ return value && (typeof value === "object" || typeof value === "function") ? trackedPromiseTrackers.get(value) : undefined;
52
109
  }
53
110
 
54
- function withSuppressedRunObservation(callback) {
55
- suppressRunObservation += 1;
56
- try {
57
- return callback();
58
- } finally {
59
- suppressRunObservation -= 1;
111
+ function trackedPromiseTarget(value) {
112
+ return value && (typeof value === "object" || typeof value === "function") ? trackedPromiseTargets.get(value) ?? value : value;
113
+ }
114
+
115
+ function addTrackedObservations(tracker, observations) {
116
+ tracker.observations = mergeObservations(tracker.observations, observations);
117
+ if (!tracker.consumed) return;
118
+ for (const observation of tracker.observations) {
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 });
60
122
  }
61
123
  }
62
124
 
125
+ function markTrackedObservationsConsumed(tracker, seen = new Set()) {
126
+ if (seen.has(tracker)) return;
127
+ seen.add(tracker);
128
+ tracker.consumed = true;
129
+ addTrackedObservations(tracker, []);
130
+ for (const dependency of tracker.dependencies ?? []) markTrackedObservationsConsumed(dependency, seen);
131
+ }
132
+
133
+ function consumeTrackedObservations(tracker) {
134
+ if (isDirectWorkflowScriptPromiseHandlerCall()) return;
135
+ const activePromise = activeNativePromises.at(-1);
136
+ if (activePromise === topLevelWorkflowPromise || descendsFromTopLevelWorkflow(activePromise)) {
137
+ markTrackedObservationsConsumed(tracker);
138
+ } else if (activePromise) {
139
+ addTrackerDependency(nativePromiseTracker(activePromise), tracker);
140
+ } else {
141
+ markTrackedObservationsConsumed(tracker);
142
+ }
143
+ }
144
+
145
+ function trackObservationTracker(tracker, promise, allowFutureObservations = false) {
146
+ const target = trackedPromiseTarget(promise);
147
+ if ((!allowFutureObservations && tracker.observations.length === 0) || !target || typeof target.then !== "function") return promise;
148
+
149
+ const tracked = new Proxy(target, {
150
+ get(promiseTarget, prop) {
151
+ if (prop === "then") return function promiseThen(onFulfilled, onRejected) {
152
+ consumeTrackedObservations(tracker);
153
+ return trackObservationTracker({ observations: tracker.observations, consumed: false, dependencies: [tracker] }, promiseTarget.then(onFulfilled, onRejected), true);
154
+ };
155
+ if (prop === "catch") return function promiseCatch(onRejected) {
156
+ consumeTrackedObservations(tracker);
157
+ return trackObservationTracker({ observations: tracker.observations, consumed: false, dependencies: [tracker] }, promiseTarget.catch(onRejected), true);
158
+ };
159
+ if (prop === "finally") return function promiseFinally(onFinally) {
160
+ consumeTrackedObservations(tracker);
161
+ return trackObservationTracker({ observations: tracker.observations, consumed: false, dependencies: [tracker] }, promiseTarget.finally(onFinally), true);
162
+ };
163
+ return Reflect.get(promiseTarget, prop, promiseTarget);
164
+ },
165
+ });
166
+ trackedPromiseTrackers.set(tracked, tracker);
167
+ trackedPromiseTargets.set(tracked, target);
168
+ return tracked;
169
+ }
170
+
63
171
  function trackRunObservation(observations, promise) {
64
- const merged = mergeObservations(trackedObservations(promise), observations);
65
- if (merged.length === 0 || !promise || typeof promise.then !== "function") return promise;
66
- trackedPromiseObservations.set(promise, merged);
67
- if (promise[trackedPromisePatch]) return promise;
68
-
69
- const observedCallIds = new Set();
70
- const markObserved = () => {
71
- if (suppressRunObservation > 0 || isDirectWorkflowScriptPromiseHandlerCall()) return;
72
- for (const observation of trackedObservations(promise)) {
73
- if (observedCallIds.has(observation.callId)) continue;
74
- observedCallIds.add(observation.callId);
75
- parentPort.postMessage({ type: "runObserved", callId: observation.callId, key: observation.key });
76
- }
77
- };
78
- const originalThen = promise.then.bind(promise);
79
- const originalCatch = promise.catch.bind(promise);
80
- const originalFinally = promise.finally.bind(promise);
81
- Object.defineProperty(promise, trackedPromisePatch, { value: true });
82
- promise.then = (onFulfilled, onRejected) => {
83
- markObserved();
84
- return trackRunObservation(trackedObservations(promise), originalThen(onFulfilled, onRejected));
85
- };
86
- promise.catch = (onRejected) => {
87
- markObserved();
88
- return trackRunObservation(trackedObservations(promise), originalCatch(onRejected));
89
- };
90
- promise.finally = (onFinally) => {
91
- markObserved();
92
- return trackRunObservation(trackedObservations(promise), originalFinally(onFinally));
93
- };
94
- return promise;
172
+ const tracker = trackedObservationTracker(promise) ?? { observations: [], consumed: false };
173
+ addTrackedObservations(tracker, observations);
174
+ return trackObservationTracker(tracker, promise);
95
175
  }
96
176
 
97
177
  function trackPromiseCombinator(items, createPromise) {
98
178
  const values = Array.from(items);
99
- const observations = mergeObservations(...values.map(trackedObservations));
100
- const promise = withSuppressedRunObservation(() => createPromise(values));
101
- return observations.length > 0 ? trackRunObservation(observations, promise) : promise;
179
+ const dependencies = [...new Set(values.map(promiseObservationTracker).filter(Boolean))];
180
+ const promise = withSuppressedNativePromiseConsumption(() => createPromise(values.map(trackedPromiseTarget)));
181
+ if (dependencies.length === 0) return promise;
182
+ return trackObservationTracker({ observations: [], consumed: false, dependencies }, promise, true);
102
183
  }
103
184
 
104
185
  const workflowPromise = new Proxy(Promise, {
105
- construct(target, args) {
106
- return new target(...args);
186
+ construct(target, [executor]) {
187
+ if (typeof executor !== "function") return new target(executor);
188
+ const tracker = { observations: [], consumed: false };
189
+ const promise = new target((resolve, reject) => {
190
+ let settled = false;
191
+ try {
192
+ executor((value) => {
193
+ if (settled) return;
194
+ settled = true;
195
+ addTrackerDependency(tracker, promiseObservationTracker(value));
196
+ resolve(trackedPromiseTarget(value));
197
+ }, (reason) => {
198
+ if (settled) return;
199
+ settled = true;
200
+ reject(reason);
201
+ });
202
+ } catch (error) {
203
+ settled = true;
204
+ throw error;
205
+ }
206
+ });
207
+ return trackObservationTracker(tracker, promise, true);
107
208
  },
108
209
  get(target, prop) {
109
210
  if (prop === "all") return (items) => trackPromiseCombinator(items, (values) => target.all(values));
@@ -111,9 +212,10 @@ const workflowPromise = new Proxy(Promise, {
111
212
  if (prop === "race") return (items) => trackPromiseCombinator(items, (values) => target.race(values));
112
213
  if (prop === "any") return (items) => trackPromiseCombinator(items, (values) => target.any(values));
113
214
  if (prop === "resolve") return (value) => {
114
- const observations = trackedObservations(value);
115
- const promise = withSuppressedRunObservation(() => target.resolve(value));
116
- return observations.length > 0 ? trackRunObservation(observations, promise) : promise;
215
+ const dependency = promiseObservationTracker(value);
216
+ const promise = withSuppressedNativePromiseConsumption(() => target.resolve(trackedPromiseTarget(value)));
217
+ if (!dependency) return promise;
218
+ return trackObservationTracker({ observations: [], consumed: false, dependencies: [dependency] }, promise, true);
117
219
  };
118
220
  const value = target[prop];
119
221
  return typeof value === "function" ? value.bind(target) : value;
@@ -126,8 +228,8 @@ function hostCall(method, args, observation) {
126
228
  pending.set(callId, { resolve, reject });
127
229
  parentPort.postMessage({ type: "call", callId, method, args });
128
230
  });
129
- return observation && typeof observation.key === "string"
130
- ? 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)
131
233
  : promise;
132
234
  }
133
235
 
@@ -156,6 +258,7 @@ function validateRunCall(key, params, label, fingerprints) {
156
258
  const hint = label === "runs.run" ? "; use runs.all(...) and JavaScript control flow for orchestration." : ".";
157
259
  throw new Error(label + " accepts one child via { agent, task } and execution controls only" + hint);
158
260
  }
261
+ if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
159
262
  if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
160
263
  if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
161
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.");
@@ -173,7 +276,7 @@ function validateRunCall(key, params, label, fingerprints) {
173
276
  const runs = Object.freeze({
174
277
  run(key, params) {
175
278
  validateRunCall(key, params, "runs.run", runFingerprints);
176
- return hostCall("run", { key, params }, { key });
279
+ return hostCall("run", { key, params }, { key, operation: "run" });
177
280
  },
178
281
  all(items) {
179
282
  if (!Array.isArray(items)) throw new Error("runs.all(items) requires an array.");
@@ -190,7 +293,18 @@ const runs = Object.freeze({
190
293
  for (const { key, params } of calls) runFingerprints.set(key, stableRunJson(params));
191
294
  const batch = { id: "batch-" + (++nextCallId), calls };
192
295
  const launched = calls.map(({ key, params }) => runHostCall(key, params, true, batch));
193
- 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" });
194
308
  },
195
309
  status(keyOrRunId) { return hostCall("status", { keyOrRunId }); },
196
310
  ref: formatRef,
@@ -214,14 +328,6 @@ const state = Object.freeze({
214
328
  },
215
329
  });
216
330
 
217
- const prompts = Object.freeze({
218
- render(ref, vars) {
219
- if (typeof ref !== "string" || !ref.trim()) throw new Error("prompts.render(ref, vars) requires a non-empty ref string.");
220
- if (vars !== undefined) assertJsonValue(vars, "prompts.render vars");
221
- return hostCall("prompts.render", { ref, vars });
222
- },
223
- });
224
-
225
331
  let contextObjectPrototype;
226
332
 
227
333
  const capturedConsole = Object.freeze(Object.fromEntries(
@@ -231,7 +337,7 @@ const capturedConsole = Object.freeze(Object.fromEntries(
231
337
  ));
232
338
 
233
339
  function formatWorkflowScriptSyntaxError(error) {
234
- const details = error && error.stack ? error.stack : String(error);
340
+ const details = formatWorkflowScriptError(error);
235
341
  return [
236
342
  "workflowScript must be valid JavaScript.",
237
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.",
@@ -241,6 +347,52 @@ function formatWorkflowScriptSyntaxError(error) {
241
347
  ].join("\n");
242
348
  }
243
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
+
244
396
  function assertJsonValue(value, path = "emit", seen = new Set()) {
245
397
  if (value === null || typeof value === "string" || typeof value === "boolean") return;
246
398
  if (typeof value === "number") {
@@ -289,29 +441,89 @@ parentPort.on("message", async (message) => {
289
441
  if (!entry) return;
290
442
  pending.delete(message.callId);
291
443
  if (message.ok) entry.resolve(message.value);
292
- else entry.reject(new Error(message.error));
444
+ else {
445
+ const error = new Error(message.error);
446
+ if (message.errorKind === "detached-child") error.workflowErrorKind = "detached-child";
447
+ entry.reject(error);
448
+ }
293
449
  return;
294
450
  }
295
451
  if (message.type !== "start") return;
296
452
  try {
297
- 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 };
298
454
  if (message.stateEnabled) sandbox.state = state;
299
455
  const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
300
456
  contextObjectPrototype = vm.runInContext("Object.prototype", context);
301
457
  let compiled;
302
458
  try {
459
+ assertPortableWorkflowScript(message.script);
303
460
  compiled = new vm.Script("(async () => {\n" + message.script + "\n})()", { filename: "workflow-script.js" });
304
461
  } catch (error) {
305
- if (!(error instanceof SyntaxError)) throw error;
306
- parentPort.postMessage({ type: "error", error: formatWorkflowScriptSyntaxError(error) });
462
+ parentPort.postMessage({ type: "error", error: isSyntaxError(error) ? formatWorkflowScriptSyntaxError(error) : formatWorkflowScriptError(error) });
307
463
  return;
308
464
  }
309
- const value = await compiled.runInContext(context);
465
+ const nativePromisePrototype = vm.runInContext("(async () => {})().constructor.prototype", context);
466
+ const nativeThenDescriptor = Object.getOwnPropertyDescriptor(nativePromisePrototype, "then");
467
+ if (!nativeThenDescriptor || typeof nativeThenDescriptor.value !== "function") throw new Error("workflowScript could not inspect the VM Promise.prototype.then method.");
468
+ const nativeThen = nativeThenDescriptor.value;
469
+ let stopWorkflowPromiseHook;
470
+ let value;
471
+ try {
472
+ Object.defineProperty(nativePromisePrototype, "then", {
473
+ ...nativeThenDescriptor,
474
+ value: function workflowPromiseThen(...args) {
475
+ if (isDirectWorkflowScriptPromiseHandlerCall() || suppressNativePromiseConsumption > 0) {
476
+ return withSuppressedNativePromiseConsumption(() => Reflect.apply(nativeThen, this, args));
477
+ }
478
+ return Reflect.apply(nativeThen, this, args);
479
+ },
480
+ });
481
+ stopWorkflowPromiseHook = createWorkflowPromiseHook({
482
+ before(promise) {
483
+ activeNativePromises.push(promise);
484
+ },
485
+ after(promise) {
486
+ const index = activeNativePromises.lastIndexOf(promise);
487
+ if (index !== -1) activeNativePromises.splice(index, 1);
488
+ },
489
+ init(promise, parent) {
490
+ const childTracker = nativePromiseTracker(promise);
491
+ if (!parent) return;
492
+ nativePromiseParents.set(promise, parent);
493
+ const parentTracker = nativePromiseTracker(parent);
494
+ addTrackerDependency(childTracker, parentTracker);
495
+ const activePromise = activeNativePromises.at(-1);
496
+ if (activePromise && activePromise !== parent) {
497
+ addTrackerDependency(nativePromiseTracker(activePromise), parentTracker);
498
+ } else if (!activePromise && suppressNativePromiseConsumption === 0) {
499
+ markTrackedObservationsConsumed(parentTracker);
500
+ }
501
+ },
502
+ });
503
+ const workflowResultPromise = compiled.runInContext(context);
504
+ topLevelWorkflowPromise = workflowResultPromise;
505
+ markTrackedObservationsConsumed(nativePromiseTracker(workflowResultPromise));
506
+ value = await workflowResultPromise;
507
+ } finally {
508
+ try {
509
+ stopWorkflowPromiseHook?.();
510
+ } finally {
511
+ try {
512
+ Object.defineProperty(nativePromisePrototype, "then", nativeThenDescriptor);
513
+ } finally {
514
+ topLevelWorkflowPromise = undefined;
515
+ activeNativePromises.length = 0;
516
+ suppressNativePromiseConsumption = 0;
517
+ nativePromiseTrackers = new WeakMap();
518
+ nativePromiseParents = new WeakMap();
519
+ }
520
+ }
521
+ }
310
522
  const persistedValue = value === undefined ? null : omitUndefinedWorkflowValues(value);
311
523
  assertJsonValue(persistedValue, "return");
312
524
  parentPort.postMessage({ type: "complete", value: persistedValue });
313
525
  } catch (error) {
314
- parentPort.postMessage({ type: "error", error: error && error.stack ? error.stack : String(error) });
526
+ parentPort.postMessage({ type: "error", error: isSyntaxError(error) ? formatWorkflowScriptSyntaxError(error) : formatWorkflowScriptError(error), ...(error && error.workflowErrorKind === "detached-child" ? { errorKind: "detached-child" } : {}) });
315
527
  }
316
528
  });
317
529
  `;
@@ -324,15 +536,16 @@ export interface WorkflowScriptChildResult {
324
536
  runId?: string;
325
537
  output: string;
326
538
  error?: string;
539
+ detached?: boolean;
327
540
  structuredOutput?: unknown;
328
541
  artifactPaths: string[];
329
542
  results?: unknown[];
330
543
  }
331
544
 
332
545
  export interface WorkflowScriptTraceEntry {
333
- operation: "run" | "status";
546
+ operation: "run" | "status" | "steer";
334
547
  key: string;
335
- state: "started" | "completed" | "failed" | "stopped" | "reused";
548
+ state: "started" | "completed" | "failed" | "detached" | "stopped" | "reused" | "queued" | "delivered" | "missed";
336
549
  /** Canonical child agent name when resolved launch or result data is available. */
337
550
  agent?: string;
338
551
  runId?: string;
@@ -342,6 +555,21 @@ export interface WorkflowScriptTraceEntry {
342
555
  error?: string;
343
556
  }
344
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
+
345
573
  export interface WorkflowScriptResult {
346
574
  value: unknown;
347
575
  emits: unknown[];
@@ -352,11 +580,13 @@ export interface WorkflowScriptResult {
352
580
 
353
581
  export class WorkflowScriptError extends Error {
354
582
  readonly partial: Omit<WorkflowScriptResult, "value">;
583
+ readonly errorKind?: "detached-child";
355
584
 
356
- constructor(message: string, partial: Omit<WorkflowScriptResult, "value">) {
585
+ constructor(message: string, partial: Omit<WorkflowScriptResult, "value">, errorKind?: "detached-child") {
357
586
  super(message);
358
587
  this.name = "WorkflowScriptError";
359
588
  this.partial = partial;
589
+ this.errorKind = errorKind;
360
590
  }
361
591
  }
362
592
 
@@ -367,13 +597,11 @@ export interface RunWorkflowScriptOptions {
367
597
  admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
368
598
  launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean }) => Promise<WorkflowScriptChildResult>;
369
599
  status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
600
+ steer?: (key: string, message: string, options: WorkflowSteerOptions, signal: AbortSignal) => Promise<WorkflowSteerResult>;
370
601
  state?: {
371
602
  get: (key: string) => unknown | Promise<unknown>;
372
603
  set: (key: string, value: unknown) => void | Promise<void>;
373
604
  };
374
- prompts?: {
375
- render: (ref: string, vars?: unknown) => string | Promise<string>;
376
- };
377
605
  onTrace?: (trace: WorkflowScriptTraceEntry[]) => void;
378
606
  onEmit?: (emits: unknown[]) => void;
379
607
  }
@@ -481,22 +709,50 @@ function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowS
481
709
  };
482
710
  }
483
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
+
484
731
  export async function runWorkflowScript(options: RunWorkflowScriptOptions): Promise<WorkflowScriptResult> {
485
732
  if (!options.script.trim()) throw new Error("workflowScript must not be empty.");
486
733
  if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error("workflow script timeout must be a positive integer.");
487
734
 
488
- 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 } });
489
742
  const emits: unknown[] = [];
490
743
  const consoleEntries: WorkflowScriptResult["console"] = [];
491
744
  const trace: WorkflowScriptTraceEntry[] = [];
492
745
  const children = new Map<string, WorkflowScriptChildResult>();
493
746
  const childOrder: string[] = [];
494
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 }>();
495
749
  const stoppedLaunches = new Set<string>();
496
750
  const batchAdmissions = new Map<string, Promise<void>>();
497
751
  const observedRunCalls = new Set<number>();
752
+ const observedSteerCalls = new Set<number>();
498
753
  const childController = new AbortController();
499
754
  let settled = false;
755
+ let finishing = false;
500
756
 
501
757
  const partial = (): Omit<WorkflowScriptResult, "value"> => ({ emits, console: consoleEntries, trace, children: childOrder.flatMap((key) => {
502
758
  const child = children.get(key);
@@ -505,20 +761,29 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
505
761
  const traceChanged = () => options.onTrace?.([...trace]);
506
762
 
507
763
  return await new Promise<WorkflowScriptResult>((resolve, reject) => {
508
- const finish = (outcome: { value: unknown } | { error: Error }) => {
509
- if (settled) return;
510
- settled = true;
511
- if (timer) clearTimeout(timer);
512
- options.signal?.removeEventListener("abort", onAbort);
513
- void worker.terminate();
514
- const unobservedKeys = "value" in outcome ? [...launches].filter(([, launch]) => !launch.observed).map(([key]) => key) : [];
515
- const completionError = unobservedKeys.length > 0
516
- ? new Error(`workflowScript completed with unawaited runs.run launch(es): ${unobservedKeys.map((key) => `'${key}'`).join(", ")}. Await or return each launch.`)
517
- : undefined;
518
- childController.abort("error" in outcome ? outcome.error : completionError ?? new Error("Workflow script completed."));
519
- if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial()));
520
- else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
521
- else resolve({ value: outcome.value, ...partial() });
764
+ const finish = (outcome: { value: unknown } | { error: Error & { workflowErrorKind?: unknown } }) => {
765
+ if (settled || finishing) return;
766
+ finishing = true;
767
+ childController.abort("error" in outcome ? outcome.error : new Error("Workflow script completed."));
768
+ void Promise.allSettled([...steers.values()].map(({ promise }) => promise)).then(() => {
769
+ if (settled) return;
770
+ settled = true;
771
+ if (timer) clearTimeout(timer);
772
+ options.signal?.removeEventListener("abort", onAbort);
773
+ void worker.terminate();
774
+ const unobservedKeys = "value" in outcome ? [...launches].filter(([, launch]) => !launch.observed).map(([key]) => key) : [];
775
+ const completionError = unobservedKeys.length > 0
776
+ ? 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.`)
777
+ : "value" in outcome
778
+ ? (() => {
779
+ const unobservedSteers = [...steers.values()].filter((steer) => !steer.observed).map((steer) => steer.key);
780
+ 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;
781
+ })()
782
+ : undefined;
783
+ if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child" ? "detached-child" : undefined));
784
+ else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
785
+ else resolve({ value: outcome.value, ...partial() });
786
+ });
522
787
  };
523
788
  const onAbort = () => {
524
789
  const signalReason = options.signal?.reason;
@@ -585,12 +850,22 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
585
850
  }
586
851
  return finish({ value: message.value });
587
852
  }
588
- if (message.type === "error") return finish({ error: new Error(typeof message.error === "string" ? message.error : "Workflow script failed.") });
589
- if (message.type === "runObserved" && typeof message.callId === "number") {
853
+ if (message.type === "error") {
854
+ const workflowError = new Error(typeof message.error === "string" ? message.error : "Workflow script failed.") as Error & { workflowErrorKind?: "detached-child" };
855
+ if (message.errorKind === "detached-child") workflowError.workflowErrorKind = "detached-child";
856
+ return finish({ error: workflowError });
857
+ }
858
+ if (message.type === "callObserved" && typeof message.callId === "number") {
590
859
  const key = typeof message.key === "string" ? message.key : undefined;
591
- const launch = key ? launches.get(key) : undefined;
592
- if (launch) launch.observed = true;
593
- else observedRunCalls.add(message.callId);
860
+ if (message.operation === "run") {
861
+ const launch = key ? launches.get(key) : undefined;
862
+ if (launch) launch.observed = true;
863
+ else observedRunCalls.add(message.callId);
864
+ } else if (message.operation === "steer") {
865
+ const steer = steers.get(message.callId);
866
+ if (steer) steer.observed = true;
867
+ else observedSteerCalls.add(message.callId);
868
+ }
594
869
  return;
595
870
  }
596
871
  if (message.type !== "call" || typeof message.callId !== "number" || typeof message.method !== "string" || !isRecord(message.args)) return;
@@ -601,22 +876,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
601
876
  if (!settled) worker.postMessage({ type: "response", callId: message.callId, ok: true, value: omitUndefinedWorkflowValues(value) });
602
877
  },
603
878
  (error: unknown) => {
604
- if (!settled) worker.postMessage({ type: "response", callId: message.callId, ok: false, error: error instanceof Error ? error.message : String(error) });
879
+ if (!settled) worker.postMessage({ type: "response", callId: message.callId, ok: false, error: error instanceof Error ? error.message : String(error), ...(error instanceof Error && (error as { workflowErrorKind?: unknown }).workflowErrorKind === "detached-child" ? { errorKind: "detached-child" } : {}) });
605
880
  },
606
881
  );
607
882
  };
608
-
609
- if (message.method === "prompts.render") {
610
- if (!options.prompts) return respond(Promise.reject(new Error("Workflow prompt rendering is unavailable.")));
611
- const ref = message.args.ref;
612
- const vars = message.args.vars;
613
- if (typeof ref !== "string" || !ref.trim()) return respond(Promise.reject(new Error("prompts.render(ref, vars) requires a non-empty ref string.")));
614
- return respond(Promise.resolve().then(() => options.prompts!.render(ref, vars)).then((rendered) => {
615
- if (typeof rendered !== "string") throw new Error("prompts.render must return task text.");
616
- return rendered;
617
- }));
618
- }
619
-
620
883
  if (message.method === "state.get" || message.method === "state.set") {
621
884
  if (!options.state) return respond(Promise.reject(new Error("Workflow state is unavailable without a mission.")));
622
885
  let key: string;
@@ -642,9 +905,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
642
905
  const target = known?.runId ?? keyOrRunId;
643
906
  trace.push({ operation: "status", key: keyOrRunId, state: "started", ...(known?.runId ? { runId: known.runId } : {}) });
644
907
  traceChanged();
645
- if (settled) return;
908
+ if (settled || finishing) return;
646
909
  respond(options.status(target, childController.signal).then((result) => {
647
- if (settled) return result;
910
+ if (settled || finishing) return result;
648
911
  trace.push({ operation: "status", key: keyOrRunId, state: result.ok ? "completed" : "failed", ...(result.runId ? { runId: result.runId } : {}), ...(!result.ok ? { error: result.output } : {}) });
649
912
  traceChanged();
650
913
  if (!result.ok) throw new Error(`Status '${keyOrRunId}' failed: ${result.output}`);
@@ -652,6 +915,37 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
652
915
  }));
653
916
  return;
654
917
  }
918
+ if (message.method === "steer") {
919
+ let key: string;
920
+ try {
921
+ key = validateKey(message.args.key, "runs.steer");
922
+ } catch (error) {
923
+ return respond(Promise.reject(error));
924
+ }
925
+ const steerMessage = message.args.message;
926
+ if (typeof steerMessage !== "string" || !steerMessage.trim()) return respond(Promise.reject(new Error(`runs.steer('${key}') requires a non-empty message.`)));
927
+ const steerOptions = isRecord(message.args.options) ? message.args.options as WorkflowSteerOptions : {};
928
+ const startedAt = Date.now();
929
+ trace.push({ operation: "steer", key, state: "started" });
930
+ traceChanged();
931
+ const promise = Promise.resolve().then(() => {
932
+ if (!launches.has(key)) throw new Error(`runs.steer('${key}') requires a prior runs.run/runs.all launch with that key.`);
933
+ if (!options.steer) throw new Error("Workflow steering is unavailable in this host.");
934
+ return options.steer(key, steerMessage.trim(), steerOptions, childController.signal);
935
+ }).then((receipt) => {
936
+ trace.push({ operation: "steer", key, state: receipt.state, durationMs: Date.now() - startedAt, ...(receipt.error ? { error: receipt.error } : {}) });
937
+ traceChanged();
938
+ return receipt;
939
+ }, (error: unknown) => {
940
+ const text = error instanceof Error ? error.message : String(error);
941
+ trace.push({ operation: "steer", key, state: "failed", durationMs: Date.now() - startedAt, error: text });
942
+ traceChanged();
943
+ throw error;
944
+ });
945
+ steers.set(message.callId, { key, promise, observed: observedSteerCalls.delete(message.callId) });
946
+ respond(promise);
947
+ return;
948
+ }
655
949
  if (message.method !== "run") return respond(Promise.reject(new Error(`Unknown runs API method '${message.method}'.`)));
656
950
 
657
951
  let key: string;
@@ -693,7 +987,11 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
693
987
  const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
694
988
  ? promise
695
989
  : promise.then((result) => {
696
- if (!result.ok) throw new Error(`Run '${key}' failed: ${result.error ?? result.output}`);
990
+ if (!result.ok) {
991
+ const childError = new Error(result.detached ? `Run '${key}' detached: ${result.error ?? result.output}` : `Run '${key}' failed: ${result.error ?? result.output}`) as Error & { workflowErrorKind?: "detached-child" };
992
+ if (result.detached) childError.workflowErrorKind = "detached-child";
993
+ throw childError;
994
+ }
697
995
  return result;
698
996
  });
699
997
  const fingerprint = stableJson(params);
@@ -719,13 +1017,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
719
1017
  return true;
720
1018
  });
721
1019
  admission = Promise.resolve().then(() => {
722
- if (settled) return;
1020
+ if (settled || finishing) return;
723
1021
  return options.admit?.(calls);
724
1022
  });
725
1023
  if (batch) batchAdmissions.set(batch.id, admission);
726
1024
  }
727
1025
  const promise = admission.then(() => {
728
- if (settled || stoppedLaunches.has(key)) {
1026
+ if (settled || finishing || stoppedLaunches.has(key)) {
729
1027
  const reason = childController.signal.reason;
730
1028
  const text = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "Workflow script aborted.";
731
1029
  return { key, ok: false, output: text, error: text, artifactPaths: [] };
@@ -735,7 +1033,8 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
735
1033
  const normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
736
1034
  if (stoppedLaunches.has(key)) return normalized;
737
1035
  children.set(key, normalized);
738
- trace.push({ operation: "run", key, state: normalized.ok ? "completed" : "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
1036
+ const state = normalized.ok ? "completed" : normalized.detached ? "detached" : "failed";
1037
+ trace.push({ operation: "run", key, state, durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
739
1038
  traceChanged();
740
1039
  return normalized;
741
1040
  }, (error: unknown) => {