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.
- package/CHANGELOG.md +103 -0
- package/agents/oracle.md +3 -1
- package/agents/reviewer.md +1 -0
- package/agents/scout.md +2 -2
- package/agents/worker.md +2 -1
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +40 -2
- package/docs/configuration.md +30 -12
- package/docs/extension-api.md +42 -1
- package/docs/models.md +2 -0
- package/docs/observability.md +41 -5
- package/docs/tool-reference.md +38 -39
- package/docs/workflows.md +171 -3
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +6 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
- package/skills/pi-subagents/references/execution-controls.md +29 -15
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
- package/src/agents/agent-management.ts +124 -351
- package/src/agents/agents.ts +157 -31
- package/src/agents/skills.ts +1 -1
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/preflight.ts +36 -8
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +3 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +166 -88
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/schemas.ts +12 -35
- package/src/extension/tool-description.ts +35 -24
- package/src/inspectors/herdr/actions.ts +2 -2
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +2 -2
- package/src/intercom/native-supervisor-channel.ts +32 -10
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/missions/workflow-state.ts +2 -2
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +31 -8
- package/src/runs/background/async-execution.ts +68 -40
- package/src/runs/background/async-job-tracker.ts +20 -4
- package/src/runs/background/async-resume.ts +47 -15
- package/src/runs/background/async-retention.ts +886 -0
- package/src/runs/background/async-status.ts +39 -53
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/completion-dedupe.ts +5 -1
- package/src/runs/background/completion-replay.ts +22 -12
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +68 -19
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/inspect-rpc.ts +443 -0
- package/src/runs/background/notify.ts +31 -5
- package/src/runs/background/result-files.ts +158 -90
- package/src/runs/background/result-watcher.ts +116 -20
- package/src/runs/background/resume-guidance.ts +8 -5
- package/src/runs/background/retained-children.ts +13 -3
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +11 -9
- package/src/runs/background/run-status.ts +27 -18
- package/src/runs/background/scheduled-runs.ts +24 -4
- package/src/runs/background/stale-run-reconciler.ts +6 -3
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +253 -140
- package/src/runs/background/subagent-wait.ts +8 -8
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +21 -4
- package/src/runs/background/wait-subscriptions.ts +81 -2
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +666 -1579
- package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/acceptance.ts +4 -4
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/external-job-bridge.ts +444 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +98 -38
- package/src/runs/shared/orca-progress-tabs.ts +84 -22
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +4 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/session-lease.ts +0 -6
- package/src/runs/shared/subagent-control.ts +26 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +14 -2
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +71 -26
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-bridge.ts +2 -1
- package/src/slash/slash-commands.ts +29 -3
- package/src/tui/fleet.ts +63 -15
- package/src/watchdog/change-signature.ts +1 -1
- package/src/watchdog/lsp-diagnostics.ts +1 -0
- package/src/workflows/chat-progress.ts +2 -2
- package/src/workflows/scripted-workflow.ts +220 -67
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- 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
|
-
|
|
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
|
|
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 (
|
|
103
|
-
|
|
104
|
-
parentPort.postMessage({ type: "
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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 === "
|
|
869
|
+
if (message.type === "callObserved" && typeof message.callId === "number") {
|
|
742
870
|
const key = typeof message.key === "string" ? message.key : undefined;
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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)
|
|
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: [] };
|