From c10e0a304a6b91ee63ecd57296764685c088d837 Mon Sep 17 00:00:00 2001 From: Christopher Nelson Date: Sun, 20 Sep 2026 00:01:24 -0400 Subject: [PATCH] feat(world-lab): present zero-swarm execution telemetry --- .../src/simulation-service.swarm.test.ts | 37 +- apps/game-api/src/simulation-service.ts | 34 +- apps/world-lab/src/app/styles.css | 74 + .../src/components/swarm-view.test.tsx | 159 ++ apps/world-lab/src/components/swarm-view.tsx | 469 +++++ .../src/components/world-lab.test.tsx | 72 + apps/world-lab/src/components/world-lab.tsx | 1822 ++++++++++------- docs/ARCHITECTURE.md | 5 +- docs/SECURITY.md | 6 + docs/TESTING.md | 4 + docs/adr/0030-zero-swarm-world-lab.md | 42 + packages/shared/src/index.ts | 11 + 12 files changed, 1961 insertions(+), 774 deletions(-) create mode 100644 apps/world-lab/src/components/swarm-view.test.tsx create mode 100644 apps/world-lab/src/components/swarm-view.tsx create mode 100644 docs/adr/0030-zero-swarm-world-lab.md diff --git a/apps/game-api/src/simulation-service.swarm.test.ts b/apps/game-api/src/simulation-service.swarm.test.ts index 5e63d46..167452f 100644 --- a/apps/game-api/src/simulation-service.swarm.test.ts +++ b/apps/game-api/src/simulation-service.swarm.test.ts @@ -3,6 +3,7 @@ import { BrowserTestAgentProvider, ReflexProviderError, ScriptedReflexProvider, + type AgentProvider, type PlannerOptions, type ReflexProvider, type SwarmPlanner, @@ -92,9 +93,10 @@ function setup( planner: SwarmPlanner, reflex: ReflexProvider, pressure = false, + provider: AgentProvider = new BrowserTestAgentProvider(), ) { const simulation = new SimulationService({ - provider: new BrowserTestAgentProvider(), + provider, swarmPlanner: planner, reflexProvider: reflex, now: () => '2026-08-13T12:00:00.000Z', @@ -129,6 +131,29 @@ function setup( } describe('zero-swarm SimulationService tick', () => { + 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(), @@ -165,7 +190,17 @@ describe('zero-swarm SimulationService tick', () => { expect(snapshot.turnNumber).toBe(0); expect(snapshot.turns).toEqual([]); expect(snapshot.swarmTicks).toHaveLength(1); + expect(snapshot.swarmProviderStatus).toMatchObject({ + plannerMode: 'scripted-swarm-test', + plannerConfigured: true, + reflexMode: 'scripted-reflex-test', + reflexConfigured: true, + }); expect(snapshot.swarmTicks?.[0]?.workers).toHaveLength(7); + expect(snapshot.swarmTicks?.[0]?.workers[0]?.situation).toMatchObject({ + directiveProgress: expect.any(String), + nearbyPressure: expect.any(String), + }); expect( snapshot.swarmTicks?.[0]?.workers.every( ({ source }) => source === 'jev-reflex', diff --git a/apps/game-api/src/simulation-service.ts b/apps/game-api/src/simulation-service.ts index a5b223f..e47058b 100644 --- a/apps/game-api/src/simulation-service.ts +++ b/apps/game-api/src/simulation-service.ts @@ -422,6 +422,21 @@ export class SimulationService { 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 } + : {}), + }, + } + : {}), modelConfiguration: this.#modelConfiguration, behaviorConfiguration: this.#behaviorConfiguration, resolvedModels: agents.map(({ id }) => this.#resolvedModel(id)), @@ -513,7 +528,14 @@ export class SimulationService { ...structuredClone(this.#scenario.behaviorConfiguration), locked: false, }; - this.#status = this.#provider.configured ? 'paused' : 'configuration-error'; + this.#status = + this.#scenario.cognitionMode === 'zero-swarm-v1' && + this.#swarmPlanner && + this.#reflexProvider + ? 'paused' + : this.#provider.configured + ? 'paused' + : 'configuration-error'; return this.getSnapshot(); } @@ -656,7 +678,14 @@ export class SimulationService { this.#scenario.executionLimits, this.#experimentRetentionLimit, ); - this.#status = this.#provider.configured ? 'paused' : 'configuration-error'; + this.#status = + this.#scenario.cognitionMode === 'zero-swarm-v1' && + this.#swarmPlanner && + this.#reflexProvider + ? 'paused' + : this.#provider.configured + ? 'paused' + : 'configuration-error'; return this.getSnapshot(); } @@ -1670,6 +1699,7 @@ export class SimulationService { directive: plan.directives.find( ({ agentId }) => agentId === worker.id, )!, + situation: selection.observation.currentSituation, action: selection.action, actionResult: applied.get(worker.id), ...(selection.decision diff --git a/apps/world-lab/src/app/styles.css b/apps/world-lab/src/app/styles.css index 97d9cc0..af9307c 100644 --- a/apps/world-lab/src/app/styles.css +++ b/apps/world-lab/src/app/styles.css @@ -1044,6 +1044,80 @@ h2 { padding: 14px; border-bottom: 1px solid #2d3835; } +.swarm-panel h2, +.swarm-panel h3, +.swarm-panel h4 { + margin-bottom: 8px; +} +.swarm-summary, +.swarm-muted, +.swarm-progress, +.swarm-empty { + color: var(--text-muted); + font-size: 0.82rem; + line-height: 1.45; +} +.swarm-facts { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1px; + margin: 10px 0; + background: var(--line); +} +.swarm-facts > div { + min-width: 0; + padding: 8px; + background: var(--surface-raised); +} +.swarm-facts dt { + color: var(--text-muted); + font-size: 0.67rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} +.swarm-facts dd { + margin: 4px 0 0; + overflow-wrap: anywhere; + font-size: 0.78rem; +} +.swarm-directives, +.swarm-probabilities, +.swarm-activity ol { + display: grid; + gap: 5px; + margin: 8px 0; + padding: 0; + list-style: none; +} +.swarm-directives li, +.swarm-activity li { + display: grid; + gap: 3px; + padding: 8px; + border-left: 2px solid var(--accent-strong); + background: var(--surface-raised); + font-size: 0.78rem; +} +.swarm-directives button, +.swarm-cell-link { + width: fit-content; + padding: 0; + border: 0; + background: transparent; + color: var(--accent); + cursor: pointer; + font: inherit; + font-weight: 700; + text-align: left; +} +.swarm-probabilities li { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 6px 8px; + background: var(--surface-raised); + font-size: 0.78rem; +} .usage-meter, .agent-usage { display: flex; diff --git a/apps/world-lab/src/components/swarm-view.test.tsx b/apps/world-lab/src/components/swarm-view.test.tsx new file mode 100644 index 0000000..0bac5ec --- /dev/null +++ b/apps/world-lab/src/components/swarm-view.test.tsx @@ -0,0 +1,159 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import type { SimulationSnapshot } from '@hexzero/shared'; +import { + SwarmAgentInspector, + SwarmRunPanel, + SwarmStrategyPanel, +} from './swarm-view'; + +const zero = '00000000-0000-4000-8000-000000000001'; +const worker = '00000000-0000-4000-8000-000000000002'; +const cell = '8928308280fffff'; + +function snapshot(withTick = true): SimulationSnapshot { + return { + scenario: { patientZeroAgentId: zero }, + world: { + agents: [ + { id: zero, name: 'Zero', currentCell: cell }, + { id: worker, name: 'Worker', currentCell: cell }, + ], + }, + swarmTicks: withTick + ? [ + { + tickNumber: 3, + planSource: 'deterministic-fallback', + plannerFailure: { code: 'timeout', message: 'Planner timed out' }, + plan: { + strategySummary: 'Secure the current infection.', + directives: [ + { + id: 'directive-1', + agentId: worker, + mission: 'expand', + targetCell: cell, + priority: 'high', + riskTolerance: 'medium', + issuedAtTick: 3, + expiresAtTick: 5, + }, + ], + zeroActionCandidateId: 'zero_action_0', + }, + zeroAction: { type: 'wait' }, + zeroActionResult: { accepted: true, event: { id: 'event-1' } }, + workers: [ + { + agentId: worker, + directive: { + id: 'directive-1', + agentId: worker, + mission: 'expand', + targetCell: cell, + priority: 'high', + riskTolerance: 'medium', + issuedAtTick: 3, + expiresAtTick: 5, + }, + action: { type: 'infect' }, + actionResult: { + accepted: false, + reason: 'already-infected', + details: 'already infected', + }, + source: 'jev-reflex', + reflexDecision: { + chosenCandidateId: 'action_1', + confidence: 0.72, + probabilities: { action_0: 0.28, action_1: 0.72 }, + model: 'jev', + latencyMs: 12, + inputTokens: 4, + outputTokens: 2, + directiveId: 'directive-1', + cognitionSource: 'jev-reflex', + }, + }, + ], + }, + ] + : [], + experiment: { + attemptAccounting: { + attemptsStarted: 2, + attemptsFinalized: 2, + attemptsWithUnknownCost: 1, + knownFinalizedCostCredits: '0.02', + }, + }, + swarmProviderStatus: { + plannerMode: 'openrouter-swarm', + plannerConfigured: true, + reflexMode: 'typesafe-jev', + reflexConfigured: true, + reflexModel: 'jev', + }, + } as unknown as SimulationSnapshot; +} + +describe('swarm telemetry panels', () => { + it('shows Zero fallback and worker reflex telemetry without social labels', () => { + const value = snapshot(); + render( + <> + + + + , + ); + expect(screen.getByText('Deterministic fallback')).toBeInTheDocument(); + expect(screen.getByText(/Planner timed out/)).toBeInTheDocument(); + expect(screen.getAllByText('action_1')).toHaveLength(2); + expect(screen.getAllByText('72%')).toHaveLength(2); + expect(screen.getByText(/TypeSafe cost unknown/)).toBeInTheDocument(); + expect( + screen.queryByText(/alliance|chat|personality|memory/i), + ).not.toBeInTheDocument(); + }); + + it('shows the telemetry empty state before the first committed tick', () => { + render(); + expect( + screen.getByText('No committed swarm tick telemetry yet.'), + ).toBeInTheDocument(); + }); + + it('keeps fallback worker actions and failures visible without Jev telemetry', () => { + const value = snapshot(); + const fallback = value.swarmTicks![0]!.workers[0]!; + fallback.source = 'deterministic-fallback'; + fallback.action = { type: 'wait' }; + fallback.reflexDecision = undefined; + fallback.failure = { + code: 'timeout', + message: 'Reflex timed out', + retryable: true, + }; + render( + , + ); + expect(screen.getByText('Fallback action')).toBeInTheDocument(); + expect( + screen.getByText(/Wait · Rejected · already-infected/), + ).toBeInTheDocument(); + expect(screen.getByText('deterministic-fallback')).toBeInTheDocument(); + expect(screen.getByText(/Reflex timed out/)).toBeInTheDocument(); + }); +}); diff --git a/apps/world-lab/src/components/swarm-view.tsx b/apps/world-lab/src/components/swarm-view.tsx new file mode 100644 index 0000000..ddb66ad --- /dev/null +++ b/apps/world-lab/src/components/swarm-view.tsx @@ -0,0 +1,469 @@ +import type { + AgentId, + H3Cell, + SimulationSnapshot, + WorldAction, +} from '@hexzero/shared'; + +type Agent = SimulationSnapshot['world']['agents'][number]; +type Tick = NonNullable[number]; + +function latestTick(snapshot: SimulationSnapshot): Tick | undefined { + return snapshot.swarmTicks?.at(-1); +} + +function isPresent(value: T | undefined): value is T { + return value !== undefined; +} + +function actionLabel(action?: WorldAction): string { + if (!action) return 'No physical action recorded'; + return action.type === 'move' + ? `Move to ${action.targetCell}` + : action.type[0]!.toUpperCase() + action.type.slice(1); +} + +function resultLabel(result?: { accepted: boolean; reason?: string }): string { + if (!result) return 'No validation result recorded'; + return result.accepted + ? 'Accepted' + : `Rejected${result.reason ? ` · ${result.reason}` : ''}`; +} + +function agentName(snapshot: SimulationSnapshot, id: AgentId): string { + return snapshot.world.agents.find((agent) => agent.id === id)?.name ?? id; +} + +function EmptyTelemetry() { + return

No committed swarm tick telemetry yet.

; +} + +export function SwarmStrategyPanel({ + snapshot, + onSelectAgent, +}: { + snapshot: SimulationSnapshot; + onSelectAgent?: (agentId: AgentId) => void; +}) { + const tick = latestTick(snapshot); + if (!tick) + return ( +
+

Swarm strategy

+

Agent Zero

+ +
+ ); + return ( +
+

+ Swarm strategy · committed tick {tick.tickNumber} +

+

Agent Zero

+

{tick.plan.strategySummary}

+
+
+
Plan source
+
+ {tick.planSource === 'zero-llm' + ? 'Zero provider plan' + : 'Deterministic fallback'} +
+
+
+
Planner failure
+
+ {tick.plannerFailure + ? `${tick.plannerFailure.code}: ${tick.plannerFailure.message}` + : 'None recorded'} +
+
+
+
Zero physical action
+
+ {actionLabel(tick.zeroAction)} ·{' '} + {resultLabel(tick.zeroActionResult)} +
+
+
+

Worker directives

+
    + {tick.plan.directives.map((directive) => ( +
  • + {onSelectAgent ? ( + + ) : ( + {agentName(snapshot, directive.agentId)} + )} + + {directive.mission} · {directive.targetCell ?? 'no target'} ·{' '} + {directive.priority} priority · {directive.riskTolerance} risk · + expires tick {directive.expiresAtTick} + +
  • + ))} +
+
+ ); +} + +function derivedProgress(ticks: readonly Tick[], agentId: AgentId): string { + const outcomes = ticks.slice(-3).flatMap((tick) => + tick.workers + .filter((worker) => worker.agentId === agentId) + .map((worker) => ({ + action: worker.action, + result: worker.actionResult, + })), + ); + if (!outcomes.length) return 'Unknown · no recent committed action outcome'; + if (outcomes.every(({ result }) => result?.accepted === false)) + return 'Stalled · derived from recent rejected committed actions'; + if ( + outcomes.some( + ({ action, result }) => result?.accepted && action?.type !== 'wait', + ) + ) + return 'Advancing · derived from a recent accepted non-wait committed action'; + return 'Unknown · recent committed action outcomes unavailable'; +} + +function workerSituation( + worker: Tick['workers'][number] | undefined, + ticks: readonly Tick[], + agentId: AgentId, +): string { + if (worker?.situation) + return `${worker.situation.directiveProgress} · pressure ${worker.situation.nearbyPressure} · territory ${worker.situation.recentTerritoryTrend} · recent outcome ${worker.situation.recentActionOutcome}`; + return derivedProgress(ticks, agentId); +} + +export function SwarmAgentInspector({ + snapshot, + agent, + cellState, + controlledCellCount, + onHighlightCell, +}: { + snapshot: SimulationSnapshot; + agent: Agent; + cellState: 'open' | 'infected'; + controlledCellCount: number; + onHighlightCell?: (cell: H3Cell) => void; +}) { + const tick = latestTick(snapshot); + const isZero = agent.id === snapshot.scenario.patientZeroAgentId; + const worker = tick?.workers.find((entry) => entry.agentId === agent.id); + const directive = + worker?.directive ?? + tick?.plan.directives.find((entry) => entry.agentId === agent.id); + return ( +
+

+ {isZero ? 'Agent Zero inspector' : 'Worker inspector'} +

+

{agent.name}

+
+
+
Cell
+
+ {agent.currentCell} · {cellState} +
+
+
+
Controlled cells
+
{controlledCellCount}
+
+
+ {!tick ? ( + + ) : isZero ? ( + <> +

Zero action

+

+ {actionLabel(tick.zeroAction)} ·{' '} + {resultLabel(tick.zeroActionResult)} +

+

+ Strategy and provider telemetry appear in the swarm strategy and run + panels. +

+ + ) : ( + <> +

Directive

+ {directive ? ( +
+
+
Mission
+
{directive.mission}
+
+
+
Target
+
+ {directive.targetCell ? ( + onHighlightCell ? ( + + ) : ( + directive.targetCell + ) + ) : ( + 'None' + )} +
+
+
+
Priority / risk
+
+ {directive.priority} / {directive.riskTolerance} +
+
+
+
Expiry
+
Tick {directive.expiresAtTick}
+
+
+ ) : ( +

No worker directive retained.

+ )} +

+ {worker?.source === 'deterministic-fallback' + ? 'Fallback action' + : 'Jev reflex'} +

+ {worker ? ( + <> +
+
+
Selected physical action
+
+ {actionLabel(worker.action)} ·{' '} + {resultLabel(worker.actionResult)} +
+
+
+
Decision source
+
{worker.source}
+
+ {worker.failure && ( +
+
Failure
+
+ {worker.failure.code}: {worker.failure.message} +
+
+ )} + {worker.reflexDecision && ( + <> +
+
Chosen candidate
+
{worker.reflexDecision.chosenCandidateId}
+
+
+
Confidence
+
+ {Math.round(worker.reflexDecision.confidence * 100)}% +
+
+ + )} +
+

+ {workerSituation(worker, snapshot.swarmTicks ?? [], agent.id)} +

+ {worker.reflexDecision ? ( + <> +

Bounded candidate probabilities

+
    + {Object.entries(worker.reflexDecision.probabilities).map( + ([id, probability]) => ( +
  • + {id} + {Math.round(probability * 100)}% +
  • + ), + )} +
+ + ) : ( +

+ No Jev probability telemetry retained for this fallback + action. +

+ )} + + ) : ( +

No worker telemetry retained.

+ )} + + )} +
+ ); +} + +export function SwarmActivityPanel({ + snapshot, +}: { + snapshot: SimulationSnapshot; +}) { + const ticks = snapshot.swarmTicks ?? []; + const latest = ticks.at(-1); + return ( +
+

Swarm activity

+

Agent Zero strategy

+ {!latest ? ( + + ) : ( + <> +

{latest.plan.strategySummary}

+

+ {latest.planSource === 'zero-llm' + ? 'Zero provider plan' + : 'Deterministic fallback plan'} +

+

Committed ticks

+
    + {ticks + .slice() + .reverse() + .map((tick) => ( +
  1. + Tick {tick.tickNumber} + + {tick.planSource === 'zero-llm' + ? 'Zero plan' + : 'Fallback plan'}{' '} + · {tick.workers.length} worker actions · Zero:{' '} + {resultLabel(tick.zeroActionResult)} + +
  2. + ))} +
+ + )} +
+ ); +} + +export function SwarmRunPanel({ + snapshot, + status, + runTarget, +}: { + snapshot: SimulationSnapshot; + status: string; + runTarget: number | null; +}) { + const tick = latestTick(snapshot); + const accounting = snapshot.experiment.attemptAccounting; + const jev = (snapshot.swarmTicks ?? []).flatMap((entry) => + entry.workers.map((worker) => worker.reflexDecision).filter(isPresent), + ); + const zeroMetadata = (snapshot.swarmTicks ?? []) + .map((entry) => entry.plannerMetadata) + .filter(isPresent); + const total = ( + items: Array<{ + inputTokens?: number; + outputTokens?: number; + promptTokens?: number; + completionTokens?: number; + latencyMs: number; + }>, + ) => + items.reduce( + (sum, item) => + sum + + (item.inputTokens ?? item.promptTokens ?? 0) + + (item.outputTokens ?? item.completionTokens ?? 0), + 0, + ); + const providers = snapshot.swarmProviderStatus; + return ( +
+

Swarm run

+

Execution

+
+
+
Status
+
{status.replaceAll('-', ' ')}
+
+
+
Run target
+
{runTarget ?? 'No bounded target'}
+
+
+
Committed swarm tick
+
{tick?.tickNumber ?? 'None'}
+
+
+
Planner provider
+
+ {providers + ? `${providers.plannerMode} · ${providers.plannerConfigured ? 'configured' : 'unavailable'}` + : 'Not reported'} +
+
+
+
Jev provider
+
+ {providers + ? `${providers.reflexMode} · ${providers.reflexConfigured ? 'configured' : 'unavailable'}${providers.reflexModel ? ` · ${providers.reflexModel}` : ''}` + : 'Not reported'} +
+
+
+
Provider attempts
+
+ {accounting.attemptsStarted} started ·{' '} + {accounting.attemptsFinalized} finalized +
+
+
+
Known finalized cost
+
{accounting.knownFinalizedCostCredits} credits
+
+
+
Unknown provider costs
+
{accounting.attemptsWithUnknownCost} · TypeSafe cost unknown
+
+
+
Zero retained reported usage
+
+ {total(zeroMetadata)} tokens ·{' '} + {zeroMetadata.reduce((sum, item) => sum + item.latencyMs, 0)} ms +
+
+
+
Jev retained reported usage
+
+ {total(jev)} tokens ·{' '} + {jev.reduce((sum, item) => sum + item.latencyMs, 0)} ms +
+
+
+
+ ); +} diff --git a/apps/world-lab/src/components/world-lab.test.tsx b/apps/world-lab/src/components/world-lab.test.tsx index b7c04b5..96ea922 100644 --- a/apps/world-lab/src/components/world-lab.test.tsx +++ b/apps/world-lab/src/components/world-lab.test.tsx @@ -1037,6 +1037,78 @@ async function openAgentsWorkspace(user: ReturnType) { } 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, + }, + }); + vi.stubGlobal( + 'fetch', + vi.fn(() => jsonResponse(swarm)), + ); + 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/ }), + ); + 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(); + 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', diff --git a/apps/world-lab/src/components/world-lab.tsx b/apps/world-lab/src/components/world-lab.tsx index 0af6311..e721dee 100644 --- a/apps/world-lab/src/components/world-lab.tsx +++ b/apps/world-lab/src/components/world-lab.tsx @@ -66,6 +66,12 @@ import { WorldMap } from './world-map'; import { buildModelOptions } from './model-options'; import { resolveAgentColor } from './ui-color'; import { BEHAVIOR_TRACE_LIMIT, deriveBehaviorTrace } from './behavior-trace'; +import { + SwarmActivityPanel, + SwarmAgentInspector, + SwarmRunPanel, + SwarmStrategyPanel, +} from './swarm-view'; const apiBase = process.env.NEXT_PUBLIC_GAME_API_BASE_URL ?? '/api/game/simulation'; @@ -95,7 +101,7 @@ export function WorldLab() { 'scoreboard' | 'agent' | 'hex' | 'run' >('agent'); const [activityTab, setActivityTab] = useState< - 'chat' | 'private' | 'events' | 'recovery' + 'chat' | 'private' | 'events' | 'recovery' | 'swarm' >('chat'); const [snapshot, setSnapshot] = useState(null); const [privateCommsUnread, setPrivateCommsUnread] = useState(0); @@ -782,6 +788,12 @@ export function WorldLab() { ? 'running' : 'paused'; const activeTick = snapshot.status === 'waiting-for-model'; + const swarmMode = snapshot.scenario.cognitionMode === 'zero-swarm-v1'; + const visibleActivityTab = swarmMode ? 'swarm' : activityTab; + const zeroAgentId = snapshot.scenario.patientZeroAgentId; + const zeroModel = snapshot.resolvedModels.find( + ({ agentId }) => agentId === zeroAgentId, + ); const personalityControlsDisabled = running || inFlight || @@ -796,9 +808,12 @@ export function WorldLab() { personalityPending || snapshot.activeAgentId !== null || activeTick; - const modelsReady = snapshot.resolvedModels.every( - ({ available }) => available, - ); + const modelsReady = swarmMode + ? Boolean(zeroModel?.available) + : snapshot.resolvedModels.every(({ available }) => available); + const executionReady = swarmMode + ? modelsReady + : snapshot.providerConfigured && modelsReady; const reasoningUnavailable = snapshot.resolvedModels.some( ({ issue }) => issue === 'reasoning-unavailable', ); @@ -864,30 +879,43 @@ export function WorldLab() {
State
{status.replaceAll('-', ' ')}
-
-
Retained turns
-
{snapshot.experiment.retainedTurns}
-
-
-
Patient Zero
-
- {snapshot.world.agents.find( - ({ id }) => id === snapshot.scenario.patientZeroAgentId, - )?.name ?? 'None'} -
-
-
-
Public messages
-
- {snapshot.experiment.metrics.aggregate.publicMessagesSent} -
-
-
-
Direct messages
-
- {snapshot.experiment.metrics.aggregate.directMessagesSent} -
-
+ {swarmMode ? ( +
+
Retained swarm ticks
+
{snapshot.swarmTicks?.length ?? 0}
+
+ ) : ( +
+
Retained turns
+
{snapshot.experiment.retainedTurns}
+
+ )} + {!swarmMode && ( +
+
Patient Zero
+
+ {snapshot.world.agents.find( + ({ id }) => id === snapshot.scenario.patientZeroAgentId, + )?.name ?? 'None'} +
+
+ )} + {!swarmMode && ( +
+
Public messages
+
+ {snapshot.experiment.metrics.aggregate.publicMessagesSent} +
+
+ )} + {!swarmMode && ( +
+
Direct messages
+
+ {snapshot.experiment.metrics.aggregate.directMessagesSent} +
+
+ )}
Provider attempts
@@ -967,21 +995,25 @@ export function WorldLab() {
)} -
-
Tokens
-
- {snapshot.experiment.metrics.aggregate.tokens.totalTokens ?? - 'Unknown'} -
-
+ {!swarmMode && ( +
+
Tokens
+
+ {snapshot.experiment.metrics.aggregate.tokens.totalTokens ?? + 'Unknown'} +
+
+ )} - + {!swarmMode && }

- {snapshot.providerMode === 'openrouter' - ? `${new Set(snapshot.resolvedModels.map(({ modelId }) => modelId ?? 'unassigned')).size} active model assignment${snapshot.resolvedModels.length === 1 ? '' : 's'}` - : 'Deterministic test model'} + {swarmMode + ? `Zero: ${zeroModel?.modelId ?? 'model required'} · Jev: ${snapshot.swarmProviderStatus?.reflexModel ?? 'deterministic reflex'}` + : snapshot.providerMode === 'openrouter' + ? `${new Set(snapshot.resolvedModels.map(({ modelId }) => modelId ?? 'unassigned')).size} active model assignment${snapshot.resolvedModels.length === 1 ? '' : 's'}` + : 'Deterministic test model'}