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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ These instructions apply throughout the repository.
- Treat “Agentborne” as a working title. Avoid heavy branding, logo, lore, or name-specific design investment.
- Keep the World Lab as a production developer/admin surface, not a disposable demo.
- The only initial hex states are `open` and `infected`.
- The only agent map actions are adjacent move, infect current cell, range-limited message, and wait.
- The only agent map actions are adjacent move, infect current cell, capture an abandoned infected current cell from another controller, range-limited message, and wait.
- Infection and agent position are independent. Movement does not remove infection.
- Full agent visibility is deliberate. Do not add fog of war, detection, scanners, or last-known positions.
- Do not add resources, inventory, structures, combat, terrain bonuses, crafting, formal factions, accounts, GPS validation, player progression, or mobile packaging before the roadmap calls for them.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Agentborne

Agentborne is the working title for an agent-first GPS containment game. Six persistent, named model-backed agents move, infect, wait, and exchange short nearby messages in a real Toledo H3 world while the World Lab exposes every safe decision record.
Agentborne is the working title for an agent-first GPS containment game. Six persistent, named model-backed agents move, infect, capture contested territory, wait, and exchange short nearby messages in a real Toledo H3 world while the World Lab exposes every safe decision record.

## Workspace

Expand Down Expand Up @@ -28,7 +28,7 @@ Open the World Lab at <http://localhost:3000>. The Game API binds to <http://127

Each Single turn or completed playback interval makes one third-party OpenRouter request and may incur cost. Start is deliberately disabled when the server has no key. This development API has no authentication or cost controls and is not suitable for an unauthenticated public deployment.

State is held only in the Game API process. The API captures one active safe experiment with up to 5,000 complete turn records while the browser snapshot remains capped at 120. A message is an exclusive turn action to one agent at H3 distance three or less; content is server-trimmed and bounded to 280 characters. Observations contain at most six recent accepted inbound or outbound communications. Minimal, Standard, Full safe, and Custom exports filter that same capture without changing prompts or inference usage. OpenRouter's returned `usage.cost` is shown as known cost; missing cost remains explicitly unknown. Restarting the API loses the experiment. Reset warns before discarding telemetry and communication history, creates a new experiment identity, reconstructs the same 61-cell Toledo world, and preserves active personalities.
State is held only in the Game API process. The API captures one active safe experiment with up to 5,000 complete turn records while the browser snapshot remains capped at 120. Every infected hex has exactly one individual controlling agent. Infect claims an open current hex; capture transfers an infected current hex from another controller only when that controller is not physically present, without movement or combat calculations. Other occupants do not defend it. Observations explicitly report current capture eligibility, an authoritative six-agent territory scoreboard, at most six relevant control changes, and at most six recent accepted inbound or outbound communications. Message remains an exclusive turn action to one agent at H3 distance three or less; content is server-trimmed and bounded to 280 characters. Mingle's social coalition-builder default deliberately favors interaction and negotiation, while the other five defaults are unchanged. Minimal, Standard, Full safe, and Custom schema-v3 exports filter that same capture without changing prompts or inference usage. OpenRouter's returned `usage.cost` is shown as known cost; missing cost remains explicitly unknown. Restarting the API loses the experiment. Reset warns before discarding telemetry and histories, creates a new experiment identity, reconstructs the same 61-cell open/uncontrolled Toledo world, and preserves active personalities.

Export previews report exact serialized UTF-8 bytes and a model-agnostic `ceil(bytes / 4)` approximate AI-input-token estimate. Compact JSON is the default for AI sharing; Pretty JSON remains available for human review, and preview estimates reflect the selected serialization. This is a sharing-budget aid, not tokenizer output or a billing guarantee. Exports exclude fixed prompts, raw provider payloads, credentials, authorization headers, private reasoning, and unbounded diagnostics.

Expand All @@ -46,7 +46,7 @@ The command uses Node 24's built-in environment parser to read only `OPENROUTER_

The map centers on Toledo, Ohio (`41.6528, -83.5379`) at H3 resolution 9 and renders a radius-four disk. MapLibre uses the public OpenStreetMap raster tile endpoint with attribution for light local development only. A public deployment must choose a compliant production tile source.

