diff --git a/.env.example b/.env.example index f8a2de6..f161a4e 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,8 @@ OPENROUTER_API_KEY=replace-with-your-openrouter-key TYPESAFE_API_KEY=replace-with-your-typesafe-key # The scripted provider is exclusively for deterministic automation. Normal -# interactive development must leave this unset so every turn uses OpenRouter. +# interactive development must leave this unset to use OpenRouter planning +# and TypeSafe Jev workers. # HEXZERO_PROVIDER=scripted # Deprecated compatibility alias (HEXZERO_PROVIDER takes precedence): # AGENTBORNE_PROVIDER=scripted diff --git a/.gitignore b/.gitignore index 6af21e6..39bbae4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ node_modules/ .pnpm-store/ .next/ -apps/world-lab/next-env.d.ts dist/ coverage/ playwright-report/ @@ -15,3 +14,6 @@ test-results/ logs/ .agentborne/ .hexzero/ +.claude/ +.codex/ +**/next-env.d.ts diff --git a/.prettierignore b/.prettierignore index 7a7f2cf..1a3c9a5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,4 @@ **/next-env.d.ts logs/ +.claude/ +.codex/ diff --git a/README.md b/README.md index de41cf2..2a63e30 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,173 @@ # Hex Zero -Hex Zero is an agent-first geographic experiment. A configurable roster of model-backed agents moves, infects, and captures territory on a real H3 map while the World Lab exposes every safe decision record. Full agent visibility is deliberate; there is no fog of war. +[![CI](https://github.com/BlackSwampAI/hexzero/actions/workflows/ci.yml/badge.svg)](https://github.com/BlackSwampAI/hexzero/actions/workflows/ci.yml) +[![Node.js 24.18.0](https://img.shields.io/badge/Node.js-24.18.0-339933?logo=nodedotjs&logoColor=white)](.nvmrc) +[![pnpm 11.21.0](https://img.shields.io/badge/pnpm-11.21.0-F69220?logo=pnpm&logoColor=white)](package.json) -`zero-swarm-v1` is the sole cognition architecture. Workers make reflex decisions each active worker tick. Agent Zero makes one OpenRouter planning call only when strategic replanning is required (for example, after roster changes, directive completions, or elevated pressure), under the versioned contract `swarm-planner-v2`; otherwise the last valid directive set is reused with no planner call. Each plan carries a strategy summary and a per-worker directive set. Worker nodes resolve their directives with TypeSafe Jev reflex cognition, choosing among enumerated `action_N` candidates with a probability distribution and a confidence value, over the deterministic H3 world engine. +Hex Zero is an agent-first geographic experiment. Agent Zero plans territory +expansion on a real H3 map, and TypeSafe Jev workers resolve those directives +into movement, infection, capture, or wait. The World Lab is the developer/admin +surface for configuring runs and inspecting safe decisions, territory, and +provider usage. Every agent is visible. -Directives carry: identifier, agent identifier, mission (`expand` | `hold` | `relocate` | `reinforce` | `evade`), a nullable target cell, priority (`low` | `normal` | `high`), risk tolerance (`low` | `medium` | `high`), issue and expiry ticks, and an optional note of at most 160 characters. When no replan is triggered, the previous plan's directives are reused without a planning call. Replans are triggered by: `initial`, `periodic-review`, `directive-complete`, `directive-expired`, `worker-request`, `worker-stalled`, `territory-loss`, `high-pressure`, `player-disinfection`, and `roster-changed`. +![World Lab Live workspace with the H3 map, agent roster, scoreboard, and swarm activity](docs/assets/world-lab-live.png) -Cognition sources are `zero-llm` (Agent Zero via OpenRouter), `jev-reflex` (TypeSafe Jev via the TypeSafe API), and `deterministic-fallback`. Routing summary: Agent Zero → OpenRouter; Workers → TypeSafe Jev API. The deterministic-worker baseline—workers that resolve directives without a model call—is retained as the ablation control that isolates what Jev's reflex calls contribute, not as a second production architecture. +## How it works -The capability-gated objective version is `durable-influence-v3`. Without simulated-player pressure, scenarios use a `durable-influence-v2`-compatible objective. +`zero-swarm-v1` is the sole cognition architecture: -## Workspace +1. **Agent Zero plans through OpenRouter** on the first tick, periodic review, + or a material change such as directive completion, expiry, or player pressure. + Other ticks reuse the current directives without a planner call. +2. **Workers resolve directives through TypeSafe Jev** using compact observations + and opaque, engine-legal action candidates. Workers share a frozen pre-action + world; their calls currently run sequentially under one tick deadline. +3. **The deterministic world engine validates and resolves actions** in seeded + order. A complete tick commits atomically; cancellation commits no world + changes. Provider failures retain safe attempt records and use explicit + deterministic fallbacks. + +Cells are `open` or `infected`, and each infected cell has one controller. +Movement leaves infection behind. Capture transfers an abandoned infected +current cell from another controller. There is no agent chat, diplomacy, +personality configuration, per-worker goal state, or prose memory. -| Path | Responsibility | -| ----------------------------- | ------------------------------------------------------------------------------------------- | -| `apps/world-lab` | Next.js developer/admin map, controls, inspector, and event log | -| `apps/game-api` | Hono HTTP boundary and in-memory simulation service | -| `packages/world-engine` | Pure world validation and consequence application | -| `packages/agent-runtime` | OpenRouter planner, TypeSafe Jev reflex provider, model catalog, and scripted testing seams | -| `packages/shared` | Runtime-validated schemas and inferred domain types | -| `packages/experiment-archive` | Durable SQLite imports and bounded research queries | +The deterministic-worker variant exists only as an ablation control for the +comparison CLIs. It is not a second production architecture. -## Local development +## Run locally -Requirements are Node.js 24.18.0 and pnpm 11.21.0. Copy the example environment file to the repository-root `.env`, replace only the placeholder key, install dependencies, and start both applications: +Use **Node.js 24.18.0** and **pnpm 11.21.0**, pinned in the repository. ```bash -cp .env.example .env -# Edit .env and set OPENROUTER_API_KEY and TYPESAFE_API_KEY. corepack enable pnpm install --frozen-lockfile +cp .env.example .env +# Set OPENROUTER_API_KEY and TYPESAFE_API_KEY in the root .env. pnpm dev ``` -For deterministic local automation, `pnpm dev:test-provider` sets -`HEXZERO_PROVIDER=scripted`. `HEXZERO_EXPERIMENT_DB` overrides the local -experiment archive path. - -Open the World Lab at . The Game API binds to ; Next.js narrowly proxies `/api/game/*` to it. `OPENROUTER_API_KEY` (Agent Zero) and `TYPESAFE_API_KEY` (Jev workers) are both required for real runs. Select a compatible model for Agent Zero in World Lab. Each assignment may use the provider's default reasoning behavior, disable optional reasoning, or select only an effort advertised by that model's catalog metadata. The Jev worker model is pinned server-side and shown as system information. - -Each tick makes one Jev reflex call per active worker; an Agent Zero planning call is made only when strategic replanning is required and may incur an OpenRouter charge plus at most one in-deadline repair or transient-retry charge. TypeSafe monetary cost is not reported. All workers observe the same frozen pre-tick world; valid decisions resolve together while an individual provider failure is retained as that worker's final lost tick. Start is deliberately disabled when the server has no key. This development API has no authentication or provider-account balance enforcement. Its experiment-scoped attempt and credit-admission limits are operator safeguards, not an upstream billing guarantee, so it is not suitable for unauthenticated public deployment. - -State is held only in the Game API process. The API captures one active safe experiment with bounded complete tick groups while the browser snapshot remains bounded without splitting a tick. The sole export format is schema version 13, which carries `swarmArchitectureVersion: 'zero-swarm-v1'`, an independent safe bounded provider-attempt ledger including work that did not produce a committed turn, and tick attribution. Pre-swarm exports (schema versions 9–12) are not readable by current code; inspecting them requires checking out a Git revision predating the zero-swarm migration. The Agent Zero model assignment and reasoning profile may be changed between ticks. A saved slug absent from the current compatible catalog is preserved and blocks execution until explicitly replaced. Agent Zero returns one structured plan under `swarm-planner-v2`; each Jev worker returns a candidate choice with a probability distribution and confidence value. The runtime extracts and conservatively repairs JSON before strict local schemas and the world engine apply authoritative validation. - -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. - -World Setup also configures server-owned provider-attempt and conservative -credit-admission limits. The per-attempt credit reservation bounds admission -exposure using exact decimal accounting; it is not an upstream provider-account -spending cap or billing guarantee. -Provider-attempt records contain only bounded sanitized attribution, usage, and -failure fields; prompts, raw responses, credentials, and private reasoning are -excluded. - -## Opt-in real-provider checks - -Two surfaces make genuine provider requests, and neither runs in default tests -or CI. World Lab's **Test Agent Zero planner** button sends exactly one bounded, -non-mutating request using the Agent Zero planner contract and selected -reasoning profile; it may incur a small charge and is cached by model, profile, -and contract version. `pnpm compare:live` runs the paid Jev-versus-deterministic-worker -comparison, which requires an explicit provider-cost acknowledgement and an -operator-selected Zero model, and enforces hard attempt and credit-admission -caps. Both read `OPENROUTER_API_KEY` from the repository-root `.env`; `pnpm compare:live` also requires `TYPESAFE_API_KEY`. +Open . The Game API binds to +; Next.js proxies `/api/game/*` to it. Select a compatible +Agent Zero model in **Agents**, then use **Single tick** or **Start**. Jev's +worker model is pinned server-side. Both provider keys are required for genuine +runs; keys never belong in browser environment variables. -## Development map source +For a deterministic walkthrough without provider keys or paid calls: -The compatible default centers on Toledo, Ohio (`41.6528, -83.5379`) at H3 resolution 9 and renders the same deterministic radius-six disk of exactly 127 cells with eight fixed perimeter starts. World Setup previews and applies resolution 8–11 scenarios with 1–32 agents, radius at most 40, and at most 5,000 actual generated cells. It may optionally add one seeded deterministic simulated player, using the `casual-cleaner` or `trail-hunter-v1` profile. MapLibre uses CARTO Dark Matter's tokenless raster tiles with `© OpenStreetMap contributors © CARTO` attribution. - -Combat systems, real-player GPS/capture, restartable world persistence, and autonomous scheduling remain deferred. - -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 tick remains an explicitly manual diagnostic -action. - -World Lab provides browser-owned absolute tick targets of **5, 10, 25, 50, -and 100**. The session-selected target defaults to 25. A bounded run pauses at -the authoritative tick target, on cancellation, or when the world is fully -infected. There is no background scheduler. - -The persistent operator shell keeps execution controls, run target, playback speed, current tick, known cost, and run state visible while switching between Live and Agents workspaces. Live centers the map between an independently scrolling agent rail and semantic Scoreboard, Agent, Hex, and Run inspector tabs; a bounded activity dock separates events and safe failure/recovery records. Agent configuration uses the same mounted execution controller and existing server-authoritative mutations, so workspace switching cannot duplicate or interrupt playback. Infrequent and destructive operations remain in the accessible overflow menu. Blackberry/teal/mint/celadon/vanilla semantic tokens define the dark application chrome without replacing domain-owned agent colors. +```bash +pnpm dev:test-provider +``` -The agent roster defaults to browser-local **Follow latest** behavior: after a -tick the inspector follows the last record in deterministic resolution order. -Selecting an agent manually disables following without hiding the roster's -textual Latest marker; the preference remains in that browser and is never -exported. The event log is a newest-first bounded feed, and the shared Model and Export dialogs keep their headers/actions fixed while their bodies scroll within the viewport. +Scripted mode bypasses `.env` loading and uses local deterministic providers. +The basemap still loads external OpenStreetMap tiles; explicit location search uses +Nominatim. Neither is needed by the offline unit tests. + +The basemap uses OpenStreetMap's standard raster tiles without a key. MapLibre +applies a dark grayscale treatment to that layer while preserving agent and +territory colors. Requests use normal browser caching and referrer behavior; +there is no tile prefetch or offline map download. Follow the +[OpenStreetMap tile usage policy](https://operations.osmfoundation.org/policies/tiles/) +when deploying or extending the map. + +![World Lab Agents workspace showing the Agent Zero planner and pinned Jev worker configuration](docs/assets/world-lab-agents.png) + +These screenshots show the current interface in scripted mode. Capture details +are in [the screenshot guide](docs/assets/README.md). + +## World Lab + +- **World Setup:** preview and apply H3 resolutions 8–11, 1–32 agents, and up + to 5,000 generated cells. The default is a 127-cell disk around Toledo, Ohio, + with eight perimeter starts. Optional seeded player pressure uses + `casual-cleaner` or `trail-hunter-v1`. +- **Bounded execution:** run to absolute tick targets of 5, 10, 25, 50, or 100, + cancel an active tick, or reset the scenario. Playback pauses at the target + or full infection. There is no background scheduler. +- **Inspection:** switch between Live and Agents while the same execution + controller stays mounted. Inspect Zero strategy, worker directives, reflex + choices, validation outcomes, territory, and safe activity records. +- **Research exports:** generate compact or pretty schema-v12 JSON, download + it, or manually save the exact generated artifact to local SQLite. Exports + include bounded safe tick and provider-attempt records, including attempts + that did not produce a committed tick. + +Live world state and active run telemetry are process-local. Restarting the +Game API loses the active run; the SQLite archive stores exported research +artifacts and cannot resume a simulation. + +## Provider costs and trust + +A genuine tick normally makes one Jev request per active worker and an +OpenRouter planning request only when replanning is required. A planner call +may make at most one transient retry within the original deadline. +OpenRouter cost is shown only when reported; TypeSafe monetary cost remains +unknown. World Setup's attempt and credit-admission limits are operator +safeguards, not upstream billing guarantees. + +The local API has no authentication or provider-account balance enforcement. +Keep it on loopback; it is not suitable for unauthenticated public deployment. +Provider credentials stay behind the agent runtime. Exports and archives omit +raw provider payloads, fixed prompts, credentials, and private reasoning. +See [Security](docs/SECURITY.md). -See [Testing](docs/TESTING.md), [Architecture](docs/ARCHITECTURE.md), [Security](docs/SECURITY.md), the accepted future [Gameplay Foundation](docs/GAMEPLAY_FOUNDATION.md), and the [Roadmap](ROADMAP.md). +## Workspace -Schema-v13 exports can be imported into an ignored local SQLite archive and queried without repeatedly loading full JSON artifacts. See [Local experiment archive](docs/EXPERIMENT_ARCHIVE.md). -After Generate export, World Lab can also save that exact current validated -artifact to the configured local archive with **Save to SQLite**. Preview -remains an optional estimate and does not gate generation or saving. -The action is manual and idempotent; changed options require regeneration. +| Path | Responsibility | +| ----------------------------- | --------------------------------------------------------------- | +| `apps/world-lab` | Next.js developer/admin map and operator controls | +| `apps/game-api` | Hono HTTP boundary and in-memory simulation orchestration | +| `packages/world-engine` | Pure deterministic world validation and consequence application | +| `packages/agent-runtime` | OpenRouter planner, TypeSafe Jev adapter, and model catalog | +| `packages/shared` | Runtime-validated boundary schemas and inferred types | +| `packages/experiment-archive` | SQLite imports and bounded research queries | -## Rename compatibility +## Validation and research -Hex Zero was formerly named Agentborne. Workspace packages now use the -`@hexzero/*` namespace, and the repository URL will be -`https://github.com/BlackSwampAI/hexzero` after the external repository rename. -Existing environments may continue using `AGENTBORNE_PROVIDER` and -`AGENTBORNE_EXPERIMENT_DB`; the corresponding `HEXZERO_` variable takes -precedence, and selecting a legacy alias emits a value-free deprecation notice. +Default tests and GitHub CI are deterministic and make no model-provider calls. +The repository owner runs local validation before a branch is pushed or a +draft PR is opened: -New archives default to `.hexzero/experiments.sqlite`. When that file does not -exist, an existing `.agentborne/experiments.sqlite` is opened in place with a -migration notice; Hex Zero never moves or overwrites it automatically. To -migrate manually, stop every Hex Zero process, create `.hexzero`, copy the -legacy database (including any `-wal` and `-shm` sidecars if present), verify -the copy opens, and only then remove the legacy files if desired. +```bash +pnpm install --frozen-lockfile +pnpm validate +pnpm exec playwright install chromium # first run only +pnpm test:e2e +``` -New downloads use `hexzero-experiment-`. A legacy -`agentborne-experiment-*.json` filename is not itself a barrier to import, but -its contents must be schema version 13; the schema-v9 artifacts that name -generally accompanies are rejected like any other pre-swarm export. Browser-owned -settings stored under legacy `agentborne` keys are schema-validated and copied -once to the new `hexzero` keys. +`pnpm validate` checks formatting, lint, types, unit/component tests, and builds. +Playwright starts scripted application servers separately. See +[Testing](docs/TESTING.md) for coverage and the owner validation workflow. + +| Command | Purpose | +| ---------------------- | ------------------------------------------------------------ | +| `pnpm compare:offline` | Reproducible Jev-fixture versus deterministic comparison | +| `pnpm compare:live` | Paid comparison; explicit acknowledgement and model required | +| `pnpm diagnose:swarm` | Summarize a running local API without provider calls | +| `pnpm experiment:db` | Import and query safe exports in the local archive | + +The **Test Agent Zero planner** button is also an explicit paid, non-mutating +probe. Neither that probe nor `compare:live` runs in default tests or CI. + +## Documentation + +- [Architecture](docs/ARCHITECTURE.md) — tick flow, boundaries, and HTTP endpoints +- [Testing](docs/TESTING.md) — coverage and exact validation commands +- [Security](docs/SECURITY.md) — secrets, prompts, telemetry, and deployment limits +- [Experiment archive](docs/EXPERIMENT_ARCHIVE.md) — SQLite import and queries +- [Offline comparison](docs/ZERO_SWARM_COMPARISON.md) and + [live comparison](docs/LIVE_SWARM_COMPARISON.md) — methodology and evidence limits +- [Roadmap](ROADMAP.md) and [Gameplay Foundation](docs/GAMEPLAY_FOUNDATION.md) — + delivered behavior and future product scope +- [ADR 0033](docs/adr/0033-retire-legacy-multi-agent-architecture.md) — retirement + of the previous architecture + +Current code reads only schema-v12 exports. Pre-swarm scenarios, snapshots, and +exports require an older Git revision. Historical ADRs and experiment reports +remain as decision history. + +Hex Zero was formerly named Agentborne. Deprecated `AGENTBORNE_PROVIDER` and +`AGENTBORNE_EXPERIMENT_DB` aliases remain supported; corresponding `HEXZERO_` +settings take precedence. Existing `.agentborne` archive paths and browser +preferences retain the documented rename compatibility. See +[archive path migration](docs/EXPERIMENT_ARCHIVE.md). diff --git a/ROADMAP.md b/ROADMAP.md index 182d408..7fa6503 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -42,7 +42,8 @@ architecture and removed all legacy infrastructure: The retirement case is structural rather than measured: the legacy path made one full generative provider call per active agent per tick, so provider attempts, cost, and tick latency all scaled linearly with roster size. The zero-swarm path -makes one Agent Zero planning call per tick regardless of roster size. No run in +makes at most one initial Agent Zero planning call when replanning is required, +regardless of roster size; other ticks reuse the current directives. No run in this repository has compared legacy cognition against zero-swarm cognition on real providers for cost, latency, or quality; that difference follows from the call pattern itself. See ADR 0033 for the full record. @@ -54,8 +55,9 @@ implementation history. ## Agent Zero planner -Agent Zero is the sole generative planner. It makes one OpenRouter call per -tick regardless of roster size, under the versioned contract `swarm-planner-v1`. +Agent Zero is the sole generative planner. It makes an OpenRouter planning call +only when strategic replanning is required, regardless of roster size, under +the versioned contract `swarm-planner-v1`. Each plan carries a strategy summary and one directive per active worker; directives persist across ticks and are reused when no replan is triggered. Agent Zero participates in the same frozen-world, simultaneous-tick transaction diff --git a/apps/world-lab/src/app/styles.css b/apps/world-lab/src/app/styles.css index f4b4f72..e793c2a 100644 --- a/apps/world-lab/src/app/styles.css +++ b/apps/world-lab/src/app/styles.css @@ -241,8 +241,7 @@ .operator-workspace { grid-template-columns: 220px minmax(0, 1fr) 300px; } - .status-popover, - .failed-turn-controls.inactive { + .status-popover { display: none; } .command-navbar { @@ -282,7 +281,7 @@ body { display: grid; grid-template-rows: auto auto minmax(0, 1fr) clamp(160px, 22dvh, 210px); } -.world-lab-shell.chat-collapsed { +.world-lab-shell.activity-collapsed { grid-template-rows: auto auto minmax(0, 1fr) 54px; } button, @@ -802,14 +801,6 @@ h2 { background: #2e684b; color: #f4fff8; } -.failed-turn-controls { - display: inline-flex; - gap: 4px; - min-width: 132px; -} -.failed-turn-controls.inactive { - visibility: hidden; -} .dock-heading { display: flex; align-items: flex-start; @@ -833,8 +824,7 @@ h2 { margin: 14px 0; } .model-filters label, -.model-select-label, -.agent-model-overrides label { +.model-select-label { display: grid; gap: 5px; color: #aebdb8; @@ -843,8 +833,7 @@ h2 { .model-console input, .model-console select, .model-console button, -.dock-heading button, -.new-message-button { +.dock-heading button { padding: 7px 9px; border: 1px solid #465752; border-radius: 5px; @@ -864,27 +853,10 @@ h2 { .model-facts div { grid-template-columns: 75px minmax(0, 1fr); } -.agent-model-overrides { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - margin-top: 14px; -} -.agent-model-overrides > strong { - grid-column: 1 / -1; -} -.agent-model-override { - display: grid; - gap: 6px; - padding: 7px; - border: 1px solid #34433f; - border-radius: 5px; -} .model-dialog { width: min(760px, calc(100vw - 28px)); } .model-dialog .model-select-label select, -.model-dialog .agent-model-override select, .export-dialog select, .export-dialog input[type='number'] { width: 100%; @@ -984,11 +956,6 @@ h2 { text-overflow: ellipsis; white-space: nowrap; } -.details-sidebar > .controls-panel, -.details-sidebar > .world-chat-panel, -.details-sidebar > .event-panel { - display: none; -} .bottom-dock { min-height: 0; height: auto; @@ -1019,33 +986,11 @@ h2 { min-height: 0; overflow-y: auto; } -.bottom-dock .world-chat-feed { - min-height: 0; - max-height: none; - flex: 1; - margin-top: 6px; -} -.bottom-dock .world-chat-panel { - display: flex; - min-height: 0; - flex-direction: column; -} -.bottom-dock .world-chat-panel.chat-collapsed { - justify-content: center; -} -.bottom-dock .event-panel.dock-collapsed { - justify-content: center; -} .bottom-dock .dock-heading { min-height: 37px; flex: 0 0 auto; overflow: visible; } -.new-message-button { - margin-top: 7px; - color: #a9e7c4; - cursor: pointer; -} .panel { padding: 14px; border-bottom: 1px solid #2d3835; @@ -1287,7 +1232,6 @@ dd { border-color: #fff2d3; box-shadow: 0 0 0 2px #fff2d344; } -.controls-panel h2, .event-panel h2 { color: #e6ece8; margin-bottom: 11px; @@ -1452,10 +1396,7 @@ dd { color: #ffafa3 !important; font-size: 0.74rem !important; } -.agent-inspector > p, -.turn-detail p, -.turn-detail details, -.compact-history { +.agent-inspector > p { color: #bcc9c5; font-size: 0.77rem; line-height: 1.5; @@ -1466,81 +1407,9 @@ dd { border: 2px solid #e6ece8; border-radius: 50%; } -.outcome { - display: inline-block; - padding: 2px 6px; - border-radius: 4px; - text-transform: uppercase; - font-size: 0.67rem !important; - font-weight: 750; -} -.outcome.accepted { - background: #214b39; - color: #8ff1ba; -} -.outcome.rejected, -.outcome.provider-error, -.outcome.lost-tick, -.outcome.operator-skipped { - background: #512b27; - color: #ffafa3; -} -.provider-meta, .muted { color: #81928c !important; } -.turn-detail details { - margin-top: 10px; -} -.turn-detail details p { - margin-top: 6px; - overflow-wrap: anywhere; -} -.observation-note { - color: #8fa19b !important; - font-style: italic; -} -.observation-difference { - color: #ffd58a !important; -} -.compact-history { - margin: 0; - padding-left: 18px; -} -.world-chat-feed small { - color: #82938e; -} -.world-chat-feed { - list-style: none; - max-height: 260px; - overflow-y: auto; - margin: 12px 0 0; - padding: 0 4px 0 0; -} -.world-chat-feed li { - border-left: 3px solid transparent; - border-bottom: 1px solid #26332f; - padding: 6px 0 6px 7px; - font-size: 13px; - line-height: 1.35; -} -.world-chat-feed li:last-child { - border-bottom: 0; -} -.world-chat-feed div { - display: flex; - justify-content: space-between; - gap: 10px; -} -.world-chat-feed p { - margin: 2px 0 0; - overflow-wrap: anywhere; - white-space: pre-wrap; -} -.world-chat-feed small { - font-size: 11px; - line-height: 1.3; -} .component-result { border-left: 2px solid #4e766a; padding-left: 8px; @@ -1665,7 +1534,6 @@ dd { .modal-body { padding: 12px; } - .agent-model-overrides, .model-global-grid, .model-filters, .range-row { @@ -1973,13 +1841,7 @@ summary:focus-visible { color: var(--text-muted); font-size: 0.66rem; } -.world-chat-feed strong { - text-shadow: - 0 1px 2px #000, - 0 0 4px #000; -} -.cancel-request-slot.inactive, -.failed-turn-controls.inactive { +.cancel-request-slot.inactive { visibility: hidden; } .overflow-menu summary { @@ -2070,8 +1932,7 @@ textarea, .model-console input, .model-console select, .model-console button, -.dock-heading button, -.new-message-button { +.dock-heading button { border-color: var(--line); background: var(--surface-raised); color: var(--text); @@ -2081,8 +1942,7 @@ dt, .catalog-state, .modal-header p, .model-filters label, -.model-select-label, -.agent-model-overrides label { +.model-select-label { color: var(--text-muted) !important; } .primary-action:not(:disabled) { diff --git a/apps/world-lab/src/components/map-config.ts b/apps/world-lab/src/components/map-config.ts index bd9ff83..a374cb7 100644 --- a/apps/world-lab/src/components/map-config.ts +++ b/apps/world-lab/src/components/map-config.ts @@ -1,7 +1,25 @@ -export const DARK_TILE_URLS = [ - 'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png', - 'https://b.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png', - 'https://c.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png', -] as const; +import type { StyleSpecification } from 'maplibre-gl'; -export const DARK_TILE_ATTRIBUTION = '© OpenStreetMap contributors © CARTO'; +type RasterSourceSpecification = Extract< + StyleSpecification['sources'][string], + { type: 'raster' } +>; +type RasterLayerSpecification = Extract< + StyleSpecification['layers'][number], + { type: 'raster' } +>; + +export const OSM_RASTER_SOURCE = { + type: 'raster', + tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'], + tileSize: 256, + maxzoom: 19, + attribution: + '© OpenStreetMap contributors', +} satisfies RasterSourceSpecification; + +export const DARK_RASTER_PAINT = { + 'raster-saturation': -1, + 'raster-brightness-min': 0.9, + 'raster-brightness-max': 0.08, +} satisfies NonNullable; diff --git a/apps/world-lab/src/components/model-options.test.ts b/apps/world-lab/src/components/model-options.test.ts index ad2db40..a8fe475 100644 --- a/apps/world-lab/src/components/model-options.test.ts +++ b/apps/world-lab/src/components/model-options.test.ts @@ -14,18 +14,16 @@ const model = (id: string, name: string, price = '0'): CompatibleModel => ({ }); describe('shared model options', () => { - it('deduplicates and orders global and per-agent options identically', () => { + it('deduplicates and orders model options consistently', () => { const catalog = [ model('zeta/model', 'Alpha'), model('Acme/model-2', 'Zulu', '0.000001'), model('acme/model-1', 'Beta'), model('zeta/model', 'Duplicate ignored'), ]; - const globalOptions = buildModelOptions(catalog); - const agentOptions = buildModelOptions(catalog); + const options = buildModelOptions(catalog); - expect(agentOptions).toEqual(globalOptions); - expect(globalOptions.map(({ value }) => value)).toEqual([ + expect(options.map(({ value }) => value)).toEqual([ 'acme/model-1', 'Acme/model-2', 'zeta/model', diff --git a/apps/world-lab/src/components/world-lab.test.tsx b/apps/world-lab/src/components/world-lab.test.tsx index 216cb2d..cbc2e49 100644 --- a/apps/world-lab/src/components/world-lab.test.tsx +++ b/apps/world-lab/src/components/world-lab.test.tsx @@ -32,15 +32,6 @@ const metrics = { territoryGainedThroughInfection: 0, territoryGainedThroughCapture: 0, territoryLostThroughCapture: 0, - publicMessagesRequested: 0, - publicMessagesAccepted: 0, - publicMessagesRejected: 0, - directMessagesRequested: 0, - directMessagesDelivered: 0, - directMessagesRejected: 0, - publicMessagesSent: 0, - directMessagesSent: 0, - directMessagesReceived: 0, uniqueVisitedCells: 0, tokens: {}, knownCostCredits: 0, @@ -129,6 +120,32 @@ function committedTick(tickNumber: number) { afterEach(() => vi.unstubAllGlobals()); describe('WorldLab swarm workspace', () => { + it('restores and persists the collapsed activity dock preference', async () => { + const key = 'hexzero.world-lab.activity-dock'; + window.localStorage.removeItem(key); + window.localStorage.setItem(key, 'collapsed'); + vi.stubGlobal( + 'fetch', + vi.fn(() => response(snapshot)), + ); + const user = userEvent.setup(); + render(); + + const expandButton = await screen.findByRole('button', { + name: 'Expand activity', + }); + expect(screen.getByRole('main')).toHaveClass('activity-collapsed'); + await user.click(expandButton); + expect( + await screen.findByRole('button', { name: 'Collapse activity' }), + ).toBeVisible(); + await waitFor(() => + expect(window.localStorage.getItem(key)).toBe('expanded'), + ); + expect(screen.getByRole('main')).not.toHaveClass('activity-collapsed'); + window.localStorage.removeItem(key); + }); + it('shows the fixed swarm architecture and Agent Zero model readiness', async () => { vi.stubGlobal( 'fetch', diff --git a/apps/world-lab/src/components/world-lab.tsx b/apps/world-lab/src/components/world-lab.tsx index 9ffece9..1b42dc8 100644 --- a/apps/world-lab/src/components/world-lab.tsx +++ b/apps/world-lab/src/components/world-lab.tsx @@ -104,7 +104,7 @@ export function WorldLab() { >({}); const [verifyingModelId, setVerifyingModelId] = useState(null); const [cancelling, setCancelling] = useState(false); - const [chatCollapsed, setChatCollapsed] = useState(false); + const [activityCollapsed, setActivityCollapsed] = useState(false); const [activityDockLoaded, setActivityDockLoaded] = useState(false); const inFlightRef = useRef(false); const boundedRunTargetRef = useRef(null); @@ -134,7 +134,7 @@ export function WorldLab() { (value) => value === 'collapsed' || value === 'expanded', ); const hydrationTask = window.setTimeout(() => { - setChatCollapsed(collapsed === 'collapsed'); + setActivityCollapsed(collapsed === 'collapsed'); setActivityDockLoaded(true); }, 0); return () => window.clearTimeout(hydrationTask); @@ -144,9 +144,9 @@ export function WorldLab() { if (!activityDockLoaded) return; window.localStorage.setItem( activityDockStorageKey, - chatCollapsed ? 'collapsed' : 'expanded', + activityCollapsed ? 'collapsed' : 'expanded', ); - }, [activityDockLoaded, chatCollapsed]); + }, [activityDockLoaded, activityCollapsed]); useEffect(() => { if ( @@ -609,7 +609,9 @@ export function WorldLab() { return (
@@ -1181,13 +1183,13 @@ export function WorldLab() { Swarm activity
- {!chatCollapsed && } + {!activityCollapsed && } ) : ( diff --git a/apps/world-lab/src/components/world-map-config.test.ts b/apps/world-lab/src/components/world-map-config.test.ts index dbedc1f..14e0724 100644 --- a/apps/world-lab/src/components/world-map-config.test.ts +++ b/apps/world-lab/src/components/world-map-config.test.ts @@ -1,14 +1,24 @@ import { describe, expect, it } from 'vitest'; -import { DARK_TILE_ATTRIBUTION, DARK_TILE_URLS } from './map-config'; +import { DARK_RASTER_PAINT, OSM_RASTER_SOURCE } from './map-config'; -describe('dark basemap configuration', () => { - it('uses tokenless CARTO Dark Matter tiles with complete attribution', () => { - expect(DARK_TILE_URLS).toHaveLength(3); - expect(DARK_TILE_URLS.every((url) => url.includes('/dark_all/'))).toBe( - true, +describe('dark OpenStreetMap basemap configuration', () => { + it('uses standard OSM tiles with attribution and the documented zoom ceiling', () => { + expect(OSM_RASTER_SOURCE).toMatchObject({ + type: 'raster', + tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'], + tileSize: 256, + maxzoom: 19, + attribution: + '© OpenStreetMap contributors', + }); + }); + + it('renders OSM tiles as desaturated dark tiles', () => { + expect(DARK_RASTER_PAINT['raster-saturation']).toBe(-1); + expect(DARK_RASTER_PAINT['raster-brightness-min']).toBeGreaterThan(0); + expect(DARK_RASTER_PAINT['raster-brightness-max']).toBeLessThan(1); + expect(DARK_RASTER_PAINT['raster-brightness-max']).toBeLessThan( + DARK_RASTER_PAINT['raster-brightness-min'], ); - expect(DARK_TILE_URLS.every((url) => !url.includes('token'))).toBe(true); - expect(DARK_TILE_ATTRIBUTION).toContain('OpenStreetMap'); - expect(DARK_TILE_ATTRIBUTION).toContain('CARTO'); }); }); diff --git a/apps/world-lab/src/components/world-map.tsx b/apps/world-lab/src/components/world-map.tsx index a001e10..d611dc7 100644 --- a/apps/world-lab/src/components/world-map.tsx +++ b/apps/world-lab/src/components/world-map.tsx @@ -23,7 +23,7 @@ import type { SimulatedPlayerState, } from '@hexzero/shared'; import { resolveAgentColor } from './ui-color'; -import { DARK_TILE_ATTRIBUTION, DARK_TILE_URLS } from './map-config'; +import { DARK_RASTER_PAINT, OSM_RASTER_SOURCE } from './map-config'; interface WorldMapProps { latitude: number; @@ -162,14 +162,16 @@ export function WorldMap(props: WorldMapProps) { style: { version: 8, sources: { - 'carto-dark': { + 'osm-dark': OSM_RASTER_SOURCE, + }, + layers: [ + { + id: 'osm-dark', type: 'raster', - tiles: [...DARK_TILE_URLS], - tileSize: 256, - attribution: DARK_TILE_ATTRIBUTION, + source: 'osm-dark', + paint: DARK_RASTER_PAINT, }, - }, - layers: [{ id: 'carto-dark', type: 'raster', source: 'carto-dark' }], + ], }, }); mapRef.current = map; diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 88d5948..6819fd0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -3,15 +3,16 @@ ## Zero-swarm execution `zero-swarm-v1` is the sole cognition architecture. The service first advances -deterministic player pressure -into an uncommitted world candidate. Agent Zero plans on the first tick, every -five ticks, and when an expiry or material event requires review. Other ticks -reuse committed unexpired worker directives and compile a fresh legal Zero wait -action. After plan validation, each worker receives a compact local projection and engine-legal -physical actions as opaque candidates. Jev selects one candidate ID, which the -service maps to a world action for seeded engine resolution. One complete tick -commits atomically. Planner and worker failures use explicit deterministic -fallbacks; provider attempts survive world rollback. See ADRs 0028, 0029, and 0031. +deterministic player pressure into an uncommitted world candidate. Agent Zero +plans on tick 1 and at scheduled five-tick reviews (ticks 6, 11, and so on). +Expiry or a material event can trigger an earlier review. Other ticks reuse +committed unexpired worker directives and compile a fresh legal Zero wait action. After +plan validation, each worker receives a compact local projection and +engine-legal physical actions as opaque candidates. Jev selects one candidate +ID, which the service maps to a world action for seeded engine resolution. One +complete tick commits atomically. Planner and worker failures use explicit +deterministic fallbacks; provider attempts survive world rollback. See ADRs +0028, 0029, and 0031. The worker observation includes bounded, current capture alerts. The TypeSafe request projects them as structured capture pressure without cell IDs or tick @@ -56,12 +57,13 @@ attempt with unknown monetary cost, including TypeSafe Jev, retains its configured per-attempt credit reserve in admission exposure. That reserve is a conservative execution limit, not a measured charge or Jev cost estimate. The OpenRouter planner asks Zero for bounded worker IDs, strategic target choice -IDs, mission, priority, risk, and its own legal action choice. Server code -materializes agent IDs, H3 targets, directive IDs, and five-tick lifetimes from -the frozen observation. Full valid plans remain accepted for compatibility, -but invalid output is classified into safe validation reasons without retaining -raw provider text. The selected Zero reasoning profile is sent to OpenRouter -with private reasoning excluded, and output is bounded. +IDs, mission, priority, risk, and its own legal action choice. For this compact +wire format, server code materializes agent IDs, H3 targets, directive IDs, and +five-tick lifetimes from the frozen observation. Full valid plans remain +accepted for compatibility, subject to validation that caps their lifetime at +ten ticks including the issue tick. Invalid output is classified into safe validation reasons +without retaining raw provider text. The selected Zero reasoning profile is +sent to OpenRouter with private reasoning excluded, and output is bounded. When Zero has no unexpired directive for a worker, a failed planning attempt gives that worker engine-legal deterministic local expansion without a Jev @@ -156,7 +158,14 @@ World Lab issues explicit ticks while Start or a bounded run is active. Provider Its command navbar is the single persistent application-control row. Browser-session run-target selection remains client orchestration and preserves absolute tick semantics; execution and reconciliation still consume authoritative API snapshots. Agent color uses retained effective color, base agent color, and a neutral fallback. -The default basemap is tokenless CARTO Dark Matter with OpenStreetMap and CARTO attribution. Deterministic tests inspect its configuration and mocked MapLibre H3 sources without requesting external tiles. +The basemap uses the standard HTTPS OpenStreetMap raster tile endpoint with +visible OpenStreetMap attribution. MapLibre applies dark, desaturated styling +to the basemap while leaving world overlays independently styled. Tile requests +remain viewport-driven and use normal browser caching and referrer behavior; +the app does not prefetch or download tiles for offline use. Deterministic +tests inspect the tile configuration and mocked MapLibre H3 sources without +requesting external tiles. See the +[OpenStreetMap Tile Usage Policy](https://operations.osmfoundation.org/policies/tiles/). Equivalent legal moves are ordered reproducibly from world seed, stable agent ID, and logical turn without process randomness. Their six-value compass labels are derived independently from the geographic initial bearing between H3 cell centers using equal 60-degree sectors; H3 traversal order never determines direction. diff --git a/docs/GAMEPLAY_FOUNDATION.md b/docs/GAMEPLAY_FOUNDATION.md index f8d151e..18c8211 100644 --- a/docs/GAMEPLAY_FOUNDATION.md +++ b/docs/GAMEPLAY_FOUNDATION.md @@ -8,22 +8,21 @@ > one generative planner (Agent Zero, contract `swarm-planner-v2`) issues > structured directives; workers resolve them via TypeSafe Jev reflex cognition. > Personalities, agent-to-agent communication, formal alliances, per-worker -> goals, and prose memories are removed. Real Player Mode, capture, respawn, -> GPS authority, and background timing remain future work. +> goals, and prose memories are removed. Agent capture by the simulated +> trail-hunter exists; capture by real players, respawn, GPS authority, and +> background timing remain future work. ## Current Agent Zero planning slice -In the zero-swarm architecture Agent Zero makes an OpenRouter planning call -only when strategic replanning is required; otherwise the last valid directive -set is reused with no planner call. Agent Zero issues a strategy summary and -per-worker structured directives (fields: mission, nullable target cell, -priority, risk tolerance, issue tick, expiry tick, optional note up to 160 -characters). Workers resolve their directive via TypeSafe Jev reflex cognition -over enumerated `action_N` candidates with a probability distribution and -confidence score; `deterministic-fallback` is used when the Jev call is -unavailable or its output fails validation. `zero-llm` is Agent Zero's own -planning cognition source, not a worker source. Agent Zero has no -extra movement or world-action power; it is one roster agent like any other. +In the zero-swarm architecture Agent Zero makes an OpenRouter planning call on +tick 1, at scheduled five-tick reviews (ticks 6, 11, and so on), and when a +material replanning trigger occurs. Other ticks reuse the committed directives +without a planner call. Agent Zero issues a strategy summary and one structured +directive per worker. Workers resolve their directive via TypeSafe Jev reflex +cognition over enumerated legal-action candidates; deterministic selection is +the fallback when Jev is unavailable or its output fails validation. Agent +Zero is also a roster agent and selects its own legal physical action. The +server assigns directive IDs, target cells, and bounded lifetimes. Current setup rejects a missing or null Agent Zero designation. Pre-swarm exports (schema versions 9–11) are not readable by current code; schema version 12 with `swarmArchitectureVersion: 'zero-swarm-v1'` is required. @@ -93,12 +92,14 @@ virtual timing. Player interactions occur continuously in real time. Agents act on server-authoritative global ticks. -- Freeze one authoritative snapshot for all surviving agents. -- Build every surviving agent's observation from that same snapshot. -- Request decisions concurrently under a shared bounded deadline. -- No agent sees another agent's decision from the same tick. -- Missing, malformed, or late decisions become final attributed lost ticks after the one permitted in-deadline repair or transient retry is exhausted. -- Valid decisions resolve simultaneously and deterministically. +- Freeze the pressure-adjusted candidate state for the tick. +- Build Agent Zero's strategic observation and worker observations from that + same state. +- Call Agent Zero when a strategic replan is required, then request worker + reflex choices sequentially under the shared tick deadline. +- Use deterministic fallbacks when planning or reflex calls fail; no worker + sees another worker's same-tick choice. +- Resolve the selected actions in seeded order and commit the tick atomically. - Each explicit tick advances the deterministic virtual clock by a hidden seeded interval, initially tunable between 5 and 10 minutes; no background scheduler exists yet. - The exact schedule remains server-only. Player Mode must not receive or display `nextTickAt`, a countdown, progress ring, or equivalent timing information. @@ -146,30 +147,15 @@ actions: Hiding indefinitely is not success; survival preserves the ability to pursue influence. -## Bounded agent knowledge +## Bounded agent observations -Agents know that human opposition exists, but receive only engine-produced evidence. -Prose and compact memories across ticks were specific to the legacy per-agent -architecture and are removed. Structured per-tick observations remain and should -include: - -- Current directive from Agent Zero (mission, target cell, priority, risk - tolerance). -- Recent cells gained and lost. -- Nearby disinfection patterns. -- Last-known player encounters with age and location. -- Explicit priority notifications for nearby territory loss. - -Examples of legitimate observations include: - -- `You lost cell X four minutes ago.` -- `Three cells southwest of you were disinfected recently.` -- `An agent was captured near cell Y.` -- `A player was last observed in your cell one tick ago.` - -Agents must not receive live player GPS, future player routes, an omniscient -global player-location list, exact next-tick timing, raw private reasoning or -chain-of-thought, or another agent's unresolved decision. +Agent Zero receives the bounded strategic observation needed to assign +directives, including current roster, strategic targets, territory and +simulated-player pressure. Each worker receives its directive, current legal +actions, a compact local world projection, and bounded local pressure facts. +The server does not add chat, per-worker goals, prose memory, diplomacy, or +future movement predictions. No runtime agent receives real-player GPS or raw +private reasoning. ## Real-time player interactions @@ -233,30 +219,10 @@ This preserves a persistent world without granting agents health or extra lives. ## Alliances -> **Removed (zero-swarm migration).** Formal alliances and agent diplomacy were -> specific to the legacy per-agent decision contract. The swarm architecture has -> one planner (Agent Zero) whose directives already coordinate all workers; -> there is no per-worker negotiation loop that alliances were designed to -> facilitate. Alliance engine code, the diplomacy affordances schema, and all -> related prompt layers were deleted in PRs 1–4 of this migration. Alliances -> are not current behavior. Whether they should return as a future feature for -> a Player Mode social layer is an open product question not settled by this -> migration; no roadmap milestone currently calls for them. The original design -> rationale is preserved below as history. - -Original design intent: alliances should initially improve survival through -information rather than numerical combat bonuses. - -Potential alliance benefits included: - -- Shared last-known player cells and observation age. -- Nearby territory-disturbance warnings. -- Capture notifications. -- Coordinated expansion directions. -- Reduced competition for the same cells. - -Allies would not initially receive health, damage, extra lives, shared -ownership, or automatic rescue mechanics. +Formal alliances, diplomacy, and agent communication were removed by ADR 0033. +They are not current behavior or a deferred feature. Reconsidering them +requires an explicit new product decision and roadmap change. Historical design +rationale remains in ADRs 0008 and 0016. ## Deterministic simulated players @@ -271,9 +237,10 @@ Initial profiles: Slice D1 implements only zero-or-one **Casual cleaner**. It moves at most one adjacent H3 cell per explicit interval toward visible infection, uses its seed for stable tie-breaking, and attempts at most one disinfection. An occupied -infected cell blocks cleaning. Other profiles and capture remain deferred. +infected cell blocks cleaning. The trail hunter described below is also +implemented; the area-defender profile remains deferred. -The later `trail-hunter-v1` experiment adds a separate optional profile. It +The optional `trail-hunter-v1` profile routes from visible infected cells without using hidden agent positions as targets. Co-location can capture an agent, immediately remove it, and leave its territory infected but abandoned. If this removes the last agent, or removes @@ -355,25 +322,22 @@ Every experiment export should preserve the complete initial scenario configurat ## Simultaneous decision dispatch -Simultaneous gameplay semantics must not depend on one inference provider's batch feature. The simulation service should own a provider-neutral decision dispatcher. In the zero-swarm architecture, a tick involves an optional planning call (Agent Zero, only when strategic replanning is required) followed by one Jev reflex call per worker (currently issued sequentially; every worker observes the same frozen pre-tick state and actions resolve together): - -1. Freeze the authoritative snapshot. -2. Build Agent Zero's world observation and each worker's reflex observation. -3. Dispatch Agent Zero's planning call through the configured transport under the shared tick deadline. -4. Distribute the resulting directives to workers; issue one Jev reflex call per worker sequentially. -5. Preserve one shared tick deadline and per-worker result identity. -6. Retry only against the saved observation. -7. Convert unfinished decisions to lost turns; fall back to `deterministic-fallback` for workers whose Jev call fails. -8. Resolve accepted worker actions in deterministic engine order. - -Expected transports include: +Simultaneous gameplay semantics do not depend on provider batch support. The +Game API owns tick execution. Each tick may include one Agent Zero planning +call when strategic replanning is required, followed by one sequential TypeSafe +Jev reflex call per worker. Workers use the same pre-action world state, and +their actions resolve in deterministic order. The engine remains authoritative. -- Concurrent ordinary OpenRouter requests for live experiments. -- Optional asynchronous OpenRouter batches after measured latency proves suitable. -- Independent concurrent OpenAI-compatible calls to local vLLM endpoints, allowing each server to schedule its own work. -- Deterministic offline providers for tests. +1. Advance simulated-player pressure into an uncommitted candidate. +2. Determine whether Agent Zero must replan; reuse valid directives otherwise. +3. Reserve the required provider-attempt capacity before any provider call. +4. Call Agent Zero if needed, then call Jev sequentially for each worker. +5. Use deterministic fallbacks for planner or worker failures. +6. Resolve actions in seeded order and commit the complete tick atomically. -The engine waits only until the shared deadline before resolving the tick regardless of which calls complete. +Provider recovery stays within the tick deadline. Cancellation or failure before +commit leaves world state unchanged, while already-started provider attempts +remain in the independent attempt ledger. ## Evaluation telemetry @@ -429,8 +393,8 @@ Do not initially add: - Real-time agent warnings that a player is approaching. - Omniscient simulated players. - Multiple movement actions per tick solely to compensate for human travel speed. -- Alliances, alliance stat bonuses, or shared lives. (Alliances were built and - removed by the zero-swarm migration; see the Alliances section.) +- Alliances, alliance stat bonuses, or shared lives. Alliances were built and + removed by ADR 0033; their return requires a new product decision. - LLM-controlled simulated players. ## Tunable values, not settled mechanics diff --git a/docs/LIVE_SWARM_COMPARISON.md b/docs/LIVE_SWARM_COMPARISON.md index 79c4305..498dc39 100644 --- a/docs/LIVE_SWARM_COMPARISON.md +++ b/docs/LIVE_SWARM_COMPARISON.md @@ -58,4 +58,6 @@ Do not reduce a run to final territory. Review these questions after the first b 6. Inspect low-confidence choices, high-confidence bad-looking choices, repeated stalls, capture-alert response, and high trail-hunter-pressure response. Does the narrow Jev state/question keep deterministic work in code and confidence routing meaningful? 7. Is Jev worth its complexity against deterministic workers? -This experiment's results, together with earlier offline comparisons, informed the decision to retire the legacy multi-agent architecture (see ADR 0033). +This experiment compares Jev reflex workers with the deterministic-worker +ablation; it does not compare against the retired per-agent architecture. ADR +0033 records the retirement decision and its structural rationale. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index d392b60..228bfa9 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -35,6 +35,13 @@ the server retains directive targets and the action mapping. The key, raw TypeSafe requests and responses, and provider error bodies never enter safe telemetry, World Lab, archives, or exports. TypeSafe token usage is factual; no monetary cost is inferred from it. + +The World Lab uses the standard HTTPS OpenStreetMap raster tile endpoint +without a provider key. Browser requests retain normal caching and referrer +behavior, and the app does not prefetch or download tiles for offline use. +Visible OpenStreetMap attribution remains on the map. See the +[OpenStreetMap Tile Usage Policy](https://operations.osmfoundation.org/policies/tiles/). + The second Jev question returns only a bounded yes probability in the same request as the action choice. Code applies the replan threshold and stores a structured signal; it grants no action or mutation authority. Reused directives @@ -83,11 +90,14 @@ unknown; the command never launches from default tests or CI. ## Model-provider isolation -Simultaneous ticks retain the same provider isolation. Every job receives a -schema-validated clone of its frozen observation, resolved model and reasoning -profile, abort signal, and the tick's shared deadline. Agent-authored output -cannot mutate the world directly or enter another same-tick observation. -Cancellation discards every result from the uncommitted tick. +Simultaneous ticks retain provider isolation. Each provider call receives a +schema-validated observation, resolved model and reasoning profile, abort +signal, and the tick's shared deadline. Agent-authored output cannot mutate the +world directly. Agent Zero's response is parsed and validated; only its +bounded structured plan and worker directives are supplied as worker +observation data. Raw planner text is not forwarded. Workers do not receive +other workers' choices. Cancellation discards every result from the +uncommitted tick. When strategic replanning is required, the OpenRouter planner receives one bounded strategic observation and is instructed to return exactly one plain JSON object naming opaque worker and target 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. @@ -95,11 +105,14 @@ The request uses the selected model, messages, `max_tokens`, `stream: false`, an Explicit scripted mode bypasses repository `.env` loading entirely. This keeps deterministic browser validation offline and prevents test-provider processes from unnecessarily reading genuine-provider credentials; genuine mode retains the existing environment conventions. -## Location-search boundary +## Tick recovery -Tick recovery is server-owned and bounded to the existing at-most-one automatic -repair or transient retry inside the shared deadline. Lost ticks are final; -there is no browser-driven Retry/Skip or unattended recovery path. +Tick recovery is server-owned and bounded inside the shared deadline. The +OpenRouter planner and Jev each allow at most one retry for HTTP 429 or 529. +Failed planning and worker choices use deterministic fallbacks. There is no +browser-driven Retry/Skip or unattended recovery path. + +## Location-search boundary Search runs only after explicit submission. Queries are trimmed to 120 characters, URL-encoded, receive no browser credentials, and are not logged by application code. The replaceable Nominatim adapter identifies the project, requests at most five results, limits upstream access to once per second per process, caches at most 100 normalized queries, times out after five seconds, and returns safe failures. `NOMINATIM_BASE_URL` replaces the upstream. Tests inject a fake; manual coordinates remain available. @@ -107,31 +120,35 @@ Non-success OpenRouter bodies are read only up to a fixed bound. The adapter ext ## Prompt and reasoning data -New logical turns may use one automatic repair or transient transport retry, -but never both, and all calls share the original 75-second deadline. A -corrective request contains the same authoritative observation plus only -allowlisted validation codes; it never contains the raw invalid response, raw -Zod issues, stack traces, provider bodies, or copied diagnostic text. -Engine-rejected normalized decisions are not retried. Tick recovery is limited -to one bounded in-deadline automatic repair or transient retry; an unresolved -decision becomes a final attributed lost tick. - -The model is explicitly instructed to return only one flat JSON decision with one concise visible summary and no hidden reasoning or chain-of-thought. Optional reasoning configuration always sets `exclude: true`; Provider default sends no reasoning instruction. Only numeric reasoning-token billing metadata is retained if OpenRouter reports it. The application stores no raw prompts, raw provider payloads, reasoning text, or private reasoning. - -Agent Zero's strategy summary and directive notes (at most 160 characters each) -and Jev's structured reflex output are bounded, agent-authored, untrusted data. -They appear only inside the immutable user observation, never the fixed system -instruction. There is no agent chat, diplomacy text, or memory prose. The engine -validates every world action, infection, and capture before committing state; -agent-authored outputs cannot grant engine authority, weaken validation, or -authorize prompt or reasoning disclosure. World Lab renders model text through -React text nodes and never raw HTML. +OpenRouter planner retries stay within the tick deadline. Planner responses +are normalized into a strict bounded schema before server code maps worker and +target choices into directives; workers receive those validated directives as +ordinary observation data. Raw planner text, raw Zod issues, stack traces, and +provider bodies are not forwarded to another provider or retained in telemetry. +Failed planner and reflex calls follow the documented deterministic fallback +paths; cancellation discards the uncommitted world tick. + +Agent Zero is instructed to return one bounded JSON plan; Jev returns a +structured reflex choice. Neither is asked for hidden reasoning or +chain-of-thought. Optional reasoning configuration always sets `exclude: true`; +Provider default sends no reasoning instruction. Only numeric reasoning-token +billing metadata is retained if OpenRouter reports it. The application stores +no raw prompts, raw provider payloads, reasoning text, or private reasoning. + +Agent Zero's strategy summary and directive notes are bounded, untrusted text. +They may appear in World Lab or as data in later planner observations. Only +schema-validated directives enter worker observations, and no agent-authored +text is placed in a system instruction. There is no agent chat, diplomacy text, +or memory prose. The engine validates every world action, infection, and +capture before committing state; agent-authored outputs cannot grant engine +authority, weaken validation, or authorize prompt or reasoning disclosure. +World Lab renders model text through React text nodes and never raw HTML. ## Experiment telemetry and exports Tick failures retain only sanitized diagnostics, model/reasoning selections, -timestamps, and the safe frozen observation. A resolved lost tick is final and -has no manual retry/skip path. Raw provider responses, reasoning text, +timestamps, and the safe frozen observation. Failed ticks do not partially +commit world changes and have no manual retry/skip path. Raw provider responses, reasoning text, credentials, and authorization headers are not retained. The Game API captures only schema-validated safe observations, requested world actions, separate result records, visible concise summaries, sanitized rejected attempts, bounded provider failures, and normalized usage metadata. Malformed identifiers use nullable or absent sanitized representations; raw provider output is never retained. It never records or exports API keys, authorization data, fixed or hidden prompts, raw provider request/response bodies, private chain-of-thought, hidden analysis, secrets, or unbounded diagnostics. Historical records are cloned and immutable. diff --git a/docs/TESTING.md b/docs/TESTING.md index f3b9b07..a3aa393 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -1,10 +1,10 @@ # Testing -The suite has 26 unit/component test files and one Playwright E2E file. All -default tests are deterministic and offline; no default test or GitHub Actions -job contacts OpenRouter or TypeSafe. Real-provider tests are separately named, -explicitly opted into, and excluded from default CI. Tests are small behavior -tests colocated with the code they cover; large snapshots are avoided. +Unit/component tests are colocated with the code they cover, alongside one +Playwright E2E file. All default tests are deterministic and offline; no default +test or GitHub Actions job contacts OpenRouter or TypeSafe. Real-provider tests +are separately named, explicitly opted into, and excluded from default CI. +Tests focus on small behaviors; large snapshots are avoided. ## Coverage @@ -208,25 +208,29 @@ reused-directive and structured-replan-request display. `world-lab.test.tsx` covers the swarm workspace: fixed swarm architecture and Agent Zero model readiness, fresh swarm setup without an architecture selector, tick commit, reset, exact-tick-cap execution without overlapping requests, -cancellation reconciliation, full-safe export preview before export actions, +cancellation reconciliation, activity-dock preference restoration and persistence, +full-safe export preview before export actions, model console showing one Agent Zero planner row and no per-agent override controls, and export dialog without agent/turn/level/outcome/action filters. -`model-options.test.ts` covers shared model options: deduplication and identical -ordering for global and per-agent options, identifier-before-name ordering, and +`model-options.test.ts` covers Agent Zero model options: deduplication, +identifier-before-name ordering, and price-metadata preservation. `ui-color.test.ts` covers agent color resolution: own-color resolution and neutral fallback for unknown agents. -`world-map-config.test.ts` covers dark basemap configuration: tokenless CARTO -Dark Matter tiles with complete attribution. +`world-map-config.test.ts` covers dark basemap configuration: standard +OpenStreetMap tile URLs, visible attribution, source zoom bounds, and a dark +raster treatment that leaves domain overlays untouched. ### Playwright E2E (`tests/e2e/world-lab.spec.ts`) Two tests: the long swarm-activity log scrolls inside the fixed-height bottom dock; and a deterministic scripted swarm tick commits and exports safe -telemetry without an OpenRouter request. +telemetry without an OpenRouter request. Browser tests fulfill OpenStreetMap +tile requests with a local image fixture instead of contacting the public tile +service. ## Scripted and deterministic seams diff --git a/docs/adr/0003-session-personality-configuration.md b/docs/adr/0003-session-personality-configuration.md index 9d7c2fd..2ab9faf 100644 --- a/docs/adr/0003-session-personality-configuration.md +++ b/docs/adr/0003-session-personality-configuration.md @@ -1,8 +1,7 @@ # ADR 0003: Server-owned session personality configuration -- Status: Accepted +- Status: Superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md) - Date: 2026-08-14 -- Status: Superseded by ADR 0033 ## Context diff --git a/docs/adr/0005-nearby-agent-messaging.md b/docs/adr/0005-nearby-agent-messaging.md index 124b265..f72b4e4 100644 --- a/docs/adr/0005-nearby-agent-messaging.md +++ b/docs/adr/0005-nearby-agent-messaging.md @@ -1,6 +1,6 @@ # ADR 0005: Nearby agent messaging as bounded world events -- Status: Accepted +- Status: Superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md) - Date: 2026-08-14 ## Context diff --git a/docs/adr/0007-decoupled-world-communication.md b/docs/adr/0007-decoupled-world-communication.md index 9664e2f..2fa8c4f 100644 --- a/docs/adr/0007-decoupled-world-communication.md +++ b/docs/adr/0007-decoupled-world-communication.md @@ -1,8 +1,7 @@ # ADR 0007: Decouple communication from world actions -- Status: Accepted +- Status: Superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md) - Date: 2026-08-14 -- Status: Superseded by ADR 0033 ## Context diff --git a/docs/adr/0008-formal-alliances-experiment.md b/docs/adr/0008-formal-alliances-experiment.md index 1114900..70b0ded 100644 --- a/docs/adr/0008-formal-alliances-experiment.md +++ b/docs/adr/0008-formal-alliances-experiment.md @@ -1,8 +1,7 @@ # ADR 0008: Formal alliances in the expanded autonomous-world experiment -- Status: Accepted +- Status: Superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md) - Date: 2026-08-14 -- Status: Superseded by ADR 0033 ## Context diff --git a/docs/adr/0010-versioned-agent-behavior.md b/docs/adr/0010-versioned-agent-behavior.md index 943fcd2..e2cfe24 100644 --- a/docs/adr/0010-versioned-agent-behavior.md +++ b/docs/adr/0010-versioned-agent-behavior.md @@ -1,8 +1,7 @@ # ADR 0010: Versioned agent behavior and seeded assignment -- Status: Accepted +- Status: Superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md) - Date: 2026-08-15 -- Status: Superseded by ADR 0033 ## Decision diff --git a/docs/adr/0012-agent-awareness-and-private-communications.md b/docs/adr/0012-agent-awareness-and-private-communications.md index a49e7dc..7097d81 100644 --- a/docs/adr/0012-agent-awareness-and-private-communications.md +++ b/docs/adr/0012-agent-awareness-and-private-communications.md @@ -1,6 +1,10 @@ # ADR 0012: Agent awareness and private communications -Status: accepted +## Status + +Superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md) for +per-agent communication and social awareness. This ADR remains a historical +record; zero-swarm uses Agent Zero planning and bounded worker observations. Agent decisions use `text-flat-json-v2` and the engine-owned `durable-influence-v2` objective. One optional communication intent is `public`, `direct`, or `alliance`. Public content is globally and future-player-visible; direct and alliance content is player-hidden and remains an untrusted claim. diff --git a/docs/adr/0013-patient-zero-coordinator.md b/docs/adr/0013-patient-zero-coordinator.md index 6d22f5e..3292d25 100644 --- a/docs/adr/0013-patient-zero-coordinator.md +++ b/docs/adr/0013-patient-zero-coordinator.md @@ -1,6 +1,11 @@ # ADR 0013: Experimental Patient Zero coordinator -Status: accepted experiment; optional designation superseded by ADR 0017 +## Status + +The Patient Zero designation remains the Agent Zero roster designation. +Superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md) for +legacy broadcasts, diplomacy, and coordinator behavior; the current role is +defined by the zero-swarm architecture. Historical scenarios may designate one roster agent as Patient Zero, or `null` for a legacy baseline. ADR 0017 requires the designation for all current/live diff --git a/docs/adr/0015-selective-agent-communication.md b/docs/adr/0015-selective-agent-communication.md index c58f686..3bf20cb 100644 --- a/docs/adr/0015-selective-agent-communication.md +++ b/docs/adr/0015-selective-agent-communication.md @@ -2,9 +2,8 @@ ## Status -Accepted for the simultaneous-tick experiment foundation. - -Superseded by ADR 0033. +Superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md); +retained as a historical record of the removed per-agent communication system. ## Context diff --git a/docs/adr/0016-fluid-alliances-and-diplomacy-affordances.md b/docs/adr/0016-fluid-alliances-and-diplomacy-affordances.md index 391081d..fba2687 100644 --- a/docs/adr/0016-fluid-alliances-and-diplomacy-affordances.md +++ b/docs/adr/0016-fluid-alliances-and-diplomacy-affordances.md @@ -2,10 +2,8 @@ ## Status -Accepted. This supersedes ADR 0008's fixed eight-member and four-active-alliance -limits while preserving its ownership, privacy, and telemetry decisions. - -Superseded by ADR 0033. +Superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md); +retained as a historical record of the removed alliance and diplomacy system. ## Context diff --git a/docs/adr/0017-mandatory-patient-zero-and-bounded-coordinator-context.md b/docs/adr/0017-mandatory-patient-zero-and-bounded-coordinator-context.md index dfe6d85..f71d16d 100644 --- a/docs/adr/0017-mandatory-patient-zero-and-bounded-coordinator-context.md +++ b/docs/adr/0017-mandatory-patient-zero-and-bounded-coordinator-context.md @@ -2,7 +2,9 @@ ## Status -Accepted. +The mandatory Agent Zero roster designation remains current. Superseded by +[ADR 0033](0033-retire-legacy-multi-agent-architecture.md) for the legacy +global diplomacy summary and coordinator powers. ## Context diff --git a/docs/adr/0018-bounded-agent-goal-state.md b/docs/adr/0018-bounded-agent-goal-state.md index 2c6cfe2..7cd0435 100644 --- a/docs/adr/0018-bounded-agent-goal-state.md +++ b/docs/adr/0018-bounded-agent-goal-state.md @@ -1,8 +1,7 @@ # ADR 0018: Bounded agent goal state -- Status: Accepted +- Status: Superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md) - Date: 2026-08-23 -- Status: Superseded by ADR 0033 ## Decision diff --git a/docs/adr/0019-bounded-agent-compact-memory.md b/docs/adr/0019-bounded-agent-compact-memory.md index 34ce7d0..9cd789b 100644 --- a/docs/adr/0019-bounded-agent-compact-memory.md +++ b/docs/adr/0019-bounded-agent-compact-memory.md @@ -1,8 +1,7 @@ # ADR 0019: Bounded agent compact memory -- Status: Accepted +- Status: Superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md) - Date: 2026-08-23 -- Status: Superseded by ADR 0033 ## Decision diff --git a/docs/adr/0023-bounded-patient-zero-cleaner-feed.md b/docs/adr/0023-bounded-patient-zero-cleaner-feed.md index 43e71fc..dad1941 100644 --- a/docs/adr/0023-bounded-patient-zero-cleaner-feed.md +++ b/docs/adr/0023-bounded-patient-zero-cleaner-feed.md @@ -4,6 +4,10 @@ Accepted for Slice D1.1. +The bounded cleaner feed remains part of the Agent Zero strategic observation. +Its legacy recommendations to communicate, negotiate alliances, or coordinate +agents are superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md). + ## Decision The existing single Patient Zero receives an engine-authored global cleaner diff --git a/docs/adr/0024-bounded-patient-zero-pressure-context.md b/docs/adr/0024-bounded-patient-zero-pressure-context.md index 2e98802..634d68d 100644 --- a/docs/adr/0024-bounded-patient-zero-pressure-context.md +++ b/docs/adr/0024-bounded-patient-zero-pressure-context.md @@ -4,6 +4,10 @@ Accepted for Slice D1.2. +The pressure rollup remains part of the Agent Zero strategic observation. +Its legacy communication and alliance recommendation behavior is superseded +by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md). + ## Decision Every displayed current-interval Patient Zero cleaner event carries a compact diff --git a/docs/adr/0031-persistent-swarm-directives.md b/docs/adr/0031-persistent-swarm-directives.md index ec97f43..5d4e565 100644 --- a/docs/adr/0031-persistent-swarm-directives.md +++ b/docs/adr/0031-persistent-swarm-directives.md @@ -4,6 +4,10 @@ Accepted for PR D. +Superseded by [ADR 0033](0033-retire-legacy-multi-agent-architecture.md) for its +statement that legacy execution remained in place; persistent directives and +event-driven replanning remain current. + ## Decision In `zero-swarm-v1`, Agent Zero plans on the first tick and at a fixed five-tick diff --git a/docs/assets/README.md b/docs/assets/README.md new file mode 100644 index 0000000..6419007 --- /dev/null +++ b/docs/assets/README.md @@ -0,0 +1,19 @@ +# World Lab screenshots + +The root README uses screenshots of the current zero-swarm interface: + +- `world-lab-live.png`: Live workspace, 1600 × 1000. +- `world-lab-agents.png`: Agents workspace, 1600 × 1000. + +Captured from the local application using `HEXZERO_PROVIDER=scripted`. The +provider labels and usage describe deterministic fixture decisions, not a real +model evaluation. The map uses standard OpenStreetMap tiles with a dark +MapLibre raster treatment and visible OpenStreetMap attribution. No provider +keys or private reasoning are shown. + +To refresh, start `pnpm dev:test-provider`, open the World Lab in a clean browser +profile at the viewport size above, advance six ticks with **Single tick**, and +select **Scoreboard** in Live before capturing. Then switch to **Agents** and +capture that workspace. Use only scripted provider calls for screenshots. +Preserve map attribution and capture the actual interface without adding mock +controls or model results. diff --git a/docs/assets/operator-console-desktop.png b/docs/assets/operator-console-desktop.png deleted file mode 100644 index 9acf2b9..0000000 Binary files a/docs/assets/operator-console-desktop.png and /dev/null differ diff --git a/docs/assets/operator-console-narrow.png b/docs/assets/operator-console-narrow.png deleted file mode 100644 index d7382e0..0000000 Binary files a/docs/assets/operator-console-narrow.png and /dev/null differ diff --git a/docs/assets/world-lab-agents.png b/docs/assets/world-lab-agents.png new file mode 100644 index 0000000..bdbc60f Binary files /dev/null and b/docs/assets/world-lab-agents.png differ diff --git a/docs/assets/world-lab-live.png b/docs/assets/world-lab-live.png new file mode 100644 index 0000000..c3267f3 Binary files /dev/null and b/docs/assets/world-lab-live.png differ diff --git a/tests/e2e/world-lab.spec.ts b/tests/e2e/world-lab.spec.ts index a37a317..0179ece 100644 --- a/tests/e2e/world-lab.spec.ts +++ b/tests/e2e/world-lab.spec.ts @@ -2,6 +2,19 @@ import { expect, test } from '@playwright/test'; import { readFile } from 'node:fs/promises'; import { experimentExportDocumentSchema } from '@hexzero/shared'; +test.beforeEach(async ({ page }) => { + await page.route('https://tile.openstreetmap.org/**', (route) => + route.fulfill({ + status: 200, + contentType: 'image/png', + body: Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', + 'base64', + ), + }), + ); +}); + async function openMoreActions(page: Parameters[0]['page']) { const menu = page.locator('details.overflow-menu'); if (