@agwab/pi-workflow 0.1.2 → 0.2.1

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 (41) hide show
  1. package/README.md +9 -13
  2. package/dist/compiler.d.ts +5 -5
  3. package/dist/compiler.js +82 -24
  4. package/dist/dynamic-generated-task-runtime.d.ts +2 -0
  5. package/dist/dynamic-generated-task-runtime.js +21 -8
  6. package/dist/engine.d.ts +6 -5
  7. package/dist/engine.js +39 -54
  8. package/dist/extension.js +211 -24
  9. package/dist/store.d.ts +3 -1
  10. package/dist/store.js +135 -38
  11. package/dist/subagent-backend.d.ts +4 -0
  12. package/dist/subagent-backend.js +128 -4
  13. package/dist/types.d.ts +5 -0
  14. package/dist/workflow-progress-health.d.ts +37 -0
  15. package/dist/workflow-progress-health.js +296 -0
  16. package/dist/workflow-runtime.d.ts +8 -0
  17. package/dist/workflow-runtime.js +63 -10
  18. package/dist/workflow-view.d.ts +2 -0
  19. package/dist/workflow-view.js +97 -18
  20. package/dist/workflow-web-source.js +32 -14
  21. package/docs/usage.md +12 -1
  22. package/package.json +6 -6
  23. package/src/compiler.ts +136 -41
  24. package/src/dynamic-generated-task-runtime.ts +47 -12
  25. package/src/engine.ts +55 -100
  26. package/src/extension.ts +270 -34
  27. package/src/store.ts +180 -44
  28. package/src/subagent-backend.ts +170 -6
  29. package/src/types.ts +10 -0
  30. package/src/workflow-progress-health.ts +461 -0
  31. package/src/workflow-runtime.ts +85 -13
  32. package/src/workflow-view.ts +186 -41
  33. package/src/workflow-web-source.ts +192 -69
  34. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +111 -37
  35. package/workflows/deep-research/helpers/final-audit-packet.mjs +191 -14
  36. package/workflows/deep-research/helpers/normalize-input-packet.mjs +159 -50
  37. package/workflows/deep-research/helpers/render-executive.mjs +671 -37
  38. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +624 -0
  39. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +2 -0
  40. package/workflows/deep-research/schemas/deep-research-final-synthesis-control.schema.json +110 -0
  41. package/workflows/deep-research/spec.json +41 -11
@@ -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,10 @@ 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 DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS = 3_000;
61
+ const MIN_TRANSIENT_RETRY_JITTER_MS = 1_000;
62
+ const MAX_TRANSIENT_RETRY_JITTER_MS = 5_000;
58
63
  const MODULE_PATH = fileURLToPath(import.meta.url);
59
64
  const MODULE_DIR = dirname(MODULE_PATH);
