Transport
Biomata Engine ships with a JSON-over-WebSocket transport that exposes a running simulation to any client (Unity, Unreal, browser, test harness). Source: src/transport/websocket/.
WebSocket server
Starting from CLI
# Host-driven mode (default): client sends tick requests biomata-ws --config examples/engine_owned/sim.yaml --port 8765 # Autonomous mode: backend self-ticks, client subscribes to events biomata-ws --config examples/engine_owned/sim.yaml --port 8765 --mode autonomous
Programmatic server
import asyncio
from src.engine.simulation import Simulation
from src.service import create_session, TickMode
from src.transport.websocket.server import WebSocketServer
async def main():
# From YAML config
server = await WebSocketServer.from_config(
"examples/engine_owned/sim.yaml",
port = 8765,
tick_mode = TickMode.HOST_DRIVEN,
)
await server.start()
print(f"Listening on ws://0.0.0.0:{server.port}")
await server.wait_for_termination()
asyncio.run(main()) from src.plugins.external.world import HostedWorld from src.engine.simulation import Simulation from src.service import create_session from src.transport.websocket.server import WebSocketServer # Fully programmatic — useful when you need to configure agents/world in code world = HostedWorld() sim = Simulation(agents=[...], world=world, registry=registry) session = create_session(sim) server = WebSocketServer(session, host="127.0.0.1", port=8765) await server.start() await server.serve()
The server defaults to host="127.0.0.1" (loopback only). To expose on a LAN interface pass host="0.0.0.0" explicitly — only do this inside a trusted network, as the snapshot restore endpoint deserialises binary data and performs HMAC verification to reject tampered payloads.
WebSocketServer API
| Method | Description |
|---|---|
await start() | Bind and start accepting. Returns bound port. |
await stop() | Close and drain connections. |
await serve() | Start + block on SIGINT/SIGTERM + stop. |
await wait_for_termination() | Block until server closed (use with start()). |
port | Bound port (useful when started with port=0). |
WebSocket Protocol v1
The protocol uses JSON text frames over WebSocket (RFC 6455). All frames are UTF-8 encoded. Binary frames are silently ignored. The authoritative Python-side definitions are in src/transport/websocket/protocol.py.
Design rationale
JSON was chosen over protobuf because:
- No codegen — any language with JSON and WebSocket can implement a client from this document
- Debuggable —
wscat,websocat, and browser DevTools work out of the box - Latency is not the bottleneck — LLM brain calls dominate at 100–5000 ms per tick; JSON parse overhead is under 1 ms
Frame envelope
Every frame is a JSON object. All frames carry "type" and "v". Unknown top-level fields MUST be ignored by both sides for forward compatibility.
hlo — Server Hello
Sent by the server immediately on connection, before any client request. Clients MUST NOT send requests until the hello frame is received.
{
"type": "hlo",
"v": 1,
"server": "biomata-engine",
"server_version": "0.5.0",
"session_id": "a3f2c1d0-e29b-41d4-a716-446655440000",
"tick_mode": "host_driven",
"capabilities": ["tick", "register_agent", "remove_agent",
"send_observation", "snapshot", "restore", "events",
"agent.register", "agent.unregister", "agent.list",
"roles.list"]
} Read tick_mode first and branch client behavior accordingly:
"host_driven"— client drives timing withtickrequests; do NOT callpause/resume"autonomous"— subscribe to events; usepause/resumeto control the loop; do NOT calltick
req — Client Request
{
"type": "req",
"v": 1,
"id": "550e8400-e29b-41d4-a716-446655440000",
"method": "tick",
"params": { }
} res — Server Response
// Success
{
"type": "res",
"v": 1,
"id": "550e8400-...",
"ok": true,
"result": { }
}
// Error
{
"type": "res",
"v": 1,
"id": "550e8400-...",
"ok": false,
"error": {
"code": -32601,
"name": "METHOD_NOT_FOUND",
"message": "unknown method 'foo'"
}
} evt — Server Event
{
"type": "evt",
"v": 1,
"session_id": "a3f2c1d0-...",
"seq": 42,
"event_type": "tick_end",
"tick": 5,
"agent_id": "engine",
"ts": "2026-05-23T14:32:01.123Z",
"data": { }
} seq is a per-connection monotonically increasing counter. A gap in seq indicates dropped events (backpressure — default queue: 2048 events).
Methods
health_check
Liveness probe. Returns current session state.
// Request
{"type":"req","v":1,"id":"...","method":"health_check","params":{}}
// Result
{
"status": "ok",
"session_state": "running",
"tick": 42,
"agent_count": 5,
"tick_mode": "host_driven"
} agent.register
Dynamically add an agent to the running simulation. The brain class is imported by dotted path. Prefer this dot-notation form for new integrations — it groups brain and memory config together and aligns with sim.yaml syntax.
{
"method": "agent.register",
"params": {
"id": "npc_guard_01",
"name": "Guard",
"brain": {
"class": "src.plugins.builtin.idle_brain.brain.IdleBrain",
"config": {}
},
"capabilities": ["patrol"],
"metadata": { "scene": "level_01" },
"reconnect": false
}
}
// Result: {"id": "npc_guard_01", "reconnected": false, "capabilities": ["patrol"]} Pass "reconnect": true on WebSocket reconnect to avoid AGENT_EXISTS errors when the agent is already registered. The server returns the existing agent state unchanged.
agent.unregister
Remove a registered agent and release its resources. The backend closes the brain, removes the agent from the scheduler, and emits an agent_unregistered event.
{"method":"agent.unregister","params":{"id":"npc_guard_01"}}
// Result: {"id": "npc_guard_01"} agent.list
Return all currently registered agents — both YAML-static agents loaded at startup and runtime-dynamic agents registered via WebSocket. Order matches the scheduler's agent list.
{"method":"agent.list","params":{}}
// Result:
{
"agents": [
{ "id": "guard_001", "name": "Aldric", "capabilities": ["patrol", "authority"], "metadata": {} },
{ "id": "scout_001", "name": "Scout", "capabilities": ["patrol"], "metadata": { "scene": "level_01" } }
],
"count": 2
} register_agent / remove_agent (legacy)
The original flat-param methods remain fully functional. Existing clients need no changes.
{
"method": "register_agent",
"params": {
"agent_id": "npc_guard_01",
"agent_name": "Guard",
"brain_class": "src.plugins.builtin.idle_brain.brain.IdleBrain",
"brain_config": {},
"memory_class": "",
"memory_config": {}
}
}
// Result: {"agent_id": "npc_guard_01"}
{"method":"remove_agent","params":{"agent_id":"npc_guard_01"}}
// Result: {"agent_id": "npc_guard_01"} | Legacy method | Dot-notation equivalent | Key difference |
|---|---|---|
register_agent | agent.register | Uses agent_id / brain_class flat keys; result uses agent_id |
remove_agent | agent.unregister | Uses agent_id instead of id |
| (none) | agent.list | New in Phase 2 — no legacy equivalent |
send_observation
Pre-load one agent's observation for the next tick without triggering cognition. Requires a HostedWorld.
{
"method": "send_observation",
"params": {
"agent_id": "npc_guard_01",
"observation": {"location": "gatehouse", "health": 100}
}
} tick
Execute one cognition cycle. HOST_DRIVEN mode only.
{
"method": "tick",
"params": {
"agent_observations": [
{"agent_id": "npc_001", "observation": {"position_x": 3.5, "position_z": -2.0}}
],
"world_metadata": {"weather": "clear", "time_of_day": "noon"}
}
}
// Result:
{
"tick": 7,
"decisions": [
{
"agent_id": "npc_001",
"agent_name": "Guard",
"action": "navigate",
"parameters": {"target_x": 5.0, "target_z": 0.0},
"outcome_text": "moving to patrol point",
"engine_commands": [{"type": "navigate", "x": 5.0, "y": 0.0, "z": 0.0}],
"error": null
}
],
"errors": []
} pause / resume
Control the autonomous tick loop. AUTONOMOUS mode only.
{"method":"pause","params":{}} // Result: {"state": "paused"}
{"method":"resume","params":{}} // Result: {"state": "running"} snapshot / restore
{"method":"snapshot","params":{}}
// Result:
{
"data_b64": "<base64 blob>",
"hmac_sha256": "<64-hex-char HMAC-SHA256 tag>",
"tick": 42,
"created_at": "2026-05-23T..."
}
{"method":"restore","params":{"data_b64": "<same base64 blob>", "hmac_sha256": "<same tag>"}}
// Result: {"tick": 42} The hmac_sha256 field is required for restore. The server verifies it with an ephemeral per-process signing key before deserialising. A missing, malformed, or mismatched tag returns error code -5 (SNAPSHOT_INVALID). Pass the data_b64 and hmac_sha256 values verbatim from the snapshot response — do not modify them.
subscribe_events / unsubscribe_events
// Subscribe to specific event types (omit event_types for all events)
{"method":"subscribe_events","params":{"event_types":["tick_end","action_completed"]}}
// Result: {"subscribed": true, "event_types": ["tick_end", "action_completed"]}
{"method":"unsubscribe_events","params":{}}
// Result: {"unsubscribed": true} Error codes
| Code | Name | Meaning |
|---|---|---|
-32700 | PARSE_ERROR | Frame was not valid JSON. id is null. |
-32600 | INVALID_REQUEST | Required envelope field missing or wrong type. |
-32601 | METHOD_NOT_FOUND | The method string is not recognized. |
-32602 | INVALID_PARAMS | Method params missing or wrong type. |
-32603 | INTERNAL_ERROR | Unhandled server exception. Report as a bug. |
-32000 | VERSION_MISMATCH | Client v not supported by this server. |
-1 | SESSION_ERROR | Operation invalid in current session state. |
-2 | AGENT_EXISTS | register_agent with an already-registered ID. |
-3 | AGENT_NOT_FOUND | remove_agent with an unregistered ID. |
-4 | IMPORT_ERROR | brain_class dotted path could not be imported. |
-5 | SNAPSHOT_INVALID | hmac_sha256 verification failed or snapshot data is malformed. |
-6 | VALIDATION_ERROR | Agent definition failed structural validation. error.message names all failing fields. |
Event types
| event_type | agent_id | Notable data fields |
|---|---|---|
tick_start | "engine" | World metadata dict |
tick_end | "engine" | {"tick": N} |
brain_decided | agent id | {"action", "parameters"} |
action_completed | agent id | {"action", "outcome", "tick"} |
action_failed | agent id | {"action", "error", "tick"} |
agent_step_error | agent id | {"error", "tick"} |
agent_registered | agent id | {"agent_name", "capabilities", "metadata"} |
agent_unregistered | agent id | {"agent_name"} |
SDK implementation checklist
When implementing a new SDK client (Unreal, Godot, browser, etc.) targeting Protocol v1:
- Connect via WebSocket (RFC 6455), UTF-8 JSON text frames only
- Wait for
"type":"hlo"frame before sending requests; recordserver_version,tick_mode, andcapabilities. Check for"agent.register"in capabilities before using the dot-notation agent methods. - Log a warning (do not disconnect) if hello
"v"differs fromPROTOCOL_VERSION - Branch on
tick_mode:"host_driven"→ use tick;"autonomous"→ subscribe to events, use pause/resume - Send
"v": 1in everyreqframe - Generate a UUID v4
idper request; correlate responses byid - Handle
"ok": falsewith structurederrorobject (code + name + message); switch onerror.code - Handle
"type":"evt"frames independently of pending requests - Track
evt.seqto detect dropped events (backpressure) - Implement a per-call timeout; fail pending requests on disconnect
- Serialize sends — WebSocket sends are not concurrent-safe on most platforms