Implement two foundational infrastructure pieces for the Morrowind integration: 1. Perception/Command Protocol (Issue #859): - Formal spec document with JSON schemas, API contracts, versioning strategy - Engine-agnostic design following the Falsework Rule - Pydantic v2 models (PerceptionOutput, CommandInput) for runtime validation 2. SQLite Command Log + Training Pipeline (Issue #855): - SQLAlchemy model for command_log table with full indexing - CommandLogger class with log_command(), query(), export_training_data() - TrainingExporter with chat-completion, episode, and instruction formats - Storage management (rotation/archival) utilities - Alembic migration for the new table Includes 39 passing tests covering schema validation, logging, querying, and export. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
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_versionso 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.
{
"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.
{
"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
{
"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
{
"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
- Implement new bridge that serves
GET /perceptionand acceptsPOST /command. - Update
raw_engine_datafield documentation for the new engine. - Extend
entity_typeenum if the new engine has novel entity categories. - Bump
protocol_versionminor (or major if schema changes are required). - Run integration tests against the new bridge.
7. Error Handling Specification
7.1 Error Response Format
All error responses follow a consistent structure:
{
"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.