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
9 changes: 9 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ Schema-v14 exports identify shared batch dispatches and preserve aggregate
billing without per-worker allocations. No native production provider, Laya,
training, planner-cadence changes, or player features are added. See ADR 0035.

The focused expansion-fulfillment clarification makes the existing completion
rule explicit in Jev's context and the assigned-target infection candidate:
the target must be infected and controlled by its worker; arrival and waiting
do not fulfill expansion. Legal choices, Zero's risk assignments, probability
validation, and replanning policy remain unchanged. The next owner-run live
comparison should stratify open-target infection/wait rates by risk tolerance
and also report captures and territory growth. One 25-tick run provides
observational evidence, not proof of causation.

## Agent Zero planner

Agent Zero is the sole generative planner. It makes an OpenRouter planning call
Expand Down
249 changes: 249 additions & 0 deletions apps/game-api/src/reflex-execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ import {
import {
applyWorldAction,
createDevelopmentWorld,
enumerateLegalWorldActions,
toWorldState,
} from '@hexzero/world-engine';
import { isSwarmDirectiveComplete } from './swarm-directives';
import { AttemptAccounting } from './attempt-accounting';
import { SimulationService } from './simulation-service';
import {
chooseReflexWorldAction,
chooseReflexWorldActions,
compileReflexObservation,
} from './reflex-execution';

Expand Down Expand Up @@ -101,6 +104,252 @@ describe('zero-swarm reflex execution seam', () => {
);
});

it('preserves engine-legal candidates and directive priority/risk across missions', () => {
const { state, directive } = fixture();
const missions = [
'expand',
'hold',
'relocate',
'reinforce',
'evade',
] as const;
const observations = missions.map(
(mission) =>
compileReflexObservation(state, {
...directive,
mission,
targetCell: mission === 'hold' ? null : directive.targetCell,
priority: 'low',
riskTolerance: 'high',
}).observation,
);
const expand = observations[0]!;
const expectedIds = enumerateLegalWorldActions(
state,
directive.agentId,
).map((_, index) => `action_${index}`);
expect(expand.candidates.map(({ id }) => id)).toEqual(expectedIds);
expect(expand.directive).toMatchObject({
mission: 'expand',
priority: 'low',
riskTolerance: 'high',
});
for (const observation of observations.slice(1)) {
expect(observation.candidates.map(({ id }) => id)).toEqual(expectedIds);
expect(observation.directive).toMatchObject({
priority: 'low',
riskTolerance: 'high',
});
}
});

it('explains infection only when standing on the assigned expand target', () => {
const { state, agent, directive, targetCell } = fixture();
const openCurrentState = {
...state,
hexes: new Map(state.hexes).set(agent.currentCell, {
state: 'open' as const,
controllerAgentId: null,
}),
};
const atTarget = compileReflexObservation(openCurrentState, {
...directive,
mission: 'expand',
targetCell: agent.currentCell,
});
const infectCandidate = atTarget.observation.candidates.find(
({ id }) => atTarget.actions.get(id)?.type === 'infect',
);
expect(infectCandidate?.description).toContain(
'infecting it establishes this worker’s control and fulfills the directive',
);
expect(infectCandidate?.description).toContain(
'Arriving here or waiting does not fulfill it.',
);

const offTarget = compileReflexObservation(openCurrentState, {
...directive,
mission: 'expand',
targetCell,
});
const genericInfect = offTarget.observation.candidates.find(
({ id }) => offTarget.actions.get(id)?.type === 'infect',
);
expect(genericInfect?.description).toBe(
'Infect the current open cell and establish local territory.',
);
const sameCellOtherMission = compileReflexObservation(openCurrentState, {
...directive,
mission: 'relocate',
targetCell: agent.currentCell,
});
const otherMissionInfect = sameCellOtherMission.observation.candidates.find(
({ id }) => sameCellOtherMission.actions.get(id)?.type === 'infect',
);
expect(otherMissionInfect?.description).toBe(
'Infect the current open cell and establish local territory.',
);
});

