cool-workflow 0.1.98 → 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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +11 -2
- 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 +236 -0
- package/dist/cli/entry.js +120 -0
- package/dist/cli/io.js +21 -4
- package/dist/cli/parseargv.js +157 -0
- package/dist/cli.js +6 -33
- package/dist/core/capability-table.js +3534 -0
- package/dist/core/format/help.js +314 -0
- package/dist/{state-explosion/format.js → core/format/state-explosion-text.js} +10 -1
- package/dist/core/hash.js +137 -0
- package/dist/core/multi-agent/candidate-scoring.js +219 -0
- package/dist/core/multi-agent/collaboration.js +481 -0
- package/dist/core/multi-agent/coordinator.js +515 -0
- package/dist/core/multi-agent/eval-replay.js +306 -0
- package/dist/core/multi-agent/runtime.js +929 -0
- package/dist/core/multi-agent/topology.js +298 -0
- package/dist/core/multi-agent/trust-policy.js +197 -0
- package/dist/core/pipeline/commit-gate.js +320 -0
- package/dist/{pipeline-contract.js → core/pipeline/contract.js} +24 -37
- package/dist/core/pipeline/dispatch.js +103 -0
- package/dist/core/pipeline/drive-decide.js +227 -0
- package/dist/core/pipeline/error-feedback.js +161 -0
- package/dist/core/pipeline/loop-expansion.js +124 -0
- package/dist/{result-normalize.js → core/pipeline/result-normalize.js} +14 -37
- package/dist/core/pipeline/runner.js +230 -0
- package/dist/{contract-migration.js → core/state/contract-migration.js} +68 -92
- package/dist/{state-migrations.js → core/state/migrations.js} +103 -96
- package/dist/core/state/node-projection.js +95 -0
- package/dist/core/state/node-snapshot.js +230 -0
- package/dist/core/state/run-paths.js +93 -0
- package/dist/{schema-validate.js → core/state/schema-validate.js} +35 -28
- package/dist/core/state/schema.js +50 -0
- package/dist/core/state/state-explosion/digest.js +243 -0
- package/dist/core/state/state-explosion/graph.js +527 -0
- package/dist/{state-explosion → core/state/state-explosion}/helpers.js +34 -3
- package/dist/core/state/state-explosion/report.js +187 -0
- package/dist/{state-explosion → core/state/state-explosion}/size.js +25 -7
- package/dist/{state-node.js → core/state/state-node.js} +90 -113
- package/dist/core/state/types.js +19 -0
- package/dist/{validation.js → core/state/validation.js} +64 -131
- package/dist/core/trust/evidence-grounding.js +134 -0
- package/dist/core/trust/ledger.js +199 -0
- package/dist/{telemetry-attestation.js → core/trust/telemetry-attestation.js} +94 -68
- package/dist/core/trust/telemetry-ledger.js +127 -0
- package/dist/core/types.js +20 -0
- package/dist/core/version.js +16 -0
- package/dist/{workflow-app-framework.js → core/workflow-apps/app-schema.js} +337 -426
- package/dist/mcp/dispatch.js +104 -0
- package/dist/mcp/server.js +144 -0
- package/dist/mcp-server.js +6 -84
- package/dist/{agent-config.js → shell/agent-config.js} +118 -71
- package/dist/shell/app-run-cli.js +88 -0
- package/dist/shell/audit-cli.js +259 -0
- package/dist/shell/audit-provenance.js +83 -0
- package/dist/shell/candidate-scoring-io.js +459 -0
- package/dist/shell/collaboration-io.js +264 -0
- package/dist/shell/commit-summary.js +104 -0
- package/dist/shell/commit.js +290 -0
- package/dist/shell/coordinator-io.js +476 -0
- package/dist/shell/demo-cli.js +19 -0
- package/dist/shell/dispatch.js +162 -0
- package/dist/{doctor.js → shell/doctor.js} +123 -56
- package/dist/shell/drive.js +873 -0
- package/dist/shell/error-feedback-io.js +305 -0
- package/dist/shell/eval-io.js +473 -0
- package/dist/{multi-agent-eval/format.js → shell/eval-text.js} +78 -49
- package/dist/{evidence-reasoning.js → shell/evidence-reasoning.js} +124 -255
- package/dist/shell/exec-backend-cli.js +88 -0
- package/dist/shell/execution-backend/agent.js +475 -0
- package/dist/shell/execution-backend/ci.js +15 -0
- package/dist/shell/execution-backend/container.js +69 -0
- package/dist/shell/execution-backend/envelopes.js +55 -0
- package/dist/shell/execution-backend/local.js +113 -0
- package/dist/shell/execution-backend/probes.js +175 -0
- package/dist/shell/execution-backend/registry.js +402 -0
- package/dist/shell/execution-backend/remote.js +128 -0
- package/dist/shell/execution-backend/types.js +11 -0
- package/dist/shell/feedback-cli.js +81 -0
- package/dist/shell/feedback-operations.js +48 -0
- package/dist/shell/fs-atomic.js +276 -0
- package/dist/shell/harness.js +98 -0
- package/dist/shell/ledger-cli.js +212 -0
- package/dist/shell/ledger-io.js +169 -0
- package/dist/shell/man-cli.js +89 -0
- package/dist/shell/metrics-cli.js +98 -0
- package/dist/shell/multi-agent-cli.js +1002 -0
- package/dist/shell/multi-agent-host.js +563 -0
- package/dist/shell/multi-agent-io.js +387 -0
- package/dist/{multi-agent-operator-ux.js → shell/multi-agent-operator-ux.js} +234 -191
- package/dist/shell/node-store.js +124 -0
- package/dist/{observability/format.js → shell/observability-format.js} +7 -1
- package/dist/{observability/intake.js → shell/observability-intake.js} +79 -56
- package/dist/{observability.js → shell/observability.js} +159 -332
- package/dist/{onramp.js → shell/onramp.js} +11 -0
- package/dist/{operator-ux/format.js → shell/operator-ux-text.js} +250 -239
- package/dist/shell/operator-ux.js +431 -0
- package/dist/shell/orchestrator.js +231 -0
- package/dist/shell/pipeline-cli.js +667 -0
- package/dist/shell/pipeline.js +217 -0
- package/dist/shell/reclamation-io.js +1366 -0
- package/dist/shell/registry-cli.js +329 -0
- package/dist/{remote-source.js → shell/remote-source.js} +2 -2
- package/dist/shell/report-cli.js +101 -0
- package/dist/shell/report-view-cli.js +117 -0
- package/dist/{orchestrator → shell}/report.js +289 -282
- package/dist/shell/reporter.js +62 -0
- package/dist/shell/run-export-cli.js +106 -0
- package/dist/shell/run-export.js +680 -0
- package/dist/shell/run-registry-io.js +1014 -0
- package/dist/shell/run-store.js +164 -0
- package/dist/{sandbox-profile.js → shell/sandbox-profile.js} +134 -95
- package/dist/{scheduler.js → shell/scheduler-io.js} +248 -48
- package/dist/shell/scheduling-io.js +311 -0
- package/dist/shell/state-cli.js +181 -0
- package/dist/shell/state-explosion-cli.js +197 -0
- package/dist/shell/telemetry-cli.js +85 -0
- package/dist/{telemetry-demo.js → shell/telemetry-demo.js} +149 -119
- package/dist/shell/telemetry-ledger-io.js +132 -0
- package/dist/{term.js → shell/term.js} +36 -46
- package/dist/shell/topology-io.js +361 -0
- package/dist/shell/trust-audit.js +471 -0
- package/dist/{multi-agent-trust.js → shell/trust-policy-io.js} +67 -205
- package/dist/shell/verifier.js +48 -0
- package/dist/shell/workbench-host.js +250 -0
- package/dist/shell/workbench-text.js +18 -0
- package/dist/shell/workbench.js +175 -0
- package/dist/shell/worker-cli.js +124 -0
- package/dist/shell/worker-isolation.js +852 -0
- package/dist/shell/workflow-app-loader.js +650 -0
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/cli-mcp-parity.7.md +280 -219
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +132 -71
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +2 -0
- package/docs/run-registry-control-plane.7.md +2 -0
- package/docs/run-retention-reclamation.7.md +23 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +2 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +9 -13
- package/package.json +1 -1
- package/scripts/agents/cw-attest-wrap.js +2 -2
- package/scripts/bump-version.js +4 -3
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-architecture-review.js +2 -3
- package/scripts/dogfood-release.js +3 -3
- package/scripts/gen-parity-doc.js +15 -2
- package/scripts/golden-path.js +4 -4
- package/scripts/onramp-check.js +1 -1
- package/scripts/parity-check.js +38 -21
- package/scripts/release-flow.js +2 -2
- package/scripts/sync-project-index.js +51 -27
- package/scripts/validate-run-state-schema.js +2 -2
- package/scripts/version-sync-check.js +30 -30
- package/dist/candidate-scoring.js +0 -729
- package/dist/capability-core.js +0 -1189
- package/dist/capability-registry.js +0 -885
- package/dist/cli/command-surface.js +0 -494
- package/dist/cli/format.js +0 -56
- package/dist/cli/handlers/audit.js +0 -82
- package/dist/cli/handlers/blackboard.js +0 -81
- package/dist/cli/handlers/candidate.js +0 -40
- package/dist/cli/handlers/clones.js +0 -34
- package/dist/cli/handlers/collaboration.js +0 -61
- package/dist/cli/handlers/eval.js +0 -40
- package/dist/cli/handlers/ledger.js +0 -169
- package/dist/cli/handlers/maintenance.js +0 -107
- package/dist/cli/handlers/multi-agent.js +0 -165
- package/dist/cli/handlers/node.js +0 -41
- package/dist/cli/handlers/operational.js +0 -155
- package/dist/cli/handlers/operator.js +0 -146
- package/dist/cli/handlers/registry.js +0 -68
- package/dist/cli/handlers/run.js +0 -153
- package/dist/cli/handlers/scheduling.js +0 -132
- package/dist/cli/handlers/workbench.js +0 -41
- package/dist/cli/handlers/worker.js +0 -45
- package/dist/cli/run-summary.js +0 -45
- package/dist/clones.js +0 -162
- package/dist/collaboration.js +0 -726
- package/dist/commit.js +0 -592
- package/dist/compare.js +0 -18
- package/dist/coordinator/classify.js +0 -45
- package/dist/coordinator/paths.js +0 -42
- package/dist/coordinator/util.js +0 -126
- package/dist/coordinator.js +0 -990
- package/dist/daemon.js +0 -44
- package/dist/dispatch.js +0 -250
- package/dist/drive.js +0 -864
- package/dist/error-feedback.js +0 -419
- package/dist/evidence-grounding.js +0 -184
- package/dist/execution-backend/agent.js +0 -354
- package/dist/execution-backend/probes.js +0 -112
- package/dist/execution-backend/util.js +0 -47
- package/dist/execution-backend.js +0 -961
- package/dist/gates.js +0 -48
- package/dist/harness.js +0 -61
- package/dist/ledger.js +0 -313
- package/dist/loop-expansion.js +0 -60
- package/dist/mcp/tool-call.js +0 -470
- package/dist/mcp/tool-definitions.js +0 -1066
- package/dist/mcp-surface.js +0 -30
- package/dist/multi-agent/graph.js +0 -84
- package/dist/multi-agent/helpers.js +0 -141
- package/dist/multi-agent/ids.js +0 -20
- package/dist/multi-agent/paths.js +0 -22
- package/dist/multi-agent-eval/normalize.js +0 -51
- package/dist/multi-agent-eval.js +0 -678
- package/dist/multi-agent-host.js +0 -777
- package/dist/multi-agent.js +0 -984
- package/dist/node-projection.js +0 -59
- package/dist/node-snapshot.js +0 -260
- package/dist/operator-ux.js +0 -631
- package/dist/orchestrator/app-operations.js +0 -211
- package/dist/orchestrator/audit-operations.js +0 -182
- package/dist/orchestrator/candidate-operations.js +0 -117
- package/dist/orchestrator/cli-options.js +0 -294
- package/dist/orchestrator/collaboration-operations.js +0 -86
- package/dist/orchestrator/feedback-operations.js +0 -81
- package/dist/orchestrator/host-operations.js +0 -78
- package/dist/orchestrator/lifecycle-operations.js +0 -650
- package/dist/orchestrator/migration-operations.js +0 -44
- package/dist/orchestrator/multi-agent-operations.js +0 -362
- package/dist/orchestrator/topology-operations.js +0 -84
- package/dist/orchestrator.js +0 -925
- package/dist/pipeline-runner.js +0 -285
- package/dist/reclamation/hash.js +0 -72
- package/dist/reclamation.js +0 -812
- package/dist/reporter.js +0 -67
- package/dist/run-export.js +0 -815
- package/dist/run-registry/derive.js +0 -175
- package/dist/run-registry/format.js +0 -124
- package/dist/run-registry/gc.js +0 -251
- package/dist/run-registry/policy.js +0 -16
- package/dist/run-registry/queue.js +0 -115
- package/dist/run-registry.js +0 -850
- package/dist/run-state-schema.js +0 -68
- package/dist/scheduling.js +0 -184
- package/dist/state-explosion.js +0 -1014
- package/dist/state.js +0 -367
- package/dist/telemetry-ledger.js +0 -196
- package/dist/topology.js +0 -565
- package/dist/triggers.js +0 -184
- package/dist/trust-audit.js +0 -644
- package/dist/types/blackboard.js +0 -2
- package/dist/types/candidate.js +0 -2
- package/dist/types/collaboration.js +0 -2
- package/dist/types/core.js +0 -2
- package/dist/types/drive.js +0 -10
- package/dist/types/error-feedback.js +0 -2
- package/dist/types/evidence-reasoning.js +0 -2
- package/dist/types/execution-backend.js +0 -2
- package/dist/types/multi-agent.js +0 -2
- package/dist/types/observability.js +0 -2
- package/dist/types/pipeline.js +0 -2
- package/dist/types/reclamation.js +0 -8
- package/dist/types/report-bundle.js +0 -6
- package/dist/types/result.js +0 -2
- package/dist/types/run-registry.js +0 -2
- package/dist/types/run.js +0 -2
- package/dist/types/sandbox.js +0 -2
- package/dist/types/schedule.js +0 -2
- package/dist/types/state-node.js +0 -2
- package/dist/types/topology.js +0 -2
- package/dist/types/trust.js +0 -2
- package/dist/types/workbench.js +0 -2
- package/dist/types/worker.js +0 -2
- package/dist/types/workflow-app.js +0 -2
- package/dist/types.js +0 -44
- package/dist/util/fingerprint.js +0 -19
- package/dist/util/fingerprint.test.js +0 -27
- package/dist/verifier.js +0 -78
- package/dist/version.js +0 -8
- package/dist/workbench-host.js +0 -192
- package/dist/workbench.js +0 -192
- package/dist/worker-accept/acceptance.js +0 -114
- package/dist/worker-accept/blackboard-fanout.js +0 -80
- package/dist/worker-accept/blackboard-linkage.js +0 -19
- package/dist/worker-accept/context.js +0 -2
- package/dist/worker-accept/telemetry-ledger.js +0 -126
- package/dist/worker-accept/validation.js +0 -77
- package/dist/worker-accept/verifier-completion.js +0 -73
- package/dist/worker-isolation/helpers.js +0 -51
- package/dist/worker-isolation/paths.js +0 -46
- package/dist/worker-isolation.js +0 -656
- package/dist/workflow-api.js +0 -131
- /package/dist/{types → core/types}/boundary.js +0 -0
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// core/multi-agent/runtime.ts — multi-agent record kernel: run/role/group/
|
|
3
|
+
// membership/fanout/fanin create+transition.
|
|
4
|
+
//
|
|
5
|
+
// MILESTONE 9. Byte-exact port of the DECISION half of the old build's
|
|
6
|
+
// src/multi-agent.ts + src/multi-agent/helpers.ts + src/multi-agent/
|
|
7
|
+
// ids.ts + src/multi-agent/graph.ts: record shape construction, the
|
|
8
|
+
// lifecycle transition table, the fanin coverage/blocked-reason math, id
|
|
9
|
+
// minting, and the provenance graph. Every function here is pure — it
|
|
10
|
+
// takes a WorkflowRun (mutated in place, matching the old build's own
|
|
11
|
+
// in-memory mutation style) plus a `now` clock value and returns the new
|
|
12
|
+
// or updated record. Persistence (writeJson, appendRunNode's disk half,
|
|
13
|
+
// recordTrustAuditEvent) is the caller's job — see
|
|
14
|
+
// shell/multi-agent-io.ts, which wires this pure kernel to real IO,
|
|
15
|
+
// exactly the way shell/dispatch.ts wires core/pipeline/dispatch.ts.
|
|
16
|
+
//
|
|
17
|
+
// BYTE-COMPAT ITEM 3 [load-bearing, HIGH priority]: `unique` in this file
|
|
18
|
+
// DROPS falsy values AND SORTS — this is the kernel-side sorting variant
|
|
19
|
+
// (byte-identical to core/state/state-explosion/helpers.ts's `unique`,
|
|
20
|
+
// but kept as its own local copy here because the old build's
|
|
21
|
+
// multi-agent/helpers.ts kept its own copy too — see that file's header).
|
|
22
|
+
// core/multi-agent/topology.ts, candidate-scoring.ts, and the host/step
|
|
23
|
+
// layer have their OWN separate `unique` that does NOT sort (insertion-
|
|
24
|
+
// order only) — never merge the two. See uniquedual-role-vs-candidate-
|
|
25
|
+
// order.case.js and v2/PLAN.md byte-compat item 3.
|
|
26
|
+
//
|
|
27
|
+
// Evidence: SPEC/multi-agent.md sections A ("Multi-agent kernel"), the
|
|
28
|
+
// "Kernel error strings" and "Fanin blocked-reason strings" Exact-outputs
|
|
29
|
+
// blocks, invariants 1-4/11; plugins/cool-workflow/src/multi-agent.ts,
|
|
30
|
+
// src/multi-agent/{helpers,ids,paths,graph}.ts (byte-exact source).
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.MULTI_AGENT_SCHEMA_VERSION = void 0;
|
|
33
|
+
exports.createId = createId;
|
|
34
|
+
exports.unique = unique;
|
|
35
|
+
exports.compact = compact;
|
|
36
|
+
exports.touch = touch;
|
|
37
|
+
exports.pluralKind = pluralKind;
|
|
38
|
+
exports.statusToNodeStatus = statusToNodeStatus;
|
|
39
|
+
exports.countBy = countBy;
|
|
40
|
+
exports.uniqueEdges = uniqueEdges;
|
|
41
|
+
exports.indexRow = indexRow;
|
|
42
|
+
exports.assertNoRecordPathCollisions = assertNoRecordPathCollisions;
|
|
43
|
+
exports.ensureMultiAgentState = ensureMultiAgentState;
|
|
44
|
+
exports.getMultiAgentRun = getMultiAgentRun;
|
|
45
|
+
exports.getAgentRole = getAgentRole;
|
|
46
|
+
exports.getAgentGroup = getAgentGroup;
|
|
47
|
+
exports.getAgentMembership = getAgentMembership;
|
|
48
|
+
exports.getAgentFanout = getAgentFanout;
|
|
49
|
+
exports.getAgentFanin = getAgentFanin;
|
|
50
|
+
exports.requireMultiAgentRun = requireMultiAgentRun;
|
|
51
|
+
exports.requireAgentRole = requireAgentRole;
|
|
52
|
+
exports.requireAgentGroup = requireAgentGroup;
|
|
53
|
+
exports.requireAgentFanout = requireAgentFanout;
|
|
54
|
+
exports.requireRunTask = requireRunTask;
|
|
55
|
+
exports.assertLifecycleTransition = assertLifecycleTransition;
|
|
56
|
+
exports.lifecycleEvent = lifecycleEvent;
|
|
57
|
+
exports.isMembershipReported = isMembershipReported;
|
|
58
|
+
exports.createMultiAgentRun = createMultiAgentRun;
|
|
59
|
+
exports.transitionMultiAgentRun = transitionMultiAgentRun;
|
|
60
|
+
exports.createAgentRole = createAgentRole;
|
|
61
|
+
exports.createAgentGroup = createAgentGroup;
|
|
62
|
+
exports.assignAgentMembership = assignAgentMembership;
|
|
63
|
+
exports.createAgentFanout = createAgentFanout;
|
|
64
|
+
exports.collectAgentFanin = collectAgentFanin;
|
|
65
|
+
exports.attachDispatchToMultiAgent = attachDispatchToMultiAgent;
|
|
66
|
+
exports.recordMultiAgentWorkerOutput = recordMultiAgentWorkerOutput;
|
|
67
|
+
exports.summarizeMultiAgent = summarizeMultiAgent;
|
|
68
|
+
exports.buildMultiAgentGraph = buildMultiAgentGraph;
|
|
69
|
+
exports.recordPath = recordPath;
|
|
70
|
+
exports.multiAgentRoot = multiAgentRoot;
|
|
71
|
+
exports.MULTI_AGENT_SCHEMA_VERSION = 1;
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Shared primitives (byte-exact to multi-agent/helpers.ts + ids.ts)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
/** Deterministic record id: `${prefix}-${4-digit zero-padded seq}`. Pure
|
|
76
|
+
* function of its arguments — no Date, no random. */
|
|
77
|
+
function createId(prefix, seq) {
|
|
78
|
+
return `${prefix}-${String(seq).padStart(4, "0")}`;
|
|
79
|
+
}
|
|
80
|
+
/** DROPS falsy values, then SORTS (default string sort). Kernel-side
|
|
81
|
+
* sorting `unique` — see file header byte-compat note. Never merge with
|
|
82
|
+
* topology.ts/candidate-scoring.ts's insertion-order sibling. */
|
|
83
|
+
function unique(values) {
|
|
84
|
+
return Array.from(new Set(values.filter(Boolean))).sort();
|
|
85
|
+
}
|
|
86
|
+
function compact(value) {
|
|
87
|
+
if (!value)
|
|
88
|
+
return undefined;
|
|
89
|
+
const entries = Object.entries(value).filter(([, entry]) => entry !== undefined);
|
|
90
|
+
return entries.length ? Object.fromEntries(entries) : undefined;
|
|
91
|
+
}
|
|
92
|
+
function touch(record, now) {
|
|
93
|
+
record.updatedAt = now;
|
|
94
|
+
return record;
|
|
95
|
+
}
|
|
96
|
+
function pluralKind(kind) {
|
|
97
|
+
switch (kind) {
|
|
98
|
+
case "multi-agent-run": return "runs";
|
|
99
|
+
case "agent-role": return "roles";
|
|
100
|
+
case "agent-group": return "groups";
|
|
101
|
+
case "agent-membership": return "memberships";
|
|
102
|
+
case "agent-fanout": return "fanouts";
|
|
103
|
+
case "agent-fanin": return "fanins";
|
|
104
|
+
default: return `${kind}s`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Status -> StateNodeStatus, kernel side (default `pending`). Kept
|
|
108
|
+
* distinct from coordinator/classify.ts's own table (default
|
|
109
|
+
* `completed`) per v2/PLAN.md byte-compat / rebuild risk 7 — collapsing
|
|
110
|
+
* the two tables changes graph output and eval dependency_parity. */
|
|
111
|
+
function statusToNodeStatus(status) {
|
|
112
|
+
switch (status) {
|
|
113
|
+
case "completed":
|
|
114
|
+
case "reported":
|
|
115
|
+
case "ready":
|
|
116
|
+
return "completed";
|
|
117
|
+
case "running":
|
|
118
|
+
case "forming":
|
|
119
|
+
case "collecting":
|
|
120
|
+
case "verifying":
|
|
121
|
+
case "assigned":
|
|
122
|
+
case "active":
|
|
123
|
+
case "dispatched":
|
|
124
|
+
return "running";
|
|
125
|
+
case "blocked":
|
|
126
|
+
return "blocked";
|
|
127
|
+
case "failed":
|
|
128
|
+
return "failed";
|
|
129
|
+
case "cancelled":
|
|
130
|
+
case "rejected":
|
|
131
|
+
return "rejected";
|
|
132
|
+
default:
|
|
133
|
+
return "pending";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function countBy(items, key) {
|
|
137
|
+
const counts = {};
|
|
138
|
+
for (const item of items) {
|
|
139
|
+
const value = key(item);
|
|
140
|
+
counts[value] = (counts[value] || 0) + 1;
|
|
141
|
+
}
|
|
142
|
+
return counts;
|
|
143
|
+
}
|
|
144
|
+
function uniqueEdges(edges) {
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
const result = [];
|
|
147
|
+
for (const edge of edges) {
|
|
148
|
+
const key = `${edge.from}\0${edge.to}\0${edge.label || ""}`;
|
|
149
|
+
if (seen.has(key))
|
|
150
|
+
continue;
|
|
151
|
+
seen.add(key);
|
|
152
|
+
result.push(edge);
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
function indexRow(record) {
|
|
157
|
+
return { id: record.id, status: record.status, updatedAt: record.updatedAt };
|
|
158
|
+
}
|
|
159
|
+
/** Byte-exact "safe file name" charset used elsewhere in core/shell:
|
|
160
|
+
* chars outside `[a-zA-Z0-9_.:-]` become `_`. Kept as a local copy since
|
|
161
|
+
* this pure module cannot import shell/fs-atomic.ts's `safeFileName`. */
|
|
162
|
+
function safeFileName(value) {
|
|
163
|
+
return value.replace(/[^a-zA-Z0-9_.:-]+/g, "_");
|
|
164
|
+
}
|
|
165
|
+
function assertNoRecordPathCollisions(label, records) {
|
|
166
|
+
const seen = new Map();
|
|
167
|
+
for (const record of records) {
|
|
168
|
+
const safe = safeFileName(record.id);
|
|
169
|
+
const existing = seen.get(safe);
|
|
170
|
+
if (existing && existing !== record.id) {
|
|
171
|
+
throw new Error(`${label} ids ${existing} and ${record.id} collide on safe file name ${safe}`);
|
|
172
|
+
}
|
|
173
|
+
seen.set(safe, record.id);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// State access
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
/** Fills `run.multiAgent` with empty arrays if absent (pure — no fs; the
|
|
180
|
+
* directory-creation half lives in shell/multi-agent-io.ts's
|
|
181
|
+
* ensureMultiAgentState). */
|
|
182
|
+
function ensureMultiAgentState(run) {
|
|
183
|
+
const existing = run.multiAgent;
|
|
184
|
+
const state = {
|
|
185
|
+
schemaVersion: exports.MULTI_AGENT_SCHEMA_VERSION,
|
|
186
|
+
runs: existing?.runs || [],
|
|
187
|
+
roles: existing?.roles || [],
|
|
188
|
+
groups: existing?.groups || [],
|
|
189
|
+
memberships: existing?.memberships || [],
|
|
190
|
+
fanouts: existing?.fanouts || [],
|
|
191
|
+
fanins: existing?.fanins || [],
|
|
192
|
+
};
|
|
193
|
+
run.multiAgent = state;
|
|
194
|
+
return state;
|
|
195
|
+
}
|
|
196
|
+
function getMultiAgentRun(run, id) {
|
|
197
|
+
return ensureMultiAgentState(run).runs.find((record) => record.id === id);
|
|
198
|
+
}
|
|
199
|
+
function getAgentRole(run, id) {
|
|
200
|
+
return ensureMultiAgentState(run).roles.find((record) => record.id === id);
|
|
201
|
+
}
|
|
202
|
+
function getAgentGroup(run, id) {
|
|
203
|
+
return ensureMultiAgentState(run).groups.find((record) => record.id === id);
|
|
204
|
+
}
|
|
205
|
+
function getAgentMembership(run, id) {
|
|
206
|
+
return ensureMultiAgentState(run).memberships.find((record) => record.id === id);
|
|
207
|
+
}
|
|
208
|
+
function getAgentFanout(run, id) {
|
|
209
|
+
return ensureMultiAgentState(run).fanouts.find((record) => record.id === id);
|
|
210
|
+
}
|
|
211
|
+
function getAgentFanin(run, id) {
|
|
212
|
+
return ensureMultiAgentState(run).fanins.find((record) => record.id === id);
|
|
213
|
+
}
|
|
214
|
+
function requireMultiAgentRun(run, id) {
|
|
215
|
+
const record = getMultiAgentRun(run, id);
|
|
216
|
+
if (!record)
|
|
217
|
+
throw new Error(`Unknown MultiAgentRun id: ${id}`);
|
|
218
|
+
return record;
|
|
219
|
+
}
|
|
220
|
+
function requireAgentRole(run, id) {
|
|
221
|
+
const record = getAgentRole(run, id);
|
|
222
|
+
if (!record)
|
|
223
|
+
throw new Error(`Unknown AgentRole id: ${id}`);
|
|
224
|
+
return record;
|
|
225
|
+
}
|
|
226
|
+
function requireAgentGroup(run, id) {
|
|
227
|
+
const record = getAgentGroup(run, id);
|
|
228
|
+
if (!record)
|
|
229
|
+
throw new Error(`Unknown AgentGroup id: ${id}`);
|
|
230
|
+
return record;
|
|
231
|
+
}
|
|
232
|
+
function requireAgentFanout(run, id) {
|
|
233
|
+
const record = getAgentFanout(run, id);
|
|
234
|
+
if (!record)
|
|
235
|
+
throw new Error(`Unknown AgentFanout id: ${id}`);
|
|
236
|
+
return record;
|
|
237
|
+
}
|
|
238
|
+
function requireRunTask(run, id) {
|
|
239
|
+
const task = run.tasks.find((record) => record.id === id);
|
|
240
|
+
if (!task)
|
|
241
|
+
throw new Error(`Unknown task id for multi-agent record: ${id}`);
|
|
242
|
+
return task;
|
|
243
|
+
}
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Lifecycle
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
/** `planned -> forming|running|failed|cancelled`; `forming ->
|
|
248
|
+
* running|failed|cancelled`; `running -> collecting|completed|failed|
|
|
249
|
+
* cancelled`; `collecting -> verifying|completed|failed|cancelled`;
|
|
250
|
+
* `verifying -> completed|failed|cancelled`; terminal states have no
|
|
251
|
+
* onward transitions. A same-status transition is always legal
|
|
252
|
+
* (no-op check). */
|
|
253
|
+
function assertLifecycleTransition(from, to) {
|
|
254
|
+
const allowed = {
|
|
255
|
+
planned: ["forming", "running", "failed", "cancelled"],
|
|
256
|
+
forming: ["running", "failed", "cancelled"],
|
|
257
|
+
running: ["collecting", "completed", "failed", "cancelled"],
|
|
258
|
+
collecting: ["verifying", "completed", "failed", "cancelled"],
|
|
259
|
+
verifying: ["completed", "failed", "cancelled"],
|
|
260
|
+
completed: [],
|
|
261
|
+
failed: [],
|
|
262
|
+
cancelled: [],
|
|
263
|
+
};
|
|
264
|
+
if (from === to)
|
|
265
|
+
return;
|
|
266
|
+
if (!allowed[from].includes(to))
|
|
267
|
+
throw new Error(`Invalid MultiAgentRun lifecycle transition: ${from} -> ${to}`);
|
|
268
|
+
}
|
|
269
|
+
function lifecycleEvent(from, to, reason, actor, metadata, now) {
|
|
270
|
+
return { at: now, from, to, actor: actor || "cw", reason, metadata: compact(metadata) };
|
|
271
|
+
}
|
|
272
|
+
/** A membership counts as reported only when status is reported/verified
|
|
273
|
+
* AND it carries at least one evidence ref. */
|
|
274
|
+
function isMembershipReported(membership) {
|
|
275
|
+
return (membership.status === "reported" || membership.status === "verified") && membership.evidenceRefs.length > 0;
|
|
276
|
+
}
|
|
277
|
+
function createMultiAgentRun(run, input, now) {
|
|
278
|
+
const state = ensureMultiAgentState(run);
|
|
279
|
+
const id = input.id || createId("mar", state.runs.length + 1);
|
|
280
|
+
if (state.runs.some((record) => record.id === id))
|
|
281
|
+
throw new Error(`Duplicate MultiAgentRun id: ${id}`);
|
|
282
|
+
const status = input.status || "planned";
|
|
283
|
+
const record = {
|
|
284
|
+
schemaVersion: exports.MULTI_AGENT_SCHEMA_VERSION,
|
|
285
|
+
id,
|
|
286
|
+
runId: run.id,
|
|
287
|
+
createdAt: now,
|
|
288
|
+
updatedAt: now,
|
|
289
|
+
status,
|
|
290
|
+
title: input.title || id,
|
|
291
|
+
objective: input.objective,
|
|
292
|
+
parentMultiAgentRunId: input.parentMultiAgentRunId,
|
|
293
|
+
childMultiAgentRunIds: [],
|
|
294
|
+
roleIds: [],
|
|
295
|
+
groupIds: [],
|
|
296
|
+
fanoutIds: [],
|
|
297
|
+
faninIds: [],
|
|
298
|
+
blackboardId: input.blackboardId,
|
|
299
|
+
topicIds: unique(input.topicIds || []),
|
|
300
|
+
lifecycle: [lifecycleEvent(undefined, status, "created", undefined, undefined, now)],
|
|
301
|
+
links: {
|
|
302
|
+
workflowRunId: run.id,
|
|
303
|
+
phase: input.phase,
|
|
304
|
+
phaseId: input.phaseId,
|
|
305
|
+
blackboardId: input.blackboardId,
|
|
306
|
+
blackboardTopicIds: unique(input.topicIds || []),
|
|
307
|
+
},
|
|
308
|
+
policy: {
|
|
309
|
+
schemaVersion: 1,
|
|
310
|
+
id: `${id}-policy`,
|
|
311
|
+
policyRef: `multiAgent.runs.${id}.policy`,
|
|
312
|
+
subjectKind: "multi-agent-run",
|
|
313
|
+
subjectId: id,
|
|
314
|
+
allowedBlackboardTopicIds: unique(input.topicIds || ["*"]),
|
|
315
|
+
allowedWriteOperations: ["message", "context", "artifact", "snapshot", "topic", "coordinator-decision"],
|
|
316
|
+
allowedCandidateOperations: ["register", "score", "select"],
|
|
317
|
+
allowedJudgeOperations: ["verdict", "rationale", "panel-decision"],
|
|
318
|
+
sandboxProfileHints: [],
|
|
319
|
+
requiredEvidenceRefs: [],
|
|
320
|
+
deniedOperations: [],
|
|
321
|
+
metadata: { title: input.title },
|
|
322
|
+
},
|
|
323
|
+
metadata: compact(input.metadata),
|
|
324
|
+
};
|
|
325
|
+
if (record.parentMultiAgentRunId) {
|
|
326
|
+
const parent = requireMultiAgentRun(run, record.parentMultiAgentRunId);
|
|
327
|
+
parent.childMultiAgentRunIds = unique([...parent.childMultiAgentRunIds, record.id]);
|
|
328
|
+
touch(parent, now);
|
|
329
|
+
}
|
|
330
|
+
state.runs.push(record);
|
|
331
|
+
return record;
|
|
332
|
+
}
|
|
333
|
+
function transitionMultiAgentRun(run, multiAgentRunId, status, options, now) {
|
|
334
|
+
ensureMultiAgentState(run);
|
|
335
|
+
const record = requireMultiAgentRun(run, multiAgentRunId);
|
|
336
|
+
assertLifecycleTransition(record.status, status);
|
|
337
|
+
if (status === "completed")
|
|
338
|
+
assertMultiAgentRunCompletionReady(run, record);
|
|
339
|
+
const before = record.status;
|
|
340
|
+
record.status = status;
|
|
341
|
+
record.updatedAt = now;
|
|
342
|
+
record.lifecycle.push(lifecycleEvent(before, status, options.reason, options.actor, options.metadata, now));
|
|
343
|
+
if (status === "completed")
|
|
344
|
+
completeOwnedMultiAgentRecords(run, record, options.reason, now);
|
|
345
|
+
return record;
|
|
346
|
+
}
|
|
347
|
+
function assertMultiAgentRunCompletionReady(run, multiAgentRun) {
|
|
348
|
+
const state = ensureMultiAgentState(run);
|
|
349
|
+
const groups = state.groups.filter((record) => record.multiAgentRunId === multiAgentRun.id);
|
|
350
|
+
const fanins = state.fanins.filter((record) => record.multiAgentRunId === multiAgentRun.id);
|
|
351
|
+
const blocked = fanins.flatMap((fanin) => {
|
|
352
|
+
const reasons = [...fanin.blockedReasons];
|
|
353
|
+
if (fanin.status === "blocked" || fanin.status === "failed")
|
|
354
|
+
reasons.push(`fanin ${fanin.id} status is ${fanin.status}`);
|
|
355
|
+
if (!fanin.verifierReady)
|
|
356
|
+
reasons.push(`fanin ${fanin.id} is not verifier-ready`);
|
|
357
|
+
return reasons.map((reason) => `${fanin.id}: ${reason}`);
|
|
358
|
+
});
|
|
359
|
+
for (const group of groups) {
|
|
360
|
+
if ((group.membershipIds.length || group.fanoutIds.length) && !group.faninIds.length) {
|
|
361
|
+
blocked.push(`group ${group.id} has no fanin record`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (blocked.length)
|
|
365
|
+
throw new Error(`Cannot complete MultiAgentRun ${multiAgentRun.id}: ${blocked.join("; ")}`);
|
|
366
|
+
}
|
|
367
|
+
function completeOwnedMultiAgentRecords(run, multiAgentRun, reason, now) {
|
|
368
|
+
const state = ensureMultiAgentState(run);
|
|
369
|
+
for (const role of state.roles.filter((record) => record.multiAgentRunId === multiAgentRun.id)) {
|
|
370
|
+
if (role.status === "completed" || role.status === "cancelled")
|
|
371
|
+
continue;
|
|
372
|
+
const before = role.status;
|
|
373
|
+
role.status = "completed";
|
|
374
|
+
role.updatedAt = now;
|
|
375
|
+
role.lifecycle.push(lifecycleEvent(before, "completed", reason || "multi-agent run completed", undefined, undefined, now));
|
|
376
|
+
}
|
|
377
|
+
for (const group of state.groups.filter((record) => record.multiAgentRunId === multiAgentRun.id)) {
|
|
378
|
+
if (group.status === "completed" || group.status === "failed" || group.status === "cancelled")
|
|
379
|
+
continue;
|
|
380
|
+
const before = group.status;
|
|
381
|
+
group.status = "completed";
|
|
382
|
+
group.updatedAt = now;
|
|
383
|
+
group.lifecycle.push(lifecycleEvent(before, "completed", reason || "multi-agent run completed", undefined, undefined, now));
|
|
384
|
+
}
|
|
385
|
+
for (const fanout of state.fanouts.filter((record) => record.multiAgentRunId === multiAgentRun.id)) {
|
|
386
|
+
if (fanout.status === "completed" || fanout.status === "failed" || fanout.status === "cancelled")
|
|
387
|
+
continue;
|
|
388
|
+
const before = fanout.status;
|
|
389
|
+
fanout.status = "completed";
|
|
390
|
+
fanout.updatedAt = now;
|
|
391
|
+
fanout.lifecycle.push(lifecycleEvent(before, "completed", reason || "multi-agent run completed", undefined, undefined, now));
|
|
392
|
+
}
|
|
393
|
+
for (const fanin of state.fanins.filter((record) => record.multiAgentRunId === multiAgentRun.id)) {
|
|
394
|
+
if (fanin.status === "completed" || fanin.status === "failed")
|
|
395
|
+
continue;
|
|
396
|
+
const before = fanin.status;
|
|
397
|
+
fanin.status = "completed";
|
|
398
|
+
fanin.updatedAt = now;
|
|
399
|
+
fanin.lifecycle.push(lifecycleEvent(before, "completed", reason || "multi-agent run completed", undefined, undefined, now));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/** `policyFor` is injected (core/multi-agent/trust-policy.ts's
|
|
403
|
+
* `policyForRole`) so this file never has to import that module's
|
|
404
|
+
* cross-cutting policy shape directly at the top level — kept as a
|
|
405
|
+
* parameter purely to avoid an import cycle risk, not for genericity. */
|
|
406
|
+
function createAgentRole(run, input, now, policyFor) {
|
|
407
|
+
const state = ensureMultiAgentState(run);
|
|
408
|
+
const multiAgentRun = requireMultiAgentRun(run, input.multiAgentRunId);
|
|
409
|
+
const id = input.id || createId("role", state.roles.length + 1);
|
|
410
|
+
if (state.roles.some((record) => record.id === id))
|
|
411
|
+
throw new Error(`Duplicate AgentRole id: ${id}`);
|
|
412
|
+
if (input.parentRoleId)
|
|
413
|
+
requireAgentRole(run, input.parentRoleId);
|
|
414
|
+
const role = {
|
|
415
|
+
schemaVersion: exports.MULTI_AGENT_SCHEMA_VERSION,
|
|
416
|
+
id,
|
|
417
|
+
runId: run.id,
|
|
418
|
+
multiAgentRunId: multiAgentRun.id,
|
|
419
|
+
createdAt: now,
|
|
420
|
+
updatedAt: now,
|
|
421
|
+
status: "planned",
|
|
422
|
+
title: input.title || id,
|
|
423
|
+
responsibilities: input.responsibilities || [],
|
|
424
|
+
requiredEvidence: input.requiredEvidence || [],
|
|
425
|
+
sandboxProfileHints: input.sandboxProfileHints || [],
|
|
426
|
+
expectedArtifacts: input.expectedArtifacts || [],
|
|
427
|
+
faninObligations: input.faninObligations || [],
|
|
428
|
+
blackboardId: input.blackboardId || multiAgentRun.blackboardId,
|
|
429
|
+
topicIds: unique([...(multiAgentRun.topicIds || []), ...(input.topicIds || [])]),
|
|
430
|
+
lifecycle: [lifecycleEvent(undefined, "planned", "created", undefined, undefined, now)],
|
|
431
|
+
parentRoleId: input.parentRoleId,
|
|
432
|
+
childRoleIds: [],
|
|
433
|
+
policy: undefined,
|
|
434
|
+
metadata: compact(input.metadata),
|
|
435
|
+
};
|
|
436
|
+
role.policy = policyFor(role);
|
|
437
|
+
if (role.parentRoleId) {
|
|
438
|
+
const parent = requireAgentRole(run, role.parentRoleId);
|
|
439
|
+
parent.childRoleIds = unique([...parent.childRoleIds, role.id]);
|
|
440
|
+
touch(parent, now);
|
|
441
|
+
}
|
|
442
|
+
state.roles.push(role);
|
|
443
|
+
multiAgentRun.roleIds = unique([...multiAgentRun.roleIds, role.id]);
|
|
444
|
+
touch(multiAgentRun, now);
|
|
445
|
+
return role;
|
|
446
|
+
}
|
|
447
|
+
function createAgentGroup(run, input, now, policyFor) {
|
|
448
|
+
const state = ensureMultiAgentState(run);
|
|
449
|
+
const multiAgentRun = requireMultiAgentRun(run, input.multiAgentRunId);
|
|
450
|
+
const id = input.id || createId("group", state.groups.length + 1);
|
|
451
|
+
if (state.groups.some((record) => record.id === id))
|
|
452
|
+
throw new Error(`Duplicate AgentGroup id: ${id}`);
|
|
453
|
+
if (input.parentGroupId)
|
|
454
|
+
requireAgentGroup(run, input.parentGroupId);
|
|
455
|
+
for (const taskId of input.taskIds || [])
|
|
456
|
+
requireRunTask(run, taskId);
|
|
457
|
+
const group = {
|
|
458
|
+
schemaVersion: exports.MULTI_AGENT_SCHEMA_VERSION,
|
|
459
|
+
id,
|
|
460
|
+
runId: run.id,
|
|
461
|
+
multiAgentRunId: multiAgentRun.id,
|
|
462
|
+
createdAt: now,
|
|
463
|
+
updatedAt: now,
|
|
464
|
+
status: "forming",
|
|
465
|
+
title: input.title || id,
|
|
466
|
+
phase: input.phase,
|
|
467
|
+
phaseId: input.phaseId,
|
|
468
|
+
taskIds: unique(input.taskIds || []),
|
|
469
|
+
roleIds: [],
|
|
470
|
+
membershipIds: [],
|
|
471
|
+
workerIds: [],
|
|
472
|
+
fanoutIds: [],
|
|
473
|
+
faninIds: [],
|
|
474
|
+
blackboardId: input.blackboardId || multiAgentRun.blackboardId,
|
|
475
|
+
topicIds: unique([...(multiAgentRun.topicIds || []), ...(input.topicIds || [])]),
|
|
476
|
+
lifecycle: [lifecycleEvent(undefined, "forming", "created", undefined, undefined, now)],
|
|
477
|
+
parentGroupId: input.parentGroupId,
|
|
478
|
+
childGroupIds: [],
|
|
479
|
+
policy: undefined,
|
|
480
|
+
metadata: compact(input.metadata),
|
|
481
|
+
};
|
|
482
|
+
group.policy = policyFor(group);
|
|
483
|
+
if (group.parentGroupId) {
|
|
484
|
+
const parent = requireAgentGroup(run, group.parentGroupId);
|
|
485
|
+
parent.childGroupIds = unique([...parent.childGroupIds, group.id]);
|
|
486
|
+
touch(parent, now);
|
|
487
|
+
}
|
|
488
|
+
state.groups.push(group);
|
|
489
|
+
multiAgentRun.groupIds = unique([...multiAgentRun.groupIds, group.id]);
|
|
490
|
+
touch(multiAgentRun, now);
|
|
491
|
+
return group;
|
|
492
|
+
}
|
|
493
|
+
function assignAgentMembership(run, input, now, policyForMembership, workerExists) {
|
|
494
|
+
const state = ensureMultiAgentState(run);
|
|
495
|
+
const group = requireAgentGroup(run, input.groupId);
|
|
496
|
+
const role = requireAgentRole(run, input.roleId);
|
|
497
|
+
if (role.multiAgentRunId !== group.multiAgentRunId) {
|
|
498
|
+
throw new Error(`AgentRole ${role.id} belongs to ${role.multiAgentRunId}, not group run ${group.multiAgentRunId}`);
|
|
499
|
+
}
|
|
500
|
+
if (input.multiAgentRunId && input.multiAgentRunId !== group.multiAgentRunId) {
|
|
501
|
+
throw new Error(`Membership multiAgentRunId ${input.multiAgentRunId} does not match group ${group.id}`);
|
|
502
|
+
}
|
|
503
|
+
const task = requireRunTask(run, input.taskId);
|
|
504
|
+
if (input.workerId && !workerExists(input.workerId)) {
|
|
505
|
+
throw new Error(`Unknown worker id for membership: ${input.workerId}`);
|
|
506
|
+
}
|
|
507
|
+
const duplicate = state.memberships.find((membership) => membership.groupId === group.id &&
|
|
508
|
+
membership.roleId === role.id &&
|
|
509
|
+
membership.taskId === task.id &&
|
|
510
|
+
(input.workerId ? membership.workerId === input.workerId : !membership.workerId));
|
|
511
|
+
if (duplicate) {
|
|
512
|
+
throw new Error(`Duplicate AgentMembership for group=${group.id}, role=${role.id}, task=${task.id}, worker=${input.workerId || "none"}`);
|
|
513
|
+
}
|
|
514
|
+
const id = input.id || createId("membership", state.memberships.length + 1);
|
|
515
|
+
if (state.memberships.some((record) => record.id === id))
|
|
516
|
+
throw new Error(`Duplicate AgentMembership id: ${id}`);
|
|
517
|
+
const status = input.status || (input.workerId ? "running" : "assigned");
|
|
518
|
+
const membership = {
|
|
519
|
+
schemaVersion: exports.MULTI_AGENT_SCHEMA_VERSION,
|
|
520
|
+
id,
|
|
521
|
+
runId: run.id,
|
|
522
|
+
multiAgentRunId: group.multiAgentRunId,
|
|
523
|
+
groupId: group.id,
|
|
524
|
+
roleId: role.id,
|
|
525
|
+
taskId: task.id,
|
|
526
|
+
workerId: input.workerId,
|
|
527
|
+
dispatchId: input.dispatchId,
|
|
528
|
+
fanoutId: input.fanoutId,
|
|
529
|
+
createdAt: now,
|
|
530
|
+
updatedAt: now,
|
|
531
|
+
status,
|
|
532
|
+
lifecycle: [lifecycleEvent(undefined, status, "assigned", undefined, undefined, now)],
|
|
533
|
+
evidenceRefs: [],
|
|
534
|
+
artifactPaths: [],
|
|
535
|
+
blackboardId: input.blackboardId || group.blackboardId || role.blackboardId,
|
|
536
|
+
topicIds: unique([...(group.topicIds || []), ...(role.topicIds || []), ...(input.topicIds || [])]),
|
|
537
|
+
blackboardMessageIds: [],
|
|
538
|
+
blackboardArtifactRefIds: [],
|
|
539
|
+
policy: undefined,
|
|
540
|
+
metadata: compact(input.metadata),
|
|
541
|
+
};
|
|
542
|
+
membership.policy = policyForMembership(membership, role);
|
|
543
|
+
state.memberships.push(membership);
|
|
544
|
+
group.membershipIds = unique([...group.membershipIds, membership.id]);
|
|
545
|
+
group.roleIds = unique([...group.roleIds, role.id]);
|
|
546
|
+
group.taskIds = unique([...group.taskIds, task.id]);
|
|
547
|
+
if (membership.workerId)
|
|
548
|
+
group.workerIds = unique([...group.workerIds, membership.workerId]);
|
|
549
|
+
touch(group, now);
|
|
550
|
+
const roleStatusBefore = role.status;
|
|
551
|
+
role.status = "active";
|
|
552
|
+
role.updatedAt = now;
|
|
553
|
+
role.lifecycle.push(lifecycleEvent(roleStatusBefore, "active", "membership assigned", undefined, undefined, now));
|
|
554
|
+
return membership;
|
|
555
|
+
}
|
|
556
|
+
function createAgentFanout(run, input, now) {
|
|
557
|
+
const state = ensureMultiAgentState(run);
|
|
558
|
+
const group = requireAgentGroup(run, input.groupId);
|
|
559
|
+
const multiAgentRun = requireMultiAgentRun(run, input.multiAgentRunId || group.multiAgentRunId);
|
|
560
|
+
if (group.multiAgentRunId !== multiAgentRun.id)
|
|
561
|
+
throw new Error(`AgentGroup ${group.id} does not belong to ${multiAgentRun.id}`);
|
|
562
|
+
const id = input.id || createId("fanout", state.fanouts.length + 1);
|
|
563
|
+
if (state.fanouts.some((record) => record.id === id))
|
|
564
|
+
throw new Error(`Duplicate AgentFanout id: ${id}`);
|
|
565
|
+
for (const roleId of input.roleIds || [])
|
|
566
|
+
requireAgentRole(run, roleId);
|
|
567
|
+
for (const taskId of input.taskIds || [])
|
|
568
|
+
requireRunTask(run, taskId);
|
|
569
|
+
const roleIds = unique(input.roleIds || group.roleIds);
|
|
570
|
+
const taskIds = unique(input.taskIds || group.taskIds);
|
|
571
|
+
const fanout = {
|
|
572
|
+
schemaVersion: exports.MULTI_AGENT_SCHEMA_VERSION,
|
|
573
|
+
id,
|
|
574
|
+
runId: run.id,
|
|
575
|
+
multiAgentRunId: multiAgentRun.id,
|
|
576
|
+
groupId: group.id,
|
|
577
|
+
createdAt: now,
|
|
578
|
+
updatedAt: now,
|
|
579
|
+
status: "planned",
|
|
580
|
+
reason: input.reason,
|
|
581
|
+
roleIds,
|
|
582
|
+
taskIds,
|
|
583
|
+
workerIds: unique(input.workerIds || []),
|
|
584
|
+
membershipIds: unique(input.membershipIds || []),
|
|
585
|
+
dispatchIds: unique(input.dispatchIds || []),
|
|
586
|
+
concurrencyLimit: input.concurrencyLimit,
|
|
587
|
+
sandboxProfileChoices: input.sandboxProfileChoices || {},
|
|
588
|
+
expectedReturnShape: input.expectedReturnShape || "Each member writes a Markdown result with a cw:result JSON fence containing summary, findings, and evidence.",
|
|
589
|
+
blackboardId: input.blackboardId || group.blackboardId || multiAgentRun.blackboardId,
|
|
590
|
+
topicIds: unique([...(group.topicIds || []), ...(multiAgentRun.topicIds || []), ...(input.topicIds || [])]),
|
|
591
|
+
lifecycle: [lifecycleEvent(undefined, "planned", "created", undefined, undefined, now)],
|
|
592
|
+
policy: {
|
|
593
|
+
schemaVersion: 1,
|
|
594
|
+
id: `${id}-policy`,
|
|
595
|
+
policyRef: `multiAgent.fanouts.${id}.policy`,
|
|
596
|
+
subjectKind: "fanout",
|
|
597
|
+
subjectId: id,
|
|
598
|
+
allowedBlackboardTopicIds: unique([...(group.topicIds || []), ...(multiAgentRun.topicIds || []), ...(input.topicIds || [])]),
|
|
599
|
+
allowedWriteOperations: ["message", "context", "artifact"],
|
|
600
|
+
allowedCandidateOperations: ["register"],
|
|
601
|
+
allowedJudgeOperations: [],
|
|
602
|
+
sandboxProfileHints: unique(Object.values(input.sandboxProfileChoices || {}).map(String)),
|
|
603
|
+
requiredEvidenceRefs: [],
|
|
604
|
+
deniedOperations: [],
|
|
605
|
+
metadata: { reason: input.reason },
|
|
606
|
+
},
|
|
607
|
+
metadata: compact(input.metadata),
|
|
608
|
+
};
|
|
609
|
+
state.fanouts.push(fanout);
|
|
610
|
+
group.fanoutIds = unique([...group.fanoutIds, fanout.id]);
|
|
611
|
+
group.roleIds = unique([...group.roleIds, ...fanout.roleIds]);
|
|
612
|
+
group.taskIds = unique([...group.taskIds, ...fanout.taskIds]);
|
|
613
|
+
touch(group, now);
|
|
614
|
+
multiAgentRun.fanoutIds = unique([...multiAgentRun.fanoutIds, fanout.id]);
|
|
615
|
+
touch(multiAgentRun, now);
|
|
616
|
+
return fanout;
|
|
617
|
+
}
|
|
618
|
+
function collectAgentFanin(run, input, now) {
|
|
619
|
+
const state = ensureMultiAgentState(run);
|
|
620
|
+
const fanout = input.fanoutId ? requireAgentFanout(run, input.fanoutId) : undefined;
|
|
621
|
+
const group = requireAgentGroup(run, input.groupId || fanout?.groupId || "");
|
|
622
|
+
const multiAgentRun = requireMultiAgentRun(run, input.multiAgentRunId || group.multiAgentRunId);
|
|
623
|
+
if (group.multiAgentRunId !== multiAgentRun.id)
|
|
624
|
+
throw new Error(`Group ${group.id} does not belong to MultiAgentRun ${multiAgentRun.id}`);
|
|
625
|
+
if (fanout && fanout.groupId !== group.id)
|
|
626
|
+
throw new Error(`Fanout ${fanout.id} does not belong to group ${group.id}`);
|
|
627
|
+
const id = input.id || createId("fanin", state.fanins.length + 1);
|
|
628
|
+
if (state.fanins.some((record) => record.id === id))
|
|
629
|
+
throw new Error(`Duplicate AgentFanin id: ${id}`);
|
|
630
|
+
const requiredRoleIds = unique(input.requiredRoleIds?.length ? input.requiredRoleIds : group.roleIds);
|
|
631
|
+
for (const roleId of requiredRoleIds)
|
|
632
|
+
requireAgentRole(run, roleId);
|
|
633
|
+
const scopedMemberships = state.memberships.filter((membership) => membership.groupId === group.id && (!fanout || membership.fanoutId === fanout.id));
|
|
634
|
+
const coverage = scopedMemberships.map((membership) => ({
|
|
635
|
+
membershipId: membership.id,
|
|
636
|
+
roleId: membership.roleId,
|
|
637
|
+
taskId: membership.taskId,
|
|
638
|
+
workerId: membership.workerId,
|
|
639
|
+
evidenceRefs: membership.evidenceRefs,
|
|
640
|
+
blackboardMessageIds: membership.blackboardMessageIds || [],
|
|
641
|
+
blackboardArtifactRefIds: membership.blackboardArtifactRefIds || [],
|
|
642
|
+
resultNodeId: membership.resultNodeId,
|
|
643
|
+
verifierNodeId: membership.verifierNodeId,
|
|
644
|
+
complete: isMembershipReported(membership),
|
|
645
|
+
}));
|
|
646
|
+
const missingRoleIds = requiredRoleIds.filter((roleId) => !scopedMemberships.some((membership) => membership.roleId === roleId));
|
|
647
|
+
const missingMembershipIds = scopedMemberships
|
|
648
|
+
.filter((membership) => requiredRoleIds.includes(membership.roleId) && !isMembershipReported(membership))
|
|
649
|
+
.map((membership) => membership.id);
|
|
650
|
+
const blockedReasons = [
|
|
651
|
+
...missingRoleIds.map((roleId) => `required role ${roleId} has no membership`),
|
|
652
|
+
...missingMembershipIds.map((membershipId) => `membership ${membershipId} has not reported required evidence`),
|
|
653
|
+
];
|
|
654
|
+
const requiredMemberships = scopedMemberships.filter((membership) => requiredRoleIds.includes(membership.roleId));
|
|
655
|
+
const blackboardId = input.blackboardId || group.blackboardId || multiAgentRun.blackboardId;
|
|
656
|
+
const requiresBlackboardEvidence = Boolean(blackboardId || requiredMemberships.some((membership) => membership.blackboardId));
|
|
657
|
+
if (requiresBlackboardEvidence) {
|
|
658
|
+
for (const membership of requiredMemberships) {
|
|
659
|
+
const indexedEvidence = [...(membership.blackboardArtifactRefIds || []), ...(membership.blackboardMessageIds || [])];
|
|
660
|
+
if (!indexedEvidence.length)
|
|
661
|
+
blockedReasons.push(`membership ${membership.id} has no indexed blackboard evidence`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const verifierReady = blockedReasons.length === 0;
|
|
665
|
+
const status = verifierReady ? "ready" : "blocked";
|
|
666
|
+
const fanin = {
|
|
667
|
+
schemaVersion: exports.MULTI_AGENT_SCHEMA_VERSION,
|
|
668
|
+
id,
|
|
669
|
+
runId: run.id,
|
|
670
|
+
multiAgentRunId: multiAgentRun.id,
|
|
671
|
+
groupId: group.id,
|
|
672
|
+
fanoutId: fanout?.id,
|
|
673
|
+
createdAt: now,
|
|
674
|
+
updatedAt: now,
|
|
675
|
+
status,
|
|
676
|
+
strategy: input.strategy || "required-role-evidence",
|
|
677
|
+
requiredRoleIds,
|
|
678
|
+
reportedMembershipIds: coverage.filter((entry) => entry.complete).map((entry) => entry.membershipId),
|
|
679
|
+
missingMembershipIds,
|
|
680
|
+
missingRoleIds,
|
|
681
|
+
evidenceCoverage: coverage,
|
|
682
|
+
verifierReady,
|
|
683
|
+
blockedReasons,
|
|
684
|
+
blackboardId,
|
|
685
|
+
topicIds: unique([...(group.topicIds || []), ...(multiAgentRun.topicIds || []), ...(input.topicIds || [])]),
|
|
686
|
+
blackboardArtifactRefIds: unique(coverage.flatMap((entry) => entry.blackboardArtifactRefIds || [])),
|
|
687
|
+
blackboardMessageIds: unique(coverage.flatMap((entry) => entry.blackboardMessageIds || [])),
|
|
688
|
+
lifecycle: [lifecycleEvent(undefined, status, "collected", undefined, undefined, now)],
|
|
689
|
+
policy: {
|
|
690
|
+
schemaVersion: 1,
|
|
691
|
+
id: `${id}-policy`,
|
|
692
|
+
policyRef: `multiAgent.fanins.${id}.policy`,
|
|
693
|
+
subjectKind: "fanin",
|
|
694
|
+
subjectId: id,
|
|
695
|
+
allowedBlackboardTopicIds: unique([...(group.topicIds || []), ...(multiAgentRun.topicIds || []), ...(input.topicIds || [])]),
|
|
696
|
+
allowedWriteOperations: ["message", "context", "artifact", "snapshot", "coordinator-decision"],
|
|
697
|
+
allowedCandidateOperations: verifierReady ? ["register", "score", "select"] : [],
|
|
698
|
+
allowedJudgeOperations: verifierReady ? ["panel-decision", "rationale"] : [],
|
|
699
|
+
sandboxProfileHints: [],
|
|
700
|
+
requiredEvidenceRefs: unique(coverage.flatMap((entry) => entry.evidenceRefs)),
|
|
701
|
+
deniedOperations: verifierReady ? [] : blockedReasons.map((reason) => ({ operation: "candidate.select", reason })),
|
|
702
|
+
metadata: { verifierReady, strategy: input.strategy || "required-role-evidence" },
|
|
703
|
+
},
|
|
704
|
+
metadata: compact(input.metadata),
|
|
705
|
+
};
|
|
706
|
+
state.fanins.push(fanin);
|
|
707
|
+
group.faninIds = unique([...group.faninIds, fanin.id]);
|
|
708
|
+
group.status = verifierReady ? "verifying" : "collecting";
|
|
709
|
+
touch(group, now);
|
|
710
|
+
multiAgentRun.faninIds = unique([...multiAgentRun.faninIds, fanin.id]);
|
|
711
|
+
multiAgentRun.status = verifierReady ? "verifying" : "collecting";
|
|
712
|
+
touch(multiAgentRun, now);
|
|
713
|
+
return fanin;
|
|
714
|
+
}
|
|
715
|
+
/** `attachDispatchToMultiAgent` — ties a dispatch to multi-agent state.
|
|
716
|
+
* Silent no-op (`{membershipIds: []}`) when NONE of the four ids are
|
|
717
|
+
* given. `policyForMembership` and `workerExists` are injected the same
|
|
718
|
+
* way `assignAgentMembership` needs them. */
|
|
719
|
+
function attachDispatchToMultiAgent(run, input, now, policyForMembership, workerExists) {
|
|
720
|
+
if (!input.multiAgentRunId && !input.groupId && !input.roleId && !input.fanoutId)
|
|
721
|
+
return { membershipIds: [] };
|
|
722
|
+
ensureMultiAgentState(run);
|
|
723
|
+
let fanout = input.fanoutId ? requireAgentFanout(run, input.fanoutId) : undefined;
|
|
724
|
+
let group = input.groupId ? requireAgentGroup(run, input.groupId) : undefined;
|
|
725
|
+
if (!group && fanout)
|
|
726
|
+
group = requireAgentGroup(run, fanout.groupId);
|
|
727
|
+
const multiAgentRun = requireMultiAgentRun(run, input.multiAgentRunId || group?.multiAgentRunId || fanout?.multiAgentRunId || "");
|
|
728
|
+
if (!group)
|
|
729
|
+
throw new Error("Dispatch multi-agent attach requires --multi-agent-group or --multiAgentGroup");
|
|
730
|
+
if (group.multiAgentRunId !== multiAgentRun.id)
|
|
731
|
+
throw new Error(`Group ${group.id} does not belong to MultiAgentRun ${multiAgentRun.id}`);
|
|
732
|
+
const roleIds = input.roleId ? [input.roleId] : unique([...(fanout ? fanout.roleIds : [])]);
|
|
733
|
+
if (roleIds.length !== 1) {
|
|
734
|
+
throw new Error(`Dispatch multi-agent attach requires exactly one role for deterministic membership; found ${roleIds.length || 0}`);
|
|
735
|
+
}
|
|
736
|
+
const role = requireAgentRole(run, roleIds[0]);
|
|
737
|
+
if (role.multiAgentRunId !== multiAgentRun.id)
|
|
738
|
+
throw new Error(`Role ${role.id} does not belong to MultiAgentRun ${multiAgentRun.id}`);
|
|
739
|
+
if (!fanout) {
|
|
740
|
+
fanout = createAgentFanout(run, {
|
|
741
|
+
multiAgentRunId: multiAgentRun.id,
|
|
742
|
+
groupId: group.id,
|
|
743
|
+
reason: "dispatch attachment",
|
|
744
|
+
roleIds: [role.id],
|
|
745
|
+
taskIds: input.tasks.map((task) => task.id),
|
|
746
|
+
dispatchIds: [input.dispatchId],
|
|
747
|
+
concurrencyLimit: input.concurrencyLimit,
|
|
748
|
+
sandboxProfileChoices: input.sandboxProfileId ? { dispatch: input.sandboxProfileId } : {},
|
|
749
|
+
}, now);
|
|
750
|
+
}
|
|
751
|
+
if (fanout.multiAgentRunId !== multiAgentRun.id || fanout.groupId !== group.id) {
|
|
752
|
+
throw new Error(`Fanout ${fanout.id} does not match MultiAgentRun ${multiAgentRun.id} and group ${group.id}`);
|
|
753
|
+
}
|
|
754
|
+
const membershipIds = [];
|
|
755
|
+
for (const task of input.tasks) {
|
|
756
|
+
if (!task.workerId)
|
|
757
|
+
throw new Error(`Task ${task.id} has no worker id for multi-agent membership`);
|
|
758
|
+
const membership = assignAgentMembership(run, { multiAgentRunId: multiAgentRun.id, groupId: group.id, roleId: role.id, taskId: task.id, workerId: task.workerId, dispatchId: input.dispatchId, fanoutId: fanout.id, status: "running" }, now, policyForMembership, workerExists);
|
|
759
|
+
task.multiAgent = { runId: multiAgentRun.id, groupId: group.id, roleId: role.id, membershipId: membership.id, fanoutId: fanout.id };
|
|
760
|
+
membershipIds.push(membership.id);
|
|
761
|
+
}
|
|
762
|
+
fanout.status = "dispatched";
|
|
763
|
+
fanout.updatedAt = now;
|
|
764
|
+
fanout.lifecycle.push(lifecycleEvent("planned", "dispatched", "dispatch created", undefined, undefined, now));
|
|
765
|
+
fanout.dispatchIds = unique([...fanout.dispatchIds, input.dispatchId]);
|
|
766
|
+
fanout.taskIds = unique([...fanout.taskIds, ...input.tasks.map((task) => task.id)]);
|
|
767
|
+
fanout.workerIds = unique([...fanout.workerIds, ...input.tasks.map((task) => task.workerId || "").filter(Boolean)]);
|
|
768
|
+
fanout.membershipIds = unique([...fanout.membershipIds, ...membershipIds]);
|
|
769
|
+
if (input.sandboxProfileId)
|
|
770
|
+
fanout.sandboxProfileChoices.dispatch = input.sandboxProfileId;
|
|
771
|
+
const groupStatusBefore = group.status;
|
|
772
|
+
group.status = "running";
|
|
773
|
+
group.updatedAt = fanout.updatedAt;
|
|
774
|
+
group.lifecycle.push(lifecycleEvent(groupStatusBefore, "running", "dispatch created", undefined, undefined, now));
|
|
775
|
+
multiAgentRun.status = multiAgentRun.status === "planned" || multiAgentRun.status === "forming" ? "running" : multiAgentRun.status;
|
|
776
|
+
touch(multiAgentRun, now);
|
|
777
|
+
return { multiAgent: { runId: multiAgentRun.id, groupId: group.id, roleId: role.id, fanoutId: fanout.id }, membershipIds };
|
|
778
|
+
}
|
|
779
|
+
function recordMultiAgentWorkerOutput(run, input, now) {
|
|
780
|
+
const state = ensureMultiAgentState(run);
|
|
781
|
+
const memberships = state.memberships.filter((membership) => membership.workerId === input.workerId && membership.taskId === input.taskId);
|
|
782
|
+
if (!memberships.length)
|
|
783
|
+
return [];
|
|
784
|
+
const evidenceRefs = input.evidence.map((entry) => entry.locator || entry.path || entry.summary || entry.id).filter(Boolean);
|
|
785
|
+
for (const membership of memberships) {
|
|
786
|
+
const before = membership.status;
|
|
787
|
+
membership.status = "reported";
|
|
788
|
+
membership.updatedAt = now;
|
|
789
|
+
membership.resultNodeId = input.resultNodeId || membership.resultNodeId;
|
|
790
|
+
membership.verifierNodeId = input.verifierNodeId || membership.verifierNodeId;
|
|
791
|
+
membership.evidenceRefs = unique([...membership.evidenceRefs, ...evidenceRefs]);
|
|
792
|
+
membership.artifactPaths = unique([...(membership.artifactPaths || []), ...(input.artifactPaths || [])]);
|
|
793
|
+
membership.blackboardMessageIds = unique([...(membership.blackboardMessageIds || []), ...(input.blackboardMessageIds || [])]);
|
|
794
|
+
membership.blackboardArtifactRefIds = unique([...(membership.blackboardArtifactRefIds || []), ...(input.blackboardArtifactRefIds || [])]);
|
|
795
|
+
membership.lifecycle.push(lifecycleEvent(before, "reported", "worker output accepted", undefined, undefined, now));
|
|
796
|
+
}
|
|
797
|
+
return memberships;
|
|
798
|
+
}
|
|
799
|
+
function summarizeMultiAgent(run) {
|
|
800
|
+
const state = ensureMultiAgentState(run);
|
|
801
|
+
const blockedReasons = [];
|
|
802
|
+
for (const fanin of state.fanins)
|
|
803
|
+
blockedReasons.push(...fanin.blockedReasons.map((reason) => `${fanin.id}: ${reason}`));
|
|
804
|
+
for (const membership of state.memberships) {
|
|
805
|
+
if (membership.status === "failed")
|
|
806
|
+
blockedReasons.push(`${membership.id}: failed membership`);
|
|
807
|
+
}
|
|
808
|
+
const groupsDetail = state.groups.map((group) => {
|
|
809
|
+
const roleIds = unique([...group.roleIds, ...state.memberships.filter((membership) => membership.groupId === group.id).map((membership) => membership.roleId)]);
|
|
810
|
+
return {
|
|
811
|
+
id: group.id,
|
|
812
|
+
multiAgentRunId: group.multiAgentRunId,
|
|
813
|
+
status: group.status,
|
|
814
|
+
phase: group.phase,
|
|
815
|
+
roles: roleIds.map((roleId) => {
|
|
816
|
+
const role = state.roles.find((entry) => entry.id === roleId);
|
|
817
|
+
const memberships = state.memberships.filter((membership) => membership.groupId === group.id && membership.roleId === roleId);
|
|
818
|
+
const reported = memberships.filter(isMembershipReported).length;
|
|
819
|
+
return { roleId, requiredEvidence: role?.requiredEvidence.length || 0, memberships: memberships.length, reported, missing: Math.max(0, memberships.length - reported) };
|
|
820
|
+
}),
|
|
821
|
+
fanouts: group.fanoutIds,
|
|
822
|
+
fanins: group.faninIds,
|
|
823
|
+
};
|
|
824
|
+
});
|
|
825
|
+
return {
|
|
826
|
+
totalRuns: state.runs.length,
|
|
827
|
+
runsByStatus: countBy(state.runs, (record) => record.status),
|
|
828
|
+
roles: state.roles.length,
|
|
829
|
+
groups: state.groups.length,
|
|
830
|
+
memberships: state.memberships.length,
|
|
831
|
+
fanouts: state.fanouts.length,
|
|
832
|
+
fanins: state.fanins.length,
|
|
833
|
+
groupsByStatus: countBy(state.groups, (record) => record.status),
|
|
834
|
+
membershipsByStatus: countBy(state.memberships, (record) => record.status),
|
|
835
|
+
faninsByStatus: countBy(state.fanins, (record) => record.status),
|
|
836
|
+
blockedReasons,
|
|
837
|
+
groupsDetail,
|
|
838
|
+
nextAction: nextMultiAgentAction(run, blockedReasons),
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function nextMultiAgentAction(run, blockedReasons) {
|
|
842
|
+
const state = ensureMultiAgentState(run);
|
|
843
|
+
if (!state.runs.length)
|
|
844
|
+
return `node scripts/cw.js multi-agent run ${run.id} --id <multi-agent-run-id>`;
|
|
845
|
+
if (blockedReasons.length)
|
|
846
|
+
return `node scripts/cw.js multi-agent fanin ${run.id} --group <group-id> --fanout <fanout-id>`;
|
|
847
|
+
const running = state.memberships.find((membership) => membership.status === "running");
|
|
848
|
+
if (running?.workerId)
|
|
849
|
+
return `node scripts/cw.js worker manifest ${run.id} ${running.workerId}`;
|
|
850
|
+
const groupWithoutFanin = state.groups.find((group) => group.membershipIds.length && !group.faninIds.length);
|
|
851
|
+
if (groupWithoutFanin)
|
|
852
|
+
return `node scripts/cw.js multi-agent fanin ${run.id} --group ${groupWithoutFanin.id}`;
|
|
853
|
+
return undefined;
|
|
854
|
+
}
|
|
855
|
+
function buildMultiAgentGraph(run) {
|
|
856
|
+
const state = ensureMultiAgentState(run);
|
|
857
|
+
const nodes = [];
|
|
858
|
+
const edges = [];
|
|
859
|
+
for (const record of state.runs) {
|
|
860
|
+
nodes.push({ id: `${run.id}:multi-agent:${record.id}`, kind: "multi-agent-run", status: record.status, label: record.title || record.id, path: recordPath(run, "runs", record.id) });
|
|
861
|
+
edges.push({ from: `${run.id}:run`, to: `${run.id}:multi-agent:${record.id}` });
|
|
862
|
+
if (record.blackboardId)
|
|
863
|
+
edges.push({ from: `${run.id}:multi-agent:${record.id}`, to: `${run.id}:blackboard:${record.blackboardId}`, label: "blackboard" });
|
|
864
|
+
if (record.parentMultiAgentRunId)
|
|
865
|
+
edges.push({ from: `${run.id}:multi-agent:${record.parentMultiAgentRunId}`, to: `${run.id}:multi-agent:${record.id}`, label: "child" });
|
|
866
|
+
}
|
|
867
|
+
for (const record of state.roles) {
|
|
868
|
+
nodes.push({ id: `${run.id}:multi-agent:role:${record.id}`, kind: "agent-role", status: record.status, label: record.title, path: recordPath(run, "roles", record.id) });
|
|
869
|
+
edges.push({ from: `${run.id}:multi-agent:${record.multiAgentRunId}`, to: `${run.id}:multi-agent:role:${record.id}` });
|
|
870
|
+
if (record.blackboardId)
|
|
871
|
+
edges.push({ from: `${run.id}:multi-agent:role:${record.id}`, to: `${run.id}:blackboard:${record.blackboardId}`, label: "blackboard" });
|
|
872
|
+
}
|
|
873
|
+
for (const record of state.groups) {
|
|
874
|
+
nodes.push({ id: `${run.id}:multi-agent:group:${record.id}`, kind: "agent-group", status: record.status, label: record.title || record.id, path: recordPath(run, "groups", record.id) });
|
|
875
|
+
edges.push({ from: `${run.id}:multi-agent:${record.multiAgentRunId}`, to: `${run.id}:multi-agent:group:${record.id}` });
|
|
876
|
+
if (record.blackboardId)
|
|
877
|
+
edges.push({ from: `${run.id}:multi-agent:group:${record.id}`, to: `${run.id}:blackboard:${record.blackboardId}`, label: "blackboard" });
|
|
878
|
+
for (const taskId of record.taskIds)
|
|
879
|
+
edges.push({ from: `${run.id}:multi-agent:group:${record.id}`, to: `${run.id}:task:${taskId}`, label: "task" });
|
|
880
|
+
}
|
|
881
|
+
for (const record of state.fanouts) {
|
|
882
|
+
nodes.push({ id: `${run.id}:multi-agent:fanout:${record.id}`, kind: "agent-fanout", status: record.status, label: record.reason, path: recordPath(run, "fanouts", record.id) });
|
|
883
|
+
edges.push({ from: `${run.id}:multi-agent:group:${record.groupId}`, to: `${run.id}:multi-agent:fanout:${record.id}` });
|
|
884
|
+
for (const dispatchId of record.dispatchIds)
|
|
885
|
+
edges.push({ from: `${run.id}:multi-agent:fanout:${record.id}`, to: `${run.id}:dispatch:${dispatchId}`, label: "dispatch" });
|
|
886
|
+
}
|
|
887
|
+
for (const record of state.memberships) {
|
|
888
|
+
nodes.push({ id: `${run.id}:multi-agent:membership:${record.id}`, kind: "agent-membership", status: record.status, label: `${record.roleId}/${record.taskId}`, path: recordPath(run, "memberships", record.id) });
|
|
889
|
+
edges.push({ from: `${run.id}:multi-agent:group:${record.groupId}`, to: `${run.id}:multi-agent:membership:${record.id}` });
|
|
890
|
+
edges.push({ from: `${run.id}:multi-agent:role:${record.roleId}`, to: `${run.id}:multi-agent:membership:${record.id}` });
|
|
891
|
+
edges.push({ from: `${run.id}:multi-agent:membership:${record.id}`, to: `${run.id}:task:${record.taskId}`, label: "task" });
|
|
892
|
+
if (record.workerId)
|
|
893
|
+
edges.push({ from: `${run.id}:multi-agent:membership:${record.id}`, to: `${run.id}:worker:${record.workerId}`, label: "worker" });
|
|
894
|
+
if (record.resultNodeId)
|
|
895
|
+
edges.push({ from: `${run.id}:multi-agent:membership:${record.id}`, to: record.resultNodeId, label: "result" });
|
|
896
|
+
if (record.verifierNodeId)
|
|
897
|
+
edges.push({ from: `${run.id}:multi-agent:membership:${record.id}`, to: record.verifierNodeId, label: "verifier" });
|
|
898
|
+
if (record.blackboardId)
|
|
899
|
+
edges.push({ from: `${run.id}:multi-agent:membership:${record.id}`, to: `${run.id}:blackboard:${record.blackboardId}`, label: "blackboard" });
|
|
900
|
+
for (const artifactId of record.blackboardArtifactRefIds || [])
|
|
901
|
+
edges.push({ from: `${run.id}:multi-agent:membership:${record.id}`, to: `${run.id}:blackboard:artifact:${artifactId}`, label: "evidence" });
|
|
902
|
+
for (const messageId of record.blackboardMessageIds || [])
|
|
903
|
+
edges.push({ from: `${run.id}:multi-agent:membership:${record.id}`, to: `${run.id}:blackboard:message:${messageId}`, label: "message" });
|
|
904
|
+
}
|
|
905
|
+
for (const record of state.fanins) {
|
|
906
|
+
nodes.push({ id: `${run.id}:multi-agent:fanin:${record.id}`, kind: "agent-fanin", status: record.status, label: record.strategy, path: recordPath(run, "fanins", record.id) });
|
|
907
|
+
edges.push({ from: `${run.id}:multi-agent:group:${record.groupId}`, to: `${run.id}:multi-agent:fanin:${record.id}` });
|
|
908
|
+
if (record.fanoutId)
|
|
909
|
+
edges.push({ from: `${run.id}:multi-agent:fanout:${record.fanoutId}`, to: `${run.id}:multi-agent:fanin:${record.id}` });
|
|
910
|
+
for (const membershipId of record.reportedMembershipIds)
|
|
911
|
+
edges.push({ from: `${run.id}:multi-agent:membership:${membershipId}`, to: `${run.id}:multi-agent:fanin:${record.id}`, label: "reported" });
|
|
912
|
+
for (const membershipId of record.missingMembershipIds)
|
|
913
|
+
edges.push({ from: `${run.id}:multi-agent:membership:${membershipId}`, to: `${run.id}:multi-agent:fanin:${record.id}`, label: "missing" });
|
|
914
|
+
if (record.blackboardId)
|
|
915
|
+
edges.push({ from: `${run.id}:multi-agent:fanin:${record.id}`, to: `${run.id}:blackboard:${record.blackboardId}`, label: "blackboard" });
|
|
916
|
+
}
|
|
917
|
+
return { nodes, edges: uniqueEdges(edges) };
|
|
918
|
+
}
|
|
919
|
+
/** Path derivation matching multi-agent/paths.ts: `<multiAgentDir>/<plural
|
|
920
|
+
* kind>/<safeFileName(id)>.json`. Pure — `run.paths.multiAgentDir` is
|
|
921
|
+
* expected to already be set (shell/multi-agent-io.ts's
|
|
922
|
+
* ensureMultiAgentState sets it before calling into this file). */
|
|
923
|
+
function recordPath(run, kind, id) {
|
|
924
|
+
const root = run.paths.multiAgentDir || `${run.paths.runDir}/multi-agent`;
|
|
925
|
+
return `${root}/${kind}/${safeFileName(id)}.json`;
|
|
926
|
+
}
|
|
927
|
+
function multiAgentRoot(run) {
|
|
928
|
+
return run.paths.multiAgentDir || `${run.paths.runDir}/multi-agent`;
|
|
929
|
+
}
|