@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.
Files changed (79) hide show
  1. package/README.md +2 -0
  2. package/dist/compiler.d.ts +4 -6
  3. package/dist/compiler.js +70 -39
  4. package/dist/dynamic-decision.d.ts +0 -1
  5. package/dist/dynamic-decision.js +0 -7
  6. package/dist/dynamic-generated-task-runtime.d.ts +2 -0
  7. package/dist/dynamic-generated-task-runtime.js +21 -8
  8. package/dist/dynamic-profiles.d.ts +0 -1
  9. package/dist/dynamic-profiles.js +0 -3
  10. package/dist/engine-run-graph.d.ts +1 -0
  11. package/dist/engine-run-graph.js +142 -2
  12. package/dist/engine.d.ts +10 -6
  13. package/dist/engine.js +146 -77
  14. package/dist/extension.d.ts +2 -1
  15. package/dist/extension.js +38 -15
  16. package/dist/index.d.ts +3 -3
  17. package/dist/index.js +2 -1
  18. package/dist/store.d.ts +3 -1
  19. package/dist/store.js +189 -49
  20. package/dist/subagent-backend.d.ts +4 -0
  21. package/dist/subagent-backend.js +281 -31
  22. package/dist/types.d.ts +9 -1
  23. package/dist/workflow-runtime.d.ts +2 -0
  24. package/dist/workflow-runtime.js +40 -1
  25. package/dist/workflow-view.js +3 -1
  26. package/dist/workflow-web-source-extension.js +167 -48
  27. package/dist/workflow-web-source.d.ts +2 -1
  28. package/dist/workflow-web-source.js +84 -19
  29. package/docs/usage.md +11 -0
  30. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  31. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  32. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  33. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  34. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  35. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  36. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  37. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  38. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  39. package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
  40. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  41. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  42. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  43. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  44. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  45. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  46. package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
  47. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  48. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  49. package/package.json +2 -2
  50. package/src/compiler.ts +127 -66
  51. package/src/dynamic-decision.ts +0 -11
  52. package/src/dynamic-generated-task-runtime.ts +47 -12
  53. package/src/dynamic-profiles.ts +0 -4
  54. package/src/engine-run-graph.ts +185 -2
  55. package/src/engine.ts +192 -107
  56. package/src/extension.ts +50 -17
  57. package/src/index.ts +3 -1
  58. package/src/store.ts +253 -55
  59. package/src/subagent-backend.ts +369 -32
  60. package/src/types.ts +13 -1
  61. package/src/workflow-runtime.ts +53 -2
  62. package/src/workflow-view.ts +2 -1
  63. package/src/workflow-web-source-extension.ts +621 -228
  64. package/src/workflow-web-source.ts +118 -28
  65. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
  66. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  67. package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
  68. package/workflows/deep-research/helpers/render-executive.mjs +8 -21
  69. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  70. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
  71. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
  72. package/workflows/impact-review/spec.json +3 -3
  73. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  74. package/dist/dynamic-loader.d.ts +0 -25
  75. package/dist/dynamic-loader.js +0 -13
  76. package/src/dynamic-loader.ts +0 -49
  77. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  78. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  79. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
@@ -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
- launched = await api.runSubagent(subagentOptions);
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
- markTaskTimedOut(task);
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
- .interruptSubagent({
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
- .interruptSubagent({
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
- const statusInfo = workflowStatusFromSubagent(snapshot, subagentResult, outputBytes);
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
- return await materializeTerminalArtifactGraphResult(cwd, run, task, {
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
- return await materializeTerminalArtifactGraphResult(cwd, run, task, {
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
- return retryOrFailTransientSubagentFailure(task, {
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 model failure (${attempt}/${maxAttempts})`;
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 model failure retries exhausted (${maxAttempts})`;
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
- return task.outputRetry?.sessionId ?? baseSubagentSessionId(run, task);
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;
@@ -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 === null || typeof current !== "object" || !(part in 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
+ }
@@ -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 || summary.interrupted > 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) {