updates-world-map-v3

World map model v3 (protocol updates)

This note summarizes a breaking-ish cleanup: one spatial model for “who is on the grid,” simpler RPC names, authored spaces with ownership, and no tool-derived WorldStructure tiles in the snapshot.

Successor model: Map inventory is spaces in snapshot.spaces (with owner, amenities, leases) plus kind: "structure" canvas anchors—not LangChain tool pads. See Structures and spaces world model.

Snapshot shape

  • worldMap.bounds — Axis-aligned extents in world units.
  • worldMap.occupants — Every agent and MCP registration appears here with kind: "agent" | "mcp", x / y, and identity fields. Agents carry journey/chat/assist metadata that used to live on separate players rows.
  • Agent platform — Optional string mirroring POST /api/agent-play/players body type (e.g. langchain). The field was previously named agentType on the wire; agentType is deprecated. The @agent-play/sdk parser still accepts agentType in JSON for older snapshots and maps it to platform on the typed result.
  • No top-level players array and no AgentPlaySnapshotPlayerRow in the SDK.
  • Coordinate rule — The server allocates cells so two occupants do not share the same (x, y); the SDK rejects unknown JSON that violates that.

RPC (POST /api/agent-play/sdk/rpc)

Op Query
getWorldSnapshot No sid required. Returns the resolved snapshot for the live server session.
getPlayerChainNode No sid required. Body { stableKey } returns one player-chain node payload (genesis string, header bounds+sid, or occupant row / removed) slice from the same resolved snapshot as getWorldSnapshot.
recordInteraction, recordJourney ?_sid= still required and must pass session validation.

Removed: getSnapshot (renamed), syncPlayerStructuresFromTools (and SSE world:structures).

SDK (@agent-play/sdk)

  • RemotePlayWorld.connect() replaces start().
  • getWorldSnapshot() — HTTP POST without sid; parses AgentPlaySnapshot with typed worldMap.
  • getPlayerChainNode(stableKey) — HTTP POST without sid; merges server slices into a local snapshot with mergeSnapshotWithPlayerChainNode. subscribeWorldState uses SSE playerChainNotify plus serialized node RPCs to update an in-memory snapshot.
  • addPlayeragentId is required; response includes registeredAgent (repository summary or synthesized stats for local dev).
  • Removed syncPlayerStructuresFromTools and structure parsing helpers from the public surface.

SSE / WebSocket

  • Still use world:player_added (not world:agent_added) for new agents.
  • world:structures is gone; refresh via getWorldSnapshot or incremental playerChainNotify + getPlayerChainNode (see SDK, Events, SSE, and remote API).
  • SSE data JSON may include rev, merkleRootHex, merkleLeafCount, and playerChainNotify merged alongside event-specific fields.

Opt-in verbose snapshot logs

On the server, AGENT_PLAY_VERBOSE=1 (or AGENT_PLAY_DEBUG=1) logs full getWorldSnapshot payloads from the RPC route for inspection.

Server architecture (Redis-first world state)

World grid and session snapshot do not live in PlayWorld Maps. The session store (Redis when REDIS_URL is set, otherwise MemorySessionStore) owns the canonical PreviewSnapshotJson.

Intended flows

  1. Player / preview client — Create or resume session → GET snapshot (or RPC getWorldSnapshot) reads from the store → subscribe to world:* fanout (Redis pub/sub, or in-process dispatch when using the memory store).
  2. Agent / SDK — Register via HTTP → mutations run under an exclusive I/O chain: getSnapshotJson → apply change (cell allocation is derived from current occupants; Redis persists snapshot and a derived grid:occupied SET in the same MULTI as the JSON for fast occupancy bookkeeping) → persistSnapshotReturningRevpublishWorldFanout.

Removed from PlayWorld (v3 persistence path): in-memory playerOrder, playerTypes, journey/interaction Maps, mcpServers array on the snapshot writer (MCP integrations appear only as kind: "mcp" occupants), enrichJourneyPath, hydrate-from-snapshot, and legacy snapshot/player-row types.

Helpers: runStoredWorldMutation, grid-allocate (computeFreeMapCell / occupiedKeysFromSnapshot), world-snapshot-helpers.

Player chain (Merkle snapshot digest)

Each persist of the world snapshot computes a player chain: canonical leaves are __genesis__ (trimmed UTF-8 contents of the workspace .root file, or the path in AGENT_PLAY_ROOT_FILE), then __header__ (session id + bounds), then occupants sorted by stable id → SHA-256 domain-separated leaf digests → binary Merkle root. The root and leaf count are written to the session hash (merkleRootHex, merkleLeafCount) in the same Redis MULTI as persistSnapshot / persistSnapshotReturningRev. publishWorldFanout attaches merkleRootHex, merkleLeafCount, and a slim playerChainNotify (updatedAt + nodes with stableKey, leafIndex, removed) so clients fetch full rows via getPlayerChainNode and merge locally (SDK subscribeWorldState, watch UI). The world allows at most 100 occupants (agents + MCP rows). Implementation lives under packages/web-ui/.../player-chain/ and load-player-chain-genesis.ts.

For a full developer-oriented walkthrough (world model, Redis keys, playerChainNotify, operational notes), see Agent Play world model and player chain.

Migrating integrations

  1. Replace startconnect, getSnapshotgetWorldSnapshot.
  2. Read agents only from snapshot.worldMap.occupants where kind === "agent".
  3. Pass agentId on every addPlayer; read registeredAgent from the HTTP response instead of structures.
  4. Drop any client code that depended on world:structures or tool-sync RPC.
  5. Author spaces with explicit owner metadata (AQL CREATE SPACE … OWNER …, registerSpaceNode, or ops scripts) instead of expecting tools to spawn map tiles.
  6. If you consumed Redis fanout playerChainDelta (per-leaf digests on the wire), migrate to playerChainNotify + getPlayerChainNode or keep using getWorldSnapshot only.