@agwab/pi-workflow 0.2.0 → 0.3.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/README.md +2 -0
- package/dist/compiler.d.ts +4 -6
- package/dist/compiler.js +70 -39
- package/dist/dynamic-decision.d.ts +0 -1
- package/dist/dynamic-decision.js +0 -7
- package/dist/dynamic-generated-task-runtime.d.ts +2 -0
- package/dist/dynamic-generated-task-runtime.js +21 -8
- package/dist/dynamic-profiles.d.ts +0 -1
- package/dist/dynamic-profiles.js +0 -3
- package/dist/engine-run-graph.d.ts +1 -0
- package/dist/engine-run-graph.js +142 -2
- package/dist/engine.d.ts +10 -6
- package/dist/engine.js +146 -77
- package/dist/extension.d.ts +2 -1
- package/dist/extension.js +38 -15
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -1
- package/dist/store.d.ts +3 -1
- package/dist/store.js +189 -49
- package/dist/subagent-backend.d.ts +4 -0
- package/dist/subagent-backend.js +281 -31
- package/dist/types.d.ts +9 -1
- package/dist/workflow-runtime.d.ts +2 -0
- package/dist/workflow-runtime.js +40 -1
- package/dist/workflow-view.js +3 -1
- package/dist/workflow-web-source-extension.js +167 -48
- package/dist/workflow-web-source.d.ts +2 -1
- package/dist/workflow-web-source.js +84 -19
- package/docs/usage.md +11 -0
- package/node_modules/@agwab/pi-subagent/README.md +3 -3
- package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
- package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
- package/node_modules/@agwab/pi-subagent/package.json +2 -2
- package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
- package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
- package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
- package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
- package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
- package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
- package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
- package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
- package/package.json +2 -2
- package/src/compiler.ts +127 -66
- package/src/dynamic-decision.ts +0 -11
- package/src/dynamic-generated-task-runtime.ts +47 -12
- package/src/dynamic-profiles.ts +0 -4
- package/src/engine-run-graph.ts +185 -2
- package/src/engine.ts +192 -107
- package/src/extension.ts +50 -17
- package/src/index.ts +3 -1
- package/src/store.ts +253 -55
- package/src/subagent-backend.ts +369 -32
- package/src/types.ts +13 -1
- package/src/workflow-runtime.ts +53 -2
- package/src/workflow-view.ts +2 -1
- package/src/workflow-web-source-extension.ts +621 -228
- package/src/workflow-web-source.ts +118 -28
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
- package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
- package/workflows/deep-research/helpers/render-executive.mjs +8 -21
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
- package/workflows/impact-review/spec.json +3 -3
- package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
- package/dist/dynamic-loader.d.ts +0 -25
- package/dist/dynamic-loader.js +0 -13
- package/src/dynamic-loader.ts +0 -49
- package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
package/dist/subagent-backend.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { copyFile, mkdir, readFile, readdir, rm, writeFile, } from "node:fs/promises";
|
|
3
3
|
import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
4
|
+
import { availableParallelism } from "node:os";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { fromProjectPath, isTerminalTaskStatus, nowIso, toProjectPath, writeRunRecord, } from "./store.js";
|
|
6
7
|
import { applyTaskResultArtifact, isTaskTimedOut, markTaskTimedOut, } from "./result.js";
|
|
@@ -15,6 +16,15 @@ const FETCH_CONTENT_CACHE_ENV = "PI_WORKFLOW_FETCH_CONTENT_CACHE";
|
|
|
15
16
|
const LEGACY_FETCH_CACHE_ENV = "PI_WORKFLOW_FETCH_CACHE";
|
|
16
17
|
const DEFAULT_TRANSIENT_MODEL_FAILURE_RETRIES = 5;
|
|
17
18
|
const DEFAULT_ARTIFACT_OUTPUT_RETRIES = 2;
|
|
19
|
+
const MAX_CONCURRENT_LAUNCHES_ENV = "PI_WORKFLOW_MAX_CONCURRENT_LAUNCHES";
|
|
20
|
+
const PARENT_SUBAGENT_CWD_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_CWD";
|
|
21
|
+
const PARENT_SUBAGENT_RUNS_DIR_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUNS_DIR";
|
|
22
|
+
const PARENT_SUBAGENT_RUN_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUN_ID";
|
|
23
|
+
const PARENT_SUBAGENT_ATTEMPT_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_ATTEMPT_ID";
|
|
24
|
+
const DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS = 3_000;
|
|
25
|
+
const STALE_LAUNCH_CLAIM_GRACE_MS = 30_000;
|
|
26
|
+
const MIN_TRANSIENT_RETRY_JITTER_MS = 1_000;
|
|
27
|
+
const MAX_TRANSIENT_RETRY_JITTER_MS = 5_000;
|
|
18
28
|
const MODULE_PATH = fileURLToPath(import.meta.url);
|
|
19
29
|
const MODULE_DIR = dirname(MODULE_PATH);
|
|
20
30
|
const BUNDLED_PI_WEB_ACCESS_EXTENSION = bundledNodeModulePath("pi-web-access", "index.ts");
|
|
@@ -34,6 +44,12 @@ function bundledNodeModulePath(packageName, ...parts) {
|
|
|
34
44
|
];
|
|
35
45
|
return (candidates.find((candidate) => existsSync(candidate)) ?? candidates[0]);
|
|
36
46
|
}
|
|
47
|
+
const GENERIC_TASK_STATUS_DETAILS = new Set([
|
|
48
|
+
"completed",
|
|
49
|
+
"failed",
|
|
50
|
+
"interrupted",
|
|
51
|
+
"running",
|
|
52
|
+
]);
|
|
37
53
|
const subagentApiSpecifier = "@agwab/pi-subagent/api";
|
|
38
54
|
let cachedSubagentApi;
|
|
39
55
|
let injectedSubagentApi;
|
|
@@ -47,10 +63,143 @@ async function loadSubagentApi() {
|
|
|
47
63
|
cachedSubagentApi ??= import(subagentApiSpecifier).then((mod) => mod);
|
|
48
64
|
return cachedSubagentApi;
|
|
49
65
|
}
|
|
66
|
+
function nonEmptyEnv(env, key) {
|
|
67
|
+
const value = env[key]?.trim();
|
|
68
|
+
return value ? value : undefined;
|
|
69
|
+
}
|
|
70
|
+
function parentSubagentRefFromEnv(env = process.env) {
|
|
71
|
+
const cwd = nonEmptyEnv(env, PARENT_SUBAGENT_CWD_ENV);
|
|
72
|
+
const runsDir = nonEmptyEnv(env, PARENT_SUBAGENT_RUNS_DIR_ENV);
|
|
73
|
+
const runId = nonEmptyEnv(env, PARENT_SUBAGENT_RUN_ID_ENV);
|
|
74
|
+
if (!cwd || !runsDir || !runId)
|
|
75
|
+
return undefined;
|
|
76
|
+
const attemptId = nonEmptyEnv(env, PARENT_SUBAGENT_ATTEMPT_ID_ENV);
|
|
77
|
+
return { cwd, runsDir, runId, ...(attemptId ? { attemptId } : {}) };
|
|
78
|
+
}
|
|
79
|
+
function terminalChildEventForTaskStatus(status) {
|
|
80
|
+
if (status === "completed")
|
|
81
|
+
return "completed";
|
|
82
|
+
if (status === "failed")
|
|
83
|
+
return "failed";
|
|
84
|
+
if (status === "interrupted")
|
|
85
|
+
return "cancelled";
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
async function recordParentSubagentChildEvent(options) {
|
|
89
|
+
const parent = parentSubagentRefFromEnv();
|
|
90
|
+
if (!parent)
|
|
91
|
+
return;
|
|
92
|
+
const api = await loadSubagentApi().catch(() => undefined);
|
|
93
|
+
if (!api?.recordSubagentChildEvent)
|
|
94
|
+
return;
|
|
95
|
+
await api
|
|
96
|
+
.recordSubagentChildEvent({
|
|
97
|
+
...parent,
|
|
98
|
+
event: options.event,
|
|
99
|
+
childRunId: options.childRunId,
|
|
100
|
+
workflowRunId: options.run.runId,
|
|
101
|
+
childTaskId: options.task.taskId,
|
|
102
|
+
...(options.failureKind === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: { failureKind: options.failureKind }),
|
|
105
|
+
...(options.message === undefined ? {} : { message: options.message }),
|
|
106
|
+
})
|
|
107
|
+
.catch(() => undefined);
|
|
108
|
+
}
|
|
109
|
+
async function recordTerminalParentSubagentChildEvent(run, task, snapshot) {
|
|
110
|
+
const event = terminalChildEventForTaskStatus(task.status);
|
|
111
|
+
if (!event)
|
|
112
|
+
return;
|
|
113
|
+
const taskFailureKind = task.statusDetail && !GENERIC_TASK_STATUS_DETAILS.has(task.statusDetail)
|
|
114
|
+
? task.statusDetail
|
|
115
|
+
: undefined;
|
|
116
|
+
await recordParentSubagentChildEvent({
|
|
117
|
+
event,
|
|
118
|
+
childRunId: snapshot.runId,
|
|
119
|
+
run,
|
|
120
|
+
task,
|
|
121
|
+
failureKind: event === "completed"
|
|
122
|
+
? undefined
|
|
123
|
+
: (snapshot.failureKind ?? taskFailureKind ?? task.statusDetail),
|
|
124
|
+
message: task.lastMessage,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
let launchSlotReleaseDelayMs = DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
|
|
128
|
+
let transientRetryJitterForTests;
|
|
129
|
+
const launchWaitQueue = [];
|
|
130
|
+
let activeLaunchSlots = 0;
|
|
131
|
+
function resolveMaxConcurrentLaunches() {
|
|
132
|
+
const override = Number.parseInt(process.env[MAX_CONCURRENT_LAUNCHES_ENV] ?? "", 10);
|
|
133
|
+
if (Number.isFinite(override))
|
|
134
|
+
return Math.max(1, Math.floor(override));
|
|
135
|
+
return Math.max(2, Math.floor(availableParallelism() / 2));
|
|
136
|
+
}
|
|
137
|
+
function isLaunchGateSaturated() {
|
|
138
|
+
return activeLaunchSlots >= resolveMaxConcurrentLaunches();
|
|
139
|
+
}
|
|
140
|
+
async function acquireLaunchSlot() {
|
|
141
|
+
if (!isLaunchGateSaturated()) {
|
|
142
|
+
activeLaunchSlots += 1;
|
|
143
|
+
return releaseLaunchSlot;
|
|
144
|
+
}
|
|
145
|
+
await new Promise((resolveWait) => launchWaitQueue.push(resolveWait));
|
|
146
|
+
return releaseLaunchSlot;
|
|
147
|
+
}
|
|
148
|
+
function releaseLaunchSlot() {
|
|
149
|
+
const next = launchWaitQueue.shift();
|
|
150
|
+
if (next) {
|
|
151
|
+
// Transfer the occupied slot directly to the queued launcher.
|
|
152
|
+
next();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
activeLaunchSlots = Math.max(0, activeLaunchSlots - 1);
|
|
156
|
+
}
|
|
157
|
+
function releaseLaunchSlotAfterDelay(delayMs, release) {
|
|
158
|
+
if (delayMs <= 0) {
|
|
159
|
+
release();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
setTimeout(release, delayMs);
|
|
163
|
+
}
|
|
164
|
+
async function runWithLaunchSlot(action) {
|
|
165
|
+
const release = await acquireLaunchSlot();
|
|
166
|
+
let holdAfterReturn = false;
|
|
167
|
+
try {
|
|
168
|
+
const result = await action();
|
|
169
|
+
holdAfterReturn = true;
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
releaseLaunchSlotAfterDelay(holdAfterReturn ? launchSlotReleaseDelayMs : 0, release);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function transientRetryJitterMs() {
|
|
177
|
+
if (transientRetryJitterForTests)
|
|
178
|
+
return transientRetryJitterForTests();
|
|
179
|
+
return (MIN_TRANSIENT_RETRY_JITTER_MS +
|
|
180
|
+
Math.floor(Math.random() *
|
|
181
|
+
(MAX_TRANSIENT_RETRY_JITTER_MS - MIN_TRANSIENT_RETRY_JITTER_MS + 1)));
|
|
182
|
+
}
|
|
183
|
+
function sleep(ms) {
|
|
184
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
185
|
+
}
|
|
186
|
+
export function setSubagentLaunchControlsForTests(options) {
|
|
187
|
+
launchSlotReleaseDelayMs =
|
|
188
|
+
options?.releaseDelayMs === undefined
|
|
189
|
+
? DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS
|
|
190
|
+
: Math.max(0, Math.floor(options.releaseDelayMs));
|
|
191
|
+
transientRetryJitterForTests =
|
|
192
|
+
options?.retryJitterMs === undefined
|
|
193
|
+
? undefined
|
|
194
|
+
: typeof options.retryJitterMs === "function"
|
|
195
|
+
? options.retryJitterMs
|
|
196
|
+
: () => Math.max(0, Math.floor(options.retryJitterMs));
|
|
197
|
+
activeLaunchSlots = 0;
|
|
198
|
+
while (launchWaitQueue.length > 0)
|
|
199
|
+
launchWaitQueue.shift()?.();
|
|
200
|
+
}
|
|
50
201
|
export async function cleanupSubagentRun(_cwd, run) {
|
|
51
202
|
for (const task of run.tasks) {
|
|
52
|
-
if (isTerminalTaskStatus(task.status))
|
|
53
|
-
continue;
|
|
54
203
|
const handle = getSubagentHandle(task);
|
|
55
204
|
if (!handle)
|
|
56
205
|
continue;
|
|
@@ -77,6 +226,14 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
|
|
|
77
226
|
message: "fast:on is not supported for pi-workflow execution.",
|
|
78
227
|
};
|
|
79
228
|
}
|
|
229
|
+
if ((task.launchRetry?.attempts ?? 0) > 0) {
|
|
230
|
+
const jitterMs = transientRetryJitterMs();
|
|
231
|
+
task.statusDetail = "retry_model_failure";
|
|
232
|
+
task.lastMessage = `waiting ${jitterMs}ms before retrying transient-model launch`;
|
|
233
|
+
await writeRunRecord(cwd, run);
|
|
234
|
+
if (jitterMs > 0)
|
|
235
|
+
await sleep(jitterMs);
|
|
236
|
+
}
|
|
80
237
|
const systemPromptFile = fromProjectPath(cwd, task.files.systemPrompt);
|
|
81
238
|
const taskPromptFile = fromProjectPath(cwd, task.files.taskPrompt);
|
|
82
239
|
const outputFile = fromProjectPath(cwd, task.files.output);
|
|
@@ -126,7 +283,11 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
|
|
|
126
283
|
subagentOptions.extensions = extensions;
|
|
127
284
|
if (captureToolCallsEnabled())
|
|
128
285
|
subagentOptions.captureToolCalls = true;
|
|
129
|
-
|
|
286
|
+
if (isLaunchGateSaturated()) {
|
|
287
|
+
task.lastMessage = `waiting for pi-subagent launch slot (${resolveMaxConcurrentLaunches()} max)`;
|
|
288
|
+
await writeRunRecord(cwd, run).catch(() => undefined);
|
|
289
|
+
}
|
|
290
|
+
launched = await runWithLaunchSlot(() => api.runSubagent(subagentOptions));
|
|
130
291
|
}
|
|
131
292
|
catch (error) {
|
|
132
293
|
task.status = "pending";
|
|
@@ -148,6 +309,13 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
|
|
|
148
309
|
task.statusDetail = "running";
|
|
149
310
|
task.lastMessage = "launched via pi-subagent/headless";
|
|
150
311
|
await writeRunRecord(cwd, run).catch(() => undefined);
|
|
312
|
+
await recordParentSubagentChildEvent({
|
|
313
|
+
event: "started",
|
|
314
|
+
childRunId: launched.runId,
|
|
315
|
+
run,
|
|
316
|
+
task,
|
|
317
|
+
message: task.lastMessage,
|
|
318
|
+
});
|
|
151
319
|
return { kind: "launched" };
|
|
152
320
|
}
|
|
153
321
|
export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
@@ -174,8 +342,13 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
|
174
342
|
}
|
|
175
343
|
}
|
|
176
344
|
if (!handle) {
|
|
345
|
+
if (isStaleLaunchClaim(task)) {
|
|
346
|
+
resetStaleLaunchClaim(task);
|
|
347
|
+
changed = true;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
177
350
|
if (isTaskTimedOut(task)) {
|
|
178
|
-
|
|
351
|
+
markSubagentTaskTimedOut(task);
|
|
179
352
|
changed = true;
|
|
180
353
|
}
|
|
181
354
|
continue;
|
|
@@ -198,16 +371,8 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
|
198
371
|
.catch(() => null);
|
|
199
372
|
if (snapshot === null) {
|
|
200
373
|
if (isTaskTimedOut(task)) {
|
|
201
|
-
await api
|
|
202
|
-
|
|
203
|
-
cwd: handle.cwd,
|
|
204
|
-
runsDir: handle.runsDir,
|
|
205
|
-
runId: handle.runId,
|
|
206
|
-
attemptId: handle.attemptId,
|
|
207
|
-
reason: "workflow timeout",
|
|
208
|
-
})
|
|
209
|
-
.catch(() => undefined);
|
|
210
|
-
markTaskTimedOut(task);
|
|
374
|
+
await interruptTimedOutSubagent(api, handle);
|
|
375
|
+
markSubagentTaskTimedOut(task);
|
|
211
376
|
changed = true;
|
|
212
377
|
}
|
|
213
378
|
continue;
|
|
@@ -220,16 +385,8 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
|
220
385
|
? `pi-subagent heartbeat ${activeAttempt.heartbeatAt}`
|
|
221
386
|
: "pi-subagent running";
|
|
222
387
|
if (isTaskTimedOut(task)) {
|
|
223
|
-
await api
|
|
224
|
-
|
|
225
|
-
cwd: handle.cwd,
|
|
226
|
-
runsDir: handle.runsDir,
|
|
227
|
-
runId: handle.runId,
|
|
228
|
-
attemptId: handle.attemptId,
|
|
229
|
-
reason: "workflow timeout",
|
|
230
|
-
})
|
|
231
|
-
.catch(() => undefined);
|
|
232
|
-
markTaskTimedOut(task);
|
|
388
|
+
await interruptTimedOutSubagent(api, handle);
|
|
389
|
+
markSubagentTaskTimedOut(task);
|
|
233
390
|
changed = true;
|
|
234
391
|
}
|
|
235
392
|
continue;
|
|
@@ -241,6 +398,40 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
|
241
398
|
await writeRunRecord(cwd, run);
|
|
242
399
|
return run;
|
|
243
400
|
}
|
|
401
|
+
async function interruptTimedOutSubagent(api, handle) {
|
|
402
|
+
await api
|
|
403
|
+
.interruptSubagent({
|
|
404
|
+
cwd: handle.cwd,
|
|
405
|
+
runsDir: handle.runsDir,
|
|
406
|
+
runId: handle.runId,
|
|
407
|
+
attemptId: handle.attemptId,
|
|
408
|
+
reason: "workflow timeout",
|
|
409
|
+
})
|
|
410
|
+
.catch(() => undefined);
|
|
411
|
+
}
|
|
412
|
+
function markSubagentTaskTimedOut(task) {
|
|
413
|
+
markTaskTimedOut(task);
|
|
414
|
+
task.backendHandle = undefined;
|
|
415
|
+
task.backendTaskId = task.taskId;
|
|
416
|
+
task.pid = undefined;
|
|
417
|
+
}
|
|
418
|
+
function isStaleLaunchClaim(task) {
|
|
419
|
+
if (task.statusDetail !== "launching" || !task.startedAt)
|
|
420
|
+
return false;
|
|
421
|
+
const startedAtMs = Date.parse(task.startedAt);
|
|
422
|
+
return (Number.isFinite(startedAtMs) &&
|
|
423
|
+
Date.now() - startedAtMs > STALE_LAUNCH_CLAIM_GRACE_MS);
|
|
424
|
+
}
|
|
425
|
+
function resetStaleLaunchClaim(task) {
|
|
426
|
+
task.status = "pending";
|
|
427
|
+
task.statusDetail = "pending";
|
|
428
|
+
task.startedAt = undefined;
|
|
429
|
+
task.backendHandle = undefined;
|
|
430
|
+
task.backendFiles = undefined;
|
|
431
|
+
task.backendTaskId = task.taskId;
|
|
432
|
+
task.pid = undefined;
|
|
433
|
+
task.lastMessage = "stale pi-subagent launch claim reset";
|
|
434
|
+
}
|
|
244
435
|
async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
245
436
|
const outputRef = findLog(snapshot, "output");
|
|
246
437
|
const stderrRef = findLog(snapshot, "stderr");
|
|
@@ -259,8 +450,23 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
259
450
|
: undefined;
|
|
260
451
|
const toolCalls = await readToolCallsSummary(snapshot, subagentResult, artifactRoot);
|
|
261
452
|
const outputText = await readFile(outputFile, "utf8").catch(() => "");
|
|
453
|
+
const stderrText = await readFile(stderrFile, "utf8").catch(() => "");
|
|
262
454
|
const outputBytes = Buffer.byteLength(outputText, "utf8");
|
|
263
|
-
|
|
455
|
+
let statusInfo = workflowStatusFromSubagent(snapshot, subagentResult, outputBytes);
|
|
456
|
+
const deterministicBootFailure = classifyDeterministicBootFailure({
|
|
457
|
+
statusInfo,
|
|
458
|
+
stderrText,
|
|
459
|
+
outputBytes,
|
|
460
|
+
contextLengthExceeded: Boolean(subagentResult?.metadata?.contextLengthExceeded ??
|
|
461
|
+
snapshot.metadata?.contextLengthExceeded),
|
|
462
|
+
});
|
|
463
|
+
if (deterministicBootFailure) {
|
|
464
|
+
statusInfo = {
|
|
465
|
+
status: "failed",
|
|
466
|
+
failureKind: "deterministic_boot",
|
|
467
|
+
errorMessage: deterministicBootFailure,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
264
470
|
const completedAt = typeof subagentResult?.completedAt === "string"
|
|
265
471
|
? subagentResult.completedAt
|
|
266
472
|
: (snapshot.completedAt ?? nowIso());
|
|
@@ -279,7 +485,7 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
279
485
|
const contextLengthExceeded = Boolean(subagentResult?.metadata?.contextLengthExceeded ??
|
|
280
486
|
snapshot.metadata?.contextLengthExceeded);
|
|
281
487
|
if (task.artifactGraph?.enabled && statusInfo.status === "completed") {
|
|
282
|
-
|
|
488
|
+
const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
|
|
283
489
|
outputFile,
|
|
284
490
|
stderrFile,
|
|
285
491
|
resultFile,
|
|
@@ -288,6 +494,8 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
288
494
|
exitCode,
|
|
289
495
|
subagentResult,
|
|
290
496
|
});
|
|
497
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
498
|
+
return changed;
|
|
291
499
|
}
|
|
292
500
|
if (shouldAttemptArtifactGraphSalvage({
|
|
293
501
|
task,
|
|
@@ -299,7 +507,7 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
299
507
|
subagentResult,
|
|
300
508
|
snapshot,
|
|
301
509
|
})) {
|
|
302
|
-
|
|
510
|
+
const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
|
|
303
511
|
outputFile,
|
|
304
512
|
stderrFile,
|
|
305
513
|
resultFile,
|
|
@@ -313,6 +521,8 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
313
521
|
subagentFailureKind: snapshot.failureKind,
|
|
314
522
|
},
|
|
315
523
|
});
|
|
524
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
525
|
+
return changed;
|
|
316
526
|
}
|
|
317
527
|
const workflowResult = {
|
|
318
528
|
status: statusInfo.status,
|
|
@@ -340,10 +550,12 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
340
550
|
};
|
|
341
551
|
if (shouldRetryTransientModelFailure(statusInfo, workflowResult, outputBytes)) {
|
|
342
552
|
await writeJson(transientFailureAttemptPath(resultFile, (task.launchRetry?.attempts ?? 0) + 1), workflowResult);
|
|
343
|
-
|
|
553
|
+
const changed = retryOrFailTransientSubagentFailure(task, {
|
|
344
554
|
reason: statusInfo.failureKind ?? "model",
|
|
345
555
|
message: errorMessage ?? "pi-subagent run failed before producing output",
|
|
346
556
|
});
|
|
557
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
558
|
+
return changed;
|
|
347
559
|
}
|
|
348
560
|
await writeJson(resultFile, workflowResult);
|
|
349
561
|
const completedAfterTimeout = resultCompletedAfterTimeout(task, completedAt);
|
|
@@ -357,6 +569,7 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
357
569
|
delete task.backendHandle;
|
|
358
570
|
delete task.backendFiles;
|
|
359
571
|
}
|
|
572
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
360
573
|
return changed;
|
|
361
574
|
}
|
|
362
575
|
function artifactGraphRetrySession(run, task, subagentResult, attempt) {
|
|
@@ -685,6 +898,23 @@ function failArtifactGraphTask(task, options) {
|
|
|
685
898
|
task.lastMessage = options.message;
|
|
686
899
|
return true;
|
|
687
900
|
}
|
|
901
|
+
function classifyDeterministicBootFailure(options) {
|
|
902
|
+
if (options.statusInfo.status !== "failed" ||
|
|
903
|
+
options.statusInfo.failureKind !== "model" ||
|
|
904
|
+
options.outputBytes !== 0 ||
|
|
905
|
+
options.contextLengthExceeded) {
|
|
906
|
+
return undefined;
|
|
907
|
+
}
|
|
908
|
+
const text = options.stderrText;
|
|
909
|
+
const deterministicPattern = /(Failed to load extension|Cannot find module|(?:failed to load|invalid|missing) (?:workflow )?config(?:uration)?|config(?:uration)? (?:error|failed|invalid))/i;
|
|
910
|
+
if (!deterministicPattern.test(text))
|
|
911
|
+
return undefined;
|
|
912
|
+
const excerpt = text
|
|
913
|
+
.split(/\r?\n/)
|
|
914
|
+
.map((line) => line.trim())
|
|
915
|
+
.find((line) => deterministicPattern.test(line)) ?? text.trim();
|
|
916
|
+
return `deterministic-boot failure: ${excerpt.slice(0, 500)}`;
|
|
917
|
+
}
|
|
688
918
|
function shouldRetryTransientModelFailure(statusInfo, workflowResult, outputBytes) {
|
|
689
919
|
return (statusInfo.status === "failed" &&
|
|
690
920
|
statusInfo.failureKind === "model" &&
|
|
@@ -714,14 +944,14 @@ function retryOrFailTransientSubagentFailure(task, options) {
|
|
|
714
944
|
if (!exhausted) {
|
|
715
945
|
task.status = "pending";
|
|
716
946
|
task.statusDetail = "retry_model_failure";
|
|
717
|
-
task.lastMessage = `${options.message}; retrying transient
|
|
947
|
+
task.lastMessage = `${options.message}; retrying transient-model failure (${attempt}/${maxAttempts})`;
|
|
718
948
|
return true;
|
|
719
949
|
}
|
|
720
950
|
task.status = "failed";
|
|
721
951
|
task.statusDetail = task.launchRetry.reason ?? "model_exhausted";
|
|
722
952
|
task.exitCode = 1;
|
|
723
953
|
task.completedAt = nowIso();
|
|
724
|
-
task.lastMessage = `${options.message}; transient
|
|
954
|
+
task.lastMessage = `${options.message}; transient-model failure retries exhausted (${maxAttempts})`;
|
|
725
955
|
return true;
|
|
726
956
|
}
|
|
727
957
|
function retryOrFailArtifactGraphTask(task, options) {
|
|
@@ -1021,6 +1251,7 @@ async function recoverSubagentHandle(run, task) {
|
|
|
1021
1251
|
const runsDir = subagentRunsDir(run, task);
|
|
1022
1252
|
const absoluteRunsDir = resolve(task.cwd, runsDir);
|
|
1023
1253
|
const expectedCorrelationId = `${run.runId}:${task.taskId}`;
|
|
1254
|
+
const claimStartedAtMs = timestampMs(task.startedAt);
|
|
1024
1255
|
const entries = await readdir(absoluteRunsDir, { withFileTypes: true }).catch(() => []);
|
|
1025
1256
|
const candidates = [];
|
|
1026
1257
|
for (const entry of entries) {
|
|
@@ -1029,6 +1260,8 @@ async function recoverSubagentHandle(run, task) {
|
|
|
1029
1260
|
const record = await readJsonLoose(join(absoluteRunsDir, entry.name, "run.json"));
|
|
1030
1261
|
if (!record || record.correlationId !== expectedCorrelationId)
|
|
1031
1262
|
continue;
|
|
1263
|
+
if (isPreClaimSubagentRecord(record, claimStartedAtMs))
|
|
1264
|
+
continue;
|
|
1032
1265
|
const attemptId = record.activeAttemptId ??
|
|
1033
1266
|
record.latestAttemptId ??
|
|
1034
1267
|
record.attempts?.at(-1)?.attemptId;
|
|
@@ -1046,6 +1279,14 @@ async function recoverSubagentHandle(run, task) {
|
|
|
1046
1279
|
candidates.sort((left, right) => right.updatedAtMs - left.updatedAtMs);
|
|
1047
1280
|
return candidates[0]?.handle;
|
|
1048
1281
|
}
|
|
1282
|
+
function isPreClaimSubagentRecord(record, claimStartedAtMs) {
|
|
1283
|
+
if (claimStartedAtMs === undefined)
|
|
1284
|
+
return false;
|
|
1285
|
+
const recordStartedAtMs = timestampMs(record.startedAt) ??
|
|
1286
|
+
timestampMs(record.attempts?.[0]?.startedAt) ??
|
|
1287
|
+
timestampMs(record.updatedAt);
|
|
1288
|
+
return (recordStartedAtMs !== undefined && recordStartedAtMs < claimStartedAtMs);
|
|
1289
|
+
}
|
|
1049
1290
|
function timestampMs(value) {
|
|
1050
1291
|
if (value === undefined)
|
|
1051
1292
|
return undefined;
|
|
@@ -1089,7 +1330,16 @@ function subagentRunsDir(run, task) {
|
|
|
1089
1330
|
function subagentSessionId(run, task) {
|
|
1090
1331
|
if (!task.artifactGraph?.enabled)
|
|
1091
1332
|
return undefined;
|
|
1092
|
-
|
|
1333
|
+
const baseSessionId = baseSubagentSessionId(run, task);
|
|
1334
|
+
if (task.outputRetry?.sessionId)
|
|
1335
|
+
return task.outputRetry.sessionId;
|
|
1336
|
+
const launchAttempt = task.launchRetry?.attempts ?? 0;
|
|
1337
|
+
if (launchAttempt > 0)
|
|
1338
|
+
return `${baseSessionId}:launch-retry-${launchAttempt}`;
|
|
1339
|
+
const resumeAttempt = task.resumeEvents?.length ?? 0;
|
|
1340
|
+
if (resumeAttempt > 0)
|
|
1341
|
+
return `${baseSessionId}:resume-${resumeAttempt}`;
|
|
1342
|
+
return baseSessionId;
|
|
1093
1343
|
}
|
|
1094
1344
|
function baseSubagentSessionId(run, task) {
|
|
1095
1345
|
return `pi-workflow.${run.runId}.${task.taskId}`.replace(/[^A-Za-z0-9._-]/g, "-");
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WorkflowRuntimeThinkingResolution } from "./workflow-runtime.js";
|
|
1
|
+
import type { WorkflowModelInfo, WorkflowRuntimeDefaults, WorkflowRuntimeThinkingResolution } from "./workflow-runtime.js";
|
|
2
2
|
export declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
3
3
|
export declare const FAST_MODES: readonly ["inherit", "off"];
|
|
4
4
|
export declare const APPROVAL_MODES: readonly ["non-interactive", "on-request"];
|
|
@@ -420,6 +420,8 @@ export interface CompiledDynamicWorkflowTask {
|
|
|
420
420
|
helpers: Record<string, CompiledDynamicWorkflowHelper>;
|
|
421
421
|
workflows: Record<string, CompiledDynamicNestedWorkflow>;
|
|
422
422
|
decisionLoop?: CompiledDynamicDecisionLoop;
|
|
423
|
+
runtimeOverrides?: WorkflowRuntimeDefaults;
|
|
424
|
+
availableModels?: WorkflowModelInfo[];
|
|
423
425
|
}
|
|
424
426
|
export interface CompiledArtifactGraphTask {
|
|
425
427
|
enabled: true;
|
|
@@ -482,6 +484,9 @@ export interface CompiledTask {
|
|
|
482
484
|
branchId?: string;
|
|
483
485
|
outputProfile?: string;
|
|
484
486
|
};
|
|
487
|
+
foreachGenerated?: {
|
|
488
|
+
placeholderSpecId: string;
|
|
489
|
+
};
|
|
485
490
|
loopChild?: CompiledLoopChildTaskRef;
|
|
486
491
|
loopPlaceholder?: {
|
|
487
492
|
loopId: string;
|
|
@@ -560,6 +565,9 @@ export interface WorkflowTaskRunRecord {
|
|
|
560
565
|
branchId?: string;
|
|
561
566
|
outputProfile?: string;
|
|
562
567
|
};
|
|
568
|
+
foreachGenerated?: {
|
|
569
|
+
placeholderSpecId: string;
|
|
570
|
+
};
|
|
563
571
|
launchRetry?: {
|
|
564
572
|
attempts: number;
|
|
565
573
|
maxAttempts?: number;
|
|
@@ -35,6 +35,8 @@ export interface ResolveWorkflowRuntimeOptions {
|
|
|
35
35
|
availableModels?: WorkflowModelInfo[];
|
|
36
36
|
prompt?: WorkflowRuntimePrompt;
|
|
37
37
|
}
|
|
38
|
+
export type WorkflowRuntimeLayer = WorkflowRuntimeDefaults | undefined;
|
|
39
|
+
export declare function selectWorkflowRuntime(...layers: WorkflowRuntimeLayer[]): WorkflowRuntimeResolutionInput;
|
|
38
40
|
export declare function toWorkflowModelInfo(model: {
|
|
39
41
|
provider: string;
|
|
40
42
|
id: string;
|
package/dist/workflow-runtime.js
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
import { THINKING_LEVELS } from "./types.js";
|
|
2
|
+
export function selectWorkflowRuntime(...layers) {
|
|
3
|
+
const modelLayer = layers.find((layer) => modelOf(layer));
|
|
4
|
+
const model = modelOf(modelLayer);
|
|
5
|
+
let thinking;
|
|
6
|
+
for (const layer of layers) {
|
|
7
|
+
if (!layer)
|
|
8
|
+
continue;
|
|
9
|
+
if (layer.thinking) {
|
|
10
|
+
thinking = layer.thinking;
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
const layerModel = modelOf(layer);
|
|
14
|
+
const modelThinking = layerModel
|
|
15
|
+
? splitKnownThinkingSuffix(layerModel).thinking
|
|
16
|
+
: undefined;
|
|
17
|
+
if (modelThinking) {
|
|
18
|
+
thinking = modelThinking;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
...(model ? { model } : {}),
|
|
24
|
+
...(thinking ? { thinking } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function modelOf(layer) {
|
|
28
|
+
return typeof layer?.model === "string" && layer.model.trim()
|
|
29
|
+
? layer.model.trim()
|
|
30
|
+
: undefined;
|
|
31
|
+
}
|
|
2
32
|
export function toWorkflowModelInfo(model) {
|
|
3
33
|
return {
|
|
4
34
|
provider: model.provider,
|
|
@@ -163,9 +193,18 @@ export function readSimpleJsonPath(value, path) {
|
|
|
163
193
|
const parts = path.slice(2).split(".").filter(Boolean);
|
|
164
194
|
let current = value;
|
|
165
195
|
for (const part of parts) {
|
|
166
|
-
if (current
|
|
196
|
+
if (!canReadJsonPathPart(current, part))
|
|
167
197
|
return undefined;
|
|
168
198
|
current = current[part];
|
|
169
199
|
}
|
|
170
200
|
return current;
|
|
171
201
|
}
|
|
202
|
+
function canReadJsonPathPart(value, part) {
|
|
203
|
+
return (isSafeJsonPathPart(part) && isRecord(value) && Object.hasOwn(value, part));
|
|
204
|
+
}
|
|
205
|
+
function isSafeJsonPathPart(part) {
|
|
206
|
+
return part !== "__proto__" && part !== "prototype" && part !== "constructor";
|
|
207
|
+
}
|
|
208
|
+
function isRecord(value) {
|
|
209
|
+
return typeof value === "object" && value !== null;
|
|
210
|
+
}
|
package/dist/workflow-view.js
CHANGED
|
@@ -953,12 +953,14 @@ function statusForSummary(summary) {
|
|
|
953
953
|
return "running";
|
|
954
954
|
if (summary.blocked > 0)
|
|
955
955
|
return "blocked";
|
|
956
|
-
if (summary.failed > 0
|
|
956
|
+
if (summary.failed > 0)
|
|
957
957
|
return "failed";
|
|
958
958
|
if (summary.pending > 0)
|
|
959
959
|
return "pending";
|
|
960
960
|
if (summary.total > 0 && summary.completed === summary.total)
|
|
961
961
|
return "completed";
|
|
962
|
+
if (summary.interrupted > 0)
|
|
963
|
+
return "interrupted";
|
|
962
964
|
return "interrupted";
|
|
963
965
|
}
|
|
964
966
|
function taskElapsed(task) {
|