# Morrowind Perception/Command Protocol Specification **Version:** 1.0.0 **Status:** Draft **Authors:** Timmy Infrastructure Team **Date:** 2026-03-21 --- ## 1. Overview This document defines the **engine-agnostic Perception/Command protocol** used by Timmy's heartbeat loop to observe the game world and issue commands. The protocol is designed around the **Falsework Rule**: TES3MP (Morrowind) is scaffolding. If the engine swaps, only the bridge and perception script change — the heartbeat, reasoning, and journal remain sovereign. ### 1.1 Design Principles - **Engine-agnostic**: Schemas reference abstract concepts (cells, entities, quests), not Morrowind-specific internals. - **Versioned**: Every payload carries a `protocol_version` so consumers can negotiate compatibility. - **Typed at the boundary**: Pydantic v2 models enforce validation on both the producer (bridge) and consumer (heartbeat) side. - **Logged by default**: Every command is persisted to the SQLite command log for training-data extraction (see Issue #855). --- ## 2. Protocol Version Strategy | Field | Type | Description | | ------------------ | ------ | ------------------------------------ | | `protocol_version` | string | SemVer string (e.g. `"1.0.0"`) | ### Compatibility Rules - **Patch** bump (1.0.x): additive fields with defaults — fully backward-compatible. - **Minor** bump (1.x.0): new optional endpoints or enum values — old clients still work. - **Major** bump (x.0.0): breaking schema change — requires coordinated upgrade of bridge and heartbeat. Consumers MUST reject payloads whose major version exceeds their own. --- ## 3. Perception Output Schema Returned by `GET /perception`. Represents a single snapshot of the game world as observed by the bridge. ```json { "protocol_version": "1.0.0", "timestamp": "2026-03-21T14:30:00Z", "agent_id": "timmy", "location": { "cell": "Balmora", "x": 1024.5, "y": -512.3, "z": 64.0, "interior": false }, "health": { "current": 85, "max": 100 }, "nearby_entities": [ { "entity_id": "npc_001", "name": "Caius Cosades", "entity_type": "npc", "distance": 12.5, "disposition": 65 } ], "inventory_summary": { "gold": 150, "item_count": 23, "encumbrance_pct": 0.45 }, "active_quests": [ { "quest_id": "mq_01", "name": "Report to Caius Cosades", "stage": 10 } ], "environment": { "time_of_day": "afternoon", "weather": "clear", "is_combat": false, "is_dialogue": false }, "raw_engine_data": {} } ``` ### 3.1 Field Reference | Field | Type | Required | Description | | -------------------- | ----------------- | -------- | ------------------------------------------------------------ | | `protocol_version` | string | yes | Protocol SemVer | | `timestamp` | ISO 8601 datetime | yes | When the snapshot was taken | | `agent_id` | string | yes | Which agent this perception belongs to | | `location.cell` | string | yes | Current cell/zone name | | `location.x/y/z` | float | yes | World coordinates | | `location.interior` | bool | yes | Whether the agent is indoors | | `health.current` | int (0–max) | yes | Current health | | `health.max` | int (>0) | yes | Maximum health | | `nearby_entities` | array | yes | Entities within perception radius (may be empty) | | `inventory_summary` | object | yes | Lightweight inventory overview | | `active_quests` | array | yes | Currently tracked quests | | `environment` | object | yes | World-state flags | | `raw_engine_data` | object | no | Opaque engine-specific blob (not relied upon by heartbeat) | ### 3.2 Entity Types The `entity_type` field uses a controlled vocabulary: | Value | Description | | ---------- | ------------------------ | | `npc` | Non-player character | | `creature` | Hostile or neutral mob | | `item` | Pickup-able world item | | `door` | Door or transition | | `container`| Lootable container | --- ## 4. Command Input Schema Sent via `POST /command`. Represents a single action the agent wants to take in the world. ```json { "protocol_version": "1.0.0", "timestamp": "2026-03-21T14:30:01Z", "agent_id": "timmy", "command": "move_to", "params": { "target_cell": "Balmora", "target_x": 1050.0, "target_y": -500.0 }, "reasoning": "Moving closer to Caius Cosades to begin the main quest dialogue.", "episode_id": "ep_20260321_001", "context": { "perception_timestamp": "2026-03-21T14:30:00Z", "heartbeat_cycle": 42 } } ``` ### 4.1 Field Reference | Field | Type | Required | Description | | ------------------------------ | ----------------- | -------- | ------------------------------------------------------- | | `protocol_version` | string | yes | Protocol SemVer | | `timestamp` | ISO 8601 datetime | yes | When the command was issued | | `agent_id` | string | yes | Which agent is issuing the command | | `command` | string (enum) | yes | Command type (see §4.2) | | `params` | object | yes | Command-specific parameters (may be empty `{}`) | | `reasoning` | string | yes | Natural-language explanation of *why* this command | | `episode_id` | string | no | Groups commands into training episodes | | `context` | object | no | Metadata linking command to its triggering perception | ### 4.2 Command Types | Command | Description | Key Params | | --------------- | ---------------------------------------- | ---------------------------------- | | `move_to` | Navigate to coordinates or entity | `target_cell`, `target_x/y/z` | | `interact` | Interact with entity (talk, activate) | `entity_id`, `interaction_type` | | `use_item` | Use an inventory item | `item_id`, `target_entity_id?` | | `wait` | Wait/idle for a duration | `duration_seconds` | | `combat_action` | Perform a combat action | `action_type`, `target_entity_id` | | `dialogue` | Choose a dialogue option | `entity_id`, `topic`, `choice_idx` | | `journal_note` | Write an internal journal observation | `content`, `tags` | | `noop` | Heartbeat tick with no action | — | --- ## 5. API Contracts ### 5.1 `GET /perception` Returns the latest perception snapshot. **Response:** `200 OK` with `PerceptionOutput` JSON body. **Error Responses:** | Status | Code | Description | | ------ | ------------------- | ----------------------------------- | | 503 | `BRIDGE_UNAVAILABLE`| Game bridge is not connected | | 504 | `PERCEPTION_TIMEOUT`| Bridge did not respond in time | | 422 | `SCHEMA_MISMATCH` | Bridge returned incompatible schema | ### 5.2 `POST /command` Submit a command for the agent to execute. **Request:** `CommandInput` JSON body. **Response:** `202 Accepted` ```json { "status": "accepted", "command_id": "cmd_abc123", "logged": true } ``` **Error Responses:** | Status | Code | Description | | ------ | -------------------- | ----------------------------------- | | 400 | `INVALID_COMMAND` | Command type not recognized | | 400 | `VALIDATION_ERROR` | Payload fails Pydantic validation | | 409 | `COMMAND_CONFLICT` | Agent is busy executing another cmd | | 503 | `BRIDGE_UNAVAILABLE` | Game bridge is not connected | ### 5.3 `GET /morrowind/status` Health-check endpoint for the Morrowind bridge. **Response:** `200 OK` ```json { "bridge_connected": true, "engine": "tes3mp", "protocol_version": "1.0.0", "uptime_seconds": 3600, "last_perception_at": "2026-03-21T14:30:00Z" } ``` --- ## 6. Engine-Swap Documentation (The Falsework Rule) ### What Changes | Component | Changes on Engine Swap? | Notes | | ---------------------- | ----------------------- | --------------------------------------------- | | Bridge process | **YES** — replaced | New bridge speaks same protocol to new engine | | Perception Lua script | **YES** — replaced | New engine's scripting language/API | | `PerceptionOutput` | NO | Schema is engine-agnostic | | `CommandInput` | NO | Schema is engine-agnostic | | Heartbeat loop | NO | Consumes `PerceptionOutput`, emits `Command` | | Reasoning/LLM layer | NO | Operates on abstract perception data | | Journal system | NO | Writes `journal_note` commands | | Command log + training | NO | Logs all commands regardless of engine | | Dashboard WebSocket | NO | Separate protocol (`src/infrastructure/protocol.py`) | ### Swap Procedure 1. Implement new bridge that serves `GET /perception` and accepts `POST /command`. 2. Update `raw_engine_data` field documentation for the new engine. 3. Extend `entity_type` enum if the new engine has novel entity categories. 4. Bump `protocol_version` minor (or major if schema changes are required). 5. Run integration tests against the new bridge. --- ## 7. Error Handling Specification ### 7.1 Error Response Format All error responses follow a consistent structure: ```json { "error": { "code": "BRIDGE_UNAVAILABLE", "message": "Human-readable error description", "details": {}, "timestamp": "2026-03-21T14:30:00Z" } } ``` ### 7.2 Error Codes | Code | HTTP Status | Retry? | Description | | -------------------- | ----------- | ------ | ---------------------------------------- | | `BRIDGE_UNAVAILABLE` | 503 | yes | Bridge process not connected | | `PERCEPTION_TIMEOUT` | 504 | yes | Bridge did not respond within deadline | | `SCHEMA_MISMATCH` | 422 | no | Protocol version incompatibility | | `INVALID_COMMAND` | 400 | no | Unknown command type | | `VALIDATION_ERROR` | 400 | no | Pydantic validation failed | | `COMMAND_CONFLICT` | 409 | yes | Agent busy — retry after current command | | `INTERNAL_ERROR` | 500 | yes | Unexpected server error | ### 7.3 Retry Policy Clients SHOULD implement exponential backoff for retryable errors: - Initial delay: 100ms - Max delay: 5s - Max retries: 5 - Jitter: ±50ms --- ## 8. Appendix: Pydantic Model Reference The canonical Pydantic v2 models live in `src/infrastructure/morrowind/schemas.py`. These models serve as both runtime validation and living documentation of this spec. Any change to this spec document MUST be reflected in the Pydantic models, and vice versa.