cool-workflow 0.2.4 → 0.2.6
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini-plugin/mcp.json +10 -0
- package/.gemini-plugin/plugin.json +40 -0
- package/.opencode-plugin/mcp.json +10 -0
- package/.opencode-plugin/plugin.json +40 -0
- package/README.md +204 -48
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/cli/dispatch.js +32 -4
- package/dist/cli/entry.js +11 -19
- package/dist/cli/global-flags.js +113 -0
- package/dist/cli/parseargv.js +7 -1
- package/dist/core/capability-data.js +337 -8
- package/dist/core/format/help.js +73 -3
- package/dist/core/format/recovery-hint.js +32 -0
- package/dist/core/multi-agent/collaboration.js +35 -6
- package/dist/core/multi-agent/runtime.js +7 -0
- package/dist/core/multi-agent/trust-policy.js +7 -1
- package/dist/core/pipeline/contract.js +7 -0
- package/dist/core/pipeline/error-feedback.js +2 -2
- package/dist/core/state/run-paths.js +3 -30
- package/dist/core/trust/evidence-grounding.js +13 -1
- package/dist/core/util/cli-args.js +22 -0
- package/dist/core/version.js +1 -1
- package/dist/mcp/dispatch.js +22 -2
- package/dist/mcp/server.js +215 -36
- package/dist/mcp/tool-process.js +181 -0
- package/dist/mcp-server.js +28 -1
- package/dist/shell/agent-config.js +11 -2
- package/dist/shell/audit-cli.js +1 -1
- package/dist/shell/commit.js +8 -2
- package/dist/shell/coordinator-io.js +73 -1
- package/dist/shell/drive.js +195 -128
- package/dist/shell/error-feedback-io.js +6 -0
- package/dist/shell/execution-backend/agent.js +201 -26
- package/dist/shell/execution-backend/container.js +44 -10
- package/dist/shell/execution-backend/local.js +33 -0
- package/dist/shell/fs-atomic.js +96 -12
- package/dist/shell/ledger-cli.js +9 -2
- package/dist/shell/metrics-cli.js +1 -1
- package/dist/shell/multi-agent-cli.js +9 -2
- package/dist/shell/multi-agent-operator-ux.js +1 -1
- package/dist/shell/observability.js +7 -4
- package/dist/shell/onramp.js +48 -5
- package/dist/shell/operator-ux.js +1 -1
- package/dist/shell/perf-trace.js +136 -0
- package/dist/shell/pipeline.js +3 -2
- package/dist/shell/reclamation-io.js +10 -9
- package/dist/shell/report-view-cli.js +2 -0
- package/dist/shell/run-export-cli.js +5 -2
- package/dist/shell/run-export.js +293 -31
- package/dist/shell/run-registry-io.js +8 -0
- package/dist/shell/run-store.js +183 -3
- package/dist/shell/scheduler-io.js +101 -10
- package/dist/shell/state-explosion-cli.js +4 -1
- package/dist/shell/telemetry-demo.js +1 -1
- package/dist/shell/telemetry-ledger-io.js +36 -24
- package/dist/shell/trust-audit.js +178 -36
- package/dist/shell/workbench-host.js +121 -10
- package/dist/shell/workbench.js +83 -6
- package/dist/shell/worker-isolation.js +1 -1
- package/dist/wiring/capability-table/basics.js +5 -0
- package/dist/wiring/capability-table/exec-backend.js +40 -22
- package/dist/wiring/capability-table/parity.js +10 -5
- package/dist/wiring/capability-table/pipeline.js +32 -0
- package/dist/wiring/capability-table/registry-core.js +26 -3
- package/dist/wiring/capability-table/reporting.js +7 -1
- package/dist/wiring/capability-table/scheduling-registry.js +8 -2
- package/dist/wiring/capability-table/trust-ledger.js +54 -12
- package/docs/agent-delegation-drive.7.md +17 -0
- package/docs/cli-mcp-parity.7.md +46 -5
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +38 -0
- package/docs/cross-agent-ledger.7.md +20 -8
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/fix.7.md +4 -4
- package/docs/getting-started.md +40 -32
- package/docs/index.md +17 -0
- package/docs/launch/demo.tape +4 -3
- package/docs/mcp-app-surface.7.md +27 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/project-index.md +53 -9
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +12 -0
- package/docs/routine.7.md +22 -0
- package/docs/run-registry-control-plane.7.md +37 -9
- package/docs/run-retention-reclamation.7.md +12 -4
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/trust-audit-anchor.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +72 -2
- package/docs/workflow-app-framework.7.md +31 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +4 -3
- package/package.json +6 -3
- package/scripts/agents/agent-adapter-core.js +26 -0
- package/scripts/agents/claude-p-agent.js +4 -1
- package/scripts/agents/codex-agent.js +4 -0
- package/scripts/agents/cw-attest-wrap.js +1 -1
- package/scripts/agents/gemini-agent.js +4 -0
- package/scripts/agents/opencode-agent.js +5 -0
- package/scripts/block-unapproved-tag.js +160 -0
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +72 -4
- package/scripts/children/http-batch-delegate-child.js +132 -0
- package/scripts/children/http-delegate-child.js +8 -0
- package/scripts/dogfood-release.js +2 -2
- package/scripts/fake-date-for-reproduction.js +1 -1
- package/scripts/gen-manifests.js +32 -61
- package/scripts/golden-path.js +4 -4
- package/scripts/lang-policy-check.js +82 -0
- package/scripts/parity-check.js +42 -23
- package/scripts/purity-baseline.json +0 -3
- package/scripts/purity-gate.js +16 -3
- package/scripts/release-check.js +2 -1
- package/scripts/release-flow.js +169 -41
- package/scripts/release-gate.js +208 -0
- package/scripts/release-oneclick.js +38 -7
- package/scripts/release-tags.js +47 -0
- package/scripts/schema-version-inventory.json +31 -0
- package/scripts/validate-run-state-schema.js +95 -4
- package/scripts/verdict-keygen.js +2 -2
- package/scripts/verify-bump-reproduction.js +193 -0
- package/scripts/verify-release-verdict.js +139 -0
- package/scripts/verify-verdict-signature.js +1 -1
- package/ui/workbench/app.css +50 -10
- package/ui/workbench/app.js +269 -34
- package/ui/workbench/index.html +4 -2
- package/ui/workbench/inspection.js +51 -0
- package/ui/workbench/navigation.js +44 -0
- package/docs/agent-framework.md +0 -177
- package/docs/designs/handoff-ledger.md +0 -145
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
- package/docs/launch/launch-kit.md +0 -195
- package/docs/launch/pre-launch-checklist.md +0 -53
- package/docs/readme-v0.1.87-full.md +0 -301
- package/docs/routines.md +0 -101
- package/docs/scheduled-tasks.md +0 -80
- package/scripts/agents/claude-p-agent.sh +0 -9
- package/scripts/block-unapproved-tag.sh +0 -75
- package/scripts/release-gate.sh +0 -94
- package/scripts/verify-bump-reproduction.sh +0 -148
- package/scripts/verify-container-selfref.js +0 -64
|
@@ -158,6 +158,75 @@ function dirtySetsFor(state) {
|
|
|
158
158
|
function markBlackboardDirty(state, kind, id) {
|
|
159
159
|
dirtySetsFor(state)[kind].add(id);
|
|
160
160
|
}
|
|
161
|
+
// Dirty-id tracking for state.messages, PARALLEL to the 5-kind tracking
|
|
162
|
+
// above (not folded in: messages share ONE ordered log file, not a
|
|
163
|
+
// recordPath-per-id file). The only push site is postBlackboardMessage
|
|
164
|
+
// (`state.messages.push(message)`), so the dirty ids at persist time are
|
|
165
|
+
// always exactly the trailing ids of state.messages since the last
|
|
166
|
+
// persist. Kept off the serialized state for the same reason as
|
|
167
|
+
// blackboardDirtySets: a WeakMap, never a field on BlackboardState.
|
|
168
|
+
const blackboardMessagesDirty = new WeakMap();
|
|
169
|
+
function dirtyMessageIdsFor(state) {
|
|
170
|
+
let ids = blackboardMessagesDirty.get(state);
|
|
171
|
+
if (!ids) {
|
|
172
|
+
ids = new Set();
|
|
173
|
+
blackboardMessagesDirty.set(state, ids);
|
|
174
|
+
}
|
|
175
|
+
return ids;
|
|
176
|
+
}
|
|
177
|
+
function markBlackboardMessageDirty(state, id) {
|
|
178
|
+
dirtyMessageIdsFor(state).add(id);
|
|
179
|
+
}
|
|
180
|
+
// Writes messages.jsonl for a persist call, or skips it entirely when
|
|
181
|
+
// nothing changed (the common case: 7 of the 8 persistBlackboardState call
|
|
182
|
+
// sites never touch state.messages).
|
|
183
|
+
//
|
|
184
|
+
// Fast path (the common case for the ONE call site that does change
|
|
185
|
+
// messages, postBlackboardMessage): the dirty ids are exactly the trailing
|
|
186
|
+
// `k` entries of state.messages (the sole push site always appends). Sort
|
|
187
|
+
// only that small batch, check it does not need to interleave with the
|
|
188
|
+
// already-on-disk tail, and APPEND it — no read, no resort, no rewrite of
|
|
189
|
+
// the earlier messages. This turns posting M messages from O(M^2) written
|
|
190
|
+
// bytes into O(M) total.
|
|
191
|
+
//
|
|
192
|
+
// Fallback (a caller-supplied custom --id ties createdAt with an earlier
|
|
193
|
+
// message and sorts BEFORE it, so a plain append would leave the file out
|
|
194
|
+
// of order): resort the whole array and rewrite the whole file, byte-
|
|
195
|
+
// identical to the code path this replaces.
|
|
196
|
+
//
|
|
197
|
+
// Either way state.messages is left fully sorted after this call, exactly
|
|
198
|
+
// as the old unconditional `.sort()` left it — buildBlackboardGraph (which
|
|
199
|
+
// iterates state.messages in raw array order) sees the same order as
|
|
200
|
+
// before.
|
|
201
|
+
function persistBlackboardMessages(run, state) {
|
|
202
|
+
const dirty = dirtyMessageIdsFor(state);
|
|
203
|
+
if (dirty.size === 0)
|
|
204
|
+
return;
|
|
205
|
+
const file = messagesPath(run);
|
|
206
|
+
const total = state.messages.length;
|
|
207
|
+
const k = dirty.size;
|
|
208
|
+
const priorCount = total - k;
|
|
209
|
+
const batch = priorCount >= 0 ? state.messages.slice(priorCount).sort(cb.compareRecords) : [];
|
|
210
|
+
const batchMatchesDirty = batch.length === dirty.size && batch.every((message) => dirty.has(message.id));
|
|
211
|
+
const priorTail = priorCount > 0 ? state.messages[priorCount - 1] : undefined;
|
|
212
|
+
const canAppend = batchMatchesDirty && (!priorTail || cb.compareRecords(priorTail, batch[0]) <= 0);
|
|
213
|
+
if (canAppend) {
|
|
214
|
+
state.messages.splice(priorCount, k, ...batch);
|
|
215
|
+
const lines = batch.map((message) => `${JSON.stringify(message)}\n`).join("");
|
|
216
|
+
const currentBytes = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
217
|
+
// Same torn-tail guard as trust-audit's events.jsonl (shared helper):
|
|
218
|
+
// a crash mid-append can leave the log without its final "\n"; put the
|
|
219
|
+
// new lines on their own clean line rather than merging with a torn
|
|
220
|
+
// tail.
|
|
221
|
+
const leadingNewline = currentBytes > 0 && !(0, fs_atomic_1.logEndsWithNewline)(file, currentBytes) ? "\n" : "";
|
|
222
|
+
(0, fs_atomic_1.durableAppendFileSync)(file, leadingNewline + lines);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// Same bytes the old unconditional code path always wrote.
|
|
226
|
+
(0, fs_atomic_1.writeTextDurable)(file, state.messages.sort(cb.compareRecords).map((message) => JSON.stringify(message)).join("\n") + (state.messages.length ? "\n" : ""));
|
|
227
|
+
}
|
|
228
|
+
dirty.clear();
|
|
229
|
+
}
|
|
161
230
|
function linkMultiAgent(run, blackboardId, topicIds, input) {
|
|
162
231
|
const groupId = input.agentGroupId ?? input.groupId;
|
|
163
232
|
const roleId = input.agentRoleId ?? input.roleId;
|
|
@@ -218,7 +287,9 @@ function persistBlackboardState(run) {
|
|
|
218
287
|
decisions: state.decisions.map(cb.indexRow),
|
|
219
288
|
messages: state.messages.map((message) => ({ id: message.id, blackboardId: message.blackboardId, topicId: message.topicId, createdAt: message.createdAt, status: message.status, author: message.author, evidenceRefs: message.linkedEvidenceRefs, artifactRefIds: message.linkedArtifactRefIds })),
|
|
220
289
|
});
|
|
221
|
-
|
|
290
|
+
// messages.jsonl: see persistBlackboardMessages above for the
|
|
291
|
+
// skip-if-unchanged / append-if-safe / full-rewrite-fallback split.
|
|
292
|
+
persistBlackboardMessages(run, state);
|
|
222
293
|
const dirty = dirtySetsFor(state);
|
|
223
294
|
for (const id of dirty.topics) {
|
|
224
295
|
const record = state.topics.find((entry) => entry.id === id);
|
|
@@ -308,6 +379,7 @@ function postBlackboardMessage(run, input) {
|
|
|
308
379
|
requireArtifactRefs(run, input.artifactRefIds || []);
|
|
309
380
|
const message = cb.buildMessage(run.id, board, topic, input, id, now(), trust_policy_io_1.hashText, sourceForActorLocal);
|
|
310
381
|
state.messages.push(message);
|
|
382
|
+
markBlackboardMessageDirty(state, message.id);
|
|
311
383
|
topic.messageIds = cb.unique([...topic.messageIds, message.id]);
|
|
312
384
|
board.messageCount = state.messages.filter((entry) => entry.blackboardId === board.id).length;
|
|
313
385
|
cb.touch(topic, now());
|
package/dist/shell/drive.js
CHANGED
|
@@ -79,6 +79,7 @@ const reporter_1 = require("./reporter");
|
|
|
79
79
|
const fs_atomic_1 = require("./fs-atomic");
|
|
80
80
|
const observability_1 = require("./observability");
|
|
81
81
|
const telemetry_attestation_1 = require("../core/trust/telemetry-attestation");
|
|
82
|
+
const perf_trace_1 = require("./perf-trace");
|
|
82
83
|
/** Total RECORDED tokens across the run's attested units, reading the
|
|
83
84
|
* agent-reported usage through normalizeReportedUsage so a hop that
|
|
84
85
|
* reported snake_case buckets (`input_tokens`/`output_tokens`, the shape
|
|
@@ -117,8 +118,9 @@ function tokenBudgetUsage(run) {
|
|
|
117
118
|
return undefined;
|
|
118
119
|
return { spent: recordedTokenTotal(run), budget };
|
|
119
120
|
}
|
|
120
|
-
|
|
121
|
-
exports.
|
|
121
|
+
var drive_decide_2 = require("../core/pipeline/drive-decide");
|
|
122
|
+
Object.defineProperty(exports, "DRIVE_SCHEMA_VERSION", { enumerable: true, get: function () { return drive_decide_2.DRIVE_SCHEMA_VERSION; } });
|
|
123
|
+
Object.defineProperty(exports, "MAX_SUB_WORKFLOW_DEPTH", { enumerable: true, get: function () { return drive_decide_2.MAX_SUB_WORKFLOW_DEPTH; } });
|
|
122
124
|
function agentConfigured(config) {
|
|
123
125
|
return Boolean(config.command || config.endpoint);
|
|
124
126
|
}
|
|
@@ -433,8 +435,8 @@ function processSelectedTask(ctx, selectedId, preparedOutcome, deferPersist = fa
|
|
|
433
435
|
}
|
|
434
436
|
function runSubWorkflow(ctx, run, selected, workerId, manifest, spec, deferPersist = false) {
|
|
435
437
|
const parentApp = run.workflow.id;
|
|
436
|
-
if (ctx.depth + 1 >
|
|
437
|
-
return handleHop(ctx, selected, workerId, `sub-workflow depth limit exceeded (> ${
|
|
438
|
+
if (ctx.depth + 1 > drive_decide_1.MAX_SUB_WORKFLOW_DEPTH) {
|
|
439
|
+
return handleHop(ctx, selected, workerId, `sub-workflow depth limit exceeded (> ${drive_decide_1.MAX_SUB_WORKFLOW_DEPTH})`, deferPersist, deferPersist ? run : undefined);
|
|
438
440
|
}
|
|
439
441
|
if ([...ctx.visitedAppIds, parentApp].includes(spec.appId)) {
|
|
440
442
|
return handleHop(ctx, selected, workerId, `sub-workflow cycle detected: ${[...ctx.visitedAppIds, parentApp, spec.appId].join(" -> ")}`, deferPersist, deferPersist ? run : undefined);
|
|
@@ -630,7 +632,7 @@ function maybeExpandLoop(run) {
|
|
|
630
632
|
requiresEvidence: t.requiresEvidence,
|
|
631
633
|
sandboxProfileId: t.sandboxProfileId,
|
|
632
634
|
},
|
|
633
|
-
}, { persist: false, persistNode: (r, node) => void (0, node_store_1.appendRunNode)(r, node) });
|
|
635
|
+
}, { persist: false, persistNode: (r, node) => void (0, node_store_1.appendRunNode)(r, node), pathExists: fs.existsSync });
|
|
634
636
|
t.stateNodeId = result.outputNodeId;
|
|
635
637
|
}
|
|
636
638
|
(0, dispatch_1.updatePhaseStatuses)(run);
|
|
@@ -656,89 +658,139 @@ function driveStep(ctx) {
|
|
|
656
658
|
return processSelectedTask(ctx, selected.id);
|
|
657
659
|
}
|
|
658
660
|
/** Dispatch every batch task (sequential — dispatch mutates state), then
|
|
659
|
-
* collect ALL
|
|
660
|
-
* (
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
661
|
+
* collect ALL agent child outcomes in one concurrent window: spawn-style
|
|
662
|
+
* (CLI-binary) agents through the batch delegate child, and endpoint-mode
|
|
663
|
+
* agents through the HTTP batch delegate child — so `--concurrency N` runs N
|
|
664
|
+
* endpoint delegations in parallel instead of one-at-a-time on the serial
|
|
665
|
+
* path. Returns outcomes keyed by task id; a cache-hit, endpoint-configured
|
|
666
|
+
* agent, or sub-workflow task gets no prepared outcome and settles through
|
|
667
|
+
* the serial accept path inside processSelectedTask (a sub-workflow task is
|
|
668
|
+
* never spawn-eligible in the first place — processSelectedTask always
|
|
669
|
+
* takes its own runSubWorkflow branch for it, so building a spawn job here
|
|
670
|
+
* would only be spawned and thrown away unused). Dispatch failures become
|
|
671
|
+
* recorded fail steps up front, exactly what the serial path would emit. */
|
|
666
672
|
function prepareConcurrentOutcomes(ctx, batch) {
|
|
667
673
|
const failSteps = new Map();
|
|
668
674
|
const jobs = [];
|
|
669
675
|
const jobTaskIds = [];
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
676
|
+
const endpointJobs = [];
|
|
677
|
+
const endpointTaskIds = [];
|
|
678
|
+
// Set whenever a task's status flips pending -> running (workerId
|
|
679
|
+
// assigned) this round, regardless of whether it later becomes a real
|
|
680
|
+
// spawn job, an endpoint job, a cache hit, or a sub-workflow task. That
|
|
681
|
+
// dispatch is a real state mutation on the round-cached run object that
|
|
682
|
+
// nothing has written to disk yet — the pre-spawn checkpoint below must
|
|
683
|
+
// flush it even when the batch produces zero spawn/endpoint jobs (e.g. an
|
|
684
|
+
// all-sub-workflow batch), or a crash before the round-end flush loses
|
|
685
|
+
// the dispatch.
|
|
686
|
+
let dispatchedThisRound = false;
|
|
687
|
+
// Dispatch is a short local mutation group. Keep its audit lock only until
|
|
688
|
+
// its events have one durable append; the agent work below stays outside it.
|
|
689
|
+
(0, trust_audit_1.withTrustAuditBatch)(loadRun(ctx), () => {
|
|
690
|
+
for (const taskId of batch) {
|
|
691
|
+
const run = loadRun(ctx);
|
|
692
|
+
const task = run.tasks.find((candidate) => candidate.id === taskId);
|
|
693
|
+
if (!task || (task.status !== "pending" && task.status !== "running"))
|
|
681
694
|
continue;
|
|
695
|
+
let workerId = task.workerId;
|
|
696
|
+
if (task.status === "pending") {
|
|
697
|
+
const manifest = (0, dispatch_2.createDispatchManifest)(run, 1, { backendId: task.agentType || "agent" });
|
|
698
|
+
const dispatchedTask = manifest.tasks.find((entry) => entry.id === task.id) || manifest.tasks[0];
|
|
699
|
+
if (!dispatchedTask || !dispatchedTask.workerId) {
|
|
700
|
+
failSteps.set(taskId, (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId, phase: task.phase, reason: "dispatch produced no worker scope" }));
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
workerId = dispatchedTask.workerId;
|
|
704
|
+
dispatchedThisRound = true;
|
|
682
705
|
}
|
|
683
|
-
workerId
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
706
|
+
if (!workerId) {
|
|
707
|
+
failSteps.set(taskId, (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId, phase: task.phase, reason: "no worker scope for task" }));
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
const freshRun = loadRun(ctx);
|
|
711
|
+
const manifest = (0, worker_isolation_1.showWorkerManifest)(freshRun, workerId);
|
|
712
|
+
const delegationDigest = ctx.incremental
|
|
713
|
+
? (0, drive_decide_1.incrementalDelegationDigest)(task.model || ctx.config.model || "", task.agentType || "agent", manifest.sandboxPolicy?.id || task.sandboxProfileId || "", ctx.config.command || "", ctx.config.args ? (0, agent_1.stripSecretArgs)(ctx.config.args) : [], ctx.config.endpoint || "")
|
|
714
|
+
: "";
|
|
715
|
+
const cachePath = resultCachePath(freshRun, task, (0, hash_1.sha256)(task.prompt || ""), ctx.incremental, delegationDigest);
|
|
716
|
+
if (cachePath && fs.existsSync(cachePath))
|
|
717
|
+
continue;
|
|
718
|
+
// A sub-workflow task never goes through the spawn path — processSelectedTask
|
|
719
|
+
// checks task.subWorkflow before it ever looks at a prepared spawn outcome
|
|
720
|
+
// and always takes its own runSubWorkflow branch instead (mirrors that
|
|
721
|
+
// check byte-for-byte). Building a spawn OR endpoint job for it here would
|
|
722
|
+
// just be a real agent child spawned/delegated and thrown away unused.
|
|
723
|
+
const subWorkflow = task.subWorkflow;
|
|
724
|
+
if (subWorkflow)
|
|
725
|
+
continue;
|
|
726
|
+
const request = {
|
|
727
|
+
schemaVersion: 1,
|
|
728
|
+
runId: ctx.runId,
|
|
729
|
+
taskId: task.id,
|
|
730
|
+
backendId: task.agentType || "agent",
|
|
731
|
+
cwd: freshRun.cwd,
|
|
732
|
+
sandboxPolicy: manifest.sandboxPolicy,
|
|
733
|
+
manifest: { workerDir: manifest.workerDir, manifestPath: manifest.manifestPath, inputPath: manifest.inputPath, resultPath: manifest.resultPath, prompt: manifest.prompt },
|
|
734
|
+
label: task.id,
|
|
735
|
+
timeoutMs: ctx.config.timeoutMs,
|
|
736
|
+
delegation: { command: ctx.config.command, args: ctx.config.args, endpoint: ctx.config.endpoint, model: task.model || ctx.config.model },
|
|
737
|
+
};
|
|
738
|
+
const job = (0, agent_1.prepareAgentSpawn)(request);
|
|
739
|
+
if (job) {
|
|
740
|
+
const sandboxPolicy = manifest.sandboxPolicy;
|
|
741
|
+
if (sandboxPolicy) {
|
|
742
|
+
job.env = (0, agent_1.buildAgentChildEnv)(sandboxPolicy).env;
|
|
743
|
+
}
|
|
744
|
+
jobs.push(job);
|
|
745
|
+
jobTaskIds.push(taskId);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
// Endpoint-configured agent: no CLI binary to batch-spawn, but its HTTP
|
|
749
|
+
// delegation joins the concurrent window through the endpoint batch child
|
|
750
|
+
// (runEndpointBatchOutcomes) rather than settling one-at-a-time on the
|
|
751
|
+
// serial path. An unconfigured agent (neither command nor endpoint) returns
|
|
752
|
+
// undefined from both prepares here and falls to the serial refusal.
|
|
753
|
+
const endpointJob = (0, agent_1.prepareEndpointJob)(request);
|
|
754
|
+
if (endpointJob) {
|
|
755
|
+
endpointJobs.push(endpointJob);
|
|
756
|
+
endpointTaskIds.push(taskId);
|
|
713
757
|
}
|
|
714
|
-
jobs.push(job);
|
|
715
|
-
jobTaskIds.push(taskId);
|
|
716
758
|
}
|
|
759
|
+
});
|
|
760
|
+
const totalJobs = jobs.length + endpointJobs.length;
|
|
761
|
+
if (totalJobs) {
|
|
762
|
+
emitProgress(`⇉ concurrent round: ${totalJobs} agent${totalJobs > 1 ? "s" : ""} spawning in parallel, may take minutes…`);
|
|
717
763
|
}
|
|
718
|
-
if (
|
|
719
|
-
emitProgress(`⇉ concurrent round: ${jobs.length} agent${jobs.length > 1 ? "s" : ""} spawning in parallel, may take minutes…`);
|
|
764
|
+
if (totalJobs || dispatchedThisRound) {
|
|
720
765
|
// Every task above that reached "pending" got dispatched (workerId
|
|
721
766
|
// assigned) in the round-cached run object, but nothing durable was
|
|
722
767
|
// written for it — unlike the serial path's own dispatch branch, which
|
|
723
768
|
// always checkpoints immediately. Flush now, BEFORE the batch's long
|
|
724
|
-
//
|
|
725
|
-
// then leaves state.json correctly showing
|
|
726
|
-
// instead of losing the dispatch entirely.
|
|
769
|
+
// settle window opens: a crash mid-spawn OR mid-sub-workflow-recursion
|
|
770
|
+
// (both can run for minutes) then leaves state.json correctly showing
|
|
771
|
+
// these tasks as dispatched, instead of losing the dispatch entirely.
|
|
772
|
+
// Gated on dispatchedThisRound (not just totalJobs) so a batch made
|
|
773
|
+
// entirely of sub-workflow and/or cache-hit tasks — which builds zero
|
|
774
|
+
// spawn/endpoint jobs — still gets this flush before its long-running
|
|
775
|
+
// settle loop starts.
|
|
727
776
|
(0, run_store_1.saveCheckpoint)(loadRun(ctx));
|
|
728
777
|
}
|
|
729
|
-
const settled = (0, agent_1.runAgentBatchOutcomes)(jobs);
|
|
730
778
|
const outcomes = new Map();
|
|
779
|
+
const settled = (0, agent_1.runAgentBatchOutcomes)(jobs);
|
|
731
780
|
jobTaskIds.forEach((taskId, index) => outcomes.set(taskId, settled[index]));
|
|
781
|
+
const endpointSettled = (0, agent_1.runEndpointBatchOutcomes)(endpointJobs);
|
|
782
|
+
endpointTaskIds.forEach((taskId, index) => outcomes.set(taskId, endpointSettled[index]));
|
|
732
783
|
return { outcomes, failSteps };
|
|
733
784
|
}
|
|
734
785
|
/** One concurrent round inside one cached in-memory run: dispatches every
|
|
735
786
|
* batch task, spawns all spawn-style agent children in one concurrent
|
|
736
|
-
* window
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
787
|
+
* window (CLI-binary agents through the batch child, endpoint agents
|
|
788
|
+
* through the HTTP batch child), then settles + accepts in DETERMINISTIC
|
|
789
|
+
* batch (task-id) order regardless of wall-clock finish order. At round end
|
|
790
|
+
* it flushes once: commitState(run, "concurrent-round:<n>-tasks") +
|
|
791
|
+
* writeReport + saveCheckpoint. Cache-hit tasks get no prepared outcome and
|
|
792
|
+
* settle through the serial path (still inside this one deferred-persist
|
|
793
|
+
* round). If no step was produced (nothing
|
|
742
794
|
* runnable at round entry — terminal/blocked/token-budget gate) the
|
|
743
795
|
* round degrades to one plain driveStep. Byte-exact to the old build's
|
|
744
796
|
* src/drive.ts's driveConcurrentRound. */
|
|
@@ -756,28 +808,32 @@ function driveConcurrentRound(ctx, limit) {
|
|
|
756
808
|
.filter((task) => phaseTaskIds.has(task.id) && (task.status === "pending" || task.status === "running"))
|
|
757
809
|
.slice(0, width)
|
|
758
810
|
.map((task) => task.id);
|
|
759
|
-
const prepared = prepareConcurrentOutcomes(ctx, batch);
|
|
811
|
+
const prepared = (0, perf_trace_1.withPerfTraceGroup)("dispatch", () => prepareConcurrentOutcomes(ctx, batch));
|
|
760
812
|
const steps = [];
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
813
|
+
// Settlement is also local and synchronous. Flush the complete audit
|
|
814
|
+
// group before the existing round checkpoint, never across it.
|
|
815
|
+
(0, perf_trace_1.withPerfTraceGroup)("settlement", () => (0, trust_audit_1.withTrustAuditBatch)(loadRun(ctx), () => {
|
|
816
|
+
for (const taskId of batch) {
|
|
817
|
+
const failStep = prepared.failSteps.get(taskId);
|
|
818
|
+
if (failStep) {
|
|
819
|
+
steps.push(failStep);
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
// Re-read per task: a prior accept in this round mutated state (the
|
|
823
|
+
// SAME cached object via loadRun's round cache — no disk round-trip
|
|
824
|
+
// until the round-end flush below).
|
|
825
|
+
const freshRun = loadRun(ctx);
|
|
826
|
+
const fresh = freshRun.tasks.find((task) => task.id === taskId);
|
|
827
|
+
if (!fresh || (fresh.status !== "pending" && fresh.status !== "running"))
|
|
828
|
+
continue;
|
|
829
|
+
steps.push(processSelectedTask(ctx, taskId, prepared.outcomes.get(taskId), true));
|
|
766
830
|
}
|
|
767
|
-
|
|
768
|
-
// SAME cached object via loadRun's round cache — no disk round-trip
|
|
769
|
-
// until the round-end flush below).
|
|
770
|
-
const freshRun = loadRun(ctx);
|
|
771
|
-
const fresh = freshRun.tasks.find((task) => task.id === taskId);
|
|
772
|
-
if (!fresh || (fresh.status !== "pending" && fresh.status !== "running"))
|
|
773
|
-
continue;
|
|
774
|
-
steps.push(processSelectedTask(ctx, taskId, prepared.outcomes.get(taskId), true));
|
|
775
|
-
}
|
|
831
|
+
}));
|
|
776
832
|
if (steps.length > 0) {
|
|
777
833
|
const settledRun = loadRun(ctx);
|
|
778
|
-
(0, commit_1.commitState)(settledRun, `concurrent-round:${batch.length}-tasks`);
|
|
779
|
-
(0, report_1.writeReport)(settledRun);
|
|
780
|
-
(0, run_store_1.saveCheckpoint)(settledRun);
|
|
834
|
+
(0, perf_trace_1.withPerfTraceGroup)("checkpoint", () => (0, commit_1.commitState)(settledRun, `concurrent-round:${batch.length}-tasks`));
|
|
835
|
+
(0, perf_trace_1.withPerfTraceGroup)("report", () => (0, report_1.writeReport)(settledRun));
|
|
836
|
+
(0, perf_trace_1.withPerfTraceGroup)("checkpoint", () => (0, run_store_1.saveCheckpoint)(settledRun));
|
|
781
837
|
}
|
|
782
838
|
return steps.length > 0 ? steps : [driveStep(ctx)];
|
|
783
839
|
});
|
|
@@ -872,7 +928,7 @@ function driveOneRound(ctx, options, steps, emitPhaseProgress) {
|
|
|
872
928
|
// re-read+re-parse state.json even though nothing on disk changed
|
|
873
929
|
// between them — reads that only reflect the mutations THIS round's
|
|
874
930
|
// own steps make, which the shared in-memory object already carries.
|
|
875
|
-
const roundSteps = withRoundCache(ctx, () => {
|
|
931
|
+
const roundSteps = (0, perf_trace_1.withPerfTraceGroup)("round", () => withRoundCache(ctx, () => {
|
|
876
932
|
const width = (0, drive_decide_1.roundWidth)(loadRun(ctx), options.concurrency);
|
|
877
933
|
// width>1 (an explicit --concurrency>1, or an auto-width parallel
|
|
878
934
|
// phase) runs the whole round through driveConcurrentRound — one or
|
|
@@ -880,7 +936,7 @@ function driveOneRound(ctx, options, steps, emitPhaseProgress) {
|
|
|
880
936
|
// end. `--once` still stops after this ONE round even though a round
|
|
881
937
|
// can yield multiple steps.
|
|
882
938
|
return width > 1 ? driveConcurrentRound(ctx, width) : [driveStep(ctx)];
|
|
883
|
-
});
|
|
939
|
+
}));
|
|
884
940
|
for (const stepResult of roundSteps)
|
|
885
941
|
steps.push(stepResult);
|
|
886
942
|
// Brew-style phase boundaries: after each round, announce a newly-active
|
|
@@ -1004,28 +1060,35 @@ function finalizeDriveResult(ctx, options, steps, plannedWorkers, maxIter, exhau
|
|
|
1004
1060
|
* exact synchronous shape) and fixed only in driveAsync(). */
|
|
1005
1061
|
function drive(runId, cwd, options = {}) {
|
|
1006
1062
|
const ctx = buildDriveContext(runId, cwd, options);
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1063
|
+
// Resolve the run dir before the mutex: a missing run throws "Run not found"
|
|
1064
|
+
// here (never a drive.lock side effect on a nonexistent run), via a stat only
|
|
1065
|
+
// so the per-round read accounting is unchanged. Mirrors withRunStateLock's
|
|
1066
|
+
// own probe-before-lock.
|
|
1067
|
+
const runDir = (0, run_store_1.resolveRunDir)(runId, cwd);
|
|
1068
|
+
return (0, run_store_1.withDriveLock)(runDir, runId, () => {
|
|
1069
|
+
const steps = [];
|
|
1070
|
+
const run0 = loadRun(ctx);
|
|
1071
|
+
const plannedWorkers = run0.tasks.length;
|
|
1072
|
+
const maxIter = (0, drive_decide_1.maxIterations)(plannedWorkers, (0, loop_expansion_1.maxLoopExpansion)(run0), ctx.policy);
|
|
1073
|
+
const emitPhaseProgress = createPhaseProgressEmitter();
|
|
1074
|
+
let exhaustedMaxIterations = !options.once;
|
|
1075
|
+
const stopSignal = createStopSignalController(runId);
|
|
1076
|
+
stopSignal.install();
|
|
1077
|
+
try {
|
|
1078
|
+
for (let i = 0; i < maxIter; i++) {
|
|
1079
|
+
if (stopSignal.getInterruptedBy())
|
|
1080
|
+
break;
|
|
1081
|
+
if (!driveOneRound(ctx, options, steps, emitPhaseProgress)) {
|
|
1082
|
+
exhaustedMaxIterations = false;
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1022
1085
|
}
|
|
1023
1086
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1087
|
+
finally {
|
|
1088
|
+
stopSignal.remove();
|
|
1089
|
+
}
|
|
1090
|
+
return finalizeDriveResult(ctx, options, steps, plannedWorkers, maxIter, exhaustedMaxIterations, stopSignal.getInterruptedBy());
|
|
1091
|
+
});
|
|
1029
1092
|
}
|
|
1030
1093
|
/** Same contract and same DriveResult as drive() -- but actually
|
|
1031
1094
|
* interruptible. After every round it awaits yieldToEventLoop(), a real
|
|
@@ -1053,29 +1116,33 @@ function drive(runId, cwd, options = {}) {
|
|
|
1053
1116
|
* future cycle if it proves needed in practice. */
|
|
1054
1117
|
async function driveAsync(runId, cwd, options = {}) {
|
|
1055
1118
|
const ctx = buildDriveContext(runId, cwd, options);
|
|
1056
|
-
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1119
|
+
// Resolve the run dir before the mutex — see drive() above.
|
|
1120
|
+
const runDir = (0, run_store_1.resolveRunDir)(runId, cwd);
|
|
1121
|
+
return (0, run_store_1.withDriveLockAsync)(runDir, runId, async () => {
|
|
1122
|
+
const steps = [];
|
|
1123
|
+
const run0 = loadRun(ctx);
|
|
1124
|
+
const plannedWorkers = run0.tasks.length;
|
|
1125
|
+
const maxIter = (0, drive_decide_1.maxIterations)(plannedWorkers, (0, loop_expansion_1.maxLoopExpansion)(run0), ctx.policy);
|
|
1126
|
+
const emitPhaseProgress = createPhaseProgressEmitter();
|
|
1127
|
+
let exhaustedMaxIterations = !options.once;
|
|
1128
|
+
const stopSignal = createStopSignalController(runId);
|
|
1129
|
+
stopSignal.install();
|
|
1130
|
+
try {
|
|
1131
|
+
for (let i = 0; i < maxIter; i++) {
|
|
1132
|
+
if (stopSignal.getInterruptedBy())
|
|
1133
|
+
break;
|
|
1134
|
+
if (!driveOneRound(ctx, options, steps, emitPhaseProgress)) {
|
|
1135
|
+
exhaustedMaxIterations = false;
|
|
1136
|
+
break;
|
|
1137
|
+
}
|
|
1138
|
+
await yieldToEventLoop();
|
|
1071
1139
|
}
|
|
1072
|
-
await yieldToEventLoop();
|
|
1073
1140
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1141
|
+
finally {
|
|
1142
|
+
stopSignal.remove();
|
|
1143
|
+
}
|
|
1144
|
+
return finalizeDriveResult(ctx, options, steps, plannedWorkers, maxIter, exhaustedMaxIterations, stopSignal.getInterruptedBy());
|
|
1145
|
+
});
|
|
1079
1146
|
}
|
|
1080
1147
|
function drivePreview(runId, cwd, args = {}) {
|
|
1081
1148
|
const run = (0, run_store_1.loadRunFromCwd)(runId, cwd);
|
|
@@ -290,6 +290,12 @@ function resolveFeedback(run, feedbackId, result) {
|
|
|
290
290
|
function runPipelineStage(run, stageId, inputNodeId, options = {}, runnerOptions = {}) {
|
|
291
291
|
return (0, runner_1.runPipelineStage)(run, stageId, inputNodeId, options, {
|
|
292
292
|
...runnerOptions,
|
|
293
|
+
// The core gate's `pathExists` defaults to `() => true` (a pure core/
|
|
294
|
+
// module never reads the filesystem). This is the shell seam, so default
|
|
295
|
+
// it to the real `fs.existsSync` — that is what makes the contract's
|
|
296
|
+
// `requireReadablePaths` / `missing-artifact-path` gate live. A caller may
|
|
297
|
+
// still override it (e.g. a replay against a captured path set).
|
|
298
|
+
pathExists: runnerOptions.pathExists || fs.existsSync,
|
|
293
299
|
// Keep the caller-named failure node id (the old build honored outputNodeId
|
|
294
300
|
// for the preserved failure node); the raw core auto-mints when unset.
|
|
295
301
|
failureNodeId: runnerOptions.failureNodeId || options.outputNodeId,
|