@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/dist/store.js
CHANGED
|
@@ -8,6 +8,9 @@ const TERMINAL_INDEX_LIMIT = 50;
|
|
|
8
8
|
const LEASE_STALE_MS = 30_000;
|
|
9
9
|
const INDEX_LOCK_WAIT_MS = 5_000;
|
|
10
10
|
const INDEX_LOCK_RETRY_MS = 50;
|
|
11
|
+
const DEFAULT_INDEX_UPDATE_DEBOUNCE_MS = 500;
|
|
12
|
+
let indexUpdateDebounceMs = DEFAULT_INDEX_UPDATE_DEBOUNCE_MS;
|
|
13
|
+
const pendingIndexUpdates = new Map();
|
|
11
14
|
const runLeaseContext = new AsyncLocalStorage();
|
|
12
15
|
const TASK_STATUSES = [
|
|
13
16
|
"pending",
|
|
@@ -256,7 +259,46 @@ export async function writeRunRecord(cwd, run) {
|
|
|
256
259
|
const derived = deriveRunStatus(run);
|
|
257
260
|
Object.assign(run, derived);
|
|
258
261
|
await writeJsonAtomic(workflowRunPath(cwd, run.runId), run);
|
|
259
|
-
|
|
262
|
+
scheduleIndexUpdate(cwd, run.runId, {
|
|
263
|
+
immediate: isTerminalWorkflowStatus(run.status),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function indexUpdateKey(cwd, runId) {
|
|
267
|
+
return `${cwd}\0${runId}`;
|
|
268
|
+
}
|
|
269
|
+
function scheduleIndexUpdate(cwd, runId, options) {
|
|
270
|
+
const key = indexUpdateKey(cwd, runId);
|
|
271
|
+
const existing = pendingIndexUpdates.get(key);
|
|
272
|
+
if (existing) {
|
|
273
|
+
clearTimeout(existing.timer);
|
|
274
|
+
pendingIndexUpdates.delete(key);
|
|
275
|
+
}
|
|
276
|
+
const runUpdate = () => {
|
|
277
|
+
pendingIndexUpdates.delete(key);
|
|
278
|
+
void updateIndex(cwd, runId).catch(() => undefined);
|
|
279
|
+
};
|
|
280
|
+
if (options.immediate) {
|
|
281
|
+
runUpdate();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// Pending debounced index writes are intentionally not flushed on process exit:
|
|
285
|
+
// the next explicit index rebuild/read path self-heals from run.json records.
|
|
286
|
+
const timer = setTimeout(runUpdate, indexUpdateDebounceMs);
|
|
287
|
+
timer.unref?.();
|
|
288
|
+
pendingIndexUpdates.set(key, { cwd, runId, timer });
|
|
289
|
+
}
|
|
290
|
+
export async function flushPendingIndexUpdatesForTests() {
|
|
291
|
+
const pending = [...pendingIndexUpdates.values()];
|
|
292
|
+
pendingIndexUpdates.clear();
|
|
293
|
+
for (const item of pending)
|
|
294
|
+
clearTimeout(item.timer);
|
|
295
|
+
await Promise.all(pending.map((item) => updateIndex(item.cwd, item.runId)));
|
|
296
|
+
}
|
|
297
|
+
export function setIndexUpdateDebounceMsForTests(value) {
|
|
298
|
+
indexUpdateDebounceMs =
|
|
299
|
+
value === undefined
|
|
300
|
+
? DEFAULT_INDEX_UPDATE_DEBOUNCE_MS
|
|
301
|
+
: Math.max(0, Math.floor(value));
|
|
260
302
|
}
|
|
261
303
|
export async function writeCompiledRunArtifact(cwd, runId, compiled) {
|
|
262
304
|
const runDir = workflowRunDir(cwd, runId);
|
|
@@ -830,48 +872,15 @@ function isRunRecordLike(value) {
|
|
|
830
872
|
typeof task.status === "string" &&
|
|
831
873
|
TASK_STATUSES.includes(task.status)));
|
|
832
874
|
}
|
|
833
|
-
export async function updateIndex(cwd) {
|
|
875
|
+
export async function updateIndex(cwd, changedRunId) {
|
|
834
876
|
const lockFile = join(workflowsRoot(cwd), "index.lock");
|
|
835
877
|
const ownerId = `${process.pid}-${randomBytes(3).toString("hex")}`;
|
|
836
878
|
await ensureDir(workflowsRoot(cwd));
|
|
837
879
|
await acquireLockWithWait(lockFile, ownerId);
|
|
838
880
|
try {
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
.filter((run) => isTerminalWorkflowStatus(run.status))
|
|
843
|
-
.slice(0, TERMINAL_INDEX_LIMIT);
|
|
844
|
-
const selected = [...active, ...terminal].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
845
|
-
const index = {
|
|
846
|
-
schemaVersion: 1,
|
|
847
|
-
updatedAt: nowIso(),
|
|
848
|
-
runs: selected.map((run) => ({
|
|
849
|
-
runId: run.runId,
|
|
850
|
-
name: run.name,
|
|
851
|
-
type: run.type,
|
|
852
|
-
artifactGraph: run.artifactGraph,
|
|
853
|
-
status: run.status,
|
|
854
|
-
taskSummary: run.taskSummary,
|
|
855
|
-
createdAt: run.createdAt,
|
|
856
|
-
updatedAt: run.updatedAt,
|
|
857
|
-
parentRunId: run.parentRunId,
|
|
858
|
-
rootRunId: run.rootRunId,
|
|
859
|
-
round: run.round,
|
|
860
|
-
fanout: run.fanout,
|
|
861
|
-
runJson: toProjectPath(cwd, workflowRunPath(cwd, run.runId)),
|
|
862
|
-
tasks: run.tasks.map((task) => ({
|
|
863
|
-
taskId: task.taskId,
|
|
864
|
-
displayName: task.displayName,
|
|
865
|
-
agent: task.agent,
|
|
866
|
-
kind: task.kind,
|
|
867
|
-
stageId: task.stageId,
|
|
868
|
-
backendHandle: task.backendHandle,
|
|
869
|
-
status: task.status,
|
|
870
|
-
statusDetail: task.statusDetail,
|
|
871
|
-
lastMessage: task.lastMessage,
|
|
872
|
-
})),
|
|
873
|
-
})),
|
|
874
|
-
};
|
|
881
|
+
const index = changedRunId
|
|
882
|
+
? await updateIndexIncremental(cwd, changedRunId)
|
|
883
|
+
: await rebuildIndex(cwd);
|
|
875
884
|
await writeJsonAtomic(workflowIndexPath(cwd), index);
|
|
876
885
|
return index;
|
|
877
886
|
}
|
|
@@ -879,6 +888,93 @@ export async function updateIndex(cwd) {
|
|
|
879
888
|
await releaseLock(lockFile, ownerId);
|
|
880
889
|
}
|
|
881
890
|
}
|
|
891
|
+
async function updateIndexIncremental(cwd, changedRunId) {
|
|
892
|
+
const existing = await readIndexForIncremental(cwd);
|
|
893
|
+
if (!existing)
|
|
894
|
+
return rebuildIndex(cwd);
|
|
895
|
+
let changedRun;
|
|
896
|
+
try {
|
|
897
|
+
changedRun = await readRunRecord(cwd, changedRunId);
|
|
898
|
+
}
|
|
899
|
+
catch {
|
|
900
|
+
return rebuildIndex(cwd);
|
|
901
|
+
}
|
|
902
|
+
const changedEntry = buildIndexEntry(cwd, changedRun);
|
|
903
|
+
const entries = existing.runs
|
|
904
|
+
.filter((entry) => entry.runId !== changedRun.runId)
|
|
905
|
+
.concat(changedEntry);
|
|
906
|
+
return {
|
|
907
|
+
schemaVersion: 1,
|
|
908
|
+
updatedAt: nowIso(),
|
|
909
|
+
runs: selectIndexEntries(entries),
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
async function readIndexForIncremental(cwd) {
|
|
913
|
+
let index;
|
|
914
|
+
try {
|
|
915
|
+
index = await readIndex(cwd);
|
|
916
|
+
}
|
|
917
|
+
catch {
|
|
918
|
+
return undefined;
|
|
919
|
+
}
|
|
920
|
+
if (!isIndexRecordLike(index))
|
|
921
|
+
return undefined;
|
|
922
|
+
return index;
|
|
923
|
+
}
|
|
924
|
+
async function rebuildIndex(cwd) {
|
|
925
|
+
const runs = await listRunRecords(cwd);
|
|
926
|
+
return {
|
|
927
|
+
schemaVersion: 1,
|
|
928
|
+
updatedAt: nowIso(),
|
|
929
|
+
runs: selectIndexEntries(runs.map((run) => buildIndexEntry(cwd, run))),
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function selectIndexEntries(entries) {
|
|
933
|
+
const sorted = [...entries].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
934
|
+
const active = sorted.filter((entry) => !isTerminalWorkflowStatus(entry.status));
|
|
935
|
+
const terminal = sorted
|
|
936
|
+
.filter((entry) => isTerminalWorkflowStatus(entry.status))
|
|
937
|
+
.slice(0, TERMINAL_INDEX_LIMIT);
|
|
938
|
+
return [...active, ...terminal].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
939
|
+
}
|
|
940
|
+
function buildIndexEntry(cwd, run) {
|
|
941
|
+
return {
|
|
942
|
+
runId: run.runId,
|
|
943
|
+
name: run.name,
|
|
944
|
+
type: run.type,
|
|
945
|
+
artifactGraph: run.artifactGraph,
|
|
946
|
+
status: run.status,
|
|
947
|
+
taskSummary: run.taskSummary,
|
|
948
|
+
createdAt: run.createdAt,
|
|
949
|
+
updatedAt: run.updatedAt,
|
|
950
|
+
parentRunId: run.parentRunId,
|
|
951
|
+
rootRunId: run.rootRunId,
|
|
952
|
+
round: run.round,
|
|
953
|
+
fanout: run.fanout,
|
|
954
|
+
runJson: toProjectPath(cwd, workflowRunPath(cwd, run.runId)),
|
|
955
|
+
tasks: run.tasks.map((task) => ({
|
|
956
|
+
taskId: task.taskId,
|
|
957
|
+
displayName: task.displayName,
|
|
958
|
+
agent: task.agent,
|
|
959
|
+
kind: task.kind,
|
|
960
|
+
stageId: task.stageId,
|
|
961
|
+
backendHandle: task.backendHandle,
|
|
962
|
+
status: task.status,
|
|
963
|
+
statusDetail: task.statusDetail,
|
|
964
|
+
lastMessage: task.lastMessage,
|
|
965
|
+
})),
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
function isIndexRecordLike(value) {
|
|
969
|
+
return (value?.schemaVersion === 1 &&
|
|
970
|
+
Array.isArray(value.runs) &&
|
|
971
|
+
value.runs.every((entry) => entry &&
|
|
972
|
+
typeof entry === "object" &&
|
|
973
|
+
typeof entry.runId === "string" &&
|
|
974
|
+
typeof entry.updatedAt === "string" &&
|
|
975
|
+
typeof entry.status === "string" &&
|
|
976
|
+
Array.isArray(entry.tasks)));
|
|
977
|
+
}
|
|
882
978
|
export function deriveRunStatus(run) {
|
|
883
979
|
const next = { ...run, tasks: run.tasks };
|
|
884
980
|
next.taskSummary = summarizeTasks(next.tasks);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { CompiledTask, WorkflowRunRecord, WorkflowTaskRunRecord } from "./types.js";
|
|
2
2
|
import type { BackendLaunchResult } from "./backend.js";
|
|
3
3
|
export declare function setSubagentApiForTests(api: unknown | undefined): void;
|
|
4
|
+
export declare function setSubagentLaunchControlsForTests(options?: {
|
|
5
|
+
releaseDelayMs?: number;
|
|
6
|
+
retryJitterMs?: number | (() => number);
|
|
7
|
+
}): void;
|
|
4
8
|
export declare function cleanupSubagentRun(_cwd: string, run: WorkflowRunRecord): Promise<void>;
|
|
5
9
|
export declare function launchSubagentTask(cwd: string, run: WorkflowRunRecord, task: WorkflowTaskRunRecord, compiledTask: CompiledTask): Promise<BackendLaunchResult>;
|
|
6
10
|
export declare function refreshRunFromSubagentArtifacts(cwd: string, run: WorkflowRunRecord): Promise<WorkflowRunRecord>;
|
package/dist/subagent-backend.js
CHANGED
|
@@ -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,10 @@ 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 DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS = 3_000;
|
|
21
|
+
const MIN_TRANSIENT_RETRY_JITTER_MS = 1_000;
|
|
22
|
+
const MAX_TRANSIENT_RETRY_JITTER_MS = 5_000;
|
|
18
23
|
const MODULE_PATH = fileURLToPath(import.meta.url);
|
|
19
24
|
const MODULE_DIR = dirname(MODULE_PATH);
|
|
20
25
|
const BUNDLED_PI_WEB_ACCESS_EXTENSION = bundledNodeModulePath("pi-web-access", "index.ts");
|
|
@@ -47,6 +52,81 @@ async function loadSubagentApi() {
|
|
|
47
52
|
cachedSubagentApi ??= import(subagentApiSpecifier).then((mod) => mod);
|
|
48
53
|
return cachedSubagentApi;
|
|
49
54
|
}
|
|
55
|
+
let launchSlotReleaseDelayMs = DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
|
|
56
|
+
let transientRetryJitterForTests;
|
|
57
|
+
const launchWaitQueue = [];
|
|
58
|
+
let activeLaunchSlots = 0;
|
|
59
|
+
function resolveMaxConcurrentLaunches() {
|
|
60
|
+
const override = Number.parseInt(process.env[MAX_CONCURRENT_LAUNCHES_ENV] ?? "", 10);
|
|
61
|
+
if (Number.isFinite(override))
|
|
62
|
+
return Math.max(1, Math.floor(override));
|
|
63
|
+
return Math.max(2, Math.floor(availableParallelism() / 2));
|
|
64
|
+
}
|
|
65
|
+
function isLaunchGateSaturated() {
|
|
66
|
+
return activeLaunchSlots >= resolveMaxConcurrentLaunches();
|
|
67
|
+
}
|
|
68
|
+
async function acquireLaunchSlot() {
|
|
69
|
+
if (!isLaunchGateSaturated()) {
|
|
70
|
+
activeLaunchSlots += 1;
|
|
71
|
+
return releaseLaunchSlot;
|
|
72
|
+
}
|
|
73
|
+
await new Promise((resolveWait) => launchWaitQueue.push(resolveWait));
|
|
74
|
+
return releaseLaunchSlot;
|
|
75
|
+
}
|
|
76
|
+
function releaseLaunchSlot() {
|
|
77
|
+
const next = launchWaitQueue.shift();
|
|
78
|
+
if (next) {
|
|
79
|
+
// Transfer the occupied slot directly to the queued launcher.
|
|
80
|
+
next();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
activeLaunchSlots = Math.max(0, activeLaunchSlots - 1);
|
|
84
|
+
}
|
|
85
|
+
function releaseLaunchSlotAfterDelay(delayMs, release) {
|
|
86
|
+
if (delayMs <= 0) {
|
|
87
|
+
release();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const timer = setTimeout(release, delayMs);
|
|
91
|
+
timer.unref?.();
|
|
92
|
+
}
|
|
93
|
+
async function runWithLaunchSlot(action) {
|
|
94
|
+
const release = await acquireLaunchSlot();
|
|
95
|
+
let holdAfterReturn = false;
|
|
96
|
+
try {
|
|
97
|
+
const result = await action();
|
|
98
|
+
holdAfterReturn = true;
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
releaseLaunchSlotAfterDelay(holdAfterReturn ? launchSlotReleaseDelayMs : 0, release);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function transientRetryJitterMs() {
|
|
106
|
+
if (transientRetryJitterForTests)
|
|
107
|
+
return transientRetryJitterForTests();
|
|
108
|
+
return (MIN_TRANSIENT_RETRY_JITTER_MS +
|
|
109
|
+
Math.floor(Math.random() *
|
|
110
|
+
(MAX_TRANSIENT_RETRY_JITTER_MS - MIN_TRANSIENT_RETRY_JITTER_MS + 1)));
|
|
111
|
+
}
|
|
112
|
+
function sleep(ms) {
|
|
113
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
114
|
+
}
|
|
115
|
+
export function setSubagentLaunchControlsForTests(options) {
|
|
116
|
+
launchSlotReleaseDelayMs =
|
|
117
|
+
options?.releaseDelayMs === undefined
|
|
118
|
+
? DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS
|
|
119
|
+
: Math.max(0, Math.floor(options.releaseDelayMs));
|
|
120
|
+
transientRetryJitterForTests =
|
|
121
|
+
options?.retryJitterMs === undefined
|
|
122
|
+
? undefined
|
|
123
|
+
: typeof options.retryJitterMs === "function"
|
|
124
|
+
? options.retryJitterMs
|
|
125
|
+
: () => Math.max(0, Math.floor(options.retryJitterMs));
|
|
126
|
+
activeLaunchSlots = 0;
|
|
127
|
+
while (launchWaitQueue.length > 0)
|
|
128
|
+
launchWaitQueue.shift()?.();
|
|
129
|
+
}
|
|
50
130
|
export async function cleanupSubagentRun(_cwd, run) {
|
|
51
131
|
for (const task of run.tasks) {
|
|
52
132
|
if (isTerminalTaskStatus(task.status))
|
|
@@ -77,6 +157,14 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
|
|
|
77
157
|
message: "fast:on is not supported for pi-workflow execution.",
|
|
78
158
|
};
|
|
79
159
|
}
|
|
160
|
+
if ((task.launchRetry?.attempts ?? 0) > 0) {
|
|
161
|
+
const jitterMs = transientRetryJitterMs();
|
|
162
|
+
task.statusDetail = "retry_model_failure";
|
|
163
|
+
task.lastMessage = `waiting ${jitterMs}ms before retrying transient-model launch`;
|
|
164
|
+
await writeRunRecord(cwd, run);
|
|
165
|
+
if (jitterMs > 0)
|
|
166
|
+
await sleep(jitterMs);
|
|
167
|
+
}
|
|
80
168
|
const systemPromptFile = fromProjectPath(cwd, task.files.systemPrompt);
|
|
81
169
|
const taskPromptFile = fromProjectPath(cwd, task.files.taskPrompt);
|
|
82
170
|
const outputFile = fromProjectPath(cwd, task.files.output);
|
|
@@ -126,7 +214,11 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
|
|
|
126
214
|
subagentOptions.extensions = extensions;
|
|
127
215
|
if (captureToolCallsEnabled())
|
|
128
216
|
subagentOptions.captureToolCalls = true;
|
|
129
|
-
|
|
217
|
+
if (isLaunchGateSaturated()) {
|
|
218
|
+
task.lastMessage = `waiting for pi-subagent launch slot (${resolveMaxConcurrentLaunches()} max)`;
|
|
219
|
+
await writeRunRecord(cwd, run).catch(() => undefined);
|
|
220
|
+
}
|
|
221
|
+
launched = await runWithLaunchSlot(() => api.runSubagent(subagentOptions));
|
|
130
222
|
}
|
|
131
223
|
catch (error) {
|
|
132
224
|
task.status = "pending";
|
|
@@ -259,8 +351,23 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
259
351
|
: undefined;
|
|
260
352
|
const toolCalls = await readToolCallsSummary(snapshot, subagentResult, artifactRoot);
|
|
261
353
|
const outputText = await readFile(outputFile, "utf8").catch(() => "");
|
|
354
|
+
const stderrText = await readFile(stderrFile, "utf8").catch(() => "");
|
|
262
355
|
const outputBytes = Buffer.byteLength(outputText, "utf8");
|
|
263
|
-
|
|
356
|
+
let statusInfo = workflowStatusFromSubagent(snapshot, subagentResult, outputBytes);
|
|
357
|
+
const deterministicBootFailure = classifyDeterministicBootFailure({
|
|
358
|
+
statusInfo,
|
|
359
|
+
stderrText,
|
|
360
|
+
outputBytes,
|
|
361
|
+
contextLengthExceeded: Boolean(subagentResult?.metadata?.contextLengthExceeded ??
|
|
362
|
+
snapshot.metadata?.contextLengthExceeded),
|
|
363
|
+
});
|
|
364
|
+
if (deterministicBootFailure) {
|
|
365
|
+
statusInfo = {
|
|
366
|
+
status: "failed",
|
|
367
|
+
failureKind: "deterministic_boot",
|
|
368
|
+
errorMessage: deterministicBootFailure,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
264
371
|
const completedAt = typeof subagentResult?.completedAt === "string"
|
|
265
372
|
? subagentResult.completedAt
|
|
266
373
|
: (snapshot.completedAt ?? nowIso());
|
|
@@ -685,6 +792,23 @@ function failArtifactGraphTask(task, options) {
|
|
|
685
792
|
task.lastMessage = options.message;
|
|
686
793
|
return true;
|
|
687
794
|
}
|
|
795
|
+
function classifyDeterministicBootFailure(options) {
|
|
796
|
+
if (options.statusInfo.status !== "failed" ||
|
|
797
|
+
options.statusInfo.failureKind !== "model" ||
|
|
798
|
+
options.outputBytes !== 0 ||
|
|
799
|
+
options.contextLengthExceeded) {
|
|
800
|
+
return undefined;
|
|
801
|
+
}
|
|
802
|
+
const text = options.stderrText;
|
|
803
|
+
const deterministicPattern = /(Failed to load extension|Cannot find module|(?:failed to load|invalid|missing) (?:workflow )?config(?:uration)?|config(?:uration)? (?:error|failed|invalid))/i;
|
|
804
|
+
if (!deterministicPattern.test(text))
|
|
805
|
+
return undefined;
|
|
806
|
+
const excerpt = text
|
|
807
|
+
.split(/\r?\n/)
|
|
808
|
+
.map((line) => line.trim())
|
|
809
|
+
.find((line) => deterministicPattern.test(line)) ?? text.trim();
|
|
810
|
+
return `deterministic-boot failure: ${excerpt.slice(0, 500)}`;
|
|
811
|
+
}
|
|
688
812
|
function shouldRetryTransientModelFailure(statusInfo, workflowResult, outputBytes) {
|
|
689
813
|
return (statusInfo.status === "failed" &&
|
|
690
814
|
statusInfo.failureKind === "model" &&
|
|
@@ -714,14 +838,14 @@ function retryOrFailTransientSubagentFailure(task, options) {
|
|
|
714
838
|
if (!exhausted) {
|
|
715
839
|
task.status = "pending";
|
|
716
840
|
task.statusDetail = "retry_model_failure";
|
|
717
|
-
task.lastMessage = `${options.message}; retrying transient
|
|
841
|
+
task.lastMessage = `${options.message}; retrying transient-model failure (${attempt}/${maxAttempts})`;
|
|
718
842
|
return true;
|
|
719
843
|
}
|
|
720
844
|
task.status = "failed";
|
|
721
845
|
task.statusDetail = task.launchRetry.reason ?? "model_exhausted";
|
|
722
846
|
task.exitCode = 1;
|
|
723
847
|
task.completedAt = nowIso();
|
|
724
|
-
task.lastMessage = `${options.message}; transient
|
|
848
|
+
task.lastMessage = `${options.message}; transient-model failure retries exhausted (${maxAttempts})`;
|
|
725
849
|
return true;
|
|
726
850
|
}
|
|
727
851
|
function retryOrFailArtifactGraphTask(task, options) {
|
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;
|
|
@@ -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;
|
package/dist/workflow-runtime.js
CHANGED
|
@@ -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,
|
package/docs/usage.md
CHANGED
|
@@ -187,6 +187,17 @@ A run prints a `workflow_*` id. Use that id for follow-up commands:
|
|
|
187
187
|
|
|
188
188
|
The runtime task is not optional. `/workflow run <workflow>` and `/workflow dynamic` without task text fail before launch.
|
|
189
189
|
|
|
190
|
+
### Opt-in fast mode
|
|
191
|
+
|
|
192
|
+
For lower-latency runs, pass `--thinking low` explicitly:
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
/workflow run --thinking low deep-research "Research this repository and summarize the architecture tradeoffs."
|
|
196
|
+
/workflow dynamic --thinking low "Research this repository and summarize the architecture tradeoffs."
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
This is an opt-in fast mode. Package defaults remain conservative until a separate holdout evaluation provides enough evidence to change them. Current evidence is limited but encouraging for explicit fast runs: the 2026-07-02 `deep-research` combined gate on P1/P2/P3-style prompts resolved non-support tasks to `low`, completed selected valid runs in about 15-17 minutes, passed the strict gate 9/9, and had zero source-ref join failures across those 9 runs. Treat this as a speed option, not proof that every workflow should default to `low`.
|
|
200
|
+
|
|
190
201
|
### Run-scoped web-source cache
|
|
191
202
|
|
|
192
203
|
Prefer normalized workflow web tools in new workflows:
|