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
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. The current milestone is the first visible LLM invasion: six persistent, named model-backed agents move and infect 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, 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. 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, 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. 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.

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.

Social messaging, relationship memory, player mechanics, persistence, autonomous scheduling, and provider configuration UI remain deferred to later roadmap milestones.
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 @@ -35,6 +35,8 @@ Second focused slice: server-owned safe experiment telemetry, actual OpenRouter

Range-limited messages, agent inboxes, relationship memories, communication visualization, cooperation, refusal, deception, and betrayal emerging through prompts rather than a large formal rules system.

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.

## PR 5 — Goals and memory

Persistent short- and long-term objectives, compact memories, plan revision, summaries, and longer simulation runs.
Expand Down
63 changes: 62 additions & 1 deletion apps/game-api/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,67 @@ describe('game API simulation boundary', () => {
expect(rejected.turn.outcome).toBe('rejected');
});

it('returns typed accepted and rejected message responses', async () => {
const bootstrap = createApp({
provider: new ScriptedAgentProvider([
{ requestedAction: { type: 'wait' }, summary: 'placeholder' },
]),
});
const snapshot = simulationSnapshotSchema.parse(
await (await bootstrap.request('/api/simulation')).json(),
);
const [sender, recipient] = snapshot.world.agents;
const acceptedApp = createApp({
provider: new ScriptedAgentProvider([
{
requestedAction: {
type: 'message',
recipientId: recipient!.id,
message: 'Nearby API message.',
},
summary: 'Send.',
},
]),
});
const accepted = singleTurnResponseSchema.parse(
await (
await acceptedApp.request('/api/simulation/turn', { method: 'POST' })
).json(),
);
expect(accepted.turn).toMatchObject({
outcome: 'accepted',
event: {
type: 'agent-messaged',
agentId: sender!.id,
recipientId: recipient!.id,
message: 'Nearby API message.',
},
});

const rejectedApp = createApp({
provider: new ScriptedAgentProvider([
{
requestedAction: {
type: 'message',
recipientId: sender!.id,
message: 'Self message.',
},
summary: 'Try.',
},
]),
});
const rejected = singleTurnResponseSchema.parse(
await (
await rejectedApp.request('/api/simulation/turn', { method: 'POST' })
).json(),
);
expect(rejected.turn).toMatchObject({
outcome: 'rejected',
validation: { reason: 'self-message' },
});
expect(rejected.snapshot.world.events).toEqual([]);
});

it('returns provider failures and missing configuration safely', async () => {
const failureProvider: AgentProvider = {
mode: 'scripted-test',
Expand Down Expand Up @@ -406,7 +467,7 @@ describe('game API simulation boundary', () => {
agents: { mode: 'all' },
turns: { mode: 'entire-retained' },
outcomes: ['accepted', 'rejected', 'provider-error'],
actions: ['move', 'infect', 'wait'],
actions: ['move', 'infect', 'message', 'wait'],
level: 'minimal',
};
const previewResponse = await app.request(
Expand Down
Loading
Loading