@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.
- package/README.md +9 -13
- package/dist/compiler.d.ts +5 -5
- package/dist/compiler.js +82 -24
- package/dist/dynamic-generated-task-runtime.d.ts +2 -0
- package/dist/dynamic-generated-task-runtime.js +21 -8
- package/dist/engine.d.ts +6 -5
- package/dist/engine.js +39 -54
- package/dist/extension.js +211 -24
- package/dist/store.d.ts +3 -1
- package/dist/store.js +135 -38
- package/dist/subagent-backend.d.ts +4 -0
- package/dist/subagent-backend.js +128 -4
- package/dist/types.d.ts +5 -0
- package/dist/workflow-progress-health.d.ts +37 -0
- package/dist/workflow-progress-health.js +296 -0
- package/dist/workflow-runtime.d.ts +8 -0
- package/dist/workflow-runtime.js +63 -10
- package/dist/workflow-view.d.ts +2 -0
- package/dist/workflow-view.js +97 -18
- package/dist/workflow-web-source.js +32 -14
- package/docs/usage.md +12 -1
- package/package.json +6 -6
- package/src/compiler.ts +136 -41
- package/src/dynamic-generated-task-runtime.ts +47 -12
- package/src/engine.ts +55 -100
- package/src/extension.ts +270 -34
- package/src/store.ts +180 -44
- package/src/subagent-backend.ts +170 -6
- package/src/types.ts +10 -0
- package/src/workflow-progress-health.ts +461 -0
- package/src/workflow-runtime.ts +85 -13
- package/src/workflow-view.ts +186 -41
- package/src/workflow-web-source.ts +192 -69
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +111 -37
- package/workflows/deep-research/helpers/final-audit-packet.mjs +191 -14
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +159 -50
- package/workflows/deep-research/helpers/render-executive.mjs +671 -37
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +624 -0
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +2 -0
- package/workflows/deep-research/schemas/deep-research-final-synthesis-control.schema.json +110 -0
- package/workflows/deep-research/spec.json +41 -11
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,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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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;
|