@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/src/subagent-backend.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
resolve,
|
|
18
18
|
sep,
|
|
19
19
|
} from "node:path";
|
|
20
|
+
import { availableParallelism } from "node:os";
|
|
20
21
|
import { fileURLToPath } from "node:url";
|
|
21
22
|
|
|
22
23
|
import type {
|
|
@@ -55,6 +56,15 @@ const FETCH_CONTENT_CACHE_ENV = "PI_WORKFLOW_FETCH_CONTENT_CACHE";
|
|
|
55
56
|
const LEGACY_FETCH_CACHE_ENV = "PI_WORKFLOW_FETCH_CACHE";
|
|
56
57
|
const DEFAULT_TRANSIENT_MODEL_FAILURE_RETRIES = 5;
|
|
57
58
|
const DEFAULT_ARTIFACT_OUTPUT_RETRIES = 2;
|
|
59
|
+
const MAX_CONCURRENT_LAUNCHES_ENV = "PI_WORKFLOW_MAX_CONCURRENT_LAUNCHES";
|
|
60
|
+
const PARENT_SUBAGENT_CWD_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_CWD";
|
|
61
|
+
const PARENT_SUBAGENT_RUNS_DIR_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUNS_DIR";
|
|
62
|
+
const PARENT_SUBAGENT_RUN_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUN_ID";
|
|
63
|
+
const PARENT_SUBAGENT_ATTEMPT_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_ATTEMPT_ID";
|
|
64
|
+
const DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS = 3_000;
|
|
65
|
+
const STALE_LAUNCH_CLAIM_GRACE_MS = 30_000;
|
|
66
|
+
const MIN_TRANSIENT_RETRY_JITTER_MS = 1_000;
|
|
67
|
+
const MAX_TRANSIENT_RETRY_JITTER_MS = 5_000;
|
|
58
68
|
const MODULE_PATH = fileURLToPath(import.meta.url);
|
|
59
69
|
const MODULE_DIR = dirname(MODULE_PATH);
|
|
60
70
|
const BUNDLED_PI_WEB_ACCESS_EXTENSION = bundledNodeModulePath(
|
|
@@ -156,8 +166,31 @@ interface SubagentApi {
|
|
|
156
166
|
): Promise<SubagentRunStatusSnapshot | null>;
|
|
157
167
|
interruptSubagent(options: Record<string, unknown>): Promise<unknown>;
|
|
158
168
|
reconcileSubagentRun(options: Record<string, unknown>): Promise<unknown>;
|
|
169
|
+
recordSubagentChildEvent?(
|
|
170
|
+
options: Record<string, unknown>,
|
|
171
|
+
): Promise<unknown>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
type ParentSubagentChildEvent =
|
|
175
|
+
| "started"
|
|
176
|
+
| "completed"
|
|
177
|
+
| "failed"
|
|
178
|
+
| "cancelled";
|
|
179
|
+
|
|
180
|
+
interface ParentSubagentRef {
|
|
181
|
+
cwd: string;
|
|
182
|
+
runsDir: string;
|
|
183
|
+
runId: string;
|
|
184
|
+
attemptId?: string;
|
|
159
185
|
}
|
|
160
186
|
|
|
187
|
+
const GENERIC_TASK_STATUS_DETAILS = new Set([
|
|
188
|
+
"completed",
|
|
189
|
+
"failed",
|
|
190
|
+
"interrupted",
|
|
191
|
+
"running",
|
|
192
|
+
]);
|
|
193
|
+
|
|
161
194
|
const subagentApiSpecifier = "@agwab/pi-subagent/api";
|
|
162
195
|
let cachedSubagentApi: Promise<SubagentApi> | undefined;
|
|
163
196
|
let injectedSubagentApi: SubagentApi | undefined;
|
|
@@ -175,12 +208,186 @@ async function loadSubagentApi(): Promise<SubagentApi> {
|
|
|
175
208
|
return cachedSubagentApi;
|
|
176
209
|
}
|
|
177
210
|
|
|
211
|
+
function nonEmptyEnv(
|
|
212
|
+
env: Record<string, string | undefined>,
|
|
213
|
+
key: string,
|
|
214
|
+
): string | undefined {
|
|
215
|
+
const value = env[key]?.trim();
|
|
216
|
+
return value ? value : undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parentSubagentRefFromEnv(
|
|
220
|
+
env: Record<string, string | undefined> = process.env,
|
|
221
|
+
): ParentSubagentRef | undefined {
|
|
222
|
+
const cwd = nonEmptyEnv(env, PARENT_SUBAGENT_CWD_ENV);
|
|
223
|
+
const runsDir = nonEmptyEnv(env, PARENT_SUBAGENT_RUNS_DIR_ENV);
|
|
224
|
+
const runId = nonEmptyEnv(env, PARENT_SUBAGENT_RUN_ID_ENV);
|
|
225
|
+
if (!cwd || !runsDir || !runId) return undefined;
|
|
226
|
+
const attemptId = nonEmptyEnv(env, PARENT_SUBAGENT_ATTEMPT_ID_ENV);
|
|
227
|
+
return { cwd, runsDir, runId, ...(attemptId ? { attemptId } : {}) };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function terminalChildEventForTaskStatus(
|
|
231
|
+
status: WorkflowTaskRunRecord["status"],
|
|
232
|
+
): ParentSubagentChildEvent | undefined {
|
|
233
|
+
if (status === "completed") return "completed";
|
|
234
|
+
if (status === "failed") return "failed";
|
|
235
|
+
if (status === "interrupted") return "cancelled";
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function recordParentSubagentChildEvent(options: {
|
|
240
|
+
event: ParentSubagentChildEvent;
|
|
241
|
+
childRunId: string;
|
|
242
|
+
run: WorkflowRunRecord;
|
|
243
|
+
task: WorkflowTaskRunRecord;
|
|
244
|
+
failureKind?: string | null;
|
|
245
|
+
message?: string;
|
|
246
|
+
}): Promise<void> {
|
|
247
|
+
const parent = parentSubagentRefFromEnv();
|
|
248
|
+
if (!parent) return;
|
|
249
|
+
const api = await loadSubagentApi().catch(() => undefined);
|
|
250
|
+
if (!api?.recordSubagentChildEvent) return;
|
|
251
|
+
await api
|
|
252
|
+
.recordSubagentChildEvent({
|
|
253
|
+
...parent,
|
|
254
|
+
event: options.event,
|
|
255
|
+
childRunId: options.childRunId,
|
|
256
|
+
workflowRunId: options.run.runId,
|
|
257
|
+
childTaskId: options.task.taskId,
|
|
258
|
+
...(options.failureKind === undefined
|
|
259
|
+
? {}
|
|
260
|
+
: { failureKind: options.failureKind }),
|
|
261
|
+
...(options.message === undefined ? {} : { message: options.message }),
|
|
262
|
+
})
|
|
263
|
+
.catch(() => undefined);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function recordTerminalParentSubagentChildEvent(
|
|
267
|
+
run: WorkflowRunRecord,
|
|
268
|
+
task: WorkflowTaskRunRecord,
|
|
269
|
+
snapshot: SubagentRunStatusSnapshot,
|
|
270
|
+
): Promise<void> {
|
|
271
|
+
const event = terminalChildEventForTaskStatus(task.status);
|
|
272
|
+
if (!event) return;
|
|
273
|
+
const taskFailureKind =
|
|
274
|
+
task.statusDetail && !GENERIC_TASK_STATUS_DETAILS.has(task.statusDetail)
|
|
275
|
+
? task.statusDetail
|
|
276
|
+
: undefined;
|
|
277
|
+
await recordParentSubagentChildEvent({
|
|
278
|
+
event,
|
|
279
|
+
childRunId: snapshot.runId,
|
|
280
|
+
run,
|
|
281
|
+
task,
|
|
282
|
+
failureKind:
|
|
283
|
+
event === "completed"
|
|
284
|
+
? undefined
|
|
285
|
+
: (snapshot.failureKind ?? taskFailureKind ?? task.statusDetail),
|
|
286
|
+
message: task.lastMessage,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let launchSlotReleaseDelayMs = DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
|
|
291
|
+
let transientRetryJitterForTests: (() => number) | undefined;
|
|
292
|
+
const launchWaitQueue: Array<() => void> = [];
|
|
293
|
+
let activeLaunchSlots = 0;
|
|
294
|
+
|
|
295
|
+
function resolveMaxConcurrentLaunches(): number {
|
|
296
|
+
const override = Number.parseInt(
|
|
297
|
+
process.env[MAX_CONCURRENT_LAUNCHES_ENV] ?? "",
|
|
298
|
+
10,
|
|
299
|
+
);
|
|
300
|
+
if (Number.isFinite(override)) return Math.max(1, Math.floor(override));
|
|
301
|
+
return Math.max(2, Math.floor(availableParallelism() / 2));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function isLaunchGateSaturated(): boolean {
|
|
305
|
+
return activeLaunchSlots >= resolveMaxConcurrentLaunches();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function acquireLaunchSlot(): Promise<() => void> {
|
|
309
|
+
if (!isLaunchGateSaturated()) {
|
|
310
|
+
activeLaunchSlots += 1;
|
|
311
|
+
return releaseLaunchSlot;
|
|
312
|
+
}
|
|
313
|
+
await new Promise<void>((resolveWait) => launchWaitQueue.push(resolveWait));
|
|
314
|
+
return releaseLaunchSlot;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function releaseLaunchSlot(): void {
|
|
318
|
+
const next = launchWaitQueue.shift();
|
|
319
|
+
if (next) {
|
|
320
|
+
// Transfer the occupied slot directly to the queued launcher.
|
|
321
|
+
next();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
activeLaunchSlots = Math.max(0, activeLaunchSlots - 1);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function releaseLaunchSlotAfterDelay(
|
|
328
|
+
delayMs: number,
|
|
329
|
+
release: () => void,
|
|
330
|
+
): void {
|
|
331
|
+
if (delayMs <= 0) {
|
|
332
|
+
release();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
setTimeout(release, delayMs);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function runWithLaunchSlot<T>(action: () => Promise<T>): Promise<T> {
|
|
339
|
+
const release = await acquireLaunchSlot();
|
|
340
|
+
let holdAfterReturn = false;
|
|
341
|
+
try {
|
|
342
|
+
const result = await action();
|
|
343
|
+
holdAfterReturn = true;
|
|
344
|
+
return result;
|
|
345
|
+
} finally {
|
|
346
|
+
releaseLaunchSlotAfterDelay(
|
|
347
|
+
holdAfterReturn ? launchSlotReleaseDelayMs : 0,
|
|
348
|
+
release,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function transientRetryJitterMs(): number {
|
|
354
|
+
if (transientRetryJitterForTests) return transientRetryJitterForTests();
|
|
355
|
+
return (
|
|
356
|
+
MIN_TRANSIENT_RETRY_JITTER_MS +
|
|
357
|
+
Math.floor(
|
|
358
|
+
Math.random() *
|
|
359
|
+
(MAX_TRANSIENT_RETRY_JITTER_MS - MIN_TRANSIENT_RETRY_JITTER_MS + 1),
|
|
360
|
+
)
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function sleep(ms: number): Promise<void> {
|
|
365
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function setSubagentLaunchControlsForTests(options?: {
|
|
369
|
+
releaseDelayMs?: number;
|
|
370
|
+
retryJitterMs?: number | (() => number);
|
|
371
|
+
}): void {
|
|
372
|
+
launchSlotReleaseDelayMs =
|
|
373
|
+
options?.releaseDelayMs === undefined
|
|
374
|
+
? DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS
|
|
375
|
+
: Math.max(0, Math.floor(options.releaseDelayMs));
|
|
376
|
+
transientRetryJitterForTests =
|
|
377
|
+
options?.retryJitterMs === undefined
|
|
378
|
+
? undefined
|
|
379
|
+
: typeof options.retryJitterMs === "function"
|
|
380
|
+
? options.retryJitterMs
|
|
381
|
+
: () => Math.max(0, Math.floor(options.retryJitterMs as number));
|
|
382
|
+
activeLaunchSlots = 0;
|
|
383
|
+
while (launchWaitQueue.length > 0) launchWaitQueue.shift()?.();
|
|
384
|
+
}
|
|
385
|
+
|
|
178
386
|
export async function cleanupSubagentRun(
|
|
179
387
|
_cwd: string,
|
|
180
388
|
run: WorkflowRunRecord,
|
|
181
389
|
): Promise<void> {
|
|
182
390
|
for (const task of run.tasks) {
|
|
183
|
-
if (isTerminalTaskStatus(task.status)) continue;
|
|
184
391
|
const handle = getSubagentHandle(task);
|
|
185
392
|
if (!handle) continue;
|
|
186
393
|
const api = await loadSubagentApi();
|
|
@@ -212,6 +419,14 @@ export async function launchSubagentTask(
|
|
|
212
419
|
};
|
|
213
420
|
}
|
|
214
421
|
|
|
422
|
+
if ((task.launchRetry?.attempts ?? 0) > 0) {
|
|
423
|
+
const jitterMs = transientRetryJitterMs();
|
|
424
|
+
task.statusDetail = "retry_model_failure";
|
|
425
|
+
task.lastMessage = `waiting ${jitterMs}ms before retrying transient-model launch`;
|
|
426
|
+
await writeRunRecord(cwd, run);
|
|
427
|
+
if (jitterMs > 0) await sleep(jitterMs);
|
|
428
|
+
}
|
|
429
|
+
|
|
215
430
|
const systemPromptFile = fromProjectPath(cwd, task.files.systemPrompt);
|
|
216
431
|
const taskPromptFile = fromProjectPath(cwd, task.files.taskPrompt);
|
|
217
432
|
const outputFile = fromProjectPath(cwd, task.files.output);
|
|
@@ -267,7 +482,11 @@ export async function launchSubagentTask(
|
|
|
267
482
|
};
|
|
268
483
|
subagentOptions.extensions = extensions;
|
|
269
484
|
if (captureToolCallsEnabled()) subagentOptions.captureToolCalls = true;
|
|
270
|
-
|
|
485
|
+
if (isLaunchGateSaturated()) {
|
|
486
|
+
task.lastMessage = `waiting for pi-subagent launch slot (${resolveMaxConcurrentLaunches()} max)`;
|
|
487
|
+
await writeRunRecord(cwd, run).catch(() => undefined);
|
|
488
|
+
}
|
|
489
|
+
launched = await runWithLaunchSlot(() => api.runSubagent(subagentOptions));
|
|
271
490
|
} catch (error) {
|
|
272
491
|
task.status = "pending";
|
|
273
492
|
task.statusDetail = "pending";
|
|
@@ -295,6 +514,13 @@ export async function launchSubagentTask(
|
|
|
295
514
|
task.statusDetail = "running";
|
|
296
515
|
task.lastMessage = "launched via pi-subagent/headless";
|
|
297
516
|
await writeRunRecord(cwd, run).catch(() => undefined);
|
|
517
|
+
await recordParentSubagentChildEvent({
|
|
518
|
+
event: "started",
|
|
519
|
+
childRunId: launched.runId,
|
|
520
|
+
run,
|
|
521
|
+
task,
|
|
522
|
+
message: task.lastMessage,
|
|
523
|
+
});
|
|
298
524
|
return { kind: "launched" };
|
|
299
525
|
}
|
|
300
526
|
|
|
@@ -326,8 +552,13 @@ export async function refreshRunFromSubagentArtifacts(
|
|
|
326
552
|
}
|
|
327
553
|
}
|
|
328
554
|
if (!handle) {
|
|
555
|
+
if (isStaleLaunchClaim(task)) {
|
|
556
|
+
resetStaleLaunchClaim(task);
|
|
557
|
+
changed = true;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
329
560
|
if (isTaskTimedOut(task)) {
|
|
330
|
-
|
|
561
|
+
markSubagentTaskTimedOut(task);
|
|
331
562
|
changed = true;
|
|
332
563
|
}
|
|
333
564
|
continue;
|
|
@@ -352,16 +583,8 @@ export async function refreshRunFromSubagentArtifacts(
|
|
|
352
583
|
|
|
353
584
|
if (snapshot === null) {
|
|
354
585
|
if (isTaskTimedOut(task)) {
|
|
355
|
-
await api
|
|
356
|
-
|
|
357
|
-
cwd: handle.cwd,
|
|
358
|
-
runsDir: handle.runsDir,
|
|
359
|
-
runId: handle.runId,
|
|
360
|
-
attemptId: handle.attemptId,
|
|
361
|
-
reason: "workflow timeout",
|
|
362
|
-
})
|
|
363
|
-
.catch(() => undefined);
|
|
364
|
-
markTaskTimedOut(task);
|
|
586
|
+
await interruptTimedOutSubagent(api, handle);
|
|
587
|
+
markSubagentTaskTimedOut(task);
|
|
365
588
|
changed = true;
|
|
366
589
|
}
|
|
367
590
|
continue;
|
|
@@ -378,16 +601,8 @@ export async function refreshRunFromSubagentArtifacts(
|
|
|
378
601
|
? `pi-subagent heartbeat ${activeAttempt.heartbeatAt}`
|
|
379
602
|
: "pi-subagent running";
|
|
380
603
|
if (isTaskTimedOut(task)) {
|
|
381
|
-
await api
|
|
382
|
-
|
|
383
|
-
cwd: handle.cwd,
|
|
384
|
-
runsDir: handle.runsDir,
|
|
385
|
-
runId: handle.runId,
|
|
386
|
-
attemptId: handle.attemptId,
|
|
387
|
-
reason: "workflow timeout",
|
|
388
|
-
})
|
|
389
|
-
.catch(() => undefined);
|
|
390
|
-
markTaskTimedOut(task);
|
|
604
|
+
await interruptTimedOutSubagent(api, handle);
|
|
605
|
+
markSubagentTaskTimedOut(task);
|
|
391
606
|
changed = true;
|
|
392
607
|
}
|
|
393
608
|
continue;
|
|
@@ -401,6 +616,48 @@ export async function refreshRunFromSubagentArtifacts(
|
|
|
401
616
|
return run;
|
|
402
617
|
}
|
|
403
618
|
|
|
619
|
+
async function interruptTimedOutSubagent(
|
|
620
|
+
api: Awaited<ReturnType<typeof loadSubagentApi>>,
|
|
621
|
+
handle: NonNullable<WorkflowTaskRunRecord["backendHandle"]>,
|
|
622
|
+
): Promise<void> {
|
|
623
|
+
await api
|
|
624
|
+
.interruptSubagent({
|
|
625
|
+
cwd: handle.cwd,
|
|
626
|
+
runsDir: handle.runsDir,
|
|
627
|
+
runId: handle.runId,
|
|
628
|
+
attemptId: handle.attemptId,
|
|
629
|
+
reason: "workflow timeout",
|
|
630
|
+
})
|
|
631
|
+
.catch(() => undefined);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function markSubagentTaskTimedOut(task: WorkflowTaskRunRecord): void {
|
|
635
|
+
markTaskTimedOut(task);
|
|
636
|
+
task.backendHandle = undefined;
|
|
637
|
+
task.backendTaskId = task.taskId;
|
|
638
|
+
task.pid = undefined;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function isStaleLaunchClaim(task: WorkflowTaskRunRecord): boolean {
|
|
642
|
+
if (task.statusDetail !== "launching" || !task.startedAt) return false;
|
|
643
|
+
const startedAtMs = Date.parse(task.startedAt);
|
|
644
|
+
return (
|
|
645
|
+
Number.isFinite(startedAtMs) &&
|
|
646
|
+
Date.now() - startedAtMs > STALE_LAUNCH_CLAIM_GRACE_MS
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function resetStaleLaunchClaim(task: WorkflowTaskRunRecord): void {
|
|
651
|
+
task.status = "pending";
|
|
652
|
+
task.statusDetail = "pending";
|
|
653
|
+
task.startedAt = undefined;
|
|
654
|
+
task.backendHandle = undefined;
|
|
655
|
+
task.backendFiles = undefined;
|
|
656
|
+
task.backendTaskId = task.taskId;
|
|
657
|
+
task.pid = undefined;
|
|
658
|
+
task.lastMessage = "stale pi-subagent launch claim reset";
|
|
659
|
+
}
|
|
660
|
+
|
|
404
661
|
async function materializeTerminalSubagentResult(
|
|
405
662
|
cwd: string,
|
|
406
663
|
run: WorkflowRunRecord,
|
|
@@ -432,12 +689,29 @@ async function materializeTerminalSubagentResult(
|
|
|
432
689
|
artifactRoot,
|
|
433
690
|
);
|
|
434
691
|
const outputText = await readFile(outputFile, "utf8").catch(() => "");
|
|
692
|
+
const stderrText = await readFile(stderrFile, "utf8").catch(() => "");
|
|
435
693
|
const outputBytes = Buffer.byteLength(outputText, "utf8");
|
|
436
|
-
|
|
694
|
+
let statusInfo = workflowStatusFromSubagent(
|
|
437
695
|
snapshot,
|
|
438
696
|
subagentResult,
|
|
439
697
|
outputBytes,
|
|
440
698
|
);
|
|
699
|
+
const deterministicBootFailure = classifyDeterministicBootFailure({
|
|
700
|
+
statusInfo,
|
|
701
|
+
stderrText,
|
|
702
|
+
outputBytes,
|
|
703
|
+
contextLengthExceeded: Boolean(
|
|
704
|
+
(subagentResult?.metadata as any)?.contextLengthExceeded ??
|
|
705
|
+
snapshot.metadata?.contextLengthExceeded,
|
|
706
|
+
),
|
|
707
|
+
});
|
|
708
|
+
if (deterministicBootFailure) {
|
|
709
|
+
statusInfo = {
|
|
710
|
+
status: "failed",
|
|
711
|
+
failureKind: "deterministic_boot",
|
|
712
|
+
errorMessage: deterministicBootFailure,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
441
715
|
const completedAt =
|
|
442
716
|
typeof subagentResult?.completedAt === "string"
|
|
443
717
|
? subagentResult.completedAt
|
|
@@ -462,7 +736,7 @@ async function materializeTerminalSubagentResult(
|
|
|
462
736
|
snapshot.metadata?.contextLengthExceeded,
|
|
463
737
|
);
|
|
464
738
|
if (task.artifactGraph?.enabled && statusInfo.status === "completed") {
|
|
465
|
-
|
|
739
|
+
const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
|
|
466
740
|
outputFile,
|
|
467
741
|
stderrFile,
|
|
468
742
|
resultFile,
|
|
@@ -471,6 +745,8 @@ async function materializeTerminalSubagentResult(
|
|
|
471
745
|
exitCode,
|
|
472
746
|
subagentResult,
|
|
473
747
|
});
|
|
748
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
749
|
+
return changed;
|
|
474
750
|
}
|
|
475
751
|
if (
|
|
476
752
|
shouldAttemptArtifactGraphSalvage({
|
|
@@ -484,7 +760,7 @@ async function materializeTerminalSubagentResult(
|
|
|
484
760
|
snapshot,
|
|
485
761
|
})
|
|
486
762
|
) {
|
|
487
|
-
|
|
763
|
+
const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
|
|
488
764
|
outputFile,
|
|
489
765
|
stderrFile,
|
|
490
766
|
resultFile,
|
|
@@ -498,6 +774,8 @@ async function materializeTerminalSubagentResult(
|
|
|
498
774
|
subagentFailureKind: snapshot.failureKind,
|
|
499
775
|
},
|
|
500
776
|
});
|
|
777
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
778
|
+
return changed;
|
|
501
779
|
}
|
|
502
780
|
const workflowResult = {
|
|
503
781
|
status: statusInfo.status,
|
|
@@ -533,10 +811,12 @@ async function materializeTerminalSubagentResult(
|
|
|
533
811
|
),
|
|
534
812
|
workflowResult,
|
|
535
813
|
);
|
|
536
|
-
|
|
814
|
+
const changed = retryOrFailTransientSubagentFailure(task, {
|
|
537
815
|
reason: statusInfo.failureKind ?? "model",
|
|
538
816
|
message: errorMessage ?? "pi-subagent run failed before producing output",
|
|
539
817
|
});
|
|
818
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
819
|
+
return changed;
|
|
540
820
|
}
|
|
541
821
|
await writeJson(resultFile, workflowResult);
|
|
542
822
|
|
|
@@ -551,6 +831,7 @@ async function materializeTerminalSubagentResult(
|
|
|
551
831
|
delete task.backendHandle;
|
|
552
832
|
delete task.backendFiles;
|
|
553
833
|
}
|
|
834
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
554
835
|
return changed;
|
|
555
836
|
}
|
|
556
837
|
|
|
@@ -1005,6 +1286,36 @@ function failArtifactGraphTask(
|
|
|
1005
1286
|
return true;
|
|
1006
1287
|
}
|
|
1007
1288
|
|
|
1289
|
+
function classifyDeterministicBootFailure(options: {
|
|
1290
|
+
statusInfo: {
|
|
1291
|
+
status: WorkflowTaskRunRecord["status"];
|
|
1292
|
+
failureKind?: string;
|
|
1293
|
+
errorMessage?: string;
|
|
1294
|
+
};
|
|
1295
|
+
stderrText: string;
|
|
1296
|
+
outputBytes: number;
|
|
1297
|
+
contextLengthExceeded: boolean;
|
|
1298
|
+
}): string | undefined {
|
|
1299
|
+
if (
|
|
1300
|
+
options.statusInfo.status !== "failed" ||
|
|
1301
|
+
options.statusInfo.failureKind !== "model" ||
|
|
1302
|
+
options.outputBytes !== 0 ||
|
|
1303
|
+
options.contextLengthExceeded
|
|
1304
|
+
) {
|
|
1305
|
+
return undefined;
|
|
1306
|
+
}
|
|
1307
|
+
const text = options.stderrText;
|
|
1308
|
+
const deterministicPattern =
|
|
1309
|
+
/(Failed to load extension|Cannot find module|(?:failed to load|invalid|missing) (?:workflow )?config(?:uration)?|config(?:uration)? (?:error|failed|invalid))/i;
|
|
1310
|
+
if (!deterministicPattern.test(text)) return undefined;
|
|
1311
|
+
const excerpt =
|
|
1312
|
+
text
|
|
1313
|
+
.split(/\r?\n/)
|
|
1314
|
+
.map((line) => line.trim())
|
|
1315
|
+
.find((line) => deterministicPattern.test(line)) ?? text.trim();
|
|
1316
|
+
return `deterministic-boot failure: ${excerpt.slice(0, 500)}`;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1008
1319
|
function shouldRetryTransientModelFailure(
|
|
1009
1320
|
statusInfo: {
|
|
1010
1321
|
status: WorkflowTaskRunRecord["status"];
|
|
@@ -1056,14 +1367,14 @@ function retryOrFailTransientSubagentFailure(
|
|
|
1056
1367
|
if (!exhausted) {
|
|
1057
1368
|
task.status = "pending";
|
|
1058
1369
|
task.statusDetail = "retry_model_failure";
|
|
1059
|
-
task.lastMessage = `${options.message}; retrying transient
|
|
1370
|
+
task.lastMessage = `${options.message}; retrying transient-model failure (${attempt}/${maxAttempts})`;
|
|
1060
1371
|
return true;
|
|
1061
1372
|
}
|
|
1062
1373
|
task.status = "failed";
|
|
1063
1374
|
task.statusDetail = task.launchRetry.reason ?? "model_exhausted";
|
|
1064
1375
|
task.exitCode = 1;
|
|
1065
1376
|
task.completedAt = nowIso();
|
|
1066
|
-
task.lastMessage = `${options.message}; transient
|
|
1377
|
+
task.lastMessage = `${options.message}; transient-model failure retries exhausted (${maxAttempts})`;
|
|
1067
1378
|
return true;
|
|
1068
1379
|
}
|
|
1069
1380
|
|
|
@@ -1317,7 +1628,10 @@ async function workflowTaskExtensions(
|
|
|
1317
1628
|
},
|
|
1318
1629
|
});
|
|
1319
1630
|
const capturedProviderExtensions = new Set(
|
|
1320
|
-
workflowWebSourceProviderExtensions(
|
|
1631
|
+
workflowWebSourceProviderExtensions(
|
|
1632
|
+
tools,
|
|
1633
|
+
compiledTask.runtime.toolProviders,
|
|
1634
|
+
),
|
|
1321
1635
|
);
|
|
1322
1636
|
extensions = uniqueStrings([
|
|
1323
1637
|
...extensions.filter(
|
|
@@ -1510,6 +1824,7 @@ async function recoverSubagentHandle(
|
|
|
1510
1824
|
const runsDir = subagentRunsDir(run, task);
|
|
1511
1825
|
const absoluteRunsDir = resolve(task.cwd, runsDir);
|
|
1512
1826
|
const expectedCorrelationId = `${run.runId}:${task.taskId}`;
|
|
1827
|
+
const claimStartedAtMs = timestampMs(task.startedAt);
|
|
1513
1828
|
const entries = await readdir(absoluteRunsDir, { withFileTypes: true }).catch(
|
|
1514
1829
|
() => [],
|
|
1515
1830
|
);
|
|
@@ -1524,6 +1839,7 @@ async function recoverSubagentHandle(
|
|
|
1524
1839
|
join(absoluteRunsDir, entry.name, "run.json"),
|
|
1525
1840
|
);
|
|
1526
1841
|
if (!record || record.correlationId !== expectedCorrelationId) continue;
|
|
1842
|
+
if (isPreClaimSubagentRecord(record, claimStartedAtMs)) continue;
|
|
1527
1843
|
const attemptId =
|
|
1528
1844
|
record.activeAttemptId ??
|
|
1529
1845
|
record.latestAttemptId ??
|
|
@@ -1550,6 +1866,20 @@ async function recoverSubagentHandle(
|
|
|
1550
1866
|
return candidates[0]?.handle;
|
|
1551
1867
|
}
|
|
1552
1868
|
|
|
1869
|
+
function isPreClaimSubagentRecord(
|
|
1870
|
+
record: SubagentRunRecordLike,
|
|
1871
|
+
claimStartedAtMs: number | undefined,
|
|
1872
|
+
): boolean {
|
|
1873
|
+
if (claimStartedAtMs === undefined) return false;
|
|
1874
|
+
const recordStartedAtMs =
|
|
1875
|
+
timestampMs(record.startedAt) ??
|
|
1876
|
+
timestampMs(record.attempts?.[0]?.startedAt) ??
|
|
1877
|
+
timestampMs(record.updatedAt);
|
|
1878
|
+
return (
|
|
1879
|
+
recordStartedAtMs !== undefined && recordStartedAtMs < claimStartedAtMs
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1553
1883
|
function timestampMs(value: string | undefined): number | undefined {
|
|
1554
1884
|
if (value === undefined) return undefined;
|
|
1555
1885
|
const time = Date.parse(value);
|
|
@@ -1610,7 +1940,14 @@ function subagentSessionId(
|
|
|
1610
1940
|
task: WorkflowTaskRunRecord,
|
|
1611
1941
|
): string | undefined {
|
|
1612
1942
|
if (!task.artifactGraph?.enabled) return undefined;
|
|
1613
|
-
|
|
1943
|
+
const baseSessionId = baseSubagentSessionId(run, task);
|
|
1944
|
+
if (task.outputRetry?.sessionId) return task.outputRetry.sessionId;
|
|
1945
|
+
const launchAttempt = task.launchRetry?.attempts ?? 0;
|
|
1946
|
+
if (launchAttempt > 0)
|
|
1947
|
+
return `${baseSessionId}:launch-retry-${launchAttempt}`;
|
|
1948
|
+
const resumeAttempt = task.resumeEvents?.length ?? 0;
|
|
1949
|
+
if (resumeAttempt > 0) return `${baseSessionId}:resume-${resumeAttempt}`;
|
|
1950
|
+
return baseSessionId;
|
|
1614
1951
|
}
|
|
1615
1952
|
|
|
1616
1953
|
function baseSubagentSessionId(
|
|
@@ -1673,7 +2010,7 @@ function buildSystemPrompt(task: CompiledTask): string {
|
|
|
1673
2010
|
enabledTools.includes("workflow_web_source_read")
|
|
1674
2011
|
? "Workflow web-source tools return compact source cards. Preserve sourceRef values in structured outputs. Use workflow_web_source_read for exact evidence snippets; when several snippets are needed from the same sourceRef, batch them with queries:[...] or reads:[...] instead of making repeated calls. If the exact quote is unknown, pass claim plus 2-6 distinctive terms to harvest a candidate source window and preserve its match metadata. Do not read workflow cache files directly."
|
|
1675
2012
|
: !enabledTools.includes("get_search_content") &&
|
|
1676
|
-
|
|
2013
|
+
(enabledTools.includes("web_search") ||
|
|
1677
2014
|
enabledTools.includes("fetch_content"))
|
|
1678
2015
|
? "Full cached search-content hydration is unavailable here. Use web_search/fetch_content results and report evidence gaps instead of broad raw document retrieval."
|
|
1679
2016
|
: undefined,
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
WorkflowModelInfo,
|
|
3
|
+
WorkflowRuntimeDefaults,
|
|
4
|
+
WorkflowRuntimeThinkingResolution,
|
|
5
|
+
} from "./workflow-runtime.js";
|
|
2
6
|
|
|
3
7
|
export const THINKING_LEVELS = [
|
|
4
8
|
"off",
|
|
@@ -472,6 +476,8 @@ export interface CompiledDynamicWorkflowTask {
|
|
|
472
476
|
helpers: Record<string, CompiledDynamicWorkflowHelper>;
|
|
473
477
|
workflows: Record<string, CompiledDynamicNestedWorkflow>;
|
|
474
478
|
decisionLoop?: CompiledDynamicDecisionLoop;
|
|
479
|
+
runtimeOverrides?: WorkflowRuntimeDefaults;
|
|
480
|
+
availableModels?: WorkflowModelInfo[];
|
|
475
481
|
}
|
|
476
482
|
|
|
477
483
|
export interface CompiledArtifactGraphTask {
|
|
@@ -536,6 +542,9 @@ export interface CompiledTask {
|
|
|
536
542
|
branchId?: string;
|
|
537
543
|
outputProfile?: string;
|
|
538
544
|
};
|
|
545
|
+
foreachGenerated?: {
|
|
546
|
+
placeholderSpecId: string;
|
|
547
|
+
};
|
|
539
548
|
loopChild?: CompiledLoopChildTaskRef;
|
|
540
549
|
loopPlaceholder?: {
|
|
541
550
|
loopId: string;
|
|
@@ -628,6 +637,9 @@ export interface WorkflowTaskRunRecord {
|
|
|
628
637
|
branchId?: string;
|
|
629
638
|
outputProfile?: string;
|
|
630
639
|
};
|
|
640
|
+
foreachGenerated?: {
|
|
641
|
+
placeholderSpecId: string;
|
|
642
|
+
};
|
|
631
643
|
launchRetry?: {
|
|
632
644
|
attempts: number;
|
|
633
645
|
maxAttempts?: number;
|
package/src/workflow-runtime.ts
CHANGED
|
@@ -46,6 +46,41 @@ export interface ResolveWorkflowRuntimeOptions {
|
|
|
46
46
|
prompt?: WorkflowRuntimePrompt;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export type WorkflowRuntimeLayer = WorkflowRuntimeDefaults | undefined;
|
|
50
|
+
|
|
51
|
+
export function selectWorkflowRuntime(
|
|
52
|
+
...layers: WorkflowRuntimeLayer[]
|
|
53
|
+
): WorkflowRuntimeResolutionInput {
|
|
54
|
+
const modelLayer = layers.find((layer) => modelOf(layer));
|
|
55
|
+
const model = modelOf(modelLayer);
|
|
56
|
+
let thinking: ThinkingLevel | undefined;
|
|
57
|
+
for (const layer of layers) {
|
|
58
|
+
if (!layer) continue;
|
|
59
|
+
if (layer.thinking) {
|
|
60
|
+
thinking = layer.thinking;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
const layerModel = modelOf(layer);
|
|
64
|
+
const modelThinking = layerModel
|
|
65
|
+
? splitKnownThinkingSuffix(layerModel).thinking
|
|
66
|
+
: undefined;
|
|
67
|
+
if (modelThinking) {
|
|
68
|
+
thinking = modelThinking;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...(model ? { model } : {}),
|
|
74
|
+
...(thinking ? { thinking } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function modelOf(layer: WorkflowRuntimeLayer): string | undefined {
|
|
79
|
+
return typeof layer?.model === "string" && layer.model.trim()
|
|
80
|
+
? layer.model.trim()
|
|
81
|
+
: undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
49
84
|
export function toWorkflowModelInfo(model: {
|
|
50
85
|
provider: string;
|
|
51
86
|
id: string;
|
|
@@ -310,9 +345,25 @@ export function readSimpleJsonPath(value: unknown, path: string): unknown {
|
|
|
310
345
|
const parts = path.slice(2).split(".").filter(Boolean);
|
|
311
346
|
let current = value as any;
|
|
312
347
|
for (const part of parts) {
|
|
313
|
-
if (current
|
|
314
|
-
return undefined;
|
|
348
|
+
if (!canReadJsonPathPart(current, part)) return undefined;
|
|
315
349
|
current = current[part];
|
|
316
350
|
}
|
|
317
351
|
return current;
|
|
318
352
|
}
|
|
353
|
+
|
|
354
|
+
function canReadJsonPathPart(
|
|
355
|
+
value: unknown,
|
|
356
|
+
part: string,
|
|
357
|
+
): value is Record<string, unknown> {
|
|
358
|
+
return (
|
|
359
|
+
isSafeJsonPathPart(part) && isRecord(value) && Object.hasOwn(value, part)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function isSafeJsonPathPart(part: string): boolean {
|
|
364
|
+
return part !== "__proto__" && part !== "prototype" && part !== "constructor";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
368
|
+
return typeof value === "object" && value !== null;
|
|
369
|
+
}
|