@tea-agent/loop-agent 0.1.0 → 0.2.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.
- package/CHANGELOG.md +17 -1
- package/README.md +127 -92
- package/dist/adapters/index.js +3 -2
- package/dist/adapters/loop-agent.js +44 -2
- package/dist/application/dag/args.js +420 -0
- package/dist/application/dag/generate-task-dag.js +280 -0
- package/dist/application/dag/report-dag.js +14 -0
- package/dist/application/dag/run-dag.js +93 -0
- package/dist/application/dag/validate-dag.js +101 -0
- package/dist/application/loop/run-action.js +23 -0
- package/dist/cli/catalog.js +2 -237
- package/dist/cli/command-definitions.js +571 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/program.js +65 -1
- package/dist/cli/router.js +13 -0
- package/dist/cli-governance/active-residue-check.js +38 -0
- package/dist/commands/dag-report.js +6 -107
- package/dist/commands/dag-run-task.js +8 -466
- package/dist/commands/dag-validate.js +7 -179
- package/dist/commands/examples.js +90 -0
- package/dist/commands/init.js +1495 -0
- package/dist/commands/loop.js +57 -31
- package/dist/commands/pi-prompt.js +2 -9
- package/dist/commands/run-dag.js +7 -180
- package/dist/executors/cursor-executor-artifacts.js +3 -4
- package/dist/executors/cursor-worker-client.js +13 -3
- package/dist/executors/dag-cursor-executor.js +2 -3
- package/dist/executors/dag-pi-executor.js +3 -4
- package/dist/executors/dag-static-executor.js +2 -5
- package/dist/executors/pi-defaults.js +9 -0
- package/dist/executors/shell-executor.js +12 -20
- package/dist/governance/manifest-types.js +1 -0
- package/dist/infrastructure/harness/active-residue-policy.js +73 -0
- package/dist/infrastructure/harness/artifact-store.js +72 -0
- package/dist/infrastructure/harness/atomic-write.js +49 -0
- package/dist/infrastructure/harness/completed-facts-guard.js +40 -0
- package/dist/infrastructure/harness/loop-action-store.js +23 -0
- package/dist/infrastructure/harness/loop-store.js +41 -0
- package/dist/infrastructure/harness/one-shot-run-store.js +94 -0
- package/dist/infrastructure/harness/task-store.js +77 -0
- package/dist/records/one-shot-runs.js +26 -61
- package/dist/records/promotion.js +3 -4
- package/dist/shared/artifacts-core.js +5 -5
- package/dist/shared/logger.js +9 -15
- package/dist/task/delegate.js +4 -4
- package/dist/task/runtime.js +5 -7
- package/dist/task/state.js +6 -20
- package/dist/workflows/dag/convergence/controller.js +277 -0
- package/dist/workflows/dag/dynamic-runtime/condition.js +48 -0
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +156 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +185 -0
- package/dist/workflows/dag/dynamic-runtime/reduction.js +72 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +133 -0
- package/dist/workflows/dag/lifecycle.js +6 -5
- package/dist/workflows/dag/node-execution.js +262 -0
- package/dist/workflows/dag/run-store.js +36 -0
- package/dist/workflows/dag/runner.js +82 -1341
- package/dist/workflows/dag/scheduler.js +84 -0
- package/dist/workflows/dag/upstream-artifacts.js +20 -18
- package/dist/workflows/loop/actions/cursor-fix.js +191 -0
- package/dist/workflows/loop/actions/dag-action.js +130 -0
- package/dist/workflows/loop/actions/pi-review.js +267 -0
- package/dist/workflows/loop/actions/shared.js +157 -0
- package/dist/workflows/loop/actions/shell-verify.js +82 -0
- package/dist/workflows/loop/actions/types.js +1 -0
- package/dist/workflows/loop/actions/workflow-action.js +255 -0
- package/dist/workflows/loop/actions.js +55 -1212
- package/dist/workflows/loop/closeout.js +5 -4
- package/dist/workflows/loop/context.js +2 -3
- package/dist/workflows/loop/events.js +3 -2
- package/dist/workflows/loop/policy/auto-policy.js +104 -0
- package/dist/workflows/loop/policy/cursor-fix-policy.js +31 -0
- package/dist/workflows/loop/rounds.js +3 -3
- package/dist/workflows/loop/signals.js +4 -7
- package/dist/workflows/loop/state.js +11 -11
- package/docs/README.md +3 -2
- package/docs/architecture/runtime-boundaries.md +147 -0
- package/docs/exec-plans/active/README.md +4 -0
- package/docs/exec-plans/completed/README.md +6 -2
- package/package.json +2 -1
- package/skills/ai-engineering-context/SKILL.md +21 -21
- package/skills/loop-agent/SKILL.md +73 -188
- package/skills/loop-agent/references/README.md +6 -2
- package/skills/loop-agent/references/harness-policy.md +113 -113
- package/skills/loop-agent/references/learned/README.md +13 -13
- package/skills/loop-agent/references/long-running-loop.md +59 -0
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +0 -2
- package/skills/loop-agent/references/verification-and-failure-handling.md +18 -0
- package/skills/requesting-code-review/SKILL.md +40 -40
- package/skills/requesting-code-review/code-reviewer.md +4 -4
- package/skills/systematic-debugging/CREATION-LOG.md +43 -43
- package/skills/systematic-debugging/SKILL.md +113 -113
- package/skills/systematic-debugging/condition-based-waiting.md +20 -20
- package/skills/systematic-debugging/defense-in-depth.md +27 -27
- package/skills/systematic-debugging/root-cause-tracing.md +38 -38
- package/skills/systematic-debugging/test-academic.md +6 -6
- package/skills/systematic-debugging/test-pressure-1.md +6 -6
- package/skills/systematic-debugging/test-pressure-2.md +2 -2
- package/skills/systematic-debugging/test-pressure-3.md +6 -6
- package/skills/verification-before-completion/SKILL.md +37 -37
|
@@ -1,30 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { access, appendFile, cp, mkdir, readdir, readFile, rm, writeFile, } from "node:fs/promises";
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, transferDagRunDir, writeDagRunState, } from "./lifecycle.js";
|
|
4
|
+
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
|
|
5
|
+
import { moveToCompletedRunDir, moveToPausedRunDir, prepareActiveRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
|
|
8
6
|
import { createDagNodeExecutor } from "./executor-registry.js";
|
|
9
7
|
import { executeDagCursorNode } from "../../executors/dag-cursor-executor.js";
|
|
10
|
-
import { executeDagPiNode } from "../../executors/dag-pi-executor.js";
|
|
11
8
|
import { assertValidDagSpec } from "./validate.js";
|
|
12
9
|
import { normalizeDagPromptSources } from "./prompt-source.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
10
|
+
import { relocateConvergenceArtifactPaths, relocateRunArtifactPaths, } from "./upstream-artifacts.js";
|
|
11
|
+
import { buildNodePrompt, buildNodePromptWithResolvedSkillInstructions, executeDagNode, } from "./node-execution.js";
|
|
12
|
+
import { runConvergencePassController, shouldEnableDagConvergence, } from "./convergence/controller.js";
|
|
13
|
+
import { executeDagRanksOnce, isConditionSkippedReason, } from "./scheduler.js";
|
|
17
14
|
import { topoSortToRanks } from "./topo.js";
|
|
18
|
-
import { parseDagSpec,
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
async function writeDagRunSpec(runDir, spec) {
|
|
26
|
-
await writeFile(path.join(runDir, "run.json"), `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
|
|
27
|
-
}
|
|
15
|
+
import { parseDagSpec, } from "./types.js";
|
|
16
|
+
import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
|
|
17
|
+
import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
|
|
18
|
+
import { executeDynamicMapExpansion } from "./dynamic-runtime/map.js";
|
|
19
|
+
import { executeDynamicReduction } from "./dynamic-runtime/reduction.js";
|
|
20
|
+
export { buildNodePrompt, buildNodePromptWithResolvedSkillInstructions };
|
|
28
21
|
export async function loadDagSpecFromFile(dagPath) {
|
|
29
22
|
const raw = JSON.parse(await readFile(dagPath, "utf-8"));
|
|
30
23
|
const normalized = await normalizeDagPromptSources(raw, path.dirname(dagPath));
|
|
@@ -108,33 +101,6 @@ export async function buildDagRunId(spec, options) {
|
|
|
108
101
|
const existing = await listExistingDagRunIds(options.cwd);
|
|
109
102
|
return collisionSafeDagRunId(baseId, existing, now);
|
|
110
103
|
}
|
|
111
|
-
export function buildNodePrompt(spec, task, upstream) {
|
|
112
|
-
return buildDagNodePromptEnvelope({
|
|
113
|
-
spec,
|
|
114
|
-
task,
|
|
115
|
-
upstream,
|
|
116
|
-
resolvedSkills: resolveDagNodeSkills(spec, task),
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd) {
|
|
120
|
-
const skillNames = resolveDagNodeSkills(spec, task);
|
|
121
|
-
const resolvedSkillInstructions = task.executor === "cursor" || task.executor === "pi"
|
|
122
|
-
? await resolveDagSkillInstructions(skillNames, {
|
|
123
|
-
cwd,
|
|
124
|
-
includeLearnedPatterns: task.role === "implementer",
|
|
125
|
-
})
|
|
126
|
-
: [];
|
|
127
|
-
return {
|
|
128
|
-
prompt: buildDagNodePromptEnvelope({
|
|
129
|
-
spec,
|
|
130
|
-
task,
|
|
131
|
-
upstream,
|
|
132
|
-
resolvedSkills: skillNames,
|
|
133
|
-
resolvedSkillInstructions,
|
|
134
|
-
}),
|
|
135
|
-
resolvedSkills: skillInstructionMetadata(resolvedSkillInstructions),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
104
|
export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "dag") {
|
|
139
105
|
const nodes = {};
|
|
140
106
|
for (const task of spec.tasks) {
|
|
@@ -184,9 +150,9 @@ export async function runDag(spec, opts) {
|
|
|
184
150
|
const completedRunDir = getDagRunDir(opts.cwd, "completed", state.runId);
|
|
185
151
|
const pausedRunDir = getDagRunDir(opts.cwd, "paused", state.runId);
|
|
186
152
|
const runDir = activeRunDir;
|
|
187
|
-
await
|
|
188
|
-
await
|
|
189
|
-
await
|
|
153
|
+
await prepareActiveRunDir(runDir);
|
|
154
|
+
await writeRunSpec(runDir, spec);
|
|
155
|
+
await writeRunState(runDir, state);
|
|
190
156
|
await notifyRunObserver(opts.observer, "onRunStart", state);
|
|
191
157
|
if (opts.dryRun || opts.initOnly) {
|
|
192
158
|
return {
|
|
@@ -273,8 +239,8 @@ async function executeDagCheckpoint(input) {
|
|
|
273
239
|
const { spec, state, ranks, runDir: initialRunDir, cwd, maxConcurrent, completedRunDir, pausedRunDir, } = input;
|
|
274
240
|
let runDir = initialRunDir;
|
|
275
241
|
let stateWriteQueue = Promise.resolve();
|
|
276
|
-
const persistState = async () => {
|
|
277
|
-
stateWriteQueue = stateWriteQueue.then(() =>
|
|
242
|
+
const persistState = async (options) => {
|
|
243
|
+
stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, state, options));
|
|
278
244
|
await stateWriteQueue;
|
|
279
245
|
};
|
|
280
246
|
const tasksById = new Map(spec.tasks.map((task) => [task.id, task]));
|
|
@@ -284,20 +250,65 @@ async function executeDagCheckpoint(input) {
|
|
|
284
250
|
runId: state.runId,
|
|
285
251
|
spec,
|
|
286
252
|
});
|
|
253
|
+
const executeDynamicNode = async (dynamicInput) => {
|
|
254
|
+
const { task } = dynamicInput;
|
|
255
|
+
if (task.dynamicExpansion) {
|
|
256
|
+
return executeDynamicMapExpansion({
|
|
257
|
+
...dynamicInput,
|
|
258
|
+
expansion: task.dynamicExpansion,
|
|
259
|
+
executeDynamicNode,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (task.dynamicCondition) {
|
|
263
|
+
return executeDynamicCondition({
|
|
264
|
+
task,
|
|
265
|
+
condition: task.dynamicCondition,
|
|
266
|
+
tasksById: dynamicInput.tasksById,
|
|
267
|
+
state: dynamicInput.state,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (task.dynamicLoopUntil) {
|
|
271
|
+
return executeDynamicLoopUntil({
|
|
272
|
+
...dynamicInput,
|
|
273
|
+
loop: task.dynamicLoopUntil,
|
|
274
|
+
executeDynamicNode,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return executeDynamicReduction({
|
|
278
|
+
task,
|
|
279
|
+
reduction: task.dynamicReduction,
|
|
280
|
+
state: dynamicInput.state,
|
|
281
|
+
runDir: dynamicInput.runDir,
|
|
282
|
+
});
|
|
283
|
+
};
|
|
287
284
|
let pausedByNodeId;
|
|
288
285
|
while (true) {
|
|
289
286
|
pausedByNodeId = await executeDagRanksOnce({
|
|
290
|
-
spec,
|
|
291
287
|
state,
|
|
292
288
|
ranks,
|
|
293
289
|
tasksById,
|
|
294
|
-
cwd,
|
|
295
|
-
runDir,
|
|
296
290
|
maxConcurrent,
|
|
297
|
-
baseExecuteNode,
|
|
298
|
-
customExecuteNode: input.executeNode,
|
|
299
|
-
observer: input.observer,
|
|
300
291
|
persistState,
|
|
292
|
+
createExecuteNodeForRank: (rankCursorNodeIds) => buildRankAwareExecuteNode({
|
|
293
|
+
baseExecuteNode,
|
|
294
|
+
customExecuteNode: input.executeNode,
|
|
295
|
+
rankCursorNodeIds,
|
|
296
|
+
tasksById,
|
|
297
|
+
meta: { runDir, runId: state.runId, spec },
|
|
298
|
+
}),
|
|
299
|
+
executeScheduledNode: (nodeId, executeNode, onPause) => executeDagNode({
|
|
300
|
+
nodeId,
|
|
301
|
+
tasksById,
|
|
302
|
+
state,
|
|
303
|
+
spec,
|
|
304
|
+
cwd,
|
|
305
|
+
runDir,
|
|
306
|
+
executeNode,
|
|
307
|
+
executeDynamicNode,
|
|
308
|
+
observer: input.observer,
|
|
309
|
+
persistState,
|
|
310
|
+
onPause,
|
|
311
|
+
}),
|
|
301
312
|
});
|
|
302
313
|
if (pausedByNodeId) {
|
|
303
314
|
break;
|
|
@@ -325,21 +336,23 @@ async function executeDagCheckpoint(input) {
|
|
|
325
336
|
state.status = "paused";
|
|
326
337
|
await persistState();
|
|
327
338
|
await notifyRunObserver(input.observer, "onRunFinish", state);
|
|
328
|
-
runDir = await
|
|
339
|
+
runDir = await moveToPausedRunDir(runDir, pausedRunDir);
|
|
329
340
|
}
|
|
330
341
|
else {
|
|
331
342
|
finalizeTerminalRunStatus(state, spec.tasks.length);
|
|
332
343
|
await persistState();
|
|
333
344
|
await notifyRunObserver(input.observer, "onRunFinish", state);
|
|
334
|
-
runDir = await
|
|
345
|
+
runDir = await moveToCompletedRunDir(runDir, completedRunDir);
|
|
335
346
|
}
|
|
336
347
|
await relocateRunArtifactPaths({
|
|
337
348
|
runDir,
|
|
338
349
|
oldRunDir: runDirBeforeTransfer,
|
|
339
350
|
nodes: state.nodes,
|
|
340
351
|
});
|
|
341
|
-
|
|
342
|
-
|
|
352
|
+
if (state.convergence) {
|
|
353
|
+
relocateConvergenceArtifactPaths(state.convergence, runDirBeforeTransfer, runDir);
|
|
354
|
+
}
|
|
355
|
+
await persistState({ allowCompletedFactsWrite: true });
|
|
343
356
|
return {
|
|
344
357
|
title: spec.title,
|
|
345
358
|
runId: state.runId,
|
|
@@ -349,1223 +362,6 @@ async function executeDagCheckpoint(input) {
|
|
|
349
362
|
runDir,
|
|
350
363
|
};
|
|
351
364
|
}
|
|
352
|
-
async function executeDagRanksOnce(input) {
|
|
353
|
-
let pausedByNodeId;
|
|
354
|
-
for (const rank of input.ranks) {
|
|
355
|
-
const runnable = rank.filter((id) => {
|
|
356
|
-
const node = input.state.nodes[id];
|
|
357
|
-
const task = input.tasksById.get(id);
|
|
358
|
-
if (!node || node.status !== "PENDING")
|
|
359
|
-
return false;
|
|
360
|
-
return !shouldSkipNode(node, task, input.state.nodes);
|
|
361
|
-
});
|
|
362
|
-
const blocked = rank.filter((id) => {
|
|
363
|
-
const node = input.state.nodes[id];
|
|
364
|
-
const task = input.tasksById.get(id);
|
|
365
|
-
return (node?.status === "PENDING" &&
|
|
366
|
-
shouldSkipNode(node, task, input.state.nodes));
|
|
367
|
-
});
|
|
368
|
-
for (const id of blocked) {
|
|
369
|
-
const node = input.state.nodes[id];
|
|
370
|
-
node.status = "SKIPPED";
|
|
371
|
-
node.skippedReason = conditionSkippedByAncestor(input.tasksById.get(id), input.state.nodes)
|
|
372
|
-
? "condition ancestor branch not selected"
|
|
373
|
-
: "upstream dependency failed or was skipped";
|
|
374
|
-
}
|
|
375
|
-
if (blocked.length > 0) {
|
|
376
|
-
await input.persistState();
|
|
377
|
-
}
|
|
378
|
-
const pauseGateRunnable = runnable.filter((id) => {
|
|
379
|
-
const task = input.tasksById.get(id);
|
|
380
|
-
return isPauseOnHumanDecisionGate(task);
|
|
381
|
-
});
|
|
382
|
-
const regularRunnable = runnable.filter((id) => !pauseGateRunnable.includes(id));
|
|
383
|
-
const rankWriteGuardedNodeIds = runnable.filter((id) => {
|
|
384
|
-
const task = input.tasksById.get(id);
|
|
385
|
-
return task ? isWriteGuardedAgentExecutor(task) : false;
|
|
386
|
-
});
|
|
387
|
-
const executeNode = buildRankAwareExecuteNode({
|
|
388
|
-
baseExecuteNode: input.baseExecuteNode,
|
|
389
|
-
customExecuteNode: input.customExecuteNode,
|
|
390
|
-
rankWriteGuardedNodeIds,
|
|
391
|
-
tasksById: input.tasksById,
|
|
392
|
-
meta: {
|
|
393
|
-
runDir: input.runDir,
|
|
394
|
-
runId: input.state.runId,
|
|
395
|
-
spec: input.spec,
|
|
396
|
-
},
|
|
397
|
-
});
|
|
398
|
-
for (const nodeId of pauseGateRunnable) {
|
|
399
|
-
await executeDagNode({
|
|
400
|
-
nodeId,
|
|
401
|
-
tasksById: input.tasksById,
|
|
402
|
-
state: input.state,
|
|
403
|
-
spec: input.spec,
|
|
404
|
-
cwd: input.cwd,
|
|
405
|
-
runDir: input.runDir,
|
|
406
|
-
executeNode,
|
|
407
|
-
observer: input.observer,
|
|
408
|
-
persistState: input.persistState,
|
|
409
|
-
onPause: (id, pausedAt, pauseReason) => {
|
|
410
|
-
pausedByNodeId = id;
|
|
411
|
-
input.state.pausedAt = pausedAt;
|
|
412
|
-
input.state.pausedByNodeId = id;
|
|
413
|
-
input.state.pauseReason = pauseReason;
|
|
414
|
-
},
|
|
415
|
-
});
|
|
416
|
-
if (pausedByNodeId)
|
|
417
|
-
break;
|
|
418
|
-
}
|
|
419
|
-
if (pausedByNodeId)
|
|
420
|
-
break;
|
|
421
|
-
await mapConcurrent(regularRunnable, input.maxConcurrent, async (nodeId) => {
|
|
422
|
-
await executeDagNode({
|
|
423
|
-
nodeId,
|
|
424
|
-
tasksById: input.tasksById,
|
|
425
|
-
state: input.state,
|
|
426
|
-
spec: input.spec,
|
|
427
|
-
cwd: input.cwd,
|
|
428
|
-
runDir: input.runDir,
|
|
429
|
-
executeNode,
|
|
430
|
-
observer: input.observer,
|
|
431
|
-
persistState: input.persistState,
|
|
432
|
-
onPause: (id, pausedAt, pauseReason) => {
|
|
433
|
-
pausedByNodeId = id;
|
|
434
|
-
input.state.pausedAt = pausedAt;
|
|
435
|
-
input.state.pausedByNodeId = id;
|
|
436
|
-
input.state.pauseReason = pauseReason;
|
|
437
|
-
},
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
if (pausedByNodeId)
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
|
-
return pausedByNodeId;
|
|
444
|
-
}
|
|
445
|
-
function convergenceChainNodeIds(tasksById) {
|
|
446
|
-
const repairId = tasksById.has("repair-pi") ? "repair-pi" : "repair-cursor";
|
|
447
|
-
return [
|
|
448
|
-
"process-supervisor-pi",
|
|
449
|
-
"process-gate-shell",
|
|
450
|
-
repairId,
|
|
451
|
-
"hard-verify-shell",
|
|
452
|
-
];
|
|
453
|
-
}
|
|
454
|
-
const CONVERGENCE_NON_RETRY_FAILURES = new Set([
|
|
455
|
-
"timeout",
|
|
456
|
-
"spawn-error",
|
|
457
|
-
"write-guard",
|
|
458
|
-
"missing-api-key",
|
|
459
|
-
"auth",
|
|
460
|
-
"human-rejected",
|
|
461
|
-
"decision-gate-requires-human",
|
|
462
|
-
]);
|
|
463
|
-
function shouldEnableDagConvergence(spec) {
|
|
464
|
-
return (process.env.HARNESS_DAG_CONVERGENCE !== "off" &&
|
|
465
|
-
spec.convergence?.enabled === true);
|
|
466
|
-
}
|
|
467
|
-
async function runConvergencePassController(input) {
|
|
468
|
-
const convergence = input.state.convergence;
|
|
469
|
-
if (!convergence?.enabled)
|
|
470
|
-
return { retry: false };
|
|
471
|
-
if (process.env.HARNESS_DAG_CONVERGENCE === "off") {
|
|
472
|
-
convergence.terminalReason = "feature-flag-off";
|
|
473
|
-
return { retry: false };
|
|
474
|
-
}
|
|
475
|
-
if (!hasConvergenceChain(input.tasksById)) {
|
|
476
|
-
convergence.terminalReason = "unsupported-dag-shape";
|
|
477
|
-
return { retry: false };
|
|
478
|
-
}
|
|
479
|
-
const hardVerify = input.state.nodes["hard-verify-shell"];
|
|
480
|
-
if (!hardVerify)
|
|
481
|
-
return { retry: false };
|
|
482
|
-
if (hardVerify.status === "FINISHED") {
|
|
483
|
-
convergence.terminalReason = "hard-verify-pass";
|
|
484
|
-
await appendConvergenceKnowledgePattern({
|
|
485
|
-
cwd: input.cwd,
|
|
486
|
-
state: input.state,
|
|
487
|
-
});
|
|
488
|
-
return { retry: false };
|
|
489
|
-
}
|
|
490
|
-
if (hardVerify.status !== "ERROR")
|
|
491
|
-
return { retry: false };
|
|
492
|
-
const currentPass = convergence.currentPass || 1;
|
|
493
|
-
const hardFailure = hardVerify.failureCategory ?? "unknown";
|
|
494
|
-
const passRecord = await buildConvergencePassRecord({
|
|
495
|
-
pass: currentPass,
|
|
496
|
-
status: "retrying",
|
|
497
|
-
reason: "hard-verify-failed",
|
|
498
|
-
state: input.state,
|
|
499
|
-
runDir: input.runDir,
|
|
500
|
-
});
|
|
501
|
-
if (CONVERGENCE_NON_RETRY_FAILURES.has(hardFailure)) {
|
|
502
|
-
passRecord.status = "terminal";
|
|
503
|
-
passRecord.reason = "non-retry-failure";
|
|
504
|
-
convergence.passHistory.push(passRecord);
|
|
505
|
-
convergence.terminalReason = "non-retry-failure";
|
|
506
|
-
await input.persistState();
|
|
507
|
-
return { retry: false };
|
|
508
|
-
}
|
|
509
|
-
if (input.spec.convergence?.pauseOnRegression !== false &&
|
|
510
|
-
detectConvergenceRegression(convergence.passHistory, passRecord)) {
|
|
511
|
-
passRecord.status = "paused";
|
|
512
|
-
passRecord.reason = "regression";
|
|
513
|
-
convergence.passHistory.push(passRecord);
|
|
514
|
-
convergence.terminalReason = "regression";
|
|
515
|
-
const pausedAt = new Date().toISOString();
|
|
516
|
-
input.state.status = "paused";
|
|
517
|
-
input.state.pausedAt = pausedAt;
|
|
518
|
-
input.state.pausedByNodeId = "hard-verify-shell";
|
|
519
|
-
input.state.pauseReason = "convergence-regression";
|
|
520
|
-
input.state.failureCategory = "convergence-regression";
|
|
521
|
-
await input.persistState();
|
|
522
|
-
return { retry: false, pausedByNodeId: "hard-verify-shell" };
|
|
523
|
-
}
|
|
524
|
-
if (currentPass >= convergence.maxPasses) {
|
|
525
|
-
passRecord.status = "terminal";
|
|
526
|
-
passRecord.reason = "max-passes";
|
|
527
|
-
convergence.passHistory.push(passRecord);
|
|
528
|
-
convergence.terminalReason = "max-passes";
|
|
529
|
-
await input.persistState();
|
|
530
|
-
return { retry: false };
|
|
531
|
-
}
|
|
532
|
-
convergence.passHistory.push(passRecord);
|
|
533
|
-
convergence.currentPass = currentPass + 1;
|
|
534
|
-
await resetConvergenceNodesForNextPass({
|
|
535
|
-
spec: input.spec,
|
|
536
|
-
state: input.state,
|
|
537
|
-
tasksById: input.tasksById,
|
|
538
|
-
});
|
|
539
|
-
await input.persistState();
|
|
540
|
-
return { retry: true };
|
|
541
|
-
}
|
|
542
|
-
function hasConvergenceChain(tasksById) {
|
|
543
|
-
return convergenceChainNodeIds(tasksById).every((id) => tasksById.has(id));
|
|
544
|
-
}
|
|
545
|
-
async function buildConvergencePassRecord(input) {
|
|
546
|
-
const hardVerify = input.state.nodes["hard-verify-shell"];
|
|
547
|
-
const processSupervisor = input.state.nodes["process-supervisor-pi"];
|
|
548
|
-
const verifyEvidence = hardVerify?.verifyEvidence;
|
|
549
|
-
return {
|
|
550
|
-
pass: input.pass,
|
|
551
|
-
startedAt: hardVerify?.startedAt,
|
|
552
|
-
finishedAt: new Date().toISOString(),
|
|
553
|
-
status: input.status,
|
|
554
|
-
reason: input.reason,
|
|
555
|
-
hardVerifyStatus: hardVerify?.status,
|
|
556
|
-
hardVerifyFailureCategory: hardVerify?.failureCategory,
|
|
557
|
-
processVerdict: parseProcessVerdict(processSupervisor),
|
|
558
|
-
repairArtifact: processSupervisor?.repairArtifact,
|
|
559
|
-
verifyPhase: verifyEvidence?.phase,
|
|
560
|
-
verifyQuota: verifyEvidence?.quota,
|
|
561
|
-
verifyCommandCount: verifyEvidence?.commandCount,
|
|
562
|
-
verifyCommandLabels: verifyEvidence?.commandLabels,
|
|
563
|
-
shellSuccessCount: countSuccessfulShellCommands(hardVerify?.stdout),
|
|
564
|
-
artifactRefs: await preserveConvergencePassArtifacts({
|
|
565
|
-
pass: input.pass,
|
|
566
|
-
state: input.state,
|
|
567
|
-
runDir: input.runDir,
|
|
568
|
-
}),
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
async function preserveConvergencePassArtifacts(input) {
|
|
572
|
-
const passDir = path.join(input.runDir, "convergence", `pass-${input.pass}`);
|
|
573
|
-
await mkdir(passDir, { recursive: true });
|
|
574
|
-
const nodeIds = nodesToPreserveForConvergence(input.state);
|
|
575
|
-
const refs = [];
|
|
576
|
-
for (const nodeId of nodeIds) {
|
|
577
|
-
const node = input.state.nodes[nodeId];
|
|
578
|
-
if (!node || node.status === "PENDING")
|
|
579
|
-
continue;
|
|
580
|
-
const preservedNodeRecordPath = path.join(passDir, `${nodeId}.json`);
|
|
581
|
-
const preservedNodeDir = path.join(passDir, nodeId);
|
|
582
|
-
await copyIfExists(path.join(input.runDir, `${nodeId}.json`), preservedNodeRecordPath);
|
|
583
|
-
await copyIfExists(path.join(input.runDir, nodeId), preservedNodeDir);
|
|
584
|
-
refs.push({
|
|
585
|
-
nodeId,
|
|
586
|
-
status: node.status,
|
|
587
|
-
failureCategory: node.failureCategory,
|
|
588
|
-
nodeRecordPath: node.nodeRecordPath ?? path.join(input.runDir, `${nodeId}.json`),
|
|
589
|
-
stdoutArtifactPath: node.stdoutArtifactPath,
|
|
590
|
-
assistantArtifactPath: node.assistantArtifactPath,
|
|
591
|
-
preservedNodeRecordPath,
|
|
592
|
-
preservedNodeDir,
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
return refs;
|
|
596
|
-
}
|
|
597
|
-
function nodesToPreserveForConvergence(state) {
|
|
598
|
-
const repairId = state.nodes["repair-pi"] ? "repair-pi" : "repair-cursor";
|
|
599
|
-
return [
|
|
600
|
-
"process-supervisor-pi",
|
|
601
|
-
"process-gate-shell",
|
|
602
|
-
repairId,
|
|
603
|
-
"hard-verify-shell",
|
|
604
|
-
].filter((nodeId) => state.nodes[nodeId]);
|
|
605
|
-
}
|
|
606
|
-
async function copyIfExists(from, to) {
|
|
607
|
-
try {
|
|
608
|
-
await access(from);
|
|
609
|
-
await cp(from, to, { recursive: true, force: true });
|
|
610
|
-
}
|
|
611
|
-
catch {
|
|
612
|
-
// Missing artifacts are represented in passHistory by absent files.
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
function parseProcessVerdict(node) {
|
|
616
|
-
const text = `${node?.assistantText ?? ""}\n${node?.stdout ?? ""}`;
|
|
617
|
-
for (const line of text.split("\n")) {
|
|
618
|
-
const trimmed = line.trim();
|
|
619
|
-
if (trimmed === "VERDICT: pass")
|
|
620
|
-
return "pass";
|
|
621
|
-
if (trimmed === "VERDICT: request-revision")
|
|
622
|
-
return "request-revision";
|
|
623
|
-
}
|
|
624
|
-
return "unknown";
|
|
625
|
-
}
|
|
626
|
-
function assertRepairArtifactVerdictMatchesSupervisor(input) {
|
|
627
|
-
const supervisorVerdict = parseProcessVerdict(input.node);
|
|
628
|
-
if (supervisorVerdict === "unknown")
|
|
629
|
-
return;
|
|
630
|
-
if (supervisorVerdict !== input.artifactVerdict) {
|
|
631
|
-
throw new Error(`repair artifact gate failed: verdict mismatch between supervisor ${supervisorVerdict} and REPAIR_ARTIFACT_JSON ${input.artifactVerdict}`);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
function countSuccessfulShellCommands(stdout) {
|
|
635
|
-
if (!stdout)
|
|
636
|
-
return undefined;
|
|
637
|
-
const matches = stdout.match(/\|\s*\d+\s*\|\s*true\s*\|/g);
|
|
638
|
-
if (matches)
|
|
639
|
-
return matches.length;
|
|
640
|
-
if (/\|\s*\d+\s*\|\s*false\s*\|/.test(stdout))
|
|
641
|
-
return 0;
|
|
642
|
-
const explicit = stdout.match(/(?:passed|success(?:es)?|ok)\s*[=:]\s*(\d+)/i);
|
|
643
|
-
return explicit ? Number.parseInt(explicit[1], 10) : undefined;
|
|
644
|
-
}
|
|
645
|
-
function detectConvergenceRegression(history, current) {
|
|
646
|
-
const previous = [...history]
|
|
647
|
-
.reverse()
|
|
648
|
-
.find((pass) => pass.shellSuccessCount !== undefined);
|
|
649
|
-
if (previous?.shellSuccessCount !== undefined &&
|
|
650
|
-
current.shellSuccessCount !== undefined &&
|
|
651
|
-
current.shellSuccessCount < previous.shellSuccessCount) {
|
|
652
|
-
return true;
|
|
653
|
-
}
|
|
654
|
-
return isWorseFailureCategory(previous?.hardVerifyFailureCategory, current.hardVerifyFailureCategory);
|
|
655
|
-
}
|
|
656
|
-
function isWorseFailureCategory(previous, current) {
|
|
657
|
-
const severity = new Map([
|
|
658
|
-
["success", 0],
|
|
659
|
-
["nonzero-exit", 1],
|
|
660
|
-
["unknown", 2],
|
|
661
|
-
["timeout", 3],
|
|
662
|
-
["spawn-error", 3],
|
|
663
|
-
["write-guard", 4],
|
|
664
|
-
]);
|
|
665
|
-
if (!previous || !current)
|
|
666
|
-
return false;
|
|
667
|
-
return (severity.get(current) ?? 2) > (severity.get(previous) ?? 2);
|
|
668
|
-
}
|
|
669
|
-
async function appendConvergenceKnowledgePattern(input) {
|
|
670
|
-
const convergence = input.state.convergence;
|
|
671
|
-
if (!convergence || convergence.passHistory.length === 0)
|
|
672
|
-
return;
|
|
673
|
-
const supervisor = input.state.nodes["process-supervisor-pi"];
|
|
674
|
-
const hardVerify = input.state.nodes["hard-verify-shell"];
|
|
675
|
-
const pattern = {
|
|
676
|
-
schemaVersion: 1,
|
|
677
|
-
type: "dag-convergence-repair",
|
|
678
|
-
runId: input.state.runId,
|
|
679
|
-
recordedAt: new Date().toISOString(),
|
|
680
|
-
passCount: convergence.currentPass,
|
|
681
|
-
terminalReason: convergence.terminalReason ?? "hard-verify-pass",
|
|
682
|
-
processVerdict: parseProcessVerdict(supervisor),
|
|
683
|
-
repairArtifact: supervisor?.repairArtifact,
|
|
684
|
-
supervisorStructuredBlock: extractSupervisorStructuredBlock(supervisor),
|
|
685
|
-
hardVerify: {
|
|
686
|
-
status: hardVerify?.status,
|
|
687
|
-
failureCategory: hardVerify?.failureCategory,
|
|
688
|
-
nodeRecordPath: hardVerify?.nodeRecordPath,
|
|
689
|
-
},
|
|
690
|
-
passHistory: convergence.passHistory.map((pass) => ({
|
|
691
|
-
pass: pass.pass,
|
|
692
|
-
reason: pass.reason,
|
|
693
|
-
hardVerifyFailureCategory: pass.hardVerifyFailureCategory,
|
|
694
|
-
shellSuccessCount: pass.shellSuccessCount,
|
|
695
|
-
})),
|
|
696
|
-
};
|
|
697
|
-
const knowledgeDir = path.join(input.cwd, ".harness", "knowledge");
|
|
698
|
-
await mkdir(knowledgeDir, { recursive: true });
|
|
699
|
-
await appendFile(path.join(knowledgeDir, "patterns.jsonl"), `${JSON.stringify(pattern)}\n`, "utf-8");
|
|
700
|
-
}
|
|
701
|
-
function extractSupervisorStructuredBlock(node) {
|
|
702
|
-
// Legacy compatibility only. New supervisor prompts must emit REPAIR_ARTIFACT_JSON.
|
|
703
|
-
const text = `${node?.assistantText ?? ""}\n${node?.stdout ?? ""}`;
|
|
704
|
-
const block = {};
|
|
705
|
-
for (const key of ["FAILURE_CLASS", "FIX_SCOPE", "INVARIANT"]) {
|
|
706
|
-
const match = text.match(new RegExp(`^${key}:\\s*(.+)$`, "im"));
|
|
707
|
-
if (match?.[1])
|
|
708
|
-
block[key] = match[1].trim();
|
|
709
|
-
}
|
|
710
|
-
return block;
|
|
711
|
-
}
|
|
712
|
-
async function resetConvergenceNodesForNextPass(input) {
|
|
713
|
-
const resetIds = new Set(convergenceChainNodeIds(input.tasksById));
|
|
714
|
-
for (const id of collectTransitiveDescendantTaskIds(input.spec, "hard-verify-shell")) {
|
|
715
|
-
const node = input.state.nodes[id];
|
|
716
|
-
if (node?.status === "SKIPPED")
|
|
717
|
-
resetIds.add(id);
|
|
718
|
-
}
|
|
719
|
-
for (const id of resetIds) {
|
|
720
|
-
const task = input.tasksById.get(id);
|
|
721
|
-
if (!task)
|
|
722
|
-
continue;
|
|
723
|
-
input.state.nodes[id] = freshNodeRecord(task);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
function collectTransitiveDescendantTaskIds(spec, rootNodeId) {
|
|
727
|
-
const childrenByParent = new Map();
|
|
728
|
-
for (const task of spec.tasks) {
|
|
729
|
-
for (const parent of task.depends_on) {
|
|
730
|
-
const children = childrenByParent.get(parent) ?? [];
|
|
731
|
-
children.push(task.id);
|
|
732
|
-
childrenByParent.set(parent, children);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
const descendants = new Set();
|
|
736
|
-
const queue = [...(childrenByParent.get(rootNodeId) ?? [])];
|
|
737
|
-
while (queue.length > 0) {
|
|
738
|
-
const id = queue.shift();
|
|
739
|
-
if (descendants.has(id))
|
|
740
|
-
continue;
|
|
741
|
-
descendants.add(id);
|
|
742
|
-
queue.push(...(childrenByParent.get(id) ?? []));
|
|
743
|
-
}
|
|
744
|
-
return descendants;
|
|
745
|
-
}
|
|
746
|
-
function freshNodeRecord(task) {
|
|
747
|
-
return {
|
|
748
|
-
id: task.id,
|
|
749
|
-
status: "PENDING",
|
|
750
|
-
executor: task.executor,
|
|
751
|
-
complexity: task.complexity,
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
function relocateConvergenceArtifactPaths(state, oldRunDir, newRunDir) {
|
|
755
|
-
const convergence = state.convergence;
|
|
756
|
-
if (!convergence || oldRunDir === newRunDir)
|
|
757
|
-
return;
|
|
758
|
-
const relocate = (value) => value?.startsWith(oldRunDir)
|
|
759
|
-
? path.join(newRunDir, path.relative(oldRunDir, value))
|
|
760
|
-
: value;
|
|
761
|
-
for (const pass of convergence.passHistory) {
|
|
762
|
-
for (const ref of pass.artifactRefs) {
|
|
763
|
-
ref.nodeRecordPath = relocate(ref.nodeRecordPath);
|
|
764
|
-
ref.stdoutArtifactPath = relocate(ref.stdoutArtifactPath);
|
|
765
|
-
ref.assistantArtifactPath = relocate(ref.assistantArtifactPath);
|
|
766
|
-
ref.preservedNodeRecordPath = relocate(ref.preservedNodeRecordPath);
|
|
767
|
-
ref.preservedNodeDir = relocate(ref.preservedNodeDir);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
function findRepairTaskForGate(input) {
|
|
772
|
-
return Array.from(input.tasksById.values()).find((candidate) => candidate.depends_on.includes(input.gateTask.id) &&
|
|
773
|
-
candidate.role === "implementer" &&
|
|
774
|
-
candidate.writePolicy === "exclusive" &&
|
|
775
|
-
(candidate.executor === "cursor" ||
|
|
776
|
-
(candidate.executor === "pi" && candidate.toolProfile === "write")));
|
|
777
|
-
}
|
|
778
|
-
function parseSupervisorRepairArtifact(node) {
|
|
779
|
-
const text = `${node?.assistantText ?? ""}\n${node?.stdout ?? ""}`;
|
|
780
|
-
return parseRepairArtifactFromText(text);
|
|
781
|
-
}
|
|
782
|
-
function validateRepairArtifactGateBeforeShell(input) {
|
|
783
|
-
const gate = input.task.shell?.repairArtifactGate;
|
|
784
|
-
if (!gate)
|
|
785
|
-
return;
|
|
786
|
-
const upstream = input.state.nodes[gate.fromNodeId];
|
|
787
|
-
const parsed = parseSupervisorRepairArtifact(upstream);
|
|
788
|
-
if (!parsed.ok) {
|
|
789
|
-
throw new Error(`repair artifact gate failed: ${parsed.reason}`);
|
|
790
|
-
}
|
|
791
|
-
assertRepairArtifactVerdictMatchesSupervisor({
|
|
792
|
-
node: upstream,
|
|
793
|
-
artifactVerdict: parsed.artifact.verdict,
|
|
794
|
-
});
|
|
795
|
-
upstream.repairArtifact = parsed.artifact;
|
|
796
|
-
const scoped = validateRepairArtifactScope({
|
|
797
|
-
artifact: parsed.artifact,
|
|
798
|
-
repairTask: findRepairTaskForGate({
|
|
799
|
-
tasksById: input.tasksById,
|
|
800
|
-
gateTask: input.task,
|
|
801
|
-
}),
|
|
802
|
-
});
|
|
803
|
-
if (!scoped.ok) {
|
|
804
|
-
throw new Error(`repair artifact gate failed: ${scoped.reason}`);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
function recordRepairArtifactForSupervisorNode(input) {
|
|
808
|
-
const dependentGate = Object.values(input.state.nodes).find((record) => record.id === "process-gate-shell");
|
|
809
|
-
if (input.task.id !== "process-supervisor-pi" || !dependentGate)
|
|
810
|
-
return;
|
|
811
|
-
const parsed = parseSupervisorRepairArtifact(input.node);
|
|
812
|
-
if (parsed.ok) {
|
|
813
|
-
input.node.repairArtifact = parsed.artifact;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
async function executeDagNode(input) {
|
|
817
|
-
const { nodeId, tasksById, state, spec, cwd, runDir, executeNode } = input;
|
|
818
|
-
const task = tasksById.get(nodeId);
|
|
819
|
-
const node = state.nodes[nodeId];
|
|
820
|
-
node.status = "RUNNING";
|
|
821
|
-
node.startedAt = new Date().toISOString();
|
|
822
|
-
if (task.shell?.verifyEvidence) {
|
|
823
|
-
node.verifyEvidence = task.shell.verifyEvidence;
|
|
824
|
-
}
|
|
825
|
-
await input.persistState();
|
|
826
|
-
await notifyNodeObserver(input.observer, "onNodeStart", nodeId, state);
|
|
827
|
-
if (task.dynamicExpansion ||
|
|
828
|
-
task.dynamicReduction ||
|
|
829
|
-
task.dynamicCondition ||
|
|
830
|
-
task.dynamicLoopUntil) {
|
|
831
|
-
const started = Date.now();
|
|
832
|
-
try {
|
|
833
|
-
const result = task.dynamicExpansion
|
|
834
|
-
? await executeDynamicMapExpansion({
|
|
835
|
-
task,
|
|
836
|
-
expansion: task.dynamicExpansion,
|
|
837
|
-
tasksById,
|
|
838
|
-
state,
|
|
839
|
-
spec,
|
|
840
|
-
cwd,
|
|
841
|
-
runDir,
|
|
842
|
-
executeNode,
|
|
843
|
-
observer: input.observer,
|
|
844
|
-
persistState: input.persistState,
|
|
845
|
-
})
|
|
846
|
-
: task.dynamicCondition
|
|
847
|
-
? await executeDynamicCondition({
|
|
848
|
-
task,
|
|
849
|
-
condition: task.dynamicCondition,
|
|
850
|
-
tasksById,
|
|
851
|
-
state,
|
|
852
|
-
})
|
|
853
|
-
: task.dynamicLoopUntil
|
|
854
|
-
? await executeDynamicLoopUntil({
|
|
855
|
-
task,
|
|
856
|
-
loop: task.dynamicLoopUntil,
|
|
857
|
-
tasksById,
|
|
858
|
-
state,
|
|
859
|
-
spec,
|
|
860
|
-
cwd,
|
|
861
|
-
runDir,
|
|
862
|
-
executeNode,
|
|
863
|
-
observer: input.observer,
|
|
864
|
-
persistState: input.persistState,
|
|
865
|
-
})
|
|
866
|
-
: await executeDynamicReduction({
|
|
867
|
-
task,
|
|
868
|
-
reduction: task.dynamicReduction,
|
|
869
|
-
state,
|
|
870
|
-
runDir,
|
|
871
|
-
});
|
|
872
|
-
node.durationMs = result.durationMs ?? Date.now() - started;
|
|
873
|
-
node.stdout = result.stdout;
|
|
874
|
-
node.stderr = result.stderr;
|
|
875
|
-
node.failureCategory = result.failureCategory;
|
|
876
|
-
node.finishedAt = new Date().toISOString();
|
|
877
|
-
node.status = result.ok ? "FINISHED" : "ERROR";
|
|
878
|
-
}
|
|
879
|
-
catch (error) {
|
|
880
|
-
node.status = "ERROR";
|
|
881
|
-
node.stderr = error instanceof Error ? error.message : String(error);
|
|
882
|
-
node.finishedAt = new Date().toISOString();
|
|
883
|
-
node.durationMs = Date.now() - started;
|
|
884
|
-
}
|
|
885
|
-
if (node.status === "FINISHED") {
|
|
886
|
-
const artifactMeta = await persistLongNodeOutputArtifacts({
|
|
887
|
-
runDir,
|
|
888
|
-
nodeId,
|
|
889
|
-
stdout: node.stdout,
|
|
890
|
-
assistantText: node.assistantText,
|
|
891
|
-
});
|
|
892
|
-
Object.assign(node, artifactMeta);
|
|
893
|
-
}
|
|
894
|
-
state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
|
|
895
|
-
await appendNodeLog(runDir, nodeId, state.nodes[nodeId]);
|
|
896
|
-
await input.persistState();
|
|
897
|
-
await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
const { prompt, resolvedSkills } = await buildNodePromptWithResolvedSkillInstructions(spec, task, state.nodes, cwd);
|
|
901
|
-
node.resolvedSkills = resolvedSkills;
|
|
902
|
-
await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
|
|
903
|
-
const model = resolveModelForTask(task, spec.executorModels);
|
|
904
|
-
const started = Date.now();
|
|
905
|
-
try {
|
|
906
|
-
validateRepairArtifactGateBeforeShell({
|
|
907
|
-
task,
|
|
908
|
-
tasksById,
|
|
909
|
-
state,
|
|
910
|
-
});
|
|
911
|
-
const result = await executeNode({ task, cwd, model, prompt });
|
|
912
|
-
node.durationMs = result.durationMs ?? Date.now() - started;
|
|
913
|
-
node.stdout = result.stdout;
|
|
914
|
-
node.stderr = result.stderr;
|
|
915
|
-
node.failureCategory = result.failureCategory;
|
|
916
|
-
if (result.assistantText !== undefined) {
|
|
917
|
-
node.assistantText = result.assistantText;
|
|
918
|
-
}
|
|
919
|
-
if (result.backend !== undefined)
|
|
920
|
-
node.backend = result.backend;
|
|
921
|
-
if (result.sdkAttempted !== undefined) {
|
|
922
|
-
node.sdkAttempted = result.sdkAttempted;
|
|
923
|
-
}
|
|
924
|
-
if (result.tokensUsed !== undefined)
|
|
925
|
-
node.tokensUsed = result.tokensUsed;
|
|
926
|
-
if (result.parsedEvents !== undefined) {
|
|
927
|
-
node.parsedEvents = result.parsedEvents;
|
|
928
|
-
}
|
|
929
|
-
recordRepairArtifactForSupervisorNode({
|
|
930
|
-
task,
|
|
931
|
-
node,
|
|
932
|
-
state,
|
|
933
|
-
});
|
|
934
|
-
const outputChunk = result.assistantText ?? result.stdout;
|
|
935
|
-
if (outputChunk.trim()) {
|
|
936
|
-
await notifyNodeObserver(input.observer, "onNodeOutput", nodeId, state, outputChunk);
|
|
937
|
-
}
|
|
938
|
-
node.finishedAt = new Date().toISOString();
|
|
939
|
-
node.status = result.ok ? "FINISHED" : "ERROR";
|
|
940
|
-
if (result.ok) {
|
|
941
|
-
const decisionRecord = await recordDecisionEnvelopeForNode({
|
|
942
|
-
task,
|
|
943
|
-
runDir,
|
|
944
|
-
nodeId,
|
|
945
|
-
assistantText: node.assistantText ?? result.assistantText,
|
|
946
|
-
});
|
|
947
|
-
if (decisionRecord) {
|
|
948
|
-
node.decisionEnvelope = decisionRecord.nodeRecord;
|
|
949
|
-
if (!decisionRecord.nodeRecord.parseOk) {
|
|
950
|
-
node.status = "ERROR";
|
|
951
|
-
node.failureCategory = "decision-envelope-invalid";
|
|
952
|
-
node.stderr = [
|
|
953
|
-
node.stderr,
|
|
954
|
-
...(decisionRecord.nodeRecord.errors ?? []),
|
|
955
|
-
]
|
|
956
|
-
.filter(Boolean)
|
|
957
|
-
.join("\n");
|
|
958
|
-
}
|
|
959
|
-
else if (shouldPauseOnHumanEscalation(task, decisionRecord.nodeRecord) &&
|
|
960
|
-
decisionRecord.envelope) {
|
|
961
|
-
const pausedAt = new Date().toISOString();
|
|
962
|
-
const nodeArtifactsDir = path.join(runDir, nodeId);
|
|
963
|
-
const { jsonPath } = await writeHumanEscalationArtifacts({
|
|
964
|
-
nodeArtifactsDir,
|
|
965
|
-
runId: state.runId,
|
|
966
|
-
nodeId,
|
|
967
|
-
envelope: decisionRecord.envelope,
|
|
968
|
-
pausedAt,
|
|
969
|
-
});
|
|
970
|
-
node.pauseReason = "decision-gate-requires-human";
|
|
971
|
-
node.escalationArtifactPath = jsonPath;
|
|
972
|
-
input.onPause(nodeId, pausedAt, node.pauseReason);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
catch (error) {
|
|
978
|
-
node.status = "ERROR";
|
|
979
|
-
node.stderr = error instanceof Error ? error.message : String(error);
|
|
980
|
-
node.finishedAt = new Date().toISOString();
|
|
981
|
-
node.durationMs = Date.now() - started;
|
|
982
|
-
}
|
|
983
|
-
if (node.status === "FINISHED") {
|
|
984
|
-
const artifactMeta = await persistLongNodeOutputArtifacts({
|
|
985
|
-
runDir,
|
|
986
|
-
nodeId,
|
|
987
|
-
stdout: node.stdout,
|
|
988
|
-
assistantText: node.assistantText,
|
|
989
|
-
});
|
|
990
|
-
Object.assign(node, artifactMeta);
|
|
991
|
-
}
|
|
992
|
-
state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
|
|
993
|
-
await appendNodeLog(runDir, nodeId, state.nodes[nodeId]);
|
|
994
|
-
await input.persistState();
|
|
995
|
-
await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
|
|
996
|
-
}
|
|
997
|
-
function sha256Json(value) {
|
|
998
|
-
return `sha256:${createHash("sha256")
|
|
999
|
-
.update(JSON.stringify(value))
|
|
1000
|
-
.digest("hex")}`;
|
|
1001
|
-
}
|
|
1002
|
-
function getNodeOutputAsJson(state, nodeId) {
|
|
1003
|
-
const record = state.nodes[nodeId];
|
|
1004
|
-
if (!record || record.status !== "FINISHED") {
|
|
1005
|
-
throw new Error(`itemsFrom upstream node "${nodeId}" is not finished`);
|
|
1006
|
-
}
|
|
1007
|
-
const raw = record.stdout?.trim() || record.assistantText?.trim();
|
|
1008
|
-
if (!raw) {
|
|
1009
|
-
throw new Error(`itemsFrom upstream node "${nodeId}" has no JSON output`);
|
|
1010
|
-
}
|
|
1011
|
-
try {
|
|
1012
|
-
return JSON.parse(raw);
|
|
1013
|
-
}
|
|
1014
|
-
catch (error) {
|
|
1015
|
-
throw new Error(`itemsFrom upstream node "${nodeId}" output is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
function resolveOutputSelector(selector, state) {
|
|
1019
|
-
const match = selector.match(/^\$\.nodes\[['"]([^'"]+)['"]\]\.output(?:\.(.+))?$/);
|
|
1020
|
-
if (!match?.[1]) {
|
|
1021
|
-
throw new Error(`unsupported itemsFrom selector: ${selector}`);
|
|
1022
|
-
}
|
|
1023
|
-
const nodeId = match[1];
|
|
1024
|
-
const pathSuffix = match[2];
|
|
1025
|
-
let current = getNodeOutputAsJson(state, nodeId);
|
|
1026
|
-
if (pathSuffix) {
|
|
1027
|
-
for (const segment of pathSuffix.split(".")) {
|
|
1028
|
-
if (current &&
|
|
1029
|
-
typeof current === "object" &&
|
|
1030
|
-
segment in current) {
|
|
1031
|
-
current = current[segment];
|
|
1032
|
-
continue;
|
|
1033
|
-
}
|
|
1034
|
-
throw new Error(`output selector path not found: ${selector} (missing "${segment}")`);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
return current;
|
|
1038
|
-
}
|
|
1039
|
-
function resolveItemsFromSelector(selector, state) {
|
|
1040
|
-
let current;
|
|
1041
|
-
try {
|
|
1042
|
-
current = resolveOutputSelector(selector, state);
|
|
1043
|
-
}
|
|
1044
|
-
catch (error) {
|
|
1045
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1046
|
-
if (message.startsWith("output selector path not found:")) {
|
|
1047
|
-
throw new Error(`itemsFrom path not found: ${selector}`);
|
|
1048
|
-
}
|
|
1049
|
-
throw error;
|
|
1050
|
-
}
|
|
1051
|
-
if (!Array.isArray(current)) {
|
|
1052
|
-
throw new Error(`itemsFrom selector did not resolve to an array: ${selector}`);
|
|
1053
|
-
}
|
|
1054
|
-
return current;
|
|
1055
|
-
}
|
|
1056
|
-
function parseConditionLiteral(raw) {
|
|
1057
|
-
const trimmed = raw.trim();
|
|
1058
|
-
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
1059
|
-
(trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
1060
|
-
return trimmed.slice(1, -1);
|
|
1061
|
-
}
|
|
1062
|
-
if (trimmed === "true")
|
|
1063
|
-
return true;
|
|
1064
|
-
if (trimmed === "false")
|
|
1065
|
-
return false;
|
|
1066
|
-
if (trimmed === "null")
|
|
1067
|
-
return null;
|
|
1068
|
-
const numeric = Number(trimmed);
|
|
1069
|
-
return Number.isFinite(numeric) ? numeric : trimmed;
|
|
1070
|
-
}
|
|
1071
|
-
function evaluateConditionExpression(expression, state) {
|
|
1072
|
-
const equality = expression.match(/^\s*(\$\..+?)\s*==\s*(.+?)\s*$/);
|
|
1073
|
-
if (!equality?.[1]) {
|
|
1074
|
-
throw new Error(`unsupported condition expression: ${expression}`);
|
|
1075
|
-
}
|
|
1076
|
-
const actual = resolveOutputSelector(equality[1], state);
|
|
1077
|
-
const expected = parseConditionLiteral(equality[2] ?? "");
|
|
1078
|
-
return actual === expected;
|
|
1079
|
-
}
|
|
1080
|
-
function evaluateLoopStopExpression(input) {
|
|
1081
|
-
const equality = input.expression.match(/^\s*(\$\..+?)\s*==\s*(.+?)\s*$/);
|
|
1082
|
-
if (!equality?.[1]) {
|
|
1083
|
-
throw new Error(`unsupported loop_until stop expression: ${input.expression}`);
|
|
1084
|
-
}
|
|
1085
|
-
const selector = equality[1];
|
|
1086
|
-
let actual;
|
|
1087
|
-
if (selector.startsWith("$.last.output.")) {
|
|
1088
|
-
actual = lookupPath(input.lastOutput, selector.slice("$.last.output.".length));
|
|
1089
|
-
}
|
|
1090
|
-
else if (selector === "$.iteration") {
|
|
1091
|
-
actual = input.iteration;
|
|
1092
|
-
}
|
|
1093
|
-
else {
|
|
1094
|
-
throw new Error(`unsupported loop_until stop selector: ${selector}`);
|
|
1095
|
-
}
|
|
1096
|
-
const expected = parseConditionLiteral(equality[2] ?? "");
|
|
1097
|
-
return actual === expected;
|
|
1098
|
-
}
|
|
1099
|
-
function executeDynamicCondition(input) {
|
|
1100
|
-
const started = Date.now();
|
|
1101
|
-
const matchedIndex = input.condition.cases.findIndex((conditionCase) => evaluateConditionExpression(conditionCase.when, input.state));
|
|
1102
|
-
const selected = matchedIndex >= 0
|
|
1103
|
-
? input.condition.cases[matchedIndex]?.then
|
|
1104
|
-
: input.condition.default;
|
|
1105
|
-
if (!selected) {
|
|
1106
|
-
throw new Error(`condition ${input.task.id} did not match and has no default`);
|
|
1107
|
-
}
|
|
1108
|
-
if (!input.tasksById.has(selected)) {
|
|
1109
|
-
throw new Error(`condition ${input.task.id} selected missing target ${selected}`);
|
|
1110
|
-
}
|
|
1111
|
-
const branchTargets = new Set([
|
|
1112
|
-
...input.condition.cases.map((conditionCase) => conditionCase.then),
|
|
1113
|
-
...(input.condition.default ? [input.condition.default] : []),
|
|
1114
|
-
]);
|
|
1115
|
-
for (const target of branchTargets) {
|
|
1116
|
-
if (target === selected)
|
|
1117
|
-
continue;
|
|
1118
|
-
const record = input.state.nodes[target];
|
|
1119
|
-
if (record?.status === "PENDING") {
|
|
1120
|
-
record.status = "SKIPPED";
|
|
1121
|
-
record.skippedReason = `condition ${input.task.id} selected ${selected}; branch not selected`;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
return {
|
|
1125
|
-
ok: true,
|
|
1126
|
-
stdout: JSON.stringify({
|
|
1127
|
-
workflowNodeId: input.condition.workflowNodeId,
|
|
1128
|
-
selected,
|
|
1129
|
-
matchedCaseIndex: matchedIndex >= 0 ? matchedIndex : undefined,
|
|
1130
|
-
defaulted: matchedIndex < 0,
|
|
1131
|
-
}),
|
|
1132
|
-
stderr: "",
|
|
1133
|
-
failureCategory: "success",
|
|
1134
|
-
durationMs: Date.now() - started,
|
|
1135
|
-
};
|
|
1136
|
-
}
|
|
1137
|
-
function renderLoopTemplate(template, iteration) {
|
|
1138
|
-
const iterationStatus = iteration >= 2 ? "passed" : "failed";
|
|
1139
|
-
return template
|
|
1140
|
-
.replace(/\{\{\s*iteration\s*\}\}/g, String(iteration))
|
|
1141
|
-
.replace(/\{\{\s*iterationStatus\s*\}\}/g, iterationStatus);
|
|
1142
|
-
}
|
|
1143
|
-
function buildLoopBodyChildTask(input) {
|
|
1144
|
-
const { parent, bodyTask, iteration, nodeId, bodyIdMap } = input;
|
|
1145
|
-
const mappedDepends = bodyTask.dependsOn.map((depId) => bodyIdMap.get(depId) ?? depId);
|
|
1146
|
-
return {
|
|
1147
|
-
id: nodeId,
|
|
1148
|
-
depends_on: mappedDepends.length > 0 ? mappedDepends : parent.depends_on,
|
|
1149
|
-
complexity: bodyTask.complexity,
|
|
1150
|
-
subtask_prompt: renderLoopTemplate(bodyTask.subtaskPromptTemplate, iteration),
|
|
1151
|
-
executor: bodyTask.executor,
|
|
1152
|
-
role: bodyTask.role,
|
|
1153
|
-
writePolicy: bodyTask.writePolicy,
|
|
1154
|
-
allowedPaths: bodyTask.allowedPaths,
|
|
1155
|
-
forbiddenPaths: bodyTask.forbiddenPaths,
|
|
1156
|
-
writeSet: bodyTask.writeSet,
|
|
1157
|
-
outputContract: bodyTask.outputContract,
|
|
1158
|
-
static: bodyTask.executor === "static"
|
|
1159
|
-
? {
|
|
1160
|
-
resultMarkdown: bodyTask.staticResultTemplate
|
|
1161
|
-
? renderLoopTemplate(bodyTask.staticResultTemplate, iteration)
|
|
1162
|
-
: "dynamic loop child completed",
|
|
1163
|
-
}
|
|
1164
|
-
: undefined,
|
|
1165
|
-
};
|
|
1166
|
-
}
|
|
1167
|
-
async function executeDynamicLoopUntil(input) {
|
|
1168
|
-
const started = Date.now();
|
|
1169
|
-
const executedChildren = [];
|
|
1170
|
-
let stopped = false;
|
|
1171
|
-
let stopReason = "";
|
|
1172
|
-
let iterations = 0;
|
|
1173
|
-
for (let iteration = 1; iteration <= input.loop.maxIterations; iteration += 1) {
|
|
1174
|
-
iterations = iteration;
|
|
1175
|
-
const bodyIdMap = new Map(input.loop.bodyTasks.map((bodyTask) => [
|
|
1176
|
-
bodyTask.id,
|
|
1177
|
-
`${input.task.id}-r${iteration}-${bodyTask.id}`,
|
|
1178
|
-
]));
|
|
1179
|
-
const children = input.loop.bodyTasks.map((bodyTask) => buildLoopBodyChildTask({
|
|
1180
|
-
parent: input.task,
|
|
1181
|
-
loop: input.loop,
|
|
1182
|
-
bodyTask,
|
|
1183
|
-
iteration,
|
|
1184
|
-
nodeId: bodyIdMap.get(bodyTask.id),
|
|
1185
|
-
bodyIdMap,
|
|
1186
|
-
}));
|
|
1187
|
-
for (const child of children) {
|
|
1188
|
-
if (!input.tasksById.has(child.id)) {
|
|
1189
|
-
input.tasksById.set(child.id, child);
|
|
1190
|
-
input.spec.tasks.push(child);
|
|
1191
|
-
}
|
|
1192
|
-
if (!input.state.nodes[child.id]) {
|
|
1193
|
-
input.state.nodes[child.id] = freshNodeRecord(child);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
input.state.ranks.push(children.map((child) => child.id));
|
|
1197
|
-
await writeDagRunSpec(input.runDir, input.spec);
|
|
1198
|
-
await input.persistState();
|
|
1199
|
-
for (const child of children) {
|
|
1200
|
-
await executeDagNode({
|
|
1201
|
-
nodeId: child.id,
|
|
1202
|
-
tasksById: input.tasksById,
|
|
1203
|
-
state: input.state,
|
|
1204
|
-
spec: input.spec,
|
|
1205
|
-
cwd: input.cwd,
|
|
1206
|
-
runDir: input.runDir,
|
|
1207
|
-
executeNode: input.executeNode,
|
|
1208
|
-
observer: input.observer,
|
|
1209
|
-
persistState: input.persistState,
|
|
1210
|
-
onPause: () => {
|
|
1211
|
-
throw new Error(`dynamic child node ${child.id} requested a human pause; loop_until children do not support pause in v0`);
|
|
1212
|
-
},
|
|
1213
|
-
});
|
|
1214
|
-
executedChildren.push(child.id);
|
|
1215
|
-
}
|
|
1216
|
-
const failedChild = children.find((child) => input.state.nodes[child.id]?.status !== "FINISHED");
|
|
1217
|
-
if (failedChild) {
|
|
1218
|
-
return {
|
|
1219
|
-
ok: false,
|
|
1220
|
-
stdout: JSON.stringify({
|
|
1221
|
-
workflowNodeId: input.loop.workflowNodeId,
|
|
1222
|
-
iterations,
|
|
1223
|
-
stopped: false,
|
|
1224
|
-
children: executedChildren,
|
|
1225
|
-
}),
|
|
1226
|
-
stderr: `loop_until child failed: ${failedChild.id}`,
|
|
1227
|
-
failureCategory: "dynamic-loop-child-failed",
|
|
1228
|
-
durationMs: Date.now() - started,
|
|
1229
|
-
};
|
|
1230
|
-
}
|
|
1231
|
-
const lastChild = children[children.length - 1];
|
|
1232
|
-
const lastOutput = parseJsonFromText(input.state.nodes[lastChild.id]?.stdout);
|
|
1233
|
-
const matched = input.loop.stopWhenAny.find((expression) => evaluateLoopStopExpression({
|
|
1234
|
-
expression,
|
|
1235
|
-
lastOutput,
|
|
1236
|
-
iteration,
|
|
1237
|
-
}));
|
|
1238
|
-
if (matched) {
|
|
1239
|
-
stopped = true;
|
|
1240
|
-
stopReason = matched;
|
|
1241
|
-
break;
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
const manifest = {
|
|
1245
|
-
workflowNodeId: input.loop.workflowNodeId,
|
|
1246
|
-
expandedAt: new Date().toISOString(),
|
|
1247
|
-
maxIterations: input.loop.maxIterations,
|
|
1248
|
-
iterations,
|
|
1249
|
-
stopped,
|
|
1250
|
-
stopReason: stopped ? stopReason : undefined,
|
|
1251
|
-
children: executedChildren.map((nodeId) => ({ nodeId })),
|
|
1252
|
-
};
|
|
1253
|
-
const expansionDir = path.join(input.runDir, "expansions");
|
|
1254
|
-
await mkdir(expansionDir, { recursive: true });
|
|
1255
|
-
await writeFile(path.join(expansionDir, `${input.task.id}.expansion.json`), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
|
|
1256
|
-
return {
|
|
1257
|
-
ok: true,
|
|
1258
|
-
stdout: JSON.stringify({
|
|
1259
|
-
workflowNodeId: input.loop.workflowNodeId,
|
|
1260
|
-
iterations,
|
|
1261
|
-
stopped,
|
|
1262
|
-
stopReason: stopped ? stopReason : undefined,
|
|
1263
|
-
maxIterationsReached: !stopped,
|
|
1264
|
-
children: executedChildren,
|
|
1265
|
-
}),
|
|
1266
|
-
stderr: "",
|
|
1267
|
-
failureCategory: "success",
|
|
1268
|
-
durationMs: Date.now() - started,
|
|
1269
|
-
};
|
|
1270
|
-
}
|
|
1271
|
-
function lookupPath(value, pathExpression) {
|
|
1272
|
-
let current = value;
|
|
1273
|
-
for (const segment of pathExpression.split(".")) {
|
|
1274
|
-
if (current &&
|
|
1275
|
-
typeof current === "object" &&
|
|
1276
|
-
segment in current) {
|
|
1277
|
-
current = current[segment];
|
|
1278
|
-
continue;
|
|
1279
|
-
}
|
|
1280
|
-
return undefined;
|
|
1281
|
-
}
|
|
1282
|
-
return current;
|
|
1283
|
-
}
|
|
1284
|
-
function templateValueToString(value) {
|
|
1285
|
-
if (typeof value === "string" ||
|
|
1286
|
-
typeof value === "number" ||
|
|
1287
|
-
typeof value === "boolean") {
|
|
1288
|
-
return String(value);
|
|
1289
|
-
}
|
|
1290
|
-
if (value === null || value === undefined)
|
|
1291
|
-
return "";
|
|
1292
|
-
return JSON.stringify(value);
|
|
1293
|
-
}
|
|
1294
|
-
function parseJsonFromText(value) {
|
|
1295
|
-
if (!value?.trim())
|
|
1296
|
-
return undefined;
|
|
1297
|
-
try {
|
|
1298
|
-
return JSON.parse(value);
|
|
1299
|
-
}
|
|
1300
|
-
catch {
|
|
1301
|
-
return undefined;
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
function renderDynamicTemplate(template, item, index, itemName) {
|
|
1305
|
-
const primitive = typeof item === "string" ||
|
|
1306
|
-
typeof item === "number" ||
|
|
1307
|
-
typeof item === "boolean"
|
|
1308
|
-
? String(item)
|
|
1309
|
-
: JSON.stringify(item);
|
|
1310
|
-
return template
|
|
1311
|
-
.replace(/\{\{\s*item\s*\}\}/g, primitive)
|
|
1312
|
-
.replace(new RegExp(`\\{\\{\\s*${itemName}\\s*\\}\\}`, "g"), primitive)
|
|
1313
|
-
.replace(new RegExp(`\\{\\{\\s*${itemName}\\.([A-Za-z0-9_.-]+)\\s*\\}\\}`, "g"), (_match, pathExpression) => templateValueToString(lookupPath(item, pathExpression)))
|
|
1314
|
-
.replace(/\{\{\s*item\.([A-Za-z0-9_.-]+)\s*\}\}/g, (_match, pathExpression) => templateValueToString(lookupPath(item, pathExpression)))
|
|
1315
|
-
.replace(/\{\{\s*itemJson\s*\}\}/g, JSON.stringify(item))
|
|
1316
|
-
.replace(/\{\{\s*index\s*\}\}/g, String(index));
|
|
1317
|
-
}
|
|
1318
|
-
function renderDynamicPatternList(patterns, item, index, itemName) {
|
|
1319
|
-
return patterns?.map((pattern) => renderDynamicTemplate(pattern, item, index, itemName));
|
|
1320
|
-
}
|
|
1321
|
-
function buildExpandedChildTask(input) {
|
|
1322
|
-
const { parent, expansion, item, index, nodeId } = input;
|
|
1323
|
-
const child = expansion.childTask;
|
|
1324
|
-
const subtaskPrompt = renderDynamicTemplate(child.subtaskPromptTemplate, item, index, expansion.itemName);
|
|
1325
|
-
const staticResult = child.staticResultTemplate
|
|
1326
|
-
? renderDynamicTemplate(child.staticResultTemplate, item, index, expansion.itemName)
|
|
1327
|
-
: undefined;
|
|
1328
|
-
return {
|
|
1329
|
-
id: nodeId,
|
|
1330
|
-
depends_on: parent.depends_on,
|
|
1331
|
-
complexity: child.complexity,
|
|
1332
|
-
subtask_prompt: subtaskPrompt,
|
|
1333
|
-
executor: child.executor,
|
|
1334
|
-
role: child.role,
|
|
1335
|
-
writePolicy: child.writePolicy,
|
|
1336
|
-
allowedPaths: renderDynamicPatternList(child.allowedPaths, item, index, expansion.itemName) ?? [],
|
|
1337
|
-
forbiddenPaths: renderDynamicPatternList(child.forbiddenPaths, item, index, expansion.itemName) ?? [],
|
|
1338
|
-
writeSet: renderDynamicPatternList(child.writeSet, item, index, expansion.itemName),
|
|
1339
|
-
outputContract: child.outputContract,
|
|
1340
|
-
static: child.executor === "static"
|
|
1341
|
-
? { resultMarkdown: staticResult ?? "dynamic child completed" }
|
|
1342
|
-
: undefined,
|
|
1343
|
-
};
|
|
1344
|
-
}
|
|
1345
|
-
function addExpansionRank(input) {
|
|
1346
|
-
if (input.state.ranks.some((rank) => input.childNodeIds.every((nodeId) => rank.includes(nodeId)))) {
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1349
|
-
const parentRankIndex = input.state.ranks.findIndex((rank) => rank.includes(input.parentNodeId));
|
|
1350
|
-
const insertAt = parentRankIndex >= 0 ? parentRankIndex + 1 : input.state.ranks.length;
|
|
1351
|
-
input.state.ranks.splice(insertAt, 0, input.childNodeIds);
|
|
1352
|
-
}
|
|
1353
|
-
function normalizedWritePattern(pattern) {
|
|
1354
|
-
return pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1355
|
-
}
|
|
1356
|
-
function writePatternsOverlap(left, right) {
|
|
1357
|
-
const normalizedLeft = normalizedWritePattern(left);
|
|
1358
|
-
const normalizedRight = normalizedWritePattern(right);
|
|
1359
|
-
if (normalizedLeft === normalizedRight)
|
|
1360
|
-
return true;
|
|
1361
|
-
const leftPrefix = normalizedLeft.replace(/\*\*.*$/, "");
|
|
1362
|
-
const rightPrefix = normalizedRight.replace(/\*\*.*$/, "");
|
|
1363
|
-
return (leftPrefix.length > 0 &&
|
|
1364
|
-
rightPrefix.length > 0 &&
|
|
1365
|
-
(leftPrefix.startsWith(rightPrefix) || rightPrefix.startsWith(leftPrefix)));
|
|
1366
|
-
}
|
|
1367
|
-
function collectDynamicChildWriteSetConflicts(children) {
|
|
1368
|
-
const conflicts = [];
|
|
1369
|
-
const exclusiveChildren = children.filter((child) => child.writePolicy === "exclusive" && (child.writeSet?.length ?? 0) > 0);
|
|
1370
|
-
for (let i = 0; i < exclusiveChildren.length; i += 1) {
|
|
1371
|
-
for (let j = i + 1; j < exclusiveChildren.length; j += 1) {
|
|
1372
|
-
const left = exclusiveChildren[i];
|
|
1373
|
-
const right = exclusiveChildren[j];
|
|
1374
|
-
for (const leftEntry of left.writeSet ?? []) {
|
|
1375
|
-
for (const rightEntry of right.writeSet ?? []) {
|
|
1376
|
-
if (writePatternsOverlap(leftEntry, rightEntry)) {
|
|
1377
|
-
conflicts.push(`${left.id}:${leftEntry} overlaps ${right.id}:${rightEntry}`);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
return conflicts;
|
|
1384
|
-
}
|
|
1385
|
-
function resolveRunLocalPath(runDir, ref, label) {
|
|
1386
|
-
if (path.isAbsolute(ref)) {
|
|
1387
|
-
throw new Error(`${label} must be relative to the DAG run directory: ${ref}`);
|
|
1388
|
-
}
|
|
1389
|
-
const runRoot = path.resolve(runDir);
|
|
1390
|
-
const resolved = path.resolve(runRoot, ref);
|
|
1391
|
-
if (resolved !== runRoot && !resolved.startsWith(`${runRoot}${path.sep}`)) {
|
|
1392
|
-
throw new Error(`${label} escapes the DAG run directory: ${ref}`);
|
|
1393
|
-
}
|
|
1394
|
-
return resolved;
|
|
1395
|
-
}
|
|
1396
|
-
async function executeDynamicMapExpansion(input) {
|
|
1397
|
-
const started = Date.now();
|
|
1398
|
-
const items = resolveItemsFromSelector(input.expansion.itemsFrom, input.state);
|
|
1399
|
-
if (items.length > input.expansion.maxExpandedNodes) {
|
|
1400
|
-
throw new Error(`map_agent ${input.task.id} item count ${items.length} exceeds maxExpandedNodes ${input.expansion.maxExpandedNodes}`);
|
|
1401
|
-
}
|
|
1402
|
-
if (items.length > input.expansion.maxItems) {
|
|
1403
|
-
throw new Error(`map_agent ${input.task.id} item count ${items.length} exceeds maxItems ${input.expansion.maxItems}`);
|
|
1404
|
-
}
|
|
1405
|
-
const childNodeIds = items.map((_, index) => `${input.expansion.childIdPrefix}-${String(index + 1).padStart(4, "0")}`);
|
|
1406
|
-
const children = childNodeIds.map((nodeId, index) => buildExpandedChildTask({
|
|
1407
|
-
parent: input.task,
|
|
1408
|
-
expansion: input.expansion,
|
|
1409
|
-
item: items[index],
|
|
1410
|
-
index,
|
|
1411
|
-
nodeId,
|
|
1412
|
-
}));
|
|
1413
|
-
const writeSetConflicts = collectDynamicChildWriteSetConflicts(children);
|
|
1414
|
-
if (writeSetConflicts.length > 0) {
|
|
1415
|
-
throw new Error(`dynamic map_agent ${input.task.id} expanded overlapping writeSets: ${writeSetConflicts.join("; ")}`);
|
|
1416
|
-
}
|
|
1417
|
-
const workspaceRefs = items.map((item, index) => input.expansion.workspaceTemplate
|
|
1418
|
-
? renderDynamicTemplate(input.expansion.workspaceTemplate, item, index, input.expansion.itemName)
|
|
1419
|
-
: undefined);
|
|
1420
|
-
for (const workspaceRef of workspaceRefs) {
|
|
1421
|
-
if (!workspaceRef)
|
|
1422
|
-
continue;
|
|
1423
|
-
await mkdir(resolveRunLocalPath(input.runDir, workspaceRef, "workspaceRef"), {
|
|
1424
|
-
recursive: true,
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
for (const child of children) {
|
|
1428
|
-
if (!input.tasksById.has(child.id)) {
|
|
1429
|
-
input.tasksById.set(child.id, child);
|
|
1430
|
-
input.spec.tasks.push(child);
|
|
1431
|
-
}
|
|
1432
|
-
if (!input.state.nodes[child.id]) {
|
|
1433
|
-
input.state.nodes[child.id] = freshNodeRecord(child);
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
addExpansionRank({
|
|
1437
|
-
state: input.state,
|
|
1438
|
-
parentNodeId: input.task.id,
|
|
1439
|
-
childNodeIds,
|
|
1440
|
-
});
|
|
1441
|
-
await writeDagRunSpec(input.runDir, input.spec);
|
|
1442
|
-
const manifest = {
|
|
1443
|
-
workflowNodeId: input.expansion.workflowNodeId,
|
|
1444
|
-
expandedAt: new Date().toISOString(),
|
|
1445
|
-
itemsFrom: input.expansion.itemsFrom,
|
|
1446
|
-
itemCount: items.length,
|
|
1447
|
-
children: childNodeIds.map((nodeId, index) => ({
|
|
1448
|
-
nodeId,
|
|
1449
|
-
itemRef: `$.items[${index}]`,
|
|
1450
|
-
itemValueHash: sha256Json(items[index]),
|
|
1451
|
-
workspaceRef: workspaceRefs[index],
|
|
1452
|
-
})),
|
|
1453
|
-
};
|
|
1454
|
-
const expansionDir = path.join(input.runDir, "expansions");
|
|
1455
|
-
await mkdir(expansionDir, { recursive: true });
|
|
1456
|
-
await writeFile(path.join(expansionDir, `${input.task.id}.expansion.json`), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
|
|
1457
|
-
await input.persistState();
|
|
1458
|
-
for (const child of children) {
|
|
1459
|
-
const childRecord = input.state.nodes[child.id];
|
|
1460
|
-
if (childRecord?.status === "FINISHED")
|
|
1461
|
-
continue;
|
|
1462
|
-
await executeDagNode({
|
|
1463
|
-
nodeId: child.id,
|
|
1464
|
-
tasksById: input.tasksById,
|
|
1465
|
-
state: input.state,
|
|
1466
|
-
spec: input.spec,
|
|
1467
|
-
cwd: input.cwd,
|
|
1468
|
-
runDir: input.runDir,
|
|
1469
|
-
executeNode: input.executeNode,
|
|
1470
|
-
observer: input.observer,
|
|
1471
|
-
persistState: input.persistState,
|
|
1472
|
-
onPause: () => {
|
|
1473
|
-
throw new Error(`dynamic child node ${child.id} requested a human pause; map_agent children do not support pause in v0`);
|
|
1474
|
-
},
|
|
1475
|
-
});
|
|
1476
|
-
}
|
|
1477
|
-
const failedChildren = childNodeIds.filter((nodeId) => input.state.nodes[nodeId]?.status !== "FINISHED");
|
|
1478
|
-
const aggregate = {
|
|
1479
|
-
workflowNodeId: input.expansion.workflowNodeId,
|
|
1480
|
-
itemCount: items.length,
|
|
1481
|
-
children: childNodeIds.map((nodeId, index) => ({
|
|
1482
|
-
nodeId,
|
|
1483
|
-
item: items[index],
|
|
1484
|
-
workspaceRef: workspaceRefs[index],
|
|
1485
|
-
status: input.state.nodes[nodeId]?.status,
|
|
1486
|
-
stdout: input.state.nodes[nodeId]?.stdout,
|
|
1487
|
-
output: parseJsonFromText(input.state.nodes[nodeId]?.stdout),
|
|
1488
|
-
assistantText: input.state.nodes[nodeId]?.assistantText,
|
|
1489
|
-
})),
|
|
1490
|
-
};
|
|
1491
|
-
return {
|
|
1492
|
-
ok: failedChildren.length === 0,
|
|
1493
|
-
stdout: JSON.stringify(aggregate),
|
|
1494
|
-
stderr: failedChildren.length > 0
|
|
1495
|
-
? `dynamic map children failed: ${failedChildren.join(", ")}`
|
|
1496
|
-
: "",
|
|
1497
|
-
failureCategory: failedChildren.length > 0 ? "dynamic-expansion-child-failed" : "success",
|
|
1498
|
-
durationMs: Date.now() - started,
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
function parseFindingsFromSelector(selector, state) {
|
|
1502
|
-
const raw = resolveOutputSelector(selector, state);
|
|
1503
|
-
if (!Array.isArray(raw)) {
|
|
1504
|
-
throw new Error(`findings selector did not resolve to an array: ${selector}`);
|
|
1505
|
-
}
|
|
1506
|
-
return raw.map((entry) => {
|
|
1507
|
-
const direct = findingSchema.safeParse(entry);
|
|
1508
|
-
if (direct.success)
|
|
1509
|
-
return direct.data;
|
|
1510
|
-
if (entry && typeof entry === "object") {
|
|
1511
|
-
const output = entry.output;
|
|
1512
|
-
const fromOutput = findingSchema.safeParse(output);
|
|
1513
|
-
if (fromOutput.success)
|
|
1514
|
-
return fromOutput.data;
|
|
1515
|
-
const stdout = entry.stdout;
|
|
1516
|
-
if (typeof stdout === "string") {
|
|
1517
|
-
const fromStdout = findingSchema.safeParse(parseJsonFromText(stdout));
|
|
1518
|
-
if (fromStdout.success)
|
|
1519
|
-
return fromStdout.data;
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
return findingSchema.parse(entry);
|
|
1523
|
-
});
|
|
1524
|
-
}
|
|
1525
|
-
function parseVerificationFromChildEntry(entry) {
|
|
1526
|
-
if (!entry || typeof entry !== "object")
|
|
1527
|
-
return undefined;
|
|
1528
|
-
const stdout = entry.stdout;
|
|
1529
|
-
if (typeof stdout !== "string" || stdout.trim().length === 0) {
|
|
1530
|
-
return undefined;
|
|
1531
|
-
}
|
|
1532
|
-
return verificationSchema.parse(JSON.parse(stdout));
|
|
1533
|
-
}
|
|
1534
|
-
function parseVerificationsFromSelector(selector, state) {
|
|
1535
|
-
const raw = resolveOutputSelector(selector, state);
|
|
1536
|
-
if (!Array.isArray(raw)) {
|
|
1537
|
-
throw new Error(`verifications selector did not resolve to an array: ${selector}`);
|
|
1538
|
-
}
|
|
1539
|
-
return raw
|
|
1540
|
-
.map(parseVerificationFromChildEntry)
|
|
1541
|
-
.filter((entry) => Boolean(entry));
|
|
1542
|
-
}
|
|
1543
|
-
async function executeDynamicReduction(input) {
|
|
1544
|
-
const started = Date.now();
|
|
1545
|
-
if (input.reduction.type !== "verified_findings_report") {
|
|
1546
|
-
throw new Error(`unsupported dynamic reduction: ${input.reduction.type}`);
|
|
1547
|
-
}
|
|
1548
|
-
const findings = parseFindingsFromSelector(input.reduction.findingsFrom, input.state);
|
|
1549
|
-
const verifications = parseVerificationsFromSelector(input.reduction.verificationsFrom, input.state);
|
|
1550
|
-
const nodeDir = path.join(input.runDir, input.task.id);
|
|
1551
|
-
await mkdir(nodeDir, { recursive: true });
|
|
1552
|
-
const refutedFindingsPath = path.join(nodeDir, input.reduction.refutedFindingsArtifactName);
|
|
1553
|
-
const { report, refutedFindings } = buildVerifiedFindingsReport({
|
|
1554
|
-
findings,
|
|
1555
|
-
verifications,
|
|
1556
|
-
refutedFindingsRef: refutedFindingsPath.startsWith(input.runDir)
|
|
1557
|
-
? repoRelativePath(input.runDir, refutedFindingsPath)
|
|
1558
|
-
: refutedFindingsPath,
|
|
1559
|
-
});
|
|
1560
|
-
await writeFile(refutedFindingsPath, `${JSON.stringify(refutedFindings, null, 2)}\n`, "utf-8");
|
|
1561
|
-
return {
|
|
1562
|
-
ok: true,
|
|
1563
|
-
stdout: JSON.stringify(report),
|
|
1564
|
-
stderr: "",
|
|
1565
|
-
failureCategory: "success",
|
|
1566
|
-
durationMs: Date.now() - started,
|
|
1567
|
-
};
|
|
1568
|
-
}
|
|
1569
365
|
async function notifyRunObserver(observer, event, state) {
|
|
1570
366
|
try {
|
|
1571
367
|
await observer?.[event]?.(state);
|
|
@@ -1574,18 +370,6 @@ async function notifyRunObserver(observer, event, state) {
|
|
|
1574
370
|
// Observers are derived views; they must not affect canonical DAG execution.
|
|
1575
371
|
}
|
|
1576
372
|
}
|
|
1577
|
-
async function notifyNodeObserver(observer, event, nodeId, state, chunk) {
|
|
1578
|
-
try {
|
|
1579
|
-
if (event === "onNodeOutput") {
|
|
1580
|
-
await observer?.onNodeOutput?.(nodeId, chunk ?? "", state);
|
|
1581
|
-
return;
|
|
1582
|
-
}
|
|
1583
|
-
await observer?.[event]?.(nodeId, state);
|
|
1584
|
-
}
|
|
1585
|
-
catch {
|
|
1586
|
-
// Observers are derived views; they must not affect canonical DAG execution.
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
373
|
function finalizeTerminalRunStatus(state, taskCount) {
|
|
1590
374
|
const finishedCount = Object.values(state.nodes).filter((n) => n.status === "FINISHED").length;
|
|
1591
375
|
const errorCount = Object.values(state.nodes).filter((n) => n.status === "ERROR").length;
|
|
@@ -1606,14 +390,10 @@ async function stopCursorWorkerIfRunning() {
|
|
|
1606
390
|
await stopCursorWorker();
|
|
1607
391
|
}
|
|
1608
392
|
}
|
|
1609
|
-
function
|
|
1610
|
-
|
|
1611
|
-
(task.executor === "pi" && task.toolProfile === "write"));
|
|
1612
|
-
}
|
|
1613
|
-
function concurrentSiblingWriteSetsForNode(rankWriteGuardedNodeIds, nodeId, tasksById) {
|
|
1614
|
-
if (rankWriteGuardedNodeIds.length <= 1)
|
|
393
|
+
function concurrentSiblingWriteSetsForNode(rankCursorNodeIds, nodeId, tasksById) {
|
|
394
|
+
if (rankCursorNodeIds.length <= 1)
|
|
1615
395
|
return [];
|
|
1616
|
-
return
|
|
396
|
+
return rankCursorNodeIds
|
|
1617
397
|
.filter((id) => id !== nodeId)
|
|
1618
398
|
.map((id) => tasksById.get(id)?.writeSet ?? [])
|
|
1619
399
|
.filter((writeSet) => writeSet.length > 0);
|
|
@@ -1623,55 +403,16 @@ function buildRankAwareExecuteNode(input) {
|
|
|
1623
403
|
return input.baseExecuteNode;
|
|
1624
404
|
}
|
|
1625
405
|
return async (nodeInput) => {
|
|
1626
|
-
if (
|
|
406
|
+
if (nodeInput.task.executor !== "cursor" || !nodeInput.model) {
|
|
1627
407
|
return input.baseExecuteNode(nodeInput);
|
|
1628
408
|
}
|
|
1629
409
|
const siblingWriteSets = nodeInput.task.writePolicy === "exclusive"
|
|
1630
|
-
? concurrentSiblingWriteSetsForNode(input.
|
|
410
|
+
? concurrentSiblingWriteSetsForNode(input.rankCursorNodeIds, nodeInput.task.id, input.tasksById)
|
|
1631
411
|
: [];
|
|
1632
|
-
|
|
412
|
+
return executeDagCursorNode({ ...nodeInput, model: nodeInput.model }, {
|
|
1633
413
|
...input.meta,
|
|
1634
414
|
concurrentSiblingWriteSets: siblingWriteSets.length > 0 ? siblingWriteSets : undefined,
|
|
1635
415
|
writeGuardAttribution: siblingWriteSets.length > 0 ? "best-effort" : "per-node",
|
|
1636
|
-
};
|
|
1637
|
-
if (nodeInput.task.executor === "pi") {
|
|
1638
|
-
return executeDagPiNode({ ...nodeInput, model: nodeInput.model }, meta);
|
|
1639
|
-
}
|
|
1640
|
-
return executeDagCursorNode({ ...nodeInput, model: nodeInput.model }, meta);
|
|
416
|
+
});
|
|
1641
417
|
};
|
|
1642
418
|
}
|
|
1643
|
-
function shouldSkipNode(node, task, nodes) {
|
|
1644
|
-
if (!node || node.status !== "PENDING")
|
|
1645
|
-
return false;
|
|
1646
|
-
return task.depends_on.some((depId) => {
|
|
1647
|
-
const dep = nodes[depId];
|
|
1648
|
-
return !dep || dep.status === "ERROR" || dep.status === "SKIPPED";
|
|
1649
|
-
});
|
|
1650
|
-
}
|
|
1651
|
-
function isConditionSkippedReason(reason) {
|
|
1652
|
-
return Boolean(reason?.startsWith("condition "));
|
|
1653
|
-
}
|
|
1654
|
-
function conditionSkippedByAncestor(task, nodes) {
|
|
1655
|
-
return task.depends_on.some((depId) => isConditionSkippedReason(nodes[depId]?.skippedReason));
|
|
1656
|
-
}
|
|
1657
|
-
async function mapConcurrent(items, limit, fn) {
|
|
1658
|
-
const executing = new Set();
|
|
1659
|
-
for (const item of items) {
|
|
1660
|
-
const job = fn(item).finally(() => executing.delete(job));
|
|
1661
|
-
executing.add(job);
|
|
1662
|
-
if (executing.size >= limit) {
|
|
1663
|
-
await Promise.race(executing);
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
await Promise.all(executing);
|
|
1667
|
-
}
|
|
1668
|
-
async function writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills) {
|
|
1669
|
-
if (resolvedSkills.length === 0)
|
|
1670
|
-
return;
|
|
1671
|
-
const nodeDir = path.join(runDir, nodeId);
|
|
1672
|
-
await mkdir(nodeDir, { recursive: true });
|
|
1673
|
-
await writeFile(path.join(nodeDir, "skills.json"), `${JSON.stringify({ resolvedSkills }, null, 2)}\n`, "utf-8");
|
|
1674
|
-
}
|
|
1675
|
-
async function appendNodeLog(runDir, nodeId, record) {
|
|
1676
|
-
await writeFile(path.join(runDir, `${nodeId}.json`), `${JSON.stringify(record, null, 2)}\n`, "utf-8");
|
|
1677
|
-
}
|