@agwab/pi-workflow 0.2.0 → 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 +2 -0
- package/dist/compiler.d.ts +4 -6
- package/dist/compiler.js +64 -31
- package/dist/dynamic-generated-task-runtime.d.ts +2 -0
- package/dist/dynamic-generated-task-runtime.js +21 -8
- package/dist/engine.d.ts +5 -6
- package/dist/engine.js +36 -52
- package/dist/extension.js +11 -9
- package/dist/store.d.ts +3 -1
- package/dist/store.js +134 -38
- package/dist/subagent-backend.d.ts +4 -0
- package/dist/subagent-backend.js +128 -4
- package/dist/types.d.ts +3 -1
- package/dist/workflow-runtime.d.ts +2 -0
- package/dist/workflow-runtime.js +30 -0
- package/docs/usage.md +11 -0
- package/package.json +1 -1
- package/src/compiler.ts +113 -57
- package/src/dynamic-generated-task-runtime.ts +47 -12
- package/src/engine.ts +49 -85
- package/src/extension.ts +18 -14
- package/src/store.ts +179 -44
- package/src/subagent-backend.ts +170 -6
- package/src/types.ts +7 -1
- package/src/workflow-runtime.ts +35 -0
package/src/extension.ts
CHANGED
|
@@ -39,7 +39,10 @@ import {
|
|
|
39
39
|
type ThinkingLevel,
|
|
40
40
|
WorkflowValidationError,
|
|
41
41
|
} from "./types.js";
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
toWorkflowModelInfo,
|
|
44
|
+
type WorkflowRuntimeDefaults,
|
|
45
|
+
} from "./workflow-runtime.js";
|
|
43
46
|
|
|
44
47
|
const UNFINISHED_RUN_NOTICE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
45
48
|
const UNFINISHED_RUN_NOTICE_MAX_RUNS = 5;
|
|
@@ -337,7 +340,8 @@ async function deliverMissedWorkflowFeedback(
|
|
|
337
340
|
const run = await readRunRecord(ctx.cwd, summary.runId).catch(
|
|
338
341
|
() => undefined,
|
|
339
342
|
);
|
|
340
|
-
if (run)
|
|
343
|
+
if (run)
|
|
344
|
+
await deliverWorkflowFeedback(ctx, api, run).catch(() => undefined);
|
|
341
345
|
}
|
|
342
346
|
}
|
|
343
347
|
|
|
@@ -548,13 +552,13 @@ interface WorkflowRunToolRequest {
|
|
|
548
552
|
workflow: string;
|
|
549
553
|
task: string;
|
|
550
554
|
detach: boolean;
|
|
551
|
-
|
|
555
|
+
runtimeOverrides?: WorkflowRuntimeDefaults;
|
|
552
556
|
}
|
|
553
557
|
|
|
554
558
|
interface WorkflowDynamicToolRequest {
|
|
555
559
|
task: string;
|
|
556
560
|
detach: boolean;
|
|
557
|
-
|
|
561
|
+
runtimeOverrides?: WorkflowRuntimeDefaults;
|
|
558
562
|
}
|
|
559
563
|
|
|
560
564
|
function parseWorkflowListToolParams(params: unknown): void {
|
|
@@ -602,9 +606,9 @@ function parseWorkflowDynamicToolParams(
|
|
|
602
606
|
"workflow_dynamic",
|
|
603
607
|
)?.trim();
|
|
604
608
|
const thinking = rawThinking ? parseThinkingLevel(rawThinking) : undefined;
|
|
605
|
-
const
|
|
609
|
+
const runtimeOverrides =
|
|
606
610
|
model || thinking ? { model: model || undefined, thinking } : undefined;
|
|
607
|
-
return { task, detach: detachValue === true,
|
|
611
|
+
return { task, detach: detachValue === true, runtimeOverrides };
|
|
608
612
|
}
|
|
609
613
|
|
|
610
614
|
function stringParam(
|
|
@@ -704,8 +708,8 @@ async function startWorkflowRunFromRequest(
|
|
|
704
708
|
);
|
|
705
709
|
const run = await runWorkflowSpec(workflow, ctx.cwd, {
|
|
706
710
|
task,
|
|
707
|
-
|
|
708
|
-
|
|
711
|
+
runtimeOverrides: request.runtimeOverrides,
|
|
712
|
+
runtimeDefaults: currentRuntimeDefaults(ctx, api),
|
|
709
713
|
availableModels: availableWorkflowModels(ctx),
|
|
710
714
|
dynamicUi: dynamicUiFromContext(ctx),
|
|
711
715
|
});
|
|
@@ -736,8 +740,8 @@ async function startDynamicRunFromRequest(
|
|
|
736
740
|
);
|
|
737
741
|
const run = await runDynamicTask(ctx.cwd, {
|
|
738
742
|
task,
|
|
739
|
-
|
|
740
|
-
|
|
743
|
+
runtimeOverrides: request.runtimeOverrides,
|
|
744
|
+
runtimeDefaults: currentRuntimeDefaults(ctx, api),
|
|
741
745
|
availableModels: availableWorkflowModels(ctx),
|
|
742
746
|
dynamicUi: dynamicUiFromContext(ctx),
|
|
743
747
|
});
|
|
@@ -1047,7 +1051,7 @@ async function handleWorkflowCommand(
|
|
|
1047
1051
|
const specPath =
|
|
1048
1052
|
parsed.specPath ||
|
|
1049
1053
|
requireArg(tokens, 1, '/workflow run <workflow-name-or-path> "<task>"');
|
|
1050
|
-
const
|
|
1054
|
+
const runtimeOverrides =
|
|
1051
1055
|
parsed.model || parsed.thinking
|
|
1052
1056
|
? { model: parsed.model, thinking: parsed.thinking }
|
|
1053
1057
|
: undefined;
|
|
@@ -1056,7 +1060,7 @@ async function handleWorkflowCommand(
|
|
|
1056
1060
|
workflow: specPath,
|
|
1057
1061
|
task: parsed.task,
|
|
1058
1062
|
detach: parsed.detach,
|
|
1059
|
-
|
|
1063
|
+
runtimeOverrides,
|
|
1060
1064
|
},
|
|
1061
1065
|
ctx,
|
|
1062
1066
|
api,
|
|
@@ -1067,7 +1071,7 @@ async function handleWorkflowCommand(
|
|
|
1067
1071
|
|
|
1068
1072
|
if (action === "dynamic") {
|
|
1069
1073
|
const parsed = parseWorkflowDynamicArgs(args);
|
|
1070
|
-
const
|
|
1074
|
+
const runtimeOverrides =
|
|
1071
1075
|
parsed.model || parsed.thinking
|
|
1072
1076
|
? { model: parsed.model, thinking: parsed.thinking }
|
|
1073
1077
|
: undefined;
|
|
@@ -1075,7 +1079,7 @@ async function handleWorkflowCommand(
|
|
|
1075
1079
|
{
|
|
1076
1080
|
task: parsed.task,
|
|
1077
1081
|
detach: parsed.detach,
|
|
1078
|
-
|
|
1082
|
+
runtimeOverrides,
|
|
1079
1083
|
},
|
|
1080
1084
|
ctx,
|
|
1081
1085
|
api,
|
package/src/store.ts
CHANGED
|
@@ -43,6 +43,12 @@ const TERMINAL_INDEX_LIMIT = 50;
|
|
|
43
43
|
const LEASE_STALE_MS = 30_000;
|
|
44
44
|
const INDEX_LOCK_WAIT_MS = 5_000;
|
|
45
45
|
const INDEX_LOCK_RETRY_MS = 50;
|
|
46
|
+
const DEFAULT_INDEX_UPDATE_DEBOUNCE_MS = 500;
|
|
47
|
+
let indexUpdateDebounceMs = DEFAULT_INDEX_UPDATE_DEBOUNCE_MS;
|
|
48
|
+
const pendingIndexUpdates = new Map<
|
|
49
|
+
string,
|
|
50
|
+
{ cwd: string; runId: string; timer: ReturnType<typeof setTimeout> }
|
|
51
|
+
>();
|
|
46
52
|
const runLeaseContext = new AsyncLocalStorage<{
|
|
47
53
|
cwd: string;
|
|
48
54
|
runId: string;
|
|
@@ -350,7 +356,56 @@ export async function writeRunRecord(
|
|
|
350
356
|
const derived = deriveRunStatus(run);
|
|
351
357
|
Object.assign(run, derived);
|
|
352
358
|
await writeJsonAtomic(workflowRunPath(cwd, run.runId), run);
|
|
353
|
-
|
|
359
|
+
scheduleIndexUpdate(cwd, run.runId, {
|
|
360
|
+
immediate: isTerminalWorkflowStatus(run.status),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function indexUpdateKey(cwd: string, runId: string): string {
|
|
365
|
+
return `${cwd}\0${runId}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function scheduleIndexUpdate(
|
|
369
|
+
cwd: string,
|
|
370
|
+
runId: string,
|
|
371
|
+
options: { immediate: boolean },
|
|
372
|
+
): void {
|
|
373
|
+
const key = indexUpdateKey(cwd, runId);
|
|
374
|
+
const existing = pendingIndexUpdates.get(key);
|
|
375
|
+
if (existing) {
|
|
376
|
+
clearTimeout(existing.timer);
|
|
377
|
+
pendingIndexUpdates.delete(key);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const runUpdate = (): void => {
|
|
381
|
+
pendingIndexUpdates.delete(key);
|
|
382
|
+
void updateIndex(cwd, runId).catch(() => undefined);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
if (options.immediate) {
|
|
386
|
+
runUpdate();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Pending debounced index writes are intentionally not flushed on process exit:
|
|
391
|
+
// the next explicit index rebuild/read path self-heals from run.json records.
|
|
392
|
+
const timer = setTimeout(runUpdate, indexUpdateDebounceMs);
|
|
393
|
+
timer.unref?.();
|
|
394
|
+
pendingIndexUpdates.set(key, { cwd, runId, timer });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function flushPendingIndexUpdatesForTests(): Promise<void> {
|
|
398
|
+
const pending = [...pendingIndexUpdates.values()];
|
|
399
|
+
pendingIndexUpdates.clear();
|
|
400
|
+
for (const item of pending) clearTimeout(item.timer);
|
|
401
|
+
await Promise.all(pending.map((item) => updateIndex(item.cwd, item.runId)));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function setIndexUpdateDebounceMsForTests(value?: number): void {
|
|
405
|
+
indexUpdateDebounceMs =
|
|
406
|
+
value === undefined
|
|
407
|
+
? DEFAULT_INDEX_UPDATE_DEBOUNCE_MS
|
|
408
|
+
: Math.max(0, Math.floor(value));
|
|
354
409
|
}
|
|
355
410
|
|
|
356
411
|
export async function writeCompiledRunArtifact(
|
|
@@ -1088,55 +1143,19 @@ function isRunRecordLike(value: unknown): value is WorkflowRunRecord {
|
|
|
1088
1143
|
);
|
|
1089
1144
|
}
|
|
1090
1145
|
|
|
1091
|
-
export async function updateIndex(
|
|
1146
|
+
export async function updateIndex(
|
|
1147
|
+
cwd: string,
|
|
1148
|
+
changedRunId?: string,
|
|
1149
|
+
): Promise<WorkflowIndexRecord> {
|
|
1092
1150
|
const lockFile = join(workflowsRoot(cwd), "index.lock");
|
|
1093
1151
|
const ownerId = `${process.pid}-${randomBytes(3).toString("hex")}`;
|
|
1094
1152
|
await ensureDir(workflowsRoot(cwd));
|
|
1095
1153
|
await acquireLockWithWait(lockFile, ownerId);
|
|
1096
1154
|
|
|
1097
1155
|
try {
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const active = runs.filter((run) => !isTerminalWorkflowStatus(run.status));
|
|
1102
|
-
const terminal = runs
|
|
1103
|
-
.filter((run) => isTerminalWorkflowStatus(run.status))
|
|
1104
|
-
.slice(0, TERMINAL_INDEX_LIMIT);
|
|
1105
|
-
const selected = [...active, ...terminal].sort((left, right) =>
|
|
1106
|
-
right.updatedAt.localeCompare(left.updatedAt),
|
|
1107
|
-
);
|
|
1108
|
-
|
|
1109
|
-
const index: WorkflowIndexRecord = {
|
|
1110
|
-
schemaVersion: 1,
|
|
1111
|
-
updatedAt: nowIso(),
|
|
1112
|
-
runs: selected.map((run) => ({
|
|
1113
|
-
runId: run.runId,
|
|
1114
|
-
name: run.name,
|
|
1115
|
-
type: run.type,
|
|
1116
|
-
artifactGraph: run.artifactGraph,
|
|
1117
|
-
status: run.status,
|
|
1118
|
-
taskSummary: run.taskSummary,
|
|
1119
|
-
createdAt: run.createdAt,
|
|
1120
|
-
updatedAt: run.updatedAt,
|
|
1121
|
-
parentRunId: run.parentRunId,
|
|
1122
|
-
rootRunId: run.rootRunId,
|
|
1123
|
-
round: run.round,
|
|
1124
|
-
fanout: run.fanout,
|
|
1125
|
-
runJson: toProjectPath(cwd, workflowRunPath(cwd, run.runId)),
|
|
1126
|
-
tasks: run.tasks.map((task) => ({
|
|
1127
|
-
taskId: task.taskId,
|
|
1128
|
-
displayName: task.displayName,
|
|
1129
|
-
agent: task.agent,
|
|
1130
|
-
kind: task.kind,
|
|
1131
|
-
stageId: task.stageId,
|
|
1132
|
-
backendHandle: task.backendHandle,
|
|
1133
|
-
status: task.status,
|
|
1134
|
-
statusDetail: task.statusDetail,
|
|
1135
|
-
lastMessage: task.lastMessage,
|
|
1136
|
-
})),
|
|
1137
|
-
})),
|
|
1138
|
-
};
|
|
1139
|
-
|
|
1156
|
+
const index = changedRunId
|
|
1157
|
+
? await updateIndexIncremental(cwd, changedRunId)
|
|
1158
|
+
: await rebuildIndex(cwd);
|
|
1140
1159
|
await writeJsonAtomic(workflowIndexPath(cwd), index);
|
|
1141
1160
|
return index;
|
|
1142
1161
|
} finally {
|
|
@@ -1144,6 +1163,122 @@ export async function updateIndex(cwd: string): Promise<WorkflowIndexRecord> {
|
|
|
1144
1163
|
}
|
|
1145
1164
|
}
|
|
1146
1165
|
|
|
1166
|
+
type WorkflowIndexRunEntry = WorkflowIndexRecord["runs"][number];
|
|
1167
|
+
|
|
1168
|
+
async function updateIndexIncremental(
|
|
1169
|
+
cwd: string,
|
|
1170
|
+
changedRunId: string,
|
|
1171
|
+
): Promise<WorkflowIndexRecord> {
|
|
1172
|
+
const existing = await readIndexForIncremental(cwd);
|
|
1173
|
+
if (!existing) return rebuildIndex(cwd);
|
|
1174
|
+
|
|
1175
|
+
let changedRun: WorkflowRunRecord;
|
|
1176
|
+
try {
|
|
1177
|
+
changedRun = await readRunRecord(cwd, changedRunId);
|
|
1178
|
+
} catch {
|
|
1179
|
+
return rebuildIndex(cwd);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const changedEntry = buildIndexEntry(cwd, changedRun);
|
|
1183
|
+
const entries = existing.runs
|
|
1184
|
+
.filter((entry) => entry.runId !== changedRun.runId)
|
|
1185
|
+
.concat(changedEntry);
|
|
1186
|
+
return {
|
|
1187
|
+
schemaVersion: 1,
|
|
1188
|
+
updatedAt: nowIso(),
|
|
1189
|
+
runs: selectIndexEntries(entries),
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async function readIndexForIncremental(
|
|
1194
|
+
cwd: string,
|
|
1195
|
+
): Promise<WorkflowIndexRecord | undefined> {
|
|
1196
|
+
let index: WorkflowIndexRecord | undefined;
|
|
1197
|
+
try {
|
|
1198
|
+
index = await readIndex(cwd);
|
|
1199
|
+
} catch {
|
|
1200
|
+
return undefined;
|
|
1201
|
+
}
|
|
1202
|
+
if (!isIndexRecordLike(index)) return undefined;
|
|
1203
|
+
return index;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async function rebuildIndex(cwd: string): Promise<WorkflowIndexRecord> {
|
|
1207
|
+
const runs = await listRunRecords(cwd);
|
|
1208
|
+
return {
|
|
1209
|
+
schemaVersion: 1,
|
|
1210
|
+
updatedAt: nowIso(),
|
|
1211
|
+
runs: selectIndexEntries(runs.map((run) => buildIndexEntry(cwd, run))),
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function selectIndexEntries(
|
|
1216
|
+
entries: WorkflowIndexRunEntry[],
|
|
1217
|
+
): WorkflowIndexRunEntry[] {
|
|
1218
|
+
const sorted = [...entries].sort((left, right) =>
|
|
1219
|
+
right.updatedAt.localeCompare(left.updatedAt),
|
|
1220
|
+
);
|
|
1221
|
+
const active = sorted.filter(
|
|
1222
|
+
(entry) => !isTerminalWorkflowStatus(entry.status),
|
|
1223
|
+
);
|
|
1224
|
+
const terminal = sorted
|
|
1225
|
+
.filter((entry) => isTerminalWorkflowStatus(entry.status))
|
|
1226
|
+
.slice(0, TERMINAL_INDEX_LIMIT);
|
|
1227
|
+
return [...active, ...terminal].sort((left, right) =>
|
|
1228
|
+
right.updatedAt.localeCompare(left.updatedAt),
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function buildIndexEntry(
|
|
1233
|
+
cwd: string,
|
|
1234
|
+
run: WorkflowRunRecord,
|
|
1235
|
+
): WorkflowIndexRunEntry {
|
|
1236
|
+
return {
|
|
1237
|
+
runId: run.runId,
|
|
1238
|
+
name: run.name,
|
|
1239
|
+
type: run.type,
|
|
1240
|
+
artifactGraph: run.artifactGraph,
|
|
1241
|
+
status: run.status,
|
|
1242
|
+
taskSummary: run.taskSummary,
|
|
1243
|
+
createdAt: run.createdAt,
|
|
1244
|
+
updatedAt: run.updatedAt,
|
|
1245
|
+
parentRunId: run.parentRunId,
|
|
1246
|
+
rootRunId: run.rootRunId,
|
|
1247
|
+
round: run.round,
|
|
1248
|
+
fanout: run.fanout,
|
|
1249
|
+
runJson: toProjectPath(cwd, workflowRunPath(cwd, run.runId)),
|
|
1250
|
+
tasks: run.tasks.map((task) => ({
|
|
1251
|
+
taskId: task.taskId,
|
|
1252
|
+
displayName: task.displayName,
|
|
1253
|
+
agent: task.agent,
|
|
1254
|
+
kind: task.kind,
|
|
1255
|
+
stageId: task.stageId,
|
|
1256
|
+
backendHandle: task.backendHandle,
|
|
1257
|
+
status: task.status,
|
|
1258
|
+
statusDetail: task.statusDetail,
|
|
1259
|
+
lastMessage: task.lastMessage,
|
|
1260
|
+
})),
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function isIndexRecordLike(
|
|
1265
|
+
value: WorkflowIndexRecord | undefined,
|
|
1266
|
+
): value is WorkflowIndexRecord {
|
|
1267
|
+
return (
|
|
1268
|
+
value?.schemaVersion === 1 &&
|
|
1269
|
+
Array.isArray(value.runs) &&
|
|
1270
|
+
value.runs.every(
|
|
1271
|
+
(entry) =>
|
|
1272
|
+
entry &&
|
|
1273
|
+
typeof entry === "object" &&
|
|
1274
|
+
typeof entry.runId === "string" &&
|
|
1275
|
+
typeof entry.updatedAt === "string" &&
|
|
1276
|
+
typeof entry.status === "string" &&
|
|
1277
|
+
Array.isArray(entry.tasks),
|
|
1278
|
+
)
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1147
1282
|
export function deriveRunStatus(run: WorkflowRunRecord): WorkflowRunRecord {
|
|
1148
1283
|
const next = { ...run, tasks: run.tasks };
|
|
1149
1284
|
next.taskSummary = summarizeTasks(next.tasks);
|
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,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 {
|
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;
|