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
17 changes: 17 additions & 0 deletions apps/game-api/src/reflex-execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ describe('zero-swarm reflex execution seam', () => {
expect(applied.state.agents.get(agent.id)?.currentCell).toBe(targetCell);
});

it('reports at-target after a worker reaches its directive target', () => {
const { state, agent, directive, targetCell } = fixture();
const moved = applyWorldAction(state, agent.id, {
type: 'move',
targetCell,
});
expect(moved.result.accepted).toBe(true);

const compiled = compileReflexObservation(moved.state, directive, {
previousCell: agent.currentCell,
});

expect(compiled.observation.currentSituation.directiveProgress).toBe(
'at-target',
);
});

it('retains only the four most recent authoritative capture alerts', () => {
const { state, directive } = fixture();
const capturedAgentId = [...state.agents.keys()][0]!;
Expand Down
14 changes: 9 additions & 5 deletions apps/game-api/src/reflex-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,18 @@ export function compileReflexObservation(
(distance(action.targetCell, directive.targetCell!) ?? Infinity) <
currentDistance,
);
const directiveProgress =
let directiveProgress: 'advancing' | 'at-target' | 'stalled' | 'blocked' =
'stalled';
if (directive.targetCell && currentDistance === 0)
directiveProgress = 'at-target';
else if (
previousDistance !== null &&
currentDistance !== null &&
currentDistance < previousDistance
? ('advancing' as const)
: directive.targetCell && currentDistance !== 0 && !hasForwardMove
? ('blocked' as const)
: ('stalled' as const);
)
directiveProgress = 'advancing';
else if (directive.targetCell && currentDistance !== 0 && !hasForwardMove)
directiveProgress = 'blocked';
const nearbyCleaned = (history.recentCleanedCells ?? [])
.slice(-6)
.map((cell) => distance(current, h3CellSchema.parse(cell)))
Expand Down
148 changes: 138 additions & 10 deletions apps/game-api/src/simulation-service.swarm.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { gridDistance } from 'h3-js';
import { describe, expect, it } from 'vitest';
import {
BrowserTestAgentProvider,
Expand Down Expand Up @@ -43,6 +44,7 @@ class InspectingPlanner implements SwarmPlanner {
constructor(
private readonly failure: boolean | number = false,
private readonly directiveLifetime = 5,
private readonly targetDistance = 0,
) {}
async plan(
observation: ZeroStrategicObservation,
Expand Down Expand Up @@ -73,16 +75,31 @@ class InspectingPlanner implements SwarmPlanner {
)!.id,
directives: observation.agents
.filter(({ agentId }) => agentId !== observation.zeroAgentId)
.map((agent, index) => ({
id: `directive-${observation.tickNumber}-${index}`,
agentId: agent.agentId,
mission: 'hold',
targetCell: agent.position,
priority: 'normal',
riskTolerance: 'low',
issuedAtTick: observation.tickNumber,
expiresAtTick: observation.tickNumber + this.directiveLifetime,
})),
.map((agent, index) => {
const targetCell =
this.targetDistance === 0
? agent.position
: observation.strategicTargetCells.find(
(cell) =>
cell !== agent.position &&
gridDistance(cell, agent.position) ===
this.targetDistance,
);
if (!targetCell)
throw new Error(
`No strategic target is ${this.targetDistance} cells from ${agent.agentId}.`,
);
return {
id: `directive-${observation.tickNumber}-${index}`,
agentId: agent.agentId,
mission: 'hold',
targetCell,
priority: 'normal',
riskTolerance: 'low',
issuedAtTick: observation.tickNumber,
expiresAtTick: observation.tickNumber + this.directiveLifetime,
};
}),
},
metadata: { provider: 'scripted-test', model: 'test/zero', latencyMs: 0 },
} satisfies Awaited<ReturnType<SwarmPlanner['plan']>>;
Expand Down Expand Up @@ -521,6 +538,117 @@ describe('zero-swarm SimulationService tick', () => {
expect(planner.observations).toHaveLength(2);
});

it('reports workers who wait on their targets as at-target to Zero', async () => {
const planner = new InspectingPlanner(false, 1);
const waitingReflex: ReflexProvider = {
mode: 'scripted-reflex-test',
model: 'test-reflex',
configured: true,
async decide(observation, options) {
const choice = observation.candidates.find(({ description }) =>
description.startsWith('Remain on the current cell'),
)!;
const decision = reflexDecisionSchema.parse({
chosenCandidateId: choice.id,
confidence: 1,
probabilities: Object.fromEntries(
observation.candidates.map(({ id }) => [
id,
id === choice.id ? 1 : 0,
]),
),
model: 'test-reflex',
latencyMs: 0,
inputTokens: 0,
outputTokens: 0,
directiveId: observation.directive.id,
cognitionSource: 'jev-reflex',
});
options?.beginAttempt?.('initial')?.({
outcome: 'completed',
reflexDecision: decision,
});
return decision;
},
};
const simulation = setup(planner, waitingReflex);

await simulation.executeNextTick();
await simulation.executeNextTick();
await simulation.executeNextTick();

expect(
simulation
.getSnapshot()
.swarmTicks?.[1]?.workers.every(
({ action, actionResult }) =>
action?.type === 'wait' && actionResult?.accepted === true,
),
).toBe(true);
const workerObservations = planner.observations[1]?.agents.filter(
({ agentId }) => agentId !== planner.observations[1]?.zeroAgentId,
);
expect(workerObservations).toHaveLength(7);
expect(workerObservations?.map(({ workerStatus }) => workerStatus)).toEqual(
Array.from({ length: 7 }, () => 'at-target'),
);
});

it('reports accepted moves toward a target as advancing to Zero', async () => {
const planner = new InspectingPlanner(false, 5, 2);
const advancingReflex: ReflexProvider = {
mode: 'scripted-reflex-test',
model: 'test-reflex',
configured: true,
async decide(observation, options) {
const choice = observation.candidates.find(({ description }) =>
description.includes('This advances toward the assigned target.'),
)!;
const decision = reflexDecisionSchema.parse({
chosenCandidateId: choice.id,
confidence: 1,
probabilities: Object.fromEntries(
observation.candidates.map(({ id }) => [
id,
id === choice.id ? 1 : 0,
]),
),
replanProbability: 0.8,
model: 'test-reflex',
latencyMs: 0,
inputTokens: 0,
outputTokens: 0,
directiveId: observation.directive.id,
cognitionSource: 'jev-reflex',
});
options?.beginAttempt?.('initial')?.({
outcome: 'completed',
reflexDecision: decision,
});
return decision;
},
};
const simulation = setup(planner, advancingReflex);

await simulation.executeNextTick();
await simulation.executeNextTick();

expect(
simulation
.getSnapshot()
.swarmTicks?.[0]?.workers.every(
({ action, actionResult }) =>
action?.type === 'move' && actionResult?.accepted === true,
),
).toBe(true);
const workerObservations = planner.observations[1]?.agents.filter(
({ agentId }) => agentId !== planner.observations[1]?.zeroAgentId,
);
expect(workerObservations?.map(({ workerStatus }) => workerStatus)).toEqual(
Array.from({ length: 7 }, () => 'advancing'),
);
});

it('releases reuse-tick reservations when a worker request is cancelled', async () => {
let calls = 0;
const reflex: ReflexProvider = {
Expand Down
31 changes: 22 additions & 9 deletions apps/game-api/src/simulation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1916,7 +1916,7 @@ export class SimulationService {
]),
);
this.#lastSwarmPositions = new Map(
[...state.agents.values()].map(({ id, currentCell }) => [
[...candidate.agents.values()].map(({ id, currentCell }) => [
id,
currentCell,
]),
Expand Down Expand Up @@ -2768,14 +2768,27 @@ export class SimulationService {
this.#lastValidSwarmPlan?.directives.find(
({ agentId }) => agentId === agent.id,
) ?? null;
const workerStatus =
priorWorker?.source === 'deterministic-fallback'
? ('blocked' as const)
: priorWorker?.actionResult?.accepted
? ('advancing' as const)
: priorWorker
? ('stalled' as const)
: ('unknown' as const);
const previousCell = this.#lastSwarmPositions.get(agent.id);
let workerStatus:
'advancing' | 'at-target' | 'stalled' | 'blocked' | 'unknown' =
'unknown';
if (priorWorker) {
if (directive?.targetCell === agent.currentCell)
workerStatus = 'at-target';
else if (
directive?.targetCell &&
previousCell &&
gridRingDistance(agent.currentCell, directive.targetCell) <
gridRingDistance(previousCell, directive.targetCell)
)
workerStatus = 'advancing';
else if (
priorWorker.source === 'deterministic-fallback' ||
priorWorker.actionResult?.accepted === false
)
workerStatus = 'blocked';
else workerStatus = 'stalled';
}
return {
agentId: agent.id,
position: agent.currentCell,
Expand Down
5 changes: 5 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ The worker observation includes bounded, current capture alerts. The TypeSafe
request projects them as structured capture pressure without cell IDs or tick
numbers. The unused prose `relevantRecentFacts` field is removed; current
situation and legal candidate descriptions supply the relevant local facts.
Directive progress compares each worker's current cell with its position before
the prior tick's physical action. Reaching the directive target has its own
`at-target` observation status. Zero's worker status uses the same position
comparison, rather than treating any accepted world action as progress. The
existing deterministic replanning trigger for repeated waits is unchanged.
Zero planning uses the same provider-reported OpenRouter usage normalization as
legacy turns, including actual `usage.cost` when returned, and preserves that
metadata when a returned plan is rejected or a bounded non-success response
Expand Down
3 changes: 3 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ The read-only `pnpm diagnose:swarm` command summarizes a running local Game API
snapshot without provider calls, keys, prompts, or raw responses. World Lab's
swarm activity summary shows pressure state, physical action counts, and
fallbacks for immediate diagnosis of apparently stationary ticks.
Focused offline progress tests cover a worker advancing after a move, reaching
a directive target, waiting at that target, and the corresponding status seen
by Zero on the next planning tick.

The pre-live provider audit tests the Jev request's bounded capture-pressure
projection, omission of hidden hunter state and empty capture noise, and the
Expand Down
9 changes: 7 additions & 2 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,12 @@ export const reflexObservationSchema = z
currentSituation: z
.object({
cellStatus: z.enum(['open', 'friendly-infected', 'other-infected']),
directiveProgress: z.enum(['advancing', 'stalled', 'blocked']),
directiveProgress: z.enum([
'advancing',
'at-target',
'stalled',
'blocked',
]),
nearbyPressure: z.enum(['low', 'rising', 'high']),
recentTerritoryTrend: z.enum(['growing', 'stable', 'shrinking']),
recentActionOutcome: z.enum(['success', 'rejected', 'unknown']),
Expand Down Expand Up @@ -704,7 +709,7 @@ export const zeroStrategicObservationSchema = z
controlledCellCount: z.number().int().nonnegative(),
territoryDelta: z.number().int(),
workerStatus: z
.enum(['advancing', 'stalled', 'blocked', 'unknown'])
.enum(['advancing', 'at-target', 'stalled', 'blocked', 'unknown'])
.optional(),
directive: swarmDirectiveSchema.nullable().optional(),
})
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/reflex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('zero-swarm reflex contracts', () => {
directive,
currentSituation: {
cellStatus: 'open',
directiveProgress: 'advancing',
directiveProgress: 'at-target',
nearbyPressure: 'low',
recentTerritoryTrend: 'growing',
recentActionOutcome: 'success',
Expand Down
Loading