Skip to content

Transport

The protocol is transport-agnostic — it defines the JSON messages, not how the bytes move. A conforming pair can exchange them over any of:

  • A platform-native invocation channel (e.g. Tauri invoke() with matching #[tauri::command] handlers). The reference desktop client uses this for solo play.
  • A WebSocket connection (ws:// / wss://), one JSON document per text frame. Used for the web client and multiplayer relay.
  • Web Worker postMessage, used by the reference web client when the engine runs in a wasm worker.
  • An in-process channel (e.g. Rust mpsc) carrying serialized JSON. Used by the test harness and embedded engine deployments.

A single session MUST use one transport for its whole duration; cross-transport sessions are out of scope.

When more than one client takes part in a session, a relay server sits between the engine host and the remote clients (the reference relay is manabrew-rs/crates/manabrew-server). The game messages — prompts, responses, logs, snapshots — are carried as an opaque state value inside the relay’s own envelope, discriminated by a kind field:

kindDirectionPayload
promptengine host → remote client{ "kind": "prompt", "forPlayer": "player-N", "prompt": <AgentPrompt> }
responseremote client → engine host{ "kind": "response", "fromPlayer": "player-N", "action": <PlayerAction> }
logengine host → all{ "kind": "log", "fromPlayer": "player-N", "entry": <GameLogEntry> }
snapshotengine host → joining observer{ "kind": "snapshot", "fromPlayer": "player-N", "entry": <GameSnapshot> }
roomRelayany → anyroom-control messages (e.g. bot lifecycle) — implementation defined

The lobby/room-control layer that wraps this (authentication, room creation, ready state, …) is specific to the reference relay and beyond the scope of the wire protocol itself. Implementations MAY define additional kind values; consumers MUST ignore unknown kind values rather than treating them as errors.