taskflow-core 0.2.1 → 0.2.2
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 +3 -3
- package/dist/build-info.d.ts +48 -0
- package/dist/build-info.d.ts.map +1 -0
- package/dist/build-info.js +112 -0
- package/dist/build-info.js.map +1 -0
- package/dist/build-info.json +4 -0
- package/dist/exec/driver.d.ts +5 -0
- package/dist/exec/driver.d.ts.map +1 -1
- package/dist/exec/driver.js +25 -16
- package/dist/exec/driver.js.map +1 -1
- package/dist/exec/kernel-policy.d.ts.map +1 -1
- package/dist/exec/kernel-policy.js +10 -0
- package/dist/exec/kernel-policy.js.map +1 -1
- package/dist/exec/step-kinds.d.ts +14 -1
- package/dist/exec/step-kinds.d.ts.map +1 -1
- package/dist/exec/step-kinds.js +98 -3
- package/dist/exec/step-kinds.js.map +1 -1
- package/dist/exec/step.d.ts +21 -0
- package/dist/exec/step.d.ts.map +1 -1
- package/dist/exec/step.js +22 -5
- package/dist/exec/step.js.map +1 -1
- package/dist/final-output.d.ts +53 -0
- package/dist/final-output.d.ts.map +1 -0
- package/dist/final-output.js +65 -0
- package/dist/final-output.js.map +1 -0
- package/dist/flowir/canonical-hash.d.ts.map +1 -1
- package/dist/flowir/canonical-hash.js +2 -0
- package/dist/flowir/canonical-hash.js.map +1 -1
- package/dist/flowir/compile.d.ts.map +1 -1
- package/dist/flowir/compile.js +4 -0
- package/dist/flowir/compile.js.map +1 -1
- package/dist/flowir/meta.d.ts +3 -0
- package/dist/flowir/meta.d.ts.map +1 -1
- package/dist/flowir/schema.d.ts +3 -0
- package/dist/flowir/schema.d.ts.map +1 -1
- package/dist/flowir/schema.js.map +1 -1
- package/dist/flowir/translate.d.ts.map +1 -1
- package/dist/flowir/translate.js +4 -0
- package/dist/flowir/translate.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/resume.d.ts +76 -0
- package/dist/resume.d.ts.map +1 -0
- package/dist/resume.js +190 -0
- package/dist/resume.js.map +1 -0
- package/dist/runtime/phases/parallel.d.ts +3 -0
- package/dist/runtime/phases/parallel.d.ts.map +1 -1
- package/dist/runtime/phases/parallel.js.map +1 -1
- package/dist/runtime/phases/reduce.d.ts +47 -0
- package/dist/runtime/phases/reduce.d.ts.map +1 -0
- package/dist/runtime/phases/reduce.js +195 -0
- package/dist/runtime/phases/reduce.js.map +1 -0
- package/dist/runtime.d.ts +28 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +392 -59
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts +67 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +177 -1
- package/dist/schema.js.map +1 -1
- package/dist/store.d.ts +49 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +5 -1
- package/dist/store.js.map +1 -1
- package/package.json +2 -2
package/dist/runtime.js
CHANGED
|
@@ -28,7 +28,7 @@ const noRunnerInjected = async (_cwd, _agents, agentName, task) => ({
|
|
|
28
28
|
});
|
|
29
29
|
export { PHASE_TIMEOUT_ABORT_GRACE_MS } from "./runner-core.js";
|
|
30
30
|
import { aggregateUsage, emptyUsage } from "./usage.js";
|
|
31
|
-
import {
|
|
31
|
+
import { asArray, dependenciesOf, LOOP_DEFAULT_MAX_ITERATIONS, LOOP_HARD_MAX_ITERATIONS, MAX_DYNAMIC_MAP_ITEMS, MAX_DYNAMIC_NESTING, MAX_DYNAMIC_PHASES, parseTtlMs, resolveArgs, topoLayers, TOURNAMENT_DEFAULT_VARIANTS, TOURNAMENT_HARD_MAX_VARIANTS, validateInvocationArgs, validateTaskflow } from "./schema.js";
|
|
32
32
|
import { verifyTaskflow } from "./verify.js";
|
|
33
33
|
import { combineScores, combineWithJudge, evaluatePureScorer, formatScorerReport, parseJudgeOutput, SCORE_DEFAULT_THRESHOLD, scoreResultJSON, scorerShapeErrors } from "./scorers.js";
|
|
34
34
|
import { parseGateVerdict, overBudget as overBudgetCheck, parseTournamentWinner } from "./deterministic.js";
|
|
@@ -40,6 +40,7 @@ export { parseGateVerdict };
|
|
|
40
40
|
import { runCodeCompilesScorer } from "./scorer-runtime.js";
|
|
41
41
|
import { buildReflexionSummary, isContractViolation, REFLEXION_SENTINEL } from "./reflexion.js";
|
|
42
42
|
import { hashInput, newRunId, runsDir } from "./store.js";
|
|
43
|
+
import { resolveFinalOutput } from "./final-output.js";
|
|
43
44
|
import { CacheStore, resolveFingerprint } from "./cache.js";
|
|
44
45
|
import { compileTaskflowToIR, phaseFingerprint } from "./flowir/index.js";
|
|
45
46
|
import { computeStaleFrontier, declaredReadMapOfDef, readMapOf } from "./stale.js";
|
|
@@ -47,6 +48,11 @@ import { ctxDirFor, drainPendingSpawns, initCtxDir, registerNode, setNodeStatus
|
|
|
47
48
|
import { allocateWorkspace, isWorkspaceKeyword } from "./workspace.js";
|
|
48
49
|
import { cwdArgName, directoryIdentity, isPathWithin, resolveCwdArg, } from "./cwd-bridge.js";
|
|
49
50
|
import { createResolveOnlyWorkspaceSession, } from "./resources/execution.js";
|
|
51
|
+
// Re-export the shared final-output helper + types so callers importing from
|
|
52
|
+
// the runtime barrel get them from one place. The event kernel
|
|
53
|
+
// (`exec/driver.ts`) imports these directly from `./final-output.ts` to avoid
|
|
54
|
+
// a static runtime↔driver import cycle.
|
|
55
|
+
export { resolveFinalOutput, } from "./final-output.js";
|
|
50
56
|
/** Compute the incremental-reuse summary from a run's terminal phase states.
|
|
51
57
|
* Pure, total, never throws. A phase is "reused" iff it carries a `cacheHit`
|
|
52
58
|
* marker (set by `cachedPhase` for both within-run resume and cross-run hits). */
|
|
@@ -556,6 +562,13 @@ function resolveEffCwd(deps, phase) {
|
|
|
556
562
|
// is not necessarily the invocation root, so anchor legacy literals here.
|
|
557
563
|
return path.resolve(deps.cwd, phase.cwd);
|
|
558
564
|
}
|
|
565
|
+
/** Resolve a literal per-branch cwd once for BOTH cache identity and execution.
|
|
566
|
+
* Existing directories are realpath-canonicalized so aliases/symlinks cannot
|
|
567
|
+
* collide with a different invocation root under the same raw cwd string. */
|
|
568
|
+
function resolveBranchCwd(deps, cwd) {
|
|
569
|
+
const resolved = path.resolve(deps.cwd, cwd);
|
|
570
|
+
return directoryIdentity(resolved)?.canonicalPath ?? resolved;
|
|
571
|
+
}
|
|
559
572
|
function flowTreeUsesCwdBridge(def, loadFlow, seenUses = new Set()) {
|
|
560
573
|
if (def.phases.some((phase) => cwdArgName(phase.cwd) !== undefined))
|
|
561
574
|
return true;
|
|
@@ -700,14 +713,11 @@ async function runInlineSubflow(subflowSpec, defaultAgent, childNodeId, phase, d
|
|
|
700
713
|
return { output: `(spawned subflow failed: ${error})`, usage: emptyUsage(), failed: true, error };
|
|
701
714
|
}
|
|
702
715
|
}
|
|
703
|
-
async function runSpawnedChildren(assignments, ctxDir, parentNodeId, phase, deps, state,
|
|
716
|
+
async function runSpawnedChildren(assignments, ctxDir, parentNodeId, phase, deps, state, runChild, ledger = { usage: emptyUsage() }) {
|
|
704
717
|
const capped = assignments.slice(0, MAX_DYNAMIC_MAP_ITEMS);
|
|
705
718
|
const lines = [];
|
|
706
719
|
const usages = [];
|
|
707
720
|
const errors = [];
|
|
708
|
-
// Effective cwd for flat spawned tasks: honour a workspace override and never
|
|
709
|
-
// pass a reserved keyword through to the runner.
|
|
710
|
-
const spawnCwd = resolveEffCwd(deps, phase);
|
|
711
721
|
let idx = 0;
|
|
712
722
|
for (const a of capped) {
|
|
713
723
|
if (deps.signal?.aborted || spawnedOverBudget(state, ledger.usage))
|
|
@@ -730,30 +740,11 @@ async function runSpawnedChildren(assignments, ctxDir, parentNodeId, phase, deps
|
|
|
730
740
|
}
|
|
731
741
|
else {
|
|
732
742
|
const task = a.task ?? "";
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
signal: deps.signal,
|
|
739
|
-
ctxDir,
|
|
740
|
-
nodeId: childNodeId,
|
|
741
|
-
};
|
|
742
|
-
const invoke = () => run(spawnCwd, deps.agents, agentName, task, runOptions, deps.globalThinking);
|
|
743
|
-
// ctx_spawn is part of the phase's execution tree, not bookkeeping.
|
|
744
|
-
// Inherit the exact cwd capability and give every descendant a unique
|
|
745
|
-
// unit owner so no child can bypass lease/WAL/permit coordination.
|
|
746
|
-
const r = deps._workspaceBinding
|
|
747
|
-
? await deps._workspaceBinding.runAgent({
|
|
748
|
-
agents: deps.agents,
|
|
749
|
-
agentName,
|
|
750
|
-
task,
|
|
751
|
-
opts: runOptions,
|
|
752
|
-
globalThinking: deps.globalThinking,
|
|
753
|
-
unitId: childNodeId,
|
|
754
|
-
invoke,
|
|
755
|
-
})
|
|
756
|
-
: await invoke();
|
|
743
|
+
// Flat ctx_spawn children use the SAME runOne policy as their parent:
|
|
744
|
+
// phase timeout, idle watchdog, retries, budget accounting, tracing,
|
|
745
|
+
// prompt diagnostics, cwd capability, and workspace binding.
|
|
746
|
+
const usageBefore = ledger.usage;
|
|
747
|
+
const r = await runChild(agentName, task, childNodeId, usageBefore);
|
|
757
748
|
out = r.output ?? "";
|
|
758
749
|
if (isFailed(r)) {
|
|
759
750
|
const detail = sanitizeErrorMessage(r.errorMessage ?? r.stderr ?? "spawned child failed");
|
|
@@ -769,7 +760,7 @@ async function runSpawnedChildren(assignments, ctxDir, parentNodeId, phase, deps
|
|
|
769
760
|
// A child may itself have queued spawns — recurse (depth-capped by the tool).
|
|
770
761
|
const grand = drainPendingSpawns(ctxDir, childNodeId);
|
|
771
762
|
if (grand.length > 0 && !deps.signal?.aborted && !spawnedOverBudget(state, ledger.usage)) {
|
|
772
|
-
const rec = await runSpawnedChildren(grand, ctxDir, childNodeId, phase, deps, state,
|
|
763
|
+
const rec = await runSpawnedChildren(grand, ctxDir, childNodeId, phase, deps, state, runChild, ledger);
|
|
773
764
|
if (rec.reports)
|
|
774
765
|
out += rec.reports;
|
|
775
766
|
usages.push(rec.usage);
|
|
@@ -810,8 +801,10 @@ async function executePhase(phase, state, deps, prior, emitProgress, _retryDepth
|
|
|
810
801
|
emitUnreplayableMarker(deps, state, phase);
|
|
811
802
|
let result;
|
|
812
803
|
let threw = false;
|
|
804
|
+
const promptCalls = [];
|
|
805
|
+
const trackedOpts = { ...opts, promptCalls };
|
|
813
806
|
try {
|
|
814
|
-
result = await executePhaseImpl(phase, state, deps, prior, emitProgress, _retryDepth,
|
|
807
|
+
result = await executePhaseImpl(phase, state, deps, prior, emitProgress, _retryDepth, trackedOpts);
|
|
815
808
|
}
|
|
816
809
|
catch (e) {
|
|
817
810
|
threw = true;
|
|
@@ -825,6 +818,9 @@ async function executePhase(phase, state, deps, prior, emitProgress, _retryDepth
|
|
|
825
818
|
}
|
|
826
819
|
if (threw)
|
|
827
820
|
return result; // unreachable; satisfies TS
|
|
821
|
+
if (promptCalls.length > 0 && !result.cacheHit) {
|
|
822
|
+
setPromptStats(result, promptCalls);
|
|
823
|
+
}
|
|
828
824
|
// S1: cache-hit decision (within-run or cross-run) for fold/replay.
|
|
829
825
|
if (result.cacheHit) {
|
|
830
826
|
traceDecision(deps, state, phase.id, {
|
|
@@ -1053,7 +1049,13 @@ async function executePhaseImpl(phase, state, deps, prior, emitProgress, _retryD
|
|
|
1053
1049
|
async function executePhaseInner(phase, state, deps, prior, emitProgress, _retryDepth = 0, opts) {
|
|
1054
1050
|
const type = phase.type ?? "agent";
|
|
1055
1051
|
const concurrency = phase.concurrency ?? state.def.concurrency ?? 8;
|
|
1056
|
-
|
|
1052
|
+
// BREAKING (dogfood issue 1): a reduce phase's {previous.output} aggregates
|
|
1053
|
+
// ALL completed `from[]` outputs in from-array order (one → raw, many →
|
|
1054
|
+
// `### <id>\n\n<output>` joined by `\n\n---\n\n`). Other phase types keep the
|
|
1055
|
+
// historical "last completed dependency" behavior. The aggregated from-ids
|
|
1056
|
+
// are recorded as observed reads (below) so staleness tracks them.
|
|
1057
|
+
const reduceAgg = type === "reduce" ? aggregateReduceFrom(state, phase) : undefined;
|
|
1058
|
+
const previousOutput = reduceAgg ? reduceAgg.value : lastCompletedOutput(state, phase);
|
|
1057
1059
|
const run = deps.runTask ?? noRunnerInjected;
|
|
1058
1060
|
// Effective working directory for THIS phase's execution. When an isolated
|
|
1059
1061
|
// workspace was allocated (worktree isolation), `_cwdOverride` is its dir and
|
|
@@ -1087,6 +1089,14 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
1087
1089
|
const onRead = (ref) => {
|
|
1088
1090
|
readRefs.push(ref);
|
|
1089
1091
|
};
|
|
1092
|
+
// Record the reduce-aggregated from-ids as observed reads: the phase consumed
|
|
1093
|
+
// these upstream outputs (folded into {previous.output}) even when the task
|
|
1094
|
+
// references {previous.output} once (a single placeholder read would otherwise
|
|
1095
|
+
// record only "previous.output", not the real upstream ids).
|
|
1096
|
+
if (reduceAgg) {
|
|
1097
|
+
for (const id of reduceAgg.ids)
|
|
1098
|
+
readRefs.push(`steps.${id}.output`);
|
|
1099
|
+
}
|
|
1090
1100
|
const ctx = buildInterpolationContext(state, previousOutput, undefined, onRead);
|
|
1091
1101
|
// M3 observed-readSet: when conditions are part of the phase's real
|
|
1092
1102
|
// dependencies. Evaluate them inside executePhaseInner so every upstream
|
|
@@ -1158,21 +1168,60 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
1158
1168
|
agentScope: state.def.agentScope,
|
|
1159
1169
|
contextSharing: state.def.contextSharing === true,
|
|
1160
1170
|
agentDefinitions: agentDefinitionsIdentity(deps.agents),
|
|
1161
|
-
executionCwd:
|
|
1171
|
+
executionCwd: directoryIdentity(effCwd)?.canonicalPath ?? path.resolve(effCwd),
|
|
1162
1172
|
};
|
|
1163
|
-
|
|
1173
|
+
// Effective idle watchdog (ms): phase overrides flow; host default (300000)
|
|
1174
|
+
// applies when neither sets it. `0` disables the watchdog (validation already
|
|
1175
|
+
// required a finite wall `timeout` in that case). Threaded into RunOptions so
|
|
1176
|
+
// every host runner that delegates to runSubagentProcess honors it.
|
|
1177
|
+
const effIdleTimeoutMs = resolveIdleTimeoutMs(phase, state.def);
|
|
1178
|
+
const baseRun = (agentName, task, onLive, ctxNodeId, signal, callCwd, onTerminalCommit) => {
|
|
1179
|
+
const invocationCwd = callCwd ? path.resolve(deps.cwd, callCwd) : effCwd;
|
|
1180
|
+
// A per-branch cwd cannot replace a phase-level workspace/cwd-bridge
|
|
1181
|
+
// binding: that would bypass the binding's containment/dirty-state
|
|
1182
|
+
// lifecycle. Validation rejects this shape; keep a runtime fail-closed
|
|
1183
|
+
// guard for direct callers or legacy persisted definitions.
|
|
1184
|
+
if (callCwd && (deps._workspaceBinding || isWorkspaceKeyword(phase.cwd))) {
|
|
1185
|
+
return Promise.resolve({
|
|
1186
|
+
agent: agentName,
|
|
1187
|
+
task,
|
|
1188
|
+
exitCode: 1,
|
|
1189
|
+
output: "",
|
|
1190
|
+
stderr: "TF_CWD_BRANCH_BINDING_CONFLICT: per-branch cwd cannot override a phase workspace binding",
|
|
1191
|
+
usage: emptyUsage(),
|
|
1192
|
+
stopReason: "error",
|
|
1193
|
+
errorMessage: "TF_CWD_BRANCH_BINDING_CONFLICT: per-branch cwd cannot override a phase workspace binding",
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
if (callCwd && deps._cwdBoundary) {
|
|
1197
|
+
const selected = directoryIdentity(invocationCwd);
|
|
1198
|
+
if (!selected || !isPathWithin(deps._cwdBoundary, selected.canonicalPath)) {
|
|
1199
|
+
return Promise.resolve({
|
|
1200
|
+
agent: agentName,
|
|
1201
|
+
task,
|
|
1202
|
+
exitCode: 1,
|
|
1203
|
+
output: "",
|
|
1204
|
+
stderr: "TF_CWD_BOUNDARY_ESCAPE: per-branch cwd must select an existing directory inside the inherited cwd boundary",
|
|
1205
|
+
usage: emptyUsage(),
|
|
1206
|
+
stopReason: "error",
|
|
1207
|
+
errorMessage: "TF_CWD_BOUNDARY_ESCAPE: per-branch cwd must select an existing directory inside the inherited cwd boundary",
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
opts?.promptCalls?.push(task);
|
|
1164
1212
|
const runOptions = {
|
|
1165
1213
|
model: phase.model,
|
|
1166
1214
|
thinking: phase.thinking,
|
|
1167
1215
|
tools: phase.tools,
|
|
1168
|
-
cwd:
|
|
1216
|
+
cwd: invocationCwd,
|
|
1169
1217
|
signal: signal ?? deps.signal,
|
|
1170
1218
|
onLive,
|
|
1171
1219
|
ctxDir: ctxDir,
|
|
1172
1220
|
nodeId: ctxDir ? ctxNodeId : undefined,
|
|
1221
|
+
idleTimeoutMs: effIdleTimeoutMs,
|
|
1173
1222
|
onTerminalCommit,
|
|
1174
1223
|
};
|
|
1175
|
-
const invoke = () => run(
|
|
1224
|
+
const invoke = () => run(invocationCwd, deps.agents, agentName, task, runOptions, deps.globalThinking);
|
|
1176
1225
|
return deps._workspaceBinding
|
|
1177
1226
|
? deps._workspaceBinding.runAgent({
|
|
1178
1227
|
agents: deps.agents,
|
|
@@ -1204,7 +1253,18 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
1204
1253
|
: undefined;
|
|
1205
1254
|
const runOne = async (agentName, task, onLive, ctxNodeId, check,
|
|
1206
1255
|
/** Extra abort (e.g. race branch cancelLosers) — chained with run + phase timeout. */
|
|
1207
|
-
extraSignal
|
|
1256
|
+
extraSignal,
|
|
1257
|
+
/** Per-call cwd override (e.g. a parallel branch's literal cwd). Falls back
|
|
1258
|
+
* to the phase effective cwd when absent. Workspace keywords are rejected
|
|
1259
|
+
* by validation for branches, so this is always a literal path or undefined. */
|
|
1260
|
+
callCwd,
|
|
1261
|
+
/** Usage already spent by earlier calls inside this same multi-call phase
|
|
1262
|
+
* (tree reduce). Included in live budget accounting, but not returned as
|
|
1263
|
+
* this call's own usage. */
|
|
1264
|
+
usageBefore,
|
|
1265
|
+
/** Optional hard admission guard invoked immediately before EVERY actual
|
|
1266
|
+
* runner attempt. Return an error message to deny that attempt. */
|
|
1267
|
+
beforeAttempt) => {
|
|
1208
1268
|
const explicitMax = Math.max(1, 1 + Math.max(0, Math.floor(retry?.max ?? 0)));
|
|
1209
1269
|
// Allow enough attempts to cover whichever policy applies on a given attempt.
|
|
1210
1270
|
const maxAttempts = Math.max(explicitMax, 1 + DEFAULT_TRANSIENT_RETRIES);
|
|
@@ -1213,6 +1273,20 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
1213
1273
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1214
1274
|
if (deps.signal?.aborted || extraSignal?.aborted)
|
|
1215
1275
|
break;
|
|
1276
|
+
const admissionError = beforeAttempt?.();
|
|
1277
|
+
if (admissionError) {
|
|
1278
|
+
last = {
|
|
1279
|
+
agent: agentName,
|
|
1280
|
+
task,
|
|
1281
|
+
exitCode: 1,
|
|
1282
|
+
output: "",
|
|
1283
|
+
stderr: admissionError,
|
|
1284
|
+
usage: emptyUsage(),
|
|
1285
|
+
stopReason: "error",
|
|
1286
|
+
errorMessage: admissionError,
|
|
1287
|
+
};
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1216
1290
|
// AbortController chains: run signal + optional extra (race cancel) + phase timeout.
|
|
1217
1291
|
// Deterministic: a timed-out call is never retried (would double-spend).
|
|
1218
1292
|
let timedOut = false;
|
|
@@ -1251,7 +1325,7 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
1251
1325
|
timer = undefined;
|
|
1252
1326
|
}
|
|
1253
1327
|
};
|
|
1254
|
-
const invocation = baseRun(agentName, task, onLive, ctxNodeId, callSignal, onTerminalCommit);
|
|
1328
|
+
const invocation = baseRun(agentName, task, onLive, ctxNodeId, callSignal, callCwd, onTerminalCommit);
|
|
1255
1329
|
if (phaseTimeoutMs && timeoutController) {
|
|
1256
1330
|
const timeoutFallback = new Promise((resolve) => {
|
|
1257
1331
|
timer = setTimeout(() => {
|
|
@@ -1332,7 +1406,7 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
1332
1406
|
// so the TUI / budget guard see the in-flight spend on every attempt.
|
|
1333
1407
|
const liveRetry = state.phases[phase.id];
|
|
1334
1408
|
if (liveRetry)
|
|
1335
|
-
liveRetry.usage = aggregateUsage(usages);
|
|
1409
|
+
liveRetry.usage = aggregateUsage(usageBefore ? [usageBefore, ...usages] : usages);
|
|
1336
1410
|
// Persist every attempt, not only the final aggregate. This is required
|
|
1337
1411
|
// for honest replay/cost accounting when a transient or explicit retry
|
|
1338
1412
|
// succeeds after earlier spend.
|
|
@@ -1432,11 +1506,33 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
1432
1506
|
attempts: 0,
|
|
1433
1507
|
};
|
|
1434
1508
|
}
|
|
1435
|
-
if (usages.length >
|
|
1509
|
+
if (usages.length > 0)
|
|
1436
1510
|
last.usage = aggregateUsage(usages);
|
|
1437
1511
|
last.attempts = usages.length;
|
|
1438
1512
|
return last;
|
|
1439
1513
|
};
|
|
1514
|
+
const runSpawnedChild = async (agentName, task, childNodeId, spawnedUsageBefore) => {
|
|
1515
|
+
// runOne updates the live phase usage so retry/budget admission sees current
|
|
1516
|
+
// spend. A spawned child is folded into the parent result later, so restore
|
|
1517
|
+
// the parent's pre-child usage after the call to avoid replacing/doubling it.
|
|
1518
|
+
const livePhase = state.phases[phase.id];
|
|
1519
|
+
const parentUsage = livePhase?.usage ? { ...livePhase.usage } : undefined;
|
|
1520
|
+
const totalUsageBefore = aggregateUsage([
|
|
1521
|
+
parentUsage ?? emptyUsage(),
|
|
1522
|
+
spawnedUsageBefore ?? emptyUsage(),
|
|
1523
|
+
]);
|
|
1524
|
+
try {
|
|
1525
|
+
return await runOne(agentName, task, undefined, childNodeId, undefined, undefined, undefined, totalUsageBefore);
|
|
1526
|
+
}
|
|
1527
|
+
finally {
|
|
1528
|
+
if (livePhase) {
|
|
1529
|
+
if (parentUsage)
|
|
1530
|
+
livePhase.usage = parentUsage;
|
|
1531
|
+
else
|
|
1532
|
+
livePhase.usage = undefined;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1440
1536
|
const parseJson = phase.output === "json";
|
|
1441
1537
|
// Output contract (`expect`): validates the parsed JSON output of a finished
|
|
1442
1538
|
// subagent call. Wired into `runOne` so a violation counts as a failed attempt
|
|
@@ -1531,12 +1627,16 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
1531
1627
|
if (l.model)
|
|
1532
1628
|
latestModel = l.model;
|
|
1533
1629
|
refresh();
|
|
1534
|
-
}, ctxDir ? nodeIdFor(String(idx)) : undefined);
|
|
1630
|
+
}, ctxDir ? nodeIdFor(String(idx)) : undefined, undefined, undefined, it.cwd);
|
|
1535
1631
|
running--;
|
|
1536
1632
|
done++;
|
|
1537
1633
|
if (isFailed(r))
|
|
1538
1634
|
failed++;
|
|
1539
1635
|
liveUsages[idx] = r.usage;
|
|
1636
|
+
// Publish the just-finished sibling's spend before considering any
|
|
1637
|
+
// ctx_spawn intents. Budgeted fan-out allows one atomic call to cross the
|
|
1638
|
+
// ceiling, but must not admit descendants after that overshoot.
|
|
1639
|
+
refresh();
|
|
1540
1640
|
// Per-item cross-run cache record (map only): persist a successful fresh
|
|
1541
1641
|
// item so a later run with this item unchanged hits per-item instead of
|
|
1542
1642
|
// re-running. Failed and budget-skipped items are never cached (a stale
|
|
@@ -1564,7 +1664,7 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
1564
1664
|
// post-run drain below only covers single-agent phases).
|
|
1565
1665
|
const spawned = drainPendingSpawns(ctxDir, itemNid);
|
|
1566
1666
|
if (spawned.length > 0 && !deps.signal?.aborted && !overBudget(state).over) {
|
|
1567
|
-
const child = await runSpawnedChildren(spawned, ctxDir, itemNid, phase, deps, state,
|
|
1667
|
+
const child = await runSpawnedChildren(spawned, ctxDir, itemNid, phase, deps, state, runSpawnedChild, { usage: emptyUsage() });
|
|
1568
1668
|
if (child.reports)
|
|
1569
1669
|
r.output = `${r.output ?? ""}${child.reports}`;
|
|
1570
1670
|
if (child.failed && deps._workspaceBinding) {
|
|
@@ -1587,6 +1687,96 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
1587
1687
|
// Single-agent phases: agent, gate, and reduce all run one subagent on an
|
|
1588
1688
|
// interpolated task. gate additionally parses a verdict; reduce simply pulls
|
|
1589
1689
|
// its inputs from `from` phases (already exposed via interpolation).
|
|
1690
|
+
//
|
|
1691
|
+
// Tree reduce (`reduceStrategy: "tree"`) is handled FIRST: it runs batched
|
|
1692
|
+
// intermediate reducer calls over the aggregated `from[]` inputs, reusing
|
|
1693
|
+
// `runOne` so retry/timeout/budget/idleTimeout behavior is identical to a
|
|
1694
|
+
// one-shot call. It forces the imperative runtime (the event kernel falls
|
|
1695
|
+
// back via kernelUnsupportedReason). The corrected `{previous.output}`
|
|
1696
|
+
// aggregation (all completed `from[]` sources) applies to EVERY round.
|
|
1697
|
+
if (type === "reduce" && phase.reduceStrategy === "tree") {
|
|
1698
|
+
const stratBs = phase.batchSize;
|
|
1699
|
+
const batchSize = typeof stratBs === "number" && Number.isFinite(stratBs) && stratBs >= 2 ? Math.floor(stratBs) : 2;
|
|
1700
|
+
const { collectTreeReduceInputs, executeTreeReduction, treeReduceCacheParts, } = await import("./runtime/phases/reduce.js");
|
|
1701
|
+
const inputs = collectTreeReduceInputs(state, phase);
|
|
1702
|
+
// 0–1 completed inputs: tree reduction is a no-op — fall through to the
|
|
1703
|
+
// one-shot path (which handles the degenerate case correctly).
|
|
1704
|
+
if (inputs.length >= 2) {
|
|
1705
|
+
const agentName = resolveAgent(phase.agent, deps, state);
|
|
1706
|
+
const cacheKey = cacheKeys(cc, treeReduceCacheParts(state, phase, inputs, batchSize));
|
|
1707
|
+
const cached = cachedPhase(cc, cacheKey);
|
|
1708
|
+
if (cached)
|
|
1709
|
+
return cached;
|
|
1710
|
+
const execution = await executeTreeReduction({
|
|
1711
|
+
phase,
|
|
1712
|
+
inputs,
|
|
1713
|
+
batchSize,
|
|
1714
|
+
agentName,
|
|
1715
|
+
inputHash: cacheKey.key,
|
|
1716
|
+
isAborted: () => deps.signal?.aborted === true,
|
|
1717
|
+
isOverBudget: () => overBudget(state).over,
|
|
1718
|
+
resolveTask: (batchValue) => {
|
|
1719
|
+
const batchCtx = buildInterpolationContext(state, batchValue, undefined, onRead);
|
|
1720
|
+
const interp = interpolate(phase.task ?? "", batchCtx);
|
|
1721
|
+
return {
|
|
1722
|
+
task: appendGateFormatSuffix(preRead + interp.text, phase),
|
|
1723
|
+
warning: warnUnresolvedRefs(phase.id, interp.missing),
|
|
1724
|
+
};
|
|
1725
|
+
},
|
|
1726
|
+
runOne: async (task, usageBefore, beforeAttempt, callId) => {
|
|
1727
|
+
const batchNodeId = nodeIdFor(`tree-${callId}`);
|
|
1728
|
+
if (ctxDir) {
|
|
1729
|
+
try {
|
|
1730
|
+
registerNode(ctxDir, batchNodeId, phase.id, undefined, "running");
|
|
1731
|
+
}
|
|
1732
|
+
catch { /* fail-open */ }
|
|
1733
|
+
}
|
|
1734
|
+
const result = await runOne(agentName, task, liveSink(state, phase.id, emitProgress), batchNodeId, contractCheck, undefined, undefined, usageBefore, beforeAttempt);
|
|
1735
|
+
if (ctxDir) {
|
|
1736
|
+
try {
|
|
1737
|
+
const spawned = drainPendingSpawns(ctxDir, batchNodeId);
|
|
1738
|
+
if (spawned.length > 0 && !deps.signal?.aborted && !overBudget(state).over) {
|
|
1739
|
+
const child = await runSpawnedChildren(spawned, ctxDir, batchNodeId, phase, deps, state, runSpawnedChild, { usage: emptyUsage() });
|
|
1740
|
+
if (child.reports)
|
|
1741
|
+
result.output = `${result.output ?? ""}${child.reports}`;
|
|
1742
|
+
if (child.failed && deps._workspaceBinding) {
|
|
1743
|
+
result.exitCode = result.exitCode === 0 ? 1 : result.exitCode;
|
|
1744
|
+
result.stopReason = "error";
|
|
1745
|
+
result.errorMessage = `Workspace ctx_spawn descendant failed: ${child.errors.join("; ")}`;
|
|
1746
|
+
}
|
|
1747
|
+
result.usage = aggregateUsage([result.usage ?? emptyUsage(), child.usage]);
|
|
1748
|
+
}
|
|
1749
|
+
setNodeStatus(ctxDir, batchNodeId, isFailed(result) ? "failed" : "done");
|
|
1750
|
+
}
|
|
1751
|
+
catch { /* fail-open */ }
|
|
1752
|
+
}
|
|
1753
|
+
const livePhase = state.phases[phase.id];
|
|
1754
|
+
if (livePhase) {
|
|
1755
|
+
livePhase.usage = aggregateUsage([
|
|
1756
|
+
usageBefore ?? emptyUsage(),
|
|
1757
|
+
result.usage ?? emptyUsage(),
|
|
1758
|
+
]);
|
|
1759
|
+
}
|
|
1760
|
+
return result;
|
|
1761
|
+
},
|
|
1762
|
+
});
|
|
1763
|
+
const ps = execution.phaseState;
|
|
1764
|
+
if (parseJson && ps.status === "done")
|
|
1765
|
+
ps.json = safeParse(ps.output ?? "");
|
|
1766
|
+
if (readRefs.length)
|
|
1767
|
+
ps.reads = readRefsToReads(readRefs, state);
|
|
1768
|
+
if (execution.refWarning)
|
|
1769
|
+
ps.warnings = [...(ps.warnings ?? []), execution.refWarning];
|
|
1770
|
+
if (ps.budgetTruncated) {
|
|
1771
|
+
ps.warnings = [...(ps.warnings ?? []), "tree reduction stopped by the run budget; output is partial"];
|
|
1772
|
+
}
|
|
1773
|
+
attachReduceInputStats(ps, state, phase);
|
|
1774
|
+
if (execution.cacheable)
|
|
1775
|
+
recordCache(cc, ps);
|
|
1776
|
+
return ps;
|
|
1777
|
+
}
|
|
1778
|
+
// inputs.length < 2 → fall through to one-shot (below).
|
|
1779
|
+
}
|
|
1590
1780
|
if (type === "agent" || type === "gate" || type === "reduce") {
|
|
1591
1781
|
// Eval gate: zero-token machine checks before the LLM gate.
|
|
1592
1782
|
if (type === "gate" && Array.isArray(phase.eval) && phase.eval.length > 0) {
|
|
@@ -1876,6 +2066,11 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
1876
2066
|
ps.reads = readRefsToReads(readRefs, state);
|
|
1877
2067
|
if (refWarning)
|
|
1878
2068
|
ps.warnings = [...(ps.warnings ?? []), refWarning];
|
|
2069
|
+
// Prompt-size diagnostics for the single agent call (durable).
|
|
2070
|
+
attachPromptStats(ps, [fullTask]);
|
|
2071
|
+
// reduce: record aggregate input stats.
|
|
2072
|
+
if (type === "reduce")
|
|
2073
|
+
attachReduceInputStats(ps, state, phase);
|
|
1879
2074
|
if (type === "gate" && ps.status === "done") {
|
|
1880
2075
|
ps.gate = parseGateVerdict(r.output);
|
|
1881
2076
|
// Trace: gate decision (fail-open). Replay re-adjudicates thresholds.
|
|
@@ -1892,7 +2087,7 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
1892
2087
|
registerNode(ctxDir, nid, phase.id, undefined, ps.status === "failed" ? "failed" : "done");
|
|
1893
2088
|
const spawned = drainPendingSpawns(ctxDir, nid);
|
|
1894
2089
|
if (spawned.length > 0 && !deps.signal?.aborted && !overBudget(state).over) {
|
|
1895
|
-
const child = await runSpawnedChildren(spawned, ctxDir, nid, phase, deps, state,
|
|
2090
|
+
const child = await runSpawnedChildren(spawned, ctxDir, nid, phase, deps, state, runSpawnedChild, { usage: emptyUsage() });
|
|
1896
2091
|
if (child.reports)
|
|
1897
2092
|
ps.output = `${ps.output ?? ""}${child.reports}`;
|
|
1898
2093
|
if (child.failed && deps._workspaceBinding) {
|
|
@@ -2024,6 +2219,7 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
|
|
|
2024
2219
|
return {
|
|
2025
2220
|
agent: resolveAgent(b.agent ?? phase.agent, deps, state),
|
|
2026
2221
|
task: preRead + r.text,
|
|
2222
|
+
cwd: typeof b.cwd === "string" ? resolveBranchCwd(deps, b.cwd) : undefined,
|
|
2027
2223
|
};
|
|
2028
2224
|
});
|
|
2029
2225
|
const ck = cacheKeys(cc, [phase.id, phase.model ?? "", JSON.stringify(branches)]);
|
|
@@ -2835,6 +3031,147 @@ function lastCompletedOutput(state, phase) {
|
|
|
2835
3031
|
}
|
|
2836
3032
|
return undefined;
|
|
2837
3033
|
}
|
|
3034
|
+
/** A conservative prompt-size warning threshold in estimated tokens. Crossed
|
|
3035
|
+
* → the phase records a `warnings` entry so an author notices they may be
|
|
3036
|
+
* approaching a model's context limit. Approximate (ceil(chars/4)), not a
|
|
3037
|
+
* real tokenizer count. Chosen conservatively well below typical 128K context
|
|
3038
|
+
* windows so the warning fires with ample headroom. */
|
|
3039
|
+
export const PROMPT_SIZE_WARN_TOKENS = 32_000;
|
|
3040
|
+
/** Compute durable prompt-size diagnostics for a resolved prompt string:
|
|
3041
|
+
* exact UTF-8 byte count, character count, and a documented approximate token
|
|
3042
|
+
* estimate (ceil(chars/4)). Never throws. */
|
|
3043
|
+
export function promptSizeStats(text) {
|
|
3044
|
+
let chars = 0;
|
|
3045
|
+
for (const _char of text)
|
|
3046
|
+
chars++; // Unicode code points, not UTF-16 code units
|
|
3047
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
3048
|
+
const estTokens = Math.ceil(chars / 4);
|
|
3049
|
+
return { bytes, chars, estTokens };
|
|
3050
|
+
}
|
|
3051
|
+
/** Replace a phase's call diagnostics with the exact prompts captured by the
|
|
3052
|
+
* shared runner boundary. This covers single calls, retries, fan-out items,
|
|
3053
|
+
* loop/tournament/judge calls, and tree-reduce rounds without per-branch drift. */
|
|
3054
|
+
function setPromptStats(ps, prompts) {
|
|
3055
|
+
try {
|
|
3056
|
+
const calls = prompts.map((text) => promptSizeStats(text));
|
|
3057
|
+
const reduceInputs = ps.promptStats?.reduceInputs;
|
|
3058
|
+
ps.promptStats = { calls, ...(reduceInputs ? { reduceInputs } : {}) };
|
|
3059
|
+
ps.warnings = (ps.warnings ?? []).filter((warning) => !warning.startsWith("Prompt size ≈"));
|
|
3060
|
+
for (const call of calls) {
|
|
3061
|
+
if (call.estTokens >= PROMPT_SIZE_WARN_TOKENS) {
|
|
3062
|
+
ps.warnings.push(`Prompt size ≈${call.estTokens} tokens (${call.chars} chars, ${call.bytes} bytes) exceeds the conservative ${PROMPT_SIZE_WARN_TOKENS}-token warning threshold — the prompt may be approaching a model's context limit.`);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
if (ps.warnings.length === 0)
|
|
3066
|
+
delete ps.warnings;
|
|
3067
|
+
}
|
|
3068
|
+
catch {
|
|
3069
|
+
/* diagnostics must never sink the phase */
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
/** Attach durable prompt-size diagnostics to a PhaseState. `prompts` is the list
|
|
3073
|
+
* of resolved prompt strings actually sent to a subagent (one entry per call —
|
|
3074
|
+
* one for an agent phase, multiple for a tree reduce). Each becomes a
|
|
3075
|
+
* `{bytes, chars, estTokens}` record on `ps.promptStats.calls`. When any call
|
|
3076
|
+
* crosses {@link PROMPT_SIZE_WARN_TOKENS}, a `warnings` entry is appended so an
|
|
3077
|
+
* author notices they may be approaching a model's context limit (the estimate
|
|
3078
|
+
* is conservative: ceil(chars/4), not a real tokenizer count). Never throws. */
|
|
3079
|
+
function attachPromptStats(ps, prompts) {
|
|
3080
|
+
try {
|
|
3081
|
+
const calls = prompts.map((t) => promptSizeStats(t));
|
|
3082
|
+
const existing = ps.promptStats;
|
|
3083
|
+
ps.promptStats = existing
|
|
3084
|
+
? { ...existing, calls: [...existing.calls, ...calls] }
|
|
3085
|
+
: { calls };
|
|
3086
|
+
for (const c of calls) {
|
|
3087
|
+
if (c.estTokens >= PROMPT_SIZE_WARN_TOKENS) {
|
|
3088
|
+
ps.warnings = [...(ps.warnings ?? []), `Prompt size ≈${c.estTokens} tokens (${c.chars} chars, ${c.bytes} bytes) exceeds the conservative ${PROMPT_SIZE_WARN_TOKENS}-token warning threshold — the prompt may be approaching a model's context limit.`];
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
catch {
|
|
3093
|
+
/* diagnostics must never sink the phase */
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
/** Attach aggregate input stats for a reduce phase: count + total bytes/chars/
|
|
3097
|
+
* estTokens over the completed `from[]` inputs being reduced. Stored on
|
|
3098
|
+
* `ps.promptStats.reduceInputs` so post-hoc inspection can account for input
|
|
3099
|
+
* size across reduce rounds. Never throws. */
|
|
3100
|
+
function attachReduceInputStats(ps, state, phase) {
|
|
3101
|
+
try {
|
|
3102
|
+
const s = reduceInputStats(state, phase);
|
|
3103
|
+
const existing = ps.promptStats;
|
|
3104
|
+
ps.promptStats = existing
|
|
3105
|
+
? { ...existing, reduceInputs: s }
|
|
3106
|
+
: { calls: [], reduceInputs: s };
|
|
3107
|
+
}
|
|
3108
|
+
catch {
|
|
3109
|
+
/* diagnostics must never sink the phase */
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
/** Format a single reduce-from input as a labeled section. */
|
|
3113
|
+
function formatReduceInput(id, output) {
|
|
3114
|
+
return `### ${id}\n\n${output}`;
|
|
3115
|
+
}
|
|
3116
|
+
/** Aggregate completed `from[]` outputs for a reduce phase's {previous.output}.
|
|
3117
|
+
*
|
|
3118
|
+
* BREAKING correction (dogfood issue 1): a reduce phase's {previous.output}
|
|
3119
|
+
* resolves to ALL completed `from[]` outputs in from-array order — not just
|
|
3120
|
+
* the last completed dependency. One completed input → its raw output.
|
|
3121
|
+
* Multiple → `### <id>\n\n<output>` sections joined by `\n\n---\n\n`.
|
|
3122
|
+
* `join:"any"` includes only completed branches (skipped/failed are omitted).
|
|
3123
|
+
*
|
|
3124
|
+
* Returns `{ value, ids }` where `ids` are the from-ids actually aggregated
|
|
3125
|
+
* (so the runtime can record them as observed reads). `value` is undefined
|
|
3126
|
+
* when no `from[]` phase completed (e.g. all skipped under join:any). */
|
|
3127
|
+
function aggregateReduceFrom(state, phase) {
|
|
3128
|
+
const fromIds = asArray(phase.from);
|
|
3129
|
+
const completed = [];
|
|
3130
|
+
for (const id of fromIds) {
|
|
3131
|
+
const ps = state.phases[id];
|
|
3132
|
+
if (ps?.status === "done" && ps.output !== undefined) {
|
|
3133
|
+
completed.push({ id, output: ps.output });
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
if (completed.length === 0)
|
|
3137
|
+
return { value: undefined, ids: [] };
|
|
3138
|
+
if (completed.length === 1)
|
|
3139
|
+
return { value: completed[0].output, ids: [completed[0].id] };
|
|
3140
|
+
const value = completed.map((c) => formatReduceInput(c.id, c.output)).join("\n\n---\n\n");
|
|
3141
|
+
return { value, ids: completed.map((c) => c.id) };
|
|
3142
|
+
}
|
|
3143
|
+
/** Aggregate stats over the completed `from[]` inputs for reduce diagnostics. */
|
|
3144
|
+
function reduceInputStats(state, phase) {
|
|
3145
|
+
const fromIds = asArray(phase.from);
|
|
3146
|
+
let count = 0;
|
|
3147
|
+
let totalBytes = 0;
|
|
3148
|
+
let totalChars = 0;
|
|
3149
|
+
let totalEstTokens = 0;
|
|
3150
|
+
for (const id of fromIds) {
|
|
3151
|
+
const ps = state.phases[id];
|
|
3152
|
+
if (ps?.status === "done" && ps.output !== undefined) {
|
|
3153
|
+
count++;
|
|
3154
|
+
const s = promptSizeStats(ps.output);
|
|
3155
|
+
totalBytes += s.bytes;
|
|
3156
|
+
totalChars += s.chars;
|
|
3157
|
+
totalEstTokens += s.estTokens;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
return { count, totalBytes, totalChars, totalEstTokens };
|
|
3161
|
+
}
|
|
3162
|
+
/** Resolve the effective idle-watchdog ms for a phase: phase overrides flow;
|
|
3163
|
+
* returns `undefined` when neither sets it (host default 300000 applies).
|
|
3164
|
+
* `0` is passed through (disables the watchdog — validation already ensured a
|
|
3165
|
+
* finite wall `timeout` exists in that case). */
|
|
3166
|
+
export function resolveIdleTimeoutMs(phase, def) {
|
|
3167
|
+
const p = phase.idleTimeout;
|
|
3168
|
+
if (typeof p === "number" && Number.isFinite(p))
|
|
3169
|
+
return p;
|
|
3170
|
+
const f = def.idleTimeout;
|
|
3171
|
+
if (typeof f === "number" && Number.isFinite(f))
|
|
3172
|
+
return f;
|
|
3173
|
+
return undefined;
|
|
3174
|
+
}
|
|
2838
3175
|
/** Stable cache identity for the fully resolved agent pool. File paths are
|
|
2839
3176
|
* excluded: content/config, not installation location, determines output. */
|
|
2840
3177
|
export function agentDefinitionsIdentity(agents) {
|
|
@@ -3567,6 +3904,8 @@ async function runTaskflowLayers(state, deps) {
|
|
|
3567
3904
|
let gateBlocked = false;
|
|
3568
3905
|
let gateReason = "";
|
|
3569
3906
|
let gateOutput = "";
|
|
3907
|
+
/** Id of the blocking gate/approval phase (source of `gateOutput`). */
|
|
3908
|
+
let gatePhaseId;
|
|
3570
3909
|
// `budgetBlocked` gates the skipping of remaining phases once the cap is hit
|
|
3571
3910
|
// and also drives the terminal "blocked" status — a maxUSD ceiling must never
|
|
3572
3911
|
// silently do nothing.
|
|
@@ -3674,6 +4013,7 @@ async function runTaskflowLayers(state, deps) {
|
|
|
3674
4013
|
gateBlocked = true;
|
|
3675
4014
|
gateReason = ps.gate.reason ?? "";
|
|
3676
4015
|
gateOutput = ps.output ?? "";
|
|
4016
|
+
gatePhaseId = phase.id;
|
|
3677
4017
|
}
|
|
3678
4018
|
// A fan-out cut short by the cap is itself a budget skip.
|
|
3679
4019
|
if (ps.budgetTruncated) {
|
|
@@ -3712,15 +4052,6 @@ async function runTaskflowLayers(state, deps) {
|
|
|
3712
4052
|
break;
|
|
3713
4053
|
}
|
|
3714
4054
|
}
|
|
3715
|
-
const fp = finalPhase(def.phases);
|
|
3716
|
-
let finalState = state.phases[fp.id];
|
|
3717
|
-
// If the designated final phase produced no output (skipped/blocked), fall
|
|
3718
|
-
// back to the last phase (in definition order) that actually completed.
|
|
3719
|
-
if (!finalState || finalState.status !== "done") {
|
|
3720
|
-
const doneInOrder = def.phases.map((p) => state.phases[p.id]).filter((p) => p?.status === "done");
|
|
3721
|
-
if (doneInOrder.length)
|
|
3722
|
-
finalState = doneInOrder[doneInOrder.length - 1];
|
|
3723
|
-
}
|
|
3724
4055
|
// A failed non-optional phase fails the run; optional failures are tolerated.
|
|
3725
4056
|
const anyFailed = Object.entries(state.phases).some(([id, p]) => p.status === "failed" && !byId.get(id)?.optional && !p.optional);
|
|
3726
4057
|
state.status = aborted
|
|
@@ -3731,13 +4062,14 @@ async function runTaskflowLayers(state, deps) {
|
|
|
3731
4062
|
? "failed"
|
|
3732
4063
|
: "completed";
|
|
3733
4064
|
safeEmit(deps, state);
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
4065
|
+
const { finalOutput, outputSourcePhaseId } = resolveFinalOutput(def.phases, state, {
|
|
4066
|
+
gate: gateBlocked,
|
|
4067
|
+
gateReason,
|
|
4068
|
+
gateOutput,
|
|
4069
|
+
gatePhaseId,
|
|
4070
|
+
budget: budgetBlocked,
|
|
4071
|
+
budgetReason,
|
|
4072
|
+
});
|
|
3741
4073
|
const totalUsage = aggregateUsage(Object.values(state.phases).map((p) => p.usage ?? emptyUsage()));
|
|
3742
4074
|
return {
|
|
3743
4075
|
state,
|
|
@@ -3745,6 +4077,7 @@ async function runTaskflowLayers(state, deps) {
|
|
|
3745
4077
|
ok: state.status === "completed",
|
|
3746
4078
|
totalUsage,
|
|
3747
4079
|
reuse: summarizeReuse(state),
|
|
4080
|
+
outputSourcePhaseId,
|
|
3748
4081
|
};
|
|
3749
4082
|
}
|
|
3750
4083
|
//# sourceMappingURL=runtime.js.map
|