From 847879d148ea1064d3c6e13cfb16da0d318096e1 Mon Sep 17 00:00:00 2001 From: Christopher Nelson Date: Tue, 22 Sep 2026 06:46:44 -0400 Subject: [PATCH] refactor: make experiment telemetry swarm-native MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the experiment export schema to version 12 and drop support for versions 9, 10, and 11. Those versions existed to carry exports written before independent attempt accounting and, earlier, tick summaries; with the legacy architecture gone, their conditional branches only described shapes the exporter can no longer produce. Attempt accounting is now unconditional rather than a schema-v11-only requirement, and the exporter always emits swarm ticks, provider attempts, retention, and accounting instead of spreading them conditionally. The archive importer accepts version 12 alone and rejects anything else outright rather than migrating it. Remove the last scenario compatibility shim: archivedAppliedScenarioSchema no longer translates cognitionMode/decisionContractVersion into historical fields, which removes the final 'legacy-multi-agent' literal from the codebase. The schema keeps its name because it still tolerates archived scenarios written under relaxed pre-v2 execution and objective defaults, which is unrelated to cognition architecture. Documentation records the decision rather than rewriting the past. ADR 0033 states the retirement and is explicit that the argument is structural — one planning call per tick versus one per agent per tick — and that no run in this repository has compared legacy against zero-swarm cognition on real providers. Eleven ADRs specific to the retired architecture are marked superseded in their existing status fields; none are deleted. The offline comparison report is marked a historical migration experiment with its findings preserved intact. Co-Authored-By: Claude Opus 5 --- apps/game-api/src/experiment-export.ts | 91 +++---- apps/game-api/src/simulation-service.ts | 2 +- docs/EXPERIMENT_ARCHIVE.md | 70 +++--- docs/LIVE_SWARM_COMPARISON.md | 10 +- docs/ZERO_SWARM_COMPARISON.md | 33 ++- .../0003-session-personality-configuration.md | 1 + .../adr/0007-decoupled-world-communication.md | 1 + docs/adr/0008-formal-alliances-experiment.md | 1 + .../0009-capability-driven-model-catalog.md | 4 + docs/adr/0010-versioned-agent-behavior.md | 1 + .../adr/0015-selective-agent-communication.md | 2 + ...uid-alliances-and-diplomacy-affordances.md | 2 + docs/adr/0018-bounded-agent-goal-state.md | 1 + docs/adr/0019-bounded-agent-compact-memory.md | 1 + docs/adr/0028-zero-swarm-reflex-seam.md | 3 + docs/adr/0030-zero-swarm-world-lab.md | 4 + ...-retire-legacy-multi-agent-architecture.md | 103 ++++++++ .../experiment-archive/src/archive.test.ts | 24 +- packages/experiment-archive/src/importer.ts | 1 - packages/shared/src/index.ts | 225 +++++------------- packages/shared/src/scenario.test.ts | 44 ---- tests/e2e/world-lab.spec.ts | 2 +- 22 files changed, 306 insertions(+), 320 deletions(-) create mode 100644 docs/adr/0033-retire-legacy-multi-agent-architecture.md diff --git a/apps/game-api/src/experiment-export.ts b/apps/game-api/src/experiment-export.ts index 16643f5..58d3f41 100644 --- a/apps/game-api/src/experiment-export.ts +++ b/apps/game-api/src/experiment-export.ts @@ -28,7 +28,7 @@ import { } from '@hexzero/shared'; export interface ExperimentSource { - schemaVersion: 9 | 10 | 11; + schemaVersion: 12; id: ExperimentId; startedAt: string; providerMode: 'openrouter' | 'scripted-test'; @@ -41,11 +41,11 @@ export interface ExperimentSource { currentWorld: WorldSnapshot; modelConfiguration: ExperimentModelConfiguration; scenario: AppliedScenario; - swarmTicks?: readonly SwarmTickRecord[]; + swarmTicks: readonly SwarmTickRecord[]; simulatedPlayerEvents: readonly SimulatedPlayerEvent[]; - providerAttempts?: readonly ProviderAttemptRecord[]; - attemptRetention?: ProviderAttemptRetention; - attemptAccounting?: ExperimentAttemptAccounting; + providerAttempts: readonly ProviderAttemptRecord[]; + attemptRetention: ProviderAttemptRetention; + attemptAccounting: ExperimentAttemptAccounting; } export class ExperimentExportValidationError extends Error { @@ -105,9 +105,7 @@ function selectTickNumbers( request: ExperimentExportRequest, ): Set | 'all' { if (request.turns.mode === 'entire-retained') return 'all'; - const allTicks = (source.swarmTicks ?? []).map( - ({ tickNumber }) => tickNumber, - ); + const allTicks = source.swarmTicks.map(({ tickNumber }) => tickNumber); if (request.turns.mode === 'range') { const { fromTurn, toTurn } = request.turns; return new Set(allTicks.filter((n) => n >= fromTurn && n <= toTurn)); @@ -121,7 +119,7 @@ function requestFilteredActions( tickNumbers: Set | 'all', ): ResolvedWorldAction[] { const zeroAgentId = source.scenario.patientZeroAgentId; - const all = resolvedActionsFromTicks(source.swarmTicks ?? [], zeroAgentId); + const all = resolvedActionsFromTicks(source.swarmTicks, zeroAgentId); return all.filter( ({ tickNumber, action, actionResult }) => request.outcomes.includes( @@ -162,9 +160,9 @@ export function createExperimentExport( selectedSet, tickNumbers, ); - const retainedTicks = source.swarmTicks?.length ?? 0; - const firstRetainedTick = source.swarmTicks?.[0]?.tickNumber; - const lastRetainedTick = source.swarmTicks?.at(-1)?.tickNumber; + const retainedTicks = source.swarmTicks.length; + const firstRetainedTick = source.swarmTicks[0]?.tickNumber; + const lastRetainedTick = source.swarmTicks.at(-1)?.tickNumber; const droppedRecords = source.totalCompletedTicks - retainedTicks; const retention = { limit: source.retentionLimit, @@ -186,7 +184,7 @@ export function createExperimentExport( source.scenario.swarmArchitectureVersion === 'zero-swarm-v1' && request.agents.mode === 'all' && request.turns.mode === 'entire-retained' - ? [...structuredClone(source.swarmTicks ?? [])] + ? [...structuredClone(source.swarmTicks)] : undefined; const simulatedPlayerEventsIncluded = source.simulatedPlayerEvents.filter( (event) => @@ -226,20 +224,14 @@ export function createExperimentExport( filters: structuredClone(request), selection: { selectedAgentIds, - ...(source.schemaVersion === 10 || source.schemaVersion === 11 - ? { - matchingTickCount: - exportedSwarmTicks?.length ?? - new Set(agentFiltered.map(({ tickNumber }) => tickNumber)).size, - } - : {}), + matchingTickCount: + exportedSwarmTicks?.length ?? + new Set(agentFiltered.map(({ tickNumber }) => tickNumber)).size, ...(exportedSwarmTicks ? { matchingSwarmTickCount: exportedSwarmTicks.length } : {}), matchingControlChangeCount: controlChanges.length, - ...(source.schemaVersion === 11 - ? { matchingProviderAttemptCount: providerAttempts.length } - : {}), + matchingProviderAttemptCount: providerAttempts.length, matchingSimulatedPlayerEventCount: simulatedPlayerEventsIncluded.length, }, agents: selectedAgents, @@ -293,28 +285,19 @@ export function createExperimentExport( ? { controlChanges: structuredClone(controlChanges) } : {}), ...(exportedSwarmTicks ? { swarmTicks: exportedSwarmTicks } : {}), - ...(source.schemaVersion === 10 || source.schemaVersion === 11 - ? { - tickSummaries: summarizeTicks( - (source.swarmTicks ?? []).filter( - (tick) => - tickNumbers === 'all' || tickNumbers.has(tick.tickNumber), - ), - providerAttempts, - ), - } - : {}), - ...(source.schemaVersion === 11 - ? { - providerAttempts, - attemptRetention: { - ...source.attemptRetention!, - requestedRangeExtendsBeyondRetention: - source.attemptRetention!.droppedRecords > 0, - }, - attemptAccounting: source.attemptAccounting!, - } - : {}), + tickSummaries: summarizeTicks( + source.swarmTicks.filter( + (tick) => tickNumbers === 'all' || tickNumbers.has(tick.tickNumber), + ), + providerAttempts, + ), + providerAttempts, + attemptRetention: { + ...source.attemptRetention, + requestedRangeExtendsBeyondRetention: + source.attemptRetention.droppedRecords > 0, + }, + attemptAccounting: source.attemptAccounting, }; return experimentExportDocumentSchema.parse(document); } @@ -362,9 +345,7 @@ export function createExperimentPreview( ).length; return experimentExportPreviewSchema.parse({ experimentId: source.id, - ...(document.selection.matchingTickCount === undefined - ? {} - : { matchingTickCount: document.selection.matchingTickCount }), + matchingTickCount: document.selection.matchingTickCount, ...(document.selection.matchingSwarmTickCount === undefined ? {} : { matchingSwarmTickCount: document.selection.matchingSwarmTickCount }), @@ -373,14 +354,8 @@ export function createExperimentPreview( document.selection.matchingProviderAttemptCount ?? 0, selectedAgentCount: document.selection.selectedAgentIds.length, retention: document.retention, - knownCostCredits: - document.schemaVersion === 11 - ? ledgerKnownCost - : (document.metrics?.aggregate.knownCostCredits ?? 0), - attemptsWithUnknownCost: - document.schemaVersion === 11 - ? ledgerUnknownCost - : (document.metrics?.aggregate.attemptsWithUnknownCost ?? 0), + knownCostCredits: ledgerKnownCost, + attemptsWithUnknownCost: ledgerUnknownCost, serializedUtf8Bytes, approximateAiInputTokens: Math.ceil(serializedUtf8Bytes / 4), tokenEstimateMethod: 'ceil(UTF-8 bytes / 4)', @@ -411,7 +386,7 @@ function filterProviderAttempts( selected: Set, tickNumbers: Set | 'all', ): ProviderAttemptRecord[] { - let attempts = (source.providerAttempts ?? []).filter(({ agentId }) => + let attempts = source.providerAttempts.filter(({ agentId }) => selected.has(agentId), ); if (tickNumbers !== 'all') @@ -457,7 +432,7 @@ function rangeExtendsBeyondRetention( last?: number, ): boolean { if (request.turns.mode === 'entire-retained') - return source.totalCompletedTicks > (source.swarmTicks?.length ?? 0); + return source.totalCompletedTicks > source.swarmTicks.length; if (request.turns.mode !== 'range') return false; if (!first || !last) return true; return request.turns.fromTurn < first || request.turns.toTurn > last; diff --git a/apps/game-api/src/simulation-service.ts b/apps/game-api/src/simulation-service.ts index a8d7f1d..cd753e9 100644 --- a/apps/game-api/src/simulation-service.ts +++ b/apps/game-api/src/simulation-service.ts @@ -1654,7 +1654,7 @@ export class SimulationService { currentWorld: this.#worldSnapshot(), modelConfiguration: this.#modelConfiguration, scenario: this.#scenario, - schemaVersion: 11, + schemaVersion: 12, providerAttempts: this.#attemptAccounting.ledger(), attemptRetention: this.#attemptAccounting.retention(), attemptAccounting: this.#attemptAccounting.snapshot(), diff --git a/docs/EXPERIMENT_ARCHIVE.md b/docs/EXPERIMENT_ARCHIVE.md index 2a1f12d..a96c053 100644 --- a/docs/EXPERIMENT_ARCHIVE.md +++ b/docs/EXPERIMENT_ARCHIVE.md @@ -1,23 +1,22 @@ # Local experiment archive -The archive accepts current schema-v11 exports and legacy schema-v9/v10 exports. -Schema-v10 may add safe goal revision/result and current-goal fields. Current archive imports validate and preserve compatibility while normalized goal analytics remain deferred; the archive never restores active goal state. -Schema-v10 may also include compact memory requests, results, and current ledgers. The archive accepts these additive fields observationally but does not normalize, rank, retrieve, or restore memory. +> **Note:** this document predates the zero-swarm migration and still describes +> several removed legacy features (per-agent communications, diplomacy, +> alliances, and the schema-v9/v10/v11 compatibility described below). The +> archive now accepts only schema-v12 exports and rejects anything else; see +> ADR 0033. A full rewrite of this document is tracked separately. + +The archive accepts only schema-v12 exports and rejects any other schema version outright. Migration 2 adds nullable tick number, deterministic tick position, virtual -time, and interval columns so legacy rows remain valid. Schema-v10 lost-tick -outcomes are preserved as source outcomes; canonical summaries continue to be -derived from normalized rows and remain distinct from source metrics. Bounded -queries order tick-attributed records by tick and tick position where exposed; -the CLI still provides no arbitrary SQL surface. -Legacy scenarios with a null Patient Zero designation remain valid historical -records. They retain null attribution and Patient Zero queries return no -coordinator activity; current live setup requirements do not rewrite them. +time, and interval columns. Bounded queries order tick-attributed records by +tick and tick position where exposed; the CLI still provides no arbitrary SQL +surface. Migration 3 adds aggregate simulated-player metrics to experiments and the `simulated_player_activity` table. Every metrics-bearing safe export preserves movement/clean/block totals; Full Safe additionally preserves tick-attributed activity without deriving player behavior from agent turns. -The experiment archive is a durable, local research surface for completed or partially retained exports. It does not participate in an active simulation: the Game API's in-memory engine remains authoritative, and an archive write cannot change an accepted game outcome. It imports schema-v10 and compatible schema-v9 JSON exports; it is not crash recovery, restartable simulation state, or a scheduler. +The experiment archive is a durable, local research surface for completed or partially retained exports. It does not participate in an active simulation: the Game API's in-memory engine remains authoritative, and an archive write cannot change an accepted game outcome. It imports schema-v12 JSON exports only; it is not crash recovery, restartable simulation state, or a scheduler. ## Storage and configuration @@ -37,7 +36,7 @@ For an optional manual migration, stop all Hex Zero processes, create the `-shm` sidecars from `.agentborne`, verify the copied archive opens, and only then remove the legacy files if desired. -The schema normalizes experiments, source exports, roster/assignments, topology, turns, model attempts, communications and recipients, diplomacy attempts, alliance events, world events, configuration changes, and research notes. Stable source IDs are retained; deterministic experiment/turn/attempt IDs make imports idempotent. Source aggregate metrics remain audit evidence, while summaries derive canonical metrics from stored records. +The schema normalizes experiments, source exports, agents, map cells, swarm ticks, provider attempts, world events, configuration changes, simulated-player activity, and research notes. Stable source IDs are retained; deterministic experiment/attempt IDs make imports idempotent. Source aggregate metrics remain audit evidence, while summaries derive canonical metrics from stored records. Retention is never silently upgraded. A filtered import, missing optional observation, or truncated source remains visible as incomplete or missing data. Import validates the complete export and rejects prohibited credential/private-reasoning fields before beginning its transaction. Persistence errors roll back and surface explicitly. @@ -48,24 +47,13 @@ pnpm experiment:db import ./exports/run.json pnpm experiment:db list pnpm experiment:db summary pnpm experiment:db compare -pnpm experiment:db turns --agent --from-turn 40 --to-turn 80 -pnpm experiment:db communications --channel direct --recipient -pnpm experiment:db alliance-events --reason expired -pnpm experiment:db patient-zero --from-turn 1 --to-turn 100 +pnpm experiment:db provider-attempts pnpm experiment:db failures --reason invalid-json ``` Commands support `--format table|json|markdown`; table is the concise default. Detail queries default to 50 rows and clamp limits to 500. Filters are exact, ordering is deterministic, and arbitrary SQL is not exposed. -Summary covers scenario/roster attribution, outcomes and retries, action distribution, territory, communication channels, alliance lifecycle/rejections, Patient Zero behavior, usage, size trends, retention, and inconsistencies. Comparison reports absolute totals plus per-turn, per-agent-turn, per-active-agent, and per-Patient-Zero-turn rates. - -`directionChangesAfterCommunication` has one canonical definition: for each agent independently, count an accepted move whose direction differs from that agent's previous accepted move when the retained observation contains an inbound direct or alliance message after the previous move and no later than the current turn. Aggregate is exactly the sum of per-agent counts. Original imported values remain unchanged in source-metric audit JSON, and disagreements are reported. - -Patient Zero output distinguishes: - -- `message-to-patient-zero`: an accepted direct message without a qualifying earlier directive. -- `reply-after-directive`: an accepted direct message after a Zero directive addressed to that sender and before a later directive to that sender. -- `observable-compliance`: the next archived action when the directive contains exactly one unambiguous action word (`move`, `infect`, `capture`, or `wait`). Other directives are `indeterminate`. +Summary covers scenario/roster attribution, outcomes, action distribution, territory, provider attempt/attempt-accounting usage, size trends, retention, and inconsistencies. Comparison reports absolute totals plus per-tick and per-active-agent rates. ## Curated notes @@ -106,24 +94,26 @@ The archive stores only schema-validated safe export fields and curated notes. I MCP and embeddings are deferred because bounded local retrieval solves the immediate need without a network/tool authorization surface or derived semantic store. `ExperimentQueryService` and `ResearchNoteService` are the future extension point for a read-only MCP adapter; write/import authority remains outside that adapter. -# Schema-v11 provider attempts +# Provider attempts -Archive schema v4 stores schema-v11 `providerAttempts` independently of turns. -Use `pnpm experiment:db provider-attempts ` to inspect committed -and uncommitted provider work. Monetary values round-trip as canonical TEXT. -For v11 summaries this ledger is canonical; legacy model attempts are not added -again. The SQLite archive is for analysis and is not active runtime recovery. +Archive schema v4 stores `providerAttempts` independently. Use +`pnpm experiment:db provider-attempts ` to inspect committed and +uncommitted provider work. Monetary values round-trip as canonical TEXT. This +ledger is canonical for every current (schema-v12) export, which always +carries independent attempt accounting. The SQLite archive is for analysis +and is not active runtime recovery. Archive schema v5 adds `swarm_ticks` for safe committed zero-swarm plans, directives, physical action results, and worker choice telemetry. Full -all-agent exports carry these records separately from legacy `turns`. -Provider attempts remain in the independent v4 ledger, including attempts -from cancelled or rolled-back swarm ticks. +all-agent exports carry these records. Provider attempts remain in the +independent v4 ledger, including attempts from cancelled or rolled-back +swarm ticks. ## Zero-swarm comparisons -The archive can preserve safe schema-v11 swarm tick records and independent -provider attempts, but its `compare` command remains centered on normalized -legacy turns. Use `pnpm compare:offline` for PR F's reproducible three-variant -same-scenario fixture report. The runner does not import, write, or modify this -archive. See [Zero-swarm offline comparison](ZERO_SWARM_COMPARISON.md). +The archive preserves safe schema-v12 swarm tick records and independent +provider attempts, but its `compare` command is not the same-scenario, +per-tick swarm harness. Use `pnpm compare:offline` for the reproducible +zero-swarm-vs-deterministic-workers fixture report. The runner does not +import, write, or modify this archive. See +[Zero-swarm offline comparison](ZERO_SWARM_COMPARISON.md). diff --git a/docs/LIVE_SWARM_COMPARISON.md b/docs/LIVE_SWARM_COMPARISON.md index 2478104..79c4305 100644 --- a/docs/LIVE_SWARM_COMPARISON.md +++ b/docs/LIVE_SWARM_COMPARISON.md @@ -5,14 +5,14 @@ - `live-zero-jev`: OpenRouter Agent Zero planning plus live TypeSafe Jev worker cognition. - `live-zero-deterministic-workers`: the same OpenRouter Agent Zero planning with the server's deterministic legal-candidate selector and no TypeSafe calls. -This is a Jev ablation, not a general claim that zero-swarm is better than legacy multi-agent mode. Legacy is deliberately not included in the first live command because its provider-call pattern and cognition are different. +This is a Jev ablation, not a general claim that zero-swarm is better than the retired legacy multi-agent mode. Legacy mode was deliberately not included in the first live command because its provider-call pattern and cognition were different; it has since been removed entirely (see ADR 0033), and zero-swarm is now the only cognition architecture Hex Zero runs. ## Observe one swarm in World Lab -World Lab opens in the legacy mode by default. To watch one zero-swarm experiment: +World Lab now always runs zero-swarm; there is no cognition-mode selector. To watch one zero-swarm experiment: 1. Set `OPENROUTER_API_KEY` and `TYPESAFE_API_KEY` for the Game API process, then run `pnpm dev`. The keys are server-only. `pnpm dev:test-provider` does not make real provider calls. -2. Open World Lab at `http://localhost:3000`. In the top-right **More World Lab actions** menu, open **World setup**. Set **Cognition mode** to **Zero swarm v1 (Agent Zero + Jev)**. Enable simulated player pressure and select **Trail hunter v1** if you want capture pressure. Set bounded provider attempt and credit admission limits. +2. Open World Lab at `http://localhost:3000`. In the top-right **More World Lab actions** menu, open **World setup**. Enable simulated player pressure and select **Trail hunter v1** if you want capture pressure. Set bounded provider attempt and credit admission limits. 3. Select **Preview**, then **Apply / Create Experiment**. The header will say **Zero swarm v1 experiment**. In **Agents**, select an available model for Agent Zero. 4. Use **Single tick** to start. The **Scoreboard** tab shows Zero's strategy and worker directives; selecting a worker shows its chosen action, confidence, and probabilities. The **Swarm** activity tab shows the tick history. A provider fallback notice means the corresponding key or provider is unavailable. @@ -56,6 +56,6 @@ Do not reduce a run to final territory. Review these questions after the first b 4. How often does Zero need generative replanning, and why? 5. What are the measured OpenRouter costs, Jev token volume, clearly labeled TypeSafe estimate if configured, and unknown cost fields? 6. Inspect low-confidence choices, high-confidence bad-looking choices, repeated stalls, capture-alert response, and high trail-hunter-pressure response. Does the narrow Jev state/question keep deterministic work in code and confidence routing meaningful? -7. Is Jev worth its complexity against deterministic workers, and does zero-swarm offer more compelling gameplay than legacy mode? +7. Is Jev worth its complexity against deterministic workers? -Do not retire legacy systems from this experiment alone. +This experiment's results, together with earlier offline comparisons, informed the decision to retire the legacy multi-agent architecture (see ADR 0033). diff --git a/docs/ZERO_SWARM_COMPARISON.md b/docs/ZERO_SWARM_COMPARISON.md index df4a18e..bd92a42 100644 --- a/docs/ZERO_SWARM_COMPARISON.md +++ b/docs/ZERO_SWARM_COMPARISON.md @@ -1,7 +1,15 @@ # Zero-swarm offline comparison -PR F compares three cognition variants with the same seeded scenario and the -same optional `trail-hunter-v1` pressure profile: +> **Historical migration experiment.** This report originally compared three +> cognition variants, including `legacy-multi-agent`. That variant has since +> been removed (see ADR 0033); Hex Zero now runs only zero-swarm. The current +> comparison tooling (`swarm-comparison.ts`/`swarm-comparison-cli.ts`) compares +> Zero+Jev (`zero-swarm-jev`) against Zero+deterministic workers +> (`zero-swarm-deterministic-workers`) only. The historical findings below are +> preserved as evidence of the experiment and are not rewritten. + +PR F originally compared three cognition variants with the same seeded +scenario and the same optional `trail-hunter-v1` pressure profile: - `legacy-multi-agent` - `zero-swarm-v1` @@ -56,9 +64,10 @@ reproducibility and exposes comparable telemetry; it does not demonstrate real-model quality, production latency, or provider cost. Real-provider studies remain explicitly opted in and should archive their safe exports separately. -The existing archive comparison command is currently legacy-turn-centric. It -can retain schema-v11 swarm tick records and independent provider attempts, but -it does not replace this same-scenario, per-tick swarm harness. +The experiment archive importer now accepts only schema-v12 swarm-native +exports, which always carry swarm tick records and independent provider +attempt accounting; it does not replace this same-scenario, per-tick swarm +harness. ## Results @@ -90,8 +99,12 @@ counts remain in the JSON report. Across the three seeds, Zero made 13 reviews after its initial plans in the scripted swarm and 20 in the greedy baseline; the fake reflex policy raised no worker replan signal. -**Retirement decision:** retain `legacy-multi-agent` for now. The offline run -proves the accounting and comparison path, but it does not establish better -survival or cost for live Zero/Jev cognition. Revisit retirement after -reproducible real-provider trials, credible pressure outcomes, and authoritative -cost or clearly labeled pricing estimates. +**Retirement decision (at the time of this run):** retain `legacy-multi-agent` +for now. The offline run proves the accounting and comparison path, but it +does not establish better survival or cost for live Zero/Jev cognition. +Revisit retirement after reproducible real-provider trials, credible pressure +outcomes, and authoritative cost or clearly labeled pricing estimates. + +**Update:** the live Zero/Jev trials referenced above were subsequently run +(see `docs/LIVE_SWARM_COMPARISON.md`) and `legacy-multi-agent` was retired; see +ADR 0033. diff --git a/docs/adr/0003-session-personality-configuration.md b/docs/adr/0003-session-personality-configuration.md index a067cee..9d7c2fd 100644 --- a/docs/adr/0003-session-personality-configuration.md +++ b/docs/adr/0003-session-personality-configuration.md @@ -2,6 +2,7 @@ - Status: Accepted - Date: 2026-08-14 +- Status: Superseded by ADR 0033 ## Context diff --git a/docs/adr/0007-decoupled-world-communication.md b/docs/adr/0007-decoupled-world-communication.md index 7a675fb..9664e2f 100644 --- a/docs/adr/0007-decoupled-world-communication.md +++ b/docs/adr/0007-decoupled-world-communication.md @@ -2,6 +2,7 @@ - Status: Accepted - Date: 2026-08-14 +- Status: Superseded by ADR 0033 ## Context diff --git a/docs/adr/0008-formal-alliances-experiment.md b/docs/adr/0008-formal-alliances-experiment.md index bb8534d..1114900 100644 --- a/docs/adr/0008-formal-alliances-experiment.md +++ b/docs/adr/0008-formal-alliances-experiment.md @@ -2,6 +2,7 @@ - Status: Accepted - Date: 2026-08-14 +- Status: Superseded by ADR 0033 ## Context diff --git a/docs/adr/0009-capability-driven-model-catalog.md b/docs/adr/0009-capability-driven-model-catalog.md index bbe0a81..6e5b320 100644 --- a/docs/adr/0009-capability-driven-model-catalog.md +++ b/docs/adr/0009-capability-driven-model-catalog.md @@ -4,6 +4,10 @@ Accepted +Superseded by ADR 0033 for its personality/message observation and +decision-contract framing; model discovery and validation continue to apply +to Agent Zero. + ## Decision Hex Zero discovers models through OpenRouter's server-side models API and accepts a model only when local validation confirms text input, text output, chat completions, advertised `max_tokens`, and at least 16,384 context tokens. Requests are non-streaming plain-text chat completions. The prompt requires exactly one deliberately flat JSON object whose sentinel-bearing fields normalize into the existing local decision unions. diff --git a/docs/adr/0010-versioned-agent-behavior.md b/docs/adr/0010-versioned-agent-behavior.md index dada2c2..943fcd2 100644 --- a/docs/adr/0010-versioned-agent-behavior.md +++ b/docs/adr/0010-versioned-agent-behavior.md @@ -2,6 +2,7 @@ - Status: Accepted - Date: 2026-08-15 +- Status: Superseded by ADR 0033 ## Decision diff --git a/docs/adr/0015-selective-agent-communication.md b/docs/adr/0015-selective-agent-communication.md index 6d2c4c1..c58f686 100644 --- a/docs/adr/0015-selective-agent-communication.md +++ b/docs/adr/0015-selective-agent-communication.md @@ -4,6 +4,8 @@ Accepted for the simultaneous-tick experiment foundation. +Superseded by ADR 0033. + ## Context The v3 flat decision contract allowed optional communication but did not give diff --git a/docs/adr/0016-fluid-alliances-and-diplomacy-affordances.md b/docs/adr/0016-fluid-alliances-and-diplomacy-affordances.md index 956b135..391081d 100644 --- a/docs/adr/0016-fluid-alliances-and-diplomacy-affordances.md +++ b/docs/adr/0016-fluid-alliances-and-diplomacy-affordances.md @@ -5,6 +5,8 @@ Accepted. This supersedes ADR 0008's fixed eight-member and four-active-alliance limits while preserving its ownership, privacy, and telemetry decisions. +Superseded by ADR 0033. + ## Context The former eight-member cap had no gameplay justification, and requiring a diff --git a/docs/adr/0018-bounded-agent-goal-state.md b/docs/adr/0018-bounded-agent-goal-state.md index 15175f4..2c6cfe2 100644 --- a/docs/adr/0018-bounded-agent-goal-state.md +++ b/docs/adr/0018-bounded-agent-goal-state.md @@ -2,6 +2,7 @@ - Status: Accepted - Date: 2026-08-23 +- Status: Superseded by ADR 0033 ## Decision diff --git a/docs/adr/0019-bounded-agent-compact-memory.md b/docs/adr/0019-bounded-agent-compact-memory.md index 9b335fb..34ce7d0 100644 --- a/docs/adr/0019-bounded-agent-compact-memory.md +++ b/docs/adr/0019-bounded-agent-compact-memory.md @@ -2,6 +2,7 @@ - Status: Accepted - Date: 2026-08-23 +- Status: Superseded by ADR 0033 ## Decision diff --git a/docs/adr/0028-zero-swarm-reflex-seam.md b/docs/adr/0028-zero-swarm-reflex-seam.md index d293e5e..849d5ec 100644 --- a/docs/adr/0028-zero-swarm-reflex-seam.md +++ b/docs/adr/0028-zero-swarm-reflex-seam.md @@ -4,6 +4,9 @@ Accepted for PR A. Two-stage production tick orchestration remains PR B. +Superseded by ADR 0033 for its `legacy-multi-agent` compatibility default; the +reflex seam itself is retained as part of the sole zero-swarm architecture. + ## Decision Scenarios carry `cognitionMode`, with `legacy-multi-agent` as the compatibility diff --git a/docs/adr/0030-zero-swarm-world-lab.md b/docs/adr/0030-zero-swarm-world-lab.md index 715e1ee..5d3dd79 100644 --- a/docs/adr/0030-zero-swarm-world-lab.md +++ b/docs/adr/0030-zero-swarm-world-lab.md @@ -4,6 +4,10 @@ Accepted for PR C. +Superseded by ADR 0033 for its dual-mode (`cognitionMode`) presentation and +legacy agent/chat/diplomacy/personality/goal/memory views; World Lab now +presents only the zero-swarm view. + ## Decision World Lab selects its presentation from the scenario's explicit diff --git a/docs/adr/0033-retire-legacy-multi-agent-architecture.md b/docs/adr/0033-retire-legacy-multi-agent-architecture.md new file mode 100644 index 0000000..58f0084 --- /dev/null +++ b/docs/adr/0033-retire-legacy-multi-agent-architecture.md @@ -0,0 +1,103 @@ +# ADR 0033: Retire the legacy multi-agent architecture + +## Status + +Accepted. + +## Context + +Hex Zero shipped two cognition architectures side by side for several PRs: +`legacy-multi-agent`, in which every roster agent made its own per-turn +provider call carrying personality, strategy, chat, diplomacy, alliance, +goal, and prose-memory state; and `zero-swarm-v1`, in which one OpenRouter +generative planner (Agent Zero) issues structured directives to worker nodes +that resolve them with TypeSafe Jev reflex cognition over the deterministic +H3 world engine. + +The decisive argument is structural rather than measured. The legacy per-agent +path made one full generative provider call per active agent per tick, so +provider attempts, cost, and tick latency all scaled linearly with roster size, +and every agent's behavior was only as reliable as that call's JSON contract. +The zero-swarm path makes one generative planning call per tick regardless of +roster size and lets workers resolve directives through a bounded, +schema-validated reflex contract. That difference follows from the call +pattern itself and does not depend on a particular model or price. + +The evidence base should be read with care, because it is narrower than the +decision it supports. The offline comparison (`docs/ZERO_SWARM_COMPARISON.md`) +ran scripted decisions against a fixture that reports provider latency as zero +by construction and deliberately omits monetary cost; it recorded attempt +counts and world outcomes, not real-model quality, production latency, or +provider cost, and its own retirement recommendation at the time was to +_retain_ `legacy-multi-agent` pending real-provider evidence. The live +comparison (`docs/LIVE_SWARM_COMPARISON.md`) is a Jev ablation that measures +Zero+Jev against a Zero+deterministic-worker baseline; legacy mode was +deliberately excluded from it. No run in this repository has measured legacy +cognition against zero-swarm cognition on real providers for cost, latency, or +quality, and this ADR does not claim one has. + +Personalities, agent-to-agent communication, formal alliances +and diplomacy, individual worker goals, and prose memories were specific to +the legacy per-agent decision contract and had no equivalent in the swarm +architecture; they existed to give each independently-prompted agent a voice +and a reason to negotiate, which a single planner with directive-following +workers does not need. + +PR 1 and PR 2 of this migration already made zero-swarm the sole cognition +architecture and deleted the legacy per-agent decision contract, personalities, +agent-to-agent communication, alliances/diplomacy, worker goals, prose +memories, and the legacy experiment-import route. This ADR is the record of +that product decision, since none of the ADRs that introduced those features +were updated at the time, and it also completes the removal at the telemetry +boundary: the experiment export schema no longer carries version-conditional +branches for schema versions 9–11 (the versions that predate independent +attempt accounting and, before that, tick summaries) and now requires exactly +schema version 12, with `swarmArchitectureVersion` always `zero-swarm-v1` and +attempt accounting always present. The archive importer accepts only version +12 and rejects anything else outright rather than migrating it. + +## Decision + +Zero-swarm (Agent Zero planning over TypeSafe Jev reflex workers) is the only +cognition architecture Hex Zero runs or reads telemetry for. There is no +legacy compatibility support of any kind: reading a pre-swarm scenario, +snapshot, or experiment export requires checking out an older Git revision. +The historical `legacy-multi-agent` implementation, its ADRs, and its +telemetry schema versions remain in Git history and are not deleted or +rewritten; ADRs whose core decision was specific to the legacy architecture +are marked superseded by this one (see Consequences) rather than removed. + +The deterministic-worker cognition baseline (workers that resolve directives +without a model call) is retained alongside Jev, not because it is a second +production architecture, but because it is the ablation control that isolates +what Jev's reflex calls contribute versus the deterministic floor. Both +`swarm-comparison.ts`/`swarm-comparison-cli.ts` (offline) and +`live-swarm-comparison.ts`/`live-swarm-comparison-cli.ts` (live) compare only +these two variants. + +## Consequences + +- Pre-swarm experiment exports (schema versions 9, 10, and 11) can no longer + be read by current code; the shared schema, the archive importer, and the + game-api exporter all assume schema version 12 unconditionally. +- The `archivedAppliedScenarioSchema` scenario-import shim that translated + `cognitionMode`/`decisionContractVersion` into `historicalCognitionMode`/ + `historicalDecisionContractVersion` is removed, and so is the + `'legacy-multi-agent'` string literal it carried. The schema is retained + under its existing name because it still serves a distinct purpose: + tolerating archived scenarios written under relaxed pre-`execution-limits-v2` + and pre-`durable-influence-v2` defaults, which is unrelated to cognition + architecture and not part of this retirement. +- ADRs 0003 (session personality configuration), 0007 (decoupled world + communication), 0008 (formal alliances experiment), 0009 (capability-driven + model catalog, for its personality/message observation), 0010 (versioned + agent behavior and seeded assignment), 0015 (selective agent communication), + 0016 (fluid alliances and diplomacy affordances), 0018 (bounded agent goal + state), 0019 (bounded agent compact memory), 0028 (zero-swarm reflex seam, + for its `legacy-multi-agent` compatibility default), and 0030 (zero-swarm + World Lab presentation, for its dual-mode UI) are marked superseded by this + ADR in their existing status fields. None of them are deleted; their + decisions and rationale remain readable as history. +- Anyone who needs to inspect or replay a pre-swarm export must check out the + Git revision immediately before PR 1 of this migration (or earlier) and run + the importer from that revision; there is no migration path forward. diff --git a/packages/experiment-archive/src/archive.test.ts b/packages/experiment-archive/src/archive.test.ts index fc1b223..278a7f1 100644 --- a/packages/experiment-archive/src/archive.test.ts +++ b/packages/experiment-archive/src/archive.test.ts @@ -36,7 +36,7 @@ async function currentExport(): Promise { describe('experiment archive', () => { it('archives a current swarm export with swarm-native provenance', async () => { const document = await currentExport(); - expect(document.schemaVersion).toBe(11); + expect(document.schemaVersion).toBe(12); expect(document.experiment).toMatchObject({ swarmPlannerContractVersion: 'swarm-planner-v1', scenario: { swarmArchitectureVersion: 'zero-swarm-v1' }, @@ -106,4 +106,26 @@ describe('experiment archive', () => { raw.experiment.scenario!.swarmArchitectureVersion = 'other-architecture'; expect(experimentExportDocumentSchema.safeParse(raw).success).toBe(false); }); + + it('rejects an export document with a non-v12 schema version', async () => { + const raw = structuredClone(await currentExport()) as unknown as Record< + string, + unknown + >; + raw.schemaVersion = 11; + expect(experimentExportDocumentSchema.safeParse(raw).success).toBe(false); + const archive = new ArchiveDatabase({ path: ':memory:' }); + expect(() => + importExperimentExport( + archive, + raw as unknown as ExperimentExportDocument, + ), + ).toThrow(ExperimentImportError); + expect( + archive.database + .prepare('SELECT COUNT(*) AS count FROM experiments') + .get(), + ).toEqual({ count: 0 }); + archive.close(); + }); }); diff --git a/packages/experiment-archive/src/importer.ts b/packages/experiment-archive/src/importer.ts index 51ffcb1..6ce5b63 100644 --- a/packages/experiment-archive/src/importer.ts +++ b/packages/experiment-archive/src/importer.ts @@ -418,7 +418,6 @@ function importProviderAttempts( document: ExperimentExportDocument, report: ImportReport, ): void { - if (document.schemaVersion !== 11) return; const statement = archive.database.prepare(` INSERT OR IGNORE INTO provider_attempts( id, experiment_id, agent_id, intended_turn_number, intended_tick_number, diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 5b94600..9e837cb 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1818,45 +1818,19 @@ export const appliedScenarioSchema = worldSetupRequestObjectSchema .superRefine(validateAppliedScenario); export type AppliedScenario = z.infer; /** - * Read-only translation for historical exports. Active setup never accepts - * these retired fields; import normalizes them at the archive boundary. + * Read-only translation for historical exports that predate the current + * execution-limits and objective-attribution requirements. Active setup + * never accepts these relaxed defaults; import normalizes them at the + * archive boundary. */ -export const archivedAppliedScenarioSchema = z.preprocess( - (input) => { - if (typeof input !== 'object' || input === null || Array.isArray(input)) - return input; - const scenario = input as Record; - const { cognitionMode, decisionContractVersion, ...current } = scenario; - return { - ...current, - historicalCognitionMode: - scenario.historicalCognitionMode ?? cognitionMode, - historicalDecisionContractVersion: - scenario.historicalDecisionContractVersion ?? decisionContractVersion, - swarmArchitectureVersion: - scenario.swarmArchitectureVersion ?? 'zero-swarm-v1', - swarmPlannerContractVersion: - scenario.swarmPlannerContractVersion ?? SWARM_PLANNER_CONTRACT_VERSION, - }; - }, - worldSetupRequestObjectSchema - .extend({ - patientZeroAgentId: agentIdSchema.nullable(), - historicalCognitionMode: z - .enum(['legacy-multi-agent', 'zero-swarm-v1']) - .optional(), - historicalDecisionContractVersion: z - .string() - .trim() - .min(1) - .max(80) - .optional(), - ...appliedScenarioShape, - }) - .superRefine((scenario, context) => - validateAppliedScenario(scenario, context, true), - ), -); +export const archivedAppliedScenarioSchema = worldSetupRequestObjectSchema + .extend({ + patientZeroAgentId: agentIdSchema.nullable(), + ...appliedScenarioShape, + }) + .superRefine((scenario, context) => + validateAppliedScenario(scenario, context, true), + ); export const worldSetupPreviewResponseSchema = z.discriminatedUnion( 'feasible', @@ -2333,9 +2307,6 @@ export const experimentManifestSchema = z.preprocess( const manifest = input as Record; const resolvedVersion = SWARM_PLANNER_CONTRACT_VERSION; const scenario = manifest.scenario; - const historicalDecisionContractVersion = - manifest.historicalDecisionContractVersion ?? - manifest.decisionContractVersion; const archivedScenarioRecord = typeof scenario === 'object' && scenario !== null && @@ -2346,9 +2317,6 @@ export const experimentManifestSchema = z.preprocess( archivedScenarioRecord !== null ? { ...archivedScenarioRecord, - historicalDecisionContractVersion: - archivedScenarioRecord.historicalDecisionContractVersion ?? - historicalDecisionContractVersion, swarmPlannerContractVersion: archivedScenarioRecord.swarmPlannerContractVersion ?? resolvedVersion, @@ -2356,7 +2324,6 @@ export const experimentManifestSchema = z.preprocess( : scenario; return { ...manifest, - historicalDecisionContractVersion, swarmPlannerContractVersion: manifest.swarmPlannerContractVersion ?? resolvedVersion, ...(archivedScenario === undefined ? {} : { scenario: archivedScenario }), @@ -2368,12 +2335,6 @@ export const experimentManifestSchema = z.preprocess( startedAt: z.iso.datetime(), generatedAt: z.iso.datetime().optional(), providerMode: providerModeSchema, - historicalDecisionContractVersion: z - .string() - .trim() - .min(1) - .max(80) - .optional(), swarmPlannerContractVersion: swarmPlannerContractVersionSchema, modelConfiguration: experimentModelConfigurationSchema.optional(), scenario: archivedAppliedScenarioSchema.optional(), @@ -2665,7 +2626,7 @@ export type ExperimentExportWorldState = z.infer< const experimentExportDocumentObjectSchema = z .object({ - schemaVersion: z.union([z.literal(9), z.literal(10), z.literal(11)]), + schemaVersion: z.literal(12), generatedAt: z.iso.datetime(), experiment: experimentManifestSchema, retention: experimentRetentionSchema, @@ -2742,80 +2703,64 @@ const experimentExportDocumentObjectSchema = z message: 'Zero-swarm tick count must match exported swarm telemetry.', }); } - if (document.schemaVersion === 9 && document.tickSummaries !== undefined) + if ( + document.providerAttempts === undefined || + document.attemptRetention === undefined || + document.attemptAccounting === undefined || + document.selection.matchingProviderAttemptCount === undefined + ) context.addIssue({ code: 'custom', - message: 'Schema-v9 exports cannot contain tick summaries.', + message: 'Exports require independent attempt accounting.', }); - if (document.schemaVersion === 11) { - if ( - document.providerAttempts === undefined || - document.attemptRetention === undefined || - document.attemptAccounting === undefined || - document.selection.matchingProviderAttemptCount === undefined - ) - context.addIssue({ - code: 'custom', - message: 'Schema-v11 exports require independent attempt accounting.', - }); - const attempts = document.providerAttempts ?? []; - const selectedIds = new Set(document.selection.selectedAgentIds); - const exportedIds = new Set(document.agents.map(({ id }) => id)); - if (new Set(attempts.map(({ id }) => id)).size !== attempts.length) - context.addIssue({ - code: 'custom', - path: ['providerAttempts'], - message: 'Provider-attempt IDs must be unique.', - }); - if ( - attempts.some( - ({ agentId }) => - !selectedIds.has(agentId) || !exportedIds.has(agentId), - ) - ) - context.addIssue({ - code: 'custom', - path: ['providerAttempts'], - message: 'Provider attempts must belong to selected exported agents.', - }); - if (document.selection.matchingProviderAttemptCount !== attempts.length) - context.addIssue({ - code: 'custom', - path: ['selection', 'matchingProviderAttemptCount'], - message: 'Provider-attempt selection count must match the export.', - }); - const retention = document.attemptRetention; - const accounting = document.attemptAccounting; - if ( - retention && - (retention.totalStartedAttempts !== - retention.retainedAttempts + retention.droppedRecords || - retention.retainedAttempts > retention.limit || - retention.complete !== (retention.droppedRecords === 0)) - ) - context.addIssue({ - code: 'custom', - path: ['attemptRetention'], - message: 'Provider-attempt retention totals must be consistent.', - }); - if ( - retention && - accounting && - retention.totalStartedAttempts !== accounting.attemptsStarted + const attempts = document.providerAttempts ?? []; + const selectedIds = new Set(document.selection.selectedAgentIds); + const exportedIds = new Set(document.agents.map(({ id }) => id)); + if (new Set(attempts.map(({ id }) => id)).size !== attempts.length) + context.addIssue({ + code: 'custom', + path: ['providerAttempts'], + message: 'Provider-attempt IDs must be unique.', + }); + if ( + attempts.some( + ({ agentId }) => !selectedIds.has(agentId) || !exportedIds.has(agentId), ) - context.addIssue({ - code: 'custom', - path: ['attemptRetention', 'totalStartedAttempts'], - message: 'Attempt retention and accounting totals must agree.', - }); - } else if ( - document.providerAttempts !== undefined || - document.attemptRetention !== undefined || - document.attemptAccounting !== undefined ) context.addIssue({ code: 'custom', - message: 'Legacy exports cannot claim schema-v11 attempt accounting.', + path: ['providerAttempts'], + message: 'Provider attempts must belong to selected exported agents.', + }); + if (document.selection.matchingProviderAttemptCount !== attempts.length) + context.addIssue({ + code: 'custom', + path: ['selection', 'matchingProviderAttemptCount'], + message: 'Provider-attempt selection count must match the export.', + }); + const retention = document.attemptRetention; + const accounting = document.attemptAccounting; + if ( + retention && + (retention.totalStartedAttempts !== + retention.retainedAttempts + retention.droppedRecords || + retention.retainedAttempts > retention.limit || + retention.complete !== (retention.droppedRecords === 0)) + ) + context.addIssue({ + code: 'custom', + path: ['attemptRetention'], + message: 'Provider-attempt retention totals must be consistent.', + }); + if ( + retention && + accounting && + retention.totalStartedAttempts !== accounting.attemptsStarted + ) + context.addIssue({ + code: 'custom', + path: ['attemptRetention', 'totalStartedAttempts'], + message: 'Attempt retention and accounting totals must agree.', }); const level = document.filters.level; const custom = level === 'custom' ? document.filters.custom : undefined; @@ -2858,46 +2803,8 @@ const experimentExportDocumentObjectSchema = z message: 'Control-change inclusion does not match the export level.', }); }); -export const experimentExportDocumentSchema = z.preprocess((input) => { - if (typeof input !== 'object' || input === null || Array.isArray(input)) - return input; - const document = input as Record; - const experiment = - typeof document.experiment === 'object' && document.experiment !== null - ? (document.experiment as Record) - : undefined; - const scenario = - typeof experiment?.scenario === 'object' && experiment.scenario !== null - ? (experiment.scenario as Record) - : undefined; - const simulatedPlayer = - typeof scenario?.simulatedPlayer === 'object' && - scenario.simulatedPlayer !== null - ? (scenario.simulatedPlayer as Record) - : undefined; - const capabilities = - typeof scenario?.capabilities === 'object' && scenario.capabilities !== null - ? (scenario.capabilities as Record) - : undefined; - const playerPressureEnabled = - simulatedPlayer?.enabled === true || - capabilities?.simulatedPlayerPressure === true; - if ( - (document.schemaVersion === 9 || document.schemaVersion === 10) && - !playerPressureEnabled && - document.metrics !== undefined && - document.simulatedPlayerMetrics === undefined - ) - return { - ...document, - simulatedPlayerMetrics: { - movements: 0, - cellsDisinfected: 0, - blockedDisinfections: 0, - }, - }; - return input; -}, experimentExportDocumentObjectSchema); +export const experimentExportDocumentSchema = + experimentExportDocumentObjectSchema; export type ExperimentExportDocument = z.infer< typeof experimentExportDocumentSchema >; diff --git a/packages/shared/src/scenario.test.ts b/packages/shared/src/scenario.test.ts index bb2da33..6ba47fd 100644 --- a/packages/shared/src/scenario.test.ts +++ b/packages/shared/src/scenario.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'; import { appliedScenarioSchema, archivedAppliedScenarioSchema, - experimentManifestSchema, worldSetupPreviewResponseSchema, worldSetupRequestSchema, WORLD_SCENARIO_LIMITS, @@ -278,49 +277,6 @@ describe('scenario contracts', () => { ).toBe(SWARM_PLANNER_CONTRACT_VERSION); }); - it('normalizes retired cognition and decision attribution only for historical scenarios', () => { - const current = appliedScenarioSchema.parse({ - ...worldSetupRequestSchema.parse(request), - exactCellCount: 1, - areaSquareKilometers: 0.1, - startingCells: ['8928308280fffff'], - setupWarnings: [], - }); - const historical = { ...current } as Record; - delete historical.swarmArchitectureVersion; - delete historical.swarmPlannerContractVersion; - historical.cognitionMode = 'legacy-multi-agent'; - historical.decisionContractVersion = 'text-flat-json-v8'; - expect(archivedAppliedScenarioSchema.parse(historical)).toMatchObject({ - swarmArchitectureVersion: 'zero-swarm-v1', - swarmPlannerContractVersion: SWARM_PLANNER_CONTRACT_VERSION, - historicalCognitionMode: 'legacy-multi-agent', - historicalDecisionContractVersion: 'text-flat-json-v8', - }); - expect(worldSetupRequestSchema.safeParse(historical).success).toBe(false); - }); - - it('carries top-level historical decision attribution into the archived scenario', () => { - const current = appliedScenarioSchema.parse({ - ...worldSetupRequestSchema.parse(request), - exactCellCount: 1, - areaSquareKilometers: 0.1, - startingCells: ['8928308280fffff'], - setupWarnings: [], - }); - const parsed = experimentManifestSchema.parse({ - id: '128f3f38-6b7d-4db7-9e95-751b4ce2681e', - startedAt: '2026-08-13T12:00:00.000Z', - providerMode: 'openrouter', - decisionContractVersion: 'text-flat-json-v8', - scenario: current, - }); - expect(parsed.historicalDecisionContractVersion).toBe('text-flat-json-v8'); - expect(parsed.scenario?.historicalDecisionContractVersion).toBe( - 'text-flat-json-v8', - ); - }); - it('preserves null only for strict archived applied scenarios with common refinements', () => { const archivedRoster = [ ...request.roster, diff --git a/tests/e2e/world-lab.spec.ts b/tests/e2e/world-lab.spec.ts index c5b7f61..0e82756 100644 --- a/tests/e2e/world-lab.spec.ts +++ b/tests/e2e/world-lab.spec.ts @@ -96,7 +96,7 @@ test('runs a deterministic swarm tick and exports safe telemetry', async ({ const exported = experimentExportDocumentSchema.parse( JSON.parse(await readFile(downloadedPath!, 'utf8')), ); - expect(exported.schemaVersion).toBe(11); + expect(exported.schemaVersion).toBe(12); expect(exported.experiment.scenario?.swarmArchitectureVersion).toBe( 'zero-swarm-v1', );