Relationships, group chat, persistent memory, player mechanics, persistence, autonomous scheduling, and provider configuration UI remain deferred to later roadmap milestones.
Formal alliances, combat systems, relationships, group chat, persistent memory, player mechanics, persistence, autonomous scheduling, and provider configuration UI remain deferred to later roadmap milestones.

When every development cell is infected, World Lab automatically pauses playback and disables Start to avoid accidental provider calls. Reset and export remain available, and Single turn remains an explicitly manual diagnostic action.

Expand Down
2 changes: 2 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Range-limited messages, agent inboxes, relationship memories, communication visu

First focused slice: messaging is one exclusive turn action to a single existing agent within inclusive H3 distance three. Accepted messages become bounded world events and supply at most six recent inbound/outbound communications to later observations. World Lab and safe experiment exports expose this event-derived context. Generic inboxes, persistent or semantic memory, relationships, group chat, automatic replies, and communication visualization remain deferred.

Second focused slice: every infected hex has one individual controller. Infect claims an open current hex and capture deterministically transfers an infected current hex from another agent only after that controller leaves it; controller presence defends against immediate capture and control ping-pong. Observations expose explicit capture eligibility, an authoritative six-agent territory scoreboard, and at most six event-derived relevant gains/losses. Mingle alone receives a social coalition-builder default so messaging remains deliberately exercised without becoming automatic. Telemetry and schema-v3 exports are victim-aware. Combat calculations, formal alliances, resources, territory bonuses, and post-infection autonomous conflict playback remain deferred.

## PR 5 — Goals and memory

Persistent short- and long-term objectives, compact memories, plan revision, summaries, and longer simulation runs.
Expand Down
33 changes: 32 additions & 1 deletion apps/game-api/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
h3CellSchema,
experimentExportPreviewSchema,
experimentExportResponseSchema,
healthResponseSchema,
resetSimulationResponseSchema,
restoreDefaultPersonalitiesResponseSchema,
simulationSnapshotSchema,
Expand All @@ -25,11 +26,19 @@ describe('game API simulation boundary', () => {
{ requestedAction: { type: 'wait' }, summary: 'Wait.' },
]),
});
expect((await app.request('/health')).status).toBe(200);
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(6);
expect(
payload.world.hexes.every(
(hex) => hex.state === 'open' && hex.controllerAgentId === null,
),
).toBe(true);
expect(payload.experiment.currentTerritory).toHaveLength(6);
});

it('returns accepted and rejected single-turn records with valid response shapes', async () => {
Expand All @@ -44,6 +53,22 @@ describe('game API simulation boundary', () => {
).json(),
);
expect(accepted.turn.outcome).toBe('accepted');
if (accepted.turn.outcome !== 'accepted')
throw new Error('Expected accepted infection fixture.');
expect(accepted.turn).toMatchObject({
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([
Expand Down Expand Up @@ -367,6 +392,12 @@ describe('game API simulation boundary', () => {
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. Move toward visible agents, initiate and continue conversations, negotiate before taking their territory, and coordinate when useful. Infect open cells opportunistically, but value interaction over silent pursuit.',
);
});

it('returns typed conflicts for an overlapping turn and reset', async () => {
Expand Down
13 changes: 6 additions & 7 deletions apps/game-api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
experimentExportRequestSchema,
experimentExportPreviewSchema,
experimentExportResponseSchema,
healthResponseSchema,
PERSONALITY_MAX_LENGTH,
resetSimulationResponseSchema,
restoreDefaultPersonalitiesResponseSchema,
Expand All @@ -28,12 +29,7 @@ import {
} from './simulation-service';
import { ExperimentExportValidationError } from './experiment-export';

export const healthResponseSchema = worldSnapshotSchema
.pick({ generatedAt: true })
.transform(({ generatedAt }) => ({
status: 'ok' as const,
checkedAt: generatedAt,
}));
export { healthResponseSchema };

export interface AppOptions {
service?: SimulationService;
Expand Down Expand Up @@ -72,7 +68,10 @@ export function createApp(options: AppOptions = {}) {

app.get('/health', (context) =>
context.json(
healthResponseSchema.parse({ generatedAt: new Date().toISOString() }),
healthResponseSchema.parse({
status: 'ok',
checkedAt: new Date().toISOString(),
}),
),
);

Expand Down
Loading
Loading