forked from Rockachopa/Timmy-time-dashboard
313 lines
12 KiB
Markdown
313 lines
12 KiB
Markdown
|
|
# 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.
|