Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion apps/game-api/src/simulation-service.swarm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
BrowserTestAgentProvider,
ReflexProviderError,
ScriptedReflexProvider,
type AgentProvider,
type PlannerOptions,
type ReflexProvider,
type SwarmPlanner,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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',
Expand Down
34 changes: 32 additions & 2 deletions apps/game-api/src/simulation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions apps/world-lab/src/app/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
159 changes: 159 additions & 0 deletions apps/world-lab/src/components/swarm-view.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<>
<SwarmStrategyPanel snapshot={value} />
<SwarmAgentInspector
snapshot={value}
agent={value.world.agents[1]!}
cellState="infected"
controlledCellCount={1}
/>
<SwarmRunPanel snapshot={value} status="paused" runTarget={10} />
</>,
);
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(<SwarmStrategyPanel snapshot={snapshot(false)} />);
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(
<SwarmAgentInspector
snapshot={value}
agent={value.world.agents[1]!}
cellState="infected"
controlledCellCount={1}
/>,
);
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();
});
});
Loading
Loading