it('accepts a legal wait for an open at-target expand directive with low risk and pressure', async () => {
const { state, agent, directive } = fixture();
const openCurrentState = {
...state,
hexes: new Map(state.hexes).set(agent.currentCell, {
state: 'open' as const,
controllerAgentId: null,
}),
};
const lowDirective = {
...directive,
mission: 'expand' as const,
targetCell: agent.currentCell,
priority: 'low' as const,
riskTolerance: 'low' as const,
};
const compiled = compileReflexObservation(openCurrentState, lowDirective);
const engineChoices = enumerateLegalWorldActions(
openCurrentState,
directive.agentId,
);
const legalIds = engineChoices.map((_, index) => `action_${index}`);
expect(compiled.observation.currentSituation.nearbyPressure).toBe('low');
expect(compiled.observation.candidates.map(({ id }) => id)).toEqual(
legalIds,
);
expect([...compiled.actions.values()]).toEqual(engineChoices);
expect(compiled.observation.currentSituation.directiveProgress).toBe(
'at-target',
);
expect(
compiled.observation.candidates.find(
({ id }) => compiled.actions.get(id)?.type === 'infect',
)?.description,
).toContain('fulfills the directive');
const waitId = compiled.observation.candidates.find(
({ id }) => compiled.actions.get(id)?.type === 'wait',
)!.id;
const probabilities = Object.fromEntries(
legalIds.map((id) => [id, id === waitId ? 1 : 0]),
);
const fetchImplementation = vi.fn<typeof fetch>().mockResolvedValue(
new Response(
JSON.stringify({
model: 'jev-1.13.0',
answers: {
choose_action: {
type: 'choice',
choice: waitId,
probabilities,
confidence: 1,
},
request_replan: { type: 'noul', noul: 0 },
},
usage: { input_tokens: 12, output_tokens: 0 },
}),
),
);
const selected = await chooseReflexWorldAction(
openCurrentState,
lowDirective,
new TypeSafeJevReflexProvider({
apiKey: 'test-only',
fetchImplementation,
}),
);
expect(selected.cognitionSource).toBe('jev-reflex');
expect(selected.action).toEqual({ type: 'wait' });
const request = JSON.parse(
String(fetchImplementation.mock.calls[0]?.[1]?.body),
);
expect(request.state.directive).toMatchObject({
mission: 'expand',
priority: 'low',
riskTolerance: 'low',
fulfillmentCondition: expect.stringContaining(
'Arriving at an open target or waiting there does not fulfill it.',
),
});

const [batchedSelection] = await chooseReflexWorldActions(
[{ compiled, intendedTurnNumber: 1 }],
new ScriptedReflexProvider([{ chosenCandidateId: waitId }]),
);
expect(batchedSelection?.cognitionSource).toBe('jev-reflex');
expect(batchedSelection?.action).toEqual({ type: 'wait' });

const scripted = new ScriptedReflexProvider([
{ chosenCandidateId: waitId },
]);
const nativeProvider: ReflexProvider = {
...scripted,
decide: scripted.decide.bind(scripted),
decideBatch: async (observations) =>
Promise.all(
observations.map(async (observation) => ({
agentId: observation.agentId,
status: 'completed' as const,
decision: await scripted.decide(observation),
})),
),
};
const [nativeSelection] = await chooseReflexWorldActions(
[{ compiled, intendedTurnNumber: 1 }],
nativeProvider,
);
expect(nativeSelection?.decision?.chosenCandidateId).toBe(waitId);
expect(nativeSelection?.action).toEqual({ type: 'wait' });
});

it('completes expand only after infection gives this worker control', () => {
const { state, agent, directive } = fixture();
const targetCell = [...state.hexes.entries()].find(
([cell, hex]) =>
hex.state === 'open' &&
cell !== agent.currentCell &&
gridDisk(agent.currentCell, 1).includes(cell),
)![0];
const expandDirective = {
...directive,
mission: 'expand' as const,
targetCell,
};
const moved = applyWorldAction(state, agent.id, {
type: 'move',
targetCell,
});
expect(moved.result.accepted).toBe(true);
expect(isSwarmDirectiveComplete(moved.state, expandDirective)).toBe(false);
expect(
compileReflexObservation(moved.state, expandDirective).observation
.currentSituation.directiveProgress,
).toBe('at-target');
const waited = applyWorldAction(moved.state, agent.id, { type: 'wait' });
expect(waited.result.accepted).toBe(true);
expect(isSwarmDirectiveComplete(waited.state, expandDirective)).toBe(false);

const infected = applyWorldAction(waited.state, agent.id, {
type: 'infect',
});
expect(infected.result.accepted).toBe(true);
expect(isSwarmDirectiveComplete(infected.state, expandDirective)).toBe(
true,
);
const otherAgentId = [...state.agents.keys()].find(
(candidateId) => candidateId !== agent.id,
)!;
const wrongControllerHexes = new Map(infected.state.hexes).set(targetCell, {
state: 'infected' as const,
controllerAgentId: otherAgentId,
});
expect(
isSwarmDirectiveComplete(
{ ...infected.state, hexes: wrongControllerHexes },
expandDirective,
),
).toBe(false);
});

