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(withowner, amenities, leases) pluskind: "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 withkind: "agent" | "mcp",x/y, and identity fields. Agents carry journey/chat/assist metadata that used to live on separateplayersrows.- Agent
platform— Optional string mirroringPOST /api/agent-play/playersbodytype(e.g.langchain). The field was previously namedagentTypeon the wire;agentTypeis deprecated. The@agent-play/sdkparser still acceptsagentTypein JSON for older snapshots and maps it toplatformon the typed result. - No top-level
playersarray and noAgentPlaySnapshotPlayerRowin 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()replacesstart().getWorldSnapshot()— HTTP POST withoutsid; parsesAgentPlaySnapshotwith typedworldMap.getPlayerChainNode(stableKey)— HTTP POST withoutsid; merges server slices into a local snapshot withmergeSnapshotWithPlayerChainNode.subscribeWorldStateuses SSEplayerChainNotifyplus serialized node RPCs to update an in-memory snapshot.addPlayer—agentIdis required; response includesregisteredAgent(repository summary or synthesized stats for local dev).- Removed
syncPlayerStructuresFromToolsand structure parsing helpers from the public surface.
SSE / WebSocket
- Still use
world:player_added(notworld:agent_added) for new agents. world:structuresis gone; refresh viagetWorldSnapshotor incrementalplayerChainNotify+getPlayerChainNode(see SDK, Events, SSE, and remote API).- SSE
dataJSON may includerev,merkleRootHex,merkleLeafCount, andplayerChainNotifymerged 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
- Player / preview client — Create or resume session →
GETsnapshot (or RPCgetWorldSnapshot) reads from the store → subscribe toworld:*fanout (Redis pub/sub, or in-process dispatch when using the memory store). - 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 derivedgrid:occupiedSET in the sameMULTIas the JSON for fast occupancy bookkeeping) →persistSnapshotReturningRev→publishWorldFanout.
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
- Replace
start→connect,getSnapshot→getWorldSnapshot. - Read agents only from
snapshot.worldMap.occupantswherekind === "agent". - Pass
agentIdon everyaddPlayer; readregisteredAgentfrom the HTTP response instead ofstructures. - Drop any client code that depended on
world:structuresor tool-sync RPC. - Author spaces with explicit owner metadata (AQL
CREATE SPACE … OWNER …,registerSpaceNode, or ops scripts) instead of expecting tools to spawn map tiles. - If you consumed Redis fanout
playerChainDelta(per-leaf digests on the wire), migrate toplayerChainNotify+getPlayerChainNodeor keep usinggetWorldSnapshotonly.