From 38abf72bfeb967ac792f3ec53dfcad5becb88354 Mon Sep 17 00:00:00 2001 From: Christopher Nelson Date: Mon, 21 Sep 2026 08:25:05 -0400 Subject: [PATCH] refactor: make zero swarm the sole cognition architecture --- apps/game-api/src/app.test.ts | 1433 +---- apps/game-api/src/app.ts | 250 +- apps/game-api/src/experiment-export.ts | 6 +- .../src/live-swarm-comparison.test.ts | 2 - apps/game-api/src/live-swarm-comparison.ts | 18 +- apps/game-api/src/reflex-execution.test.ts | 17 +- apps/game-api/src/server.ts | 5 +- .../src/simulation-service.swarm.test.ts | 43 +- apps/game-api/src/simulation-service.test.ts | 5036 +---------------- apps/game-api/src/simulation-service.ts | 1972 +------ apps/game-api/src/swarm-comparison.test.ts | 23 +- apps/game-api/src/swarm-comparison.ts | 39 +- .../src/swarm-diagnostics-cli.test.ts | 2 +- apps/game-api/src/swarm-diagnostics-cli.ts | 2 +- .../src/components/world-lab.test.tsx | 4491 +-------------- apps/world-lab/src/components/world-lab.tsx | 256 +- package.json | 1 - packages/agent-runtime/package.json | 1 - packages/agent-runtime/src/index.test.ts | 1703 ------ packages/agent-runtime/src/index.ts | 1279 +---- .../agent-runtime/src/provider-environment.ts | 5 +- .../agent-runtime/src/real-provider-smoke.ts | 61 - packages/agent-runtime/src/reflex-provider.ts | 67 +- .../agent-runtime/src/smoke-arguments.test.ts | 76 - packages/agent-runtime/src/smoke-arguments.ts | 25 - .../agent-runtime/src/smoke-observation.ts | 125 - .../agent-runtime/src/swarm-planner.test.ts | 27 +- packages/agent-runtime/src/swarm-planner.ts | 69 +- .../agent-runtime/src/tick-dispatcher.test.ts | 410 -- packages/agent-runtime/src/tick-dispatcher.ts | 420 -- .../src/typesafe-jev-reflex-provider.test.ts | 11 + .../experiment-archive/src/archive.test.ts | 890 +-- packages/experiment-archive/src/importer.ts | 2 +- .../experiment-archive/src/query-service.ts | 2 +- packages/shared/src/index.test.ts | 82 +- packages/shared/src/index.ts | 248 +- packages/shared/src/scenario.test.ts | 55 +- packages/world-engine/src/index.ts | 6 +- packages/world-engine/src/scenario.test.ts | 6 +- tests/e2e/world-lab.spec.ts | 456 +- 40 files changed, 986 insertions(+), 18636 deletions(-) delete mode 100644 packages/agent-runtime/src/index.test.ts delete mode 100644 packages/agent-runtime/src/real-provider-smoke.ts delete mode 100644 packages/agent-runtime/src/smoke-arguments.test.ts delete mode 100644 packages/agent-runtime/src/smoke-arguments.ts delete mode 100644 packages/agent-runtime/src/smoke-observation.ts delete mode 100644 packages/agent-runtime/src/tick-dispatcher.test.ts delete mode 100644 packages/agent-runtime/src/tick-dispatcher.ts diff --git a/apps/game-api/src/app.test.ts b/apps/game-api/src/app.test.ts index 47cb189..6de1a48 100644 --- a/apps/game-api/src/app.test.ts +++ b/apps/game-api/src/app.test.ts @@ -1,1431 +1,76 @@ -import { describe, expect, it, vi } from 'vitest'; -import { createHash } from 'node:crypto'; +import { describe, expect, it } from 'vitest'; import { - AgentProviderError, - BrowserTestAgentProvider, - OpenRouterAgentProvider, - ScriptedAgentProvider, - type AgentProvider, - type ProviderDecision, + DeterministicReflexProvider, + DeterministicSwarmPlanner, } from '@hexzero/agent-runtime'; import { - ArchivePersistenceError, - ExperimentImportError, -} from '@hexzero/experiment-archive'; -import { - cancelledTurnResponseSchema, - archiveExperimentExportResponseSchema, - apiErrorSchema, defaultWorldSetupResponseSchema, - h3CellSchema, - experimentExportPreviewSchema, - experimentExportResponseSchema, healthResponseSchema, - modelCatalogResponseSchema, - resetSimulationResponseSchema, - restoreDefaultPersonalitiesResponseSchema, simulationSnapshotSchema, - singleTurnResponseSchema, singleTickResponseSchema, - updateAgentPersonalityResponseSchema, - updateExperimentModelsResponseSchema, - verifyModelResponseSchema, - type ExperimentExportDocument, } from '@hexzero/shared'; -import { - createApp, - providerFromEnvironment, - resolveProviderModeFromEnvironment, -} from './app'; - -describe('provider environment compatibility', () => { - it('uses the canonical provider variable without a warning', () => { - const warn = vi.fn(); - expect( - providerFromEnvironment({ HEXZERO_PROVIDER: 'scripted' }, warn), - ).toBeInstanceOf(BrowserTestAgentProvider); - expect(warn).not.toHaveBeenCalled(); - }); +import { createApp, swarmProvidersFromEnvironment } from './app'; - it('prefers the canonical provider variable over the legacy alias', () => { - const warn = vi.fn(); - expect( - resolveProviderModeFromEnvironment( - { - HEXZERO_PROVIDER: 'openrouter', - AGENTBORNE_PROVIDER: 'scripted', - }, - warn, - ), - ).toBe('openrouter'); - expect(warn).not.toHaveBeenCalled(); - }); - - it('supports the legacy provider alias with a value-free warning', () => { - const warn = vi.fn(); - expect( - providerFromEnvironment({ AGENTBORNE_PROVIDER: 'scripted' }, warn), - ).toBeInstanceOf(BrowserTestAgentProvider); - expect(warn).toHaveBeenCalledWith( - 'AGENTBORNE_PROVIDER is deprecated; use HEXZERO_PROVIDER. Continuing with the legacy setting.', - ); - }); -}); - -describe('game API simulation boundary', () => { - it('returns a specific 409 before dispatch when a full tick cannot be reserved', async () => { - let calls = 0; - const app = createApp({ - provider: { - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(): Promise { - calls += 1; - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait.' }, - metadata: { - provider: 'scripted-test', - model: 'deterministic-script', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }, +describe('game API swarm boundary', () => { + it('uses repeatable swarm-native providers for scripted environments', () => { + const providers = swarmProvidersFromEnvironment({ + HEXZERO_PROVIDER: 'scripted', }); - const defaults = defaultWorldSetupResponseSchema.parse( - await ( - await app.request('/api/simulation/experiment/setup/default') - ).json(), - ).request; - await app.request('/api/simulation/experiment/setup', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ - ...defaults, - modelConfiguration: { - ...defaults.modelConfiguration, - globalModelId: 'deterministic-script', - }, - executionLimits: { - version: 'execution-limits-v2', - providerAttemptLimit: 1, - creditLimit: null, - reservationCreditsPerAttempt: '0.01', - }, - }), - }); - const response = await app.request('/api/simulation/tick', { - method: 'POST', - }); - expect(response.status).toBe(409); - expect(await response.json()).toEqual({ - error: { - code: 'experiment_budget_exhausted', - message: - 'The experiment does not have enough provider-attempt or credit-admission capacity for a complete tick.', - }, - }); - expect(calls).toBe(0); - expect( - simulationSnapshotSchema.parse( - await (await app.request('/api/simulation')).json(), - ), - ).toMatchObject({ - tickNumber: 0, - status: 'budget-exhausted', - experiment: { attemptAccounting: { attemptsStarted: 0 } }, - }); - }); - - it('requires a known Patient Zero through the public setup boundary', async () => { - const app = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - }); - const defaultsResponse = await app.request( - '/api/simulation/experiment/setup/default', + expect(providers.swarmPlanner).toBeInstanceOf(DeterministicSwarmPlanner); + expect(providers.reflexProvider).toBeInstanceOf( + DeterministicReflexProvider, ); - expect(defaultsResponse.status).toBe(200); - const defaults = defaultWorldSetupResponseSchema.parse( - await defaultsResponse.json(), - ).request; - expect(defaults.patientZeroAgentId).toBe(defaults.roster[0]!.id); - - const invalidRequests: unknown[] = [ - { ...defaults, patientZeroAgentId: undefined }, - { ...defaults, patientZeroAgentId: null }, - { - ...defaults, - patientZeroAgentId: '00000000-0000-4000-8000-000000000999', - }, - ]; - for (const request of invalidRequests) { - const response = await app.request('/api/simulation/experiment/setup', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify(request), - }); - expect(response.status).toBe(400); - expect(apiErrorSchema.parse(await response.json())).toMatchObject({ - error: { code: 'invalid_request' }, - }); - } }); - it('coalesces repeated delivery of the same tick mutation ID', async () => { - let calls = 0; + it('serves health and the swarm setup contract', async () => { const app = createApp({ - provider: { - mode: 'scripted-test', - configured: true, - async decide(): Promise { - calls += 1; - await Promise.resolve(); - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait.' }, - metadata: { - provider: 'scripted-test', - model: 'tick-idempotency', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }, + swarmPlanner: new DeterministicSwarmPlanner(), + reflexProvider: new DeterministicReflexProvider(), }); - const request = () => - app.request('/api/simulation/tick?mutationId=tick_same_001', { - method: 'POST', - }); - const [first, second] = await Promise.all([request(), request()]); - expect(first.status).toBe(200); - expect(second.status).toBe(200); - expect(calls).toBe(8); expect( - simulationSnapshotSchema.parse( - await (await app.request('/api/simulation')).json(), - ).tickNumber, - ).toBe(1); - }); - - it('replays the original complete tick envelope after later ticks commit', async () => { - let calls = 0; - const app = createApp({ - provider: { - mode: 'scripted-test', - configured: true, - async decide(): Promise { - calls += 1; - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait.' }, - metadata: { - provider: 'scripted-test', - model: 'tick-replay', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }, - }); - const first = singleTickResponseSchema.parse( + healthResponseSchema.parse(await (await app.request('/health')).json()), + ).toMatchObject({ status: 'ok' }); + const setup = defaultWorldSetupResponseSchema.parse( await ( - await app.request('/api/simulation/tick?mutationId=tick_replay_A', { - method: 'POST', - }) - ).json(), - ); - const second = singleTickResponseSchema.parse( - await ( - await app.request('/api/simulation/tick?mutationId=tick_replay_B', { - method: 'POST', - }) - ).json(), - ); - const replay = singleTickResponseSchema.parse( - await ( - await app.request('/api/simulation/tick?mutationId=tick_replay_A', { - method: 'POST', - }) + await app.request('/api/simulation/experiment/setup/default') ).json(), ); - expect(first).toMatchObject({ tickNumber: 1, snapshot: { tickNumber: 1 } }); - expect(second).toMatchObject({ - tickNumber: 2, - snapshot: { tickNumber: 2 }, - }); - expect(replay).toEqual(first); - expect(calls).toBe(16); - expect( - simulationSnapshotSchema.parse( - await (await app.request('/api/simulation')).json(), - ).tickNumber, - ).toBe(2); + expect(setup.request.swarmArchitectureVersion).toBe('zero-swarm-v1'); }); - it('returns one complete simultaneous tick group', async () => { + it('commits an atomic swarm tick through the public endpoint', async () => { const app = createApp({ - provider: new ScriptedAgentProvider( - Array.from({ length: 8 }, () => ({ - worldAction: { type: 'wait' as const }, - summary: 'Wait.', - })), - ), + swarmPlanner: new DeterministicSwarmPlanner(), + reflexProvider: new DeterministicReflexProvider(), }); const response = await app.request('/api/simulation/tick', { method: 'POST', }); expect(response.status).toBe(200); - expect(singleTickResponseSchema.parse(await response.json())).toMatchObject( - { - tickNumber: 1, - snapshot: { tickNumber: 1, turnNumber: 8 }, - }, - ); - }); - - it('coalesces repeated delivery of the same turn mutation ID', async () => { - let calls = 0; - const app = createApp({ - provider: { - mode: 'scripted-test', - configured: true, - async decide(): Promise { - calls += 1; - await Promise.resolve(); - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait once.' }, - metadata: { - provider: 'scripted-test', - model: 'mutation-test', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }, - }); - const request = () => - app.request('/api/simulation/turn', { - method: 'POST', - headers: { 'X-Hexzero-Mutation-Id': 'mutation_same_001' }, - }); - const [first, duplicate] = await Promise.all([request(), request()]); - expect(first.status).toBe(200); - expect(duplicate.status).toBe(200); - expect(calls).toBe(1); + const body: unknown = await response.json(); + expect(singleTickResponseSchema.parse(body).tickNumber).toBe(1); + expect(body).not.toHaveProperty('records'); expect( simulationSnapshotSchema.parse( await (await app.request('/api/simulation')).json(), - ).turnNumber, - ).toBe(1); - }); - - it('reports health and serves a schema-valid snapshot', async () => { - const app = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - }); - const health = await app.request('/health'); - expect(health.status).toBe(200); - expect(healthResponseSchema.parse(await health.json()).status).toBe('ok'); - const response = await app.request('/api/simulation'); - expect(response.status).toBe(200); - const payload = simulationSnapshotSchema.parse(await response.json()); - expect(payload.world.agents).toHaveLength(8); - expect( - payload.world.hexes.every( - (hex) => hex.state === 'open' && hex.controllerAgentId === null, - ), - ).toBe(true); - expect(payload.experiment.currentTerritory).toHaveLength(8); - }); - - it('returns a non-tick-consuming cancellation response from the tick route', async () => { - let requestStarted!: () => void; - const started = new Promise((resolve) => { - requestStarted = resolve; - }); - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'cancel-test', - configured: true, - async decide(_observation, _model, options) { - requestStarted(); - await new Promise((resolve) => { - options?.signal?.addEventListener('abort', () => resolve(), { - once: true, - }); - }); - throw new AgentProviderError({ - code: 'cancelled', - message: 'The model request was cancelled by the operator.', - retryable: false, - }); - }, - }; - const app = createApp({ provider }); - const pendingTurn = app.request('/api/simulation/tick', { method: 'POST' }); - await started; - expect( - (await app.request('/api/simulation/tick/cancel', { method: 'POST' })) - .status, - ).toBe(200); - const response = await pendingTurn; - expect(response.status).toBe(200); - expect( - cancelledTurnResponseSchema.parse(await response.json()), - ).toMatchObject({ - cancelled: true, - snapshot: { - status: 'paused', - turnNumber: 0, - tickNumber: 0, - turns: [], - experiment: { totalCompletedTurns: 0 }, - }, - }); - }); - - it('serves and refreshes sanitized catalogs without exposing the server key', async () => { - const secret = 'server-only-secret-marker'; - const forced: boolean[] = []; - const catalogResponse = modelCatalogResponseSchema.parse({ - models: [ - { - id: 'example/compatible-model', - name: 'Compatible model', - author: 'example', - contextLength: 32_768, - inputPricePerToken: '0.000001', - outputPricePerToken: '0.000002', - supportedParameters: ['max_tokens'], - isFree: false, - }, - ], - filteredOutCount: 4, - stale: false, - requirements: { - input: 'text', - output: 'text', - endpoint: 'chat-completions', - requiredParameters: ['max_tokens'], - minimumContextLength: 16_384, - streaming: false, - }, - }); - const app = createApp({ - provider: new OpenRouterAgentProvider({ apiKey: secret }), - catalog: { - async getCatalog(force = false) { - forced.push(force); - return catalogResponse; - }, - }, - }); - const catalog = await (await app.request('/api/simulation/models')).json(); - expect(modelCatalogResponseSchema.parse(catalog).models).toHaveLength(1); - const assigned = updateExperimentModelsResponseSchema.parse( - await ( - await app.request('/api/simulation/experiment/models', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - globalModelId: 'example/compatible-model', - overrides: [], - }), - }) - ).json(), - ); - expect( - assigned.snapshot.resolvedModels.every(({ available }) => available), - ).toBe(true); - const refreshed = await ( - await app.request('/api/simulation/models/refresh', { method: 'POST' }) - ).json(); - expect(forced).toEqual([false, false, true]); - expect(JSON.stringify({ catalog, assigned, refreshed })).not.toContain( - secret, - ); - }); - - it('caches an explicit model probe without advancing the world', async () => { - let calls = 0; - const profiles: string[] = []; - const provider: AgentProvider = { - mode: 'openrouter', - configured: true, - async decide(_observation, model, options) { - calls += 1; - expect(options?.reasoningProfile).toBeDefined(); - profiles.push(options?.reasoningProfile ?? 'provider-default'); - return { - decision: { worldAction: { type: 'wait' }, summary: 'Probe.' }, - metadata: { provider: 'openrouter', model, latencyMs: 1 }, - }; - }, - }; - const catalogResponse = modelCatalogResponseSchema.parse({ - models: [ - { - id: 'example/probe-model', - name: 'Probe model', - author: 'example', - contextLength: 16_384, - inputPricePerToken: '0', - outputPricePerToken: '0', - supportedParameters: ['max_tokens'], - isFree: true, - reasoning: { - mandatory: false, - supportedEfforts: ['low', 'medium', 'xhigh'], - }, - }, - ], - filteredOutCount: 0, - stale: false, - requirements: { - input: 'text', - output: 'text', - endpoint: 'chat-completions', - requiredParameters: ['max_tokens'], - minimumContextLength: 16_384, - streaming: false, - }, - }); - const app = createApp({ - provider, - catalog: { - async getCatalog() { - return catalogResponse; - }, - }, - }); - const before = simulationSnapshotSchema.parse( - await (await app.request('/api/simulation')).json(), - ); - for (let index = 0; index < 2; index += 1) { - const response = await app.request('/api/simulation/models/verify', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - modelId: 'example/probe-model', - reasoningProfile: 'low', - }), - }); - expect( - verifyModelResponseSchema.parse(await response.json()).verification - .status, - ).toBe('verified'); - } - const differentProfile = await app.request( - '/api/simulation/models/verify', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - modelId: 'example/probe-model', - reasoningProfile: 'medium', - }), - }, - ); - expect( - verifyModelResponseSchema.parse(await differentProfile.json()) - .verification.reasoningProfile, - ).toBe('medium'); - const after = simulationSnapshotSchema.parse( - await (await app.request('/api/simulation')).json(), - ); - expect(calls).toBe(2); - expect(profiles).toEqual(['low', 'medium']); - expect(after.world).toEqual(before.world); - expect(after.turnNumber).toBe(0); - }); - - it('returns accepted and rejected single-turn records with valid response shapes', async () => { - const acceptedApp = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'infect' }, summary: 'Infect.' }, - ]), - }); - const accepted = singleTurnResponseSchema.parse( - await ( - await acceptedApp.request('/api/simulation/turn', { method: 'POST' }) - ).json(), - ); - expect(accepted.turn.outcome).toBe('accepted'); - if (accepted.turn.outcome !== 'accepted') - throw new Error('Expected accepted infection fixture.'); - expect(accepted.turn).toMatchObject({ - worldActionResult: { - event: { - type: 'hex-infected', - controllerAgentId: accepted.turn.agentId, - }, - }, - }); - expect( - accepted.snapshot.world.hexes.find( - ({ cell }) => cell === accepted.turn.observation.currentCell.cell, - ), - ).toMatchObject({ - state: 'infected', - controllerAgentId: accepted.turn.agentId, - }); - - const rejectedApp = createApp({ - provider: new ScriptedAgentProvider([ - { - worldAction: { - type: 'move', - targetCell: h3CellSchema.parse('8928308280fffff'), - }, - summary: 'Move far away.', - }, - ]), - }); - const rejected = singleTurnResponseSchema.parse( - await ( - await rejectedApp.request('/api/simulation/turn', { method: 'POST' }) - ).json(), - ); - expect(rejected.turn.outcome).toBe('rejected'); - }); - - it('returns independently typed world-action and communication responses', async () => { - const bootstrap = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'placeholder' }, - ]), - }); - const snapshot = simulationSnapshotSchema.parse( - await (await bootstrap.request('/api/simulation')).json(), - ); - const [sender, recipient] = snapshot.world.agents; - const acceptedApp = createApp({ - provider: new ScriptedAgentProvider([ - { - worldAction: { type: 'wait' }, - communication: { - channel: 'direct', - recipientId: recipient!.id, - message: 'Nearby API message.', - }, - summary: 'Send.', - }, - ]), - }); - const accepted = singleTurnResponseSchema.parse( - await ( - await acceptedApp.request('/api/simulation/turn', { method: 'POST' }) - ).json(), - ); - expect(accepted.turn).toMatchObject({ - outcome: 'accepted', - communicationResult: { - accepted: true, - event: { - type: 'direct-message-sent', - agentId: sender!.id, - recipientId: recipient!.id, - message: 'Nearby API message.', - }, - }, - }); - - const rejectedApp = createApp({ - provider: new ScriptedAgentProvider([ - { - worldAction: { type: 'wait' }, - communication: { - channel: 'direct', - recipientId: sender!.id, - message: 'Self message.', - }, - summary: 'Try.', - }, - ]), - }); - const rejected = singleTurnResponseSchema.parse( - await ( - await rejectedApp.request('/api/simulation/turn', { method: 'POST' }) - ).json(), - ); - expect(rejected.turn).toMatchObject({ - outcome: 'accepted', - communicationResult: { accepted: false, reason: 'self-message' }, - }); - expect(rejected.snapshot.world.events).toHaveLength(1); - }); - - it('returns provider failures and missing configuration safely', async () => { - const failureProvider: AgentProvider = { - mode: 'scripted-test', - model: 'failure-test', - configured: true, - async decide() { - throw new AgentProviderError( - { - code: 'network', - message: 'The model provider could not be reached.', - retryable: true, - }, - undefined, - { - httpStatus: 400, - providerMessage: 'internal-diagnostic-marker', - model: 'example/compatible-model', - }, - ); - }, - }; - const failed = singleTurnResponseSchema.parse( - await ( - await createApp({ provider: failureProvider }).request( - '/api/simulation/turn', - { method: 'POST' }, - ) - ).json(), - ); - expect(failed.turn.outcome).toBe('provider-error'); - expect(JSON.stringify(failed)).not.toContain('internal-diagnostic-marker'); - - const missing = createApp({ provider: new OpenRouterAgentProvider() }); - const snapshot = simulationSnapshotSchema.parse( - await (await missing.request('/api/simulation')).json(), - ); - expect(snapshot).toMatchObject({ - status: 'configuration-error', - providerConfigured: false, - }); - }); - - it('exposes explicit retry and skip operations for one pending logical turn', async () => { - let calls = 0; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'manual-control-test', - configured: true, - async decide() { - calls += 1; - throw new AgentProviderError({ - code: 'timeout', - message: 'Timed out.', - retryable: true, - }); - }, - }; - const app = createApp({ provider }); - const failed = singleTurnResponseSchema.parse( - await ( - await app.request('/api/simulation/turn', { method: 'POST' }) - ).json(), - ); - expect(failed.snapshot).toMatchObject({ - turnNumber: 0, - pendingFailedTurn: { turnNumber: 1 }, - }); - const retried = singleTurnResponseSchema.parse( - await ( - await app.request('/api/simulation/turn/retry', { method: 'POST' }) - ).json(), - ); - expect(calls).toBe(3); - expect(retried.snapshot.pendingFailedTurn?.attempts).toHaveLength(3); - const skipped = singleTurnResponseSchema.parse( - await ( - await app.request('/api/simulation/turn/skip', { method: 'POST' }) - ).json(), - ); - expect(skipped.turn).toMatchObject({ - turnNumber: 1, - outcome: 'operator-skipped', - }); - expect(skipped.snapshot).toMatchObject({ - turnNumber: 1, - status: 'paused', - pendingFailedTurn: null, - }); - }); - - it('returns recoverable post-provider validation failures without advancing', async () => { - let calls = 0; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'invalid-metadata-test', - configured: true, - async decide(): Promise { - calls += 1; - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait.' }, - metadata: { - provider: 'scripted-test', - model: calls === 1 ? '' : 'invalid-metadata-test', - latencyMs: 0, - }, - } as ProviderDecision; - }, - }; - const app = createApp({ provider }); - const initial = simulationSnapshotSchema.parse( - await (await app.request('/api/simulation')).json(), - ); - - const failed = await app.request('/api/simulation/turn', { - method: 'POST', - }); - expect(failed.status).toBe(200); - const failedTurn = singleTurnResponseSchema.parse(await failed.json()); - expect(failedTurn.turn).toMatchObject({ - outcome: 'provider-error', - failure: { code: 'simulation-validation' }, - }); - - const afterFailure = simulationSnapshotSchema.parse( - await (await app.request('/api/simulation')).json(), - ); - expect(afterFailure).toMatchObject({ - turnNumber: 0, - turns: [], - nextAgentId: initial.nextAgentId, - activeAgentId: null, - status: 'provider-error', - pendingFailedTurn: { turnNumber: 1 }, - }); - expect(afterFailure.world).toEqual(initial.world); - - const recovered = singleTurnResponseSchema.parse( - await ( - await app.request('/api/simulation/turn/retry', { method: 'POST' }) - ).json(), - ); - expect(recovered.turn).toMatchObject({ - turnNumber: 1, - agentId: initial.nextAgentId, - outcome: 'accepted', - }); - }); - - it('resets world progress while preserving personality configuration', async () => { - const app = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'infect' }, summary: 'Infect.' }, - ]), - }); - const initial = simulationSnapshotSchema.parse( - await (await app.request('/api/simulation')).json(), - ); - await app.request( - `/api/simulation/agents/${initial.world.agents[0]!.id}/personality`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ personality: 'Preserved through reset.' }), - }, - ); - await app.request('/api/simulation/turn', { method: 'POST' }); - const response = await app.request('/api/simulation/reset', { - method: 'POST', - }); - const reset = resetSimulationResponseSchema.parse(await response.json()); - expect(reset.snapshot.turnNumber).toBe(0); - expect(reset.snapshot.world.events).toEqual([]); - expect(reset.snapshot.world.agents[0]!.personality).toBe( - 'Preserved through reset.', - ); - }); - - it('updates one agent personality through a runtime-validated safe response', async () => { - const app = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - }); - const initial = simulationSnapshotSchema.parse( - await (await app.request('/api/simulation')).json(), - ); - const agent = initial.world.agents[0]!; - const response = await app.request( - `/api/simulation/agents/${agent.id}/personality`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ personality: ' Explore open edges. ' }), - }, - ); - expect(response.status).toBe(200); - const payload = updateAgentPersonalityResponseSchema.parse( - await response.json(), - ); - expect(payload.agent.personality).toBe('Explore open edges.'); - expect(payload.snapshot.world.agents[0]!.personality).toBe( - 'Explore open edges.', - ); - expect(JSON.stringify(payload)).not.toMatch(/api[_-]?key|secret|prompt/i); + ).swarmTicks, + ).toHaveLength(1); }); - it.each([ - JSON.stringify({ personality: '' }), - JSON.stringify({ personality: 42 }), - '{malformed', - ])('rejects invalid personality request bodies safely', async (body) => { + it('does not expose legacy sequential-turn execution routes', async () => { const app = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - }); - const response = await app.request( - '/api/simulation/agents/128f3f38-6b7d-4db7-9e95-751b4ce2681e/personality', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body, - }, - ); - expect(response.status).toBe(400); - await expect(response.json()).resolves.toEqual({ - error: { - code: 'invalid_personality', - message: 'Personality must contain 1 to 600 characters.', - }, + swarmPlanner: new DeterministicSwarmPlanner(), + reflexProvider: new DeterministicReflexProvider(), }); - }); - - it('returns typed invalid and unknown agent errors without internal details', async () => { - const app = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - }); - for (const [agentId, status, code] of [ - ['not-a-uuid', 400, 'invalid_agent_id'], - ['aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', 404, 'unknown_agent'], - ] as const) { - const response = await app.request( - `/api/simulation/agents/${agentId}/personality`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ personality: 'Valid request.' }), - }, - ); - expect(response.status).toBe(status); - const body = await response.json(); - expect(body).toMatchObject({ error: { code } }); - expect(JSON.stringify(body)).not.toMatch( - /stack|provider|openrouter|secret/i, + for (const route of [ + '/api/simulation/turn', + '/api/simulation/turn/retry', + '/api/simulation/turn/skip', + '/api/simulation/turn/unattended-retry', + '/api/simulation/turn/unattended-skip', + ]) + expect((await app.request(route, { method: 'POST' })).status, route).toBe( + 404, ); - } }); - - it('restores all default personalities without resetting progress', async () => { - const app = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'infect' }, summary: 'Infect.' }, - ]), - }); - const initial = simulationSnapshotSchema.parse( - await (await app.request('/api/simulation')).json(), - ); - const agent = initial.world.agents[0]!; - await app.request(`/api/simulation/agents/${agent.id}/personality`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ personality: 'Custom.' }), - }); - await app.request('/api/simulation/turn', { method: 'POST' }); - - const response = await app.request( - '/api/simulation/personalities/restore-defaults', - { method: 'POST' }, - ); - expect(response.status).toBe(200); - const restored = restoreDefaultPersonalitiesResponseSchema.parse( - await response.json(), - ); - expect(restored.snapshot.turnNumber).toBe(1); - expect(restored.snapshot.world.events).toHaveLength(1); - expect(restored.snapshot.world.agents[0]!.personality).toBe( - agent.personality, - ); - expect( - restored.snapshot.world.agents.find(({ name }) => name === 'Mingle') - ?.personality, - ).toBe( - 'You are a social coalition-builder. Seek agents, initiate and continue conversations, propose alliances, answer offers, negotiate borders, and coordinate captures against dominant rivals. Prefer cooperation and public diplomacy over silent expansion, but protect your own territory and leave an alliance that repeatedly ignores or exploits you. Make concrete proposals rather than merely announcing actions.', - ); - }); - - it('returns typed conflicts for an overlapping turn and reset', async () => { - let release!: (result: ProviderDecision) => void; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'deferred-test', - configured: true, - decide: () => - new Promise((resolve) => { - release = resolve; - }), - }; - const app = createApp({ provider }); - const pending = app.request('/api/simulation/turn', { method: 'POST' }); - expect( - (await app.request('/api/simulation/turn', { method: 'POST' })).status, - ).toBe(409); - expect( - (await app.request('/api/simulation/reset', { method: 'POST' })).status, - ).toBe(409); - const agentId = simulationSnapshotSchema.parse( - await (await app.request('/api/simulation')).json(), - ).world.agents[0]!.id; - const editConflict = await app.request( - `/api/simulation/agents/${agentId}/personality`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ personality: 'Blocked.' }), - }, - ); - expect(editConflict.status).toBe(409); - await expect(editConflict.json()).resolves.toMatchObject({ - error: { code: 'personality_conflict' }, - }); - const restoreConflict = await app.request( - '/api/simulation/personalities/restore-defaults', - { method: 'POST' }, - ); - expect(restoreConflict.status).toBe(409); - await expect(restoreConflict.json()).resolves.toMatchObject({ - error: { code: 'personality_conflict' }, - }); - const exportConflict = await app.request( - '/api/simulation/experiment/export/preview', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - agents: { mode: 'all' }, - turns: { mode: 'entire-retained' }, - outcomes: ['accepted'], - actions: ['wait'], - level: 'minimal', - }), - }, - ); - expect(exportConflict.status).toBe(409); - await expect(exportConflict.json()).resolves.toMatchObject({ - error: { code: 'export_conflict' }, - }); - release({ - decision: { worldAction: { type: 'wait' }, summary: 'Done.' }, - metadata: { - provider: 'scripted-test', - model: 'deferred-test', - latencyMs: 0, - }, - }); - expect((await pending).status).toBe(200); - }); - - it('uses a predictable error envelope', async () => { - const app = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - }); - const response = await app.request('/missing'); - expect(response.status).toBe(404); - await expect(response.json()).resolves.toEqual({ - error: { - code: 'not_found', - message: 'The requested route does not exist.', - }, - }); - }); - - it('previews and generates schema-valid retained exports through narrow endpoints', async () => { - const app = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - }); - await app.request('/api/simulation/turn', { method: 'POST' }); - const request = { - agents: { mode: 'all' }, - turns: { mode: 'entire-retained' }, - outcomes: ['accepted', 'rejected', 'provider-error'], - actions: ['move', 'infect', 'capture', 'wait'], - level: 'minimal', - }; - const previewResponse = await app.request( - '/api/simulation/experiment/export/preview', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(request), - }, - ); - expect(previewResponse.status).toBe(200); - const preview = experimentExportPreviewSchema.parse( - await previewResponse.json(), - ); - expect(preview).toMatchObject({ - matchingTurnCount: 1, - knownCostCredits: 0, - attemptsWithUnknownCost: 0, - turnsWithUnknownCost: 0, - }); - const generatedResponse = await app.request( - '/api/simulation/experiment/export', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(request), - }, - ); - const generated = experimentExportResponseSchema.parse( - await generatedResponse.json(), - ); - expect( - generated.document.turns.map(({ turnNumber }) => turnNumber), - ).toEqual([1]); - expect(JSON.stringify(generated)).not.toMatch( - /authorization|api[_-]?key|rawPrompt|rawBody|chainOfThought|privateReasoning/i, - ); - }); - - it('exports a Full Safe simultaneous tick containing a lost record', async () => { - let failingAgentId: string | undefined; - const app = createApp({ - provider: { - mode: 'scripted-test', - configured: true, - async decide(observation): Promise { - failingAgentId ??= observation.agentId; - if (observation.agentId === failingAgentId) - throw new AgentProviderError({ - code: 'timeout', - message: 'Deadline exhausted.', - retryable: false, - }); - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait.' }, - metadata: { - provider: 'scripted-test', - model: 'lost-tick-export-test', - latencyMs: 0, - }, - }; - }, - }, - }); - const tickResponse = await app.request('/api/simulation/tick', { - method: 'POST', - }); - expect(tickResponse.status).toBe(200); - - const response = await app.request('/api/simulation/experiment/export', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - agents: { mode: 'all' }, - turns: { mode: 'entire-retained' }, - outcomes: [ - 'accepted', - 'rejected', - 'provider-error', - 'operator-skipped', - 'lost-tick', - ], - actions: ['move', 'infect', 'capture', 'wait'], - communications: { channel: 'all', status: 'all' }, - level: 'full-safe', - }), - }); - expect(response.status).toBe(200); - const { document } = experimentExportResponseSchema.parse( - await response.json(), - ); - expect(document.schemaVersion).toBe(11); - const lostTick = document.turns.find( - ({ outcome }) => outcome === 'lost-tick', - ); - expect(lostTick).toBeDefined(); - expect(lostTick).not.toHaveProperty('worldActionResult'); - expect(lostTick).not.toHaveProperty('communicationResult'); - expect(lostTick).not.toHaveProperty('diplomacyResult'); - }); - - it('archives the exact supplied generated artifact through an injected writer', async () => { - const archiveExperimentExport = vi.fn( - (document: ExperimentExportDocument) => ({ - experimentId: document.experiment.id, - inserted: 3, - existing: 0, - skipped: 1, - rejected: 0, - idempotent: false, - }), - ); - const app = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - archiveExperimentExport, - }); - const request = { - agents: { mode: 'all' as const }, - turns: { mode: 'entire-retained' as const }, - outcomes: ['accepted' as const], - actions: ['wait' as const], - level: 'minimal' as const, - }; - const generatedResponse = await app.request( - '/api/simulation/experiment/export', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(request), - }, - ); - const generated = experimentExportResponseSchema.parse( - await generatedResponse.json(), - ); - const response = await app.request( - '/api/simulation/experiment/export/archive', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(generated), - }, - ); - expect(response.status).toBe(200); - expect(archiveExperimentExport).toHaveBeenCalledWith(generated.document); - expect( - archiveExperimentExportResponseSchema.parse(await response.json()), - ).toMatchObject({ inserted: 3, idempotent: false }); - }); - - it('regenerates and verifies an exact artifact from a compact archive request', async () => { - const archiveExperimentExport = vi.fn( - (document: ExperimentExportDocument) => ({ - experimentId: document.experiment.id, - inserted: 3, - existing: 0, - skipped: 0, - rejected: 0, - idempotent: false, - }), - ); - const app = createApp({ archiveExperimentExport }); - const request = { - agents: { mode: 'all' as const }, - turns: { mode: 'entire-retained' as const }, - outcomes: ['accepted' as const], - actions: ['wait' as const], - level: 'full-safe' as const, - }; - const generatedResponse = await app.request( - '/api/simulation/experiment/export', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(request), - }, - ); - const generated = experimentExportResponseSchema.parse( - await generatedResponse.json(), - ); - const response = await app.request( - '/api/simulation/experiment/export/archive', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - request, - generatedAt: generated.document.generatedAt, - sha256: createHash('sha256') - .update(JSON.stringify(generated.document)) - .digest('hex'), - }), - }, - ); - expect(response.status).toBe(200); - expect(archiveExperimentExport).toHaveBeenCalledWith(generated.document); - }); - - it('rejects a compact archive request when the generated artifact changed', async () => { - const archiveExperimentExport = vi.fn(); - const app = createApp({ archiveExperimentExport }); - const exportRequest = { - agents: { mode: 'all' as const }, - turns: { mode: 'entire-retained' as const }, - outcomes: ['accepted' as const], - actions: ['wait' as const], - level: 'full-safe' as const, - }; - const generatedResponse = await app.request( - '/api/simulation/experiment/export', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(exportRequest), - }, - ); - expect(generatedResponse.status).toBe(200); - const generated = experimentExportResponseSchema.parse( - await generatedResponse.json(), - ); - const response = await app.request( - '/api/simulation/experiment/export/archive', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - request: generated.document.filters, - generatedAt: generated.document.generatedAt, - sha256: '0'.repeat(64), - }), - }, - ); - expect(response.status).toBe(409); - await expect(response.json()).resolves.toEqual({ - error: { - code: 'artifact_changed', - message: - 'The experiment changed after this export was generated. Generate it again before saving.', - }, - }); - expect(archiveExperimentExport).not.toHaveBeenCalled(); - }); - - it('rejects invalid archive artifacts before invoking persistence', async () => { - const archiveExperimentExport = vi.fn(); - const app = createApp({ archiveExperimentExport }); - const response = await app.request( - '/api/simulation/experiment/export/archive', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ document: { schemaVersion: 10 } }), - }, - ); - expect(response.status).toBe(400); - await expect(response.json()).resolves.toMatchObject({ - error: { code: 'invalid_artifact' }, - }); - expect(archiveExperimentExport).not.toHaveBeenCalled(); - }); - - it.each([ - { - error: new ExperimentImportError('unsafe internal rejection detail'), - status: 422, - code: 'archive_rejected', - message: 'The experiment archive rejected the export safely.', - }, - { - error: new ArchivePersistenceError('private filesystem detail'), - status: 500, - code: 'archive_persistence_failed', - message: 'The local experiment archive could not be updated.', - }, - { - error: new ExperimentImportError( - 'wrapped private persistence detail', - new ArchivePersistenceError('private database detail'), - ), - status: 500, - code: 'archive_persistence_failed', - message: 'The local experiment archive could not be updated.', - }, - ])( - 'maps archive failures to safe API errors', - async ({ error, status, code, message }) => { - const sourceApp = createApp(); - const generatedResponse = await sourceApp.request( - '/api/simulation/experiment/export', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - agents: { mode: 'all' }, - turns: { mode: 'entire-retained' }, - outcomes: ['accepted'], - actions: ['wait'], - level: 'minimal', - }), - }, - ); - const generated = experimentExportResponseSchema.parse( - await generatedResponse.json(), - ); - const app = createApp({ - archiveExperimentExport: () => { - throw error; - }, - }); - const response = await app.request( - '/api/simulation/experiment/export/archive', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(generated), - }, - ); - expect(response.status).toBe(status); - const body = await response.json(); - expect(body).toEqual({ error: { code, message } }); - expect(JSON.stringify(body)).not.toMatch(/private|filesystem|database/); - }, - ); - - it.each([ - [{}, 400, 'invalid_export'], - [ - { - agents: { mode: 'selected', agentIds: [] }, - turns: { mode: 'entire-retained' }, - outcomes: ['accepted'], - actions: ['wait'], - level: 'minimal', - }, - 400, - 'invalid_export', - ], - [ - { - agents: { - mode: 'selected', - agentIds: ['aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'], - }, - turns: { mode: 'entire-retained' }, - outcomes: ['accepted'], - actions: ['wait'], - level: 'minimal', - }, - 404, - 'unknown_agent', - ], - ])( - 'returns typed safe export validation failures', - async (body, status, code) => { - const app = createApp({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - }); - const response = await app.request('/api/simulation/experiment/export', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - expect(response.status).toBe(status); - await expect(response.json()).resolves.toMatchObject({ error: { code } }); - }, - ); }); diff --git a/apps/game-api/src/app.ts b/apps/game-api/src/app.ts index 69e2116..673771d 100644 --- a/apps/game-api/src/app.ts +++ b/apps/game-api/src/app.ts @@ -2,13 +2,12 @@ import { Hono, type Context } from 'hono'; import { cors } from 'hono/cors'; import { createHash } from 'node:crypto'; import { - BrowserTestAgentProvider, - AgentProviderError, + DeterministicReflexProvider, + DeterministicSwarmPlanner, OpenRouterModelCatalog, - OpenRouterAgentProvider, OpenRouterSwarmPlanner, + SwarmPlannerError, TypeSafeJevReflexProvider, - type AgentProvider, type ReflexProvider, type SwarmPlanner, } from '@hexzero/agent-runtime'; @@ -16,9 +15,9 @@ import { archiveExperimentExportRequestSchema, archiveExperimentExportResponseSchema, apiErrorSchema, - AGENT_DECISION_CONTRACT_VERSION, + SWARM_PLANNER_CONTRACT_VERSION, cancelSimulationResponseSchema, - cancelledTurnResponseSchema, + cancelledTickResponseSchema, experimentExportRequestSchema, experimentExportPreviewSchema, experimentExportResponseSchema, @@ -31,7 +30,6 @@ import { resetSimulationResponseSchema, restoreDefaultPersonalitiesResponseSchema, simulationSnapshotSchema, - singleTurnResponseSchema, singleTickResponseSchema, updateAgentPersonalityRequestSchema, updateAgentPersonalityResponseSchema, @@ -77,7 +75,6 @@ export { healthResponseSchema }; export interface AppOptions { service?: SimulationService; - provider?: AgentProvider; swarmPlanner?: SwarmPlanner; reflexProvider?: ReflexProvider; catalog?: Pick; @@ -107,41 +104,32 @@ async function archiveExperimentExportDefault( } } -export function resolveProviderModeFromEnvironment( +export function swarmProvidersFromEnvironment( environment: NodeJS.ProcessEnv = process.env, - warn: (message: string) => void = console.warn, -): string | undefined { - if (environment.HEXZERO_PROVIDER !== undefined) - return environment.HEXZERO_PROVIDER; - if (environment.AGENTBORNE_PROVIDER !== undefined) { - warn( - 'AGENTBORNE_PROVIDER is deprecated; use HEXZERO_PROVIDER. Continuing with the legacy setting.', - ); - return environment.AGENTBORNE_PROVIDER; - } - return undefined; -} - -export function providerFromEnvironment( - environment: NodeJS.ProcessEnv = process.env, - warn: (message: string) => void = console.warn, -): AgentProvider { - if (resolveProviderModeFromEnvironment(environment, warn) === 'scripted') { - return new BrowserTestAgentProvider(); - } - return new OpenRouterAgentProvider({ - apiKey: environment.OPENROUTER_API_KEY, - }); +): { swarmPlanner: SwarmPlanner; reflexProvider: ReflexProvider } { + if (environment.HEXZERO_PROVIDER === 'scripted') + return { + swarmPlanner: new DeterministicSwarmPlanner(), + reflexProvider: new DeterministicReflexProvider(), + }; + return { + swarmPlanner: new OpenRouterSwarmPlanner({ + apiKey: environment.OPENROUTER_API_KEY, + }), + reflexProvider: new TypeSafeJevReflexProvider({ + apiKey: environment.TYPESAFE_API_KEY, + }), + }; } export function createApp(options: AppOptions = {}) { const app = new Hono(); + const providers = swarmProvidersFromEnvironment(); const service = options.service ?? new SimulationService({ - provider: options.provider ?? providerFromEnvironment(), - swarmPlanner: options.swarmPlanner ?? new OpenRouterSwarmPlanner(), - reflexProvider: options.reflexProvider ?? new TypeSafeJevReflexProvider(), + swarmPlanner: options.swarmPlanner ?? providers.swarmPlanner, + reflexProvider: options.reflexProvider ?? providers.reflexProvider, }); const catalog = options.catalog ?? @@ -153,13 +141,7 @@ export function createApp(options: AppOptions = {}) { const turnMutations = new Map>(); const mutationPromise = ( context: Context, - operation: - | 'turn' - | 'tick' - | 'retry' - | 'unattended-retry' - | 'unattended-skip' - | 'setup', + operation: 'tick' | 'setup', execute: () => Promise, ): Promise => { const supplied = @@ -345,7 +327,7 @@ export function createApp(options: AppOptions = {}) { }), 400, ); - const cacheKey = `${request.data.modelId}:${request.data.reasoningProfile}:${AGENT_DECISION_CONTRACT_VERSION}`; + const cacheKey = `${request.data.modelId}:${request.data.reasoningProfile}:${SWARM_PLANNER_CONTRACT_VERSION}`; const cached = modelVerifications.get(cacheKey); if (cached && !request.data.force) return context.json( @@ -363,7 +345,7 @@ export function createApp(options: AppOptions = {}) { const verification = modelVerificationSchema.parse({ modelId: request.data.modelId, reasoningProfile: request.data.reasoningProfile, - contractVersion: AGENT_DECISION_CONTRACT_VERSION, + contractVersion: SWARM_PLANNER_CONTRACT_VERSION, status: 'verified', testedAt: new Date().toISOString(), provider, @@ -371,11 +353,11 @@ export function createApp(options: AppOptions = {}) { modelVerifications.set(cacheKey, verification); return context.json(verifyModelResponseSchema.parse({ verification })); } catch (error) { - if (error instanceof AgentProviderError) { + if (error instanceof SwarmPlannerError) { const verification = modelVerificationSchema.parse({ modelId: request.data.modelId, reasoningProfile: request.data.reasoningProfile, - contractVersion: AGENT_DECISION_CONTRACT_VERSION, + contractVersion: SWARM_PLANNER_CONTRACT_VERSION, status: 'failed', testedAt: new Date().toISOString(), failure: { @@ -501,73 +483,22 @@ export function createApp(options: AppOptions = {}) { } }); - app.post('/api/simulation/turn', async (context) => { - try { - const turn = await mutationPromise(context, 'turn', () => - service.executeNextTurn(), - ); - return context.json( - singleTurnResponseSchema.parse({ - snapshot: service.getSnapshot(), - turn, - }), - ); - } catch (error) { - if (error instanceof SimulationTurnCancelledError) - return context.json( - cancelledTurnResponseSchema.parse({ - snapshot: service.getSnapshot(), - cancelled: true, - }), - ); - if (error instanceof SimulationConflictError) { - return context.json( - apiErrorSchema.parse({ - error: { code: 'turn_conflict', message: error.message }, - }), - 409, - ); - } - if ( - error instanceof SimulationValidationError && - error.code === 'models_unavailable' - ) - return context.json( - apiErrorSchema.parse({ - error: { code: error.code, message: error.message }, - }), - 409, - ); - if (error instanceof SimulationValidationError) - return context.json( - apiErrorSchema.parse({ - error: { code: 'invalid_request', message: error.message }, - }), - 400, - ); - throw error; - } - }); - app.post('/api/simulation/tick', async (context) => { try { const response = await mutationPromise(context, 'tick', async () => { - const records = await service.executeNextTick(); + const swarmTick = await service.executeNextTick(); const snapshot = service.getSnapshot(); return singleTickResponseSchema.parse({ snapshot, tickNumber: snapshot.tickNumber, - records, - ...(snapshot.scenario.cognitionMode === 'zero-swarm-v1' - ? { swarmTick: snapshot.swarmTicks?.at(-1) } - : {}), + swarmTick, }); }); return context.json(response); } catch (error) { if (error instanceof SimulationTurnCancelledError) return context.json( - cancelledTurnResponseSchema.parse({ + cancelledTickResponseSchema.parse({ snapshot: service.getSnapshot(), cancelled: true, }), @@ -590,24 +521,6 @@ export function createApp(options: AppOptions = {}) { } }); - app.post('/api/simulation/turn/cancel', (context) => { - try { - return context.json( - cancelSimulationResponseSchema.parse({ - snapshot: service.cancelCurrentRequest(), - }), - ); - } catch (error) { - if (error instanceof SimulationConflictError) - return context.json( - apiErrorSchema.parse({ - error: { code: 'cancel_conflict', message: error.message }, - }), - 409, - ); - throw error; - } - }); app.post('/api/simulation/tick/cancel', (context) => { try { return context.json( @@ -627,107 +540,6 @@ export function createApp(options: AppOptions = {}) { } }); - const respondToManualTurn = async ( - context: Context, - operation: 'retry' | 'skip', - ) => { - try { - const turn = - operation === 'retry' - ? await mutationPromise(context, 'retry', () => - service.retryFailedTurn(), - ) - : service.skipFailedTurn(); - return context.json( - singleTurnResponseSchema.parse({ - snapshot: service.getSnapshot(), - turn, - }), - ); - } catch (error) { - if (error instanceof SimulationTurnCancelledError) - return context.json( - cancelledTurnResponseSchema.parse({ - snapshot: service.getSnapshot(), - cancelled: true, - }), - ); - if (error instanceof SimulationConflictError) - return context.json( - apiErrorSchema.parse({ - error: { code: 'turn_conflict', message: error.message }, - }), - 409, - ); - if (error instanceof SimulationValidationError) - return context.json( - apiErrorSchema.parse({ - error: { code: error.code, message: error.message }, - }), - 409, - ); - throw error; - } - }; - - app.post('/api/simulation/turn/retry', (context) => - respondToManualTurn(context, 'retry'), - ); - app.post('/api/simulation/turn/skip', (context) => - respondToManualTurn(context, 'skip'), - ); - app.post('/api/simulation/turn/unattended-retry', async (context) => { - try { - const turn = await mutationPromise(context, 'unattended-retry', () => - service.retryFailedTurn('unattended-retry'), - ); - return context.json( - singleTurnResponseSchema.parse({ - snapshot: service.getSnapshot(), - turn, - }), - ); - } catch (error) { - if (error instanceof SimulationTurnCancelledError) - return context.json( - cancelledTurnResponseSchema.parse({ - snapshot: service.getSnapshot(), - cancelled: true, - }), - ); - if (error instanceof SimulationConflictError) - return context.json( - apiErrorSchema.parse({ - error: { code: 'turn_conflict', message: error.message }, - }), - 409, - ); - throw error; - } - }); - app.post('/api/simulation/turn/unattended-skip', async (context) => { - try { - const turn = await mutationPromise(context, 'unattended-skip', async () => - service.skipFailedTurn('unattended'), - ); - return context.json( - singleTurnResponseSchema.parse({ - snapshot: service.getSnapshot(), - turn, - }), - ); - } catch (error) { - if (error instanceof SimulationConflictError) - return context.json( - apiErrorSchema.parse({ - error: { code: 'turn_conflict', message: error.message }, - }), - 409, - ); - throw error; - } - }); - app.post('/api/simulation/reset', (context) => { try { return context.json( diff --git a/apps/game-api/src/experiment-export.ts b/apps/game-api/src/experiment-export.ts index 8b583b7..3527fb2 100644 --- a/apps/game-api/src/experiment-export.ts +++ b/apps/game-api/src/experiment-export.ts @@ -1,5 +1,5 @@ import { - AGENT_DECISION_CONTRACT_VERSION, + SWARM_PLANNER_CONTRACT_VERSION, experimentExportDocumentSchema, experimentExportPreviewSchema, experimentExportRequestSchema, @@ -751,7 +751,7 @@ export function createExperimentExport( }; const include = inclusionsFor(request); const exportedSwarmTicks: SwarmTickRecord[] | undefined = - source.scenario.cognitionMode === 'zero-swarm-v1' && + source.scenario.swarmArchitectureVersion === 'zero-swarm-v1' && request.agents.mode === 'all' && request.turns.mode === 'entire-retained' ? [...structuredClone(source.swarmTicks ?? [])] @@ -800,7 +800,7 @@ export function createExperimentExport( id: source.id, startedAt: source.startedAt, providerMode: source.providerMode, - decisionContractVersion: AGENT_DECISION_CONTRACT_VERSION, + swarmPlannerContractVersion: SWARM_PLANNER_CONTRACT_VERSION, modelConfiguration: structuredClone(source.modelConfiguration), behaviorConfiguration: structuredClone(source.behaviorConfiguration), scenario: structuredClone(source.scenario), diff --git a/apps/game-api/src/live-swarm-comparison.test.ts b/apps/game-api/src/live-swarm-comparison.test.ts index b70487a..2f03bd3 100644 --- a/apps/game-api/src/live-swarm-comparison.test.ts +++ b/apps/game-api/src/live-swarm-comparison.test.ts @@ -26,7 +26,6 @@ describe('runLiveComparison admission guard', () => { const providers = { createPlanner, createReflex: vi.fn(), - createLegacy: vi.fn(), } as unknown as LiveComparisonProviders; await expect( @@ -40,7 +39,6 @@ describe('runLiveComparison admission guard', () => { const providers = { createPlanner, createReflex: vi.fn(), - createLegacy: vi.fn(), } as unknown as LiveComparisonProviders; await expect( diff --git a/apps/game-api/src/live-swarm-comparison.ts b/apps/game-api/src/live-swarm-comparison.ts index 8568b13..c38ec2c 100644 --- a/apps/game-api/src/live-swarm-comparison.ts +++ b/apps/game-api/src/live-swarm-comparison.ts @@ -1,8 +1,4 @@ -import { - BrowserTestAgentProvider, - type ReflexProvider, - type SwarmPlanner, -} from '@hexzero/agent-runtime'; +import { type ReflexProvider, type SwarmPlanner } from '@hexzero/agent-runtime'; import { assignBehavior, type CompatibleModel } from '@hexzero/shared'; import { generateDeterministicRoster } from '@hexzero/world-engine'; import { SimulationService } from './simulation-service'; @@ -141,7 +137,7 @@ export interface LiveComparisonReport { seeds: readonly string[]; tickCap: number; agentCount: number; - cognitionMode: 'zero-swarm-v1'; + swarmArchitectureVersion: 'zero-swarm-v1'; trailHunterProfile: 'trail-hunter-v1'; rosterSeed: 'worker-capture-roster'; patientZeroRosterIndex: 1; @@ -375,9 +371,6 @@ async function runVariant( providers: LiveComparisonProviders, ): Promise { const service = new SimulationService({ - // Zero-swarm never asks the legacy AgentProvider. Keeping this local test - // provider prevents an accidental legacy network call in the ablation. - provider: new BrowserTestAgentProvider(), swarmPlanner: providers.createPlanner(), reflexProvider: providers.createReflex(), ...(variant === 'live-zero-deterministic-workers' @@ -385,8 +378,6 @@ async function runVariant( : {}), createEventId: ids('1'), createExperimentId: ids('2'), - createAllianceId: ids('3'), - createProposalId: ids('4'), }); service.setCompatibleModels([modelFor(config.modelId)]); const request = service.getDefaultWorldSetup(); @@ -397,7 +388,6 @@ async function runVariant( const zeroAgentId = roster[1]!.id; service.applyWorldSetup({ ...request, - cognitionMode: 'zero-swarm-v1', roster, patientZeroAgentId: zeroAgentId, worldSeed: `offline-world-${seed}`, @@ -417,6 +407,8 @@ async function runVariant( overrides: [], locked: false, }, + // This transitional scenario field remains required until PR 2 removes + // legacy behavior configuration from the shared world setup schema. behaviorConfiguration: { ...request.behaviorConfiguration, assignments: assignBehavior( @@ -753,7 +745,7 @@ export async function runLiveComparison( seeds: [...config.seeds], tickCap: config.tickCap, agentCount: config.agentCount, - cognitionMode: 'zero-swarm-v1', + swarmArchitectureVersion: 'zero-swarm-v1', trailHunterProfile: 'trail-hunter-v1', rosterSeed: 'worker-capture-roster', patientZeroRosterIndex: 1, diff --git a/apps/game-api/src/reflex-execution.test.ts b/apps/game-api/src/reflex-execution.test.ts index 597dcbb..5afd190 100644 --- a/apps/game-api/src/reflex-execution.test.ts +++ b/apps/game-api/src/reflex-execution.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; import { gridDisk } from 'h3-js'; import { - BrowserTestAgentProvider, + DeterministicReflexProvider, + DeterministicSwarmPlanner, ScriptedReflexProvider, TypeSafeJevReflexProvider, type ReflexProvider, @@ -45,20 +46,18 @@ function fixture() { } describe('zero-swarm reflex execution seam', () => { - it('identifies zero-swarm scenarios without running the legacy tick executor', async () => { + it('executes swarm scenarios through the planner and reflex seams', async () => { const service = new SimulationService({ - provider: new BrowserTestAgentProvider(), + swarmPlanner: new DeterministicSwarmPlanner(), + reflexProvider: new DeterministicReflexProvider(), }); const setup = service.getDefaultWorldSetup(); const snapshot = service.applyWorldSetup({ ...setup, - cognitionMode: 'zero-swarm-v1', }); - expect(snapshot.scenario.cognitionMode).toBe('zero-swarm-v1'); - await expect(service.executeNextTick()).rejects.toThrow( - 'Zero-swarm execution requires a planner and reflex provider', - ); - expect(service.getSnapshot().tickNumber).toBe(0); + expect(snapshot.scenario.swarmArchitectureVersion).toBe('zero-swarm-v1'); + await service.executeNextTick(); + expect(service.getSnapshot().tickNumber).toBe(1); }); it('takes a directive through legal candidate choice and the real engine', async () => { diff --git a/apps/game-api/src/server.ts b/apps/game-api/src/server.ts index e136833..c181e5a 100644 --- a/apps/game-api/src/server.ts +++ b/apps/game-api/src/server.ts @@ -3,10 +3,7 @@ import { serve } from '@hono/node-server'; import { applyProviderEnvironmentFile } from '@hexzero/agent-runtime'; import { createApp } from './app'; -if ( - (process.env.HEXZERO_PROVIDER ?? process.env.AGENTBORNE_PROVIDER) !== - 'scripted' -) { +if (process.env.HEXZERO_PROVIDER !== 'scripted') { try { process.loadEnvFile('../../.env'); applyProviderEnvironmentFile( diff --git a/apps/game-api/src/simulation-service.swarm.test.ts b/apps/game-api/src/simulation-service.swarm.test.ts index 99a4c6f..d736072 100644 --- a/apps/game-api/src/simulation-service.swarm.test.ts +++ b/apps/game-api/src/simulation-service.swarm.test.ts @@ -1,10 +1,8 @@ import { gridDistance } from 'h3-js'; import { describe, expect, it } from 'vitest'; import { - BrowserTestAgentProvider, ReflexProviderError, ScriptedReflexProvider, - type AgentProvider, type PlannerOptions, type ReflexProvider, type SwarmPlanner, @@ -232,10 +230,8 @@ function setup( planner: SwarmPlanner, reflex: ReflexProvider, pressure = false, - provider: AgentProvider = new BrowserTestAgentProvider(), ) { const simulation = new SimulationService({ - provider, swarmPlanner: planner, reflexProvider: reflex, now: () => '2026-08-13T12:00:00.000Z', @@ -244,7 +240,6 @@ function setup( const request = simulation.getDefaultWorldSetup(); simulation.applyWorldSetup({ ...request, - cognitionMode: 'zero-swarm-v1', ...(pressure ? { objectiveVersion: 'durable-influence-v3' as const, @@ -381,7 +376,6 @@ describe('zero-swarm SimulationService tick', () => { }, }; const simulation = new SimulationService({ - provider: new BrowserTestAgentProvider(), swarmPlanner: planner, reflexProvider: reflex, now: () => '2026-08-13T12:00:00.000Z', @@ -391,7 +385,6 @@ describe('zero-swarm SimulationService tick', () => { const roster = request.roster.slice(0, 2); simulation.applyWorldSetup({ ...request, - cognitionMode: 'zero-swarm-v1', roster, patientZeroAgentId: roster[0]!.id, spawnSeed: 'pressure-spawn-0', @@ -489,7 +482,6 @@ describe('zero-swarm SimulationService tick', () => { const roster = generateDeterministicRoster(1, 'terminal-roster'); simulation.applyWorldSetup({ ...request, - cognitionMode: 'zero-swarm-v1', roster, patientZeroAgentId: roster[0]!.id, spawnSeed: 'terminal-spawn', @@ -516,12 +508,11 @@ describe('zero-swarm SimulationService tick', () => { }, }); - await expect(simulation.executeNextTick()).resolves.toEqual([]); + await expect(simulation.executeNextTick()).resolves.toBeNull(); const snapshot = simulation.getSnapshot(); expect(snapshot.status).toBe('infection-eliminated'); expect(snapshot.tickNumber).toBe(1); expect(snapshot.resolutionOrder).toEqual([]); - expect(snapshot.nextAgentId).toBeNull(); expect(snapshot.world.agents).toEqual([]); expect(snapshot.world.events).toContainEqual( expect.objectContaining({ type: 'simulated-player-agent-captured' }), @@ -562,7 +553,6 @@ describe('zero-swarm SimulationService tick', () => { const roster = generateDeterministicRoster(2, 'worker-capture-roster'); simulation.applyWorldSetup({ ...request, - cognitionMode: 'zero-swarm-v1', roster, patientZeroAgentId: roster[1]!.id, spawnSeed: 'worker-capture-spawn', @@ -617,7 +607,6 @@ describe('zero-swarm SimulationService tick', () => { const roster = generateDeterministicRoster(3, 'attempt-capture-roster'); simulation.applyWorldSetup({ ...request, - cognitionMode: 'zero-swarm-v1', roster, patientZeroAgentId: roster[1]!.id, spawnSeed: 'attempt-capture-spawn', @@ -663,29 +652,6 @@ describe('zero-swarm SimulationService tick', () => { expect(attempts.map(({ agentId }) => agentId)).not.toContain(roster[0]!.id); }); - it('reports swarm providers without requiring the unused legacy provider', () => { - const legacy: AgentProvider = { - mode: 'openrouter', - configured: false, - async decide() { - throw new Error('Legacy provider must not run in zero-swarm mode.'); - }, - }; - const simulation = setup( - new InspectingPlanner(), - new ScriptedReflexProvider([{ chosenCandidateId: 'action_0' }]), - false, - legacy, - ); - const snapshot = simulation.getSnapshot(); - expect(snapshot.providerConfigured).toBe(false); - expect(snapshot.status).toBe('paused'); - expect(snapshot.swarmProviderStatus).toMatchObject({ - plannerConfigured: true, - reflexConfigured: true, - }); - }); - it('returns a schema-valid swarm tick through the API without legacy turn records', async () => { const simulation = setup( new InspectingPlanner(), @@ -699,7 +665,6 @@ describe('zero-swarm SimulationService tick', () => { ); expect(response.status).toBe(200); const tick = singleTickResponseSchema.parse(await response.json()); - expect(tick.records).toEqual([]); expect(tick.swarmTick?.tickNumber).toBe(1); }); @@ -716,11 +681,11 @@ describe('zero-swarm SimulationService tick', () => { .getSnapshot() .world.agents.map(({ id, currentCell }) => [id, currentCell]), ); - await expect(simulation.executeNextTick()).resolves.toEqual([]); + await expect(simulation.executeNextTick()).resolves.toMatchObject({ + tickNumber: 1, + }); const snapshot = simulation.getSnapshot(); expect(snapshot.tickNumber).toBe(1); - expect(snapshot.turnNumber).toBe(0); - expect(snapshot.turns).toEqual([]); expect(snapshot.swarmTicks).toHaveLength(1); expect(snapshot.swarmProviderStatus).toMatchObject({ plannerMode: 'scripted-swarm-test', diff --git a/apps/game-api/src/simulation-service.test.ts b/apps/game-api/src/simulation-service.test.ts index b23a6c1..b8d4f16 100644 --- a/apps/game-api/src/simulation-service.test.ts +++ b/apps/game-api/src/simulation-service.test.ts @@ -1,5006 +1,56 @@ -import { describe, expect, it, vi } from 'vitest'; -import { gridDisk, gridDistance } from 'h3-js'; +import { describe, expect, it } from 'vitest'; import { - AgentProviderError, - ScriptedAgentProvider, - type AgentProvider, - type ProviderDecision, + DeterministicReflexProvider, + DeterministicSwarmPlanner, } from '@hexzero/agent-runtime'; -import { - AGENT_DECISION_CONTRACT_VERSION, - ALLIANCE_COLOR_PALETTE, - NEUTRAL_AGENT_COLOR, - PERSONALITY_MAX_LENGTH, - PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS, - assignBehavior, - agentIdSchema, - allianceIdSchema, - agentTurnRecordSchema, - experimentExportDocumentSchema, - h3CellSchema, - worldEventSchema, - memoryIdSchema, - type Alliance, - type AgentId, - type AgentObservation, - type AgentTurnRecord, - type CompatibleModel, - type WorldEvent, -} from '@hexzero/shared'; -import { - DEVELOPMENT_AGENT_BLUEPRINTS, - createDevelopmentWorld, - defaultWorldSetupRequest, - generateDeterministicRoster, - physicalDistanceKm, - toWorldState, - type WorldState, -} from '@hexzero/world-engine'; -import { - SimulationConflictError, - SimulationService, - SimulationTurnCancelledError, - SimulationValidationError, - selectDiplomacyBlockerExamples, - selectMostRecentPatientZeroThreats, - calculatePatientZeroPressureContext, - applyGoalRevision, - applyMemoryOperation, -} from './simulation-service'; -import { - calculateExperimentMetrics, - serializeExperimentExport, -} from './experiment-export'; -import { geographicDirectionBetweenCells } from './geographic-direction'; - -const now = () => '2026-08-13T12:00:01.000Z'; -function deterministicEventIdGenerator() { - let sequence = 0; - return () => - `67aa21b9-fc78-4b04-9f92-${String(++sequence).padStart(12, '0')}`; -} -const compatibleModels: CompatibleModel[] = [ - { - id: 'author/global-model', - name: 'Global Model', - author: 'author', - contextLength: 16_384, - inputPricePerToken: '0.000001', - outputPricePerToken: '0.000002', - supportedParameters: ['max_tokens'], - isFree: false, - reasoning: { - mandatory: false, - supportedEfforts: ['xhigh', 'low', 'medium'], - }, - }, - { - id: 'author/override-model', - name: 'Override Model', - author: 'author', - contextLength: 32_768, - inputPricePerToken: '0', - outputPricePerToken: '0', - supportedParameters: ['max_tokens'], - isFree: true, - reasoning: { - mandatory: true, - supportedEfforts: ['high', 'low'], +import type { CompatibleModel } from '@hexzero/shared'; +import { SimulationService } from './simulation-service'; + +const model: CompatibleModel = { + id: 'test/zero', + name: 'Test Zero', + author: 'test', + contextLength: 4096, + inputPricePerToken: '0', + outputPricePerToken: '0', + supportedParameters: [], + isFree: true, + reasoning: { mandatory: false, supportedEfforts: ['low'] }, +}; + +function service() { + const simulation = new SimulationService({ + swarmPlanner: new DeterministicSwarmPlanner(), + reflexProvider: new DeterministicReflexProvider(), + now: () => '2026-08-13T12:00:00.000Z', + }); + simulation.setCompatibleModels([model]); + simulation.applyWorldSetup({ + ...simulation.getDefaultWorldSetup(), + modelConfiguration: { + globalModelId: model.id, + globalReasoningProfile: 'low', + overrides: [], + locked: false, }, - }, -]; - -function service(provider: AgentProvider) { - return new SimulationService({ - provider, - now, - createEventId: deterministicEventIdGenerator(), }); + return simulation; } -function exportRequest(level: 'minimal' | 'standard' | 'full-safe' | 'custom') { - return { - agents: { mode: 'all' as const }, - turns: { mode: 'entire-retained' as const }, - outcomes: ['accepted', 'rejected', 'provider-error'] as const, - actions: ['move', 'infect', 'capture', 'wait'] as const, - communications: { channel: 'all' as const, status: 'all' as const }, - level, - }; -} - -describe('SimulationService', () => { - it('retains the most recent Patient Zero threats in chronological order', () => { - const threats = Array.from({ length: 130 }, (_, index) => ({ - eventId: `30000000-0000-4000-8000-${String(index).padStart(12, '0')}`, - occurredAt: new Date( - new Date('2026-08-23T12:00:00.000Z').getTime() + index, - ).toISOString(), - ordinal: index, - })).reverse(); - const selected = selectMostRecentPatientZeroThreats(threats); - expect(selected).toHaveLength(128); - expect(selected.map(({ ordinal }) => ordinal)).toEqual( - Array.from({ length: 128 }, (_, index) => index + 2), - ); - }); - - it('calculates a truthful six-tick pressure window despite unrelated event churn', () => { - const subject = agentIdSchema.parse('128f3f38-6b7d-4db7-9e95-751b4ce2681e'); - const ally = agentIdSchema.parse('2507bb46-7ae4-45ca-8dda-644c4f85ca14'); - const makeEvent = ( - tick: number, - type: 'hex-disinfected' | 'simulated-player-clean-blocked', - agentId: AgentId, - ordinal: number, - ): WorldEvent => - worldEventSchema.parse({ - id: `30000000-0000-4000-8000-${String(ordinal).padStart(12, '0')}`, - type, - occurredAt: new Date( - new Date('2026-08-23T12:00:00.000Z').getTime() + ordinal, - ).toISOString(), - profile: 'casual-cleaner', - originatingTick: tick, - cell: '892a1072893ffff', - ...(type === 'hex-disinfected' - ? { previousControllerAgentId: agentId } - : { blockingAgentId: agentId }), - }); - const events: WorldEvent[] = [ - makeEvent(2, 'hex-disinfected', subject, 1), - makeEvent(3, 'simulated-player-clean-blocked', ally, 2), - makeEvent(4, 'hex-disinfected', subject, 3), - makeEvent(6, 'simulated-player-clean-blocked', subject, 4), - makeEvent(7, 'hex-disinfected', subject, 5), - makeEvent(8, 'simulated-player-clean-blocked', subject, 6), - makeEvent(8, 'hex-disinfected', ally, 7), - makeEvent(9, 'hex-disinfected', subject, 9), - worldEventSchema.parse({ - id: '30000000-0000-4000-8000-000000000008', - type: 'simulated-player-moved', - occurredAt: '2026-08-23T12:00:00.008Z', - profile: 'casual-cleaner', - originatingTick: 8, - fromCell: '892a1072893ffff', - toCell: '892a1072883ffff', - }), - ...Array.from({ length: 160 }, (_, index) => - worldEventSchema.parse({ - id: `40000000-0000-4000-8000-${String(index).padStart(12, '0')}`, - type: 'agent-waited', - occurredAt: new Date( - new Date('2026-08-23T12:01:00.000Z').getTime() + index, - ).toISOString(), - agentId: subject, - }), - ), - ]; - - expect( - calculatePatientZeroPressureContext(events, subject, [subject, ally], 8), - ).toEqual({ - window: { tickCount: 6, startTick: 3, endTick: 8 }, - subject: { - totalEvents: 4, - disinfections: 2, - blockedCleans: 2, - consecutiveAffectedTicks: 3, - }, - currentAlliance: { - totalEvents: 6, - disinfections: 3, - blockedCleans: 3, - }, - }); - }); - - it('commits cleaner pressure before frozen observations without exposing live GPS', async () => { - const seen: AgentObservation[] = []; - const simulation = service({ - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(observation): Promise { - seen.push(structuredClone(observation)); - return { - decision: { - worldAction: - observation.currentCell.state === 'open' - ? { type: 'infect' } - : { - type: 'move', - targetCell: - observation.actionAvailability.moveTargetCellIds[0]!, - }, - goalRevision: observation.currentGoal - ? { operation: 'keep' } - : { - operation: 'establish', - longTermGoal: 'Preserve territory under cleaner pressure.', - shortTermGoal: 'Respond to authoritative local evidence.', - planSummary: 'Expand and adapt to observed losses.', - reason: 'Player pressure is enabled.', - }, - memoryOperation: { operation: 'keep' }, - summary: 'Take a deterministic legal action.', - }, - metadata: { - provider: 'scripted-test', - model: 'deterministic-script', - latencyMs: 0, - }, - }; - }, - }); - const setup = defaultWorldSetupRequest(); - simulation.applyWorldSetup({ - ...setup, - objectiveVersion: 'durable-influence-v3', - modelConfiguration: { - ...setup.modelConfiguration, - globalModelId: 'deterministic-script', - }, - capabilities: { ...setup.capabilities, simulatedPlayerPressure: true }, - simulatedPlayer: { - enabled: true, - profile: 'casual-cleaner', - seed: 'pressure-test', - }, - }); - for (let tick = 0; tick < 12; tick += 1) await simulation.executeNextTick(); - const snapshot = simulation.getSnapshot(); - expect( - snapshot.experiment.simulatedPlayerMetrics.cellsDisinfected, - ).toBeGreaterThan(0); - const pressured = seen.filter( - ({ playerPressure }) => playerPressure.recentThreats.length > 0, - ); - expect(pressured.length).toBeGreaterThan(0); - expect(pressured[0]!.playerPressure).not.toHaveProperty('currentCell'); - expect(snapshot.world.simulatedPlayer?.currentCell).toBeTruthy(); - const patientZeroObservations = seen.filter( - ({ patientZero }) => patientZero.isPatientZero, - ); - const ordinaryObservations = seen.filter( - ({ patientZero }) => !patientZero.isPatientZero, - ); - expect( - ordinaryObservations.every( - ({ patientZeroGlobalView }) => patientZeroGlobalView === null, - ), - ).toBe(true); - expect( - patientZeroObservations.every( - ({ patientZeroGlobalView }) => - patientZeroGlobalView?.playerThreatFeed !== null, - ), - ).toBe(true); - const intervalEventKeys = patientZeroObservations.map( - ({ patientZeroGlobalView }) => - patientZeroGlobalView!.playerThreatFeed!.events.map( - ({ eventId, kind, cell, occurredAt }) => - `${eventId}:${kind}:${cell}:${occurredAt}`, - ), - ); - expect(intervalEventKeys.flat().length).toBeGreaterThan(0); - expect(new Set(intervalEventKeys.flat()).size).toBe( - intervalEventKeys.flat().length, - ); - expect( - patientZeroObservations.flatMap( - ({ patientZeroGlobalView }) => - patientZeroGlobalView!.playerThreatFeed!.events, - ), - ).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - kind: 'occupied-clean-blocked', - blockingAgentId: expect.any(String), - blockingAgentName: expect.any(String), - }), - expect.objectContaining({ - kind: 'territory-disinfected', - affectedAgentId: expect.any(String), - affectedAgentName: expect.any(String), - }), - ]), - ); - expect( - patientZeroObservations - .flatMap( - ({ patientZeroGlobalView }) => - patientZeroGlobalView!.playerThreatFeed!.events, - ) - .every(({ pressureContext }) => pressureContext !== undefined), - ).toBe(true); - expect( - JSON.stringify( - patientZeroObservations.map( - ({ patientZeroGlobalView }) => - patientZeroGlobalView?.playerThreatFeed, - ), - ), - ).not.toMatch(/fromCell|toCell|currentCell|route|target|profile|playerId/i); - const redacted = simulation.generateExperimentExport({ - ...exportRequest('custom'), - custom: { - turnObservations: true, - personalityTextHistory: false, - nearbyAgents: false, - recentEvents: false, - recentPublicMessages: false, - recentDirectMessages: false, - recentControlChanges: false, - validationDetails: false, - resultingEvents: false, - providerUsageMetadata: false, - initialWorldState: false, - currentWorldState: false, - computedMetrics: false, - communications: false, - controlChanges: false, - }, - }); - expect( - redacted.turns.every( - ({ observation }) => - observation?.playerPressure?.enabled === true && - observation.playerPressure.recentThreats?.length === 0, - ), - ).toBe(true); - expect(redacted).not.toHaveProperty('simulatedPlayerMetrics'); - const redactedGlobalFeeds = redacted.turns - .map( - ({ observation }) => - observation?.patientZeroGlobalView?.playerThreatFeed, - ) - .filter((feed) => feed !== null && feed !== undefined); - expect(redactedGlobalFeeds.length).toBeGreaterThan(0); - expect( - redactedGlobalFeeds.every( - (feed) => - feed.events.length === 0 && - feed.truncated === feed.totalEventCount > 0, - ), - ).toBe(true); - expect(JSON.stringify(redactedGlobalFeeds)).not.toContain( - 'pressureContext', - ); - }); - - it('keeps compact memory canonical and rejects full or missing operations independently', () => { - const agent = agentIdSchema.parse('128f3f38-6b7d-4db7-9e95-751b4ce2681e'); - const remembered = applyMemoryOperation( - [], - { operation: 'remember', text: 'The northern route was blocked.' }, - agent, - 1, - ); - expect(remembered).toMatchObject({ - entries: [ - { - text: 'The northern route was blocked.', - createdAtTick: 1, - revisedAtTick: 1, - }, - ], - result: { accepted: true, operation: 'remember' }, - }); - const id = remembered.entries[0]!.id; - const revised = applyMemoryOperation( - remembered.entries, - { operation: 'revise', memoryId: id, text: 'The route reopened.' }, - agent, - 2, - ); - expect(revised.entries[0]).toMatchObject({ - id, - text: 'The route reopened.', - createdAtTick: 1, - revisedAtTick: 2, - }); - expect(remembered.entries[0]!.text).toBe('The northern route was blocked.'); - expect( - applyMemoryOperation( - revised.entries, - { - operation: 'forget', - memoryId: memoryIdSchema.parse( - 'memory:00000000-0000-4000-8000-000000000999:1', - ), - }, - agent, - 3, - ).result, - ).toMatchObject({ accepted: false, reason: 'memory-not-found' }); - const full = Array.from({ length: 8 }, (_, index) => ({ - id: memoryIdSchema.parse(`memory:${agent}:${index + 1}`), - text: `Memory ${index + 1}`, - createdAtTick: index + 1, - revisedAtTick: index + 1, - })); - expect( - applyMemoryOperation( - full, - { operation: 'remember', text: 'Overflow.' }, - agent, - 9, - ).result, - ).toMatchObject({ accepted: false, reason: 'memory-full' }); - expect( - applyMemoryOperation( - revised.entries, - { operation: 'forget', memoryId: id }, - agent, - 3, - ).entries, - ).toEqual([]); - }); - - it('keeps, revises, completes, and abandons active goal state deterministically', () => { - const initial = applyGoalRevision( - undefined, - { - operation: 'establish', - longTermGoal: 'Hold a corridor.', - shortTermGoal: 'Infect the frontier.', - planSummary: 'Expand north.', - reason: 'Begin a plan.', - }, - 1, - ).goal!; - expect(applyGoalRevision(initial, { operation: 'keep' }, 2)).toEqual({ - goal: initial, - result: { requested: true, accepted: true, operation: 'keep' }, - }); - const revised = applyGoalRevision( - initial, - { - operation: 'revise', - longTermGoal: 'Hold a corridor.', - shortTermGoal: 'Turn east.', - planSummary: 'Avoid the occupied route.', - reason: 'The frontier changed.', - }, - 3, - ); - expect(revised.goal).toMatchObject({ - establishedAtTick: 1, - revisedAtTick: 3, - shortTermGoal: 'Turn east.', - }); - expect( - applyGoalRevision( - revised.goal, - { operation: 'complete', reason: 'Done.' }, - 4, - ), - ).toMatchObject({ - goal: undefined, - result: { accepted: true, operation: 'complete' }, - }); - expect( - applyGoalRevision( - revised.goal, - { operation: 'abandon', reason: 'Blocked.' }, - 4, - ), - ).toMatchObject({ - goal: undefined, - result: { accepted: true, operation: 'abandon' }, - }); - }); - - it('applies bounded goal revisions independently from world actions and clears them on reset', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { - worldAction: { type: 'wait' }, - goalRevision: { - operation: 'establish', - longTermGoal: 'Control a durable corridor.', - shortTermGoal: 'Secure the current frontier.', - planSummary: 'Infect locally before moving outward.', - reason: 'Create strategic continuity.', - }, - memoryOperation: { - operation: 'remember', - text: 'The corridor plan began here.', - }, - summary: 'Establish a corridor goal.', - }, - { - worldAction: { type: 'wait' }, - goalRevision: { operation: 'complete', reason: 'No goal exists.' }, - memoryOperation: { - operation: 'revise', - memoryId: memoryIdSchema.parse( - 'memory:00000000-0000-4000-8000-000000000999:1', - ), - text: 'This memory is unavailable.', - }, - summary: 'Wait while requesting an unavailable completion.', - }, - ]), - ); - - const established = await simulation.executeNextTurn(); - expect(established).toMatchObject({ - outcome: 'accepted', - goalRevisionResult: { - requested: true, - accepted: true, - operation: 'establish', - }, - }); - expect( - simulation - .getSnapshot() - .agentGoals.find(({ agentId }) => agentId === established.agentId) - ?.goal, - ).toMatchObject({ establishedAtTick: 1, revisedAtTick: 1 }); - expect( - simulation - .getSnapshot() - .agentMemories.find(({ agentId }) => agentId === established.agentId) - ?.entries, - ).toEqual([ - expect.objectContaining({ text: 'The corridor plan began here.' }), - ]); - simulation.updateAgentPersonality( - established.agentId, - 'A deliberate memory-preservation test personality.', - ); - expect( - simulation - .getSnapshot() - .agentMemories.find(({ agentId }) => agentId === established.agentId) - ?.entries, - ).toEqual([ - expect.objectContaining({ text: 'The corridor plan began here.' }), - ]); - simulation.restoreDefaultPersonalities(); - expect( - simulation - .getSnapshot() - .agentGoals.find(({ agentId }) => agentId === established.agentId) - ?.goal, - ).toMatchObject({ longTermGoal: 'Control a durable corridor.' }); - const modelConfiguration = simulation.getSnapshot().modelConfiguration; - simulation.updateModelConfiguration({ - globalModelId: modelConfiguration.globalModelId, - globalReasoningProfile: modelConfiguration.globalReasoningProfile, - overrides: modelConfiguration.overrides, - }); - expect( - simulation - .getSnapshot() - .agentGoals.find(({ agentId }) => agentId === established.agentId) - ?.goal, - ).toMatchObject({ longTermGoal: 'Control a durable corridor.' }); - const exported = simulation.generateExperimentExport( - exportRequest('full-safe'), - ); - expect(exported.currentGoals).toContainEqual({ - agentId: established.agentId, - goal: expect.objectContaining({ - longTermGoal: 'Control a durable corridor.', - }), - }); - expect(exported.turns[0]).toMatchObject({ - goalRevision: { operation: 'establish' }, - goalRevisionResult: { accepted: true, operation: 'establish' }, - memoryOperation: { operation: 'remember' }, - memoryOperationResult: { accepted: true, operation: 'remember' }, - }); - expect(exported.currentMemories).toContainEqual({ - agentId: established.agentId, - entries: [ - expect.objectContaining({ text: 'The corridor plan began here.' }), - ], - }); - expect( - experimentExportDocumentSchema.safeParse({ - ...exported, - currentMemories: [ - exported.currentMemories![0], - exported.currentMemories![0], - ], - }).success, - ).toBe(false); - const memorySelectionMismatch = structuredClone(exported); - delete memorySelectionMismatch.currentGoals; - memorySelectionMismatch.selection.selectedAgentIds = - memorySelectionMismatch.selection.selectedAgentIds.slice(1); - expect( - experimentExportDocumentSchema.safeParse(memorySelectionMismatch).success, - ).toBe(false); - expect( - experimentExportDocumentSchema.safeParse({ - ...exported, - currentMemories: exported.currentMemories!.slice(1), - }).success, - ).toBe(false); - simulation.importModelConfiguration(exported); - expect( - simulation - .getSnapshot() - .agentGoals.find(({ agentId }) => agentId === established.agentId) - ?.goal, - ).toMatchObject({ longTermGoal: 'Control a durable corridor.' }); - expect( - simulation - .getSnapshot() - .agentMemories.find(({ agentId }) => agentId === established.agentId) - ?.entries, - ).toEqual([ - expect.objectContaining({ text: 'The corridor plan began here.' }), - ]); - expect( - experimentExportDocumentSchema.safeParse({ - ...exported, - currentGoals: [exported.currentGoals![0], exported.currentGoals![0]], - }).success, - ).toBe(false); - expect( - experimentExportDocumentSchema.safeParse({ - ...exported, - currentGoals: exported.currentGoals!.slice(1), - }).success, - ).toBe(false); - expect( - experimentExportDocumentSchema.safeParse({ - ...exported, - currentGoals: Array.from( - { length: 33 }, - () => exported.currentGoals![0], - ), - }).success, - ).toBe(false); - expect( - experimentExportDocumentSchema.safeParse({ - ...exported, - currentGoals: [ - { - agentId: '00000000-0000-4000-8000-000000000999', - goal: null, - }, - ], - }).success, - ).toBe(false); - - const independentlyRejected = await simulation.executeNextTurn(); - expect(independentlyRejected).toMatchObject({ - outcome: 'accepted', - worldActionResult: { accepted: true }, - goalRevisionResult: { - requested: true, - accepted: false, - operation: 'complete', - reason: 'goal-not-active', - }, - memoryOperationResult: { - requested: true, - accepted: false, - operation: 'revise', - reason: 'memory-not-found', - }, - }); - simulation.reset(); - expect(simulation.getSnapshot().agentGoals.every(({ goal }) => !goal)).toBe( - true, - ); - expect( - simulation - .getSnapshot() - .agentMemories.every(({ entries }) => entries.length === 0), - ).toBe(true); - }); - - it('freezes simultaneous goal observations and commits every completed revision together', async () => { - const seen: AgentObservation[] = []; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(observation) { - seen.push(structuredClone(observation)); - return { - decision: { - worldAction: { type: 'wait' }, - goalRevision: { - operation: 'establish', - longTermGoal: `Durable influence for ${observation.agentName}.`, - shortTermGoal: 'Hold the local frontier.', - planSummary: 'Wait for this deterministic test.', - reason: 'Establish initial continuity.', - }, - memoryOperation: { - operation: 'remember', - text: `Initial memory for ${observation.agentName}.`, - }, - summary: 'Establish a strategic goal.', - }, - metadata: { - provider: 'scripted-test', - model: 'deterministic-script', - latencyMs: 0, - }, - }; - }, - }; - const simulation = service(provider); - const records = await simulation.executeNextTick(); - expect(seen).toHaveLength(records.length); - expect(seen.every(({ currentGoal }) => currentGoal === null)).toBe(true); - expect(seen.every(({ currentMemory }) => currentMemory.length === 0)).toBe( - true, - ); - expect( - simulation.getSnapshot().agentGoals.every(({ goal }) => goal !== null), - ).toBe(true); - expect( - simulation - .getSnapshot() - .agentMemories.every(({ entries }) => entries.length === 1), - ).toBe(true); - expect(records.every((record) => record.outcome === 'accepted')).toBe(true); - }); - - it('requires a known Patient Zero at the live setup boundary', () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - ); - const setup = defaultWorldSetupRequest(); - expect(setup.patientZeroAgentId).toBe(setup.roster[0]!.id); - expect(() => - simulation.applyWorldSetup({ ...setup, patientZeroAgentId: null }), - ).toThrow(SimulationValidationError); - expect(() => - simulation.applyWorldSetup({ - ...setup, - patientZeroAgentId: '00000000-0000-4000-8000-000000000999', - }), - ).toThrow(SimulationValidationError); - expect(simulation.getSnapshot().scenario.patientZeroAgentId).toBe( - setup.patientZeroAgentId, - ); - }); - - it('requires objective attribution to match simulated-player pressure', () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - ); - const setup = defaultWorldSetupRequest(); - - expect(() => - simulation.applyWorldSetup({ - ...setup, - objectiveVersion: 'durable-influence-v3', - }), - ).toThrow(SimulationValidationError); - expect(() => - simulation.applyWorldSetup({ - ...setup, - capabilities: { - ...setup.capabilities, - simulatedPlayerPressure: true, - }, - simulatedPlayer: { - enabled: true, - profile: 'casual-cleaner', - seed: 'mismatched-pressure', - }, - }), - ).toThrow(SimulationValidationError); - - expect(simulation.getSnapshot().scenario.objectiveVersion).toBe( - 'durable-influence-v2', - ); - }); - - it('prioritizes free diplomacy blocker examples before allied relationships', () => { - const base = toWorldState(createDevelopmentWorld({ generatedAt: now() })); - const agents = [...base.agents.values()]; - const allianceEntries: Array<[Alliance['id'], Alliance]> = Array.from( - { length: 3 }, - (_, index) => { - const id = allianceIdSchema.parse( - `10000000-0000-4000-8000-${String(index).padStart(12, '0')}`, - ); - return [ - id, - { - id, - color: ALLIANCE_COLOR_PALETTE[index]!, - memberAgentIds: [agents[index * 2]!.id, agents[index * 2 + 1]!.id], - }, - ]; - }, - ); - const state: WorldState = { - ...base, - alliances: new Map(allianceEntries), - }; - const firstFreeId = agents[6]!.id; - const distantFreeCounterpartId = agents[7]!.id; - const blockersFor = (actingAgentId: AgentId) => - agents - .filter(({ id }) => id !== actingAgentId) - .map(({ id }) => ({ agentId: id, reason: 'out-of-range' as const })); - expect( - selectDiplomacyBlockerExamples( - state, - firstFreeId, - blockersFor(firstFreeId), - ['out-of-range'], - )[0]?.agentId, - ).toBe(distantFreeCounterpartId); - expect( - selectDiplomacyBlockerExamples( - state, - distantFreeCounterpartId, - blockersFor(distantFreeCounterpartId), - ['out-of-range'], - )[0]?.agentId, - ).toBe(firstFreeId); - expect( - selectDiplomacyBlockerExamples( - state, - agents[0]!.id, - blockersFor(agents[0]!.id), - ['out-of-range'], - ) - .slice(0, 2) - .map(({ agentId }) => agentId), - ).toEqual([firstFreeId, distantFreeCounterpartId].toSorted()); - }); - - it('authors exact range-blocked diplomacy affordances before inference', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - ); - const setup = defaultWorldSetupRequest(); - simulation.applyWorldSetup({ - ...setup, - communicationRangeKm: 0.1, - modelConfiguration: { - ...setup.modelConfiguration, - globalModelId: 'deterministic-script', - }, - }); - const record = await simulation.executeNextTurn(); - expect( - record.observation.diplomacyAvailability.propose.blockedRecipients, - ).toEqual( - expect.arrayContaining([ - expect.objectContaining({ reason: 'out-of-range' }), - ]), - ); - expect( - simulation.generateExperimentExport(exportRequest('full-safe')).turns[0] - ?.observation?.diplomacyAvailability, - ).toMatchObject({ - propose: { - blockedRecipients: expect.arrayContaining([ - expect.objectContaining({ reason: 'out-of-range' }), - ]), - }, - }); - }); - - it('freezes all observations, dispatches concurrently, and commits one complete tick', async () => { - const observations: AgentObservation[] = []; - const deadlines = new Set(); - let active = 0; - let maximumActive = 0; - const simulation = service({ - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(observation, _model, options): Promise { - observations.push(structuredClone(observation)); - deadlines.add(options?.deadlineAtMs); - active += 1; - maximumActive = Math.max(maximumActive, active); - await Promise.resolve(); - active -= 1; - return { - decision: { - worldAction: { type: 'wait' }, - communication: { - channel: 'public', - message: `hello-${observation.agentName}`, - }, - summary: 'Wait and report.', - }, - metadata: { - provider: 'scripted-test', - model: 'deterministic-script', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }); - - const records = await simulation.executeNextTick(); - expect(records).toHaveLength(8); - expect(maximumActive).toBeGreaterThan(1); - expect(deadlines.size).toBe(1); - expect(new Set(records.map(({ tickNumber }) => tickNumber))).toEqual( - new Set([1]), - ); - expect( - observations.every( - ({ recentPublicMessages }) => recentPublicMessages.length === 0, - ), - ).toBe(true); - expect(simulation.getSnapshot()).toMatchObject({ - tickNumber: 1, - turnNumber: 8, - }); - }); - - it('keeps resolution records and world events independent of provider completion order', async () => { - const makeProvider = (reverse: boolean): AgentProvider => ({ - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(observation): Promise { - const index = DEVELOPMENT_AGENT_BLUEPRINTS.findIndex( - ({ id }) => id === observation.agentId, - ); - for (let count = 0; count < (reverse ? 7 - index : index); count += 1) - await Promise.resolve(); - return { - decision: { - worldAction: { type: 'wait' }, - communication: { - channel: 'public', - message: observation.agentName, - }, - summary: 'Wait.', - }, - metadata: { - provider: 'scripted-test', - model: 'deterministic-script', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }); - const forward = service(makeProvider(false)); - const reverse = service(makeProvider(true)); - const forwardRecords = await forward.executeNextTick(); - const reverseRecords = await reverse.executeNextTick(); - expect( - reverseRecords.map(({ agentId, outcome }) => ({ agentId, outcome })), - ).toEqual( - forwardRecords.map(({ agentId, outcome }) => ({ agentId, outcome })), - ); - expect(reverse.getSnapshot().world).toEqual(forward.getSnapshot().world); - }); - - it('resolves same-tick diplomacy contention in deterministic phase order', async () => { - const recipientId = agentIdSchema.parse( - DEVELOPMENT_AGENT_BLUEPRINTS[0]!.id, - ); - const simulation = service({ - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(observation): Promise { - return { - decision: { - worldAction: { type: 'wait' }, - ...(observation.agentId === recipientId - ? {} - : { - diplomacy: { type: 'propose-alliance' as const, recipientId }, - }), - summary: 'Propose.', - }, - metadata: { - provider: 'scripted-test', - model: 'deterministic-script', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }); - const records = await simulation.executeNextTick(); - type CompletedRecord = Exclude< - AgentTurnRecord, - { outcome: 'provider-error' | 'lost-tick' | 'operator-skipped' } - >; - const completed = records.filter( - (record): record is CompletedRecord => - record.outcome !== 'lost-tick' && - record.outcome !== 'provider-error' && - record.outcome !== 'operator-skipped', - ); - expect( - completed - .filter(({ agentId }) => agentId !== recipientId) - .every(({ observation }) => - observation.diplomacyAvailability.propose.eligibleRecipientAgentIds.includes( - recipientId, - ), - ), - ).toBe(true); - const requested = completed.filter( - (record) => record.diplomacyResult.requested, - ); - expect( - requested.some( - (record) => - record.diplomacyResult.requested && record.diplomacyResult.accepted, - ), - ).toBe(true); - expect( - requested.some( - (record) => - record.diplomacyResult.requested && !record.diplomacyResult.accepted, - ), - ).toBe(true); - expect( - simulation.getSnapshot().world.pendingAllianceProposals, - ).toHaveLength(1); - }); - - it('records one provider failure as a lost tick while committing sibling decisions', async () => { - let failingAgent: string | undefined; - const simulation = service({ - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(observation): Promise { - failingAgent ??= observation.agentId; - if (observation.agentId === failingAgent) - throw new AgentProviderError({ - code: 'timeout', - message: 'deadline', - retryable: false, - latencyMs: 37, - }); - return { - decision: { - worldAction: { type: 'wait' }, - goalRevision: { - operation: 'establish', - longTermGoal: 'Preserve durable influence.', - shortTermGoal: 'Hold position.', - planSummary: 'Wait safely.', - reason: 'Start continuity.', - }, - memoryOperation: { - operation: 'remember', - text: 'This sibling decision completed.', - }, - summary: 'Wait.', - }, - metadata: { - provider: 'scripted-test', - model: 'deterministic-script', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }); - const records = await simulation.executeNextTick(); - expect( - records.filter(({ outcome }) => outcome === 'lost-tick'), - ).toHaveLength(1); - expect( - records.filter(({ outcome }) => outcome === 'accepted'), - ).toHaveLength(7); +describe('SimulationService swarm execution', () => { + it('commits Zero directives and worker reflex actions in one tick', async () => { + const simulation = service(); + const tick = await simulation.executeNextTick(); + expect(tick?.planSource).toBe('zero-llm'); + expect(tick?.workers.length).toBeGreaterThan(0); expect(simulation.getSnapshot().tickNumber).toBe(1); - expect( - simulation - .getSnapshot() - .agentGoals.find(({ agentId }) => agentId === failingAgent)?.goal, - ).toBeNull(); - expect( - simulation.getSnapshot().agentGoals.filter(({ goal }) => goal), - ).toHaveLength(7); - expect( - simulation - .getSnapshot() - .agentMemories.find(({ agentId }) => agentId === failingAgent)?.entries, - ).toEqual([]); - expect( - simulation - .getSnapshot() - .agentMemories.filter(({ entries }) => entries.length > 0), - ).toHaveLength(7); - const lostTickOutcomes = [ - 'accepted', - 'rejected', - 'lost-tick', - 'provider-error', - 'operator-skipped', - ] as const; - const customWithResults = { - turnObservations: false, - personalityTextHistory: false, - nearbyAgents: false, - recentEvents: false, - recentPublicMessages: false, - recentDirectMessages: false, - recentControlChanges: false, - validationDetails: true, - resultingEvents: true, - providerUsageMetadata: false, - initialWorldState: false, - currentWorldState: false, - computedMetrics: false, - communications: false, - controlChanges: false, - }; - const exports = [ - simulation.generateExperimentExport({ - ...exportRequest('minimal'), - outcomes: lostTickOutcomes, - }), - simulation.generateExperimentExport({ - ...exportRequest('standard'), - outcomes: lostTickOutcomes, - }), - simulation.generateExperimentExport({ - ...exportRequest('full-safe'), - outcomes: lostTickOutcomes, - }), - simulation.generateExperimentExport({ - ...exportRequest('custom'), - outcomes: lostTickOutcomes, - custom: customWithResults, - }), - ]; - for (const exported of exports) { - expect(experimentExportDocumentSchema.safeParse(exported).success).toBe( - true, - ); - const lostTick = exported.turns.find( - ({ outcome }) => outcome === 'lost-tick', - ); - expect(lostTick).toBeDefined(); - expect(lostTick).not.toHaveProperty('worldActionResult'); - expect(lostTick).not.toHaveProperty('communicationResult'); - expect(lostTick).not.toHaveProperty('diplomacyResult'); - } - const minimalAccepted = exports[0]!.turns.find( - ({ outcome }) => outcome === 'accepted', - ); - expect(minimalAccepted).not.toHaveProperty('worldActionResult'); - for (const exported of exports.slice(1)) { - const accepted = exported.turns.find( - ({ outcome }) => outcome === 'accepted', - ); - expect(accepted).toHaveProperty('worldActionResult'); - expect(accepted).toHaveProperty('communicationResult'); - expect(accepted).toHaveProperty('diplomacyResult'); - } - expect(exports[0]!.tickSummaries).toEqual([ - expect.objectContaining({ - providerCallCount: 8, - aggregateDecisionLatencyMs: 37, - maximumDecisionLatencyMs: 37, - lostTicks: 1, - }), - ]); - }); - - it('cancels a simultaneous tick atomically without advancing virtual time', async () => { - let started!: () => void; - const requestStarted = new Promise((resolve) => { - started = resolve; - }); - const simulation = service({ - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(_observation, _model, options): Promise { - started(); - await new Promise((resolve) => - options?.signal?.addEventListener('abort', () => resolve(), { - once: true, - }), - ); - throw new AgentProviderError({ - code: 'cancelled', - message: 'cancelled', - retryable: false, - }); - }, - }); - const setup = defaultWorldSetupRequest(); - const roster = generateDeterministicRoster(12, 'cancel-roster'); - simulation.applyWorldSetup({ - ...setup, - roster, - patientZeroAgentId: roster[0]!.id, - objectiveVersion: 'durable-influence-v3', - modelConfiguration: { - ...setup.modelConfiguration, - globalModelId: 'deterministic-script', - }, - capabilities: { ...setup.capabilities, simulatedPlayerPressure: true }, - behaviorConfiguration: { - ...setup.behaviorConfiguration, - seed: 'cancel-behavior', - assignments: assignBehavior( - roster.map(({ id }) => id), - 'cancel-behavior', - 'balanced-random', - ), - }, - simulatedPlayer: { - enabled: true, - profile: 'casual-cleaner', - seed: 'cancel-pressure', - }, - }); - const before = simulation.getSnapshot(); - const pending = simulation.executeNextTick(); - await requestStarted; - simulation.cancelCurrentRequest(); - await expect(pending).rejects.toBeInstanceOf(SimulationTurnCancelledError); - expect(simulation.getSnapshot()).toMatchObject({ - tickNumber: before.tickNumber, - turnNumber: before.turnNumber, - virtualTime: before.virtualTime, - turns: before.turns, - world: before.world, - agentGoals: before.agentGoals, - agentMemories: before.agentMemories, - experiment: { - attemptAccounting: { - reservedPermits: 0, - attemptsInFlight: 0, - attemptsStarted: expect.any(Number), - attemptsWithUnknownCost: expect.any(Number), - }, - }, - }); - const accounting = simulation.getSnapshot().experiment.attemptAccounting; - expect(accounting.attemptsStarted).toBeGreaterThan(0); - expect(accounting.attemptsFinalized).toBe(accounting.attemptsStarted); - expect(accounting.attemptsWithUnknownCost).toBe(accounting.attemptsStarted); - expect(accounting.attemptsStarted).toBe(8); - const cancelledExport = simulation.generateExperimentExport( - exportRequest('minimal'), - ); - expect(cancelledExport.turns).toEqual([]); - expect(cancelledExport.providerAttempts).toHaveLength(8); - expect( - cancelledExport.providerAttempts?.every( - ({ outcome, failure }) => - outcome === 'cancelled' && failure?.code === 'cancelled', - ), - ).toBe(true); - }); - - it('retains safe provider cost when legacy cancellation is observed after the response', async () => { - const simulation = service({ - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(): Promise { - simulation.cancelCurrentRequest(); - return { - decision: { - worldAction: { type: 'wait' }, - goalRevision: { operation: 'keep' }, - memoryOperation: { operation: 'keep' }, - summary: 'The response completed as cancellation arrived.', - }, - metadata: { - provider: 'scripted-test', - model: 'deterministic-script', - latencyMs: 2, - costCredits: 0.125, - }, - }; - }, - }); - await expect(simulation.executeNextTurn()).rejects.toBeInstanceOf( - SimulationTurnCancelledError, - ); - expect(simulation.getSnapshot()).toMatchObject({ - turnNumber: 0, - turns: [], - experiment: { - attemptAccounting: { - attemptsStarted: 1, - attemptsFinalized: 1, - attemptsInFlight: 0, - knownFinalizedCostCredits: '0.125', - attemptsWithUnknownCost: 0, - }, - }, - }); - expect( - simulation.generateExperimentExport(exportRequest('minimal')), - ).toMatchObject({ - turns: [], - providerAttempts: [ - { - outcome: 'completed', - actualCostCredits: '0.125', - }, - ], - }); - expect( - simulation.generateExperimentExport(exportRequest('minimal')) - .providerAttempts?.[0], - ).not.toHaveProperty('failure'); - }); - - it('keeps completed and truly cancelled simultaneous calls distinct without committing', async () => { - const firstAgentId = defaultWorldSetupRequest().roster[0]!.id; - let completed!: () => void; - const firstCompleted = new Promise((resolve) => { - completed = resolve; - }); - const simulation = service({ - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(observation, _model, options): Promise { - if (observation.agentId === firstAgentId) { - setTimeout(completed, 0); - return { - decision: { - worldAction: { type: 'wait' }, - summary: 'Provider work completed before cancellation.', - }, - metadata: { - provider: 'scripted-test', - model: 'deterministic-script', - latencyMs: 1, - costCredits: 0.01, - }, - }; - } - await new Promise((resolve) => - options?.signal?.addEventListener('abort', () => resolve(), { - once: true, - }), - ); - throw new AgentProviderError({ - code: 'cancelled', - message: 'Provider work was actually aborted.', - retryable: false, - }); - }, - }); - const pending = simulation.executeNextTick(); - await firstCompleted; - simulation.cancelCurrentRequest(); - await expect(pending).rejects.toBeInstanceOf(SimulationTurnCancelledError); - const snapshot = simulation.getSnapshot(); - expect(snapshot).toMatchObject({ tickNumber: 0, turnNumber: 0, turns: [] }); - const exported = simulation.generateExperimentExport( - exportRequest('minimal'), - ); - expect(exported.turns).toEqual([]); - expect( - exported.providerAttempts?.filter( - ({ outcome }) => outcome === 'completed', - ), - ).toHaveLength(1); - expect( - exported.providerAttempts?.filter( - ({ outcome }) => outcome === 'cancelled', - ), - ).toHaveLength(7); - }); - - it('dispatches per-agent model and reasoning overrides for a tick', async () => { - const dispatched: Array<{ - agentId: string; - model: string; - reasoning: string | undefined; - }> = []; - const simulation = service({ - mode: 'scripted-test', - configured: true, - async decide(observation, model, options): Promise { - dispatched.push({ - agentId: observation.agentId, - model, - reasoning: options?.reasoningProfile, - }); - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait.' }, - metadata: { - provider: 'scripted-test', - model, - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }); - simulation.setCompatibleModels(compatibleModels); - const overriddenAgent = simulation.getSnapshot().world.agents[0]!.id; - simulation.updateModelConfiguration({ - globalModelId: compatibleModels[0]!.id, - globalReasoningProfile: 'low', - overrides: [ - { - agentId: overriddenAgent, - modelId: compatibleModels[1]!.id, - reasoningProfile: 'high', - }, - ], - }); - await simulation.executeNextTick(); - expect( - dispatched.find(({ agentId }) => agentId === overriddenAgent), - ).toMatchObject({ - model: compatibleModels[1]!.id, - reasoning: 'high', - }); - expect( - dispatched - .filter(({ agentId }) => agentId !== overriddenAgent) - .every( - ({ model, reasoning }) => - model === compatibleModels[0]!.id && reasoning === 'low', - ), - ).toBe(true); - }); - - it('prevents mixing legacy sequential records and simultaneous ticks in either direction', async () => { - const legacy = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Legacy wait.' }, - ]), - ); - await legacy.executeNextTurn(); - await expect(legacy.executeNextTick()).rejects.toBeInstanceOf( - SimulationConflictError, - ); - expect( - legacy.generateExperimentExport(exportRequest('minimal')).schemaVersion, - ).toBe(11); - - const tick = service( - new ScriptedAgentProvider( - Array.from({ length: 8 }, () => ({ - worldAction: { type: 'wait' as const }, - summary: 'Tick wait.', - })), - ), - ); - await tick.executeNextTick(); - await expect(tick.executeNextTurn()).rejects.toBeInstanceOf( - SimulationConflictError, - ); - await expect(tick.retryFailedTurn()).rejects.toBeInstanceOf( - SimulationConflictError, - ); - expect(() => tick.skipFailedTurn()).toThrow(SimulationConflictError); - expect( - tick.generateExperimentExport(exportRequest('minimal')).schemaVersion, - ).toBe(11); - expect( - tick.generateExperimentExport(exportRequest('minimal')).experiment - .decisionContractVersion, - ).toBe(AGENT_DECISION_CONTRACT_VERSION); }); - it('reproduces tick order and interval after reset and retains only complete tick groups', async () => { - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(): Promise { - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait.' }, - metadata: { - provider: 'scripted-test', - model: 'deterministic-script', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = new SimulationService({ - provider, - now, - createEventId: deterministicEventIdGenerator(), - experimentRetentionLimit: 10, - }); - await simulation.executeNextTick(); - const first = simulation.getSnapshot(); - await simulation.executeNextTick(); - const retained = simulation.generateExperimentExport({ - ...exportRequest('minimal'), - outcomes: [ - 'accepted', - 'rejected', - 'lost-tick', - 'provider-error', - 'operator-skipped', - ], - }); - expect(retained.turns).toHaveLength(8); - expect(retained.schemaVersion).toBe(11); - expect(new Set(retained.turns.map(({ tickNumber }) => tickNumber))).toEqual( - new Set([2]), - ); - expect(experimentExportDocumentSchema.safeParse(retained).success).toBe( - true, - ); - expect( - experimentExportDocumentSchema.safeParse({ - ...retained, - simulatedPlayerMetrics: undefined, - }).success, - ).toBe(false); - const legacyV10 = structuredClone(retained); - legacyV10.schemaVersion = 10; - delete legacyV10.providerAttempts; - delete legacyV10.attemptRetention; - delete legacyV10.attemptAccounting; - delete legacyV10.selection.matchingProviderAttemptCount; - delete legacyV10.simulatedPlayerMetrics; - expect( - experimentExportDocumentSchema.parse(legacyV10).simulatedPlayerMetrics, - ).toEqual({ - movements: 0, - cellsDisinfected: 0, - blockedDisinfections: 0, - }); - const enabledWithoutMetrics = structuredClone(legacyV10); - enabledWithoutMetrics.experiment.scenario = { - ...enabledWithoutMetrics.experiment.scenario!, - objectiveVersion: 'durable-influence-v3', - capabilities: { - ...enabledWithoutMetrics.experiment.scenario!.capabilities, - simulatedPlayerPressure: true, - }, - simulatedPlayer: { - enabled: true, - profile: 'casual-cleaner', - seed: 'missing-metrics', - }, - }; - expect( - experimentExportDocumentSchema.safeParse(enabledWithoutMetrics).success, - ).toBe(false); - expect( - experimentExportDocumentSchema.safeParse({ - ...retained, - tickSummaries: retained.tickSummaries?.map((summary) => ({ - ...summary, - intervalMinutes: summary.intervalMinutes + 1, - })), - }).success, - ).toBe(false); - - simulation.reset(); + it('resets committed swarm state without retaining ticks', async () => { + const simulation = service(); await simulation.executeNextTick(); - expect(simulation.getSnapshot()).toMatchObject({ - resolutionOrder: first.resolutionOrder, - lastTickIntervalMinutes: first.lastTickIntervalMinutes, - virtualTime: first.virtualTime, - }); - }); - - it('preserves the default and custom physical communication range', () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - ); - expect(simulation.getSnapshot().scenario.communicationRangeKm).toBe(12); - const request = defaultWorldSetupRequest(); - const applied = simulation.applyWorldSetup({ - ...request, - communicationRangeKm: 7.5, - }); - expect(applied.scenario.communicationRangeKm).toBe(7.5); - }); - - it('gives only Patient Zero bounded global awareness and delivers a Zero directive plus global reply', async () => { - const seen: AgentObservation[] = []; - const simulation = service({ - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(observation): Promise { - seen.push(structuredClone(observation)); - return { - decision: { - worldAction: { type: 'wait' }, - communication: observation.patientZero.isPatientZero - ? { channel: 'zero', message: 'Take separate infection fronts.' } - : { - channel: 'direct', - recipientId: observation.patientZero.agentId!, - message: 'Taking the eastern front.', - }, - summary: 'Coordinate while waiting.', - }, - metadata: { - provider: 'scripted-test', - model: 'deterministic-script', - latencyMs: 0, - }, - }; - }, - }); - const setup = defaultWorldSetupRequest(); - const patientZeroId = setup.roster[0]!.id; - simulation.applyWorldSetup({ - ...setup, - patientZeroAgentId: patientZeroId, - modelConfiguration: { - ...setup.modelConfiguration, - globalModelId: 'deterministic-script', - }, - }); - const directive = await simulation.executeNextTurn(); - const reply = await simulation.executeNextTurn(); - expect(directive.observation.patientZeroGlobalView?.agents).toHaveLength(8); - expect( - directive.observation.patientZeroGlobalView?.playerThreatFeed, - ).toBeNull(); - const diplomacySummary = - directive.observation.patientZeroGlobalView?.diplomacySummary; - expect(diplomacySummary).toMatchObject({ - eligiblePairCount: 56, - eligiblePairsTruncated: true, - acceptableProposals: [], - acceptableProposalCount: 0, - acceptableProposalsTruncated: false, - leaveAvailableAgentIds: [], - leaveAvailableCount: 0, - leaveAvailableTruncated: false, - blockedCounts: [], - blockerExamples: [], - }); - expect(diplomacySummary?.displayedEligiblePairs).toHaveLength(12); - expect( - new Set( - diplomacySummary?.displayedEligiblePairs.map( - ({ proposerId }) => proposerId, - ), - ).size, - ).toBe(8); - expect(reply.observation.patientZeroGlobalView).toBeNull(); - expect( - JSON.stringify(directive.observation.patientZeroGlobalView), - ).not.toMatch(/provider|credential|pendingDecision|gps|future/i); - if ( - directive.outcome === 'provider-error' || - directive.outcome === 'lost-tick' || - directive.outcome === 'operator-skipped' || - reply.outcome === 'provider-error' || - reply.outcome === 'lost-tick' || - reply.outcome === 'operator-skipped' - ) - throw new Error('Expected completed Patient Zero communication turns.'); - expect(directive.communicationResult).toMatchObject({ - accepted: true, - event: { channel: 'zero', recipientIds: expect.any(Array) }, - }); - expect(reply.communicationResult).toMatchObject({ - accepted: true, - event: { channel: 'direct', recipientId: patientZeroId }, - }); - expect(reply.observation.recentZeroMessages).toHaveLength(1); - expect(seen).toHaveLength(2); - expect(simulation.getSnapshot().experiment.metrics.aggregate).toMatchObject( - { - zeroBroadcastsRequested: 1, - zeroBroadcastsDelivered: 1, - zeroRecipientDeliveries: 7, - uniqueZeroDirectiveRecipients: 7, - directRepliesToPatientZero: 1, - uniquePatientZeroRepliers: 1, - firstZeroDirectiveTurn: 1, - }, - ); - }); - - it('keeps 32-agent Patient Zero diplomacy guidance compact and surfaces a distant free-agent blocker', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - ); - const setup = defaultWorldSetupRequest(); - const roster = generateDeterministicRoster(32, 'pz-diplomacy-bound'); - const patientZeroAgentId = roster[0]!.id; - simulation.applyWorldSetup({ - ...setup, - radius: 12, - communicationRangeKm: 0.1, - roster, - patientZeroAgentId, - modelConfiguration: { - ...setup.modelConfiguration, - globalModelId: 'deterministic-script', - }, - behaviorConfiguration: { - ...setup.behaviorConfiguration, - assignments: assignBehavior( - roster.map(({ id }) => id), - setup.behaviorConfiguration.seed, - 'balanced-random', - ), - }, - }); - const record = await simulation.executeNextTurn(); - const summary = record.observation.patientZeroGlobalView?.diplomacySummary; - expect(summary).toMatchObject({ - eligiblePairCount: 0, - eligiblePairsTruncated: false, - blockedCounts: expect.arrayContaining([ - expect.objectContaining({ reason: 'out-of-range' }), - ]), - blockerExamples: expect.arrayContaining([ - expect.objectContaining({ reason: 'out-of-range' }), - ]), - }); - expect(summary?.displayedEligiblePairs).toHaveLength(0); - expect(summary?.blockerExamples.length).toBeLessThanOrEqual(8); - expect( - new TextEncoder().encode(JSON.stringify(summary)).byteLength, - ).toBeLessThanOrEqual( - PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.serializedUtf8Bytes, - ); - }); - - it('fairly and deterministically rotates sparse diplomacy pairs across ticks', async () => { - const decisions = Array.from({ length: 64 }, () => ({ - worldAction: { type: 'wait' as const }, - summary: 'Wait.', - })); - const createSimulation = () => { - const simulation = service(new ScriptedAgentProvider(decisions)); - const setup = defaultWorldSetupRequest(); - const roster = generateDeterministicRoster(32, 'pz-fair-pairs'); - simulation.applyWorldSetup({ - ...setup, - radius: 12, - communicationRangeKm: 100, - roster, - patientZeroAgentId: roster[0]!.id, - modelConfiguration: { - ...setup.modelConfiguration, - globalModelId: 'deterministic-script', - }, - behaviorConfiguration: { - ...setup.behaviorConfiguration, - assignments: assignBehavior( - roster.map(({ id }) => id), - setup.behaviorConfiguration.seed, - 'balanced-random', - ), - }, - }); - return simulation; - }; - const first = createSimulation(); - const duplicate = createSimulation(); - const firstTick = (await first.executeNextTick()).find( - ({ observation }) => observation.patientZero.isPatientZero, - )?.observation.patientZeroGlobalView?.diplomacySummary; - const duplicateTick = (await duplicate.executeNextTick()).find( - ({ observation }) => observation.patientZero.isPatientZero, - )?.observation.patientZeroGlobalView?.diplomacySummary; - expect(firstTick?.displayedEligiblePairs).toEqual( - duplicateTick?.displayedEligiblePairs, - ); - expect( - new Set( - firstTick?.displayedEligiblePairs.map(({ proposerId }) => proposerId), - ).size, - ).toBe(12); - const secondTick = (await first.executeNextTick()).find( - ({ observation }) => observation.patientZero.isPatientZero, - )?.observation.patientZeroGlobalView?.diplomacySummary; - expect(secondTick?.displayedEligiblePairs).not.toEqual( - firstTick?.displayedEligiblePairs, - ); - expect( - new Set( - secondTick?.displayedEligiblePairs.map(({ proposerId }) => proposerId), - ).size, - ).toBe(12); - }); - - it('reproduces move ordering for identical inputs and varies it across agents', async () => { - const provider = () => - new ScriptedAgentProvider( - Array.from({ length: 8 }, () => ({ - worldAction: { type: 'wait' as const }, - summary: 'Wait.', - })), - ); - const first = service(provider()); - const second = service(provider()); - const firstOrders: string[] = []; - const secondOrders: string[] = []; - for (let index = 0; index < 8; index += 1) { - firstOrders.push( - ( - await first.executeNextTurn() - ).observation.actionAvailability.moveTargetCellIds.join(','), - ); - secondOrders.push( - ( - await second.executeNextTurn() - ).observation.actionAvailability.moveTargetCellIds.join(','), - ); - } - expect(firstOrders).toEqual(secondOrders); - expect(new Set(firstOrders).size).toBeGreaterThan(1); - }); - - it('derives compact action availability from the same authoritative world state', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'infect' }, summary: 'Infect.' }, - ]), - ); - const worldCells = new Set( - simulation.getSnapshot().world.hexes.map(({ cell }) => cell), - ); - const turn = await simulation.executeNextTurn(); - expect(turn.observation.actionAvailability).toMatchObject({ - moveTargetCellIds: turn.observation.adjacentCells.map(({ cell }) => cell), - infect: { available: true }, - capture: { available: false, reason: 'capture-open-cell' }, - wait: { available: true }, - }); - expect(turn.observation.actionAvailability.moveOptions).toHaveLength( - turn.observation.adjacentCells.length, - ); - const legalTargets = gridDisk(turn.observation.currentCell.cell, 1) - .filter((cell) => cell !== turn.observation.currentCell.cell) - .map((cell) => h3CellSchema.parse(cell)) - .filter((cell) => worldCells.has(cell)); - expect( - new Set(turn.observation.actionAvailability.moveTargetCellIds), - ).toEqual(new Set(legalTargets)); - for (const option of turn.observation.actionAvailability.moveOptions) { - expect(option.direction).toBe( - geographicDirectionBetweenCells( - turn.observation.currentCell.cell, - option.targetCell, - ), - ); - } - expect(turn.outcome).toBe('accepted'); - if ( - turn.outcome === 'provider-error' || - turn.outcome === 'lost-tick' || - turn.outcome === 'operator-skipped' - ) - throw new Error('Expected a completed engine decision.'); - expect(turn.worldActionResult.accepted).toBe(true); - }); - - it('uses one bounded automatic repair with the same observation and deadline', async () => { - const calls: Array<{ - observation: AgentObservation; - deadlineAtMs?: number; - feedback?: readonly string[]; - }> = []; - const simulation = service({ - mode: 'scripted-test', - configured: true, - async decide(observation, model, options) { - calls.push({ - observation: structuredClone(observation), - deadlineAtMs: options?.deadlineAtMs, - feedback: options?.validationFeedback, - }); - if (calls.length === 1) - throw new AgentProviderError({ - code: 'invalid-json', - message: 'Invalid decision JSON.', - retryable: true, - model, - validationCodes: ['invalid-json'], - }); - return { - decision: { worldAction: { type: 'wait' }, summary: 'Repaired.' }, - metadata: { - provider: 'scripted-test', - model, - latencyMs: 1, - costCredits: 0, - }, - }; - }, - }); - const turn = await simulation.executeNextTurn(); - expect(calls).toHaveLength(2); - expect(calls[1]!.observation).toEqual(calls[0]!.observation); - expect(calls[1]!.deadlineAtMs).toBe(calls[0]!.deadlineAtMs); - expect(calls[1]!.feedback).toEqual(['invalid-json']); - expect(turn).toMatchObject({ - outcome: 'accepted', - modelAttempts: [{ kind: 'initial' }, { kind: 'automatic-repair' }], - }); - }); - - it('denies an automatic retry when the first unknown-cost call consumed credit exposure', async () => { - let calls = 0; - const simulation = service({ - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide(_observation, model) { - calls += 1; - throw new AgentProviderError({ - code: 'invalid-json', - message: 'Invalid decision JSON.', - retryable: true, - model, - validationCodes: ['invalid-json'], - }); - }, - }); - const setup = defaultWorldSetupRequest(); - simulation.applyWorldSetup({ - ...setup, - modelConfiguration: { - ...setup.modelConfiguration, - globalModelId: 'deterministic-script', - }, - executionLimits: { - version: 'execution-limits-v2', - providerAttemptLimit: null, - creditLimit: '0.01', - reservationCreditsPerAttempt: '0.01', - }, - }); - - await expect(simulation.executeNextTurn()).rejects.toMatchObject({ - code: 'experiment_budget_exhausted', - message: - 'The experiment does not have enough provider-attempt or credit-admission capacity.', - }); - expect(calls).toBe(1); - expect(simulation.getSnapshot().experiment.attemptAccounting).toMatchObject( - { - committedCreditExposure: '0.01', - attemptsWithUnknownCost: 1, - exhaustionReason: 'credit-admission-limit', - }, - ); - }); - - it.each([ - ['valid Retry-After', 2_000, 2_000], - ['missing Retry-After', undefined, 1_500], - ])( - 'backs off for %s before the one automatic 429 retry', - async (_label, retryAfterMs, expectedDelay) => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-08-15T12:00:00.000Z')); - try { - let calls = 0; - const simulation = service({ - mode: 'scripted-test', - configured: true, - async decide(_observation, model) { - calls += 1; - if (calls === 1) - throw new AgentProviderError({ - code: 'provider-http', - message: 'Rate limited.', - retryable: true, - model, - httpStatus: 429, - ...(retryAfterMs === undefined ? {} : { retryAfterMs }), - }); - return { - decision: { - worldAction: { type: 'wait' }, - summary: 'Recovered.', - }, - metadata: { provider: 'scripted-test', model, latencyMs: 1 }, - }; - }, - }); - const turn = simulation.executeNextTurn(); - await vi.advanceTimersByTimeAsync(expectedDelay - 1); - expect(calls).toBe(1); - await vi.advanceTimersByTimeAsync(1); - await expect(turn).resolves.toMatchObject({ outcome: 'accepted' }); - expect(calls).toBe(2); - } finally { - vi.useRealTimers(); - } - }, - ); - - it('does not retry a 429 when the fallback cannot fit the shared deadline', async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-08-15T12:00:00.000Z')); - try { - let calls = 0; - const simulation = service({ - mode: 'scripted-test', - configured: true, - async decide(_observation, model) { - calls += 1; - vi.setSystemTime(new Date('2026-08-15T12:01:14.000Z')); - throw new AgentProviderError({ - code: 'provider-http', - message: 'Rate limited.', - retryable: true, - model, - httpStatus: 429, - }); - }, - }); - await expect(simulation.executeNextTurn()).resolves.toMatchObject({ - outcome: 'provider-error', - modelAttempts: [{ kind: 'initial' }], - }); - expect(calls).toBe(1); - } finally { - vi.useRealTimers(); - } - }); - - it('cancellation interrupts the 429 fallback without starting another call', async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-08-15T12:00:00.000Z')); - try { - let calls = 0; - const simulation = service({ - mode: 'scripted-test', - configured: true, - async decide(_observation, model) { - calls += 1; - throw new AgentProviderError({ - code: 'provider-http', - message: 'Rate limited.', - retryable: true, - model, - httpStatus: 429, - }); - }, - }); - const turn = simulation.executeNextTurn(); - await Promise.resolve(); - simulation.cancelCurrentRequest(); - await expect(turn).rejects.toBeInstanceOf(SimulationTurnCancelledError); - expect(calls).toBe(1); - expect(simulation.getSnapshot()).toMatchObject({ - activeAgentId: null, - pendingFailedTurn: null, - turnNumber: 0, - }); - } finally { - vi.useRealTimers(); - } - }); - - it('applies formal diplomacy independently and exposes authoritative alliance observations', async () => { - const [emberId, rookId] = DEVELOPMENT_AGENT_BLUEPRINTS.slice(0, 2).map( - ({ id }) => agentIdSchema.parse(id), - ); - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'alliance-test', - configured: true, - async decide(observation): Promise { - return { - decision: { - worldAction: { - type: - observation.currentCell.state === 'open' ? 'infect' : 'wait', - }, - communication: { - channel: 'public', - message: 'Formal diplomacy accompanies this action.', - }, - diplomacy: - observation.agentId === emberId - ? observation.actingAllianceId - ? { type: 'leave-alliance' } - : { type: 'propose-alliance', recipientId: rookId! } - : observation.agentId === rookId && - observation.inboundAllianceProposals[0] - ? { - type: 'accept-alliance', - proposalId: observation.inboundAllianceProposals[0].id, - } - : undefined, - summary: 'Exercise all independent components.', - }, - metadata: { - provider: 'scripted-test', - model: 'alliance-test', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = service(provider); - expect( - simulation - .generateExperimentExport(exportRequest('minimal')) - .currentTerritory?.map(({ effectiveColor }) => effectiveColor), - ).toEqual(DEVELOPMENT_AGENT_BLUEPRINTS.map(() => NEUTRAL_AGENT_COLOR)); - const proposed = await simulation.executeNextTurn(); - const formed = await simulation.executeNextTurn(); - expect(proposed).toMatchObject({ - diplomacyResult: { - accepted: true, - events: [{ type: 'alliance-proposed' }], - }, - }); - expect(formed).toMatchObject({ - diplomacyResult: { - accepted: true, - events: [{ type: 'alliance-formed' }], - }, - }); - const snapshot = simulation.getSnapshot(); - expect(snapshot.world.alliances).toHaveLength(1); - expect(snapshot.experiment.currentAlliances[0]).toMatchObject({ - totalControlledCellCount: 2, - members: [{ agentId: emberId }, { agentId: rookId }], - }); - expect( - snapshot.experiment.currentTerritory - .slice(0, 2) - .map(({ effectiveColor }) => effectiveColor), - ).toEqual(['#0072B2', '#0072B2']); - expect( - simulation - .generateExperimentExport(exportRequest('minimal')) - .currentTerritory?.slice(0, 3) - .map(({ effectiveColor }) => effectiveColor), - ).toEqual(['#0072B2', '#0072B2', NEUTRAL_AGENT_COLOR]); - expect(formed.observation.inboundAllianceProposals).toHaveLength(1); - expect(snapshot.experiment.metrics.aggregate).toMatchObject({ - proposalsCreated: 1, - alliancesFormed: 1, - alliancesJoined: 2, - }); - for (let index = 0; index < 6; index += 1) - await simulation.executeNextTurn(); - const left = await simulation.executeNextTurn(); - expect(left).toMatchObject({ - diplomacyResult: { - accepted: true, - events: expect.arrayContaining([ - expect.objectContaining({ type: 'agent-left-alliance' }), - expect.objectContaining({ type: 'alliance-dissolved' }), - ]), - }, - }); - expect( - simulation - .generateExperimentExport(exportRequest('minimal')) - .currentTerritory?.slice(0, 2) - .map(({ effectiveColor }) => effectiveColor), - ).toEqual([NEUTRAL_AGENT_COLOR, NEUTRAL_AGENT_COLOR]); - }); - - it('keeps an exact eight-agent round robin through 200 completed turns', async () => { - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'two-hundred-turn-test', - configured: true, - async decide(): Promise { - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait.' }, - metadata: { - provider: 'scripted-test', - model: 'two-hundred-turn-test', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = service(provider); - for (let turn = 0; turn < 200; turn += 1) - await simulation.executeNextTurn(); - const snapshot = simulation.getSnapshot(); - expect(snapshot.experiment.totalCompletedTurns).toBe(200); - expect( - snapshot.experiment.metrics.byAgent.map( - ({ metrics }) => metrics.totalTurns, - ), - ).toEqual(Array(8).fill(25)); - }); - - it('counts rejected diplomacy by sanitized type and reason without cancelling valid siblings', async () => { - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'malformed-diplomacy-test', - configured: true, - async decide(): Promise { - return { - decision: { - worldAction: { type: 'infect' }, - communication: { channel: 'public', message: 'Valid sibling.' }, - diplomacy: { type: 'propose-alliance', recipientId: 'unsafe-id' }, - summary: 'Reject only diplomacy.', - }, - metadata: { - provider: 'scripted-test', - model: 'malformed-diplomacy-test', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = service(provider); - const turn = await simulation.executeNextTurn(); - expect(turn).toMatchObject({ - worldActionResult: { accepted: true }, - communicationResult: { accepted: true }, - diplomacyResult: { - accepted: false, - reason: 'invalid-diplomacy', - attempt: { type: 'propose-alliance', recipientId: null }, - }, - }); - expect( - simulation.getSnapshot().experiment.metrics.aggregate.diplomacyRejections, - ).toEqual([ - { type: 'propose-alliance', reason: 'invalid-diplomacy', count: 1 }, - ]); - }); - - it('derives authoritative territory, bounded control history, capture metrics, and victim-aware exports', async () => { - const emberId = agentIdSchema.parse(DEVELOPMENT_AGENT_BLUEPRINTS[0].id); - const rookId = agentIdSchema.parse(DEVELOPMENT_AGENT_BLUEPRINTS[1].id); - let targetCell: AgentObservation['currentCell']['cell'] | undefined; - let emberDeparted = false; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'capture-scenario', - configured: true, - async decide(observation): Promise { - let worldAction: ProviderDecision['decision']['worldAction']; - if (observation.agentId === emberId && !targetCell) { - targetCell = observation.currentCell.cell; - worldAction = { type: 'infect' }; - } else if (observation.agentId === emberId && !emberDeparted) { - emberDeparted = true; - worldAction = { - type: 'move', - targetCell: observation.adjacentCells[0]!.cell, - }; - } else if (observation.agentId === rookId) { - if ( - observation.currentCell.cell === targetCell && - observation.captureEligibility.eligible - ) { - worldAction = { type: 'capture' }; - } else if (observation.currentCell.cell === targetCell) { - worldAction = { type: 'wait' }; - } else { - const target = targetCell!; - const next = observation.adjacentCells.toSorted( - (left, right) => - gridDistance(left.cell, target) - - gridDistance(right.cell, target) || - left.cell.localeCompare(right.cell), - )[0]!; - worldAction = { type: 'move', targetCell: next.cell }; - } - } else { - worldAction = { type: 'wait' }; - } - return { - decision: { worldAction, summary: 'Deterministic contest.' }, - metadata: { - provider: 'scripted-test', - model: 'capture-scenario', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = service(provider); - let capture: AgentTurnRecord | undefined; - for (let index = 0; index < 31 && !capture; index += 1) { - const turn = await simulation.executeNextTurn(); - if ( - turn.outcome === 'accepted' && - turn.worldActionResult.event.type === 'hex-captured' - ) - capture = turn; - } - expect(capture).toMatchObject({ - agentId: rookId, - worldAction: { type: 'capture' }, - worldActionResult: { - event: { - controllerAgentId: rookId, - previousControllerAgentId: emberId, - cell: targetCell, - }, - }, - }); - if (!capture || capture.outcome !== 'accepted') - throw new Error('Expected a successful capture fixture.'); - expect(capture.observation.currentCell).toMatchObject({ - state: 'infected', - controllerAgentId: emberId, - }); - expect(capture.observation.captureEligibility).toEqual({ eligible: true }); - expect(capture.observation.adjacentCells).toEqual( - expect.arrayContaining([ - expect.objectContaining({ controllerAgentId: null }), - ]), - ); - expect(capture.observation.territoryScoreboard).toHaveLength(8); - expect( - capture.observation.territoryScoreboard.reduce( - (sum, { controlledCellCount }) => sum + controlledCellCount, - 0, - ), - ).toBe(1); - const snapshot = simulation.getSnapshot(); - expect( - snapshot.world.hexes.filter(({ state }) => state === 'infected'), - ).toHaveLength(1); - expect( - snapshot.world.hexes.find(({ cell }) => cell === targetCell), - ).toMatchObject({ state: 'infected', controllerAgentId: rookId }); - expect( - snapshot.experiment.currentTerritory.reduce( - (sum, { controlledCellCount }) => sum + controlledCellCount, - 0, - ), - ).toBe(1); - expect( - snapshot.experiment.currentTerritory.find( - ({ agentId }) => agentId === rookId, - )?.controlledCellCount, - ).toBe(1); - expect( - snapshot.experiment.metrics.byAgent.find( - ({ agentId }) => agentId === emberId, - )?.metrics, - ).toMatchObject({ - territoryGainedThroughInfection: 1, - territoryLostThroughCapture: 1, - }); - expect( - snapshot.experiment.metrics.byAgent.find( - ({ agentId }) => agentId === rookId, - )?.metrics, - ).toMatchObject({ - requestedCaptures: 1, - successfulCaptures: 1, - territoryGainedThroughCapture: 1, - }); - - const subsequent: AgentTurnRecord[] = []; - for (let index = 0; index < 8; index += 1) - subsequent.push(await simulation.executeNextTurn()); - const emberObservation = subsequent.find( - ({ agentId }) => agentId === emberId, - )?.observation; - const rookObservation = subsequent.find( - ({ agentId }) => agentId === rookId, - )?.observation; - expect(emberObservation?.recentControlChanges).toMatchObject([ - { direction: 'lost', otherAgentId: rookId, cell: targetCell }, - ]); - expect(rookObservation?.recentControlChanges).toMatchObject([ - { direction: 'gained', otherAgentId: emberId, cell: targetCell }, - ]); - expect( - subsequent - .filter(({ agentId }) => agentId !== emberId && agentId !== rookId) - .every( - ({ observation }) => observation.recentControlChanges.length === 0, - ), - ).toBe(true); - - const victimExport = simulation.generateExperimentExport({ - agents: { mode: 'selected', agentIds: [emberId] }, - turns: { mode: 'entire-retained' }, - outcomes: ['accepted'], - actions: ['capture'], - level: 'minimal', - }); - expect(victimExport.schemaVersion).toBe(11); - expect(victimExport.turns).toHaveLength(0); - expect(victimExport.selection).toMatchObject({ - matchingTurnCount: 0, - matchingControlChangeCount: 1, - }); - expect(victimExport.controlChanges).toMatchObject([ - { - controllerAgentId: rookId, - previousControllerAgentId: emberId, - }, - ]); - expect(victimExport.metrics?.byAgent[0]?.metrics).toMatchObject({ - totalTurns: 0, - territoryLostThroughCapture: 1, - }); - expect(victimExport.currentTerritory).toHaveLength(8); - const unrelatedAgentId = agentIdSchema.parse( - DEVELOPMENT_AGENT_BLUEPRINTS[2].id, - ); - expect( - simulation.generateExperimentExport({ - ...exportRequest('minimal'), - agents: { mode: 'selected', agentIds: [unrelatedAgentId] }, - outcomes: ['accepted'], - actions: ['capture'], - }).controlChanges, - ).toEqual([]); - }); - - it('records controller-present rejection without control mutation or gain/loss metrics', async () => { - const emberId = agentIdSchema.parse(DEVELOPMENT_AGENT_BLUEPRINTS[0].id); - const rookId = agentIdSchema.parse(DEVELOPMENT_AGENT_BLUEPRINTS[1].id); - let targetCell: AgentObservation['currentCell']['cell'] | undefined; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'defended-capture-scenario', - configured: true, - async decide(observation): Promise { - let worldAction: ProviderDecision['decision']['worldAction']; - if (observation.agentId === emberId && !targetCell) { - targetCell = observation.currentCell.cell; - worldAction = { type: 'infect' }; - } else if (observation.agentId === rookId) { - if (observation.currentCell.cell === targetCell) { - worldAction = { type: 'capture' }; - } else { - const next = observation.adjacentCells.toSorted( - (left, right) => - gridDistance(left.cell, targetCell!) - - gridDistance(right.cell, targetCell!) || - left.cell.localeCompare(right.cell), - )[0]!; - worldAction = { type: 'move', targetCell: next.cell }; - } - } else { - worldAction = { type: 'wait' }; - } - return { - decision: { worldAction, summary: 'Test defended capture.' }, - metadata: { - provider: 'scripted-test', - model: 'defended-capture-scenario', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = service(provider); - let rejected: AgentTurnRecord | undefined; - for (let index = 0; index < 31 && !rejected; index += 1) { - const turn = await simulation.executeNextTurn(); - if ( - turn.outcome === 'rejected' && - turn.worldActionResult.reason === 'controller-present' - ) - rejected = turn; - } - expect(rejected).toMatchObject({ - agentId: rookId, - worldAction: { type: 'capture' }, - observation: { - captureEligibility: { - eligible: false, - blockedReason: 'controller-present', - }, - }, - }); - const snapshot = simulation.getSnapshot(); - expect( - snapshot.world.hexes.find(({ cell }) => cell === targetCell), - ).toEqual(expect.objectContaining({ controllerAgentId: emberId })); - expect(snapshot.world.events).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ type: 'hex-captured' }), - ]), - ); - expect(snapshot.experiment.metrics.aggregate).toMatchObject({ - requestedCaptures: 1, - successfulCaptures: 0, - territoryGainedThroughCapture: 0, - territoryLostThroughCapture: 0, - }); - const exported = simulation.generateExperimentExport({ - ...exportRequest('standard'), - agents: { mode: 'selected', agentIds: [rookId] }, - outcomes: ['rejected'], - actions: ['capture'], - }); - expect(exported.schemaVersion).toBe(11); - const behavior = exported.turns[0]!.behavior!; - expect( - exported.metrics!.byPersonality.find( - ({ personalityId }) => personalityId === behavior.personalityId, - )?.metrics.totalTurns, - ).toBe(1); - expect( - exported.metrics!.byStrategy.find( - ({ strategyId }) => strategyId === behavior.strategyId, - )?.metrics.rejected, - ).toBe(1); - expect( - exported.metrics!.byBehaviorCombination.find( - (entry) => - entry.personalityId === behavior.personalityId && - entry.strategyId === behavior.strategyId, - )?.metrics.modelCalls, - ).toBe(exported.metrics!.aggregate.modelCalls); - expect( - exported.metrics!.byPersonality.reduce( - (sum, entry) => sum + entry.metrics.totalTurns, - 0, - ), - ).toBe(exported.metrics!.aggregate.totalTurns); - expect(exported.turns).toMatchObject([ - { - outcome: 'rejected', - worldActionResult: { - accepted: false, - reason: 'controller-present', - }, - observation: { - captureEligibility: { - eligible: false, - blockedReason: 'controller-present', - }, - }, - }, - ]); - expect(exported.controlChanges).toEqual([]); - }); - - it('resets to the exact deterministic eight-agent starting world', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'infect' }, summary: 'Infect.' }, - ]), - ); - const initial = simulation.getSnapshot(); - await simulation.executeNextTurn(); - const reset = simulation.reset(); - expect(reset.world).toEqual(initial.world); - expect(reset.experiment.id).not.toBe(initial.experiment.id); - expect(reset.experiment.totalCompletedTurns).toBe(0); - expect(initial.world.agents).toHaveLength(8); - expect(initial.world.hexes).toHaveLength(127); - }); - - it('retains complete experiment records independently of the 120-turn browser snapshot', async () => { - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'retention-test', - configured: true, - async decide() { - return { - decision: { - worldAction: { type: 'wait' as const }, - summary: 'Wait.', - }, - metadata: { - provider: 'scripted-test' as const, - model: 'retention-test', - latencyMs: 1, - costCredits: 0, - }, - }; - }, - }; - const simulation = new SimulationService({ - provider, - now, - createEventId: deterministicEventIdGenerator(), - experimentRetentionLimit: 125, - }); - for (let index = 0; index < 125; index += 1) - await simulation.executeNextTurn(); - const snapshot = simulation.getSnapshot(); - expect(snapshot.turns).toHaveLength(120); - expect(snapshot.experiment).toMatchObject({ - totalCompletedTurns: 125, - retainedTurns: 125, - droppedRecords: 0, - complete: true, - }); - expect( - simulation.generateExperimentExport(exportRequest('full-safe')).turns, - ).toHaveLength(125); - }); - - it('reports configurable experiment truncation and absolute retained bounds', async () => { - const simulation = new SimulationService({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: '1' }, - { worldAction: { type: 'wait' }, summary: '2' }, - { worldAction: { type: 'wait' }, summary: '3' }, - ]), - now, - createEventId: deterministicEventIdGenerator(), - experimentRetentionLimit: 2, - }); - await simulation.executeNextTurn(); - await simulation.executeNextTurn(); - await simulation.executeNextTurn(); - expect(simulation.getSnapshot().experiment).toMatchObject({ - retainedTurns: 2, - firstRetainedTurn: 2, - lastRetainedTurn: 3, - droppedRecords: 1, - complete: false, - }); - const preview = simulation.previewExperimentExport({ - ...exportRequest('minimal'), - turns: { mode: 'range', fromTurn: 1, toTurn: 3 }, - }); - expect(preview.retention.requestedRangeExtendsBeyondRetention).toBe(true); - }); - - it('estimates the selected Compact or Pretty serialization and defaults to Compact', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - ); - await simulation.executeNextTurn(); - const compact = simulation.previewExperimentExport( - exportRequest('minimal'), - ); - const pretty = simulation.previewExperimentExport({ - ...exportRequest('minimal'), - serialization: 'pretty', - }); - expect(compact.serializedUtf8Bytes).toBeLessThan( - pretty.serializedUtf8Bytes, - ); - expect(compact.approximateAiInputTokens).toBeLessThan( - pretty.approximateAiInputTokens, - ); - expect( - simulation.generateExperimentExport(exportRequest('minimal')).filters - .serialization, - ).toBe('compact'); - }); - - it('aggregates charged cost as exact decimal input without JSON artifacts', async () => { - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'decimal-cost-test', - configured: true, - async decide() { - return { - decision: { - worldAction: { type: 'wait' as const }, - summary: 'Wait.', - }, - metadata: { - provider: 'scripted-test' as const, - model: 'decimal-cost-test', - latencyMs: 0, - costCredits: 0.14064472125, - }, - }; - }, - }; - const simulation = service(provider); - await simulation.executeNextTurn(); - await simulation.executeNextTurn(); - await simulation.executeNextTurn(); - const document = simulation.generateExperimentExport( - exportRequest('minimal'), - ); - expect(document.metrics?.aggregate.knownCostCredits).toBe(0.42193416375); - expect(serializeExperimentExport(document)).toContain( - '"knownCostCredits":0.42193416375', - ); - expect(serializeExperimentExport(document)).not.toContain( - '0.4219341637499998', - ); - }); - - it('records immutable personality configuration history and clears it on reset', () => { - let sequence = 0; - const simulation = new SimulationService({ - provider: new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - now, - createEventId: deterministicEventIdGenerator(), - createExperimentId: () => - `aaaaaaaa-aaaa-4aaa-8aaa-${String(++sequence).padStart(12, '0')}`, - }); - const before = simulation.getSnapshot(); - const agent = before.world.agents[0]!; - simulation.updateAgentPersonality(agent.id, 'Custom immutable edit.'); - simulation.restoreDefaultPersonalities(); - const full = simulation.generateExperimentExport( - exportRequest('full-safe'), - ); - expect(full.configurationEvents).toMatchObject([ - { - operation: 'custom-edit', - previousPersonality: agent.personality, - newPersonality: 'Custom immutable edit.', - }, - { - operation: 'restore-default', - previousPersonality: 'Custom immutable edit.', - newPersonality: agent.personality, - }, - ]); - const captured = structuredClone(full.configurationEvents); - simulation.updateAgentPersonality(agent.id, 'Another edit.'); - expect(full.configurationEvents).toEqual(captured); const reset = simulation.reset(); - expect(reset.experiment.id).not.toBe(before.experiment.id); - expect( - simulation.generateExperimentExport(exportRequest('full-safe')) - .configurationEvents, - ).toEqual([]); - }); - - it('filters agents, latest/ranges, outcomes and actions chronologically with subset metrics', async () => { - let call = 0; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'filter-test', - configured: true, - async decide(observation) { - call += 1; - const worldAction = - call === 1 - ? { type: 'infect' as const } - : call === 2 - ? { - type: 'move' as const, - targetCell: observation.adjacentCells[0]!.cell, - } - : { type: 'wait' as const }; - return { - decision: { worldAction, summary: 'Safe summary.' }, - metadata: { - provider: 'scripted-test', - model: 'filter-test', - latencyMs: call, - promptTokens: call, - completionTokens: call, - totalTokens: call * 2, - costCredits: call === 3 ? undefined : 0.00000001, - }, - }; - }, - }; - const simulation = service(provider); - await simulation.executeNextTurn(); - await simulation.executeNextTurn(); - await simulation.executeNextTurn(); - const agents = simulation.getSnapshot().world.agents; - const selected = [agents[0]!.id, agents[1]!.id]; - const document = simulation.generateExperimentExport({ - ...exportRequest('standard'), - agents: { mode: 'selected', agentIds: selected }, - turns: { mode: 'latest', count: 10 }, - outcomes: ['accepted'], - actions: ['move', 'infect'], - }); - expect(document.selection.selectedAgentIds).toEqual(selected); - expect(document.turns.map(({ turnNumber }) => turnNumber)).toEqual([1, 2]); - expect(document.metrics?.aggregate).toMatchObject({ - totalTurns: 2, - requestedMoves: 1, - requestedInfections: 1, - acceptedMovements: 1, - successfullyInfectedCells: 1, - knownCostCredits: 0.00000002, - attemptsWithUnknownCost: 0, - turnsWithUnknownCost: 0, - }); - expect(document.metrics?.aggregate.uniqueVisitedCells).toBeGreaterThan(1); - const oneAgent = simulation.generateExperimentExport({ - ...exportRequest('minimal'), - agents: { mode: 'selected', agentIds: [agents[2]!.id] }, - turns: { mode: 'range', fromTurn: 3, toTurn: 3 }, - }); - expect(oneAgent.selection).toMatchObject({ - selectedAgentIds: [agents[2]!.id], - matchingTurnCount: 1, - firstMatchingTurn: 3, - lastMatchingTurn: 3, - }); - expect( - simulation.generateExperimentExport(exportRequest('minimal')).metrics - ?.aggregate, - ).toMatchObject({ - knownCostCredits: 0.00000002, - attemptsWithUnknownCost: 1, - turnsWithUnknownCost: 1, - }); - }); - - it('keeps Full safe world snapshots state-only and scopes canonical events to the export selection', async () => { - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'full-safe-event-scope-test', - configured: true, - async decide() { - return { - decision: { - worldAction: { type: 'wait' as const }, - summary: 'Wait.', - }, - metadata: { - provider: 'scripted-test' as const, - model: 'full-safe-event-scope-test', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = service(provider); - for (let index = 0; index < 7; index += 1) - await simulation.executeNextTurn(); - - const agents = simulation.getSnapshot().world.agents; - const selectedAgent = agents[0]!; - const oneAgent = simulation.generateExperimentExport({ - ...exportRequest('full-safe'), - agents: { mode: 'selected', agentIds: [selectedAgent.id] }, - turns: { mode: 'range', fromTurn: 1, toTurn: 6 }, - outcomes: ['accepted'], - actions: ['wait'], - }); - - expect(oneAgent.initialWorld?.agents).toHaveLength(8); - expect(oneAgent.initialWorld?.hexes).toHaveLength(127); - expect(oneAgent.currentWorld?.agents).toHaveLength(8); - expect(oneAgent.currentWorld?.hexes).toHaveLength(127); - expect(oneAgent.initialWorld).not.toHaveProperty('events'); - expect(oneAgent.currentWorld).not.toHaveProperty('events'); - expect(oneAgent.worldEvents).toHaveLength(1); - expect( - oneAgent.worldEvents?.every( - (event) => 'agentId' in event && event.agentId === selectedAgent.id, - ), - ).toBe(true); - expect(oneAgent.turns.map(({ turnNumber }) => turnNumber)).toEqual([1]); - - const allAgents = simulation.generateExperimentExport( - exportRequest('full-safe'), - ); - expect(allAgents.selection.selectedAgentIds).toEqual( - agents.map(({ id }) => id), - ); - expect(allAgents.turns).toHaveLength(7); - expect(allAgents.worldEvents).toHaveLength(7); - expect(allAgents.initialWorld).not.toHaveProperty('events'); - expect(allAgents.currentWorld).not.toHaveProperty('events'); - }); - - it('exports accepted communications for either participant without importing unrelated or rejected messages', async () => { - const initial = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'placeholder' }, - ]), - ).getSnapshot(); - const [sender, recipient] = initial.world.agents; - let call = 0; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'message-export-test', - configured: true, - async decide(observation): Promise { - call += 1; - const communication = - call === 1 - ? { - channel: 'direct' as const, - recipientId: recipient!.id, - message: 'Inbound selection proof.', - } - : call === 2 - ? { - channel: 'public' as const, - message: 'Selected-author public message.', - } - : call === 3 - ? { - channel: 'direct' as const, - recipientId: observation.nearbyAgents.find( - ({ id, distance }) => - distance <= 3 && - id !== sender!.id && - id !== recipient!.id, - )!.id, - message: 'Unrelated communication.', - } - : call === 4 - ? { - channel: 'direct' as const, - recipientId: observation.agentId, - message: 'Rejected self message.', - } - : call === 5 - ? { - channel: 'public' as const, - message: 'Unselected-author public message.', - } - : undefined; - return { - decision: { - worldAction: { type: 'wait' }, - ...(communication ? { communication } : {}), - summary: 'Test export.', - }, - metadata: { - provider: 'scripted-test', - model: 'message-export-test', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = service(provider); - for (let index = 0; index < 5; index += 1) - await simulation.executeNextTurn(); - - const inboundRequest = { - ...exportRequest('minimal'), - agents: { mode: 'selected', agentIds: [recipient!.id] }, - outcomes: ['accepted'], - actions: ['wait'], - communications: { channel: 'direct', status: 'accepted' }, - } as const; - const inbound = simulation.generateExperimentExport(inboundRequest); - expect(inbound.turns.map(({ turnNumber }) => turnNumber)).toEqual([2]); - expect(inbound.communications).toMatchObject([ - { - originatingTurn: 1, - agentId: sender!.id, - recipientId: recipient!.id, - message: 'Inbound selection proof.', - }, - ]); - expect(inbound.metrics?.aggregate).toMatchObject({ - directMessagesRequested: 0, - directMessagesDelivered: 0, - directMessagesSent: 0, - directMessagesReceived: 1, - }); - const inboundPreview = simulation.previewExperimentExport(inboundRequest); - const inboundBytes = new TextEncoder().encode( - serializeExperimentExport(inbound), - ).byteLength; - expect(inboundPreview).toMatchObject({ - matchingTurnCount: 1, - matchingCommunicationCount: 1, - serializedUtf8Bytes: inboundBytes, - approximateAiInputTokens: Math.ceil(inboundBytes / 4), - }); - const multiAgent = simulation.generateExperimentExport({ - ...inboundRequest, - agents: { - mode: 'selected', - agentIds: [sender!.id, recipient!.id], - }, - communications: { channel: 'all', status: 'all' }, - }); - expect(multiAgent.communications).toMatchObject([ - { channel: 'direct', message: 'Inbound selection proof.' }, - { channel: 'public', message: 'Selected-author public message.' }, - ]); - const allAgent = simulation.generateExperimentExport({ - ...exportRequest('minimal'), - communications: { channel: 'all', status: 'all' }, - }); - expect(allAgent.communications).toHaveLength(5); - expect( - simulation.generateExperimentExport({ - ...inboundRequest, - communications: { channel: 'public', status: 'all' }, - }).communications, - ).toMatchObject([ - { - originatingTurn: 2, - agentId: recipient!.id, - channel: 'public', - message: 'Selected-author public message.', - }, - ]); - for (const filteredRequest of [ - { - ...inboundRequest, - communications: { channel: 'all', status: 'rejected' }, - }, - { - ...inboundRequest, - turns: { mode: 'range', fromTurn: 2, toTurn: 4 }, - }, - ]) - expect( - simulation.generateExperimentExport(filteredRequest).communications, - ).toEqual([]); - const senderFull = simulation.generateExperimentExport({ - ...inboundRequest, - level: 'full-safe', - agents: { mode: 'selected', agentIds: [sender!.id] }, - actions: ['wait'], - }); - expect(senderFull.turns).toHaveLength(1); - expect(senderFull.turns[0]?.communicationResult).toMatchObject({ - accepted: true, - event: { type: 'direct-message-sent' }, - }); - expect(senderFull.communications).toHaveLength(1); - expect(senderFull.worldEvents).toMatchObject([{ type: 'agent-waited' }]); - expect(senderFull.initialWorld).not.toHaveProperty('events'); - expect(senderFull.currentWorld).not.toHaveProperty('events'); - - const rejectedSender = initial.world.agents[3]!; - const rejected = simulation.generateExperimentExport({ - ...exportRequest('full-safe'), - agents: { mode: 'selected', agentIds: [rejectedSender.id] }, - outcomes: ['accepted'], - actions: ['wait'], - communications: { channel: 'direct', status: 'rejected' }, - }); - expect(rejected.turns).toHaveLength(1); - expect(rejected.turns[0]).toMatchObject({ - outcome: 'accepted', - communicationResult: { accepted: false, reason: 'self-message' }, - }); - expect(rejected.communications).toMatchObject([ - { status: 'rejected', rejectionReason: 'self-message' }, - ]); - expect(rejected.worldEvents).toMatchObject([{ type: 'agent-waited' }]); - expect(rejected.metrics?.aggregate).toMatchObject({ - directMessagesRequested: 1, - directMessagesDelivered: 0, - directMessagesRejected: 1, - }); - }); - - it('produces predictable Minimal, Standard, Full safe and Custom omissions without mutation', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - ); - await simulation.executeNextTurn(); - const before = simulation.getSnapshot(); - expect(() => - simulation.generateExperimentExport({ - ...exportRequest('minimal'), - outcomes: [], - }), - ).toThrow(/invalid/i); - expect(simulation.getSnapshot()).toEqual(before); - const minimal = simulation.generateExperimentExport( - exportRequest('minimal'), - ); - const standard = simulation.generateExperimentExport( - exportRequest('standard'), - ); - const full = simulation.generateExperimentExport( - exportRequest('full-safe'), - ); - const custom = simulation.generateExperimentExport({ - ...exportRequest('custom'), - custom: { - turnObservations: false, - personalityTextHistory: false, - nearbyAgents: false, - recentEvents: false, - recentPublicMessages: false, - recentDirectMessages: false, - recentControlChanges: false, - validationDetails: false, - resultingEvents: false, - providerUsageMetadata: false, - initialWorldState: false, - currentWorldState: false, - computedMetrics: false, - communications: false, - controlChanges: false, - }, - }); - expect(minimal.schemaVersion).toBe(11); - expect(minimal.providerAttempts).toHaveLength(1); - expect(minimal.selection.matchingProviderAttemptCount).toBe(1); - expect( - experimentExportDocumentSchema.safeParse({ - ...minimal, - providerAttempts: [ - ...minimal.providerAttempts!, - minimal.providerAttempts![0], - ], - selection: { - ...minimal.selection, - matchingProviderAttemptCount: 2, - }, - }).success, - ).toBe(false); - expect( - experimentExportDocumentSchema.safeParse({ - ...minimal, - selection: { - ...minimal.selection, - matchingProviderAttemptCount: 0, - }, - }).success, - ).toBe(false); - expect( - experimentExportDocumentSchema.safeParse({ - ...minimal, - attemptRetention: { - ...minimal.attemptRetention!, - totalStartedAttempts: 2, - }, - }).success, - ).toBe(false); - expect( - experimentExportDocumentSchema.safeParse({ - ...minimal, - schemaVersion: 2, - }).success, - ).toBe(false); - expect(minimal.turns[0]).not.toHaveProperty('observation'); - expect(standard.turns[0]).toHaveProperty('observation'); - expect(full).toHaveProperty('initialWorld'); - expect(full).toHaveProperty('configurationEvents'); - expect(full.initialWorld).not.toHaveProperty('events'); - expect(full.currentWorld).not.toHaveProperty('events'); - expect(full.worldEvents).toEqual([ - expect.objectContaining({ agentId: before.world.agents[0]!.id }), - ]); - expect(minimal.communications).toEqual([]); - expect(standard.communications).toEqual([]); - expect(full.communications).toEqual([]); - expect(custom).not.toHaveProperty('communications'); - expect(custom).not.toHaveProperty('controlChanges'); - expect(custom).not.toHaveProperty('metrics'); - expect(custom).not.toHaveProperty('simulatedPlayerMetrics'); - expect(minimal).toHaveProperty('simulatedPlayerMetrics'); - expect( - experimentExportDocumentSchema.safeParse({ - ...custom, - simulatedPlayerMetrics: { - movements: 0, - cellsDisinfected: 0, - blockedDisinfections: 0, - }, - }).success, - ).toBe(false); - expect(custom.turns[0]).not.toHaveProperty('provider'); - minimal.turns[0]!.outcome = 'rejected'; - expect( - simulation.generateExperimentExport(exportRequest('minimal')).turns[0] - ?.outcome, - ).toBe('accepted'); - expect(simulation.getSnapshot()).toEqual(before); - }); - - it('calls the provider exactly once per turn in round-robin order', async () => { - const seen: AgentObservation[] = []; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'recording-test', - configured: true, - async decide(observation): Promise { - seen.push(observation); - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait.' }, - metadata: { - provider: 'scripted-test', - model: 'recording-test', - latencyMs: 0, - }, - }; - }, - }; - const simulation = service(provider); - const order = simulation.getSnapshot().world.agents.map(({ id }) => id); - for (let index = 0; index < 9; index += 1) - await simulation.executeNextTurn(); - expect(seen).toHaveLength(9); - expect(seen.map(({ agentId }) => agentId)).toEqual([...order, order[0]]); - }); - - it('keeps total turn numbering and round robin independent of retained history', async () => { - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'long-running-test', - configured: true, - async decide(): Promise { - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait.' }, - metadata: { - provider: 'scripted-test', - model: 'long-running-test', - latencyMs: 0, - }, - }; - }, - }; - const simulation = service(provider); - const agentOrder = simulation - .getSnapshot() - .world.agents.map(({ id }) => id); - - for (let index = 0; index < 125; index += 1) { - await simulation.executeNextTurn(); - } - - const snapshot = simulation.getSnapshot(); - const retainedNumbers = snapshot.turns.map(({ turnNumber }) => turnNumber); - expect(snapshot.turnNumber).toBe(125); - expect(snapshot.turns).toHaveLength(120); - expect(retainedNumbers).toEqual( - Array.from({ length: 120 }, (_, index) => index + 6), - ); - expect(new Set(retainedNumbers).size).toBe(120); - expect(snapshot.turns.map(({ agentId }) => agentId)).toEqual( - snapshot.turns.map( - ({ turnNumber }) => agentOrder[(turnNumber - 1) % agentOrder.length], - ), - ); - expect(snapshot.nextAgentId).toBe(agentOrder[125 % agentOrder.length]); - }); - - it('builds each observation from the latest authoritative world state', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'infect' }, summary: 'Infect.' }, - { worldAction: { type: 'wait' }, summary: 'Observe.' }, - ]), - ); - await simulation.executeNextTurn(); - const second = await simulation.executeNextTurn(); - expect(second.observation.recentEvents).toHaveLength(1); - expect(second.observation.recentEvents[0]?.type).toBe('hex-infected'); - }); - - it('applies an infection and public message from the same provider decision', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { - worldAction: { type: 'infect' }, - communication: { - channel: 'public', - message: ' The center is claimed. ', - }, - summary: 'Claim and announce.', - }, - { worldAction: { type: 'wait' }, summary: 'Observe.' }, - ]), - ); - const first = await simulation.executeNextTurn(); - expect(first).toMatchObject({ - outcome: 'accepted', - worldActionResult: { event: { type: 'hex-infected' } }, - communicationResult: { - accepted: true, - event: { - type: 'public-message-sent', - message: 'The center is claimed.', - }, - }, - }); - expect( - simulation.getSnapshot().world.events.map(({ type }) => type), - ).toEqual(['hex-infected', 'public-message-sent']); - const second = await simulation.executeNextTurn(); - expect(second.observation.recentPublicMessages).toMatchObject([ - { senderId: first.agentId, message: 'The center is claimed.' }, - ]); - }); - - it('preserves accepted communication when the world action is rejected', async () => { - const initial = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'placeholder' }, - ]), - ).getSnapshot(); - const [sender, recipient] = initial.world.agents; - for (const communication of [ - { channel: 'public' as const, message: 'Still speaking.' }, - { - channel: 'direct' as const, - recipientId: recipient!.id, - message: 'Nearby despite the bad move.', - }, - ]) { - const simulation = service( - new ScriptedAgentProvider([ - { - worldAction: { - type: 'move', - targetCell: h3CellSchema.parse('8928308280fffff'), - }, - communication, - summary: 'Try both.', - }, - ]), - ); - const turn = await simulation.executeNextTurn(); - expect(turn).toMatchObject({ - agentId: sender!.id, - outcome: 'rejected', - worldActionResult: { accepted: false }, - communicationResult: { accepted: true }, - }); - expect(simulation.getSnapshot().world.events).toHaveLength(1); - } - }); - - it('rejects an oversized communication without cancelling a valid world action', async () => { - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'invalid-communication-test', - configured: true, - async decide(): Promise { - return { - decision: { - worldAction: { type: 'infect' }, - communication: { - channel: 'public', - message: 'x'.repeat(281), - }, - summary: 'Apply the valid component.', - }, - metadata: { - provider: 'scripted-test', - model: 'invalid-communication-test', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = service(provider); - const turn = await simulation.executeNextTurn(); - expect(turn).toMatchObject({ - outcome: 'accepted', - worldActionResult: { event: { type: 'hex-infected' } }, - communicationResult: { - accepted: false, - reason: 'invalid-communication', - attempt: { channel: 'public' }, - }, - }); - if ( - turn.outcome === 'provider-error' || - turn.outcome === 'lost-tick' || - turn.outcome === 'operator-skipped' || - !turn.communicationResult.requested || - turn.communicationResult.accepted - ) - throw new Error('Expected rejected communication fixture.'); - expect(turn.communicationResult.attempt.message).toHaveLength(280); - expect(simulation.getSnapshot().experiment.metrics.aggregate).toMatchObject( - { - publicMessagesRequested: 1, - publicMessagesRejected: 1, - successfullyInfectedCells: 1, - }, - ); - }); - - it('counts and exports malformed direct recipients as rejected direct attempts', async () => { - const invalidDirectCommunications = [ - { channel: 'direct', recipientId: 'Verge', message: 'Malformed ID.' }, - { channel: 'direct', message: 'Missing ID.' }, - ]; - let call = 0; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'invalid-direct-recipient-test', - configured: true, - async decide(): Promise { - return { - decision: { - worldAction: { type: 'wait' }, - communication: invalidDirectCommunications[call++], - summary: 'Keep the malformed attempt safe.', - }, - metadata: { - provider: 'scripted-test', - model: 'invalid-direct-recipient-test', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = service(provider); - const turns = [ - await simulation.executeNextTurn(), - await simulation.executeNextTurn(), - ]; - expect( - turns.map((turn) => - turn.outcome === 'provider-error' || - turn.outcome === 'lost-tick' || - turn.outcome === 'operator-skipped' - ? undefined - : turn.communicationResult, - ), - ).toMatchObject([ - { - accepted: false, - reason: 'invalid-communication', - attempt: { channel: 'direct', recipientId: null, distance: null }, - }, - { - accepted: false, - reason: 'invalid-communication', - attempt: { channel: 'direct', recipientId: null, distance: null }, - }, - ]); - expect(simulation.getSnapshot().experiment.metrics.aggregate).toMatchObject( - { - publicMessagesRequested: 0, - publicMessagesRejected: 0, - directMessagesRequested: 2, - directMessagesRejected: 2, - }, - ); - - const exported = simulation.generateExperimentExport({ - ...exportRequest('minimal'), - communications: { channel: 'direct', status: 'rejected' }, - }); - expect(exported.communications).toMatchObject([ - { - originatingTurn: 1, - channel: 'direct', - recipientId: null, - message: 'Malformed ID.', - status: 'rejected', - rejectionReason: 'invalid-communication', - }, - { - originatingTurn: 2, - channel: 'direct', - recipientId: null, - message: 'Missing ID.', - status: 'rejected', - rejectionReason: 'invalid-communication', - }, - ]); - expect(exported.metrics?.aggregate).toMatchObject({ - publicMessagesRequested: 0, - publicMessagesRejected: 0, - directMessagesRequested: 2, - directMessagesRejected: 2, - }); - expect( - simulation.generateExperimentExport({ - ...exportRequest('minimal'), - communications: { channel: 'public', status: 'rejected' }, - }).communications, - ).toEqual([]); - }); - - it('uses pre-action positions for direct-message range', async () => { - const initial = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'placeholder' }, - ]), - ).getSnapshot(); - const sender = initial.world.agents[0]!; - const recipient = initial.world.agents.find( - (candidate) => - candidate.id !== sender.id && - gridDistance(sender.currentCell, candidate.currentCell) > 3, - )!; - const initialDistance = gridDistance( - sender.currentCell, - recipient.currentCell, - ); - const targetCell = initial.world.hexes.find( - ({ cell }) => - gridDistance(sender.currentCell, cell) === 1 && - gridDistance(cell, recipient.currentCell) === initialDistance - 1, - )!.cell; - const simulation = service( - new ScriptedAgentProvider([ - { - worldAction: { type: 'move', targetCell }, - communication: { - channel: 'direct', - recipientId: recipient.id, - message: 'This must use the old distance.', - }, - summary: 'Move closer and try to message.', - }, - ]), - ); - const initialPhysicalDistance = physicalDistanceKm( - sender.currentCell, - recipient.currentCell, - )!; - const turn = await simulation.executeNextTurn(); - expect(turn).toMatchObject({ - outcome: 'accepted', - communicationResult: { - accepted: true, - event: { distance: initialPhysicalDistance }, - }, - }); - expect(simulation.getSnapshot().world.agents[0]?.currentCell).toBe( - targetCell, - ); - }); - - it('delivers direct messages alongside world actions and exposes inbound and outbound context', async () => { - const initial = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'placeholder' }, - ]), - ).getSnapshot(); - const sender = initial.world.agents[0]!; - const recipient = initial.world.agents[1]!; - const simulation = service( - new ScriptedAgentProvider([ - { - worldAction: { type: 'wait' }, - communication: { - channel: 'direct', - recipientId: recipient.id, - message: ' Hold near the center. ', - }, - summary: 'Coordinate.', - }, - { worldAction: { type: 'wait' }, summary: 'Observe.' }, - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - { worldAction: { type: 'wait' }, summary: 'Observe sender.' }, - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - { worldAction: { type: 'wait' }, summary: 'Observe sender again.' }, - ]), - ); - const before = simulation.getSnapshot().world; - const sent = await simulation.executeNextTurn(); - expect(sent).toMatchObject({ - agentId: sender.id, - outcome: 'accepted', - communicationResult: { - event: { - type: 'direct-message-sent', - recipientId: recipient.id, - message: 'Hold near the center.', - }, - }, - }); - expect(simulation.getSnapshot().world.agents).toEqual(before.agents); - expect(simulation.getSnapshot().world.hexes).toEqual(before.hexes); - - const recipientTurn = await simulation.executeNextTurn(); - expect(recipientTurn.observation.recentDirectMessages).toMatchObject([ - { - senderId: sender.id, - recipientId: recipient.id, - direction: 'inbound', - message: 'Hold near the center.', - }, - ]); - const unrelatedTurn = await simulation.executeNextTurn(); - expect(unrelatedTurn.observation.recentDirectMessages).toEqual([]); - for (let index = 0; index < 6; index += 1) - await simulation.executeNextTurn(); - const senderTurn = simulation.getSnapshot().turns.at(-1)!; - expect(senderTurn.agentId).toBe(sender.id); - expect(senderTurn.observation.recentDirectMessages[0]).toMatchObject({ - direction: 'outbound', - recipientId: recipient.id, - }); - }); - - it('rejects self and unknown recipients without a delivered event', async () => { - const ids = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'placeholder' }, - ]), - ) - .getSnapshot() - .world.agents.map(({ id }) => id); - for (const [recipientId, reason] of [ - [ids[0]!, 'self-message'], - [ - agentIdSchema.parse('6b58a30d-5d47-4ea3-8c1c-43edcc919553'), - 'unknown-recipient', - ], - ] as const) { - const simulation = service( - new ScriptedAgentProvider([ - { - worldAction: { type: 'wait' }, - communication: { - channel: 'direct', - recipientId, - message: 'Hello.', - }, - summary: 'Try message.', - }, - ]), - ); - expect(await simulation.executeNextTurn()).toMatchObject({ - outcome: 'accepted', - communicationResult: { accepted: false, reason }, - }); - expect(simulation.getSnapshot().world.events).toHaveLength(1); - } - }); - - it('keeps recent communication context chronological and capped at six', async () => { - const seen: AgentObservation[] = []; - let clock = 0; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'communication-history-test', - configured: true, - async decide(observation): Promise { - seen.push(observation); - const target = observation.nearbyAgents.find( - ({ distance }) => distance <= 3, - ); - return { - decision: { - worldAction: { type: 'wait' }, - communication: target - ? { - channel: 'direct', - recipientId: target.id, - message: `Turn ${seen.length}`, - } - : undefined, - summary: 'Message.', - }, - metadata: { - provider: 'scripted-test', - model: 'communication-history-test', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = new SimulationService({ - provider, - now: () => - new Date( - Date.parse('2026-08-13T12:00:00.000Z') + clock++, - ).toISOString(), - createEventId: deterministicEventIdGenerator(), - }); - for (let index = 0; index < 48; index += 1) - await simulation.executeNextTurn(); - const bounded = seen.findLast( - ({ recentDirectMessages }) => recentDirectMessages.length === 6, - ); - expect(bounded?.recentDirectMessages).toHaveLength(6); - expect( - bounded?.recentDirectMessages.map(({ occurredAt }) => occurredAt), - ).toEqual( - bounded?.recentDirectMessages - .map(({ occurredAt }) => occurredAt) - .toSorted(), - ); - }); - - it('keeps public world chat chronological, globally visible, and capped at twelve', async () => { - const seen: AgentObservation[] = []; - let clock = 0; - let eventSequence = 0; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'public-history-test', - configured: true, - async decide(observation): Promise { - seen.push(observation); - return { - decision: { - worldAction: { type: 'wait' }, - communication: { - channel: 'public', - message: `Public ${seen.length}`, - }, - summary: 'Publish.', - }, - metadata: { - provider: 'scripted-test', - model: 'public-history-test', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = new SimulationService({ - provider, - now: () => - new Date( - Date.parse('2026-08-13T12:00:00.000Z') + clock++, - ).toISOString(), - createEventId: () => - `67aa21b9-fc78-4b04-9f92-${String(++eventSequence).padStart(12, '0')}`, - }); - for (let index = 0; index < 14; index += 1) - await simulation.executeNextTurn(); - const bounded = seen.at(-1)!.recentPublicMessages; - expect(bounded).toHaveLength(12); - expect(bounded.map(({ message }) => message)).toEqual( - Array.from({ length: 12 }, (_, index) => `Public ${index + 2}`), - ); - expect(new Set(bounded.map(({ senderId }) => senderId))).not.toEqual( - new Set([seen.at(-1)!.agentId]), - ); - }); - - it('reset clears accepted communication history and metrics', async () => { - const initial = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'placeholder' }, - ]), - ).getSnapshot(); - const simulation = service( - new ScriptedAgentProvider([ - { - worldAction: { type: 'wait' }, - communication: { - channel: 'public', - message: 'Public before reset.', - }, - summary: 'Publish.', - }, - { - worldAction: { type: 'wait' }, - communication: { - channel: 'direct', - recipientId: initial.world.agents[0]!.id, - message: 'Direct before reset.', - }, - summary: 'Send directly.', - }, - { worldAction: { type: 'wait' }, summary: 'After reset.' }, - ]), - ); - await simulation.executeNextTurn(); - await simulation.executeNextTurn(); - expect(simulation.getSnapshot().world.events).toHaveLength(4); - expect(simulation.getSnapshot().experiment.metrics.aggregate).toMatchObject( - { - publicMessagesRequested: 1, - publicMessagesAccepted: 1, - directMessagesRequested: 1, - directMessagesDelivered: 1, - }, - ); - const reset = simulation.reset(); - expect(reset.world.events).toEqual([]); - expect(reset.experiment.metrics.aggregate).toMatchObject({ - publicMessagesSent: 0, - publicMessagesRequested: 0, - publicMessagesAccepted: 0, - directMessagesRequested: 0, - directMessagesDelivered: 0, - directMessagesSent: 0, - directMessagesReceived: 0, - }); - const afterReset = await simulation.executeNextTurn(); - expect(afterReset.observation.recentPublicMessages).toEqual([]); - expect(afterReset.observation.recentDirectMessages).toEqual([]); - }); - - it('applied World Setup clears bounded observation history', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { - worldAction: { type: 'wait' }, - communication: { - channel: 'public', - message: 'Before applying setup.', - }, - summary: 'Publish.', - }, - { worldAction: { type: 'wait' }, summary: 'After setup.' }, - ]), - ); - await simulation.executeNextTurn(); - const setup = defaultWorldSetupRequest(); - simulation.applyWorldSetup({ - ...setup, - modelConfiguration: { - ...setup.modelConfiguration, - globalModelId: 'deterministic-script', - }, - }); - - const afterSetup = await simulation.executeNextTurn(); - expect(afterSetup.observation.recentPublicMessages).toEqual([]); - expect(afterSetup.observation.recentEvents).toEqual([]); - }); - - it('updates an existing agent and uses the trimmed personality on its next turn', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Use the edit.' }, - ]), - ); - const agent = simulation.getSnapshot().world.agents[0]!; - const updated = simulation.updateAgentPersonality( - agent.id, - ' Prioritize adjacent open cells. ', - ); - expect(updated).toMatchObject({ - id: agent.id, - personality: 'Prioritize adjacent open cells.', - }); - expect((await simulation.executeNextTurn()).observation.personality).toBe( - 'Prioritize adjacent open cells.', - ); - }); - - it('rejects unknown agents and invalid personalities without mutation, then recovers', () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Wait.' }, - ]), - ); - const before = simulation.getSnapshot(); - expect(() => - simulation.updateAgentPersonality( - 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', - 'Valid personality.', - ), - ).toThrow(SimulationValidationError); - expect(() => - simulation.updateAgentPersonality(before.world.agents[0]!.id, ' '), - ).toThrow(SimulationValidationError); - expect(() => - simulation.updateAgentPersonality( - before.world.agents[0]!.id, - 'x'.repeat(PERSONALITY_MAX_LENGTH + 1), - ), - ).toThrow(SimulationValidationError); - expect(simulation.getSnapshot()).toEqual(before); - - simulation.updateAgentPersonality( - before.world.agents[0]!.id, - 'Recovered personality.', - ); - expect(simulation.getSnapshot().world.agents[0]!.personality).toBe( - 'Recovered personality.', - ); - }); - - it('edits personality without changing world progress or historical observations', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'infect' }, summary: 'Infect.' }, - ]), - ); - await simulation.executeNextTurn(); - const before = simulation.getSnapshot(); - const agent = before.world.agents[0]!; - simulation.updateAgentPersonality(agent.id, 'A new active personality.'); - const after = simulation.getSnapshot(); - - expect(after.world.hexes).toEqual(before.world.hexes); - expect(after.world.events).toEqual(before.world.events); - expect(after.world.agents.map(({ currentCell }) => currentCell)).toEqual( - before.world.agents.map(({ currentCell }) => currentCell), - ); - expect( - after.world.agents.map(({ id, name, color }) => ({ id, name, color })), - ).toEqual( - before.world.agents.map(({ id, name, color }) => ({ id, name, color })), - ); - expect(after.turns).toEqual(before.turns); - expect(after.turns[0]!.observation.personality).toBe(agent.personality); - expect(after.turnNumber).toBe(before.turnNumber); - expect(after.nextAgentId).toBe(before.nextAgentId); - }); - - it('reset preserves active personality edits while restoring deterministic progress', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'infect' }, summary: 'Infect.' }, - ]), - ); - const initial = simulation.getSnapshot(); - for (const agent of initial.world.agents) { - simulation.updateAgentPersonality( - agent.id, - `Preserve ${agent.name}'s edit.`, - ); - } - await simulation.executeNextTurn(); - const reset = simulation.reset(); - - expect(reset).toMatchObject({ turnNumber: 0, turns: [] }); - expect(reset.world.events).toEqual([]); - expect(reset.world.hexes).toEqual(initial.world.hexes); - expect(reset.world.agents.map(({ currentCell }) => currentCell)).toEqual( - initial.world.agents.map(({ currentCell }) => currentCell), - ); - expect(reset.world.agents.map(({ personality }) => personality)).toEqual( - initial.world.agents.map(({ name }) => `Preserve ${name}'s edit.`), - ); - }); - - it('restores all eight defaults without resetting current world progress', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'infect' }, summary: 'Infect.' }, - ]), - ); - for (const agent of simulation.getSnapshot().world.agents) { - simulation.updateAgentPersonality(agent.id, `Custom ${agent.name}.`); - } - await simulation.executeNextTurn(); - const before = simulation.getSnapshot(); - const restored = simulation.restoreDefaultPersonalities(); - - expect(restored.world.agents.map(({ personality }) => personality)).toEqual( - DEVELOPMENT_AGENT_BLUEPRINTS.map(({ personality }) => personality), - ); - expect( - restored.world.agents.find(({ name }) => name === 'Mingle')?.personality, - ).toBe( - 'You are a social coalition-builder. Seek agents, initiate and continue conversations, propose alliances, answer offers, negotiate borders, and coordinate captures against dominant rivals. Prefer cooperation and public diplomacy over silent expansion, but protect your own territory and leave an alliance that repeatedly ignores or exploits you. Make concrete proposals rather than merely announcing actions.', - ); - expect(restored.world.hexes).toEqual(before.world.hexes); - expect(restored.world.events).toEqual(before.world.events); - expect(restored.turns).toEqual(before.turns); - expect(restored.turnNumber).toBe(before.turnNumber); - expect(restored.nextAgentId).toBe(before.nextAgentId); - expect(restored.world.agents.map(({ currentCell }) => currentCell)).toEqual( - before.world.agents.map(({ currentCell }) => currentCell), - ); - }); - - it('records accepted and rejected actions without mutating on rejection', async () => { - const initial = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'infect' }, summary: 'Infect.' }, - ]), - ); - expect((await initial.executeNextTurn()).outcome).toBe('accepted'); - - const rejected = service( - new ScriptedAgentProvider([ - { - worldAction: { - type: 'move', - targetCell: h3CellSchema.parse('8928308280fffff'), - }, - summary: 'Attempt a distant move.', - }, - { worldAction: { type: 'wait' }, summary: 'Continue.' }, - ]), - ); - const before = rejected.getSnapshot().world; - expect(await rejected.executeNextTurn()).toMatchObject({ - turnNumber: 1, - outcome: 'rejected', - }); - expect(rejected.getSnapshot().world.hexes).toEqual(before.hexes); - expect(rejected.getSnapshot().world.agents).toEqual(before.agents); - expect(await rejected.executeNextTurn()).toMatchObject({ - turnNumber: 2, - outcome: 'accepted', - }); - expect(rejected.getSnapshot().turnNumber).toBe(2); - }); - - it('manually retries the same failed logical turn without mutating the world', async () => { - let calls = 0; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'failure-test', - configured: true, - async decide(): Promise { - calls += 1; - if (calls <= 2) - throw new AgentProviderError( - { - code: 'timeout', - message: 'The model request timed out.', - retryable: true, - }, - { - provider: 'scripted-test', - model: 'failure-test', - latencyMs: 10, - promptTokens: 10, - completionTokens: 2, - reasoningTokens: 1, - cachedReadTokens: 3, - cacheWriteTokens: 4, - totalTokens: 12, - costCredits: 0.1, - }, - ); - return { - decision: { - worldAction: { type: 'wait' }, - summary: 'Recovered.', - }, - metadata: { - provider: 'scripted-test', - model: 'failure-test', - latencyMs: 20, - promptTokens: 20, - completionTokens: 5, - reasoningTokens: 2, - cachedReadTokens: 6, - cacheWriteTokens: 8, - totalTokens: 25, - costCredits: 0.2, - }, - }; - }, - }; - const simulation = service(provider); - const before = simulation.getSnapshot().world; - expect(await simulation.executeNextTurn()).toMatchObject({ - turnNumber: 1, - outcome: 'provider-error', - }); - expect(simulation.getSnapshot().world).toEqual(before); - expect(simulation.getSnapshot().turnNumber).toBe(0); - expect(simulation.getSnapshot().pendingFailedTurn).toMatchObject({ - turnNumber: 1, - attempts: [{ kind: 'initial' }, { kind: 'automatic-transport-retry' }], - }); - expect(await simulation.retryFailedTurn()).toMatchObject({ - turnNumber: 1, - outcome: 'accepted', - modelAttempts: [ - { kind: 'initial' }, - { kind: 'automatic-transport-retry' }, - { kind: 'manual-retry' }, - ], - }); - expect(simulation.getSnapshot().turnNumber).toBe(1); - expect(simulation.getSnapshot().experiment.metrics.aggregate).toMatchObject( - { - totalTurns: 1, - accepted: 1, - rejected: 0, - providerErrors: 0, - operatorSkipped: 0, - modelCalls: 3, - failedModelAttempts: 2, - automaticTransportRetries: 1, - manualRetryAttempts: 1, - retriedTurns: 1, - recoveredByRetry: 1, - tokens: { - promptTokens: 40, - completionTokens: 9, - reasoningTokens: 4, - cachedReadTokens: 12, - cacheWriteTokens: 16, - totalTokens: 49, - }, - knownCostCredits: 0.4, - }, - ); - }); - - it('preserves partial-known token and cost totals per attempt and per logical turn', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Known usage.' }, - ]), - ); - const completed = await simulation.executeNextTurn(); - const failedAttempt = (attemptNumber: number) => ({ - attemptNumber, - kind: - attemptNumber === 1 - ? ('initial' as const) - : ('automatic-transport-retry' as const), - startedAt: completed.startedAt, - completedAt: completed.completedAt, - modelId: 'acceptance/model', - reasoningProfile: 'provider-default' as const, - failure: { - code: 'provider-http' as const, - message: 'The model provider rate limited the request.', - retryable: true, - httpStatus: 429, - }, - provider: { - provider: 'openrouter' as const, - model: 'acceptance/model', - httpStatus: 429, - latencyMs: 10, - }, - }); - const acceptanceTurn = agentTurnRecordSchema.parse({ - ...completed, - provider: { - provider: 'openrouter', - model: 'acceptance/model', - latencyMs: 20, - promptTokens: 73_931, - completionTokens: 5_079, - totalTokens: 79_010, - reasoningTokens: 2_642, - cachedReadTokens: 432, - cacheWriteTokens: 8_175, - costCredits: 1.25, - }, - modelAttempts: [ - failedAttempt(1), - failedAttempt(2), - { - attemptNumber: 3, - kind: 'manual-retry', - startedAt: completed.startedAt, - completedAt: completed.completedAt, - modelId: 'acceptance/model', - reasoningProfile: 'provider-default', - provider: { - provider: 'openrouter', - model: 'acceptance/model', - latencyMs: 20, - promptTokens: 73_931, - completionTokens: 5_079, - totalTokens: 79_010, - reasoningTokens: 2_642, - cachedReadTokens: 432, - cacheWriteTokens: 8_175, - costCredits: 1.25, - }, - }, - ], - }); - const metrics = calculateExperimentMetrics( - [acceptanceTurn], - [acceptanceTurn.agentId], - ).aggregate; - expect(metrics).toMatchObject({ - tokens: { - promptTokens: 73_931, - completionTokens: 5_079, - totalTokens: 79_010, - reasoningTokens: 2_642, - cachedReadTokens: 432, - cacheWriteTokens: 8_175, - }, - tokenUsageComplete: false, - attemptsWithUnknownTokenUsage: 2, - knownCostCredits: 1.25, - attemptsWithUnknownCost: 2, - turnsWithUnknownCost: 1, - manualRetryAttempts: 1, - recoveredManually: 1, - }); - }); - - it('sums each partially reported token field independently', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Partial usage.' }, - ]), - ); - const completed = await simulation.executeNextTurn(); - const partial = agentTurnRecordSchema.parse({ - ...completed, - modelAttempts: [ - { - attemptNumber: 1, - kind: 'initial', - startedAt: completed.startedAt, - completedAt: completed.completedAt, - modelId: 'partial/model', - reasoningProfile: 'provider-default', - provider: { - provider: 'openrouter', - model: 'partial/model', - latencyMs: 1, - promptTokens: 5, - costCredits: 0.1, - }, - }, - { - attemptNumber: 2, - kind: 'automatic-repair', - startedAt: completed.startedAt, - completedAt: completed.completedAt, - modelId: 'partial/model', - reasoningProfile: 'provider-default', - provider: { - provider: 'openrouter', - model: 'partial/model', - latencyMs: 1, - completionTokens: 3, - costCredits: 0.2, - }, - }, - ], - }); - expect( - calculateExperimentMetrics([partial], [partial.agentId]).aggregate, - ).toMatchObject({ - tokens: { promptTokens: 5, completionTokens: 3 }, - tokenUsageComplete: false, - attemptsWithUnknownTokenUsage: 2, - knownCostCredits: 0.3, - attemptsWithUnknownCost: 0, - turnsWithUnknownCost: 0, - automaticRepairAttempts: 1, - recoveredAutomatically: 1, - }); - }); - - it('skips one failed logical turn without applying an action and exports its attempts', async () => { - const simulation = service({ - mode: 'scripted-test', - model: 'failure-test', - configured: true, - async decide(_observation, model) { - throw new AgentProviderError( - { - code: 'timeout', - message: 'Timed out.', - retryable: true, - model, - }, - { - provider: 'scripted-test', - model, - latencyMs: 5, - promptTokens: 4, - completionTokens: 1, - totalTokens: 5, - reasoningTokens: 0, - cachedReadTokens: 0, - cacheWriteTokens: 0, - costCredits: 0.01, - }, - ); - }, - }); - const before = simulation.getSnapshot().world; - await simulation.executeNextTurn(); - await simulation.retryFailedTurn(); - await simulation.retryFailedTurn(); - const beforeSkipAccounting = - simulation.getSnapshot().experiment.attemptAccounting; - expect(beforeSkipAccounting).toMatchObject({ - attemptsStarted: 4, - attemptsFinalized: 4, - attemptsInFlight: 0, - knownFinalizedCostCredits: '0.04', - attemptsWithUnknownCost: 0, - }); - const skipped = simulation.skipFailedTurn(); - expect(skipped).toMatchObject({ - turnNumber: 1, - outcome: 'operator-skipped', - failure: { code: 'timeout', model: 'failure-test' }, - provider: { model: 'failure-test' }, - modelAttempts: [ - { kind: 'initial' }, - { kind: 'automatic-transport-retry' }, - { kind: 'manual-retry' }, - { kind: 'manual-retry' }, - ], - }); - expect(simulation.getSnapshot().world).toEqual(before); - expect(simulation.getSnapshot()).toMatchObject({ - turnNumber: 1, - status: 'paused', - pendingFailedTurn: null, - }); - expect(simulation.getSnapshot().experiment.attemptAccounting).toEqual( - beforeSkipAccounting, - ); - const exported = simulation.generateExperimentExport({ - ...exportRequest('minimal'), - outcomes: ['operator-skipped'], - }); - expect(exported.turns[0]).toMatchObject({ - outcome: 'operator-skipped', - modelAttempts: [ - { kind: 'initial' }, - { kind: 'automatic-transport-retry' }, - { kind: 'manual-retry' }, - { kind: 'manual-retry' }, - ], - }); - expect(exported.metrics?.aggregate).toMatchObject({ - totalTurns: 1, - accepted: 0, - rejected: 0, - providerErrors: 0, - operatorSkipped: 1, - modelCalls: 4, - failedModelAttempts: 4, - automaticTransportRetries: 1, - manualRetryAttempts: 2, - retriedTurns: 1, - recoveredByRetry: 0, - knownCostCredits: 0.04, - }); - }); - - it('uses the operator current model and reasoning profile for one manual retry', async () => { - const calls: Array<{ model: string; reasoningProfile?: string }> = []; - const simulation = service({ - mode: 'openrouter', - configured: true, - async decide(_observation, model, options) { - calls.push({ model, reasoningProfile: options?.reasoningProfile }); - if (calls.length <= 2) - throw new AgentProviderError({ - code: 'timeout', - message: 'Timed out.', - retryable: true, - model, - }); - return { - decision: { worldAction: { type: 'wait' }, summary: 'Recovered.' }, - metadata: { provider: 'openrouter', model, latencyMs: 1 }, - }; - }, - }); - simulation.setCompatibleModels(compatibleModels); - simulation.updateModelConfiguration({ - globalModelId: compatibleModels[0]!.id, - globalReasoningProfile: 'low', - overrides: [], - }); - await simulation.executeNextTurn(); - simulation.updateModelConfiguration({ - globalModelId: compatibleModels[1]!.id, - globalReasoningProfile: 'low', - overrides: [], - }); - await simulation.retryFailedTurn(); - expect(calls).toEqual([ - { model: compatibleModels[0]!.id, reasoningProfile: 'low' }, - { model: compatibleModels[0]!.id, reasoningProfile: 'low' }, - { model: compatibleModels[1]!.id, reasoningProfile: 'low' }, - ]); - }); - - it('counts legacy top-level provider metadata once when attempts are absent', async () => { - const simulation = service( - new ScriptedAgentProvider([ - { worldAction: { type: 'wait' }, summary: 'Legacy wait.' }, - ]), - ); - const turn = await simulation.executeNextTurn(); - const legacy = agentTurnRecordSchema.parse({ - ...turn, - modelAttempts: undefined, - }); - const metrics = calculateExperimentMetrics([legacy], [legacy.agentId]); - expect(metrics.aggregate).toMatchObject({ - totalTurns: 1, - accepted: 1, - providerErrors: 0, - operatorSkipped: 0, - modelCalls: 1, - failedModelAttempts: 0, - knownCostCredits: 0, - }); - }); - - it('does not commit or advance after post-provider validation fails', async () => { - const seenAgentIds: AgentObservation['agentId'][] = []; - let calls = 0; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'invalid-metadata-test', - configured: true, - async decide(observation): Promise { - calls += 1; - seenAgentIds.push(observation.agentId); - if (calls === 1) { - return { - decision: { - worldAction: { type: 'wait' }, - summary: 'Invalid metadata follows.', - }, - metadata: { - provider: 'scripted-test', - model: '', - latencyMs: 4, - promptTokens: 3, - completionTokens: 2, - totalTokens: 5, - costCredits: 0.125, - }, - } as ProviderDecision; - } - return { - decision: { worldAction: { type: 'wait' }, summary: 'Valid.' }, - metadata: { - provider: 'scripted-test', - model: 'invalid-metadata-test', - latencyMs: 0, - }, - }; - }, - }; - const simulation = service(provider); - const before = simulation.getSnapshot(); - - await expect(simulation.executeNextTurn()).resolves.toMatchObject({ - outcome: 'provider-error', - failure: { code: 'simulation-validation' }, - }); - - const afterFailure = simulation.getSnapshot(); - expect(afterFailure.world).toEqual(before.world); - expect(afterFailure.turnNumber).toBe(0); - expect(afterFailure.turns).toEqual([]); - expect(afterFailure.nextAgentId).toBe(before.nextAgentId); - expect(afterFailure).toMatchObject({ - activeAgentId: null, - status: 'provider-error', - pendingFailedTurn: { - turnNumber: 1, - attempts: [ - { - provider: { - model: 'invalid-metadata-test', - latencyMs: 4, - promptTokens: 3, - completionTokens: 2, - totalTokens: 5, - costCredits: 0.125, - }, - }, - ], - }, - experiment: { - attemptAccounting: { - attemptsStarted: 1, - attemptsFinalized: 1, - knownFinalizedCostCredits: '0.125', - attemptsWithUnknownCost: 0, - }, - }, - }); - expect( - simulation.generateExperimentExport(exportRequest('minimal')) - .providerAttempts, - ).toMatchObject([ - { - intendedTurnNumber: 1, - outcome: 'provider-error', - failure: { code: 'simulation-validation' }, - actualCostCredits: '0.125', - }, - ]); - - const recovered = await simulation.retryFailedTurn(); - expect(recovered).toMatchObject({ - turnNumber: 1, - agentId: before.nextAgentId, - outcome: 'accepted', - }); - expect(seenAgentIds).toEqual([before.nextAgentId, before.nextAgentId]); - }); - - it('retains only the newest world events without changing current state', async () => { - let clock = 0; - let eventSequence = 0; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'moving-history-test', - configured: true, - async decide(observation): Promise { - return { - decision: { - worldAction: { - type: 'move', - targetCell: observation.adjacentCells[0]!.cell, - }, - summary: 'Move.', - }, - metadata: { - provider: 'scripted-test', - model: 'moving-history-test', - latencyMs: 0, - }, - }; - }, - }; - const simulation = new SimulationService({ - provider, - now: () => - new Date( - Date.parse('2026-08-13T12:00:00.000Z') + clock++, - ).toISOString(), - createEventId: () => - `67aa21b9-fc78-4b04-9f92-${String(++eventSequence).padStart(12, '0')}`, - }); - const producedEvents: WorldEvent[] = []; - let lastRecord: AgentTurnRecord | undefined; - - for (let index = 0; index < 125; index += 1) { - lastRecord = await simulation.executeNextTurn(); - if (lastRecord.outcome !== 'accepted') { - throw new Error( - 'The moving history fixture must produce accepted turns.', - ); - } - producedEvents.push(lastRecord.worldActionResult.event); - } - - const snapshot = simulation.getSnapshot(); - expect(snapshot.world.events).toHaveLength(120); - expect(snapshot.world.events).toEqual(producedEvents.slice(-120)); - expect( - lastRecord?.observation.recentEvents.map(({ occurredAt }) => occurredAt), - ).toEqual(producedEvents.slice(-9, -1).map(({ occurredAt }) => occurredAt)); - - for (const agent of snapshot.world.agents) { - const latestMove = producedEvents - .filter( - (event) => event.type === 'agent-moved' && event.agentId === agent.id, - ) - .at(-1); - expect(latestMove?.type).toBe('agent-moved'); - if (latestMove?.type === 'agent-moved') { - expect(agent.currentCell).toBe(latestMove.toCell); - } - } - }); - - it('prevents overlapping turns and reset during an in-flight request', async () => { - let release!: (result: ProviderDecision) => void; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'deferred-test', - configured: true, - decide: () => - new Promise((resolve) => { - release = resolve; - }), - }; - const simulation = service(provider); - const pending = simulation.executeNextTurn(); - await expect(simulation.executeNextTurn()).rejects.toBeInstanceOf( - SimulationConflictError, - ); - expect(() => simulation.reset()).toThrow(SimulationConflictError); - expect(() => - simulation.updateAgentPersonality( - simulation.getSnapshot().world.agents[0]!.id, - 'Blocked edit.', - ), - ).toThrow(SimulationConflictError); - expect(() => simulation.restoreDefaultPersonalities()).toThrow( - SimulationConflictError, - ); - expect(() => - simulation.previewExperimentExport(exportRequest('minimal')), - ).toThrow(SimulationConflictError); - expect(() => - simulation.generateExperimentExport(exportRequest('minimal')), - ).toThrow(SimulationConflictError); - release({ - decision: { worldAction: { type: 'wait' }, summary: 'Done.' }, - metadata: { - provider: 'scripted-test', - model: 'deferred-test', - latencyMs: 0, - }, - }); - await pending; - expect(simulation.getSnapshot().turnNumber).toBe(1); - }); - - it('cancels an active provider request without mutating or consuming a turn', async () => { - let shouldBlock = true; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'cancel-test', - configured: true, - async decide(_observation, _model, options) { - if (shouldBlock) - await new Promise((resolve) => { - options?.signal?.addEventListener('abort', () => resolve()); - }); - return { - decision: { worldAction: { type: 'wait' }, summary: 'Too late.' }, - metadata: { - provider: 'scripted-test', - model: 'cancel-test', - latencyMs: 1, - }, - }; - }, - }; - const simulation = service(provider); - const before = simulation.getSnapshot().world; - const pending = simulation.executeNextTurn(); - expect(simulation.cancelCurrentRequest().cancellationRequested).toBe(true); - await expect(pending).rejects.toBeInstanceOf(SimulationTurnCancelledError); - expect(simulation.getSnapshot()).toMatchObject({ - activeAgentId: null, - cancellationRequested: false, - status: 'paused', - turnNumber: 0, - turns: [], - experiment: { totalCompletedTurns: 0 }, - }); - expect(simulation.getSnapshot().world).toEqual(before); - shouldBlock = false; - const afterCancellation = await simulation.executeNextTurn(); - expect(afterCancellation.observation.recentEvents).toEqual([]); - expect(afterCancellation.observation.recentPublicMessages).toEqual([]); - }); - - it('resolves models per turn and records between-turn model changes', async () => { - const usedModels: string[] = []; - const usedReasoningProfiles: string[] = []; - const provider: AgentProvider = { - mode: 'openrouter', - configured: true, - async decide(_observation, model, options) { - usedModels.push(model); - usedReasoningProfiles.push( - options?.reasoningProfile ?? 'provider-default', - ); - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait.' }, - metadata: { - provider: 'openrouter', - model, - latencyMs: 1, - costCredits: 0, - }, - }; - }, - }; - const simulation = service(provider); - const [first, second] = simulation.getSnapshot().world.agents; - await expect(simulation.executeNextTurn()).rejects.toMatchObject({ - code: 'models_unavailable', - }); - simulation.setCompatibleModels(compatibleModels); - simulation.updateModelConfiguration({ - globalModelId: compatibleModels[0]!.id, - globalReasoningProfile: 'xhigh', - overrides: [ - { - agentId: second!.id, - modelId: compatibleModels[1]!.id, - reasoningProfile: 'low', - }, - ], - }); - expect(simulation.getSnapshot().resolvedModels.slice(0, 2)).toMatchObject([ - { - agentId: first!.id, - modelId: compatibleModels[0]!.id, - reasoningProfile: 'xhigh', - source: 'global', - }, - { - agentId: second!.id, - modelId: compatibleModels[1]!.id, - reasoningProfile: 'low', - source: 'override', - }, - ]); - await simulation.executeNextTurn(); - await simulation.executeNextTurn(); - expect(usedModels).toEqual([ - compatibleModels[0]!.id, - compatibleModels[1]!.id, - ]); - expect(usedReasoningProfiles).toEqual(['xhigh', 'low']); - expect(simulation.getSnapshot().modelConfiguration.locked).toBe(false); - expect(() => - simulation.updateModelConfiguration({ - globalModelId: compatibleModels[1]!.id, - globalReasoningProfile: 'high', - overrides: [], - }), - ).not.toThrow(); - await simulation.executeNextTurn(); - expect(usedModels.at(-1)).toBe(compatibleModels[1]!.id); - const fourth = simulation.getSnapshot().world.agents[3]!; - simulation.updateModelConfiguration({ - globalModelId: compatibleModels[1]!.id, - globalReasoningProfile: 'high', - overrides: [ - { - agentId: fourth.id, - modelId: compatibleModels[0]!.id, - reasoningProfile: 'medium', - }, - ], - }); - await simulation.executeNextTurn(); - expect(usedModels.at(-1)).toBe(compatibleModels[0]!.id); - const exported = simulation.generateExperimentExport( - exportRequest('full-safe'), - ); - expect(exported.configurationEvents).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: 'model-assignment-changed', - scope: 'global', - effectiveTurn: 3, - }), - expect.objectContaining({ - type: 'model-assignment-changed', - scope: 'agent', - agentId: fourth.id, - previousModelId: compatibleModels[1]!.id, - newModelId: compatibleModels[0]!.id, - previousReasoningProfile: 'high', - newReasoningProfile: 'medium', - effectiveTurn: 4, - }), - ]), - ); - }); - - it('removes overrides, preserves unavailable imports, and migrates legacy exports safely', () => { - const simulation = service({ - mode: 'openrouter', - configured: true, - async decide(_observation, model) { - return { - decision: { worldAction: { type: 'wait' }, summary: 'Wait.' }, - metadata: { provider: 'openrouter', model, latencyMs: 1 }, - }; - }, - }); - simulation.setCompatibleModels(compatibleModels); - const agent = simulation.getSnapshot().world.agents[0]!; - expect(() => - simulation.updateModelConfiguration({ - globalModelId: compatibleModels[1]!.id, - globalReasoningProfile: 'off', - overrides: [], - }), - ).toThrow(SimulationValidationError); - simulation.updateModelConfiguration({ - globalModelId: compatibleModels[0]!.id, - overrides: [{ agentId: agent.id, modelId: compatibleModels[1]!.id }], - }); - simulation.updateModelConfiguration({ - globalModelId: compatibleModels[0]!.id, - overrides: [], - }); - expect(simulation.getSnapshot().resolvedModels[0]).toMatchObject({ - modelId: compatibleModels[0]!.id, - source: 'global', - }); - - const exported = simulation.generateExperimentExport( - exportRequest('minimal'), - ); - expect(exported.experiment.modelConfiguration).toEqual( - simulation.getSnapshot().modelConfiguration, - ); - const olderVersionSix = structuredClone(exported) as unknown as { - schemaVersion: number; - experiment: { - modelConfiguration: { - globalReasoningProfile?: string; - overrides: Array<{ reasoningProfile?: string }>; - }; - }; - }; - olderVersionSix.schemaVersion = 6; - delete olderVersionSix.experiment.modelConfiguration.globalReasoningProfile; - for (const override of olderVersionSix.experiment.modelConfiguration - .overrides) - delete override.reasoningProfile; - const migrated = simulation.importModelConfiguration(olderVersionSix); - expect(migrated.snapshot.modelConfiguration).toMatchObject({ - globalReasoningProfile: 'provider-default', - overrides: [], - }); - const unavailable = structuredClone(exported); - unavailable.experiment.modelConfiguration!.globalModelId = 'retired/model'; - const imported = simulation.importModelConfiguration(unavailable); - expect(imported.snapshot.resolvedModels[0]).toMatchObject({ - modelId: 'retired/model', - available: false, - issue: 'unavailable', - }); - expect(() => - simulation.importModelConfiguration({ schemaVersion: 12 }), - ).toThrow('Only schema-version 5 through 11'); - const legacy = simulation.importModelConfiguration({ schemaVersion: 5 }); - expect(legacy.legacy).toBe(true); - expect(legacy.snapshot.modelConfiguration.globalModelId).toBeNull(); - }); - - it('admits a complete roster atomically and stops at the provider-attempt cap', async () => { - let calls = 0; - const provider: AgentProvider = { - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide() { - calls += 1; - return { - decision: { - worldAction: { type: 'wait' }, - goalRevision: { operation: 'keep' }, - memoryOperation: { operation: 'keep' }, - summary: 'Wait within the bounded experiment.', - }, - metadata: { - provider: 'scripted-test', - model: 'deterministic-script', - latencyMs: 0, - costCredits: 0, - }, - }; - }, - }; - const simulation = service(provider); - const setup = defaultWorldSetupRequest(); - simulation.applyWorldSetup({ - ...setup, - modelConfiguration: { - ...setup.modelConfiguration, - globalModelId: 'deterministic-script', - }, - executionLimits: { - version: 'execution-limits-v2', - providerAttemptLimit: setup.roster.length, - creditLimit: '0.08', - reservationCreditsPerAttempt: '0.01', - }, - }); - await simulation.executeNextTick(); - const afterFirst = simulation.getSnapshot(); - expect(afterFirst.experiment.attemptAccounting).toMatchObject({ - attemptsStarted: setup.roster.length, - attemptsFinalized: setup.roster.length, - attemptsInFlight: 0, - knownFinalizedCostCredits: '0', - attemptsWithUnknownCost: 0, - remainingAttempts: 0, - exhausted: true, - creditLimit: '0.08', - committedCreditExposure: '0', - }); - simulation.updateAgentPersonality( - afterFirst.world.agents[0]!.id, - 'Preserve the attempt ledger while changing this personality.', - ); - expect( - simulation.getSnapshot().experiment.attemptAccounting.attemptsStarted, - ).toBe(setup.roster.length); - const world = structuredClone(afterFirst.world); - await expect(simulation.executeNextTick()).rejects.toMatchObject({ - code: 'experiment_budget_exhausted', - }); - expect(calls).toBe(setup.roster.length); - expect(simulation.getSnapshot().world.hexes).toEqual(world.hexes); - expect(simulation.reset().experiment.attemptAccounting).toMatchObject({ - attemptsStarted: 0, - attemptsFinalized: 0, - knownFinalizedCostCredits: '0', - attemptsWithUnknownCost: 0, - exhausted: false, - creditLimit: '0.08', - committedCreditExposure: '0', - }); - await simulation.executeNextTick(); - expect( - simulation.getSnapshot().experiment.attemptAccounting.attemptsStarted, - ).toBeGreaterThan(0); - const applied = simulation.applyWorldSetup({ - ...setup, - modelConfiguration: { - ...setup.modelConfiguration, - globalModelId: 'deterministic-script', - }, - }); - expect(applied.experiment.attemptAccounting).toMatchObject({ - attemptsStarted: 0, - attemptsFinalized: 0, - knownFinalizedCostCredits: '0', - attemptsWithUnknownCost: 0, - exhausted: false, - creditLimit: null, - committedCreditExposure: '0', - }); - }); - - it('rejects a whole simultaneous tick before dispatch when credit cannot cover the roster', async () => { - let calls = 0; - const simulation = service({ - mode: 'scripted-test', - model: 'deterministic-script', - configured: true, - async decide() { - calls += 1; - throw new Error('Credit admission must prevent this call.'); - }, - }); - const setup = defaultWorldSetupRequest(); - simulation.applyWorldSetup({ - ...setup, - modelConfiguration: { - ...setup.modelConfiguration, - globalModelId: 'deterministic-script', - }, - executionLimits: { - version: 'execution-limits-v2', - providerAttemptLimit: null, - creditLimit: '0.07999999', - reservationCreditsPerAttempt: '0.01', - }, - }); - - await expect(simulation.executeNextTick()).rejects.toMatchObject({ - code: 'experiment_budget_exhausted', - }); - expect(calls).toBe(0); - expect(simulation.getSnapshot()).toMatchObject({ - tickNumber: 0, - turnNumber: 0, - status: 'budget-exhausted', - experiment: { - attemptAccounting: { - attemptsStarted: 0, - unstartedReservedCredits: '0', - committedCreditExposure: '0', - remainingAdmissionCredits: '0.07999999', - exhaustionReason: 'credit-admission-limit', - }, - }, - }); + expect(reset.tickNumber).toBe(0); + expect(reset.swarmTicks).toEqual([]); }); }); diff --git a/apps/game-api/src/simulation-service.ts b/apps/game-api/src/simulation-service.ts index a0ee331..9f36bb1 100644 --- a/apps/game-api/src/simulation-service.ts +++ b/apps/game-api/src/simulation-service.ts @@ -1,22 +1,14 @@ import { gridDisk, gridDistance } from 'h3-js'; import { - AgentProviderError, SwarmPlannerError, - dispatchTickDecisions, type ReflexProvider, type SwarmPlanner, - type AgentProvider, - type ProviderDecision, } from '@hexzero/agent-runtime'; import { agentIdSchema, - appliedScenarioSchema, + archivedAppliedScenarioSchema, assignBehavior, behaviorConfigurationSchema, - agentObservationSchema, - agentTurnRecordSchema, - communicationIntentSchema, - diplomacyIntentSchema, experimentIdSchema, experimentExportDocumentSchema, experimentExportPreviewSchema, @@ -30,7 +22,6 @@ import { RECENT_ZERO_STRATEGIC_EVENT_LIMIT, PERSONALITY_MAX_LENGTH, OPENROUTER_PROVIDER_TIMEOUT_MS, - OPENROUTER_429_FALLBACK_BACKOFF_MS, WORLD_SCENARIO_LIMITS, PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS, PATIENT_ZERO_PLAYER_THREAT_FEED_LIMIT, @@ -44,14 +35,12 @@ import { simulationSnapshotSchema, type Agent, type AgentId, - type AgentObservation, type AgentGoalState, type GoalRevisionResult, type RequestedGoalRevision, type MemoryEntry, type MemoryOperationResult, type RequestedMemoryOperation, - type AgentTurnRecord, type ExperimentExportDocument, type ExperimentExportPreview, type ExperimentId, @@ -59,7 +48,6 @@ import { type BehaviorConfiguration, type CompatibleModel, type ModelId, - type ModelAttempt, type ExperimentConfigurationEvent, type H3Cell, type ProviderFailure, @@ -84,8 +72,6 @@ import { type WorldSetupRequest, } from '@hexzero/shared'; import { - applyCommunication, - applyDiplomacy, applyWorldAction, createDevelopmentWorld, createDefaultAppliedScenario, @@ -93,12 +79,9 @@ import { defaultWorldSetupRequest, previewWorldSetup, DEVELOPMENT_AGENT_BLUEPRINTS, - getCaptureEligibility, getAgentAlliance, getEffectiveAgentColor, - getProposalTargetEligibility, physicalDistanceKm, - expireAllianceProposals, seededTickIntervalMinutes, seededTickOrder, advanceSimulatedPlayer, @@ -118,7 +101,6 @@ import { boundedRecentCaptures, localPressureAtCell, } from './swarm-pressure'; -import { ObservationHistory } from './observation-history'; import { AttemptAccounting } from './attempt-accounting'; import { chooseReflexWorldAction, @@ -297,15 +279,6 @@ function captureAlertsFrom( })); } -interface PendingFailedTurn { - turnNumber: number; - agentId: AgentId; - startedAt: string; - observation: AgentObservation; - failure: ProviderFailure; - attempts: ModelAttempt[]; -} - export class SimulationConflictError extends Error { constructor(message: string) { super(message); @@ -340,10 +313,8 @@ export class SimulationValidationError extends Error { } export interface SimulationServiceOptions { - provider: AgentProvider; - /** Separate strategic and reflex cognition used only by zero-swarm-v1. */ - swarmPlanner?: SwarmPlanner; - reflexProvider?: ReflexProvider; + swarmPlanner: SwarmPlanner; + reflexProvider: ReflexProvider; /** * Offline comparison seam: choose one opaque, already legal candidate without * calling a reflex provider. Production zero-swarm execution leaves this unset. @@ -354,42 +325,32 @@ export interface SimulationServiceOptions { now?: () => string; createEventId?: () => string; createExperimentId?: () => string; - createAllianceId?: () => string; - createProposalId?: () => string; experimentRetentionLimit?: number; } export class SimulationService { - readonly #provider: AgentProvider; - readonly #swarmPlanner: SwarmPlanner | undefined; - readonly #reflexProvider: ReflexProvider | undefined; + readonly #swarmPlanner: SwarmPlanner; + readonly #reflexProvider: ReflexProvider; readonly #deterministicWorkerCandidateSelector: ((compiled: CompiledReflexObservation) => string) | undefined; readonly #now: () => string; readonly #createEventId: () => string; readonly #createExperimentId: () => string; - readonly #createAllianceId: () => string; - readonly #createProposalId: () => string; readonly #experimentRetentionLimit: number; #state: WorldState; - #turns: AgentTurnRecord[] = []; - #completedTurnCount = 0; #completedSwarmDecisionCount = 0; #completedTickCount = 0; #virtualTime = RESET_GENERATED_AT; #lastTickIntervalMinutes: number | null = null; #resolutionOrder: AgentId[] = []; - #cursor = 0; #busy = false; #verificationBusy = false; #status: SimulationStatus; #activeAgentId: AgentId | null = null; #activeRequestController: AbortController | null = null; #cancellationRequested = false; - #pendingFailedTurn: PendingFailedTurn | null = null; #experimentId: ExperimentId; #experimentStartedAt: string; - #experimentTurns: AgentTurnRecord[] = []; #initialExperimentAgents: Agent[]; #initialExperimentWorld: SimulationSnapshot['world']; #configurationEvents: ExperimentConfigurationEvent[] = []; @@ -402,7 +363,6 @@ export class SimulationService { #agentGoals = new Map(); #agentMemories = new Map(); #simulatedPlayerEvents: SimulatedPlayerEvent[] = []; - #observationHistory: ObservationHistory; #attemptAccounting: AttemptAccounting; #swarmTicks: SwarmTickRecord[] = []; #experimentSwarmTicks: SwarmTickRecord[] = []; @@ -412,15 +372,12 @@ export class SimulationService { #lastSwarmPositions = new Map(); constructor({ - provider, swarmPlanner, reflexProvider, deterministicWorkerCandidateSelector, now = () => new Date().toISOString(), createEventId = () => crypto.randomUUID(), createExperimentId = () => crypto.randomUUID(), - createAllianceId = () => crypto.randomUUID(), - createProposalId = () => crypto.randomUUID(), experimentRetentionLimit = DEFAULT_EXPERIMENT_RETENTION, }: SimulationServiceOptions) { if ( @@ -428,7 +385,6 @@ export class SimulationService { experimentRetentionLimit < 1 ) throw new Error('Experiment retention limit must be a positive integer.'); - this.#provider = provider; this.#swarmPlanner = swarmPlanner; this.#reflexProvider = reflexProvider; this.#deterministicWorkerCandidateSelector = @@ -436,14 +392,14 @@ export class SimulationService { this.#now = now; this.#createEventId = createEventId; this.#createExperimentId = createExperimentId; - this.#createAllianceId = createAllianceId; - this.#createProposalId = createProposalId; this.#experimentRetentionLimit = experimentRetentionLimit; this.#state = toWorldState( createDevelopmentWorld({ generatedAt: RESET_GENERATED_AT }), ); - this.#observationHistory = new ObservationHistory(this.#state.events); - this.#status = provider.configured ? 'paused' : 'configuration-error'; + this.#status = + swarmPlanner.configured && reflexProvider.configured + ? 'paused' + : 'configuration-error'; this.#experimentId = experimentIdSchema.parse(this.#createExperimentId()); this.#experimentStartedAt = this.#now(); this.#initialExperimentAgents = structuredClone([ @@ -454,10 +410,9 @@ export class SimulationService { ...this.#state.agents.keys(), ]); const scriptedModel = - (provider.model as ModelId | undefined) ?? - (provider.mode === 'scripted-test' + swarmPlanner.mode === 'scripted-swarm-test' ? ('deterministic-script' as ModelId) - : null); + : null; this.#modelConfiguration = experimentModelConfigurationSchema.parse({ globalModelId: scriptedModel, globalReasoningProfile: 'provider-default', @@ -489,46 +444,31 @@ export class SimulationService { getSnapshot(): SimulationSnapshot { const agents = [...this.#state.agents.values()]; - const next = agents[this.#cursor % agents.length]; - const droppedRecords = - this.#completedTurnCount - this.#experimentTurns.length; return simulationSnapshotSchema.parse({ world: this.#worldSnapshot(), scenario: this.#scenario, - turnNumber: this.#completedTurnCount, tickNumber: this.#completedTickCount, virtualTime: this.#virtualTime, lastTickIntervalMinutes: this.#lastTickIntervalMinutes, resolutionOrder: this.#resolutionOrder, - nextAgentId: next?.id ?? null, activeAgentId: this.#activeAgentId, cancellationRequested: this.#cancellationRequested, - pendingFailedTurn: this.#pendingFailedTurn - ? { - turnNumber: this.#pendingFailedTurn.turnNumber, - agentId: this.#pendingFailedTurn.agentId, - failure: this.#pendingFailedTurn.failure, - attempts: this.#pendingFailedTurn.attempts, - } - : null, status: this.#status, - providerMode: this.#provider.mode, - providerConfigured: this.#provider.configured, - ...(this.#scenario.cognitionMode === 'zero-swarm-v1' && - this.#swarmPlanner && - this.#reflexProvider - ? { - swarmProviderStatus: { - plannerMode: this.#swarmPlanner.mode, - plannerConfigured: this.#swarmPlanner.configured, - reflexMode: this.#reflexProvider.mode, - reflexConfigured: this.#reflexProvider.configured, - ...(this.#reflexProvider.model - ? { reflexModel: this.#reflexProvider.model } - : {}), - }, - } - : {}), + providerMode: + this.#swarmPlanner.mode === 'openrouter-swarm' + ? 'openrouter' + : 'scripted-test', + providerConfigured: + this.#swarmPlanner.configured && this.#reflexProvider.configured, + swarmProviderStatus: { + plannerMode: this.#swarmPlanner.mode, + plannerConfigured: this.#swarmPlanner.configured, + reflexMode: this.#reflexProvider.mode, + reflexConfigured: this.#reflexProvider.configured, + ...(this.#reflexProvider.model + ? { reflexModel: this.#reflexProvider.model } + : {}), + }, modelConfiguration: this.#modelConfiguration, ...(agents.length > 0 ? { behaviorConfiguration: this.#behaviorConfiguration } @@ -542,19 +482,10 @@ export class SimulationService { agentId: id, entries: structuredClone(this.#agentMemories.get(id) ?? []), })), - ...(this.#scenario.cognitionMode === 'zero-swarm-v1' - ? { swarmTicks: structuredClone(this.#swarmTicks) } - : {}), - turns: this.#turns, + swarmTicks: structuredClone(this.#swarmTicks), experiment: { id: this.#experimentId, startedAt: this.#experimentStartedAt, - totalCompletedTurns: this.#completedTurnCount, - retainedTurns: this.#experimentTurns.length, - firstRetainedTurn: this.#experimentTurns[0]?.turnNumber, - lastRetainedTurn: this.#experimentTurns.at(-1)?.turnNumber, - droppedRecords, - complete: droppedRecords === 0, attemptAccounting: this.#attemptAccounting.snapshot(), metrics: this.#experimentMetrics.snapshot(agents.map(({ id }) => id)), currentTerritory: this.#territoryScoreboard(), @@ -578,18 +509,14 @@ export class SimulationService { this.#state = toWorldState( createWorldFromScenario(this.#scenario, RESET_GENERATED_AT), ); - this.#turns = []; - this.#completedTurnCount = 0; this.#completedSwarmDecisionCount = 0; this.#completedTickCount = 0; this.#virtualTime = RESET_GENERATED_AT; this.#lastTickIntervalMinutes = null; this.#resolutionOrder = []; - this.#cursor = 0; this.#activeAgentId = null; this.#activeRequestController = null; this.#cancellationRequested = false; - this.#pendingFailedTurn = null; this.#agentGoals = new Map(); this.#agentMemories = new Map(); this.#swarmTicks = []; @@ -600,10 +527,8 @@ export class SimulationService { this.#lastSwarmPositions = new Map(); this.#experimentId = experimentIdSchema.parse(this.#createExperimentId()); this.#experimentStartedAt = this.#now(); - this.#experimentTurns = []; this.#configurationEvents = []; this.#simulatedPlayerEvents = []; - this.#observationHistory = new ObservationHistory(this.#state.events); this.#initialExperimentAgents = structuredClone([ ...this.#state.agents.values(), ]); @@ -624,13 +549,9 @@ export class SimulationService { locked: false, }; this.#status = - this.#scenario.cognitionMode === 'zero-swarm-v1' && - this.#swarmPlanner && - this.#reflexProvider + this.#swarmPlanner.configured && this.#reflexProvider.configured ? 'paused' - : this.#provider.configured - ? 'paused' - : 'configuration-error'; + : 'configuration-error'; return this.getSnapshot(); } @@ -737,18 +658,14 @@ export class SimulationService { }; this.#modelConfiguration = nextModels; this.#behaviorConfiguration = nextBehavior; - this.#turns = []; - this.#completedTurnCount = 0; this.#completedSwarmDecisionCount = 0; this.#completedTickCount = 0; this.#virtualTime = RESET_GENERATED_AT; this.#lastTickIntervalMinutes = null; this.#resolutionOrder = []; - this.#cursor = 0; this.#activeAgentId = null; this.#activeRequestController = null; this.#cancellationRequested = false; - this.#pendingFailedTurn = null; this.#agentGoals = new Map(); this.#agentMemories = new Map(); this.#swarmTicks = []; @@ -759,10 +676,8 @@ export class SimulationService { this.#lastSwarmPositions = new Map(); this.#experimentId = experimentIdSchema.parse(this.#createExperimentId()); this.#experimentStartedAt = this.#now(); - this.#experimentTurns = []; this.#configurationEvents = []; this.#simulatedPlayerEvents = []; - this.#observationHistory = new ObservationHistory(this.#state.events); this.#initialExperimentAgents = structuredClone([ ...this.#state.agents.values(), ]); @@ -775,20 +690,16 @@ export class SimulationService { this.#experimentRetentionLimit, ); this.#status = - this.#scenario.cognitionMode === 'zero-swarm-v1' && - this.#swarmPlanner && - this.#reflexProvider + this.#swarmPlanner.configured && this.#reflexProvider.configured ? 'paused' - : this.#provider.configured - ? 'paused' - : 'configuration-error'; + : 'configuration-error'; return this.getSnapshot(); } setCompatibleModels(models: CompatibleModel[]): void { this.#availableModels = new Map(models.map((model) => [model.id, model])); this.#availableModelIds = new Set(models.map(({ id }) => id)); - if (this.#provider.mode === 'scripted-test') + if (this.#swarmPlanner.mode === 'scripted-swarm-test') this.#availableModelIds.add('deterministic-script' as ModelId); } @@ -853,7 +764,7 @@ export class SimulationService { } updateBehaviorConfiguration(input: unknown): SimulationSnapshot { - if (this.#busy || this.#verificationBusy || this.#completedTurnCount > 0) + if (this.#busy || this.#verificationBusy || this.#completedTickCount > 0) throw new SimulationConflictError( 'Behavior is locked after the experiment begins. Reset to create new assignments.', ); @@ -964,7 +875,7 @@ export class SimulationService { (version === 9 || version === 10) && typeof experiment?.scenario === 'object' && experiment.scenario !== null - ? (appliedScenarioSchema.safeParse(experiment.scenario).data + ? (archivedAppliedScenarioSchema.safeParse(experiment.scenario).data ?.patientZeroAgentId ?? null) : null; const knownAgents = new Set(this.#state.agents.keys()); @@ -1008,7 +919,7 @@ export class SimulationService { ); this.#behaviorConfiguration = { ...structuredClone(importedBehavior.data), - locked: this.#completedTurnCount > 0, + locked: this.#completedTickCount > 0, }; } this.#recordModelConfigurationChanges( @@ -1184,13 +1095,19 @@ export class SimulationService { 'invalid_model_configuration', 'The selected reasoning profile is not advertised by this model.', ); - const agents = [...this.#state.agents.values()]; - const agent = agents[this.#cursor % agents.length]; - if (!agent) throw new Error('The development world has no agents.'); + const zero = this.#state.agents.get(this.#scenario.patientZeroAgentId); + if (!zero) throw new Error('The development world has no Agent Zero.'); this.#verificationBusy = true; try { - const result = await this.#provider.decide( - structuredClone(this.#buildObservation(agent.id)), + const result = await this.#swarmPlanner.plan( + this.#buildZeroStrategicObservation( + this.#state, + zero.id, + this.#completedTickCount + 1, + this.#virtualTime, + [], + ['initial'], + ), modelId, { reasoningProfile }, ); @@ -1200,400 +1117,12 @@ export class SimulationService { } } - async executeNextTurn(): Promise { - if (this.#scenario.cognitionMode === 'zero-swarm-v1') - throw new SimulationConflictError( - 'Zero-swarm execution supports whole simultaneous ticks only.', - ); - if (this.#completedTickCount > 0) - throw new SimulationConflictError( - 'Legacy sequential turns cannot run after a simultaneous tick.', - ); - if (this.#pendingFailedTurn) - throw new SimulationConflictError( - 'The failed turn must be retried or skipped before starting another turn.', - ); - return this.#executeTurnAttempt('initial'); - } - - /** Execute one atomic simultaneous tick for every active agent. */ - async executeNextTick(): Promise { - if (this.#scenario.cognitionMode === 'zero-swarm-v1') - return this.#executeZeroSwarmTick(); + /** Execute one atomic Zero strategy → worker reflex → world resolution tick. */ + async executeNextTick(): Promise { if (this.#busy || this.#verificationBusy) throw new SimulationConflictError( 'A simulation tick is already in progress.', ); - if ( - this.#pendingFailedTurn || - (this.#completedTurnCount > 0 && this.#completedTickCount === 0) - ) - throw new SimulationConflictError( - 'A simultaneous tick cannot start inside a legacy sequential experiment. Reset first.', - ); - if (this.#isTerminal()) - throw new SimulationConflictError( - 'This simulation has reached a terminal infection outcome. Reset before running another tick.', - ); - const tickNumber = this.#completedTickCount + 1; - const preTickState = this.#state; - const interval = seededTickIntervalMinutes( - this.#scenario.worldSeed, - tickNumber, - this.#scenario.minimumTickIntervalMinutes, - this.#scenario.maximumTickIntervalMinutes, - ); - const virtualTime = new Date( - new Date(this.#virtualTime).getTime() + interval * 60_000, - ).toISOString(); - const playerAdvance = advanceSimulatedPlayer( - preTickState, - this.#scenario.simulatedPlayer.seed, - tickNumber, - { createEventId: this.#createEventId, now: () => virtualTime }, - ); - const agents = [...playerAdvance.state.agents.values()]; - if (!agents.length) { - this.#commitTerminalPlayerTick( - playerAdvance, - tickNumber, - virtualTime, - interval, - ); - return []; - } - const unresolved = agents - .map(({ id }) => this.#resolvedModel(id)) - .filter(({ available }) => !available); - if (unresolved.length) - throw new SimulationValidationError( - 'models_unavailable', - 'Every agent requires an available compatible model before the experiment can run.', - ); - if (!this.#attemptAccounting.reserve(agents.length)) { - this.#status = 'budget-exhausted'; - throw new SimulationValidationError( - 'experiment_budget_exhausted', - 'The experiment does not have enough provider-attempt or credit-admission capacity for a complete tick.', - ); - } - - const order = seededTickOrder( - agents.map(({ id }) => id), - this.#scenario.worldSeed, - tickNumber, - ); - // Observation construction is synchronous. Temporarily point it at the - // uncommitted candidate so cancellation cannot expose or persist a partial - // player interval while every agent still observes the same frozen state. - this.#state = playerAdvance.state; - let observations: Map; - try { - observations = new Map( - agents.map(({ id }) => [ - id, - structuredClone(this.#buildObservation(id, playerAdvance.events)), - ]), - ); - } catch (error) { - this.#attemptAccounting.releaseReservations(); - throw error; - } finally { - this.#state = preTickState; - } - const controller = new AbortController(); - this.#busy = true; - this.#activeRequestController = controller; - this.#activeAgentId = null; - this.#cancellationRequested = false; - this.#status = 'waiting-for-model'; - const deadlineAtMs = Date.now() + OPENROUTER_PROVIDER_TIMEOUT_MS; - let dispatched: Awaited> = []; - try { - dispatched = await dispatchTickDecisions( - this.#provider, - agents.map(({ id }) => { - const resolved = this.#resolvedModel(id); - return { - agentId: id, - observation: observations.get(id)!, - modelId: resolved.modelId!, - reasoningProfile: resolved.reasoningProfile, - }; - }), - { - concurrency: Math.min(8, agents.length), - deadlineAtMs, - signal: controller.signal, - now: this.#now, - deferSuccessfulFinalization: true, - beginAttempt: (job, kind, attemptStartedAt) => { - const intendedTurnNumber = - this.#completedTurnCount + - Math.max(1, order.indexOf(job.agentId) + 1); - const details = { - agentId: job.agentId, - intendedTurnNumber, - intendedTickNumber: tickNumber, - kind, - startedAt: attemptStartedAt, - modelId: job.modelId, - reasoningProfile: job.reasoningProfile, - }; - const permitId = - kind === 'initial' - ? this.#attemptAccounting.startReserved(details) - : this.#attemptAccounting.startAdditional(details); - return permitId === null - ? null - : (completion) => - this.#attemptAccounting.finalize(permitId, completion); - }, - }, - ); - if (controller.signal.aborted) throw new SimulationTurnCancelledError(); - const byAgent = new Map( - dispatched.map((result) => [result.agentId, result]), - ); - const context = { - now: () => virtualTime, - createEventId: this.#createEventId, - createAllianceId: this.#createAllianceId, - createProposalId: this.#createProposalId, - communicationRangeKm: this.#scenario.communicationRangeKm, - patientZeroAgentId: this.#scenario.patientZeroAgentId, - tickNumber, - diplomacyRangeState: playerAdvance.state, - }; - const recordOrdinal = new Map( - order.map((agentId, index) => [ - agentId, - this.#completedTurnCount + index + 1, - ]), - ); - let state = playerAdvance.state; - const actionResults = new Map< - AgentId, - ReturnType['result'] - >(); - for (const agentId of order) { - const result = byAgent.get(agentId)!; - if (result.outcome === 'lost-tick') continue; - const applied = applyWorldAction( - state, - agentId, - result.decision.decision.worldAction, - context, - ); - state = applied.state; - actionResults.set(agentId, applied.result); - } - const communicationResults = new Map< - AgentId, - ReturnType['result'] - >(); - for (const agentId of order) { - const result = byAgent.get(agentId)!; - if (result.outcome === 'lost-tick') continue; - const applied = applyCommunication( - state, - playerAdvance.state, - agentId, - result.decision.decision.communication, - context, - ); - state = applied.state; - communicationResults.set(agentId, applied.result); - } - const diplomacyResults = new Map< - AgentId, - ReturnType['result'] - >(); - const diplomacyEvents = new Map(); - for (const agentId of order) { - const result = byAgent.get(agentId)!; - if (result.outcome === 'lost-tick') continue; - const before = state; - const applied = applyDiplomacy( - state, - agentId, - result.decision.decision.diplomacy, - recordOrdinal.get(agentId)!, - context, - ); - state = applied.state; - diplomacyResults.set(agentId, applied.result); - diplomacyEvents.set(agentId, allianceEventsSince(before, state)); - } - const beforeExpiration = state; - state = expireAllianceProposals( - state, - recordOrdinal.get(order.at(-1)!)!, - context, - ); - const expirationEvents = allianceEventsSince(beforeExpiration, state); - if (expirationEvents.length) { - const finalAgentId = order.at(-1)!; - diplomacyEvents.set(finalAgentId, [ - ...(diplomacyEvents.get(finalAgentId) ?? []), - ...expirationEvents, - ]); - } - const committedObservationEvents = state.events.slice( - preTickState.events.length, - ); - state = { - ...state, - events: state.events.slice(-MAX_WORLD_EVENT_HISTORY), - }; - const nextGoals = new Map(this.#agentGoals); - const goalResults = new Map(); - const nextMemories = new Map(this.#agentMemories); - const memoryResults = new Map(); - for (const agentId of order) { - const result = byAgent.get(agentId)!; - if (result.outcome === 'lost-tick') continue; - const applied = applyGoalRevision( - this.#agentGoals.get(agentId), - result.decision.decision.goalRevision, - tickNumber, - ); - goalResults.set(agentId, applied.result); - if (applied.goal) nextGoals.set(agentId, applied.goal); - else nextGoals.delete(agentId); - const appliedMemory = applyMemoryOperation( - this.#agentMemories.get(agentId) ?? [], - result.decision.decision.memoryOperation, - agentId, - tickNumber, - ); - memoryResults.set(agentId, appliedMemory.result); - nextMemories.set(agentId, appliedMemory.entries); - } - - const records = order.map((agentId, index) => { - const result = byAgent.get(agentId)!; - const base = { - turnNumber: this.#completedTurnCount + index + 1, - tickNumber, - tickPosition: index + 1, - virtualTime, - tickIntervalMinutes: interval, - agentId, - startedAt: result.attempts[0]?.startedAt ?? this.#now(), - completedAt: result.attempts.at(-1)?.completedAt ?? this.#now(), - observation: observations.get(agentId)!, - behavior: this.#behaviorFor(agentId), - modelAttempts: result.attempts, - allianceEvents: diplomacyEvents.get(agentId) ?? [], - }; - if (result.outcome === 'lost-tick') - return agentTurnRecordSchema.parse({ - ...base, - outcome: 'lost-tick', - failure: result.failure, - provider: result.attempts.at(-1)?.provider, - }); - const decision = result.decision.decision; - const actionResult = actionResults.get(agentId)!; - return agentTurnRecordSchema.parse({ - ...base, - outcome: actionResult.accepted ? 'accepted' : 'rejected', - worldAction: decision.worldAction, - communication: communicationIntentSchema.safeParse( - decision.communication, - ).data, - diplomacy: diplomacyIntentSchema.safeParse(decision.diplomacy).data, - goalRevision: decision.goalRevision, - goalRevisionResult: goalResults.get(agentId), - memoryOperation: decision.memoryOperation, - memoryOperationResult: memoryResults.get(agentId), - summary: decision.summary, - worldActionResult: actionResult, - communicationResult: communicationResults.get(agentId)!, - diplomacyResult: diplomacyResults.get(agentId)!, - provider: result.decision.metadata, - }); - }); - if (controller.signal.aborted) throw new SimulationTurnCancelledError(); - this.#commitCompletedTick( - records, - state, - tickNumber, - virtualTime, - interval, - order, - nextGoals, - nextMemories, - playerAdvance.events, - committedObservationEvents, - ); - for (const result of dispatched) - if (result.outcome === 'completed') result.finalizeAttempt?.(); - this.#status = this.#attemptAccounting.snapshot().exhausted - ? 'budget-exhausted' - : 'paused'; - return records; - } catch (error) { - const cancelledAttempt = - controller.signal.aborted || - error instanceof SimulationTurnCancelledError; - const failure: ProviderFailure = cancelledAttempt - ? { - code: 'cancelled', - message: 'The model request was cancelled by the operator.', - retryable: false, - } - : { - code: 'simulation-validation', - message: 'The simultaneous tick could not be committed safely.', - retryable: true, - }; - for (const result of dispatched) - if (result.outcome === 'completed') { - if (cancelledAttempt) result.finalizeAttempt?.('completed'); - else result.finalizeAttempt?.('provider-error', failure); - } - if ( - controller.signal.aborted || - (error && - typeof error === 'object' && - 'failure' in error && - (error as { failure?: ProviderFailure }).failure?.code === - 'cancelled') - ) { - this.#status = 'paused'; - throw new SimulationTurnCancelledError(); - } - throw error; - } finally { - this.#attemptAccounting.releaseReservations(); - this.#busy = false; - this.#activeRequestController = null; - this.#activeAgentId = null; - this.#cancellationRequested = false; - if (this.#status === 'waiting-for-model') - this.#status = this.#attemptAccounting.snapshot().exhausted - ? 'budget-exhausted' - : 'paused'; - if (this.#attemptAccounting.snapshot().exhausted) - this.#status = 'budget-exhausted'; - } - } - - /** - * The swarm path intentionally has no AgentTurnRecord: its safe telemetry is - * a SwarmTickRecord and it never invokes legacy social cognition. - */ - async #executeZeroSwarmTick(): Promise { - if (this.#busy || this.#verificationBusy) - throw new SimulationConflictError( - 'A simulation tick is already in progress.', - ); - if (!this.#swarmPlanner || !this.#reflexProvider) - throw new SimulationConflictError( - 'Zero-swarm execution requires a planner and reflex provider.', - ); if (this.#isTerminal()) throw new SimulationConflictError( 'This simulation has reached a terminal infection outcome. Reset before running another tick.', @@ -1633,7 +1162,7 @@ export class SimulationService { virtualTime, interval, ); - return []; + return null; } const resolvedZero = this.#resolvedModel(zero.id); if (!resolvedZero.available || !resolvedZero.modelId) @@ -1912,13 +1441,11 @@ export class SimulationService { }), ...(signals.length ? { signals } : {}), }); - const observationEvents = state.events.slice(preTickState.events.length); this.#state = { ...state, events: state.events.slice(-MAX_WORLD_EVENT_HISTORY), }; this.#pruneCapturedRosterState(); - this.#observationHistory.ingest(observationEvents); this.#completedTickCount = tickNumber; this.#completedSwarmDecisionCount += order.length; this.#virtualTime = virtualTime; @@ -1969,7 +1496,7 @@ export class SimulationService { this.#status = this.#attemptAccounting.snapshot().exhausted ? 'budget-exhausted' : 'paused'; - return []; + return tick; } catch (error) { if ( controller.signal.aborted || @@ -1993,53 +1520,6 @@ export class SimulationService { } } - #commitCompletedTick( - records: AgentTurnRecord[], - state: WorldState, - tickNumber: number, - virtualTime: string, - interval: number, - order: AgentId[], - goals: Map, - memories: Map, - playerEvents: SimulatedPlayerEvent[], - observationEvents: WorldEvent[], - ): void { - this.#state = state; - this.#pruneCapturedRosterState(); - this.#observationHistory.ingest(observationEvents); - this.#completedTickCount = tickNumber; - this.#virtualTime = virtualTime; - this.#lastTickIntervalMinutes = interval; - this.#resolutionOrder = [...order]; - this.#agentGoals = new Map( - [...goals].filter(([agentId]) => this.#state.agents.has(agentId)), - ); - this.#agentMemories = new Map( - [...memories].filter(([agentId]) => this.#state.agents.has(agentId)), - ); - this.#simulatedPlayerEvents = [ - ...this.#simulatedPlayerEvents, - ...structuredClone(playerEvents), - ].slice(-this.#experimentRetentionLimit * 2); - this.#completedTurnCount = - records.at(-1)?.turnNumber ?? this.#completedTurnCount; - this.#turns = retainCompleteTickGroups( - [...this.#turns, ...records], - MAX_TURN_HISTORY, - ); - this.#experimentTurns = retainCompleteTickGroups( - [...this.#experimentTurns, ...structuredClone(records)], - this.#experimentRetentionLimit, - ); - for (const record of records) this.#experimentMetrics.add(record); - this.#behaviorConfiguration = { - ...this.#behaviorConfiguration, - locked: true, - }; - this.#modelConfiguration = { ...this.#modelConfiguration, locked: false }; - } - #isTerminal(): boolean { return ( this.#status === 'patient-zero-captured' || @@ -2064,13 +1544,11 @@ export class SimulationService { events: playerAdvance.state.events.slice(-MAX_WORLD_EVENT_HISTORY), }; this.#pruneCapturedRosterState(); - this.#observationHistory.ingest(playerAdvance.events); this.#completedTickCount = tickNumber; this.#virtualTime = virtualTime; this.#lastTickIntervalMinutes = interval; this.#resolutionOrder = []; this.#activeAgentId = null; - this.#pendingFailedTurn = null; this.#simulatedPlayerEvents = [ ...this.#simulatedPlayerEvents, ...structuredClone(playerAdvance.events), @@ -2111,581 +1589,12 @@ export class SimulationService { }; } - async retryFailedTurn( - kind: 'manual-retry' | 'unattended-retry' = 'manual-retry', - ): Promise { - if (this.#completedTickCount > 0) - throw new SimulationConflictError( - 'Legacy retry is unavailable after a simultaneous tick.', - ); - if (!this.#pendingFailedTurn) - throw new SimulationConflictError( - 'There is no failed turn awaiting a manual retry.', - ); - return this.#executeTurnAttempt(kind); - } - - skipFailedTurn( - skipKind: 'manual' | 'unattended' = 'manual', - ): AgentTurnRecord { - if (this.#completedTickCount > 0) - throw new SimulationConflictError( - 'Legacy skip is unavailable after a simultaneous tick.', - ); - if (this.#busy || this.#verificationBusy) - throw new SimulationConflictError('A model request is still active.'); - const pending = this.#pendingFailedTurn; - if (!pending) - throw new SimulationConflictError( - 'There is no failed turn awaiting an operator decision.', - ); - const agents = [...this.#state.agents.values()]; - const record = agentTurnRecordSchema.parse({ - turnNumber: pending.turnNumber, - agentId: pending.agentId, - startedAt: pending.startedAt, - completedAt: this.#now(), - observation: pending.observation, - behavior: this.#behaviorFor(pending.agentId), - outcome: 'operator-skipped', - skipKind, - failure: pending.failure, - provider: pending.attempts.at(-1)?.provider, - modelAttempts: pending.attempts, - allianceEvents: [], - }); - this.#pendingFailedTurn = null; - this.#commitCompletedTurn( - record, - this.#state, - agents.length, - undefined, - [], - ); - this.#status = this.#attemptAccounting.snapshot().exhausted - ? 'budget-exhausted' - : 'paused'; - return record; - } - - async #executeTurnAttempt( - attemptKind: 'initial' | 'manual-retry' | 'unattended-retry', - ): Promise { - if (this.#busy || this.#verificationBusy) { - throw new SimulationConflictError( - 'Model execution is already in progress.', - ); - } - const agents = [...this.#state.agents.values()]; - const unresolved = agents - .map(({ id }) => this.#resolvedModel(id)) - .filter(({ available }) => !available); - if (unresolved.length) - throw new SimulationValidationError( - 'models_unavailable', - 'Every agent requires an available compatible model before the experiment can run.', - ); - const pending = this.#pendingFailedTurn; - const agent = pending - ? agents.find(({ id }) => id === pending.agentId) - : agents[this.#cursor % agents.length]; - if (!agent) throw new Error('The development world has no agents.'); - - this.#busy = true; - this.#activeAgentId = agent.id; - this.#activeRequestController = new AbortController(); - this.#cancellationRequested = false; - this.#status = 'waiting-for-model'; - const startedAt = pending?.startedAt ?? this.#now(); - const observation = - pending?.observation ?? this.#buildObservation(agent.id); - const turnNumber = pending?.turnNumber ?? this.#completedTurnCount + 1; - const attemptStartedAt = this.#now(); - let successfulAttemptStartedAt = attemptStartedAt; - let successfulAttemptKind: ModelAttempt['kind'] = attemptKind; - const resolvedModel = this.#resolvedModel(agent.id); - const selectedModel = resolvedModel.modelId!; - let providerResult: ProviderDecision | undefined; - let successfulProviderMetadata: ProviderMetadata | undefined; - let successfulAccountingPermitId: number | null = null; - const attemptHistory = [...(pending?.attempts ?? [])]; - const deadlineAtMs = Date.now() + OPENROUTER_PROVIDER_TIMEOUT_MS; - - try { - const providerObservation = structuredClone(observation); - - const automaticRecoveryAllowed = attemptKind === 'initial'; - let nextKind: ModelAttempt['kind'] = attemptKind; - let validationFeedback: ProviderFailure['validationCodes'] = - pending?.failure.validationCodes; - for (let automaticCall = 0; automaticCall < 2; automaticCall += 1) { - const currentAttemptStartedAt = this.#now(); - const accountingPermitId = this.#attemptAccounting.startAdditional({ - agentId: agent.id, - intendedTurnNumber: turnNumber, - kind: nextKind, - startedAt: currentAttemptStartedAt, - modelId: selectedModel, - reasoningProfile: resolvedModel.reasoningProfile, - }); - if (accountingPermitId === null) { - this.#status = 'budget-exhausted'; - throw new SimulationValidationError( - 'experiment_budget_exhausted', - 'The experiment does not have enough provider-attempt or credit-admission capacity.', - ); - } - successfulAttemptStartedAt = currentAttemptStartedAt; - successfulAttemptKind = nextKind; - let accountingMetadata: ProviderMetadata | undefined; - let accountingFailure: ProviderFailure | undefined; - try { - providerResult = await this.#provider.decide( - providerObservation, - selectedModel, - { - reasoningProfile: resolvedModel.reasoningProfile, - signal: this.#activeRequestController.signal, - deadlineAtMs, - validationFeedback, - }, - ); - successfulProviderMetadata = safeRecoveryProviderMetadata( - providerResult.metadata, - this.#provider.mode, - selectedModel, - ); - accountingMetadata = successfulProviderMetadata; - if (this.#activeRequestController.signal.aborted) - throw new AgentProviderError({ - code: 'cancelled', - message: 'The model request was cancelled by the operator.', - retryable: false, - model: selectedModel, - }); - break; - } catch (error) { - const providerError = asProviderError(error); - accountingMetadata = providerError.metadata ?? accountingMetadata; - if (providerError.failure.code === 'cancelled') { - // A response that already returned is completed provider work even - // when cancellation prevents the later world commit. Leave its - // finalization to the outer rollback boundary. - if (!successfulProviderMetadata) - accountingFailure = providerError.failure; - this.#status = 'paused'; - throw new SimulationTurnCancelledError(); - } - accountingFailure = providerError.failure; - const attemptProvider = providerError.metadata ?? { - provider: this.#provider.mode, - model: providerError.failure.model ?? selectedModel, - latencyMs: providerError.failure.latencyMs ?? 0, - }; - const attempt = { - attemptNumber: attemptHistory.length + 1, - kind: nextKind, - startedAt: currentAttemptStartedAt, - completedAt: this.#now(), - modelId: selectedModel, - reasoningProfile: resolvedModel.reasoningProfile, - failure: providerError.failure, - provider: attemptProvider, - } satisfies ModelAttempt; - attemptHistory.push(attempt); - const formatFailure = Boolean( - providerError.failure.validationCodes?.length, - ); - const transientFailure = - providerError.failure.retryable && - (providerError.failure.code === 'network' || - providerError.failure.code === 'timeout' || - providerError.failure.code === 'malformed-response' || - providerError.failure.code === 'unsupported-response' || - (providerError.failure.code === 'provider-http' && - [408, 429, 500, 502, 503, 504].includes( - providerError.failure.httpStatus ?? 0, - ))); - const retryDelayMs = automaticRetryDelayMs( - providerError.failure, - deadlineAtMs, - ); - const canRetry = - automaticRecoveryAllowed && - automaticCall === 0 && - Date.now() < deadlineAtMs && - Date.now() + retryDelayMs < deadlineAtMs && - (formatFailure || transientFailure); - if (canRetry) { - if (retryDelayMs > 0) { - try { - await waitForRetryBackoff( - retryDelayMs, - this.#activeRequestController.signal, - selectedModel, - ); - } catch (error) { - if ( - error instanceof AgentProviderError && - error.failure.code === 'cancelled' - ) { - this.#status = 'paused'; - throw new SimulationTurnCancelledError(); - } - throw error; - } - } - validationFeedback = providerError.failure.validationCodes; - nextKind = formatFailure - ? 'automatic-repair' - : 'automatic-transport-retry'; - continue; - } - this.#pendingFailedTurn = { - turnNumber, - agentId: agent.id, - startedAt, - observation, - failure: providerError.failure, - attempts: attemptHistory, - }; - const record = agentTurnRecordSchema.parse({ - turnNumber, - agentId: agent.id, - startedAt, - completedAt: this.#now(), - observation, - behavior: this.#behaviorFor(agent.id), - outcome: 'provider-error', - failure: providerError.failure, - provider: attemptProvider, - modelAttempts: attemptHistory, - allianceEvents: [], - }); - this.#status = - providerError.failure.code === 'configuration' - ? 'configuration-error' - : 'provider-error'; - return record; - } finally { - if (accountingFailure) - this.#attemptAccounting.finalize(accountingPermitId, { - outcome: - accountingFailure.code === 'cancelled' - ? 'cancelled' - : accountingFailure.code === 'timeout' - ? 'timeout' - : 'provider-error', - completedAt: this.#now(), - provider: accountingMetadata, - failure: accountingFailure, - }); - else successfulAccountingPermitId = accountingPermitId; - } - } - - if (!providerResult) - throw new Error('The provider completed without a decision result.'); - - const preActionState = this.#state; - const occurredAt = this.#now(); - const communicationInput = - providerResult.decision.communication ?? undefined; - const diplomacyInput = providerResult.decision.diplomacy ?? undefined; - const parsedCommunication = - communicationIntentSchema.safeParse(communicationInput); - const communication = parsedCommunication.success - ? parsedCommunication.data - : undefined; - const parsedDiplomacy = diplomacyIntentSchema.safeParse(diplomacyInput); - const diplomacy = parsedDiplomacy.success - ? parsedDiplomacy.data - : undefined; - const context = { - now: () => occurredAt, - createEventId: this.#createEventId, - createAllianceId: this.#createAllianceId, - createProposalId: this.#createProposalId, - communicationRangeKm: this.#scenario.communicationRangeKm, - patientZeroAgentId: this.#scenario.patientZeroAgentId, - diplomacyRangeState: preActionState, - }; - const appliedAction = applyWorldAction( - preActionState, - agent.id, - providerResult.decision.worldAction, - context, - ); - const appliedCommunication = applyCommunication( - appliedAction.state, - preActionState, - agent.id, - communicationInput, - context, - ); - const appliedDiplomacy = applyDiplomacy( - appliedCommunication.state, - agent.id, - diplomacyInput, - turnNumber, - context, - ); - const stateAfterExpiration = expireAllianceProposals( - appliedDiplomacy.state, - turnNumber, - context, - ); - const committedObservationEvents = stateAfterExpiration.events.slice( - preActionState.events.length, - ); - const candidateState = { - ...stateAfterExpiration, - events: stateAfterExpiration.events.slice(-MAX_WORLD_EVENT_HISTORY), - }; - const appliedGoal = applyGoalRevision( - this.#agentGoals.get(agent.id), - providerResult.decision.goalRevision, - turnNumber, - ); - const appliedMemory = applyMemoryOperation( - this.#agentMemories.get(agent.id) ?? [], - providerResult.decision.memoryOperation, - agent.id, - turnNumber, - ); - - const completed = { - turnNumber, - agentId: agent.id, - startedAt, - completedAt: this.#now(), - observation, - behavior: this.#behaviorFor(agent.id), - worldAction: providerResult.decision.worldAction, - communication, - diplomacy, - goalRevision: providerResult.decision.goalRevision, - goalRevisionResult: appliedGoal.result, - memoryOperation: providerResult.decision.memoryOperation, - memoryOperationResult: appliedMemory.result, - summary: providerResult.decision.summary, - worldActionResult: appliedAction.result, - communicationResult: appliedCommunication.result, - diplomacyResult: appliedDiplomacy.result, - allianceEvents: allianceEventsSince( - preActionState, - stateAfterExpiration, - ), - provider: providerResult.metadata, - modelAttempts: [ - ...attemptHistory, - { - attemptNumber: attemptHistory.length + 1, - kind: nextKind, - startedAt: successfulAttemptStartedAt, - completedAt: this.#now(), - modelId: selectedModel, - reasoningProfile: resolvedModel.reasoningProfile, - provider: providerResult.metadata, - }, - ], - }; - const record = agentTurnRecordSchema.parse( - appliedAction.result.accepted - ? { - ...completed, - outcome: 'accepted', - worldActionResult: appliedAction.result, - } - : { - ...completed, - outcome: 'rejected', - worldActionResult: appliedAction.result, - }, - ); - - this.#pendingFailedTurn = null; - this.#commitCompletedTurn( - record, - candidateState, - agents.length, - { - agentId: agent.id, - goal: appliedGoal.goal, - memoryEntries: appliedMemory.entries, - }, - committedObservationEvents, - ); - if (successfulAccountingPermitId !== null) { - this.#attemptAccounting.finalize(successfulAccountingPermitId, { - outcome: 'completed', - completedAt: this.#now(), - provider: successfulProviderMetadata, - }); - successfulAccountingPermitId = null; - } - this.#status = this.#attemptAccounting.snapshot().exhausted - ? 'budget-exhausted' - : 'paused'; - return record; - } catch (error) { - if ( - error instanceof SimulationTurnCancelledError || - !(error instanceof Error) || - error.name !== 'ZodError' - ) - throw error; - const failure: ProviderFailure = { - code: 'simulation-validation', - message: 'The model decision failed post-provider validation.', - retryable: true, - model: selectedModel, - }; - const attempt = { - attemptNumber: attemptHistory.length + 1, - kind: successfulAttemptKind, - startedAt: successfulAttemptStartedAt, - completedAt: this.#now(), - modelId: selectedModel, - reasoningProfile: resolvedModel.reasoningProfile, - failure, - provider: successfulProviderMetadata ?? { - provider: this.#provider.mode, - model: selectedModel, - latencyMs: 0, - }, - } satisfies ModelAttempt; - const attempts = [...attemptHistory, attempt]; - if (successfulAccountingPermitId !== null) { - this.#attemptAccounting.finalize(successfulAccountingPermitId, { - outcome: 'provider-error', - completedAt: this.#now(), - provider: attempt.provider, - failure, - }); - successfulAccountingPermitId = null; - } - this.#pendingFailedTurn = { - turnNumber, - agentId: agent.id, - startedAt, - observation, - failure, - attempts, - }; - this.#status = this.#attemptAccounting.snapshot().exhausted - ? 'budget-exhausted' - : 'provider-error'; - return agentTurnRecordSchema.parse({ - turnNumber, - agentId: agent.id, - startedAt, - completedAt: this.#now(), - observation, - behavior: this.#behaviorFor(agent.id), - outcome: 'provider-error', - failure, - provider: attempt.provider, - modelAttempts: attempts, - allianceEvents: [], - }); - } finally { - if (successfulAccountingPermitId !== null) { - const cancelled = Boolean( - this.#activeRequestController?.signal.aborted, - ); - if (cancelled && successfulProviderMetadata) { - this.#attemptAccounting.finalize(successfulAccountingPermitId, { - outcome: 'completed', - completedAt: this.#now(), - provider: successfulProviderMetadata, - }); - successfulAccountingPermitId = null; - } - } - if (successfulAccountingPermitId !== null) { - const cancelled = Boolean( - this.#activeRequestController?.signal.aborted, - ); - const failure: ProviderFailure = cancelled - ? { - code: 'cancelled', - message: 'The model request was cancelled by the operator.', - retryable: false, - model: selectedModel, - } - : { - code: 'simulation-validation', - message: 'The provider result could not be committed safely.', - retryable: true, - model: selectedModel, - }; - this.#attemptAccounting.finalize(successfulAccountingPermitId, { - outcome: cancelled ? 'cancelled' : 'provider-error', - completedAt: this.#now(), - provider: successfulProviderMetadata, - failure, - }); - } - this.#busy = false; - this.#activeAgentId = null; - this.#activeRequestController = null; - this.#cancellationRequested = false; - if (this.#status === 'waiting-for-model') { - this.#status = this.#provider.configured - ? 'paused' - : 'configuration-error'; - } - if ( - this.#status !== 'configuration-error' && - this.#attemptAccounting.snapshot().exhausted - ) - this.#status = 'budget-exhausted'; - } - } - - #commitCompletedTurn( - record: AgentTurnRecord, - state: WorldState, - agentCount: number, - goalCommit?: { - agentId: AgentId; - goal: AgentGoalState | undefined; - memoryEntries?: MemoryEntry[]; - }, - observationEvents: WorldEvent[] = [], - ): void { - const turns = [...this.#turns, record].slice(-MAX_TURN_HISTORY); - const cursor = (this.#cursor + 1) % agentCount; - - this.#state = state; - this.#observationHistory.ingest(observationEvents); - if (goalCommit?.goal) - this.#agentGoals.set(goalCommit.agentId, goalCommit.goal); - else if (goalCommit) this.#agentGoals.delete(goalCommit.agentId); - if (goalCommit?.memoryEntries) - this.#agentMemories.set(goalCommit.agentId, goalCommit.memoryEntries); - this.#behaviorConfiguration = { - ...this.#behaviorConfiguration, - locked: true, - }; - this.#turns = turns; - this.#experimentTurns = [ - ...this.#experimentTurns, - structuredClone(record), - ].slice(-this.#experimentRetentionLimit); - this.#experimentMetrics.add(record); - this.#completedTurnCount = record.turnNumber; - this.#cursor = cursor; - this.#modelConfiguration = { ...this.#modelConfiguration, locked: false }; - } - #recordModelConfigurationChanges( previous: ExperimentModelConfiguration, next: ExperimentModelConfiguration, ): void { const timestamp = this.#now(); - const effectiveTurn = this.#completedTurnCount + 1; + const effectiveTurn = this.#completedSwarmDecisionCount + 1; const events: ExperimentConfigurationEvent[] = []; if ( previous.globalModelId !== next.globalModelId || @@ -3123,10 +2032,13 @@ export class SimulationService { return { id: this.#experimentId, startedAt: this.#experimentStartedAt, - providerMode: this.#provider.mode, + providerMode: + this.#swarmPlanner.mode === 'openrouter-swarm' + ? 'openrouter' + : 'scripted-test', retentionLimit: this.#experimentRetentionLimit, - totalCompletedTurns: this.#completedTurnCount, - turns: this.#experimentTurns, + totalCompletedTurns: 0, + turns: [], initialAgents: this.#initialExperimentAgents, currentAgents: [...this.#state.agents.values()], configurationEvents: this.#configurationEvents, @@ -3155,14 +2067,6 @@ export class SimulationService { }; } - #behaviorFor(agentId: AgentId) { - const assignment = this.#behaviorConfiguration.assignments.find( - (candidate) => candidate.agentId === agentId, - ); - if (!assignment) throw new Error('The agent has no behavior assignment.'); - return assignment; - } - #resolvedModel(agentId: AgentId) { const override = this.#modelConfiguration.overrides.find( (candidate) => candidate.agentId === agentId, @@ -3199,692 +2103,6 @@ export class SimulationService { }; } - #historicalAgent(agentId: AgentId): Agent | undefined { - return ( - this.#state.agents.get(agentId) ?? - this.#initialExperimentAgents.find(({ id }) => id === agentId) - ); - } - - #buildObservation( - agentId: AgentId, - currentCandidatePlayerEvents: readonly SimulatedPlayerEvent[] = [], - ): AgentObservation { - const agent = this.#state.agents.get(agentId); - if (!agent) throw new Error('The active agent does not exist.'); - const currentGoal = this.#agentGoals.get(agentId) ?? null; - const currentMemory = this.#agentMemories.get(agentId) ?? []; - const stateFor = (cell: H3Cell) => { - const state = this.#state.hexes.get(cell); - if (!state) throw new Error('Observation cell is outside the world.'); - if (state.state === 'open') - return { - cell, - ...state, - controllerAllianceId: null, - effectiveColor: null, - } as const; - const controller = state.controllerAgentId; - return { - cell, - ...state, - controllerAllianceId: - controller === null - ? null - : (getAgentAlliance(this.#state, controller)?.id ?? null), - effectiveColor: - controller === null - ? null - : getEffectiveAgentColor(this.#state, controller), - } as const; - }; - const adjacentCells = gridDisk(agent.currentCell, 1) - .filter((cell) => cell !== agent.currentCell) - .map((cell) => h3CellSchema.parse(cell)) - .filter((cell) => this.#state.hexes.has(cell)) - .map(stateFor) - .toSorted( - (a, b) => - stableOrder( - `${this.#scenario.worldSeed}:${agent.id}:${this.#completedTurnCount + 1}:${a.cell}`, - ) - - stableOrder( - `${this.#scenario.worldSeed}:${agent.id}:${this.#completedTurnCount + 1}:${b.cell}`, - ), - ); - const recentMovements = this.#observationHistory - .movements(agent.id) - .map(({ fromCell, toCell, occurredAt }) => ({ - fromCell, - toCell, - occurredAt, - })); - const captureEligibility = getCaptureEligibility(this.#state, agent.id); - const actingAlliance = getAgentAlliance(this.#state, agent.id); - const patientZeroAgentId = this.#scenario.patientZeroAgentId; - const territory = this.#territoryScoreboard(); - const nearbyAgents = [...this.#state.agents.values()] - .filter((candidate) => candidate.id !== agent.id) - .map((candidate) => ({ - id: candidate.id, - name: candidate.name, - currentCell: candidate.currentCell, - distanceKm: - physicalDistanceKm(agent.currentCell, candidate.currentCell) ?? - Number.POSITIVE_INFINITY, - distance: gridRingDistance(agent.currentCell, candidate.currentCell), - allianceId: getAgentAlliance(this.#state, candidate.id)?.id ?? null, - allianceRelationship: actingAlliance?.memberAgentIds.includes( - candidate.id, - ) - ? ('allied' as const) - : ('not-allied' as const), - controlledCellCount: - territory.find(({ agentId }) => agentId === candidate.id) - ?.controlledCellCount ?? 0, - })) - .filter( - ({ id, distanceKm, allianceRelationship }) => - allianceRelationship === 'allied' || - distanceKm <= this.#scenario.communicationRangeKm || - id === patientZeroAgentId || - agent.id === patientZeroAgentId, - ) - .map((entry) => ({ - ...entry, - directMessageLegal: - entry.distanceKm <= this.#scenario.communicationRangeKm || - entry.id === patientZeroAgentId || - agent.id === patientZeroAgentId, - })) - .sort( - (a, b) => - Number(b.id === patientZeroAgentId) - - Number(a.id === patientZeroAgentId) || - a.distanceKm - b.distanceKm || - a.id.localeCompare(b.id), - ) - .slice(0, 8); - const recentEvents = this.#observationHistory.actions().map((event) => ({ - type: event.type, - agentId: event.agentId, - occurredAt: event.occurredAt, - summary: summarizeEvent(event, this.#state), - })); - const recentPublicMessages = this.#observationHistory - .publicMessages() - .map((event) => { - const sender = this.#historicalAgent(event.agentId); - if (!sender) throw new Error('A public-message sender does not exist.'); - return { - eventId: event.id, - senderId: sender.id, - senderName: sender.name, - message: event.message, - occurredAt: event.occurredAt, - }; - }); - const recentDirectMessages = this.#observationHistory - .directMessages(agent.id) - .map((event) => { - const sender = this.#historicalAgent(event.agentId); - const recipient = this.#historicalAgent(event.recipientId); - if (!sender || !recipient) - throw new Error('A communication participant does not exist.'); - return { - eventId: event.id, - senderId: sender.id, - senderName: sender.name, - recipientId: recipient.id, - recipientName: recipient.name, - direction: event.agentId === agent.id ? 'outbound' : 'inbound', - message: event.message, - occurredAt: event.occurredAt, - distance: event.distance, - } as const; - }); - const recentAllianceMessages = this.#observationHistory - .allianceMessages(agent.id) - .map((event) => { - const sender = this.#historicalAgent(event.agentId); - if (!sender) - throw new Error('An alliance-message sender does not exist.'); - return { - eventId: event.id, - senderId: sender.id, - senderName: sender.name, - allianceId: event.allianceId, - message: event.message, - occurredAt: event.occurredAt, - }; - }); - const recentZeroMessages = this.#observationHistory - .zeroMessages(agent.id) - .map((event) => { - const sender = this.#historicalAgent(event.agentId); - if (!sender) throw new Error('A Zero-message sender does not exist.'); - return { - eventId: event.id, - senderId: sender.id, - senderName: sender.name, - recipientCount: event.recipientIds.length, - message: event.message, - occurredAt: event.occurredAt, - }; - }); - const recentControlChanges = this.#observationHistory - .controlChanges(agent.id) - .flatMap((event) => { - const gained = event.controllerAgentId === agent.id; - const otherAgentId = gained - ? event.previousControllerAgentId - : event.controllerAgentId; - if (otherAgentId === null) return []; - const otherAgent = this.#historicalAgent(otherAgentId); - if (!otherAgent) - throw new Error('A control-change participant does not exist.'); - return [ - { - eventId: event.id, - direction: gained ? ('gained' as const) : ('lost' as const), - otherAgentId, - otherAgentName: otherAgent.name, - cell: event.cell, - occurredAt: event.occurredAt, - }, - ]; - }); - const completePlayerPressureEvents = [ - ...this.#simulatedPlayerEvents, - ...currentCandidatePlayerEvents, - ]; - const recentPlayerThreats = this.#scenario.capabilities - .simulatedPlayerPressure - ? completePlayerPressureEvents - .filter( - ( - event, - ): event is Extract => - event.type === 'hex-disinfected', - ) - .map((event) => ({ - event, - distanceCells: gridRingDistance(agent.currentCell, event.cell), - affectedOwnTerritory: event.previousControllerAgentId === agent.id, - })) - .filter( - ({ distanceCells, affectedOwnTerritory }) => - affectedOwnTerritory || distanceCells <= 2, - ) - .slice(-6) - .map(({ event, distanceCells, affectedOwnTerritory }) => ({ - eventId: event.id, - kind: affectedOwnTerritory - ? ('territory-disinfected' as const) - : ('nearby-disinfection' as const), - cell: event.cell, - occurredAt: event.occurredAt, - distanceCells, - affectedOwnTerritory, - })) - : []; - const patientZeroPlayerThreats = this.#scenario.capabilities - .simulatedPlayerPressure - ? currentCandidatePlayerEvents - .filter( - ( - event, - ): event is Extract< - WorldEvent, - { type: 'hex-disinfected' | 'simulated-player-clean-blocked' } - > => - (event.type === 'hex-disinfected' || - event.type === 'simulated-player-clean-blocked') && - event.originatingTick === this.#completedTickCount + 1, - ) - .toSorted( - (left, right) => - left.occurredAt.localeCompare(right.occurredAt) || - left.id.localeCompare(right.id), - ) - .flatMap((event) => { - const referencedAgentId = - event.type === 'hex-disinfected' - ? event.previousControllerAgentId - : event.blockingAgentId; - if (referencedAgentId === null) return []; - const referencedAgent = this.#state.agents.get(referencedAgentId); - if (!referencedAgent) - throw new Error( - 'A simulated-player threat references an unknown agent.', - ); - const alliance = getAgentAlliance(this.#state, referencedAgentId); - const pressureContext = calculatePatientZeroPressureContext( - completePlayerPressureEvents, - referencedAgentId, - alliance?.memberAgentIds ?? null, - this.#completedTickCount + 1, - ); - return [ - event.type === 'hex-disinfected' - ? { - eventId: event.id, - kind: 'territory-disinfected' as const, - cell: event.cell, - occurredAt: event.occurredAt, - affectedAgentId: referencedAgent.id, - affectedAgentName: referencedAgent.name, - affectedAllianceId: alliance?.id ?? null, - affectedAllianceColor: alliance?.color ?? null, - pressureContext, - } - : { - eventId: event.id, - kind: 'occupied-clean-blocked' as const, - cell: event.cell, - occurredAt: event.occurredAt, - blockingAgentId: referencedAgent.id, - blockingAgentName: referencedAgent.name, - blockingAllianceId: alliance?.id ?? null, - blockingAllianceColor: alliance?.color ?? null, - pressureContext, - }, - ]; - }) - : []; - const captureAlerts = captureAlertsFrom(currentCandidatePlayerEvents); - return agentObservationSchema.parse({ - agentId: agent.id, - agentName: agent.name, - personality: agent.personality, - behavior: this.#behaviorFor(agent.id), - currentGoal: structuredClone(currentGoal), - goalAvailability: currentGoal - ? { - active: true, - availableOperations: ['keep', 'revise', 'complete', 'abandon'], - } - : { active: false, availableOperations: ['establish'] }, - currentMemory: structuredClone(currentMemory), - memoryAvailability: { - remember: currentMemory.length < MEMORY_ENTRY_LIMIT, - revisableMemoryIds: currentMemory.map(({ id }) => id), - forgettableMemoryIds: currentMemory.map(({ id }) => id), - }, - currentCell: stateFor(agent.currentCell), - captureEligibility, - actionAvailability: { - moveTargetCellIds: adjacentCells.map(({ cell }) => cell), - moveOptions: adjacentCells.map((destination) => { - const controllerAlliance = destination.controllerAgentId - ? getAgentAlliance(this.#state, destination.controllerAgentId) - : undefined; - const relationship = - destination.state === 'open' - ? ('open' as const) - : destination.controllerAgentId === agent.id - ? ('self' as const) - : actingAlliance && controllerAlliance?.id === actingAlliance.id - ? ('allied' as const) - : ('other' as const); - return { - targetCell: destination.cell, - direction: geographicDirectionBetweenCells( - agent.currentCell, - destination.cell, - ), - destinationState: destination.state, - controllerRelationship: relationship, - recentlyOccupied: recentMovements.some( - ({ toCell }) => toCell === destination.cell, - ), - nearbyAgentCount: nearbyAgents.filter( - ({ currentCell }) => currentCell === destination.cell, - ).length, - }; - }), - infect: - this.#state.hexes.get(agent.currentCell)?.state === 'open' - ? { available: true } - : { - available: false, - reason: 'current-cell-already-infected', - }, - capture: captureEligibility.eligible - ? { available: true } - : { available: false, reason: captureEligibility.blockedReason }, - wait: { available: true }, - }, - diplomacyAvailability: this.#diplomacyAvailability(agent.id), - communicationAvailability: { - public: { available: true, playerVisible: true }, - direct: { - eligibleRecipientAgentIds: nearbyAgents - .filter(({ directMessageLegal }) => directMessageLegal) - .map(({ id }) => id), - }, - alliance: actingAlliance - ? { available: true, allianceId: actingAlliance.id } - : { available: false, allianceId: null }, - zero: { available: agent.id === patientZeroAgentId }, - }, - adjacentCells, - nearbyAgents, - recentEvents, - recentPublicMessages, - recentDirectMessages, - recentAllianceMessages, - recentZeroMessages, - patientZero: { - agentId: patientZeroAgentId, - agentName: - (patientZeroAgentId - ? this.#state.agents.get(patientZeroAgentId)?.name - : null) ?? null, - isPatientZero: agent.id === patientZeroAgentId, - directRangeBypass: patientZeroAgentId !== null, - }, - patientZeroGlobalView: - agent.id === patientZeroAgentId - ? { - agents: [...this.#state.agents.values()].map((candidate) => ({ - id: candidate.id, - name: candidate.name, - currentCell: candidate.currentCell, - allianceId: - getAgentAlliance(this.#state, candidate.id)?.id ?? null, - controlledCellCount: - territory.find(({ agentId: id }) => id === candidate.id) - ?.controlledCellCount ?? 0, - personality: candidate.personality, - strategyId: this.#behaviorFor(candidate.id).strategyId, - })), - individualTerritory: territory, - allianceTerritory: this.#allianceTerritorySummaries(), - alliances: [...(this.#state.alliances?.values() ?? [])], - activeAllianceProposals: [ - ...(this.#state.pendingAllianceProposals?.values() ?? []), - ], - diplomacyFeasibility: [], - diplomacySummary: this.#patientZeroDiplomacySummary(), - recentStrategicEvents: this.#observationHistory - .allianceEvents(RECENT_ZERO_STRATEGIC_EVENT_LIMIT) - .map((event) => ({ - event, - summary: summarizeAllianceEvent(event, this.#state), - })), - recentTerritoryChanges: this.#observationHistory.captures(), - playerThreatFeed: this.#scenario.capabilities - .simulatedPlayerPressure - ? { - events: selectMostRecentPatientZeroThreats( - patientZeroPlayerThreats, - ), - totalEventCount: patientZeroPlayerThreats.length, - truncated: - patientZeroPlayerThreats.length > - PATIENT_ZERO_PLAYER_THREAT_FEED_LIMIT, - } - : null, - } - : null, - territoryScoreboard: this.#territoryScoreboard(), - actingAllianceId: getAgentAlliance(this.#state, agent.id)?.id ?? null, - actingAlliance: - this.#allianceTerritorySummaries().find( - ({ allianceId }) => - allianceId === getAgentAlliance(this.#state, agent.id)?.id, - ) ?? null, - activeAlliances: this.#allianceTerritorySummaries(), - inboundAllianceProposals: [ - ...(this.#state.pendingAllianceProposals?.values() ?? []), - ].filter(({ recipientAgentId }) => recipientAgentId === agent.id), - outboundAllianceProposals: [ - ...(this.#state.pendingAllianceProposals?.values() ?? []), - ].filter(({ proposerAgentId }) => proposerAgentId === agent.id), - recentAllianceEvents: this.#observationHistory - .allianceEvents(RECENT_ALLIANCE_EVENT_LIMIT) - .map((event) => ({ - event, - summary: summarizeAllianceEvent(event, this.#state), - })), - recentControlChanges, - playerPressure: { - enabled: this.#scenario.capabilities.simulatedPlayerPressure, - recentThreats: recentPlayerThreats, - }, - ...(captureAlerts.length ? { captureAlerts } : {}), - recentMovements, - }); - } - - #diplomacyAvailability( - agentId: AgentId, - blockedRecipientLimit: number = WORLD_SCENARIO_LIMITS.maximumNearbyAgentObservations, - ) { - const proposals = [ - ...(this.#state.pendingAllianceProposals?.values() ?? []), - ]; - const actingAlliance = getAgentAlliance(this.#state, agentId); - const hasOutgoing = proposals.some( - ({ proposerAgentId }) => proposerAgentId === agentId, - ); - const eligibleRecipientAgentIds: AgentId[] = []; - const blockedRecipients: Array<{ - agentId: AgentId; - reason: - | 'current-ally' - | 'out-of-range' - | 'outgoing-proposal-exists' - | 'incoming-proposal-exists' - | 'alliance-to-alliance-merge'; - }> = []; - for (const candidateId of [...this.#state.agents.keys()].toSorted()) { - let reason: (typeof blockedRecipients)[number]['reason'] | null = null; - if (candidateId === agentId) continue; - const eligibility = getProposalTargetEligibility( - this.#state, - agentId, - candidateId, - this.#scenario.communicationRangeKm, - this.#state, - ); - if (!eligibility.eligible) reason = eligibility.reason; - if (reason) { - if (blockedRecipients.length < blockedRecipientLimit) - blockedRecipients.push({ agentId: candidateId, reason }); - } else eligibleRecipientAgentIds.push(candidateId); - } - const acceptableProposalIds = proposals - .filter((proposal) => { - if (proposal.recipientAgentId !== agentId) return false; - const proposerAlliance = getAgentAlliance( - this.#state, - proposal.proposerAgentId, - ); - return ( - (proposal.proposerAllianceId === null - ? !proposerAlliance - : proposerAlliance?.id === proposal.proposerAllianceId) && - (proposal.recipientAllianceId === null - ? !actingAlliance - : actingAlliance?.id === proposal.recipientAllianceId) && - !(proposerAlliance && actingAlliance) - ); - }) - .map(({ id }) => id); - return { - neutral: { available: true as const }, - propose: eligibleRecipientAgentIds.length - ? { - available: true as const, - eligibleRecipientAgentIds, - blockedRecipients, - } - : { - available: false as const, - eligibleRecipientAgentIds: [], - blockedRecipients, - reason: hasOutgoing - ? 'A pending outgoing formal proposal already exists.' - : 'No eligible formal proposal recipient is available.', - }, - accept: acceptableProposalIds.length - ? { available: true as const, acceptableProposalIds } - : { - available: false as const, - acceptableProposalIds: [], - reason: 'No acceptable inbound formal alliance proposal exists.', - }, - leave: actingAlliance - ? { available: true as const, allianceId: actingAlliance.id } - : { - available: false as const, - allianceId: null, - reason: 'The agent is not currently in an alliance.', - }, - }; - } - - #patientZeroDiplomacySummary() { - const eligiblePairs: Array<{ - proposerId: AgentId; - recipientId: AgentId; - }> = []; - const acceptableProposals: Array<{ - agentId: AgentId; - proposalId: AllianceProposalId; - }> = []; - const leaveAvailableAgentIds: AgentId[] = []; - const blockedCounts = new Map(); - const blockers: Array<{ - proposerId: AgentId; - recipientId: AgentId; - reason: - | 'current-ally' - | 'out-of-range' - | 'outgoing-proposal-exists' - | 'incoming-proposal-exists' - | 'alliance-to-alliance-merge'; - }> = []; - for (const proposerAgentId of [...this.#state.agents.keys()].toSorted()) { - const availability = this.#diplomacyAvailability( - proposerAgentId, - WORLD_SCENARIO_LIMITS.maximumAgents, - ); - for (const recipientAgentId of availability.propose - .eligibleRecipientAgentIds) - eligiblePairs.push({ - proposerId: proposerAgentId, - recipientId: recipientAgentId, - }); - for (const blocked of availability.propose.blockedRecipients) { - blockedCounts.set( - blocked.reason, - (blockedCounts.get(blocked.reason) ?? 0) + 1, - ); - blockers.push({ - proposerId: proposerAgentId, - recipientId: blocked.agentId, - reason: blocked.reason, - }); - } - for (const proposalId of availability.accept.acceptableProposalIds) - acceptableProposals.push({ - agentId: proposerAgentId, - proposalId, - }); - if (availability.leave.available) - leaveAvailableAgentIds.push(proposerAgentId); - } - const blockerPriority = [ - 'out-of-range', - 'alliance-to-alliance-merge', - 'current-ally', - 'incoming-proposal-exists', - 'outgoing-proposal-exists', - ] as const; - blockers.sort( - (a, b) => - blockerPriority.indexOf(a.reason) - blockerPriority.indexOf(b.reason) || - a.proposerId.localeCompare(b.proposerId) || - a.recipientId.localeCompare(b.recipientId), - ); - const displayedEligiblePairs: typeof eligiblePairs = []; - const proposerBuckets = [...this.#state.agents.keys()] - .toSorted() - .map((proposerAgentId) => ({ - proposerAgentId, - recipientAgentIds: eligiblePairs - .filter((pair) => pair.proposerId === proposerAgentId) - .map(({ recipientId }) => recipientId), - })) - .filter(({ recipientAgentIds }) => recipientAgentIds.length > 0); - if (proposerBuckets.length) { - const offset = - (this.#completedTickCount * - PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.displayedEligiblePairs) % - proposerBuckets.length; - const rotated = [ - ...proposerBuckets.slice(offset), - ...proposerBuckets.slice(0, offset), - ]; - for ( - let recipientIndex = 0; - displayedEligiblePairs.length < - PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.displayedEligiblePairs; - recipientIndex += 1 - ) { - let added = false; - for (const bucket of rotated) { - const recipientAgentId = bucket.recipientAgentIds[recipientIndex]; - if (!recipientAgentId) continue; - displayedEligiblePairs.push({ - proposerId: bucket.proposerAgentId, - recipientId: recipientAgentId, - }); - added = true; - if ( - displayedEligiblePairs.length === - PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.displayedEligiblePairs - ) - break; - } - if (!added) break; - } - } - return { - eligiblePairCount: eligiblePairs.length, - displayedEligiblePairs, - eligiblePairsTruncated: - eligiblePairs.length > displayedEligiblePairs.length, - acceptableProposals: acceptableProposals.slice( - 0, - PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.acceptableProposals, - ), - acceptableProposalCount: acceptableProposals.length, - acceptableProposalsTruncated: - acceptableProposals.length > - PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.acceptableProposals, - leaveAvailableAgentIds: leaveAvailableAgentIds.slice( - 0, - PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.leaveAvailableAgentIds, - ), - leaveAvailableCount: leaveAvailableAgentIds.length, - leaveAvailableTruncated: - leaveAvailableAgentIds.length > - PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.leaveAvailableAgentIds, - blockedCounts: blockerPriority.flatMap((reason) => { - const count = blockedCounts.get(reason); - return count ? [{ reason, count }] : []; - }), - blockerExamples: blockers.slice( - 0, - PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.blockerExamples, - ), - }; - } - #territoryScoreboard() { const counts = new Map( [...this.#state.agents.keys()].map((id) => [id, 0]), @@ -4035,82 +2253,6 @@ function summarizeAllianceEvent( return `The proposal from ${name(event.proposerAgentId)} to ${name(event.recipientAgentId)} was ${event.reason}.`; } -function automaticRetryDelayMs( - failure: ProviderFailure, - deadlineAtMs: number, -): number { - if (failure.code !== 'provider-http' || failure.httpStatus !== 429) return 0; - const retryAfterMs = failure.retryAfterMs; - if (retryAfterMs !== undefined && Date.now() + retryAfterMs < deadlineAtMs) - return retryAfterMs; - return OPENROUTER_429_FALLBACK_BACKOFF_MS; -} - -function waitForRetryBackoff( - delayMs: number, - signal: AbortSignal, - model: string, -): Promise { - return new Promise((resolve, reject) => { - if (signal.aborted) { - reject(cancelledBackoffError(model)); - return; - } - const timeout = setTimeout(() => { - signal.removeEventListener('abort', cancel); - resolve(); - }, delayMs); - const cancel = () => { - clearTimeout(timeout); - reject(cancelledBackoffError(model)); - }; - signal.addEventListener('abort', cancel, { once: true }); - }); -} - -function cancelledBackoffError(model: string): AgentProviderError { - return new AgentProviderError({ - code: 'cancelled', - message: 'The model request was cancelled by the operator.', - retryable: false, - model, - }); -} - -function asProviderError(error: unknown): { - failure: ProviderFailure; - metadata?: AgentProviderError['metadata']; -} { - if (error instanceof AgentProviderError) { - return { failure: error.failure, metadata: error.metadata }; - } - return { - failure: { - code: 'network', - message: 'The model provider failed unexpectedly.', - retryable: true, - }, - }; -} - -function retainCompleteTickGroups( - records: AgentTurnRecord[], - limit: number, -): AgentTurnRecord[] { - if (records.length <= limit) return records; - const groups = new Map(); - for (const record of records) { - const key = record.tickNumber ?? record.turnNumber; - groups.set(key, [...(groups.get(key) ?? []), record]); - } - const retained: AgentTurnRecord[] = []; - for (const group of [...groups.values()].reverse()) { - if (retained.length > 0 && retained.length + group.length > limit) break; - retained.unshift(...group); - } - return retained; -} - export function applyGoalRevision( current: AgentGoalState | undefined, requested: RequestedGoalRevision | undefined, diff --git a/apps/game-api/src/swarm-comparison.test.ts b/apps/game-api/src/swarm-comparison.test.ts index 5f9199b..211c40d 100644 --- a/apps/game-api/src/swarm-comparison.test.ts +++ b/apps/game-api/src/swarm-comparison.test.ts @@ -9,15 +9,14 @@ describe('runOfflineComparison', () => { ); }); - it('covers every comparison mode and retains engine capture telemetry', async () => { + it('compares Jev workers with the deterministic worker baseline', async () => { const report = await runOfflineComparison({ seeds: ['worker-capture-spawn'], tickCap: 3, }); expect(report.variants.map(({ variant }) => variant)).toEqual([ - 'legacy-multi-agent', - 'zero-swarm-v1', - 'deterministic-worker-baseline', + 'zero-swarm-jev', + 'zero-swarm-deterministic-workers', ]); for (const variant of report.variants) { const run = variant.runs[0]!; @@ -26,12 +25,12 @@ describe('runOfflineComparison', () => { expect(run.final.captures).toBeGreaterThan(0); } const swarm = report.variants.find( - ({ variant }) => variant === 'zero-swarm-v1', + ({ variant }) => variant === 'zero-swarm-jev', )!; expect(swarm.aggregate.totalGenerativeAttempts).toBeGreaterThan(0); expect(swarm.aggregate.totalReflexAttempts).toBeGreaterThan(0); const baseline = report.variants.find( - ({ variant }) => variant === 'deterministic-worker-baseline', + ({ variant }) => variant === 'zero-swarm-deterministic-workers', )!; expect(baseline.aggregate.totalReflexAttempts).toBe(0); expect(baseline.aggregate.totalProviderAttempts).toBe( @@ -49,16 +48,4 @@ describe('runOfflineComparison', () => { ).toEqual(expect.arrayContaining([expect.any(Object)])); expect(report.costDisclaimer).toContain('no authoritative billed'); }); - - it('continues legacy observations after captured message participants leave the roster', async () => { - const report = await runOfflineComparison({ - seeds: ['worker-capture-spawn'], - tickCap: 12, - }); - const legacy = report.variants.find( - ({ variant }) => variant === 'legacy-multi-agent', - )!.runs[0]!; - expect(legacy.samples).toHaveLength(12); - expect(legacy.final.captures).toBeGreaterThan(0); - }); }); diff --git a/apps/game-api/src/swarm-comparison.ts b/apps/game-api/src/swarm-comparison.ts index 00d46bf..e9d84f7 100644 --- a/apps/game-api/src/swarm-comparison.ts +++ b/apps/game-api/src/swarm-comparison.ts @@ -1,5 +1,4 @@ import { - BrowserTestAgentProvider, ReflexProviderError, type PlannerOptions, type ReflexDecisionOptions, @@ -21,7 +20,7 @@ import { SimulationService } from './simulation-service'; import type { CompiledReflexObservation } from './reflex-execution'; export type OfflineComparisonVariant = - 'legacy-multi-agent' | 'zero-swarm-v1' | 'deterministic-worker-baseline'; + 'zero-swarm-jev' | 'zero-swarm-deterministic-workers'; export interface OfflineComparisonOptions { /** Defaults include a known deterministic capture case. */ @@ -351,9 +350,6 @@ function sample( completionTokens: outputTokens, totalTokens: inputTokens + outputTokens, })), - ...snapshot.turns - .filter(({ tickNumber }) => tickNumber === snapshot.tickNumber) - .flatMap(({ provider }) => (provider ? [provider] : [])), ]; return { tick: snapshot.tickNumber, @@ -384,12 +380,7 @@ function sample( (total, item) => total + item.latencyMs, 0, ), - generativeAttempts: - swarm?.planSource === 'zero-llm' - ? 1 - : snapshot.turns.filter( - ({ tickNumber }) => tickNumber === snapshot.tickNumber, - ).length, + generativeAttempts: swarm?.planSource === 'zero-llm' ? 1 : 0, zeroPlans: swarm?.planSource === 'zero-llm' ? 1 : 0, reflexDecisions: reflex.length, workerStalls: @@ -410,20 +401,17 @@ function sample( function createService(variant: OfflineComparisonVariant, seed: string) { const planner = new OfflinePlanner(); const service = new SimulationService({ - provider: new BrowserTestAgentProvider(), swarmPlanner: planner, // Kept for the ordinary swarm variant and snapshot provider status. The // deterministic baseline uses the explicit server-side selector below and // never invokes this provider. reflexProvider: new OfflineReflex('semantic'), - ...(variant === 'deterministic-worker-baseline' + ...(variant === 'zero-swarm-deterministic-workers' ? { deterministicWorkerCandidateSelector: selectGreedyCandidate } : {}), now: () => '2026-08-13T12:00:00.000Z', createEventId: deterministicIds('1'), createExperimentId: deterministicIds('2'), - createAllianceId: deterministicIds('3'), - createProposalId: deterministicIds('4'), }); service.setCompatibleModels([model]); const request = service.getDefaultWorldSetup(); @@ -431,8 +419,6 @@ function createService(variant: OfflineComparisonVariant, seed: string) { const roster = generateDeterministicRoster(8, 'worker-capture-roster'); service.applyWorldSetup({ ...request, - cognitionMode: - variant === 'legacy-multi-agent' ? 'legacy-multi-agent' : 'zero-swarm-v1', roster, patientZeroAgentId: roster[1]!.id, worldSeed: `offline-world-${seed}`, @@ -446,6 +432,8 @@ function createService(variant: OfflineComparisonVariant, seed: string) { overrides: [], locked: false, }, + // This transitional scenario field remains required until PR 2 removes + // legacy behavior configuration from the shared world setup schema. behaviorConfiguration: { ...request.behaviorConfiguration, assignments: assignBehavior( @@ -551,7 +539,7 @@ function aggregate( }; } -/** Run the three comparison modes through SimulationService without a network call. */ +/** Run the Jev and deterministic-worker variants without a network call. */ export async function runOfflineComparison( options: OfflineComparisonOptions = {}, ): Promise { @@ -564,9 +552,8 @@ export async function runOfflineComparison( `tickCap must be an integer between 1 and ${MAX_TICK_CAP}.`, ); const variants: OfflineComparisonVariant[] = [ - 'legacy-multi-agent', - 'zero-swarm-v1', - 'deterministic-worker-baseline', + 'zero-swarm-jev', + 'zero-swarm-deterministic-workers', ]; return { formatVersion: 1, @@ -574,7 +561,7 @@ export async function runOfflineComparison( seeds, tickCap, providerDisclaimer: - 'All providers in this report are deterministic offline fakes. Legacy mode uses BrowserTestAgentProvider, whose scripted social policy is not a behavioral substitute for a live legacy model. Token and latency fields describe only fake-provider telemetry, never live inference usage.', + 'All providers in this report are deterministic offline fakes. Token and latency fields describe only fake-provider telemetry, never live inference usage.', costDisclaimer: 'Fake-provider costCredits are accounting fixtures only. This report contains no authoritative billed monetary cost.', variants: await Promise.all( @@ -585,11 +572,9 @@ export async function runOfflineComparison( return { variant, providerKind: - variant === 'legacy-multi-agent' - ? 'BrowserTestAgentProvider' - : variant === 'zero-swarm-v1' - ? 'OfflinePlanner + semantic OfflineReflex' - : 'OfflinePlanner + deterministic legal-candidate selector', + variant === 'zero-swarm-jev' + ? 'OfflinePlanner + semantic OfflineReflex' + : 'OfflinePlanner + deterministic legal-candidate selector', runs, aggregate: aggregate(runs), }; diff --git a/apps/game-api/src/swarm-diagnostics-cli.test.ts b/apps/game-api/src/swarm-diagnostics-cli.test.ts index 244dd13..4923cdb 100644 --- a/apps/game-api/src/swarm-diagnostics-cli.test.ts +++ b/apps/game-api/src/swarm-diagnostics-cli.test.ts @@ -7,7 +7,7 @@ describe('swarm diagnostic projection', () => { const snapshot = { tickNumber: 1, scenario: { - cognitionMode: 'zero-swarm-v1', + swarmArchitectureVersion: 'zero-swarm-v1', patientZeroAgentId: 'zero', simulatedPlayer: { enabled: false, profile: 'casual-cleaner' }, }, diff --git a/apps/game-api/src/swarm-diagnostics-cli.ts b/apps/game-api/src/swarm-diagnostics-cli.ts index bc20016..ed3affa 100644 --- a/apps/game-api/src/swarm-diagnostics-cli.ts +++ b/apps/game-api/src/swarm-diagnostics-cli.ts @@ -18,7 +18,7 @@ export function summarizeSwarmSnapshot(snapshot: SimulationSnapshot) { }); return { tick: snapshot.tickNumber, - mode: snapshot.scenario.cognitionMode, + swarmArchitectureVersion: snapshot.scenario.swarmArchitectureVersion, infectedCells: snapshot.world.hexes.filter( ({ state }) => state === 'infected', ).length, diff --git a/apps/world-lab/src/components/world-lab.test.tsx b/apps/world-lab/src/components/world-lab.test.tsx index 5829855..532d056 100644 --- a/apps/world-lab/src/components/world-lab.test.tsx +++ b/apps/world-lab/src/components/world-lab.test.tsx @@ -1,220 +1,71 @@ -import { - act, - fireEvent, - render, - screen, - waitFor, - within, -} from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { gridDisk } from 'h3-js'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { - AGENT_DECISION_CONTRACT_VERSION, - experimentExportDocumentSchema, - assignBehavior, - modelCatalogResponseSchema, - agentTurnRecordSchema, - simulationSnapshotSchema, - NEUTRAL_AGENT_COLOR, - type SimulationSnapshot, -} from '@hexzero/shared'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { NEUTRAL_AGENT_COLOR, simulationSnapshotSchema } from '@hexzero/shared'; import { createDefaultAppliedScenario, createDevelopmentWorld, - defaultWorldSetupRequest, - generateDeterministicRoster, - previewWorldSetup, } from '@hexzero/world-engine'; import { WorldLab } from './world-lab'; -import { PERSONALITY_PRESETS } from './personality-presets'; -const mapLibreMock = vi.hoisted(() => ({ - renderMode: 'complete' as 'complete' | 'incomplete', - rejectSource: false, - rejectLayers: false, - duplicateFeatures: false, - autoRender: true, - pendingRenderCallbacks: [] as Array<() => void>, - mapClick: undefined as - | ((event: { features: Array<{ properties: { cell: string } }> }) => void) - | undefined, - mapBackgroundClick: undefined as (() => void) | undefined, - layers: [] as Array<{ - id: string; - paint?: Record; - }>, - queryRenderedFeatures: vi.fn(), - setData: vi.fn(), - latestSourceData: undefined as unknown, +vi.mock('./world-map', () => ({ + WorldMap: () =>
, })); -vi.mock('maplibre-gl', () => { - class Map { - source: - | { - data: { - features: Array<{ - properties: { cell: string; state: string; selected: boolean }; - }>; - }; - setData: (data: unknown) => void; - } - | undefined; - sourceLoaded = false; - layers = new Set(); - listeners = new globalThis.Map void>>(); - addControl() {} - addLayer(layer: { id: string; paint?: Record }) { - mapLibreMock.layers.push(layer); - if (!mapLibreMock.rejectLayers) this.layers.add(layer.id); - } - addSource( - id: string, - source: { - data: { - features: Array<{ - properties: { cell: string; state: string; selected: boolean }; - }>; - }; - }, - ) { - if (mapLibreMock.rejectSource) return; - const completeSourceUpdate = () => { - this.sourceLoaded = true; - this.emit('sourcedata', { sourceId: id, isSourceLoaded: true }); - }; - this.source = { - data: source.data, - setData: (data) => { - mapLibreMock.setData(data); - mapLibreMock.latestSourceData = data; - this.source!.data = data as typeof source.data; - this.sourceLoaded = false; - queueMicrotask(completeSourceUpdate); - }, - }; - mapLibreMock.latestSourceData = source.data; - queueMicrotask(completeSourceUpdate); - } - emit(event: string, eventData?: unknown) { - for (const listener of [...(this.listeners.get(event) ?? [])]) { - listener(eventData); - } - } - fitBounds() {} - getCanvas() { - return { style: { cursor: '' } }; - } - getLayer(id: string) { - return this.layers.has(id) ? { id } : undefined; - } - getSource() { - return this.source; - } - isSourceLoaded() { - return Boolean(this.source) && this.sourceLoaded; - } - queryRenderedFeatures(options: { layers: string[] }) { - mapLibreMock.queryRenderedFeatures(options); - if (!this.source || !this.layers.has('development-hex-fills')) return []; - const features = - mapLibreMock.renderMode === 'incomplete' - ? this.source.data.features.slice(0, -1) - : [...this.source.data.features]; - return mapLibreMock.duplicateFeatures && features[0] - ? [...features, features[0], features[0]] - : features; - } - on( - event: string, - layerOrCallback: unknown, - callback?: (event: { - features: Array<{ properties: { cell: string } }>; - }) => void, - ) { - if (event === 'style.load' && typeof layerOrCallback === 'function') { - queueMicrotask(() => layerOrCallback()); - } - if (event === 'click' && typeof callback === 'function') { - mapLibreMock.mapClick = callback; - } else if (event === 'click' && typeof layerOrCallback === 'function') { - mapLibreMock.mapBackgroundClick = layerOrCallback as () => void; - } else if ( - event !== 'style.load' && - typeof layerOrCallback === 'function' - ) { - const listeners = - this.listeners.get(event) ?? new Set<(event?: unknown) => void>(); - listeners.add(layerOrCallback as (event?: unknown) => void); - this.listeners.set(event, listeners); - } - } - off(event: string, layerOrCallback: unknown, callback?: () => void) { - if (typeof layerOrCallback === 'function') { - this.listeners.get(event)?.delete(layerOrCallback as () => void); - } - if (event === 'click' && callback) mapLibreMock.mapClick = undefined; - if (event === 'click' && !callback) - mapLibreMock.mapBackgroundClick = undefined; - } - triggerRepaint() { - const completeRender = () => this.emit('render'); - if (mapLibreMock.autoRender) queueMicrotask(completeRender); - else mapLibreMock.pendingRenderCallbacks.push(completeRender); - } - remove() {} - } - class Marker { - constructor(private options: { element: HTMLElement }) {} - setLngLat() { - return this; - } - addTo() { - document.body.append(this.options.element); - return this; - } - remove() { - this.options.element.remove(); - } - } - return { - setWorkerUrl: vi.fn(), - Map, - Marker, - LngLatBounds: class { - extend() { - return this; - } - }, - NavigationControl: class {}, - AttributionControl: class {}, - }; -}); - const world = createDevelopmentWorld({ generatedAt: '2026-08-13T12:00:00.000Z', }); -const HOSTILE_MESSAGE = ' Hold position.'; -const emptyTerritory = world.agents.map(({ id, name, color }) => ({ - agentId: id, - name, - color, - allianceId: null, - effectiveColor: NEUTRAL_AGENT_COLOR, - controlledCellCount: 0, -})); -const initial = simulationSnapshotSchema.parse({ +const metrics = { + totalTurns: 0, + accepted: 0, + rejected: 0, + providerErrors: 0, + requestedMoves: 0, + requestedInfections: 0, + requestedCaptures: 0, + requestedWaits: 0, + acceptedMovements: 0, + successfullyInfectedCells: 0, + successfulCaptures: 0, + acceptedWaits: 0, + rejectedWorldActions: 0, + territoryGainedThroughInfection: 0, + territoryGainedThroughCapture: 0, + territoryLostThroughCapture: 0, + publicMessagesRequested: 0, + publicMessagesAccepted: 0, + publicMessagesRejected: 0, + directMessagesRequested: 0, + directMessagesDelivered: 0, + directMessagesRejected: 0, + publicMessagesSent: 0, + directMessagesSent: 0, + directMessagesReceived: 0, + uniqueVisitedCells: 0, + tokens: {}, + knownCostCredits: 0, + turnsWithUnknownCost: 0, +}; +const snapshot = simulationSnapshotSchema.parse({ world, scenario: createDefaultAppliedScenario('2026-08-13T12:00:00.000Z'), - turnNumber: 0, - nextAgentId: world.agents[0]!.id, + tickNumber: 0, + virtualTime: '2026-08-13T12:00:00.000Z', + lastTickIntervalMinutes: null, + resolutionOrder: [], activeAgentId: null, status: 'paused', providerMode: 'scripted-test', providerConfigured: true, + swarmProviderStatus: { + plannerMode: 'scripted-swarm-test', + plannerConfigured: true, + reflexMode: 'scripted-reflex-test', + reflexConfigured: true, + }, modelConfiguration: { globalModelId: 'deterministic-script', + globalReasoningProfile: 'provider-default', overrides: [], locked: false, }, @@ -224,4140 +75,288 @@ const initial = simulationSnapshotSchema.parse({ source: 'global', available: true, })), - turns: [], + swarmTicks: [], experiment: { id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', startedAt: '2026-08-13T12:00:00.000Z', - totalCompletedTurns: 0, - retainedTurns: 0, - droppedRecords: 0, - complete: true, metrics: { - aggregate: emptyMetrics(), - byAgent: world.agents.map(({ id }) => ({ - agentId: id, - metrics: emptyMetrics(), - })), + aggregate: metrics, + byAgent: world.agents.map(({ id }) => ({ agentId: id, metrics })), }, - currentTerritory: emptyTerritory, + currentTerritory: world.agents.map(({ id, name, color }) => ({ + agentId: id, + name, + color, + allianceId: null, + effectiveColor: NEUTRAL_AGENT_COLOR, + controlledCellCount: 0, + })), currentAlliances: [], + simulatedPlayerMetrics: { + movements: 0, + cellsDisinfected: 0, + blockedDisinfections: 0, + }, }, }); -function emptyMetrics() { - return { - totalTurns: 0, - accepted: 0, - rejected: 0, - providerErrors: 0, - requestedMoves: 0, - requestedInfections: 0, - requestedCaptures: 0, - requestedWaits: 0, - acceptedMovements: 0, - successfullyInfectedCells: 0, - successfulCaptures: 0, - acceptedWaits: 0, - rejectedWorldActions: 0, - territoryGainedThroughInfection: 0, - territoryGainedThroughCapture: 0, - territoryLostThroughCapture: 0, - publicMessagesRequested: 0, - publicMessagesAccepted: 0, - publicMessagesRejected: 0, - directMessagesRequested: 0, - directMessagesDelivered: 0, - directMessagesRejected: 0, - publicMessagesSent: 0, - directMessagesSent: 0, - directMessagesReceived: 0, - uniqueVisitedCells: 0, - tokens: {}, - knownCostCredits: 0, - turnsWithUnknownCost: 0, - }; -} - -function afterInfection(): SimulationSnapshot { - const agent = world.agents[0]!; - const event = { - id: '67aa21b9-fc78-4b04-9f92-9862bf346f96', - agentId: agent.id, - occurredAt: '2026-08-13T12:00:01.000Z', - type: 'hex-infected' as const, - cell: agent.currentCell, - controllerAgentId: agent.id, - }; - const adjacent = gridDisk(agent.currentCell, 1).find( - (cell) => - cell !== agent.currentCell && - world.hexes.some((hex) => hex.cell === cell), - )!; - const turn = { - turnNumber: 1, - agentId: agent.id, - startedAt: '2026-08-13T12:00:00.000Z', - completedAt: '2026-08-13T12:00:01.000Z', - observation: { - agentId: agent.id, - agentName: agent.name, - personality: agent.personality, - currentCell: { - cell: agent.currentCell, - state: 'open' as const, - controllerAgentId: null, - controllerAllianceId: null, - effectiveColor: null, - }, - captureEligibility: { - eligible: false as const, - blockedReason: 'capture-open-cell' as const, - }, - actionAvailability: { - moveTargetCellIds: [adjacent], - infect: { available: true as const }, - capture: { - available: false as const, - reason: 'capture-open-cell' as const, - }, - wait: { available: true as const }, - }, - adjacentCells: [ - { - cell: adjacent, - state: 'open' as const, - controllerAgentId: null, - controllerAllianceId: null, - effectiveColor: null, - }, - ], - nearbyAgents: [], - recentEvents: [], - recentPublicMessages: [], - recentDirectMessages: [], - territoryScoreboard: emptyTerritory, - actingAllianceId: null, - actingAlliance: null, - activeAlliances: [], - inboundAllianceProposals: [], - outboundAllianceProposals: [], - recentAllianceEvents: [], - recentControlChanges: [], - }, - outcome: 'accepted' as const, - worldAction: { type: 'infect' as const }, - summary: 'Infecting this open cell.', - worldActionResult: { accepted: true as const, event }, - communicationResult: { requested: false as const }, - diplomacyResult: { requested: false as const }, - provider: { - provider: 'scripted-test' as const, - model: 'test', - latencyMs: 0, - promptTokens: 0, - completionTokens: 0, - totalTokens: 0, - costCredits: 0, - }, - }; - return simulationSnapshotSchema.parse({ - ...initial, - world: { - ...world, - hexes: world.hexes.map((hex) => - hex.cell === agent.currentCell - ? { - ...hex, - state: 'infected' as const, - controllerAgentId: agent.id, - } - : hex, - ), - events: [event], - }, - turnNumber: 1, - nextAgentId: world.agents[1]!.id, - turns: [turn], - experiment: { - ...initial.experiment, - totalCompletedTurns: 1, - retainedTurns: 1, - firstRetainedTurn: 1, - lastRetainedTurn: 1, - metrics: { - aggregate: { - ...emptyMetrics(), - totalTurns: 1, - accepted: 1, - requestedInfections: 1, - successfullyInfectedCells: 1, - territoryGainedThroughInfection: 1, - uniqueVisitedCells: 1, - averageLatencyMs: 0, - }, - byAgent: initial.experiment.metrics.byAgent.map((entry, index) => - index === 0 - ? { - ...entry, - metrics: { - ...emptyMetrics(), - totalTurns: 1, - accepted: 1, - requestedInfections: 1, - successfullyInfectedCells: 1, - territoryGainedThroughInfection: 1, - uniqueVisitedCells: 1, - averageLatencyMs: 0, - }, - } - : entry, - ), - }, - currentTerritory: emptyTerritory.map((entry, index) => ({ - ...entry, - controlledCellCount: index === 0 ? 1 : 0, - })), - }, - }); -} - -function afterMessage(): SimulationSnapshot { - const sender = world.agents[0]!; - const recipient = world.agents[1]!; - const message = HOSTILE_MESSAGE; - const event = { - id: '67aa21b9-fc78-4b04-9f92-9862bf346f96', - agentId: sender.id, - recipientId: recipient.id, - occurredAt: '2026-08-13T12:00:01.000Z', - type: 'direct-message-sent' as const, - channel: 'direct' as const, - message, - distance: 2, - }; - const waitEvent = { - id: '77bb21b9-fc78-4b04-9f92-9862bf346f97', - agentId: sender.id, - occurredAt: '2026-08-13T12:00:01.000Z', - type: 'agent-waited' as const, - }; - const turn = { - turnNumber: 1, - agentId: sender.id, - startedAt: '2026-08-13T12:00:00.000Z', - completedAt: '2026-08-13T12:00:01.000Z', - observation: { - agentId: sender.id, - agentName: sender.name, - personality: sender.personality, - currentCell: { - cell: sender.currentCell, - state: 'open' as const, - controllerAgentId: null, - controllerAllianceId: null, - effectiveColor: null, - }, - captureEligibility: { - eligible: false as const, - blockedReason: 'capture-open-cell' as const, - }, - actionAvailability: { - moveTargetCellIds: [world.hexes[1]!.cell], - infect: { available: true as const }, - capture: { - available: false as const, - reason: 'capture-open-cell' as const, - }, - wait: { available: true as const }, - }, - adjacentCells: [ - { - cell: world.hexes[1]!.cell, - state: 'open' as const, - controllerAgentId: null, - controllerAllianceId: null, - effectiveColor: null, - }, - ], - nearbyAgents: [ - { - id: recipient.id, - name: recipient.name, - currentCell: recipient.currentCell, - distance: 2, - allianceId: null, - }, - ], - recentEvents: [], - recentPublicMessages: [], - recentDirectMessages: [], - territoryScoreboard: emptyTerritory, - actingAllianceId: null, - actingAlliance: null, - activeAlliances: [], - inboundAllianceProposals: [], - outboundAllianceProposals: [], - recentAllianceEvents: [], - recentControlChanges: [], - }, - outcome: 'accepted' as const, - worldAction: { type: 'wait' as const }, - communication: { - channel: 'direct' as const, - recipientId: recipient.id, - message, - }, - summary: 'Sending a nearby message.', - worldActionResult: { accepted: true as const, event: waitEvent }, - communicationResult: { - requested: true as const, - accepted: true as const, - event, - }, - diplomacyResult: { requested: false as const }, - provider: { - provider: 'scripted-test' as const, - model: 'test', - latencyMs: 0, - costCredits: 0, - }, - }; - return simulationSnapshotSchema.parse({ - ...initial, - world: { ...world, events: [waitEvent, event] }, - turnNumber: 1, - nextAgentId: recipient.id, - turns: [turn], - experiment: { - ...initial.experiment, - totalCompletedTurns: 1, - retainedTurns: 1, - firstRetainedTurn: 1, - lastRetainedTurn: 1, - metrics: { - aggregate: { - ...emptyMetrics(), - totalTurns: 1, - accepted: 1, - requestedWaits: 1, - acceptedWaits: 1, - directMessagesRequested: 1, - directMessagesDelivered: 1, - directMessagesSent: 1, - directMessagesReceived: 1, - uniqueVisitedCells: 1, - averageLatencyMs: 0, - }, - byAgent: initial.experiment.metrics.byAgent.map((entry, index) => ({ - ...entry, - metrics: - index === 0 - ? { - ...emptyMetrics(), - totalTurns: 1, - accepted: 1, - requestedWaits: 1, - acceptedWaits: 1, - directMessagesRequested: 1, - directMessagesDelivered: 1, - directMessagesSent: 1, - uniqueVisitedCells: 1, - averageLatencyMs: 0, - } - : index === 1 - ? { ...emptyMetrics(), directMessagesReceived: 1 } - : entry.metrics, - })), - }, - }, - }); -} - -function afterPublicMessage(): SimulationSnapshot { - const direct = afterMessage(); - const turn = direct.turns[0]!; - if (turn.outcome !== 'accepted' || !turn.worldActionResult.accepted) - throw new Error('Expected accepted fixture turn.'); - const event = { - id: '88cc21b9-fc78-4b04-9f92-9862bf346f98', - agentId: turn.agentId, - occurredAt: turn.completedAt, - type: 'public-message-sent' as const, - channel: 'public' as const, - message: HOSTILE_MESSAGE, - }; - return simulationSnapshotSchema.parse({ - ...direct, - world: { - ...direct.world, - events: [turn.worldActionResult.event, event], - }, - turns: [ - { - ...turn, - communication: { channel: 'public', message: HOSTILE_MESSAGE }, - communicationResult: { - requested: true, - accepted: true, - event, - }, - }, - ], - }); -} - -function afterCapture(): SimulationSnapshot { - const infected = afterInfection(); - const previous = world.agents[0]!; - const capturer = world.agents[1]!; - const cell = previous.currentCell; - const controllerDepartureCell = gridDisk(cell, 1).find( - (candidate) => - candidate !== cell && world.hexes.some(({ cell }) => cell === candidate), - )!; - const captureEvent = { - id: '77bb21b9-fc78-4b04-9f92-9862bf346f97', - agentId: capturer.id, - occurredAt: '2026-08-13T12:00:02.000Z', - type: 'hex-captured' as const, - cell, - controllerAgentId: capturer.id, - previousControllerAgentId: previous.id, - }; - const captureTurn = { - turnNumber: 2, - agentId: capturer.id, - startedAt: '2026-08-13T12:00:01.000Z', - completedAt: '2026-08-13T12:00:02.000Z', - observation: { - agentId: capturer.id, - agentName: capturer.name, - personality: capturer.personality, - currentCell: { - cell, - state: 'infected' as const, - controllerAgentId: previous.id, - controllerAllianceId: null, - effectiveColor: previous.color, - }, - captureEligibility: { eligible: true as const }, - actionAvailability: { - moveTargetCellIds: [world.hexes[1]!.cell], - infect: { - available: false as const, - reason: 'current-cell-already-infected' as const, - }, - capture: { available: true as const }, - wait: { available: true as const }, - }, - adjacentCells: [ - { - ...world.hexes[1]!, - controllerAllianceId: null, - effectiveColor: null, - }, - ], - nearbyAgents: [ - { - id: previous.id, - name: previous.name, - currentCell: controllerDepartureCell, - distance: 1, - allianceId: null, - }, - ], - recentEvents: [], - recentPublicMessages: [], - recentDirectMessages: [], - territoryScoreboard: emptyTerritory.map((entry, index) => ({ - ...entry, - controlledCellCount: index === 0 ? 1 : 0, - })), - actingAllianceId: null, - actingAlliance: null, - activeAlliances: [], - inboundAllianceProposals: [], - outboundAllianceProposals: [], - recentAllianceEvents: [], - recentControlChanges: [], - }, - outcome: 'accepted' as const, - worldAction: { type: 'capture' as const }, - summary: 'Capturing this contested hex.', - worldActionResult: { accepted: true as const, event: captureEvent }, - communicationResult: { requested: false as const }, - diplomacyResult: { requested: false as const }, - provider: { - provider: 'scripted-test' as const, - model: 'test', - latencyMs: 0, - costCredits: 0, - }, - }; - return simulationSnapshotSchema.parse({ - ...infected, - world: { - ...infected.world, - hexes: infected.world.hexes.map((hex) => - hex.cell === cell ? { ...hex, controllerAgentId: capturer.id } : hex, - ), - agents: infected.world.agents.map((agent) => - agent.id === capturer.id - ? { ...agent, currentCell: cell } - : agent.id === previous.id - ? { ...agent, currentCell: controllerDepartureCell } - : agent, - ), - events: [...infected.world.events, captureEvent], - }, - turnNumber: 2, - nextAgentId: world.agents[2]!.id, - turns: [...infected.turns, captureTurn], - experiment: { - ...infected.experiment, - totalCompletedTurns: 2, - retainedTurns: 2, - lastRetainedTurn: 2, - metrics: { - aggregate: { - ...infected.experiment.metrics.aggregate, - totalTurns: 2, - accepted: 2, - requestedCaptures: 1, - successfulCaptures: 1, - territoryGainedThroughCapture: 1, - territoryLostThroughCapture: 1, - }, - byAgent: infected.experiment.metrics.byAgent.map((entry, index) => - index === 0 - ? { - ...entry, - metrics: { - ...entry.metrics, - territoryLostThroughCapture: 1, - }, - } - : index === 1 - ? { - ...entry, - metrics: { - ...entry.metrics, - totalTurns: 1, - accepted: 1, - requestedCaptures: 1, - successfulCaptures: 1, - territoryGainedThroughCapture: 1, - uniqueVisitedCells: 1, - averageLatencyMs: 0, - }, - } - : entry, - ), - }, - currentTerritory: emptyTerritory.map((entry, index) => ({ - ...entry, - controlledCellCount: index === 1 ? 1 : 0, - })), - }, - }); -} - -function jsonResponse(value: unknown) { +function response(value: unknown) { return Promise.resolve(new Response(JSON.stringify(value), { status: 200 })); } -function completeTickResponse( - source: SimulationSnapshot, - tickNumber = Math.max(1, source.tickNumber || 1), -) { - const template = source.turns.at(-1) ?? afterInfection().turns[0]!; - const virtualTime = '2026-08-13T12:05:00.000Z'; - const records = source.world.agents.map((agent, index) => - agentTurnRecordSchema.parse({ - ...template, - turnNumber: (tickNumber - 1) * source.world.agents.length + index + 1, - tickNumber, - tickPosition: index + 1, - virtualTime, - tickIntervalMinutes: 5, - agentId: agent.id, - observation: { - ...template.observation, - agentId: agent.id, - agentName: agent.name, - }, - ...(index === 0 - ? {} - : { - outcome: 'accepted', - worldAction: { type: 'wait' }, - communication: undefined, - diplomacy: undefined, - summary: 'Waited for the next tick.', - worldActionResult: { - accepted: true, - event: { - id: `00000000-0000-4000-8000-${String(index).padStart(12, '0')}`, - agentId: agent.id, - occurredAt: virtualTime, - type: 'agent-waited', - }, - }, - communicationResult: { requested: false }, - diplomacyResult: { requested: false }, - }), - }), - ); - const snapshot = simulationSnapshotSchema.parse({ - ...source, +function committedTick(tickNumber: number) { + const virtualTime = `2026-08-13T12:${String(tickNumber).padStart(2, '0')}:00.000Z`; + const next = simulationSnapshotSchema.parse({ + ...snapshot, tickNumber, - turnNumber: records.at(-1)!.turnNumber, virtualTime, - lastTickIntervalMinutes: 5, - resolutionOrder: records.map(({ agentId }) => agentId), - turns: records, - experiment: { - ...source.experiment, - totalCompletedTurns: records.length, - retainedTurns: records.length, - firstRetainedTurn: 1, - lastRetainedTurn: records.length, - }, - }); - return { snapshot, tickNumber, records }; -} - -const compatibleCatalog = modelCatalogResponseSchema.parse({ - models: [ - { - id: 'example/alpha', - name: 'Alpha', - author: 'example', - contextLength: 32_768, - inputPricePerToken: '0.000001', - outputPricePerToken: '0.000002', - requestPrice: '0', - supportedParameters: ['max_tokens'], - createdAt: '2026-08-01T00:00:00.000Z', - isFree: false, - reasoning: { - mandatory: false, - supportedEfforts: ['xhigh', 'low', 'medium'], + lastTickIntervalMinutes: 1, + resolutionOrder: world.agents.map(({ id }) => id), + swarmTicks: [ + { + tickNumber, + virtualTime, + tickIntervalMinutes: 1, + plan: { + strategySummary: 'Hold the frontier.', + directives: [], + zeroActionCandidateId: 'zero_action_0', + }, + planSource: 'deterministic-fallback', + workers: [], }, - }, - { - id: 'sample/beta', - name: 'Beta Free', - author: 'sample', - contextLength: 65_536, - inputPricePerToken: '0', - outputPricePerToken: '0', - supportedParameters: ['max_tokens'], - createdAt: '2026-08-02T00:00:00.000Z', - isFree: true, - }, - ], - filteredOutCount: 12, - fetchedAt: '2026-08-15T12:00:00.000Z', - expiresAt: '2026-08-15T12:05:00.000Z', - stale: false, - requirements: { - input: 'text', - output: 'text', - endpoint: 'chat-completions', - requiredParameters: ['max_tokens'], - minimumContextLength: 16_384, - streaming: false, - }, -}); - -function openRouterSnapshot( - globalModelId: string | null = 'example/alpha', - overrides: SimulationSnapshot['modelConfiguration']['overrides'] = [], - globalReasoningProfile: SimulationSnapshot['modelConfiguration']['globalReasoningProfile'] = 'provider-default', -): SimulationSnapshot { - return simulationSnapshotSchema.parse({ - ...initial, - providerMode: 'openrouter', - modelConfiguration: { - globalModelId, - globalReasoningProfile, - overrides, - locked: false, - }, - resolvedModels: world.agents.map(({ id }) => { - const override = overrides.find(({ agentId }) => agentId === id); - const modelId = override?.modelId ?? globalModelId; - return { - agentId: id, - modelId, - reasoningProfile: override?.reasoningProfile ?? globalReasoningProfile, - source: override ? 'override' : modelId ? 'global' : 'missing', - available: Boolean(modelId), - ...(modelId ? {} : { issue: 'missing' }), - }; - }), + ], }); + return { snapshot: next, tickNumber, swarmTick: next.swarmTicks![0] }; } -function twelveAgentSnapshot(readyCount = 12): SimulationSnapshot { - const roster = generateDeterministicRoster(12, 'world-lab-twelve'); - const request = defaultWorldSetupRequest(); - const modelConfiguration = { - globalModelId: - 'example/alpha' as SimulationSnapshot['modelConfiguration']['globalModelId'], - globalReasoningProfile: 'provider-default' as const, - overrides: [], - locked: false, - }; - const behaviorConfiguration = { - ...request.behaviorConfiguration, - assignments: assignBehavior( - roster.map(({ id }) => id), - request.behaviorConfiguration.seed, - 'balanced-random', - ), - }; - const preview = previewWorldSetup({ - ...request, - radius: 12, - roster, - patientZeroAgentId: roster[0]!.id, - modelConfiguration, - behaviorConfiguration, - }); - if (!preview.feasible) throw new Error('Expected a feasible test scenario.'); - return simulationSnapshotSchema.parse({ - ...initial, - world: preview.world, - scenario: preview.scenario, - nextAgentId: preview.world.agents[0]!.id, - providerMode: 'openrouter', - modelConfiguration, - behaviorConfiguration, - resolvedModels: preview.world.agents.map(({ id }, index) => ({ - agentId: id, - modelId: index < readyCount ? 'example/alpha' : null, - reasoningProfile: 'provider-default', - source: index < readyCount ? 'global' : 'missing', - available: index < readyCount, - ...(index < readyCount ? {} : { issue: 'missing' }), - })), - agentGoals: preview.world.agents.map(({ id }) => ({ - agentId: id, - goal: null, - })), - agentMemories: preview.world.agents.map(({ id }) => ({ - agentId: id, - entries: [], - })), - experiment: { - ...initial.experiment, - metrics: { - aggregate: emptyMetrics(), - byAgent: preview.world.agents.map(({ id }) => ({ - agentId: id, - metrics: emptyMetrics(), - })), - }, - currentTerritory: preview.world.agents.map(({ id, name, color }) => ({ - agentId: id, - name, - color, - allianceId: null, - effectiveColor: NEUTRAL_AGENT_COLOR, - controlledCellCount: 0, - })), - currentAlliances: [], - }, - }); -} +afterEach(() => vi.unstubAllGlobals()); -function withPersonality( - snapshot: SimulationSnapshot, - agentId: string, - personality: string, -): SimulationSnapshot { - return simulationSnapshotSchema.parse({ - ...snapshot, - world: { - ...snapshot.world, - agents: snapshot.world.agents.map((agent) => - agent.id === agentId ? { ...agent, personality } : agent, - ), - }, +describe('WorldLab swarm workspace', () => { + it('shows the fixed swarm architecture and Agent Zero model readiness', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(() => response(snapshot)), + ); + render(); + expect(await screen.findByText('Swarm experiment')).toBeVisible(); + expect(screen.getByText('Architecture: zero-swarm-v1')).toBeVisible(); + expect(screen.getByText(/Zero: deterministic-script/)).toBeVisible(); + expect(screen.getByRole('button', { name: 'Start' })).toBeEnabled(); }); -} - -beforeEach(() => { - window.localStorage.clear(); - window.sessionStorage.clear(); - mapLibreMock.renderMode = 'complete'; - mapLibreMock.rejectSource = false; - mapLibreMock.rejectLayers = false; - mapLibreMock.duplicateFeatures = false; - mapLibreMock.autoRender = true; - mapLibreMock.pendingRenderCallbacks = []; - mapLibreMock.mapClick = undefined; - mapLibreMock.mapBackgroundClick = undefined; - mapLibreMock.layers = []; - mapLibreMock.queryRenderedFeatures.mockReset(); - mapLibreMock.setData.mockReset(); - mapLibreMock.latestSourceData = undefined; - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(initial)), - ); - vi.stubGlobal( - 'confirm', - vi.fn(() => true), - ); -}); - -afterEach(() => { - vi.unstubAllGlobals(); -}); - -async function openOverflow(user: ReturnType) { - const menu = await screen.findByLabelText('More World Lab actions'); - if (!menu.closest('details')?.hasAttribute('open')) await user.click(menu); -} - -async function selectMinimalFixtureExport( - user: ReturnType, -) { - await user.click(screen.getByRole('button', { name: 'Clear' })); - await user.click(screen.getByRole('checkbox', { name: /Ember/ })); - await user.click(screen.getByRole('checkbox', { name: 'lost tick' })); - await user.click(screen.getByRole('checkbox', { name: 'operator skipped' })); -} - -async function openAgentsWorkspace(user: ReturnType) { - await user.click(await screen.findByRole('button', { name: 'Agents' })); -} -describe('WorldLab', () => { - it('renders the zero swarm workspace without legacy social or personality surfaces', async () => { - const swarm = simulationSnapshotSchema.parse({ - ...initial, - scenario: { ...initial.scenario, cognitionMode: 'zero-swarm-v1' }, - swarmProviderStatus: { - plannerMode: 'scripted-swarm-test', - plannerConfigured: false, - reflexMode: 'scripted-reflex-test', - reflexConfigured: false, - }, - }); + it('applies a fresh swarm setup without an architecture selector', async () => { + const calls: string[] = []; vi.stubGlobal( 'fetch', - vi.fn(() => jsonResponse(swarm)), + vi.fn((input: RequestInfo | URL) => { + calls.push(String(input)); + if (String(input).endsWith('/setup/preview')) + return response({ + feasible: true, + scenario: snapshot.scenario, + world: snapshot.world, + }); + if (String(input).endsWith('/experiment/setup')) + return response({ snapshot }); + return response(snapshot); + }), ); const user = userEvent.setup(); render(); - - expect(await screen.findByText('Swarm planner')).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: 'Swarm' })).toBeInTheDocument(); - expect( - screen.queryByRole('tab', { name: 'Public chat' }), - ).not.toBeInTheDocument(); - expect( - screen.queryByText('Restore default personalities'), - ).not.toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Start' })).toBeEnabled(); - - await user.click(screen.getByRole('tab', { name: 'Scoreboard' })); - expect(await screen.findByLabelText('Swarm strategy')).toBeInTheDocument(); - expect(screen.queryByText('Alliance status')).not.toBeInTheDocument(); - - await openAgentsWorkspace(user); - expect(await screen.findByText(/Agent Zero model/)).toBeInTheDocument(); await user.click( - screen.getByRole('button', { name: /Open Agent Zero model Controller/ }), + await screen.findByRole('button', { name: 'Current swarm architecture' }), ); - expect( - screen.queryByRole('tab', { name: 'Behavior' }), - ).not.toBeInTheDocument(); - expect(screen.queryByText(/personality/i)).not.toBeInTheDocument(); - expect(screen.queryByText('Agent overrides')).not.toBeInTheDocument(); - expect( - screen.queryByText('Import saved experiment model assignments'), - ).not.toBeInTheDocument(); - - await user.click(screen.getByLabelText('Close model selection')); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'Export' })); - expect( - await screen.findByText(/Swarm exports include all agents/), - ).toBeInTheDocument(); - expect(screen.queryByText('Communication channel')).not.toBeInTheDocument(); - expect( - screen.queryByRole('group', { name: 'Agents' }), - ).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Cognition mode')).not.toBeInTheDocument(); await user.click(screen.getByRole('button', { name: 'Preview' })); - await waitFor(() => { - const request = vi - .mocked(fetch) - .mock.calls.find(([url]) => - String(url).endsWith('/experiment/export/preview'), - ); - expect(request).toBeDefined(); - expect(JSON.parse(String(request?.[1]?.body))).toMatchObject({ - agents: { mode: 'all' }, - turns: { mode: 'entire-retained' }, - level: 'full-safe', - }); - }); - }); - - it('migrates supported legacy browser preferences and rejects retired targets', async () => { - window.localStorage.setItem( - 'agentborne.world-lab.activity-dock', - 'collapsed', - ); - window.sessionStorage.setItem('agentborne.world-lab.run-target', '500'); - const first = render(); - await screen.findByRole('button', { name: 'Start' }); - await waitFor(() => { - expect( - window.localStorage.getItem('hexzero.world-lab.activity-dock'), - ).toBe('collapsed'); - expect( - window.sessionStorage.getItem('hexzero.world-lab.run-target'), - ).toBe('25'); - }); - first.unmount(); - render(); - await screen.findByRole('button', { name: 'Start' }); - }); - - it('renders all controls, status, H3 readiness, and eight visible markers', async () => { - render(); - expect( - await screen.findByText( - /H3 overlay ready · 127\/127 rendered cells · 8 agents/, - ), - ).toBeInTheDocument(); - expect(screen.getByTestId('world-map')).toHaveAttribute( - 'data-rendered-h3-cell-count', - '127', + await user.click( + screen.getByRole('button', { name: 'Apply / Create Experiment' }), ); - expect(mapLibreMock.queryRenderedFeatures).toHaveBeenCalledWith({ - layers: ['development-hex-fills'], - }); - expect( - screen.getByRole('heading', { name: 'World Lab' }), - ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Start' })).toBeEnabled(); - expect(screen.getByRole('button', { name: 'Single tick' })).toBeEnabled(); - fireEvent.click(screen.getByLabelText('More World Lab actions')); - expect(screen.getByRole('button', { name: 'Reset world' })).toBeEnabled(); - expect( - screen.getByRole('button', { name: 'Restore default personalities' }), - ).toBeEnabled(); - expect(screen.getByLabelText('Playback speed')).toBeInTheDocument(); - expect( - screen.getByRole('banner', { name: 'World Lab command bar' }), - ).toBeInTheDocument(); - expect(document.querySelector('.command-bar')).not.toBeInTheDocument(); - expect(document.querySelector('.provider-badge')).not.toBeInTheDocument(); - expect( - screen.queryByRole('button', { name: 'Export this agent' }), - ).not.toBeInTheDocument(); - expect( - screen - .getAllByRole('option', { name: /^(5|10|25|50|100)$/ }) - .map((option) => Number((option as HTMLOptionElement).value)), - ).toEqual([5, 10, 25, 50, 100]); - expect( - screen.getByLabelText('Experiment details. Tick 0, paused'), - ).toBeInTheDocument(); - expect( - screen.getByLabelText('Experiment details. Tick 0, paused'), - ).toHaveTextContent('0.0 credits'); - expect( - await screen.findAllByRole('button', { name: /Select agent/ }), - ).toHaveLength(8); - expect(screen.getByText('Deterministic test model')).toBeInTheDocument(); + expect(calls.some((call) => call.endsWith('/experiment/setup'))).toBe(true); }); - it('stops execution controls and explains exhausted provider attempts', async () => { - const exhausted = simulationSnapshotSchema.parse({ - ...initial, - status: 'budget-exhausted', - experiment: { - ...initial.experiment, - attemptAccounting: { - providerAttemptLimit: 8, - reservedPermits: 0, - attemptsStarted: 8, - attemptsFinalized: 8, - attemptsInFlight: 0, - remainingAttempts: 0, - creditLimit: '1', - reservationCreditsPerAttempt: '0.01', - unstartedReservedCredits: '0', - committedCreditExposure: '0.25', - remainingAdmissionCredits: '0.75', - knownFinalizedCostCredits: '0.000000004', - reservationOverageCredits: '0', - attemptsWithUnknownCost: 1, - exhausted: true, - exhaustionReason: 'provider-attempt-limit', - }, - }, - }); + it('commits a swarm tick through the tick endpoint', async () => { vi.stubGlobal( 'fetch', - vi.fn(() => jsonResponse(exhausted)), + vi.fn((input: RequestInfo | URL) => + String(input).includes('/tick') + ? response(committedTick(1)) + : response(snapshot), + ), ); - render(); - expect(await screen.findByRole('button', { name: 'Start' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Single tick' })).toBeDisabled(); - await userEvent - .setup() - .click( - screen.getByLabelText(/Experiment details\. Tick 0, budget exhausted/), - ); - expect(screen.getByText('Provider-attempt limit exhausted')).toBeVisible(); - expect(screen.getByText('0.000000004 credits')).toBeVisible(); - expect(screen.getByText('8 / 8')).toBeVisible(); - expect(screen.getByText('1', { selector: 'dd' })).toBeVisible(); - }); - - it('reconciles the authoritative exhausted snapshot after a budget 409', async () => { - const exhausted = simulationSnapshotSchema.parse({ - ...initial, - status: 'budget-exhausted', - experiment: { - ...initial.experiment, - attemptAccounting: { - providerAttemptLimit: 1, - reservedPermits: 0, - attemptsStarted: 1, - attemptsFinalized: 1, - attemptsInFlight: 0, - remainingAttempts: 0, - creditLimit: '0.01', - reservationCreditsPerAttempt: '0.01', - unstartedReservedCredits: '0', - committedCreditExposure: '0.01', - remainingAdmissionCredits: '0', - knownFinalizedCostCredits: '0', - reservationOverageCredits: '0', - attemptsWithUnknownCost: 1, - exhausted: true, - exhaustionReason: 'credit-admission-limit', - }, - }, - }); - let snapshotReads = 0; - const fetchMock = vi.fn((input, init) => { - if (String(input).includes('/tick') && init?.method === 'POST') - return Promise.resolve( - new Response( - JSON.stringify({ - error: { - code: 'experiment_budget_exhausted', - message: 'The complete tick cannot be reserved.', - }, - }), - { status: 409, headers: { 'content-type': 'application/json' } }, - ), - ); - snapshotReads += 1; - return jsonResponse(snapshotReads === 1 ? initial : exhausted); - }); - vi.stubGlobal('fetch', fetchMock); - render(); - const singleTick = await screen.findByRole('button', { - name: 'Single tick', - }); - expect(singleTick).toBeEnabled(); - await userEvent.setup().click(singleTick); - await waitFor(() => expect(singleTick).toBeDisabled()); - expect(screen.getByRole('button', { name: 'Start' })).toBeDisabled(); - expect( - screen.getByLabelText(/Experiment details\. Tick 0, budget exhausted/), - ).toBeInTheDocument(); - await userEvent - .setup() - .click( - screen.getByLabelText(/Experiment details\. Tick 0, budget exhausted/), - ); - expect(screen.getByText('Credit admission limit exhausted')).toBeVisible(); - expect(snapshotReads).toBe(2); - }); - - it('opens World setup only from the top-right overflow menu with map semantics', async () => { const user = userEvent.setup(); render(); - await screen.findByRole('button', { name: 'Start' }); - const executionControls = screen.getByRole('navigation', { - name: 'Simulation execution controls', - }); - expect( - within(executionControls).queryByRole('button', { name: /world setup/i }), - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('button', { name: /gps|locate|location|crosshair/i }), - ).not.toBeInTheDocument(); - - const overflowTrigger = screen.getByLabelText('More World Lab actions'); - await user.click(overflowTrigger); - const setupTrigger = screen.getByRole('button', { name: 'World setup' }); - expect(setupTrigger.querySelector('[data-icon="map"]')).not.toBeNull(); - await user.click(setupTrigger); - - expect( - screen.getByRole('dialog', { name: 'World Setup' }), - ).toBeInTheDocument(); - expect( - screen.getByLabelText('Minimum virtual minutes per tick'), - ).toHaveValue(5); - expect( - screen.getByLabelText('Maximum virtual minutes per tick'), - ).toHaveValue(10); - expect(screen.getByLabelText('Provider attempt limit')).toHaveValue(1000); - expect( - screen.getByLabelText('Unlimited provider attempts'), - ).not.toBeChecked(); - expect( - screen.getByLabelText('Unlimited experiment credit admission'), - ).toBeChecked(); - expect( - screen.getByLabelText('Experiment credit admission limit'), - ).toBeDisabled(); - expect( - screen.getByLabelText('Reserved credits per provider attempt'), - ).toHaveValue('0.01'); + await user.click( + await screen.findByRole('button', { name: 'Single tick' }), + ); expect( - screen.getByText(/does not guarantee the upstream provider bill/i), + await screen.findByRole('button', { + name: 'Experiment details. Tick 1, paused', + }), ).toBeVisible(); - for (const label of [ - 'World simulation seed', - 'Spawn assignment seed', - 'Roster generation seed', - 'Behavior assignment seed', - 'Simulated player seed', - ]) { - expect(screen.getByLabelText(label)).toHaveAttribute('maxlength', '80'); - } - const objective = screen.getByLabelText( - 'Active objective version (engine-owned)', - ); - expect(objective).toHaveValue('durable-influence-v2'); - expect(objective).toHaveAttribute('readonly'); - expect(objective).toHaveAccessibleDescription( - /Engine-owned version provenance.*not a seed and cannot be edited/i, - ); - expect(overflowTrigger.closest('details')).not.toHaveAttribute('open'); + expect(fetch).toHaveBeenCalledTimes(2); }); - it('lets an operator create a zero-swarm experiment from World Setup', async () => { - const swarm = simulationSnapshotSchema.parse({ - ...initial, - scenario: { ...initial.scenario, cognitionMode: 'zero-swarm-v1' }, - swarmProviderStatus: { - plannerMode: 'scripted-swarm-test', - plannerConfigured: false, - reflexMode: 'scripted-reflex-test', - reflexConfigured: false, - }, - }); - let previewMode: string | undefined; - let appliedMode: string | undefined; + it('resets from a committed swarm tick', async () => { + let reset = false; + vi.stubGlobal( + 'confirm', + vi.fn(() => true), + ); vi.stubGlobal( 'fetch', - vi.fn((input: RequestInfo | URL, init?: RequestInit) => { + vi.fn((input: RequestInfo | URL) => { const url = String(input); - if (url.endsWith('/setup/preview')) { - const request = JSON.parse(String(init?.body)) as ReturnType< - typeof defaultWorldSetupRequest - >; - previewMode = request.cognitionMode; - return jsonResponse(previewWorldSetup(request)); - } - if (url.endsWith('/experiment/setup')) { - appliedMode = ( - JSON.parse(String(init?.body)) as ReturnType< - typeof defaultWorldSetupRequest - > - ).cognitionMode; - return jsonResponse({ snapshot: swarm }); + if (url.includes('/reset')) { + reset = true; + return response({ snapshot }); } - return jsonResponse(initial); + return response(reset ? snapshot : committedTick(1).snapshot); }), ); const user = userEvent.setup(); render(); - expect( - await screen.findByText('Legacy multi-agent experiment'), - ).toBeVisible(); await user.click( - screen.getByRole('button', { - name: /Current execution mode: Legacy multi-agent/, - }), - ); - await user.selectOptions( - screen.getByLabelText('Cognition mode'), - 'zero-swarm-v1', - ); - await user.click(screen.getByRole('button', { name: 'Preview' })); - expect(previewMode).toBe('zero-swarm-v1'); - await user.click( - screen.getByRole('button', { name: 'Apply / Create Experiment' }), + await screen.findByRole('button', { name: 'Reset world' }), ); - expect(appliedMode).toBe('zero-swarm-v1'); - expect(await screen.findByText('Zero swarm v1 experiment')).toBeVisible(); - expect(screen.getByRole('tab', { name: 'Swarm' })).toBeInTheDocument(); - expect( - screen.queryByRole('tab', { name: 'Public chat' }), - ).not.toBeInTheDocument(); + expect(reset).toBe(true); + expect(await screen.findByText('Tick 0')).toBeVisible(); }); - it.each(['balanced-random', 'fully-random'] as const)( - 'previews and applies reproducible seed edits with regenerated %s assignments', - async (behaviorMode) => { - let previewBody: ReturnType | undefined; - let applyBody: ReturnType | undefined; - vi.stubGlobal( - 'fetch', - vi.fn((input: RequestInfo | URL, init?: RequestInit) => { - const url = String(input); - if (url.endsWith('/setup/preview')) { - previewBody = JSON.parse(String(init?.body)); - return jsonResponse(previewWorldSetup(previewBody!)); - } - if (url.endsWith('/experiment/setup')) { - applyBody = JSON.parse(String(init?.body)); - return jsonResponse({ snapshot: initial }); - } - return jsonResponse(initial); - }), - ); - const user = userEvent.setup(); - render(); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'World setup' })); - - if (behaviorMode === 'fully-random') - await user.selectOptions( - screen.getByLabelText('Behavior mode'), - behaviorMode, - ); - const seedEdits = [ - ['World simulation seed', 'ui-world-seed'], - ['Spawn assignment seed', 'ui-spawn-seed'], - ['Roster generation seed', 'ui-roster-seed'], - ['Behavior assignment seed', 'ui-behavior-seed'], - ] as const; - for (const [label, value] of seedEdits) { - const input = screen.getByLabelText(label); - await user.clear(input); - await user.type(input, value); - } - await user.click( - screen.getByRole('checkbox', { - name: 'Enable simulated player pressure', - }), - ); - const cleanerSeed = screen.getByLabelText('Simulated player seed'); - await user.clear(cleanerSeed); - await user.type(cleanerSeed, 'ui-cleaner-seed'); - - const expectedAssignments = assignBehavior( - initial.scenario.roster.map(({ id }) => id), - 'ui-behavior-seed', - behaviorMode, - ); - await user.click(screen.getByRole('button', { name: 'Preview' })); - expect(previewBody).toMatchObject({ - worldSeed: 'ui-world-seed', - spawnSeed: 'ui-spawn-seed', - rosterSeed: 'ui-roster-seed', - behaviorConfiguration: { - assignmentMode: behaviorMode, - seed: 'ui-behavior-seed', - assignments: expectedAssignments, - }, - simulatedPlayer: { enabled: true, seed: 'ui-cleaner-seed' }, - objectiveVersion: 'durable-influence-v3', - }); - await user.click( - screen.getByRole('button', { name: 'Apply / Create Experiment' }), - ); - expect(applyBody).toEqual(previewBody); - }, - ); - - it('preserves explicit manual assignments when its generation seed changes', async () => { - let previewBody: ReturnType | undefined; + it('runs to the exact tick cap without overlapping tick requests', async () => { + let current = snapshot; + let requests = 0; + let active = 0; + let maximumActive = 0; vi.stubGlobal( 'fetch', - vi.fn((input: RequestInfo | URL, init?: RequestInit) => { - if (String(input).endsWith('/setup/preview')) { - previewBody = JSON.parse(String(init?.body)); - return jsonResponse(previewWorldSetup(previewBody!)); - } - return jsonResponse(initial); + vi.fn((input: RequestInfo | URL) => { + if (!String(input).includes('/tick')) return response(current); + requests += 1; + active += 1; + maximumActive = Math.max(maximumActive, active); + current = committedTick(requests).snapshot; + return new Promise((resolve) => { + setTimeout(() => { + active -= 1; + resolve( + new Response(JSON.stringify(committedTick(requests)), { + status: 200, + }), + ); + }, 1); + }); }), ); const user = userEvent.setup(); render(); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'World setup' })); - await user.selectOptions(screen.getByLabelText('Behavior mode'), 'manual'); - const expectedAssignments = - initial.scenario.behaviorConfiguration.assignments.map((assignment) => ({ - ...assignment, - manual: true, - })); - const seed = screen.getByLabelText('Behavior assignment seed'); - await user.clear(seed); - await user.type(seed, 'manual-provenance-seed'); - await user.click(screen.getByRole('button', { name: 'Preview' })); - - expect(previewBody?.behaviorConfiguration).toEqual({ - ...initial.scenario.behaviorConfiguration, - assignmentMode: 'manual', - seed: 'manual-provenance-seed', - assignments: expectedAssignments, - }); - expect( - screen.getByText(/Manual choices override it and remain unchanged/), - ).toBeVisible(); - }); + await screen.findByRole('button', { name: 'Run to tick 25' }); + await user.selectOptions(screen.getByLabelText('Tick target'), '5'); + await user.selectOptions(screen.getByLabelText('Playback speed'), '250'); + await user.click(screen.getByRole('button', { name: 'Run to tick 5' })); + await new Promise((resolve) => setTimeout(resolve, 1_400)); + await waitFor(() => + expect( + screen.getByRole('button', { + name: 'Experiment details. Tick 5, paused', + }), + ).toBeVisible(), + ); + expect(requests).toBe(5); + expect(maximumActive).toBe(1); + await new Promise((resolve) => setTimeout(resolve, 300)); + expect(requests).toBe(5); + }, 10_000); - it('hydrates every seed and objective provenance from an authoritative reset snapshot', async () => { - const behaviorConfiguration = { - ...initial.scenario.behaviorConfiguration, - seed: 'reset-behavior-seed', - assignments: assignBehavior( - initial.scenario.roster.map(({ id }) => id), - 'reset-behavior-seed', - 'balanced-random', - ), - }; - const resetSnapshot = simulationSnapshotSchema.parse({ - ...initial, - scenario: { - ...initial.scenario, - worldSeed: 'reset-world-seed', - spawnSeed: 'reset-spawn-seed', - rosterSeed: 'reset-roster-seed', - behaviorConfiguration, - objectiveVersion: 'durable-influence-v3', - capabilities: { - ...initial.scenario.capabilities, - simulatedPlayerPressure: true, - }, - simulatedPlayer: { - enabled: true, - profile: 'casual-cleaner', - seed: 'reset-cleaner-seed', - }, - }, - behaviorConfiguration, - world: { - ...initial.world, - simulatedPlayer: { - profile: 'casual-cleaner', - currentCell: initial.world.hexes[0]!.cell, - metrics: { - movements: 0, - cellsDisinfected: 0, - blockedDisinfections: 0, - }, - }, - }, - }); + it('reconciles the authoritative snapshot after cancelling an active tick', async () => { + let resolveTick!: (value: Response) => void; vi.stubGlobal( 'fetch', - vi.fn((input: RequestInfo | URL) => - String(input).endsWith('/reset') - ? jsonResponse({ snapshot: resetSnapshot }) - : jsonResponse(initial), - ), - ); - const user = userEvent.setup(); - render(); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'Reset world' })); - await screen.findByTestId('simulated-player-activity'); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'World setup' })); - - expect(screen.getByLabelText('World simulation seed')).toHaveValue( - 'reset-world-seed', - ); - expect(screen.getByLabelText('Spawn assignment seed')).toHaveValue( - 'reset-spawn-seed', - ); - expect(screen.getByLabelText('Roster generation seed')).toHaveValue( - 'reset-roster-seed', - ); - expect(screen.getByLabelText('Behavior assignment seed')).toHaveValue( - 'reset-behavior-seed', - ); - expect(screen.getByLabelText('Simulated player seed')).toHaveValue( - 'reset-cleaner-seed', - ); - expect( - screen.getByLabelText('Active objective version (engine-owned)'), - ).toHaveValue('durable-influence-v3'); - }); - - it('previews an explicitly seeded casual cleaner without enabling it by default', async () => { - let previewBody: ReturnType | undefined; - vi.stubGlobal( - 'fetch', - vi.fn((input: RequestInfo | URL, init?: RequestInit) => { - const url = String(input); - if (url.endsWith('/setup/preview')) { - previewBody = JSON.parse(String(init?.body)); - return jsonResponse(previewWorldSetup(previewBody!)); - } - return jsonResponse(initial); - }), - ); - const user = userEvent.setup(); - render(); - await user.click(await screen.findByLabelText('More World Lab actions')); - await user.click(screen.getByRole('button', { name: 'World setup' })); - const enabled = screen.getByRole('checkbox', { - name: 'Enable simulated player pressure', - }); - expect(enabled).not.toBeChecked(); - const objective = screen.getByLabelText( - 'Active objective version (engine-owned)', - ); - expect(objective).toHaveValue('durable-influence-v2'); - await user.click(enabled); - const seed = screen.getByLabelText('Simulated player seed'); - await user.clear(seed); - await user.type(seed, 'ui-pressure-a'); - expect(objective).toHaveValue('durable-influence-v3'); - await user.click(screen.getByRole('button', { name: 'Preview' })); - expect(previewBody).toMatchObject({ - objectiveVersion: 'durable-influence-v3', - capabilities: { simulatedPlayerPressure: true }, - simulatedPlayer: { - enabled: true, - profile: 'casual-cleaner', - seed: 'ui-pressure-a', - }, - }); - expect(await screen.findByText(/1 seeded casual cleaner/)).toBeVisible(); - await user.click(enabled); - await user.click(screen.getByRole('button', { name: 'Preview' })); - expect(previewBody).toMatchObject({ - objectiveVersion: 'durable-influence-v2', - capabilities: { simulatedPlayerPressure: false }, - simulatedPlayer: { enabled: false }, - }); - expect(objective).toHaveValue('durable-influence-v2'); - expect(await screen.findByText(/player pressure disabled/)).toBeVisible(); - }); - - it('selects the seeded trail-hunter-v1 profile for a pressure experiment', async () => { - let previewBody: ReturnType | undefined; - vi.stubGlobal( - 'fetch', - vi.fn((input: RequestInfo | URL, init?: RequestInit) => { - if (String(input).endsWith('/setup/preview')) { - previewBody = JSON.parse(String(init?.body)); - return jsonResponse(previewWorldSetup(previewBody!)); - } - return jsonResponse(initial); - }), - ); - const user = userEvent.setup(); - render(); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'World setup' })); - await user.click( - screen.getByRole('checkbox', { - name: 'Enable simulated player pressure', - }), - ); - await user.selectOptions( - screen.getByLabelText('Simulated player profile'), - 'trail-hunter-v1', - ); - await user.click(screen.getByRole('button', { name: 'Preview' })); - - expect(previewBody?.simulatedPlayer).toMatchObject({ - enabled: true, - profile: 'trail-hunter-v1', - }); - expect(await screen.findByText(/1 seeded trail hunter/)).toBeVisible(); - }); - - it('shows omniscient casual-cleaner position identity and activity', async () => { - const pressured = simulationSnapshotSchema.parse({ - ...initial, - scenario: { - ...initial.scenario, - objectiveVersion: 'durable-influence-v3', - capabilities: { - ...initial.scenario.capabilities, - simulatedPlayerPressure: true, - }, - simulatedPlayer: { - enabled: true, - profile: 'casual-cleaner', - seed: 'ui-pressure-a', - }, - }, - world: { - ...initial.world, - simulatedPlayer: { - profile: 'casual-cleaner', - currentCell: initial.world.hexes[0]!.cell, - metrics: { - movements: 4, - cellsDisinfected: 2, - blockedDisinfections: 1, - }, - }, - }, - experiment: { - ...initial.experiment, - simulatedPlayerMetrics: { - movements: 4, - cellsDisinfected: 2, - blockedDisinfections: 1, - }, - }, - }); - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(pressured)), - ); - render(); - expect( - await screen.findByTestId('simulated-player-activity'), - ).toHaveTextContent('Cleaner 4 moved · 2 cleaned · 1 blocked'); - expect( - await screen.findByRole('img', { - name: 'Casual cleaner simulated player', - }), - ).toBeInTheDocument(); - }); - - it('shows Patient Zero in the roster, marker, inspector, setup selector, and private filter', async () => { - const patientZero = initial.world.agents[0]!; - const designated = simulationSnapshotSchema.parse({ - ...initial, - scenario: { ...initial.scenario, patientZeroAgentId: patientZero.id }, - }); - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(designated)), - ); - const user = userEvent.setup(); - render(); - expect(await screen.findByText('HEX-0')).toBeInTheDocument(); - expect( - await screen.findByRole('button', { - name: `Select agent ${patientZero.name}, Patient Zero`, - }), - ).toHaveClass('patient-zero'); - await user.click( - within( - screen.getByRole('complementary', { name: 'Agent roster' }), - ).getByRole('button', { name: new RegExp(patientZero.name) }), - ); - expect(screen.getByText('Patient Zero role')).toBeInTheDocument(); - await user.click(screen.getByRole('tab', { name: /Private comms/ })); - expect(screen.getByRole('button', { name: 'Zero' })).toBeInTheDocument(); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'World setup' })); - const selector = screen.getByLabelText('Patient Zero'); - expect(selector).toHaveValue(patientZero.id); - expect(within(selector).queryByRole('option', { name: 'None' })).toBeNull(); - await user.click(screen.getAllByRole('button', { name: 'Remove' })[0]!); - expect(selector).toHaveValue(initial.world.agents[1]!.id); - }); - - it('removes sequential recovery controls and explains per-tick provider cost', async () => { - const user = userEvent.setup(); - render(); - await screen.findByRole('button', { name: 'Start' }); - await user.click(screen.getByLabelText('More World Lab actions')); - expect(screen.queryByText('Unattended recovery')).not.toBeInTheDocument(); - expect( - screen.queryByRole('button', { name: 'Retry' }), - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('button', { name: 'Skip turn' }), - ).not.toBeInTheDocument(); - expect( - screen.getByText(/Each tick requests every active agent/), - ).toBeInTheDocument(); - }); - - it.each([ - { - snapshot: openRouterSnapshot('example/alpha'), - expected: '8/8 ready', - accessible: /8 of 8 agents ready/, - }, - { - snapshot: twelveAgentSnapshot(), - expected: '12/12 ready', - accessible: /12 of 12 agents ready/, - }, - { - snapshot: twelveAgentSnapshot(10), - expected: '10/12 ready', - accessible: /10 of 12 agents ready/, - }, - ])( - 'reports authoritative active-roster readiness as $expected', - async ({ snapshot, expected, accessible }) => { - vi.stubGlobal( - 'fetch', - vi.fn((input: RequestInfo | URL) => - String(input).endsWith('/models') - ? jsonResponse(compatibleCatalog) - : jsonResponse(snapshot), - ), - ); - const user = userEvent.setup(); - render(); - await openAgentsWorkspace(user); - const trigger = await screen.findByRole('button', { name: accessible }); - expect(trigger.querySelector('.setup-label')).toHaveTextContent(expected); - }, - ); - - it('does not count unapplied roster edits and reconciles readiness after reset without reload', async () => { - const twelve = twelveAgentSnapshot(); - vi.stubGlobal( - 'fetch', - vi.fn((input: RequestInfo | URL) => { - const url = String(input); - if (url.endsWith('/models')) return jsonResponse(compatibleCatalog); - if (url.endsWith('/reset')) - return jsonResponse({ - snapshot: openRouterSnapshot('example/alpha'), - }); - return jsonResponse(twelve); - }), - ); - const user = userEvent.setup(); - render(); - await openAgentsWorkspace(user); - let setupStatus = await screen.findByRole('button', { - name: /12 of 12 agents ready/, - }); - expect(setupStatus.querySelector('.setup-label')).toHaveTextContent( - '12/12 ready', - ); - await user.click(screen.getByLabelText('More World Lab actions')); - await user.click(screen.getByRole('button', { name: 'World setup' })); - await user.clear(screen.getByLabelText('Desired agent count')); - await user.type(screen.getByLabelText('Desired agent count'), '20'); - setupStatus = screen.getByRole('button', { - name: /12 of 12 agents ready/, - }); - expect(setupStatus.querySelector('.setup-label')).toHaveTextContent( - '12/12 ready', - ); - await user.click(screen.getByRole('button', { name: 'Close World Setup' })); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'Reset world' })); - setupStatus = await screen.findByRole('button', { - name: /8 of 8 agents ready/, - }); - expect(setupStatus.querySelector('.setup-label')).toHaveTextContent( - '8/8 ready', - ); - }); - - it('updates readiness from the authoritative applied scenario response without reload', async () => { - const eight = openRouterSnapshot('example/alpha'); - const twelve = twelveAgentSnapshot(); - const generatedRoster = twelve.scenario.roster; - vi.stubGlobal( - 'fetch', - vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { - const url = String(input); - if (url.endsWith('/models')) return jsonResponse(compatibleCatalog); - if (url.endsWith('/roster/generate')) - return jsonResponse({ roster: generatedRoster }); - if (url.endsWith('/setup/preview')) { - const preview = previewWorldSetup(JSON.parse(String(init?.body))); - return jsonResponse(preview); - } - if (url.endsWith('/experiment/setup')) - return jsonResponse({ snapshot: twelve }); - return jsonResponse(eight); - }), - ); - const user = userEvent.setup(); - render(); - await openAgentsWorkspace(user); - let setupStatus = await screen.findByRole('button', { - name: /8 of 8 agents ready/, - }); - expect(setupStatus.querySelector('.setup-label')).toHaveTextContent( - '8/8 ready', - ); - await user.click(screen.getByLabelText('More World Lab actions')); - await user.click(screen.getByRole('button', { name: 'World setup' })); - await user.clear(screen.getByLabelText('Desired agent count')); - await user.type(screen.getByLabelText('Desired agent count'), '12'); - await user.click( - screen.getByRole('button', { name: 'Generate desired roster' }), - ); - setupStatus = screen.getByRole('button', { - name: /8 of 8 agents ready/, - }); - expect(setupStatus.querySelector('.setup-label')).toHaveTextContent( - '8/8 ready', - ); - await user.click(screen.getByRole('button', { name: 'Preview' })); - await screen.findByText(/12 valid spawns/); - await user.click( - screen.getByRole('button', { name: 'Apply / Create Experiment' }), - ); - setupStatus = await screen.findByRole('button', { - name: /12 of 12 agents ready/, - }); - expect(setupStatus.querySelector('.setup-label')).toHaveTextContent( - '12/12 ready', - ); - }); - - it('runs exactly 19 additional ticks from tick 6 and never schedules tick 26', async () => { - vi.useFakeTimers(); - const at6 = completeTickResponse(initial, 6).snapshot; - let turnRequests = 0; - vi.stubGlobal( - 'fetch', - vi.fn(() => { - if (turnRequests === 0) { - turnRequests += 1; - return jsonResponse(at6); - } - const turnNumber = 6 + turnRequests; - turnRequests += 1; - if (turnNumber > 25) - return Promise.reject(new Error('tick 26 must not be requested')); - return jsonResponse(completeTickResponse(at6, turnNumber)); - }), - ); - - try { - render(); - await act(async () => { - await Promise.resolve(); - await Promise.resolve(); - }); - expect(screen.getByText('Tick 6')).toBeInTheDocument(); - - await act(async () => { - screen.getByRole('button', { name: 'Run to tick 25' }).click(); - }); - for (let expectedTurn = 7; expectedTurn <= 25; expectedTurn += 1) { - await act(async () => { - await vi.advanceTimersByTimeAsync(1_000); - }); - } - - expect(screen.getByText('Tick 25')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Start' })).toBeEnabled(); - expect(turnRequests - 1).toBe(19); - expect(fetch).toHaveBeenCalledTimes(20); - - await act(async () => { - await vi.advanceTimersByTimeAsync(10_000); - }); - expect(turnRequests - 1).toBe(19); - expect(fetch).toHaveBeenCalledTimes(20); - } finally { - vi.useRealTimers(); - } - }, 15_000); - - it('starts a fresh trail-hunter experiment after setup even when the prior experiment was at tick 50', async () => { - try { - const at50 = simulationSnapshotSchema.parse({ - ...initial, - tickNumber: 50, - turnNumber: 400, - virtualTime: '2026-08-13T16:10:00.000Z', - lastTickIntervalMinutes: 5, - resolutionOrder: initial.world.agents.map(({ id }) => id), - experiment: { ...initial.experiment, totalCompletedTurns: 400 }, - }); - const fresh = simulationSnapshotSchema.parse({ - ...initial, - scenario: { - ...initial.scenario, - objectiveVersion: 'durable-influence-v3', - capabilities: { - ...initial.scenario.capabilities, - simulatedPlayerPressure: true, - }, - simulatedPlayer: { - enabled: true, - profile: 'trail-hunter-v1', - seed: 'reset-trail-hunter', - }, - }, - world: { - ...initial.world, - simulatedPlayer: { - profile: 'trail-hunter-v1', - currentCell: initial.world.hexes[0]!.cell, - metrics: { - movements: 0, - cellsDisinfected: 0, - blockedDisinfections: 0, - }, - }, - }, - }); - let tickRequests = 0; - let appliedProfile: string | undefined; - vi.stubGlobal( - 'fetch', - vi.fn((input: RequestInfo | URL, init?: RequestInit) => { - const url = String(input); - if (url.endsWith('/setup/preview')) - return jsonResponse( - previewWorldSetup(JSON.parse(String(init?.body))), - ); - if (url.endsWith('/experiment/setup')) { - appliedProfile = JSON.parse(String(init?.body)).simulatedPlayer - .profile; - return jsonResponse({ snapshot: fresh }); - } - if (url.includes('/tick?mutationId=') && init?.method === 'POST') { - tickRequests += 1; - return jsonResponse(completeTickResponse(fresh, 1)); - } - return jsonResponse(at50); - }), - ); - - const user = userEvent.setup(); - render(); - await act(async () => { - await Promise.resolve(); - await Promise.resolve(); - }); - expect(screen.getByText('Tick 50')).toBeInTheDocument(); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'World setup' })); - await user.click( - screen.getByRole('checkbox', { - name: 'Enable simulated player pressure', - }), - ); - await user.selectOptions( - screen.getByLabelText('Simulated player profile'), - 'trail-hunter-v1', - ); - await user.click(screen.getByRole('button', { name: 'Preview' })); - await user.click( - screen.getByRole('button', { name: 'Apply / Create Experiment' }), - ); - expect(appliedProfile).toBe('trail-hunter-v1'); - await act(async () => { - await Promise.resolve(); - await Promise.resolve(); - }); - expect(screen.getByText('Tick 0')).toBeInTheDocument(); - await user.selectOptions(screen.getByLabelText('Tick target'), '50'); - await user.click(screen.getByRole('button', { name: 'Run to tick 50' })); - await waitFor(() => { - expect(tickRequests).toBe(1); - }); - } finally { - vi.useRealTimers(); - } - }); - - it('keeps run targets absolute and makes current or past targets unavailable', async () => { - window.sessionStorage.setItem('hexzero.world-lab.run-target', '25'); - const at50 = simulationSnapshotSchema.parse({ - ...initial, - turnNumber: 400, - tickNumber: 50, - virtualTime: '2026-08-13T16:10:00.000Z', - lastTickIntervalMinutes: 5, - resolutionOrder: initial.world.agents.map(({ id }) => id), - experiment: { ...initial.experiment, totalCompletedTurns: 400 }, - }); - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(at50)), - ); - render(); - const selector = await screen.findByLabelText('Tick target'); - expect(screen.getByRole('option', { name: '25' })).toBeDisabled(); - expect(screen.getByRole('option', { name: '50' })).toBeDisabled(); - expect(screen.getByRole('option', { name: '100' })).toBeEnabled(); - expect(selector).toHaveValue('100'); - }); - - it('derives allied marker and existing-territory colors while retaining individual ownership labels', async () => { - const progressed = afterInfection(); - const [ember, rook] = progressed.world.agents; - const allianceId = 'a1111111-1111-4111-8111-111111111111'; - const allianceColor = '#0072B2' as const; - const formedEvent = { - id: 'd4444444-4444-4444-8444-444444444444', - agentId: rook!.id, - occurredAt: '2026-08-13T12:00:02.000Z', - turnNumber: 2, - type: 'alliance-formed' as const, - allianceId, - allianceColor, - memberAgentIds: [ember!.id, rook!.id], - }; - const allied = simulationSnapshotSchema.parse({ - ...progressed, - world: { - ...progressed.world, - alliances: [ - { - id: allianceId, - color: allianceColor, - memberAgentIds: [ember!.id, rook!.id], - }, - ], - events: [...progressed.world.events, formedEvent], - }, - experiment: { - ...progressed.experiment, - currentTerritory: progressed.experiment.currentTerritory.map((entry) => - entry.agentId === ember!.id || entry.agentId === rook!.id - ? { ...entry, allianceId, effectiveColor: allianceColor } - : entry, - ), - currentAlliances: [ - { - allianceId, - color: allianceColor, - totalControlledCellCount: 1, - members: [ - { agentId: ember!.id, name: ember!.name, controlledCellCount: 1 }, - { agentId: rook!.id, name: rook!.name, controlledCellCount: 0 }, - ], - }, - ], - }, - }); - const openRouterAllied = simulationSnapshotSchema.parse({ - ...allied, - providerMode: 'openrouter', - }); - vi.stubGlobal( - 'fetch', - vi.fn((input: RequestInfo | URL) => - String(input).endsWith('/models') - ? jsonResponse(compatibleCatalog) - : jsonResponse(openRouterAllied), - ), - ); - const user = userEvent.setup(); - render(); - const markers = await screen.findAllByRole('button', { - name: /Select agent (Ember|Rook)/, - }); - expect(markers).toHaveLength(2); - expect( - markers.every( - (marker) => marker.dataset.effectiveColor === allianceColor, - ), - ).toBe(true); - expect( - screen.getByTestId('world-map').getAttribute('data-controller-colors'), - ).toContain(allianceColor); - await user.click(screen.getByRole('tab', { name: 'Scoreboard' })); - expect( - screen.getByLabelText('Alliance and territory panel'), - ).toHaveTextContent('Ember (1), Rook (0)'); - expect( - screen.getAllByText('Ember and Rook formed an alliance.'), - ).toHaveLength(1); - const roster = screen.getByLabelText('Agent roster'); - expect( - within(roster) - .getByRole('button', { name: /Ember/ }) - .querySelector('.agent-swatch'), - ).toHaveStyle({ background: allianceColor }); - await user.click(screen.getByRole('tab', { name: 'Agent' })); - expect( - within(screen.getByLabelText('Agent inspector')) - .getByRole('heading', { name: /Ember/ }) - .querySelector('.agent-swatch'), - ).toHaveStyle({ background: allianceColor }); - await user.click(screen.getByRole('button', { name: 'Agents' })); - await user.click( - await screen.findByRole('button', { name: /Open Agent Controller/ }), - ); - await user.click(screen.getByRole('tab', { name: 'Overview' })); - expect( - within(screen.getByRole('tabpanel', { name: 'Overview' })) - .getByText('Ember') - .closest('button') - ?.querySelector('.agent-swatch'), - ).toHaveStyle({ background: allianceColor }); - }); - - it('uses neutral affiliation color across roster, controller, inspector, and setup', async () => { - const openRouterInitial = openRouterSnapshot('example/alpha'); - vi.stubGlobal( - 'fetch', - vi.fn((input: RequestInfo | URL) => - String(input).endsWith('/models') - ? jsonResponse(compatibleCatalog) - : jsonResponse(openRouterInitial), - ), - ); - const user = userEvent.setup(); - render(); - const roster = await screen.findByLabelText('Agent roster'); - expect( - within(roster) - .getByRole('button', { name: /Ember/ }) - .querySelector('.agent-swatch'), - ).toHaveStyle({ background: NEUTRAL_AGENT_COLOR }); - expect( - within(screen.getByLabelText('Agent inspector')) - .getByRole('heading', { name: /Ember/ }) - .querySelector('.agent-swatch'), - ).toHaveStyle({ background: NEUTRAL_AGENT_COLOR }); - await user.click(screen.getByRole('button', { name: 'Agents' })); - await user.click( - await screen.findByRole('button', { name: /Open Agent Controller/ }), - ); - await user.click(screen.getByRole('tab', { name: 'Overview' })); - expect( - within(screen.getByRole('tabpanel', { name: 'Overview' })) - .getByText('Ember') - .closest('button') - ?.querySelector('.agent-swatch'), - ).toHaveStyle({ background: NEUTRAL_AGENT_COLOR }); - await user.click( - screen.getByRole('button', { name: 'Close model selection' }), - ); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'World setup' })); - expect( - screen.queryByLabelText(`${world.agents[0]!.name} color`), - ).not.toBeInTheDocument(); - expect( - screen.getByLabelText( - `${world.agents[0]!.name} starts unaffiliated with neutral color`, - ), - ).toHaveStyle({ background: NEUTRAL_AGENT_COLOR }); - }); - - it('deduplicates rendered H3 features before reporting readiness', async () => { - mapLibreMock.duplicateFeatures = true; - render(); - expect( - await screen.findByText(/H3 overlay ready · 127\/127 rendered cells/), - ).toBeInTheDocument(); - expect(screen.getByTestId('world-map')).toHaveAttribute( - 'data-rendered-h3-cell-count', - '127', - ); - }); - - it('waits for an H3 source update and render cycle before inspecting readiness', async () => { - mapLibreMock.autoRender = false; - render(); - await screen.findByRole('button', { - name: 'Select agent Ember, Patient Zero', - }); - expect(screen.getByText(/H3 overlay initializing/)).toBeInTheDocument(); - expect(mapLibreMock.queryRenderedFeatures).not.toHaveBeenCalled(); - - act(() => { - const pendingRenders = mapLibreMock.pendingRenderCallbacks.splice(0); - for (const completeRender of pendingRenders) { - completeRender(); - } - }); - - expect( - await screen.findByText(/H3 overlay ready · 127\/127 rendered cells/), - ).toBeInTheDocument(); - }); - - it.each([ - { - scenario: 'incomplete rendering', - configure: () => (mapLibreMock.renderMode = 'incomplete'), - expectedStatus: 'incomplete', - expectedCount: '126', - }, - { - scenario: 'rejected layers', - configure: () => (mapLibreMock.rejectLayers = true), - expectedStatus: 'failed', - expectedCount: '0', - }, - ])( - 'does not report readiness for $scenario', - async ({ configure, expectedCount, expectedStatus }) => { - configure(); - render(); - expect( - await screen.findByText(/H3 overlay (?:incomplete|failed)/), - ).toBeInTheDocument(); - expect(screen.queryByText(/H3 overlay ready/)).not.toBeInTheDocument(); - expect(screen.getByTestId('world-map')).not.toHaveAttribute( - 'data-overlay-status', - 'ready', - ); - expect(screen.getByTestId('world-map')).toHaveAttribute( - 'data-overlay-status', - expectedStatus, - ); - expect(screen.getByTestId('world-map')).toHaveAttribute( - 'data-rendered-h3-cell-count', - expectedCount, - ); - }, - ); - - it('uses explicit boolean assertions in every conditional paint expression', async () => { - render(); - await screen.findByText(/H3 overlay ready/); - const conditions = mapLibreMock.layers.flatMap(({ paint = {} }) => - Object.values(paint) - .filter( - (expression): expression is unknown[] => - Array.isArray(expression) && expression[0] === 'case', - ) - .map((expression) => expression[1]), - ); - expect(conditions).toHaveLength(3); - expect(conditions).toEqual( - Array(3).fill(['boolean', ['get', 'selected'], false]), - ); - }); - - it('selects an agent and populates its inspector', async () => { - const user = userEvent.setup(); - render(); - await user.click( - await screen.findByRole('button', { name: 'Select agent Rook' }), - ); - expect(screen.getByRole('heading', { name: /Rook/ })).toBeInTheDocument(); - expect(screen.getByText(world.agents[1]!.personality)).toBeInTheDocument(); - expect(screen.getByText(world.agents[1]!.id)).toBeInTheDocument(); - expect( - screen.getByText('No direct messages for this agent yet.'), - ).toBeInTheDocument(); - }); - - it('keeps an explicitly selected agent stable after a simultaneous tick', async () => { - const completed = completeTickResponse(afterInfection()); - vi.stubGlobal( - 'fetch', - vi - .fn() - .mockImplementationOnce(() => jsonResponse(initial)) - .mockImplementationOnce(() => jsonResponse(completed)), - ); - const user = userEvent.setup(); - render(); - await user.click( - await screen.findByRole('button', { name: 'Select agent Rook' }), - ); - await user.click(screen.getByRole('button', { name: 'Single tick' })); - expect( - await screen.findByRole('heading', { name: /Rook/ }), - ).toBeInTheDocument(); - const roster = screen.getByLabelText('Agent roster'); - expect(within(roster).queryByText('Follow latest')).not.toBeInTheDocument(); - expect(within(roster).queryByText('Latest')).not.toBeInTheDocument(); - }); - - it('renders accepted messages, directions, and hostile-looking text as plain text', async () => { - const changed = afterMessage(); - vi.stubGlobal( - 'fetch', - vi - .fn() - .mockImplementationOnce(() => jsonResponse(initial)) - .mockImplementationOnce(() => - jsonResponse(completeTickResponse(changed)), - ), - ); - const user = userEvent.setup(); - render(); - await user.click( - await screen.findByRole('button', { - name: 'Select agent Ember, Patient Zero', - }), - ); - await user.click( - await screen.findByRole('button', { name: 'Single tick' }), - ); - await user.click(screen.getByRole('tab', { name: 'Event log' })); - expect( - await screen.findByText(/Waited.*direct message accepted/), - ).toBeInTheDocument(); - expect(screen.getByLabelText('Direct-message history')).toHaveTextContent( - 'Sent Rook', - ); - expect(screen.getAllByText(HOSTILE_MESSAGE).length).toBeGreaterThan(0); - expect(document.querySelector('img[src="x"]')).toBeNull(); - - await user.click(screen.getByRole('button', { name: 'Select agent Rook' })); - expect(screen.getByLabelText('Direct-message history')).toHaveTextContent( - 'Received Ember', - ); - }); - - it('renders legacy public chat with an explicit turn fallback and timestamp', async () => { - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(afterPublicMessage())), - ); - render(); - const feed = await screen.findByLabelText('Public world chat'); - expect(feed).toHaveTextContent('Ember'); - expect(feed).toHaveTextContent('Turn 1'); - expect(feed).toHaveTextContent(/\d{2}:\d{2}:\d{2}/); - expect(feed).toHaveTextContent(HOSTILE_MESSAGE); - expect(document.querySelector('img[src="x"]')).toBeNull(); - }); - - it('uses tick-native labels for private and direct-message history', async () => { - const source = afterMessage(); - const tick = completeTickResponse(source).snapshot; - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(tick)), - ); - const user = userEvent.setup(); - render(); - const history = await screen.findByLabelText('Direct-message history'); - expect(history).toHaveTextContent('Tick 1'); - expect(history).toHaveTextContent(/\d{2}:\d{2}:\d{2}/); - await user.click(await screen.findByRole('tab', { name: 'Private comms' })); - const privateFeed = screen.getByLabelText('Private communications'); - expect(privateFeed).toHaveTextContent('Tick 1 · Delivered'); - expect(privateFeed).toHaveTextContent(/\d{2}:\d{2}:\d{2}/); - }); - - it('uses a tick-native label and attempt timestamp for rejected private communication', async () => { - const delivered = afterMessage(); - const turn = delivered.turns[0]!; - if (turn.outcome !== 'accepted') - throw new Error('Expected a completed message fixture.'); - const rejected = simulationSnapshotSchema.parse({ - ...delivered, - world: { ...delivered.world, events: delivered.world.events.slice(0, 1) }, - turns: [ - { - ...turn, - communicationResult: { - requested: true, - accepted: false, - attempt: { - id: '97dd21b9-fc78-4b04-9f92-9862bf346f99', - agentId: turn.agentId, - occurredAt: '2026-08-13T12:00:01.000Z', - channel: 'direct', - recipientId: world.agents[1]!.id, - distance: 2, - message: HOSTILE_MESSAGE, - }, - reason: 'self-message', - details: 'An agent cannot message itself.', - }, - }, - ], - }); - const tick = completeTickResponse(rejected).snapshot; - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(tick)), - ); - const user = userEvent.setup(); - render(); - await user.click(await screen.findByRole('tab', { name: 'Private comms' })); - const privateFeed = screen.getByLabelText('Private communications'); - expect(privateFeed).toHaveTextContent('Tick 1 · Rejected: self-message'); - expect(privateFeed).toHaveTextContent(/\d{2}:\d{2}:\d{2}/); - }); - - it('uses a tick-native label and timestamp in public chat', async () => { - const publicTick = completeTickResponse(afterPublicMessage()).snapshot; - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(publicTick)), - ); - render(); - const feed = await screen.findByLabelText('Public world chat'); - expect(feed).toHaveTextContent('Tick 1'); - expect(feed).toHaveTextContent(/\d{2}:\d{2}:\d{2}/); - }); - - it('renders public chat newest first in DOM order', async () => { - const first = afterPublicMessage(); - const original = first.world.events.find( - ({ type }) => type === 'public-message-sent', - )!; - const newer = { - ...original, - id: '99cc21b9-fc78-4b04-9f92-9862bf346f99', - occurredAt: '2026-08-13T12:00:02.000Z', - message: 'Newest public message.', - }; - vi.stubGlobal( - 'fetch', - vi.fn(() => - jsonResponse( - simulationSnapshotSchema.parse({ - ...first, - world: { ...first.world, events: [...first.world.events, newer] }, - }), - ), - ), - ); - render(); - const items = within( - await screen.findByLabelText('Public world chat'), - ).getAllByRole('listitem'); - expect(items[0]).toHaveTextContent('Newest public message.'); - expect(items[1]).toHaveTextContent(HOSTILE_MESSAGE); - }); - - it('keeps direct communication in the operator-only private feed with filters and inspection', async () => { - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(afterMessage())), - ); - const user = userEvent.setup(); - render(); - const activityTabs = await screen.findByRole('tablist', { - name: 'Activity views', - }); - expect( - within(activityTabs) - .getAllByRole('tab') - .map(({ textContent }) => textContent), - ).toEqual([ - 'Public chat', - 'Private comms', - 'Event log', - 'Failures & recovery', - ]); - expect( - await screen.findByLabelText('Public world chat'), - ).not.toHaveTextContent(HOSTILE_MESSAGE); - await user.click(screen.getByRole('tab', { name: 'Private comms' })); - const privateFeed = screen.getByLabelText('Private communications'); - expect(privateFeed).toHaveTextContent('Ember'); - expect(privateFeed).toHaveTextContent('Rook'); - expect(privateFeed).toHaveTextContent('Turn 1 · Delivered'); - expect(privateFeed).toHaveTextContent(/\d{2}:\d{2}:\d{2}/); - expect(privateFeed).toHaveTextContent('2.00 km'); - await user.click( - within(privateFeed).getByRole('button', { name: 'Alliance' }), - ); - expect(privateFeed).toHaveTextContent('No private communications yet.'); - await user.click( - within(privateFeed).getByRole('button', { name: 'Direct' }), - ); - await user.click(within(privateFeed).getByRole('button', { name: 'Rook' })); - expect( - within(screen.getByLabelText('Agent inspector')).getByRole('heading', { - name: /Rook/, - }), - ).toBeInTheDocument(); - }); - - it('shows empty and active strategic goal state in the agent inspector', async () => { - const agent = initial.world.agents[0]!; - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(initial)), - ); - const emptyUser = userEvent.setup(); - const emptyRender = render(); - await emptyUser.click( - await screen.findByRole('button', { - name: new RegExp(`Select agent ${agent.name}`), - }), - ); - const emptyInspector = screen.getByLabelText('Agent inspector'); - expect( - within(emptyInspector).getByText('No active strategic goal.'), - ).toBeVisible(); - expect( - within(emptyInspector).getByText('No goal operation recorded.'), - ).toBeVisible(); - expect( - within(emptyInspector).getByText('No compact memories.'), - ).toBeVisible(); - expect( - within(emptyInspector).getByText('No memory operation recorded.'), - ).toBeVisible(); - emptyRender.unmount(); - - const base = afterInfection(); - const active = simulationSnapshotSchema.parse({ - ...base, - agentGoals: base.world.agents.map(({ id }) => ({ - agentId: id, - goal: - id === agent.id - ? { - longTermGoal: 'Hold a durable corridor.', - shortTermGoal: 'Secure the frontier.', - planSummary: 'Expand methodically.', - establishedAtTick: 1, - revisedAtTick: 2, - } - : null, - })), - agentMemories: base.world.agents.map(({ id }) => ({ - agentId: id, - entries: - id === agent.id - ? [ - { - id: `memory:${id}:1`, - text: 'The northern route was blocked.', - createdAtTick: 1, - revisedAtTick: 2, - }, - ] - : [], - })), - turns: base.turns.map((turn) => - turn.agentId === agent.id && turn.outcome === 'accepted' - ? { - ...turn, - goalRevision: { - operation: 'establish', - longTermGoal: 'Hold a durable corridor.', - shortTermGoal: 'Secure the frontier.', - planSummary: 'Expand methodically.', - reason: 'Start continuity.', - }, - goalRevisionResult: { - requested: true, - accepted: true, - operation: 'establish', - }, - memoryOperation: { - operation: 'remember', - text: 'The northern route was blocked.', - }, - memoryOperationResult: { - requested: true, - accepted: true, - operation: 'remember', - memoryId: `memory:${agent.id}:1`, - }, - } - : turn, - ), - }); - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(active)), - ); - const user = userEvent.setup(); - render(); - await user.click( - await screen.findByRole('button', { - name: new RegExp(`Select agent ${agent.name}`), - }), - ); - const inspector = screen.getByLabelText('Agent inspector'); - expect( - within(inspector).getByText('Hold a durable corridor.'), - ).toBeVisible(); - expect( - within(inspector).getByText('Latest: establish · accepted'), - ).toBeVisible(); - expect( - within(inspector).getByText('Agent reason: Start continuity.'), - ).toBeVisible(); - expect( - within(inspector).getByText('The northern route was blocked.'), - ).toBeVisible(); - expect( - within(inspector).getByText('Latest: remember · accepted'), - ).toBeVisible(); - }); - - it('shows a bounded read-only behavior trace and highlights observed cells', async () => { - const base = afterInfection(); - const patientZeroId = base.scenario.patientZeroAgentId; - const agent = base.world.agents.find(({ id }) => id === patientZeroId)!; - const changed = simulationSnapshotSchema.parse({ - ...base, - turns: base.turns.map((turn) => - turn.agentId === patientZeroId - ? { - ...turn, - observation: { - ...turn.observation, - patientZero: { - agentId: patientZeroId, - agentName: agent.name, - isPatientZero: true, - directRangeBypass: true, - }, - playerPressure: { - enabled: true, - recentThreats: [ - { - eventId: '97aa21b9-fc78-4b04-9f92-9862bf346f96', - kind: 'territory-disinfected', - cell: agent.currentCell, - occurredAt: turn.startedAt, - distanceCells: 0, - affectedOwnTerritory: true, - }, - ], - }, - patientZeroGlobalView: { - agents: [], - individualTerritory: turn.observation.territoryScoreboard, - allianceTerritory: [], - alliances: [], - activeAllianceProposals: [], - recentStrategicEvents: [], - recentTerritoryChanges: [], - playerThreatFeed: { - events: [ - { - eventId: '97aa21b9-fc78-4b04-9f92-9862bf346f96', - kind: 'territory-disinfected', - cell: agent.currentCell, - occurredAt: turn.startedAt, - affectedAgentId: agent.id, - affectedAgentName: agent.name, - affectedAllianceId: null, - affectedAllianceColor: null, - pressureContext: { - window: { tickCount: 2, startTick: 1, endTick: 2 }, - subject: { - totalEvents: 2, - disinfections: 1, - blockedCleans: 1, - consecutiveAffectedTicks: 2, - }, - currentAlliance: null, - }, - }, - ], - totalEventCount: 2, - truncated: true, - }, - }, - }, - } - : turn, - ), - }); - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(changed)), - ); - const user = userEvent.setup(); - render(); - await user.click( - await screen.findByRole('button', { - name: new RegExp(`Select agent ${agent.name}`), - }), - ); - - const inspector = screen.getByLabelText('Agent inspector'); - const trace = within(inspector).getByLabelText('Recent behavior trace'); - const sectionNavigation = within(inspector).getByRole('navigation', { - name: `${agent.name} inspector sections`, - }); - for (const section of [ - 'Trace', - 'Goals', - 'Memories', - 'History', - 'Configuration', - 'Latest', - ]) - expect( - within(sectionNavigation).getByRole('link', { name: section }), - ).toBeVisible(); - expect(within(inspector).getByText('1/6 retained')).toBeVisible(); - expect(trace).toHaveTextContent( - 'First retained observation for this agent.', - ); - expect(trace).toHaveTextContent('1 legal move target · Infect · Wait'); - expect(trace).toHaveTextContent('Chosen: Infect'); - expect(trace).toHaveTextContent( - 'Local cleaner threat: own territory disinfected', - ); - expect(trace).toHaveTextContent( - 'Patient Zero global cleaner feed: 1/2 displayed · truncated', - ); - expect(trace).not.toHaveTextContent(`${agent.name} lost`); - expect(trace).toHaveTextContent( - 'subject 2 total (1 disinfected, 1 blocked), 2 consecutive', - ); - expect(trace).toHaveTextContent( - 'Model summary (self-reported, not proof): Infecting this open cell.', - ); - expect(inspector).toHaveTextContent( - 'Observation evidence and self-reported summaries show correlation, not proven causation.', - ); - - await user.click( - within(trace).getByRole('button', { name: 'Highlight cell' }), - ); - await waitFor(() => - expect(mapLibreMock.latestSourceData).toEqual( - expect.objectContaining({ - features: expect.arrayContaining([ - expect.objectContaining({ - properties: expect.objectContaining({ - cell: agent.currentCell, - selected: true, - }), - }), - ]), - }), - ), - ); - - await user.click( - within(trace).getByRole('button', { name: 'Highlight observed cell' }), - ); - await waitFor(() => - expect(mapLibreMock.latestSourceData).toEqual( - expect.objectContaining({ - features: expect.arrayContaining([ - expect.objectContaining({ - properties: expect.objectContaining({ - cell: agent.currentCell, - selected: true, - }), - }), - ]), - }), - ), - ); - }); - - it('clears visible communications after reset', async () => { - const changed = afterMessage(); - vi.stubGlobal( - 'fetch', - vi - .fn() - .mockImplementationOnce(() => jsonResponse(changed)) - .mockImplementationOnce(() => jsonResponse({ snapshot: initial })), - ); - const user = userEvent.setup(); - render(); - await user.click( - await screen.findByRole('button', { - name: 'Select agent Ember, Patient Zero', - }), - ); - expect( - await screen.findByLabelText('Direct-message history'), - ).toHaveTextContent('Sent Rook'); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'Reset world' })); - expect( - await screen.findByText('No direct messages for this agent yet.'), - ).toBeInTheDocument(); - expect(screen.queryByText(HOSTILE_MESSAGE)).not.toBeInTheDocument(); - }); - - it('executes one turn and renders infection and decision details safely', async () => { - const changed = afterInfection(); - vi.stubGlobal( - 'fetch', - vi - .fn() - .mockImplementationOnce(() => jsonResponse(initial)) - .mockImplementationOnce(() => - jsonResponse(completeTickResponse(changed)), - ), - ); - const user = userEvent.setup(); - render(); - await user.click( - await screen.findByRole('button', { - name: 'Select agent Ember, Patient Zero', - }), - ); - await user.click( - await screen.findByRole('button', { name: 'Single tick' }), - ); - await user.click(screen.getByRole('tab', { name: 'Event log' })); - expect( - await screen.findByText('Infection · ' + world.agents[0]!.currentCell), - ).toBeInTheDocument(); - const latestTurn = screen - .getByRole('heading', { name: 'Latest turn' }) - .closest('.turn-detail'); - expect(latestTurn).toHaveTextContent('Summary: Infecting this open cell.'); - await user.click(screen.getByText('Latest structured observation')); - expect( - screen.getByText('Latest structured observation').closest('details'), - ).toHaveTextContent('Capture: blocked · capture-open-cell'); - expect( - screen.getByText( - 'Immutable input supplied for Tick 1 · record 1. It is not rewritten when the active personality changes.', - ), - ).toBeInTheDocument(); - await waitFor(() => - expect(screen.getByTestId('world-map')).toHaveAttribute( - 'data-rendered-infected-cell-count', - '1', - ), - ); - expect(screen.getByTestId('infected-count')).toHaveTextContent( - '1 rendered infected', - ); - expect(mapLibreMock.setData).toHaveBeenLastCalledWith( - expect.objectContaining({ - features: expect.arrayContaining([ - expect.objectContaining({ - properties: expect.objectContaining({ state: 'infected' }), - }), - ]), - }), - ); - expect(screen.queryByText(/chain-of-thought/i)).not.toBeInTheDocument(); - }); - - it('renders controller identity, territory totals, capture events, and both gain/loss views', async () => { - const user = userEvent.setup(); - const captured = afterCapture(); - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(captured)), - ); - render(); - await user.click( - await screen.findByRole('button', { - name: 'Select agent Ember, Patient Zero', - }), - ); - await user.click(screen.getByRole('tab', { name: 'Scoreboard' })); - expect( - await screen.findByRole('heading', { name: 'Territory scoreboard' }), - ).toBeInTheDocument(); - const scoreboard = screen.getByLabelText('Territory scoreboard'); - expect(scoreboard).toHaveTextContent('Ember0'); - expect(scoreboard).toHaveTextContent('Rook1'); - await user.click(screen.getByRole('tab', { name: 'Event log' })); - expect(screen.getByText(/Rook captured .* from Ember/)).toBeInTheDocument(); - await user.click(screen.getByRole('tab', { name: 'Agent' })); - expect(screen.getByLabelText('Recent territory changes')).toHaveTextContent( - 'Lost', - ); - expect(screen.getAllByText('0 controlled cells').length).toBeGreaterThan(0); - await user.click( - await screen.findByRole('button', { name: 'Select agent Rook' }), - ); - expect( - within(screen.getByLabelText('Agent inspector')).getByRole('heading', { - name: /Rook/, - }), - ).toBeInTheDocument(); - expect(screen.getByLabelText('Recent territory changes')).toHaveTextContent( - 'Gained', - ); - expect(screen.getByText('1 controlled cells')).toBeInTheDocument(); - await user.click(screen.getByText('Latest structured observation')); - expect( - screen.getByText('Latest structured observation').closest('details'), - ).toHaveTextContent('Capture: eligible'); - await waitFor(() => - expect(mapLibreMock.latestSourceData).toEqual( - expect.objectContaining({ - features: expect.arrayContaining([ - expect.objectContaining({ - properties: expect.objectContaining({ - controllerColor: NEUTRAL_AGENT_COLOR, - controllerName: 'Rook', - }), - }), - ]), - }), - ), - ); - }); - - it('starts, pauses, and changes playback speed without overlapping immediately', async () => { - const user = userEvent.setup(); - render(); - await user.click(await screen.findByRole('button', { name: 'Start' })); - expect(screen.getByRole('button', { name: 'Pause' })).toBeInTheDocument(); - await user.selectOptions(screen.getByLabelText('Playback speed'), '250'); - await user.click(screen.getByRole('button', { name: 'Agents' })); - expect( - screen.getByRole('region', { name: 'Agent management workspace' }), - ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Pause' })).toBeInTheDocument(); - await user.click(screen.getByRole('button', { name: 'Live' })); - expect(screen.getByTestId('world-map')).toBeInTheDocument(); - await user.click(screen.getByRole('button', { name: 'Pause' })); - expect(screen.getByRole('button', { name: 'Start' })).toBeInTheDocument(); - }); - - it('routes agent and hex selections to semantic inspector tabs and keeps scoreboard reachable', async () => { - const user = userEvent.setup(); - render(); - await screen.findByRole('button', { name: 'Start' }); - expect(screen.getByRole('tab', { name: 'Agent' })).toHaveAttribute( - 'aria-selected', - 'true', - ); - await user.click( - (await screen.findAllByRole('button', { name: /Select agent/ }))[0]!, - ); - expect(screen.getByRole('tab', { name: 'Agent' })).toHaveAttribute( - 'aria-selected', - 'true', - ); - await user.click(screen.getByRole('tab', { name: 'Scoreboard' })); - expect(screen.getByLabelText('Territory scoreboard')).toBeInTheDocument(); - }); - - it('bounds recovery activity and exposes newest failures through the dock tab', async () => { - const user = userEvent.setup(); - render(); - await screen.findByRole('button', { name: 'Start' }); - await user.click(screen.getByRole('tab', { name: 'Failures & recovery' })); - expect( - screen.getByText('No failures or recovery actions recorded.'), - ).toBeInTheDocument(); - expect( - screen.queryByLabelText('Failures and recovery log'), - ).not.toBeInTheDocument(); - }); - - it('resets turn history while preserving an available agent selection', async () => { - const changed = afterInfection(); - vi.stubGlobal( - 'fetch', - vi - .fn() - .mockImplementationOnce(() => jsonResponse(changed)) - .mockImplementationOnce(() => jsonResponse({ snapshot: initial })), - ); - const user = userEvent.setup(); - render(); - await user.click( - await screen.findByRole('button', { name: 'Select agent Rook' }), - ); - await waitFor(() => - expect(screen.getByTestId('world-map')).toHaveAttribute( - 'data-rendered-infected-cell-count', - '1', - ), - ); - await user.click(screen.getByRole('tab', { name: 'Event log' })); - expect( - await screen.findByText('Infection · ' + world.agents[0]!.currentCell), - ).toBeInTheDocument(); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'Reset world' })); - expect(confirm).toHaveBeenCalledWith( - expect.stringContaining('unexported telemetry'), - ); - await waitFor(() => - expect( - screen.getByText('Development world loaded with 8 agents.'), - ).toBeInTheDocument(), - ); - expect(screen.getByText('Tick 0')).toBeInTheDocument(); - await waitFor(() => - expect(screen.getByTestId('world-map')).toHaveAttribute( - 'data-rendered-infected-cell-count', - '0', - ), - ); - await user.click(screen.getByRole('tab', { name: 'Agent' })); - expect(screen.getByRole('heading', { name: /Rook/ })).toBeInTheDocument(); - }); - - it('supports hex selection independently of agent selection', async () => { - render(); - await screen.findByRole('button', { - name: 'Select agent Ember, Patient Zero', - }); - const target = world.hexes[1]!.cell; - act(() => { - mapLibreMock.mapClick?.({ - features: [{ properties: { cell: target } }], - }); - mapLibreMock.mapBackgroundClick?.(); - }); - expect(screen.getByLabelText('Selected hex details')).toHaveTextContent( - target, - ); - expect(screen.getByRole('tab', { name: 'Hex' })).toHaveAttribute( - 'aria-selected', - 'true', - ); - fireEvent.click(screen.getByRole('tab', { name: 'Agent' })); - expect(screen.getByRole('heading', { name: /Ember/ })).toBeInTheDocument(); - act(() => mapLibreMock.mapBackgroundClick?.()); - expect( - screen.queryByLabelText('Selected hex details'), - ).not.toBeInTheDocument(); - }); - - it('renders missing configuration without enabling cost-incurring controls', async () => { - const unconfigured = simulationSnapshotSchema.parse({ - ...initial, - status: 'configuration-error', - providerMode: 'openrouter', - providerConfigured: false, - }); - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(unconfigured)), - ); - render(); - expect( - await screen.findByText(/Model calls unavailable/), - ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Start' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Single tick' })).toBeDisabled(); - }); - - it('reserves a compact cancel slot when no request is active', async () => { - render(); - await screen.findByRole('button', { - name: 'Select agent Ember, Patient Zero', - }); - const cancel = document.querySelector( - '.cancel-request-slot button', - ); - expect(cancel).toHaveTextContent('Cancel'); - expect(cancel).toBeDisabled(); - expect(cancel?.closest('.cancel-request-slot')).toHaveClass('inactive'); - }); - - it('renders catalog facts and preserves overrides until Apply to all is explicit', async () => { - const emberId = world.agents[0]!.id; - let current = openRouterSnapshot('example/alpha', [ - { - agentId: emberId, - modelId: 'sample/beta', - reasoningProfile: 'provider-default', - }, - ]); - vi.stubGlobal( - 'fetch', - vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { - const url = String(input); - if (url.endsWith('/experiment/models')) { - const body = JSON.parse(String(init?.body)) as { - globalModelId: string | null; - globalReasoningProfile: SimulationSnapshot['modelConfiguration']['globalReasoningProfile']; - overrides: SimulationSnapshot['modelConfiguration']['overrides']; - }; - current = openRouterSnapshot( - body.globalModelId, - body.overrides, - body.globalReasoningProfile, - ); - return jsonResponse({ snapshot: current }); - } - if (url.endsWith('/models')) return jsonResponse(compatibleCatalog); - return jsonResponse(current); - }), - ); - const user = userEvent.setup(); - render(); - await openAgentsWorkspace(user); - const summary = await screen.findByText('Model: Alpha'); - await user.click(summary); - const modelConsole = within(summary.closest('.model-console')!); - expect(screen.getByText(/12 filtered out/)).toBeInTheDocument(); - expect(screen.getByText('$1/M')).toBeInTheDocument(); - expect(screen.getByText('$2/M')).toBeInTheDocument(); - expect(screen.getByText('32,768 tokens')).toBeInTheDocument(); - expect( - screen.getByText(/Catalog compatible: text and context requirements met/), - ).toBeInTheDocument(); - expect( - within(modelConsole.getByLabelText('Global reasoning')) - .getAllByRole('option') - .map(({ textContent }) => textContent), - ).toEqual(['Provider default', 'Off', 'Low', 'Medium', 'XHigh']); - await user.selectOptions( - modelConsole.getByLabelText('Global reasoning'), - 'xhigh', - ); - - await user.selectOptions( - modelConsole.getByLabelText('Global model'), - 'sample/beta', - ); - await screen.findByText('Model: Beta Free'); - expect(modelConsole.getByLabelText('Ember')).toHaveValue('sample/beta'); - const firstUpdate = vi - .mocked(fetch) - .mock.calls.find(([url]) => String(url).endsWith('/experiment/models')); - expect( - JSON.parse(String(firstUpdate?.[1]?.body)).globalReasoningProfile, - ).toBe('xhigh'); - expect(JSON.parse(String(firstUpdate?.[1]?.body)).overrides).toEqual([ - { - agentId: emberId, - modelId: 'sample/beta', - reasoningProfile: 'provider-default', - }, - ]); - - await user.click( - screen.getByRole('button', { name: 'Apply global model to all agents' }), - ); - await waitFor(() => - expect(modelConsole.getByLabelText('Ember')).toHaveValue(''), - ); - const updates = vi - .mocked(fetch) - .mock.calls.filter(([url]) => String(url).endsWith('/experiment/models')); - expect(JSON.parse(String(updates.at(-1)?.[1]?.body)).overrides).toEqual([]); - - await user.selectOptions( - modelConsole.getByLabelText('Ember'), - 'example/alpha', - ); - expect( - within(modelConsole.getByLabelText('Ember reasoning')) - .getAllByRole('option') - .map(({ textContent }) => textContent), - ).toEqual(['Provider default', 'Off', 'Low', 'Medium', 'XHigh']); - await user.selectOptions( - modelConsole.getByLabelText('Ember reasoning'), - 'low', - ); - const finalUpdate = vi - .mocked(fetch) - .mock.calls.filter(([url]) => String(url).endsWith('/experiment/models')) - .at(-1); - expect(JSON.parse(String(finalUpdate?.[1]?.body)).overrides).toEqual([ - { - agentId: emberId, - modelId: 'example/alpha', - reasoningProfile: 'low', - }, - ]); - await user.click( - screen.getByRole('button', { name: 'Close model selection' }), - ); - expect( - screen.queryByRole('dialog', { name: 'Model selection' }), - ).toBeNull(); - const agentControllerTrigger = screen.getByRole('button', { - name: /Open Agent Controller.*Beta Free/, - }); - expect(agentControllerTrigger).toHaveFocus(); - await user.click(agentControllerTrigger); - fireEvent.mouseDown( - screen.getByRole('dialog', { name: 'Model selection' }), - ); - expect( - screen.getByRole('dialog', { name: 'Model selection' }), - ).toBeInTheDocument(); - fireEvent.keyDown(window, { key: 'Escape' }); - expect( - screen.queryByRole('dialog', { name: 'Model selection' }), - ).toBeNull(); - }); - - it('recovers from a failed compatibility probe without changing the world', async () => { - const current = openRouterSnapshot('example/alpha'); - let probeCalls = 0; - const probeProfiles: string[] = []; - vi.stubGlobal( - 'fetch', - vi.fn((input: RequestInfo | URL, init?: RequestInit) => { - const url = String(input); - if (url.endsWith('/models/verify')) { - probeCalls += 1; - probeProfiles.push( - JSON.parse(String(init?.body)).reasoningProfile as string, - ); - return jsonResponse({ - verification: { - modelId: 'example/alpha', - contractVersion: AGENT_DECISION_CONTRACT_VERSION, - status: probeCalls === 1 ? 'failed' : 'verified', - testedAt: '2026-08-15T12:00:00.000Z', - ...(probeCalls === 1 - ? { - failure: { - code: 'invalid-json', - message: 'The model returned no usable JSON decision.', - }, - } - : { - provider: { - provider: 'openrouter', - model: 'example/alpha', - latencyMs: 20, - }, - }), - }, - }); - } - if (url.endsWith('/models')) return jsonResponse(compatibleCatalog); - return jsonResponse(current); - }), - ); - const user = userEvent.setup(); - render(); - await openAgentsWorkspace(user); - await user.click(await screen.findByText('Model: Alpha')); - await user.click( - screen.getByRole('button', { name: 'Test selected model' }), - ); - expect( - await screen.findByText(/returned no usable JSON decision/), - ).toBeInTheDocument(); - expect(screen.getByLabelText('Global model')).toBeEnabled(); - await user.click(screen.getByRole('button', { name: 'Retry model test' })); - expect( - await screen.findByText('Runtime verified: yes'), - ).toBeInTheDocument(); - expect(screen.getByText(/may incur a small charge/)).toBeInTheDocument(); - expect(screen.getByText('Tick 0')).toBeInTheDocument(); - expect(probeProfiles).toEqual(['provider-default', 'provider-default']); - }); - - it('shows stale catalog and unavailable saved-model states without substitution', async () => { - const unavailable = openRouterSnapshot('retired/model'); - unavailable.resolvedModels.forEach((entry) => { - entry.available = false; - entry.issue = 'unavailable'; - }); - vi.stubGlobal( - 'fetch', - vi.fn((input: RequestInfo | URL) => - String(input).endsWith('/models') - ? jsonResponse({ - ...compatibleCatalog, - stale: true, - error: { - code: 'timeout', - message: 'The OpenRouter model catalog request timed out.', - }, - }) - : jsonResponse(unavailable), - ), - ); - const user = userEvent.setup(); - render(); - await openAgentsWorkspace(user); - await user.click(await screen.findByText('Model: retired/model')); - expect( - screen.getByText(/Showing the last successful catalog/), - ).toHaveTextContent('timed out'); - expect(screen.getByLabelText('Global model')).toHaveValue('retired/model'); - expect( - screen.getByText(/Select an available compatible model/), - ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Start' })).toBeDisabled(); - }); - - it('renders a final lost tick without sequential recovery controls', async () => { - const current = openRouterSnapshot('example/alpha'); - const completed = completeTickResponse(current); - const failure = { - code: 'timeout' as const, - message: 'The shared tick deadline elapsed.', - retryable: false, - model: 'example/alpha', - }; - const lost = agentTurnRecordSchema.parse({ - ...completed.records[0], - outcome: 'lost-tick', - failure, - provider: undefined, - }); - const records = [lost, ...completed.records.slice(1)]; - const snapshot = simulationSnapshotSchema.parse({ - ...completed.snapshot, - turns: records, - }); - vi.stubGlobal( - 'fetch', - vi.fn((input: RequestInfo | URL) => - String(input).includes('/tick?mutationId=') - ? jsonResponse({ snapshot, tickNumber: 1, records }) - : jsonResponse(current), - ), - ); - render(); - fireEvent.click(await screen.findByRole('button', { name: 'Single tick' })); - expect( - await screen.findByText(/1 agent lost this tick/), - ).toBeInTheDocument(); - expect( - screen.queryByRole('button', { name: 'Retry' }), - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('button', { name: 'Skip turn' }), - ).not.toBeInTheDocument(); - }); - - it('reconciles a lost tick response from the authoritative snapshot without resubmitting', async () => { - const initial = simulationSnapshotSchema.parse({ - ...afterInfection(), - turnNumber: 0, - turns: [], - experiment: { - ...afterInfection().experiment, - totalCompletedTurns: 0, - retainedTurns: 0, - firstRetainedTurn: undefined, - lastRetainedTurn: undefined, - }, - }); - const completed = completeTickResponse(initial).snapshot; - let turnRequests = 0; - let snapshotRequests = 0; - vi.stubGlobal( - 'fetch', - vi.fn((input: RequestInfo | URL) => { - const url = String(input); - if (url.endsWith('/models')) return jsonResponse(compatibleCatalog); - if (url.includes('/tick?mutationId=')) { - turnRequests += 1; - return Promise.reject(new TypeError('ECONNRESET')); - } - snapshotRequests += 1; - return jsonResponse(snapshotRequests === 1 ? initial : completed); - }), - ); - render(); - await screen.findByText('Tick 0'); - fireEvent.click(screen.getByRole('button', { name: 'Single tick' })); - await screen.findByText('Tick 1'); - expect(turnRequests).toBe(1); - expect(screen.getByRole('button', { name: 'Single tick' })).toBeEnabled(); - expect(screen.queryByText('Reconciling request…')).not.toBeInTheDocument(); - }); - - it('polls and disables conflicting controls for an externally active tick with no active agent', async () => { - vi.useFakeTimers(); - const active = simulationSnapshotSchema.parse({ - ...initial, - status: 'waiting-for-model', - activeAgentId: null, - }); - const completed = completeTickResponse(initial).snapshot; - let requests = 0; - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(requests++ === 0 ? active : completed)), - ); - render(); - await act(async () => { - await Promise.resolve(); - }); - expect( - screen.getByLabelText('Experiment details. Tick 0, waiting for model'), - ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Start' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Single tick' })).toBeDisabled(); - expect( - screen.getByRole('button', { name: 'Run to tick 25' }), - ).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Cancel' })).toBeEnabled(); - fireEvent.click(screen.getByLabelText('More World Lab actions')); - expect(screen.getByRole('button', { name: 'World setup' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Reset world' })).toBeDisabled(); - await act(async () => { - await vi.advanceTimersByTimeAsync(500); - }); - expect(screen.getByText('Tick 1')).toBeInTheDocument(); - vi.useRealTimers(); - }); - - it('collapses and expands the bounded activity dock', async () => { - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(afterPublicMessage())), - ); - const user = userEvent.setup(); - render(); - await screen.findByLabelText('Public world chat'); - expect(document.querySelector('main')).toHaveClass('world-lab-shell'); - await user.click(screen.getByRole('button', { name: 'Collapse activity' })); - expect(document.querySelector('main')).toHaveClass('chat-collapsed'); - expect( - screen.queryByLabelText('Public world chat'), - ).not.toBeInTheDocument(); - expect(screen.queryByLabelText('World event log')).not.toBeInTheDocument(); - await user.click(screen.getByRole('button', { name: 'Expand activity' })); - expect(document.querySelector('main')).not.toHaveClass('chat-collapsed'); - expect(screen.getByLabelText('Public world chat')).toHaveTextContent( - HOSTILE_MESSAGE, - ); - }); - - it('pauses chat auto-scroll and offers a jump when new messages arrive above the bottom', async () => { - const first = afterPublicMessage(); - const nextEvent = { - ...first.world.events.find(({ type }) => type === 'public-message-sent')!, - id: '99cc21b9-fc78-4b04-9f92-9862bf346f99', - occurredAt: '2026-08-13T12:00:02.000Z', - message: 'A newer public message.', - }; - const next = simulationSnapshotSchema.parse({ - ...first, - world: { ...first.world, events: [...first.world.events, nextEvent] }, - }); - let scrollHeight = 1_000; - vi.stubGlobal( - 'fetch', - vi - .fn() - .mockImplementationOnce(() => jsonResponse(first)) - .mockImplementationOnce(() => { - scrollHeight = 1_100; - return jsonResponse(completeTickResponse(next)); - }), - ); - const user = userEvent.setup(); - render(); - const chat = await screen.findByLabelText('Public world chat'); - const feed = within(chat).getByRole('list'); - Object.defineProperties(feed, { - scrollHeight: { configurable: true, get: () => scrollHeight }, - clientHeight: { configurable: true, value: 100 }, - scrollTop: { configurable: true, writable: true, value: 200 }, - }); - fireEvent.scroll(feed); - await act(async () => undefined); - await user.click(screen.getByRole('button', { name: 'Single tick' })); - const jump = await screen.findByRole('button', { - name: '1 new message · Return to latest', - }); - expect(feed.scrollTop).toBe(300); - await user.click(jump); - expect(feed.scrollTop).toBe(0); - expect(screen.queryByText(/new message · Jump/)).not.toBeInTheDocument(); - }); - - it('enters and cancels explicit personality editing without a request', async () => { - const user = userEvent.setup(); - render(); - await user.click(await screen.findByRole('button', { name: 'Edit' })); - const textarea = screen.getByRole('textbox', { - name: 'Personality directive', - }); - expect(textarea).toHaveValue(world.agents[0]!.personality); - await user.type(textarea, ' unsaved'); - await user.click(screen.getByRole('button', { name: 'Cancel' })); - expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); - expect(screen.getByText(world.agents[0]!.personality)).toBeInTheDocument(); - expect(fetch).toHaveBeenCalledTimes(1); - }); - - it('applies a trimmed custom personality only after Apply', async () => { - const custom = 'Choose open adjacent cells before waiting.'; - const changed = withPersonality(initial, world.agents[0]!.id, custom); - vi.stubGlobal( - 'fetch', - vi - .fn() - .mockImplementationOnce(() => jsonResponse(initial)) - .mockImplementationOnce(() => - jsonResponse({ snapshot: changed, agent: changed.world.agents[0] }), - ), - ); - const user = userEvent.setup(); - render(); - await user.click(await screen.findByRole('button', { name: 'Edit' })); - const textarea = screen.getByRole('textbox', { - name: 'Personality directive', - }); - await user.clear(textarea); - await user.type(textarea, ` ${custom} `); - expect(fetch).toHaveBeenCalledTimes(1); - await user.click(screen.getByRole('button', { name: 'Apply' })); - expect(await screen.findByText(custom)).toBeInTheDocument(); - expect(screen.getByText('Custom')).toBeInTheDocument(); - expect(fetch).toHaveBeenLastCalledWith( - `${apiBaseForTest()}/agents/${world.agents[0]!.id}/personality`, - expect.objectContaining({ - method: 'POST', - body: JSON.stringify({ personality: custom }), + vi.fn((input: RequestInfo | URL) => { + const url = String(input); + if (url.includes('/tick/cancel')) return response({ snapshot }); + if (url.includes('/tick')) + return new Promise((resolve) => { + resolveTick = resolve; + }); + return response(snapshot); }), ); - }); - - it.each(PERSONALITY_PRESETS)( - 'selects and applies the $name preset explicitly', - async (preset) => { - const changed = withPersonality( - initial, - world.agents[0]!.id, - preset.personality, - ); - vi.stubGlobal( - 'fetch', - vi - .fn() - .mockImplementationOnce(() => jsonResponse(initial)) - .mockImplementationOnce(() => - jsonResponse({ - snapshot: changed, - agent: changed.world.agents[0], - }), - ), - ); - const user = userEvent.setup(); - render(); - await user.click(await screen.findByRole('button', { name: 'Edit' })); - await user.selectOptions( - screen.getByLabelText('Personality preset'), - preset.id, - ); - expect( - screen.getByRole('textbox', { name: 'Personality directive' }), - ).toHaveValue(preset.personality); - expect(fetch).toHaveBeenCalledTimes(1); - await user.click(screen.getByRole('button', { name: 'Apply' })); - expect(await screen.findByText(preset.personality)).toBeInTheDocument(); - expect(screen.getByText(preset.name)).toBeInTheDocument(); - }, - ); - - it('shows character count, empty validation, and Custom preset state', async () => { - const user = userEvent.setup(); - render(); - await user.click(await screen.findByRole('button', { name: 'Edit' })); - const textarea = screen.getByRole('textbox', { - name: 'Personality directive', - }); - expect( - screen.getByText(`${world.agents[0]!.personality.length}/600`), - ).toBeInTheDocument(); - expect(textarea).toHaveAttribute('maxlength', '600'); - expect(screen.getByLabelText('Personality preset')).toHaveValue('custom'); - await user.clear(textarea); - expect(screen.getByText('0/600')).toBeInTheDocument(); - await user.click(screen.getByRole('button', { name: 'Apply' })); - expect(screen.getByRole('alert')).toHaveTextContent( - 'Enter a personality between 1 and 600 characters.', - ); - expect(fetch).toHaveBeenCalledTimes(1); - }); - - it('disables personality mutations during playback and pending requests', async () => { - const user = userEvent.setup(); - render(); - await user.click(await screen.findByRole('button', { name: 'Edit' })); - await user.click(screen.getByRole('button', { name: 'Start' })); - expect( - screen.getByRole('textbox', { name: 'Personality directive' }), - ).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled(); - await openOverflow(user); - expect( - screen.getByRole('button', { name: 'Restore default personalities' }), - ).toBeDisabled(); - await user.click(screen.getByRole('button', { name: 'Pause' })); - - let resolveUpdate!: (response: Response) => void; - vi.mocked(fetch).mockImplementationOnce( - () => new Promise((resolve) => (resolveUpdate = resolve)), - ); - await user.click(screen.getByRole('button', { name: 'Apply' })); - expect(screen.getByRole('button', { name: 'Applying…' })).toBeDisabled(); - await openOverflow(user); - expect(screen.getByRole('button', { name: 'Reset world' })).toBeDisabled(); - resolveUpdate( - new Response( - JSON.stringify({ snapshot: initial, agent: initial.world.agents[0] }), - { status: 200 }, - ), - ); - await screen.findByRole('button', { name: 'Edit' }); - }); - - it('keeps an edited personality through world reset', async () => { - const edited = withPersonality( - afterInfection(), - world.agents[0]!.id, - 'Persistent lab edit.', - ); - const resetWithEdit = withPersonality( - initial, - world.agents[0]!.id, - 'Persistent lab edit.', - ); - vi.stubGlobal( - 'fetch', - vi - .fn() - .mockImplementationOnce(() => jsonResponse(edited)) - .mockImplementationOnce(() => - jsonResponse({ snapshot: resetWithEdit }), - ), - ); - const user = userEvent.setup(); - render(); - await openOverflow(user); - await user.click( - await screen.findByRole('button', { name: 'Reset world' }), - ); - expect(await screen.findByText('Persistent lab edit.')).toBeInTheDocument(); - expect(screen.getByText('Tick 0')).toBeInTheDocument(); - }); - - it('confirms restoring defaults and preserves current world progress', async () => { - const progressed = completeTickResponse( - withPersonality(afterInfection(), world.agents[0]!.id, 'Temporary edit.'), - ).snapshot; - const restored = simulationSnapshotSchema.parse({ - ...progressed, - world: { - ...progressed.world, - agents: progressed.world.agents.map((agent, index) => ({ - ...agent, - personality: world.agents[index]!.personality, - })), - }, - }); - const confirm = vi - .fn() - .mockReturnValueOnce(false) - .mockReturnValueOnce(true); - vi.stubGlobal('confirm', confirm); - vi.stubGlobal( - 'fetch', - vi - .fn() - .mockImplementationOnce(() => jsonResponse(progressed)) - .mockImplementationOnce(() => jsonResponse({ snapshot: restored })), - ); const user = userEvent.setup(); render(); await user.click( - await screen.findByRole('button', { - name: 'Select agent Ember, Patient Zero', - }), + await screen.findByRole('button', { name: 'Single tick' }), ); - await openOverflow(user); - const restore = await screen.findByRole('button', { - name: 'Restore default personalities', - }); - await user.click(restore); - expect(fetch).toHaveBeenCalledTimes(1); - await user.click(restore); - expect( - await screen.findByRole('group', { - name: 'Active personality configuration', - }), - ).toHaveTextContent(world.agents[0]!.personality); - expect(screen.getByText('Tick 1')).toBeInTheDocument(); - expect(screen.getByTestId('infected-count')).toHaveTextContent( - '1 rendered infected', + await waitFor(() => + expect(screen.getByRole('button', { name: 'Cancel' })).toBeEnabled(), ); - expect(confirm).toHaveBeenCalledTimes(2); - }); - - it('distinguishes an active edit from the immutable latest observation', async () => { - const changed = completeTickResponse( - withPersonality( - afterInfection(), - world.agents[0]!.id, - 'New active personality.', + await user.click(screen.getByRole('button', { name: 'Cancel' })); + await waitFor(() => + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/tick/cancel'), + expect.objectContaining({ method: 'POST' }), ), - ).snapshot; - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(changed)), ); - const user = userEvent.setup(); - render(); - await user.click( - await screen.findByRole('button', { - name: 'Select agent Ember, Patient Zero', + resolveTick( + new Response(JSON.stringify({ cancelled: true, snapshot }), { + status: 200, }), ); - expect( - await screen.findByText('New active personality.'), - ).toBeInTheDocument(); - expect( - screen.getByText(world.agents[0]!.personality, { exact: true }), - ).toBeInTheDocument(); - expect( - screen.getByText( - 'Immutable input supplied for Tick 1 · record 1. It is not rewritten when the active personality changes.', - ), - ).toBeInTheDocument(); - expect( - screen.getByText( - 'The active personality has changed since this observation.', - ), - ).toBeInTheDocument(); + expect(await screen.findByText('Tick 0')).toBeVisible(); }); - it('shows current and selected-agent experiment usage', async () => { + it('requests a bounded full-safe swarm export preview before enabling export actions', async () => { + const preview = { + experimentId: snapshot.experiment.id, + matchingTurnCount: 0, + matchingTickCount: 0, + matchingSwarmTickCount: 0, + matchingCommunicationCount: 0, + matchingControlChangeCount: 0, + matchingDiplomacyEventCount: 0, + matchingProviderAttemptCount: 0, + selectedAgentCount: world.agents.length, + retention: { + limit: 5000, + totalCompletedTurns: 0, + retainedTurns: 0, + droppedRecords: 0, + complete: true, + requestedRangeExtendsBeyondRetention: false, + }, + knownCostCredits: 0, + attemptsWithUnknownCost: 0, + turnsWithUnknownCost: 0, + serializedUtf8Bytes: 100, + approximateAiInputTokens: 25, + tokenEstimateMethod: 'ceil(UTF-8 bytes / 4)' as const, + }; vi.stubGlobal( 'fetch', - vi.fn(() => jsonResponse(afterInfection())), - ); - const user = userEvent.setup(); - render(); - await user.click( - await screen.findByRole('button', { - name: 'Select agent Ember, Patient Zero', - }), - ); - expect( - await screen.findByLabelText('Current experiment usage'), - ).toHaveTextContent('1 turns'); - expect(screen.getByLabelText('Current experiment usage')).toHaveTextContent( - '0.0 credits known cost', - ); - expect(screen.getByLabelText('Selected agent usage')).toHaveTextContent( - '1 turns', + vi.fn((input: RequestInfo | URL) => + String(input).endsWith('/experiment/export/preview') + ? response(preview) + : response(snapshot), + ), ); - }); - - it('supports agent selection and previews server-owned export', async () => { const user = userEvent.setup(); render(); - await openOverflow(user); + await user.click(await screen.findByLabelText('More World Lab actions')); await user.click(screen.getByRole('button', { name: 'Export' })); - await user.click(screen.getByRole('button', { name: 'Clear' })); - await user.click(screen.getByRole('checkbox', { name: /Ember/ })); - expect(screen.getByRole('checkbox', { name: /Ember/ })).toBeChecked(); - expect(screen.getByRole('checkbox', { name: /Rook/ })).not.toBeChecked(); - await user.click(screen.getByRole('checkbox', { name: /Rook/ })); - vi.mocked(fetch).mockImplementationOnce(() => - jsonResponse({ - experimentId: initial.experiment.id, - matchingTurnCount: 0, - matchingCommunicationCount: 0, - matchingControlChangeCount: 0, - matchingDiplomacyEventCount: 0, - selectedAgentCount: 2, - retention: { - limit: 5000, - totalCompletedTurns: 0, - retainedTurns: 0, - droppedRecords: 0, - complete: true, - requestedRangeExtendsBeyondRetention: false, - }, - knownCostCredits: 0, - turnsWithUnknownCost: 0, - serializedUtf8Bytes: 900, - approximateAiInputTokens: 225, - tokenEstimateMethod: 'ceil(UTF-8 bytes / 4)', - }), - ); await user.click(screen.getByRole('button', { name: 'Preview' })); - expect(await screen.findByLabelText('Export preview')).toHaveTextContent( - '900 bytes', - ); - const request = JSON.parse( - String(vi.mocked(fetch).mock.calls.at(-1)?.[1]?.body), - ); - expect(request.agents).toEqual({ - mode: 'selected', - agentIds: [world.agents[0]!.id, world.agents[1]!.id], - }); - await user.click(screen.getByRole('button', { name: 'Clear' })); - expect(screen.getByRole('button', { name: 'Preview' })).toBeDisabled(); - await user.click(screen.getByRole('button', { name: 'Select all' })); - for (const agent of world.agents) - expect( - screen.getByRole('checkbox', { name: new RegExp(agent.name) }), - ).toBeChecked(); - }); - - it('keeps export out of the details panel and dismisses its modal without losing settings', async () => { - const user = userEvent.setup(); - render(); - await openOverflow(user); - const exportButton = screen.getByRole('button', { name: 'Export' }); - expect( - screen.queryByRole('dialog', { name: 'Experiment export' }), - ).toBeNull(); - await user.click(exportButton); - await user.selectOptions(screen.getByLabelText('Export level'), 'standard'); - await user.click(screen.getByRole('button', { name: 'Close export' })); - expect( - screen.queryByRole('dialog', { name: 'Experiment export' }), - ).toBeNull(); - expect(exportButton).toHaveFocus(); - await openOverflow(user); - await user.click(exportButton); - expect(screen.getByLabelText('Export level')).toHaveValue('standard'); - const reopenedDialog = screen.getByRole('dialog', { - name: 'Experiment export', - }); - fireEvent.mouseDown(reopenedDialog); - expect( - screen.getByRole('dialog', { name: 'Experiment export' }), - ).toBeInTheDocument(); - expect(reopenedDialog.querySelector('.modal-body')).toBeInTheDocument(); - expect(reopenedDialog.querySelector('.modal-footer')).toBeInTheDocument(); - fireEvent.mouseDown(reopenedDialog.closest('.modal-backdrop')!); - expect( - screen.queryByRole('dialog', { name: 'Experiment export' }), - ).toBeNull(); - await openOverflow(user); - await user.click(exportButton); - fireEvent.keyDown(window, { key: 'Escape' }); - expect( - screen.queryByRole('dialog', { name: 'Experiment export' }), - ).toBeNull(); - }); - - it('offers every tier, turn selector, outcome/action filters, and dependent Custom switches', async () => { - const user = userEvent.setup(); - render(); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'Export' })); - const level = screen.getByLabelText('Export level'); - expect(level).toHaveTextContent('Minimal'); - expect(level).toHaveTextContent('Standard'); - expect(level).toHaveTextContent('Full safe'); - expect(level).toHaveTextContent('Custom'); - expect(screen.getByLabelText('JSON serialization')).toHaveValue('compact'); - expect(screen.getByLabelText('Communication channel')).toHaveValue('all'); - expect(screen.getByLabelText('Communication result')).toHaveValue('all'); - expect(screen.getByRole('checkbox', { name: 'lost tick' })).toBeChecked(); - await user.selectOptions( - screen.getByLabelText('Communication channel'), - 'direct', - ); - await user.selectOptions( - screen.getByLabelText('Communication result'), - 'rejected', - ); - expect(screen.getByLabelText('Communication channel')).toHaveValue( - 'direct', - ); - expect(screen.getByLabelText('Communication result')).toHaveValue( - 'rejected', - ); - await user.selectOptions( - screen.getByLabelText('JSON serialization'), - 'pretty', - ); - await user.selectOptions(level, 'custom'); - expect(screen.getByText('Advanced Custom switches')).toBeInTheDocument(); - const observations = screen.getByRole('checkbox', { - name: 'Turn observations', - }); - await user.click(observations); - expect( - screen.getByRole('checkbox', { name: 'Nearby agents' }), - ).toBeDisabled(); - expect( - screen.getByRole('checkbox', { name: 'Recent events' }), - ).toBeDisabled(); - expect( - screen.getByRole('checkbox', { - name: 'Recent public messages in observations', - }), - ).toBeDisabled(); - expect( - screen.getByRole('checkbox', { - name: 'Recent direct messages in observations', - }), - ).toBeDisabled(); - expect( - screen.getByRole('checkbox', { name: 'Canonical communications' }), - ).toBeEnabled(); - await user.selectOptions(screen.getByLabelText('Turn range'), 'range'); - expect(screen.getByLabelText('From turn')).toBeInTheDocument(); - await user.click(screen.getByRole('checkbox', { name: 'accepted' })); - await user.click(screen.getByRole('checkbox', { name: 'rejected' })); - await user.click(screen.getByRole('checkbox', { name: 'provider error' })); - await user.click(screen.getByRole('checkbox', { name: 'lost tick' })); - await user.click( - screen.getByRole('checkbox', { name: 'operator skipped' }), - ); - expect(screen.getByRole('button', { name: 'Preview' })).toBeDisabled(); - }); - - it('copies and downloads the exact same validated generated JSON and revokes its URL', async () => { - const user = userEvent.setup(); - const progressed = afterInfection(); - const fetchMock = vi.fn(async () => jsonResponse(progressed)); - vi.stubGlobal('fetch', fetchMock); - const clipboardWrite = vi.fn(async () => undefined); - Object.defineProperty(navigator, 'clipboard', { - configurable: true, - value: { writeText: clipboardWrite }, - }); - const createObjectURL = vi.fn(() => 'blob:experiment'); - const revokeObjectURL = vi.fn(); - vi.stubGlobal('URL', { ...URL, createObjectURL, revokeObjectURL }); - let downloadedFilename: string | undefined; - const click = vi - .spyOn(HTMLAnchorElement.prototype, 'click') - .mockImplementation(function (this: HTMLAnchorElement) { - downloadedFilename = this.download; - }); - render(); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'Export' })); - await selectMinimalFixtureExport(user); - expect(screen.getByRole('button', { name: 'Copy JSON' })).toBeDisabled(); - expect( - screen.getByRole('button', { name: 'Download JSON' }), - ).toBeDisabled(); - expect( - screen.getByRole('button', { name: 'Save to SQLite' }), - ).toBeDisabled(); - const document = minimalExportDocument(progressed); - const validatedDocument = experimentExportDocumentSchema.parse(document); - fetchMock.mockImplementationOnce(() => jsonResponse({ document })); - await user.click(screen.getByRole('button', { name: 'Generate export' })); - await user.click(await screen.findByRole('button', { name: 'Copy JSON' })); - expect(clipboardWrite).toHaveBeenCalledWith( - JSON.stringify(validatedDocument), - ); - clipboardWrite.mockRejectedValueOnce(new Error('denied')); - await user.click(screen.getByRole('button', { name: 'Copy JSON' })); - expect(await screen.findByText(/Copy failed/)).toBeInTheDocument(); - await user.click(screen.getByRole('button', { name: 'Download JSON' })); - expect(createObjectURL).toHaveBeenCalledTimes(1); - expect(revokeObjectURL).toHaveBeenCalledWith('blob:experiment'); - expect(click).toHaveBeenCalledTimes(1); - expect(downloadedFilename).toMatch( - /^hexzero-experiment-.+-one-agent-entire-retained\.json$/, - ); - let resolveArchive!: (response: Response) => void; - fetchMock.mockImplementationOnce( - () => - new Promise((resolve) => { - resolveArchive = resolve; - }), - ); - const archiveButton = screen.getByRole('button', { - name: 'Save to SQLite', - }); - const requestsBeforeArchive = fetchMock.mock.calls.length; - fireEvent.click(archiveButton); - fireEvent.click(archiveButton); - await waitFor(() => - expect(fetchMock.mock.calls).toHaveLength(requestsBeforeArchive + 1), - ); - expect(screen.getByRole('button', { name: 'Saving…' })).toBeDisabled(); - resolveArchive( - await jsonResponse({ - experimentId: document.experiment.id, - inserted: 4, - existing: 0, - skipped: 0, - rejected: 0, - idempotent: false, - }), - ); - const [archiveUrl, archiveInit] = fetchMock.mock.calls.at(-1)!; - expect(archiveUrl).toEqual( - expect.stringMatching(/\/experiment\/export\/archive$/), - ); - expect(archiveInit).toEqual( - expect.objectContaining({ - method: 'POST', - signal: expect.any(AbortSignal), - }), - ); - const archiveBody = JSON.parse(String(archiveInit?.body)); - expect(archiveBody).toMatchObject({ - request: validatedDocument.filters, - generatedAt: validatedDocument.generatedAt, - sha256: expect.stringMatching(/^[a-f0-9]{64}$/), - }); - expect(archiveBody).not.toHaveProperty('document'); - expect(String(archiveInit?.body).length).toBeLessThan(10_000); - expect(await screen.findByText(/saved to SQLite/)).toBeInTheDocument(); - fetchMock.mockRejectedValueOnce(new Error('archive unavailable')); - await user.click(screen.getByRole('button', { name: 'Save to SQLite' })); - expect( - await screen.findByText(/Could not confirm the SQLite save/), - ).toBeInTheDocument(); - const exportDialog = screen.getByRole('dialog', { - name: 'Experiment export', + expect(await screen.findByLabelText('Export preview')).toBeVisible(); + const previewRequest = vi + .mocked(fetch) + .mock.calls.find(([url]) => + String(url).endsWith('/experiment/export/preview'), + ); + expect(JSON.parse(String(previewRequest?.[1]?.body))).toMatchObject({ + agents: { mode: 'all' }, + turns: { mode: 'entire-retained' }, + level: 'full-safe', }); - expect(within(exportDialog).getByRole('status')).toHaveTextContent( - 'Retry safely with the same generated export.', - ); - await user.selectOptions( - screen.getByLabelText('JSON serialization'), - 'pretty', - ); - expect(screen.getByRole('button', { name: 'Copy JSON' })).toBeDisabled(); - expect( - screen.getByRole('button', { name: 'Download JSON' }), - ).toBeDisabled(); - expect( - screen.getByRole('button', { name: 'Save to SQLite' }), - ).toBeDisabled(); - expect( - screen.getByText('Options changed — regenerate export.'), - ).toBeInTheDocument(); - click.mockRestore(); - }); - - it('invalidates a generated artifact when compact SQLite archival reports it changed', async () => { - const user = userEvent.setup(); - const progressed = afterInfection(); - const fetchMock = vi.fn(async () => jsonResponse(progressed)); - vi.stubGlobal('fetch', fetchMock); - render(); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'Export' })); - await selectMinimalFixtureExport(user); - const document = minimalExportDocument(progressed); - fetchMock.mockImplementationOnce(() => jsonResponse({ document })); - await user.click(screen.getByRole('button', { name: 'Generate export' })); - expect( - await screen.findByRole('button', { name: 'Save to SQLite' }), - ).toBeEnabled(); - fetchMock.mockResolvedValueOnce( - new Response( - JSON.stringify({ - error: { - code: 'artifact_changed', - message: - 'The experiment changed after this export was generated. Generate it again before saving.', - }, - }), - { - status: 409, - headers: { 'Content-Type': 'application/json' }, - }, - ), - ); - await user.click(screen.getByRole('button', { name: 'Save to SQLite' })); - expect( - await screen.findByText( - 'The experiment changed after this export was generated. Generate it again before saving.', - ), - ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Copy JSON' })).toBeDisabled(); expect( screen.getByRole('button', { name: 'Download JSON' }), ).toBeDisabled(); - expect( - screen.getByRole('button', { name: 'Save to SQLite' }), - ).toBeDisabled(); - expect( - screen.getByRole('button', { name: 'Generate export' }), - ).toBeEnabled(); - expect(screen.queryByText(/saved to SQLite/)).not.toBeInTheDocument(); - }); - - it('aborts a stalled compact SQLite archive request after ten seconds', async () => { - const user = userEvent.setup(); - const progressed = afterInfection(); - const fetchMock = vi.fn(async () => jsonResponse(progressed)); - vi.stubGlobal('fetch', fetchMock); - render(); - await openOverflow(user); - await user.click(screen.getByRole('button', { name: 'Export' })); - await selectMinimalFixtureExport(user); - const document = minimalExportDocument(progressed); - fetchMock.mockImplementationOnce(() => jsonResponse({ document })); - await user.click(screen.getByRole('button', { name: 'Generate export' })); - - let archiveStarted!: () => void; - const started = new Promise((resolve) => { - archiveStarted = resolve; - }); - fetchMock.mockImplementationOnce( - (_input, init) => - new Promise((_resolve, reject) => { - archiveStarted(); - init?.signal?.addEventListener( - 'abort', - () => reject(new DOMException('Aborted', 'AbortError')), - { once: true }, - ); - }), - ); - vi.useFakeTimers(); - try { - fireEvent.click(screen.getByRole('button', { name: 'Save to SQLite' })); - await started; - await act(async () => { - await vi.advanceTimersByTimeAsync(10_000); - }); - expect( - screen.getByText(/Could not confirm the SQLite save/), - ).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Save to SQLite' }), - ).toBeEnabled(); - expect(screen.queryByText(/saved to SQLite/)).not.toBeInTheDocument(); - } finally { - vi.useRealTimers(); - } - }); - - it('auto-pauses a fully infected world and disables automatic Start only', async () => { - const infected = simulationSnapshotSchema.parse({ - ...initial, - world: { - ...initial.world, - hexes: initial.world.hexes.map((hex) => ({ - ...hex, - state: 'infected' as const, - controllerAgentId: initial.world.agents[0]!.id, - })), - }, - experiment: { - ...initial.experiment, - currentTerritory: initial.experiment.currentTerritory.map( - (entry, index) => ({ - ...entry, - controlledCellCount: index === 0 ? 127 : 0, - }), - ), - }, - }); - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(infected)), - ); - render(); - expect( - await screen.findByText(/Development world fully infected/), - ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Start' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Single tick' })).toBeEnabled(); - fireEvent.click(screen.getByLabelText('More World Lab actions')); - expect(screen.getByRole('button', { name: 'Reset world' })).toBeEnabled(); - }); - - it('stops all tick controls after infection is eliminated', async () => { - const eliminated = simulationSnapshotSchema.parse({ - ...initial, - status: 'infection-eliminated', - tickNumber: 1, - lastTickIntervalMinutes: 5, - resolutionOrder: [], - nextAgentId: null, - world: { ...initial.world, agents: [] }, - resolvedModels: [], - behaviorConfiguration: undefined, - agentGoals: [], - agentMemories: [], - experiment: { ...initial.experiment, currentTerritory: [] }, - }); - expect( - simulationSnapshotSchema.safeParse(JSON.parse(JSON.stringify(eliminated))) - .success, - ).toBe(true); - vi.stubGlobal( - 'fetch', - vi.fn(() => jsonResponse(eliminated)), - ); - render(); - - expect( - await screen.findByText(/All infection has been eliminated/), - ).toBeVisible(); - expect(screen.getByRole('button', { name: 'Start' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Single tick' })).toBeDisabled(); }); }); - -function minimalExportDocument(snapshot: SimulationSnapshot) { - const turn = snapshot.turns[0]!; - const agent = snapshot.world.agents[0]!; - return { - schemaVersion: 9 as const, - generatedAt: '2026-08-13T12:00:02.000Z', - experiment: { - id: snapshot.experiment.id, - startedAt: snapshot.experiment.startedAt, - providerMode: snapshot.providerMode, - }, - retention: { - limit: 5000, - totalCompletedTurns: 1, - retainedTurns: 1, - firstRetainedTurn: 1, - lastRetainedTurn: 1, - droppedRecords: 0, - complete: true, - requestedRangeExtendsBeyondRetention: false, - }, - filters: { - agents: { mode: 'selected' as const, agentIds: [agent.id] }, - turns: { mode: 'entire-retained' as const }, - outcomes: ['accepted', 'rejected', 'provider-error'] as const, - actions: ['move', 'infect', 'capture', 'wait'] as const, - communications: { channel: 'all' as const, status: 'all' as const }, - level: 'minimal' as const, - }, - selection: { - selectedAgentIds: [agent.id], - matchingTurnCount: 1, - matchingCommunicationCount: 0, - matchingControlChangeCount: 0, - matchingDiplomacyEventCount: 0, - firstMatchingTurn: 1, - lastMatchingTurn: 1, - }, - agents: [agent], - metrics: { - aggregate: snapshot.experiment.metrics.aggregate, - byAgent: [snapshot.experiment.metrics.byAgent[0]!], - }, - currentTerritory: snapshot.experiment.currentTerritory, - currentAlliances: snapshot.experiment.currentAlliances, - communications: [], - controlChanges: [], - allianceEvents: [], - turns: [ - { - turnNumber: turn.turnNumber, - startedAt: turn.startedAt, - completedAt: turn.completedAt, - agentId: turn.agentId, - outcome: turn.outcome, - ...(turn.outcome === 'accepted' - ? { - worldAction: turn.worldAction, - summary: turn.summary, - worldActionSummary: `Infected ${agent.currentCell}.`, - provider: turn.provider, - } - : {}), - }, - ], - }; -} - -function apiBaseForTest() { - return process.env.NEXT_PUBLIC_GAME_API_BASE_URL ?? '/api/game/simulation'; -} diff --git a/apps/world-lab/src/components/world-lab.tsx b/apps/world-lab/src/components/world-lab.tsx index d54718f..ce8b8dd 100644 --- a/apps/world-lab/src/components/world-lab.tsx +++ b/apps/world-lab/src/components/world-lab.tsx @@ -18,7 +18,7 @@ import { assignBehavior, archiveExperimentExportResponseSchema, cancelSimulationResponseSchema, - cancelledTurnResponseSchema, + cancelledTickResponseSchema, experimentExportPreviewSchema, experimentExportRequestSchema, experimentExportResponseSchema, @@ -62,6 +62,7 @@ import { matchingPersonalityPreset, PERSONALITY_PRESETS, } from './personality-presets'; + import { WorldMap } from './world-map'; import { buildModelOptions } from './model-options'; import { resolveAgentColor } from './ui-color'; @@ -530,9 +531,9 @@ export function WorldLab() { } return; } - if (!response.ok) throw new Error('turn request failed'); + if (!response.ok) throw new Error('tick request failed'); const body: unknown = await response.json(); - const cancellation = cancelledTurnResponseSchema.safeParse(body); + const cancellation = cancelledTickResponseSchema.safeParse(body); if (cancellation.success) { applySnapshot(cancellation.data.snapshot); setUiError('The request was cancelled without consuming a tick.'); @@ -540,12 +541,9 @@ export function WorldLab() { } const payload = singleTickResponseSchema.parse(body); applySnapshot(payload.snapshot); - const lost = payload.records.filter( - ({ outcome }) => outcome === 'lost-tick', - ); - if (lost.length) + if (payload.swarmTick?.plannerFailure) setRecoveryNotice( - `${lost.length} agent${lost.length === 1 ? '' : 's'} lost this tick; all other decisions committed.`, + 'Agent Zero fell back to the deterministic directive plan for this tick.', ); } catch { setUiError('The response was lost. Reconciling with the Game API…'); @@ -621,9 +619,9 @@ export function WorldLab() { if (inFlightRef.current) return; if ( snapshot && - snapshot.experiment.totalCompletedTurns > 0 && + snapshot.tickNumber > 0 && !window.confirm( - `Reset World will discard ${snapshot.tickNumber} completed ticks (${snapshot.experiment.totalCompletedTurns} agent records) and all unexported telemetry. Continue?`, + `Reset World will discard ${snapshot.tickNumber} completed ticks and all unexported telemetry. Continue?`, ) ) return; @@ -774,9 +772,7 @@ export function WorldLab() { memberAgentIds.includes(selectedHexController.id), ) : undefined; - const latestTurn = selectedAgent - ? snapshot.turns.findLast(({ agentId }) => agentId === selectedAgent.id) - : undefined; + const latestTurn = undefined; const status = resetting ? 'resetting' : reconciling @@ -797,7 +793,9 @@ export function WorldLab() { const terminal = snapshot.status === 'patient-zero-captured' || snapshot.status === 'infection-eliminated'; - const swarmMode = snapshot.scenario.cognitionMode === 'zero-swarm-v1'; + // PR2 removes the inactive social panels; World Lab now always enters via + // the swarm workspace. + const swarmMode = true; const visibleActivityTab = swarmMode ? 'swarm' : activityTab; const zeroAgentId = snapshot.scenario.patientZeroAgentId; const zeroModel = snapshot.resolvedModels.find( @@ -817,12 +815,8 @@ export function WorldLab() { personalityPending || snapshot.activeAgentId !== null || activeTick; - const modelsReady = swarmMode - ? Boolean(zeroModel?.available) - : snapshot.resolvedModels.every(({ available }) => available); - const executionReady = swarmMode - ? modelsReady - : snapshot.providerConfigured && modelsReady; + const modelsReady = Boolean(zeroModel?.available); + const executionReady = modelsReady; const reasoningUnavailable = snapshot.resolvedModels.some( ({ issue }) => issue === 'reasoning-unavailable', ); @@ -845,9 +839,7 @@ export function WorldLab() { WL
-

- {swarmMode ? 'Zero swarm v1' : 'Legacy multi-agent'} experiment -

+

Swarm experiment

World Lab