it('retains only the four most recent authoritative capture alerts', () => {
const { state, directive } = fixture();
const capturedAgentId = [...state.agents.keys()][0]!;
Expand Down
5 changes: 4 additions & 1 deletion apps/game-api/src/reflex-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ function actionDescription(
if (action.type === 'wait')
return 'Remain on the current cell for this tick.';
if (action.type === 'infect')
return 'Infect the current open cell and establish local territory.';
return directive.mission === 'expand' &&
directive.targetCell === agent.currentCell
? 'Infect the current open cell and establish local territory. This is the assigned expand target; infecting it establishes this worker’s control and fulfills the directive. Arriving here or waiting does not fulfill it.'
: 'Infect the current open cell and establish local territory.';
if (action.type === 'capture')
return 'Capture the abandoned infected current cell from another controller.';
const status = cellStatus(state, action.targetCell, directive.agentId);
Expand Down
9 changes: 7 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,13 @@ 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.
An `expand` directive completes only after its target is worker-controlled;
arriving on an open target is not completion. A `relocate` directive completes
An `expand` directive completes only after its target is infected and controlled
by that worker; arriving on an open target or waiting there is not completion.
Jev's expand context states this fulfillment condition and identifies
`at-target` as positional. The infection candidate at the assigned open target
also explains that infection establishes worker control and fulfills expansion.
All engine-legal choices remain available, and a valid wait remains acceptable.
A `relocate` directive completes
when its worker reaches the target. A `reinforce` directive completes on arrival
at its target, which must be infected or adjacent to infection when issued. An
`evade` directive completes on arrival or when a worker that previously faced
Expand Down
6 changes: 6 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ observation data. Raw planner text is not forwarded. Workers do not receive
other workers' choices. Cancellation discards every result from the
uncommitted tick.

Expand fulfillment wording is fixed server-authored Jev context. The
assigned-target infection cue is compiled from the validated directive and
engine-authoritative worker position. These cues explain the existing objective;
provider choices still pass the same probability validation and engine action
validation before world mutation.

When strategic replanning is required, the OpenRouter planner receives one bounded strategic observation (a coarse `worldSummary` plus per-worker semantic `workerOptions`, with no raw H3 cell IDs or agent IDs) and is instructed to return exactly one plain JSON object naming opaque worker and option choices plus a Zero-action selection. TypeSafe Jev receives a compact semantic observation with opaque legal candidate IDs per worker and returns a probability distribution over candidates; a second question in the same request returns an optional bounded replan probability. The runtime performs bounded extraction and conservative repair for wrappers such as code fences, surrounding prose, and trailing commas, then rejects missing text, unusable JSON, unknown fields, or output truncation before the deterministic world engine validates all resolved components independently.

The request uses the selected model, messages, `max_tokens`, `stream: false`, and at most one normalized reasoning object selected from sanitized model metadata. Provider default omits the object. Off is offered only for non-mandatory reasoning and sends `{ enabled: false, exclude: true }`; an advertised effort sends `{ enabled: true, effort, exclude: true }`. It deliberately sends no tools, `tool_choice`, `response_format`, `provider.require_parameters`, standalone `reasoning_effort`, or model-specific parameter. Model IDs are never inspected or special-cased. Transport/provider failures, unavailable-model/profile failures, text/JSON contract failures, and later simulation-rule rejection remain distinct safe outcomes. The adapter never silently substitutes a model or scripted behavior.
Expand Down
7 changes: 7 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ rejection.

### Game API (`apps/game-api`)

Expansion clarification tests check Jev's fulfillment context across risk
tolerances, the assigned-open-target infection description, and unchanged legal
candidate IDs/actions. Engine-backed assertions distinguish arrival and waiting
from successful worker-controlled infection. Valid wait decisions remain
selectable through the individual and batch seams. Other missions and off-target
infection retain their existing wording and completion behavior.

Batch reflex tests use offline controlled providers and deferred promises to
assert the individual-call concurrency cap, stable native-batch attribution,
reordered/duplicate/extra/missing/malformed result policy, failure isolation,
Expand Down
38 changes: 38 additions & 0 deletions packages/agent-runtime/src/typesafe-jev-reflex-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,44 @@ describe('TypeSafeJevReflexProvider', () => {
);
});

it('adds bounded fulfillment context only for expand directives', () => {
const fulfillmentCondition =
'An expand directive is fulfilled when its target is infected and controlled by this worker. Arriving at an open target or waiting there does not fulfill it. The at-target progress label describes position, not fulfillment.';
for (const riskTolerance of ['low', 'medium', 'high'] as const) {
for (const mission of [
'hold',
'relocate',
'reinforce',
'evade',
] as const) {
const request = buildTypeSafeJevRequest({
...observation,
directive: {
...observation.directive,
mission,
riskTolerance,
},
});
expect(request.state.directive).toEqual({
mission,
priority: observation.directive.priority,
riskTolerance,
});
}

const expandRequest = buildTypeSafeJevRequest({
...observation,
directive: { ...observation.directive, riskTolerance },
});
expect(expandRequest.state.directive).toEqual({
mission: 'expand',
priority: observation.directive.priority,
riskTolerance,
fulfillmentCondition,
});
}
});

it('rejects malformed responses and selections outside its candidates', async () => {
const malformed = new TypeSafeJevReflexProvider({
apiKey: 'test-key',
Expand Down
6 changes: 6 additions & 0 deletions packages/agent-runtime/src/typesafe-jev-reflex-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,12 @@ export function buildTypeSafeJevRequest(observationInput: ReflexObservation) {
mission: observation.directive.mission,
priority: observation.directive.priority,
riskTolerance: observation.directive.riskTolerance,
...(observation.directive.mission === 'expand'
? {
fulfillmentCondition:
'An expand directive is fulfilled when its target is infected and controlled by this worker. Arriving at an open target or waiting there does not fulfill it. The at-target progress label describes position, not fulfillment.',
}
: {}),
},
currentSituation: observation.currentSituation,
...(recentCaptures?.length
Expand Down
Loading