60
65
  const BUNDLED_PI_WEB_ACCESS_EXTENSION = bundledNodeModulePath(
@@ -175,6 +180,103 @@ async function loadSubagentApi(): Promise<SubagentApi> {
175
180
  return cachedSubagentApi;
176
181
  }
177
182
 
183
+ let launchSlotReleaseDelayMs = DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
184
+ let transientRetryJitterForTests: (() => number) | undefined;
185
+ const launchWaitQueue: Array<() => void> = [];
186
+ let activeLaunchSlots = 0;
187
+
188
+ function resolveMaxConcurrentLaunches(): number {
189
+ const override = Number.parseInt(
190
+ process.env[MAX_CONCURRENT_LAUNCHES_ENV] ?? "",
191
+ 10,
192
+ );
193
+ if (Number.isFinite(override)) return Math.max(1, Math.floor(override));
194
+ return Math.max(2, Math.floor(availableParallelism() / 2));
195
+ }
196
+
197
+ function isLaunchGateSaturated(): boolean {
198
+ return activeLaunchSlots >= resolveMaxConcurrentLaunches();
199
+ }
200
+
201
+ async function acquireLaunchSlot(): Promise<() => void> {
202
+ if (!isLaunchGateSaturated()) {
203
+ activeLaunchSlots += 1;
204
+ return releaseLaunchSlot;
205
+ }
206
+ await new Promise<void>((resolveWait) => launchWaitQueue.push(resolveWait));
207
+ return releaseLaunchSlot;
208
+ }
209
+
210
+ function releaseLaunchSlot(): void {
211
+ const next = launchWaitQueue.shift();
212
+ if (next) {
213
+ // Transfer the occupied slot directly to the queued launcher.
214
+ next();
215
+ return;
216
+ }
217
+ activeLaunchSlots = Math.max(0, activeLaunchSlots - 1);
218
+ }
219
+
220
+ function releaseLaunchSlotAfterDelay(
221
+ delayMs: number,
222
+ release: () => void,
223
+ ): void {
224
+ if (delayMs <= 0) {
225
+ release();
226
+ return;
227
+ }
228
+ const timer = setTimeout(release, delayMs);
229
+ timer.unref?.();
230
+ }
231
+
232
+ async function runWithLaunchSlot<T>(action: () => Promise<T>): Promise<T> {
233
+ const release = await acquireLaunchSlot();
234
+ let holdAfterReturn = false;
235
+ try {
236
+ const result = await action();
237
+ holdAfterReturn = true;
238
+ return result;
239
+ } finally {
240
+ releaseLaunchSlotAfterDelay(
241
+ holdAfterReturn ? launchSlotReleaseDelayMs : 0,
242
+ release,
243
+ );
244
+ }
245
+ }
246
+
247
+ function transientRetryJitterMs(): number {
248
+ if (transientRetryJitterForTests) return transientRetryJitterForTests();
249
+ return (
250
+ MIN_TRANSIENT_RETRY_JITTER_MS +
251
+ Math.floor(
252
+ Math.random() *
253
+ (MAX_TRANSIENT_RETRY_JITTER_MS - MIN_TRANSIENT_RETRY_JITTER_MS + 1),
254
+ )
255
+ );
256
+ }
257
+
258
+ function sleep(ms: number): Promise<void> {
259
+ return new Promise((resolve) => setTimeout(resolve, ms));
260
+ }
261
+
262
+ export function setSubagentLaunchControlsForTests(options?: {
263
+ releaseDelayMs?: number;
264
+ retryJitterMs?: number | (() => number);
265
+ }): void {
266
+ launchSlotReleaseDelayMs =
267
+ options?.releaseDelayMs === undefined
268
+ ? DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS
269
+ : Math.max(0, Math.floor(options.releaseDelayMs));
270
+ transientRetryJitterForTests =
271
+ options?.retryJitterMs === undefined
272
+ ? undefined
273
+ : typeof options.retryJitterMs === "function"
274
+ ? options.retryJitterMs
275
+ : () => Math.max(0, Math.floor(options.retryJitterMs as number));
276
+ activeLaunchSlots = 0;
277
+ while (launchWaitQueue.length > 0) launchWaitQueue.shift()?.();
278
+ }
279
+
178
280
  export async function cleanupSubagentRun(
179
281
  _cwd: string,
180
282
  run: WorkflowRunRecord,
@@ -212,6 +314,14 @@ export async function launchSubagentTask(
212
314
  };
213
315
  }
214
316
 
317
+ if ((task.launchRetry?.attempts ?? 0) > 0) {
318
+ const jitterMs = transientRetryJitterMs();
319
+ task.statusDetail = "retry_model_failure";
320
+ task.lastMessage = `waiting ${jitterMs}ms before retrying transient-model launch`;
321
+ await writeRunRecord(cwd, run);
322
+ if (jitterMs > 0) await sleep(jitterMs);
323
+ }
324
+
215
325
  const systemPromptFile = fromProjectPath(cwd, task.files.systemPrompt);
216
326
  const taskPromptFile = fromProjectPath(cwd, task.files.taskPrompt);
217
327
  const outputFile = fromProjectPath(cwd, task.files.output);
@@ -267,7 +377,11 @@ export async function launchSubagentTask(
267
377
  };
268
378
  subagentOptions.extensions = extensions;
269
379
  if (captureToolCallsEnabled()) subagentOptions.captureToolCalls = true;
270
- launched = await api.runSubagent(subagentOptions);
380
+ if (isLaunchGateSaturated()) {
381
+ task.lastMessage = `waiting for pi-subagent launch slot (${resolveMaxConcurrentLaunches()} max)`;
382
+ await writeRunRecord(cwd, run).catch(() => undefined);
383
+ }
384
+ launched = await runWithLaunchSlot(() => api.runSubagent(subagentOptions));
271
385
  } catch (error) {
272
386
  task.status = "pending";
273
387
  task.statusDetail = "pending";
@@ -432,12 +546,29 @@ async function materializeTerminalSubagentResult(
432
546
  artifactRoot,
433
547
  );
434
548
  const outputText = await readFile(outputFile, "utf8").catch(() => "");
549
+ const stderrText = await readFile(stderrFile, "utf8").catch(() => "");
435
550
  const outputBytes = Buffer.byteLength(outputText, "utf8");
436
- const statusInfo = workflowStatusFromSubagent(
551
+ let statusInfo = workflowStatusFromSubagent(
437
552
  snapshot,
438
553
  subagentResult,
439
554
  outputBytes,
440
555
  );
556
+ const deterministicBootFailure = classifyDeterministicBootFailure({
557
+ statusInfo,
558
+ stderrText,
559
+ outputBytes,
560
+ contextLengthExceeded: Boolean(
561
+ (subagentResult?.metadata as any)?.contextLengthExceeded ??
562
+ snapshot.metadata?.contextLengthExceeded,
563
+ ),
564
+ });
565
+ if (deterministicBootFailure) {
566
+ statusInfo = {
567
+ status: "failed",
568
+ failureKind: "deterministic_boot",
569
+ errorMessage: deterministicBootFailure,
570
+ };
571
+ }
441
572
  const completedAt =
442
573
  typeof subagentResult?.completedAt === "string"
443
574
  ? subagentResult.completedAt
@@ -1005,6 +1136,36 @@ function failArtifactGraphTask(
1005
1136
  return true;
1006
1137
  }
1007
1138
 
1139
+ function classifyDeterministicBootFailure(options: {
1140
+ statusInfo: {
1141
+ status: WorkflowTaskRunRecord["status"];
1142
+ failureKind?: string;
1143
+ errorMessage?: string;
1144
+ };
1145
+ stderrText: string;
1146
+ outputBytes: number;
1147
+ contextLengthExceeded: boolean;
1148
+ }): string | undefined {
1149
+ if (
1150
+ options.statusInfo.status !== "failed" ||
1151
+ options.statusInfo.failureKind !== "model" ||
1152
+ options.outputBytes !== 0 ||
1153
+ options.contextLengthExceeded
1154
+ ) {
1155
+ return undefined;
1156
+ }
1157
+ const text = options.stderrText;
1158
+ const deterministicPattern =
1159
+ /(Failed to load extension|Cannot find module|(?:failed to load|invalid|missing) (?:workflow )?config(?:uration)?|config(?:uration)? (?:error|failed|invalid))/i;
1160
+ if (!deterministicPattern.test(text)) return undefined;
1161
+ const excerpt =
1162
+ text
1163
+ .split(/\r?\n/)
1164
+ .map((line) => line.trim())
1165
+ .find((line) => deterministicPattern.test(line)) ?? text.trim();
1166
+ return `deterministic-boot failure: ${excerpt.slice(0, 500)}`;
1167
+ }
1168
+
1008
1169
  function shouldRetryTransientModelFailure(
1009
1170
  statusInfo: {
1010
1171
  status: WorkflowTaskRunRecord["status"];
@@ -1056,14 +1217,14 @@ function retryOrFailTransientSubagentFailure(
1056
1217
  if (!exhausted) {
1057
1218
  task.status = "pending";
1058
1219
  task.statusDetail = "retry_model_failure";
1059
- task.lastMessage = `${options.message}; retrying transient model failure (${attempt}/${maxAttempts})`;
1220
+ task.lastMessage = `${options.message}; retrying transient-model failure (${attempt}/${maxAttempts})`;
1060
1221
  return true;
1061
1222
  }
1062
1223
  task.status = "failed";
1063
1224
  task.statusDetail = task.launchRetry.reason ?? "model_exhausted";
1064
1225
  task.exitCode = 1;
1065
1226
  task.completedAt = nowIso();
1066
- task.lastMessage = `${options.message}; transient model failure retries exhausted (${maxAttempts})`;
1227
+ task.lastMessage = `${options.message}; transient-model failure retries exhausted (${maxAttempts})`;
1067
1228
  return true;
1068
1229
  }
1069
1230
 
@@ -1317,7 +1478,10 @@ async function workflowTaskExtensions(
1317
1478
  },
1318
1479
  });
1319
1480
  const capturedProviderExtensions = new Set(
1320
- workflowWebSourceProviderExtensions(tools, compiledTask.runtime.toolProviders),
1481
+ workflowWebSourceProviderExtensions(
1482
+ tools,
1483
+ compiledTask.runtime.toolProviders,
1484
+ ),
1321
1485
  );
1322
1486
  extensions = uniqueStrings([
1323
1487
  ...extensions.filter(
@@ -1673,7 +1837,7 @@ function buildSystemPrompt(task: CompiledTask): string {
1673
1837
  enabledTools.includes("workflow_web_source_read")
1674
1838
  ? "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
1839
  : !enabledTools.includes("get_search_content") &&
1676
- (enabledTools.includes("web_search") ||
1840
+ (enabledTools.includes("web_search") ||
1677
1841
  enabledTools.includes("fetch_content"))
1678
1842
  ? "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
1843
  : undefined,
package/src/types.ts CHANGED
@@ -1,3 +1,9 @@
1
+ import type {
2
+ WorkflowModelInfo,
3
+ WorkflowRuntimeDefaults,
4
+ WorkflowRuntimeThinkingResolution,
5
+ } from "./workflow-runtime.js";
6
+
1
7
  export const THINKING_LEVELS = [
2
8
  "off",
3
9
  "minimal",
@@ -284,6 +290,7 @@ export interface PermissionPreview {
284
290
  export interface CompiledTaskRuntime {
285
291
  model?: string;
286
292
  thinking?: ThinkingLevel;
293
+ thinkingResolution?: WorkflowRuntimeThinkingResolution;
287
294
  fast?: FastMode;
288
295
  approvalMode: ApprovalMode;
289
296
  tools?: string[];
@@ -469,6 +476,8 @@ export interface CompiledDynamicWorkflowTask {
469
476
  helpers: Record<string, CompiledDynamicWorkflowHelper>;
470
477
  workflows: Record<string, CompiledDynamicNestedWorkflow>;
471
478
  decisionLoop?: CompiledDynamicDecisionLoop;
479
+ runtimeOverrides?: WorkflowRuntimeDefaults;
480
+ availableModels?: WorkflowModelInfo[];
472
481
  }
473
482
 
474
483
  export interface CompiledArtifactGraphTask {
@@ -572,6 +581,7 @@ export interface WorkflowTaskRunRecord {
572
581
  runtime: {
573
582
  model?: string;
574
583
  thinking?: ThinkingLevel;
584
+ thinkingResolution?: WorkflowRuntimeThinkingResolution;
575
585
  fast?: FastMode;
576
586
  approvalMode: ApprovalMode;
577
587
  maxRuntimeMs?: number;