DM Orchestration

DM orchestration modules in engine/dm/ provide the application-facing assistant wiring. This subsystem implements a lightweight agent architecture for a "Dungeon Master copilot" — an LLM-powered assistant that can answer rules questions, evaluate encounters, and provide actionable DM guidance during game sessions.

Components

  • model_gateway.py: model/provider abstraction boundary. Defines a ModelGateway Protocol with a single complete(prompt, *, system) method, plus a concrete OpenAIModelGateway implementation. The protocol pattern allows tests to inject stubs without touching HTTP, and makes it straightforward to add other providers (Anthropic, local models) by implementing the same interface.
  • tool_registry.py: callable tool inventory and dispatch definitions. The ToolRegistry is a name→handler dict where each handler is a Callable[[dict], dict]. Tools are registered imperatively (registry.register("lookup_rule", handler)) rather than declaratively, giving callers full control over which tools are available in a given session.
  • session_orchestrator.py: session lifecycle and orchestration state. The SessionOrchestrator composes a ModelGateway and a ToolRegistry, builds a system prompt that advertises available tools, and routes user input through the model. Currently a single-turn request/response loop — it does not yet implement tool-call parsing or multi-turn agentic execution.
  • tools/: tool implementations such as rules lookup (rules_lookup.py) and encounter balance. Each tool is a standalone function with a dict → dict signature, making tools trivially unit-testable in isolation.

Design Rationale

The DM orchestration layer is deliberately minimal — a thin shell around a model call with an extensible tool registry. This reflects several design choices:

  1. Protocol-based model abstraction — rather than coupling to a specific SDK, the ModelGateway Protocol decouples the orchestration logic from any particular LLM provider. This means the same SessionOrchestrator can run against OpenAI, a local model, or a test stub, with zero code changes in the orchestration layer.
  2. Imperative tool registration — tools are registered at runtime rather than discovered via decorators or metaclasses. This makes the tool set fully deterministic and testable: you construct a ToolRegistry, register exactly the tools you want, and pass it in. No hidden global state.
  3. Single-turn simplicity — the current implementation intentionally avoids multi-turn agentic loops, tool-call parsing, or conversation memory. The run_turn method takes user input and context, builds one prompt, gets one completion, and returns. This keeps the surface area small while the tool and model abstractions stabilize.

An alternative — adopting a framework like LangChain or building a full ReAct loop — was deferred because the tool set is still small and the primary value is in getting the abstraction boundaries right before adding complexity.

Assumptions & Constraints

  • Stateless turns. The orchestrator does not maintain conversation history. Each call to run_turn is independent. Session continuity (if needed) must be managed by the caller, passing relevant context via the context dict.
  • Tools are not yet invoked automatically. The orchestrator advertises tool names in the system prompt but does not parse the model's output to detect tool-call requests. Tool execution is a planned extension, not a current capability.
  • Placeholder model wiring. OpenAIModelGateway.complete() currently returns a truncated echo of the prompt rather than making a real API call. This is flagged as a placeholder in the source — production wiring requires injecting an actual OpenAI client via client_factory.
  • No authentication or rate limiting. The orchestration layer assumes a trusted caller. Access control and usage limits are expected to be handled at the API route level in Server.
  • Mem0 as memory backend. The prototype memory layer will use Mem0 through an adapter boundary. App-db should still store raw turns and memory-operation audit rows so campaign history is inspectable.
  • Deterministic combat boundary. Combat legality and resolution belong in engine code. The model can propose structured NPC moves, but the engine validates and applies all state changes.

Conceptual Model

The DM orchestration subsystem follows a classic gateway + tools + orchestrator pattern:

User input + context
        ↓
SessionOrchestrator
   ├── builds prompt (system prompt + tool list + context + input)
   ├── calls ModelGateway.complete()
   └── returns response dict
        ↓
API route (in server/)

The ToolRegistry is injected but not yet invoked in the main loop — it currently serves only to populate the tool list in the prompt. The planned extension is straightforward: parse the model response for tool-call markers, dispatch through ToolRegistry.run(), and feed results back into a follow-up completion.

The key boundary is between orchestration (which knows about turns, prompts, and tool dispatch) and tools (which know about domain logic like rules lookup). Tools never see the model or the session — they receive a payload dict and return a result dict. This keeps tools portable and testable without any orchestration infrastructure.

Integration Notes