Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 26 additions & 26 deletions docs/front-ends/llm-referees.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# LLM referees

An LLM-driven referee a model that reads the game and decides what happens nextis a first-class consumer of osrlib, not an afterthought. The engine's shape is already the agent loop's shape: typed commands in, typed events out, a full-knowledge view to observe, and a deterministic core that makes every run reproducible. Everything such an agent would consume ships today: the schemas, the referee surface, the authored-content story, and the determinism guarantee. This page assembles the pieces: [the complete program](#the-complete-program) at the end runs as written, and every fragment along the way is an excerpt of it.
An LLM-driven referee (a model that reads the game and decides what happens next) is one of the consumers osrlib is built for. The engine's shape is already the agent loop's shape: typed commands in, typed events out, a full-knowledge view to observe, and a deterministic core that makes every run reproducible. Everything such an agent consumes ships with the library: the schemas, the referee surface, the authored-content story, and the determinism guarantee. [The complete program](#the-complete-program) at the end runs as written, and every snippet along the way comes from it.

## The schemas are the tool definitions

The reference section ships two raw artifacts alongside its pages: [commands.json](../reference/commands/commands.json) and [events.json](../reference/events/events.json) — the complete command and event surfaces as discriminated-union JSON Schemas, keyed on `command_type` and `event_type`. They are generated from the same registries the engine executes, so they cannot drift from what a session will actually accept and emit; [the command schema reference](../reference/commands/index.md) and [the event schema reference](../reference/events/index.md) render the same schemas page by page for human readers.
The reference section ships two raw artifacts alongside its pages: [commands.json](../reference/commands/commands.json) and [events.json](../reference/events/events.json). The command surface is a discriminated-union JSON Schema keyed on `command_type`, and the event surface is one keyed on `event_type`. They are generated from the same registries the engine executes, so they cannot drift from what a session will actually accept and emit. [The command schema reference](../reference/commands/index.md) and [the event schema reference](../reference/events/index.md) render the same schemas page by page for human readers.

The same unions are importable [`AnyCommand`][osrlib.crawl.commands.AnyCommand] and [`AnyEvent`][osrlib.crawl.events.AnyEvent] so a Python agent can build its tool definitions in-process instead of shipping files around:
The same unions are importable as [`AnyCommand`][osrlib.crawl.commands.AnyCommand] and [`AnyEvent`][osrlib.crawl.events.AnyEvent], so a Python agent can build its tool definitions in-process instead of shipping files around:

```{.python .no-run}
# The whole command surface as one discriminated union: a ready-made tool definition.
Expand All @@ -21,7 +21,7 @@ assert len(observations["oneOf"]) == len(ALL_EVENT_CLASSES)
assert json.loads(json.dumps(tools)) == tools # plain JSON Schema, ready for a tool registry
```

The whole command surface and the whole event surface, one discriminator field each — an agent framework that accepts JSON Schema tool definitions can load the command union as-is and let the model emit any command in the game, with validation for free. The loop such an agent runs is short (this is a sketch, not a framework):
The whole command surface and the whole event surface have one discriminator field each. An agent framework that accepts JSON Schema tool definitions can load the command union as-is and let the model emit any command in the game, with validation for free. The loop such an agent runs is short (this is a sketch, not a framework):

```{.python .no-run}
# Sketch: the agent loop, framework left to the reader.
Expand All @@ -34,7 +34,7 @@ while not session.mode.terminal: # the party fell, or the adventure is won

## The referee sees everything

The observation side is [`GameSession.view`][osrlib.crawl.session.GameSession.view] with [`Visibility.REFEREE`][osrlib.core.events.Visibility], which returns a [`RefereeView`][osrlib.crawl.views.RefereeView]: the full session state party internals, monster hit points, session flags, door states, the complete event logwith exactly two things withheld, the RNG internals and the master seed (those live only in the save document). Each group is a field of its own, typed as the session's own model, so an agent reads `view.monsters[0].current_hp` and `view.flags["key"]` off it and serializes the whole observation with `view.model_dump(mode="json")`. The player view is the opposite discipline, an enumerated whitelist; [Views and visibility](../guides/views-and-visibility.md) draws the line precisely.
The observation side is [`GameSession.view`][osrlib.crawl.session.GameSession.view] with [`Visibility.REFEREE`][osrlib.core.events.Visibility], which returns a [`RefereeView`][osrlib.crawl.views.RefereeView]: the full session state (party internals, monster hit points, session flags, door states, the complete event log) with exactly two things withheld, the RNG internals and the master seed, which live only in the save document. Each group is a field of its own, typed as the session's own model, so an agent reads `view.monsters[0].current_hp` and `view.flags["key"]` off it and serializes the whole observation with `view.model_dump(mode="json")`. The player view is the opposite discipline, an enumerated whitelist. [Views and visibility](../guides/views-and-visibility.md) draws the line precisely.

```{.python .no-run}
# The referee view is full state — flags, monster internals — minus RNG state and the seed.
Expand All @@ -45,7 +45,7 @@ dumped = view.model_dump()
assert "master_seed" not in dumped and "rng_streams" not in dumped
```

The event stream carries the same privilege. [`GameSession.execute`][osrlib.crawl.session.GameSession.execute] returns its events unfiltered, and each event is stamped with a visibility: referee-visibility events carry the hidden rolls surprise, reaction, secret-door detectionthat a player-facing front end must strip at its wire (as [the FastAPI pattern](fastapi-pattern.md) does). An in-process referee agent reads them all; they are its perception of what the dice just did.
The event stream is unfiltered for the same reason. [`GameSession.execute`][osrlib.crawl.session.GameSession.execute] returns its events unfiltered, and each event is stamped with a visibility: referee-visibility events include the hidden rolls (surprise, reaction, secret-door detection) that a player-facing front end must strip at its wire, as [the FastAPI pattern](fastapi-pattern.md) does. An in-process referee agent reads them all. They are its perception of what the dice just did.

```{.python .no-run}
# The unfiltered event stream is the observation: referee events carry the hidden rolls.
Expand All @@ -56,15 +56,15 @@ assert any(event.visibility is Visibility.REFEREE for event in result.events)

## The authorial surface

Player commands let the model drive the party's turn; referee commands let it *run the table*. They ride the same envelope and the same rejection discipline as everything elseno separate API, just more entries in the union:
Player commands let the model drive the party's turn. Referee commands let it *run the table*. They use the same envelope and the same rejection discipline as everything else: no separate API, only more entries in the union:

- [`SetFlag`][osrlib.crawl.commands.SetFlag] record a durable fact (the lever was pulled, the alarm was raised) that listeners and later narration can react to; see [Listeners and flags](../guides/listeners-and-flags.md)
- [`SpawnMonsters`][osrlib.crawl.commands.SpawnMonsters] and [`SpawnNpcParty`][osrlib.crawl.commands.SpawnNpcParty] open an encounter at a chosen distance, by fixed count or dice
- [`GrantItem`][osrlib.crawl.commands.GrantItem], [`GrantCoins`][osrlib.crawl.commands.GrantCoins], [`AwardXP`][osrlib.crawl.commands.AwardXP] place rewards directly
- [`SetDoorState`][osrlib.crawl.commands.SetDoorState] rewrite any door's state anywhere: lock it, wedge it, reveal it
- [`PlaceParty`][osrlib.crawl.commands.PlaceParty] and [`AdvanceTime`][osrlib.crawl.commands.AdvanceTime] teleport the party, advance the clock
- [`AddJournalEntry`][osrlib.crawl.commands.AddJournalEntry] and [`RecordNote`][osrlib.crawl.commands.RecordNote] the agent's durable in-world memory: a journal entry speaks to the players and ships in their view, a note is the referee's own margin and stays behind the screen
- [`ActivateQuest`][osrlib.crawl.commands.ActivateQuest], [`RevealObjective`][osrlib.crawl.commands.RevealObjective], [`CompleteObjective`][osrlib.crawl.commands.CompleteObjective], [`CompleteQuest`][osrlib.crawl.commands.CompleteQuest] advance authored quest state by hand, with one sharp edge: `CompleteQuest` pays nothing. Whoever completes a quest issues its rewards afterwards the interpreter does exactly that — so a hand-issued completion that expects the payout to follow on its own will strand the party unpaid
- [`SetFlag`][osrlib.crawl.commands.SetFlag] - record a durable fact (the lever was pulled, the alarm was raised) that listeners and later narration can react to. See [Listeners and flags](../guides/listeners-and-flags.md)
- [`SpawnMonsters`][osrlib.crawl.commands.SpawnMonsters] and [`SpawnNpcParty`][osrlib.crawl.commands.SpawnNpcParty] - open an encounter at a chosen distance, by fixed count or dice
- [`GrantItem`][osrlib.crawl.commands.GrantItem], [`GrantCoins`][osrlib.crawl.commands.GrantCoins], [`AwardXP`][osrlib.crawl.commands.AwardXP] - place rewards directly
- [`SetDoorState`][osrlib.crawl.commands.SetDoorState] - rewrite any door's state anywhere: lock it, wedge it, reveal it
- [`PlaceParty`][osrlib.crawl.commands.PlaceParty] and [`AdvanceTime`][osrlib.crawl.commands.AdvanceTime] - teleport the party, advance the clock
- [`AddJournalEntry`][osrlib.crawl.commands.AddJournalEntry] and [`RecordNote`][osrlib.crawl.commands.RecordNote] - the agent's durable in-world memory: a journal entry is written for the players and ships in their view, a note is the referee's own margin and stays behind the screen
- [`ActivateQuest`][osrlib.crawl.commands.ActivateQuest], [`RevealObjective`][osrlib.crawl.commands.RevealObjective], [`CompleteObjective`][osrlib.crawl.commands.CompleteObjective], [`CompleteQuest`][osrlib.crawl.commands.CompleteQuest] - advance authored quest state by hand. `CompleteQuest` pays nothing: whoever completes a quest issues its rewards afterwards, as the interpreter does, so a hand-issued completion leaves the party unpaid unless the agent issues the rewards too

```{.python .no-run}
# Referee commands are the authorial surface: record a fact, then spring an ambush.
Expand All @@ -77,37 +77,37 @@ The rejection contract matters as much here as it does for players: a rejected c

## Narrate from codes, not prose

Events never carry *engine-baked* prose. Each carries a stable message `code` a compact fact like `session.monsters.spawned` or `encounter.surprise.rolled`plus typed fields; [the message code reference](../reference/message-codes.md) lists every shipped code with its emitting event class and default template, and each event's fields are on [its schema page](../reference/events/index.md). That is exactly what a narrator model wants: ground truth it can render freely without parsing English back into facts. The one kind of English an event does carry is *authored* narrativea beat the adventure's author wrote, riding a structured field, which the next section teaches the narrator to treat differently from its own words. When a plain default line is enough, [`format_message`][osrlib.messages.format_message] renders one for any event, appending any authored beat verbatim:
Events never include *engine-baked* prose. Each has a stable message `code` (a compact fact like `session.monsters.spawned` or `encounter.surprise.rolled`) plus typed fields. [The message code reference](../reference/message-codes.md) lists every shipped code with its emitting event class and default template, and each event's fields are on [its schema page](../reference/events/index.md). That is exactly what a narrator model wants: ground truth it can render freely without parsing English back into facts. The one kind of English an event does include is *authored* narrative, a beat the adventure's author wrote in a structured field, which [Narrating authored content](#narrating-authored-content) below treats separately from the model's own words. When a plain default line is enough, [`format_message`][osrlib.messages.format_message] renders one for any event, appending any authored beat verbatim:

```{.python .no-run}
# Every event also renders to a default English line the model can lean on.
lines = [format_message(event) for event in result.events]
assert all(lines)
```

A practical narrator prompt sends the structured events (or their codes and fields) as the facts to narrate, and keeps the model's creativity in the telling — the dice already decided what happened.
A practical narrator prompt sends the structured events (or their codes and fields) as the facts to narrate, and keeps the model's creativity in the telling. The dice already decided what happened.

## Narrating authored content

An adventure written for the authored layer ([Gates, triggers, and quests](../guides/gates-triggers-quests.md)) arrives with material aimed squarely at a narrating referee, and an agent serving one should use all of it.

**Register the library's [`Interpreter`][osrlib.crawl.interpreter.Interpreter] and do no bookkeeping.** One `session.register_listener(Interpreter(session))` after the session is built (and again after a load), and the triggers, the quests, the fired-marks, and the rewards all play themselves as ordinary logged commands. The agent referees; the adventure runs its own wiring.
**Register the library's [`Interpreter`][osrlib.crawl.interpreter.Interpreter] and do no bookkeeping.** One `session.register_listener(Interpreter(session))` after the session is built (and again after a load), and the interpreter plays the triggers, the quests, the fired-marks, and the rewards as ordinary logged commands. The agent referees, and the interpreter runs the adventure's wiring.

**`guidance` is steering, never script.** [`NarrativeBlock`][osrlib.crawl.narrative.NarrativeBlock] carries a `guidance` field on any authored object, and [`LevelSpec.guidance`][osrlib.crawl.dungeon.LevelSpec] holds whole-level ambience that hangs on no object at all — the TUI barrow's first level reads:
**`guidance` is steering, never script.** [`NarrativeBlock`][osrlib.crawl.narrative.NarrativeBlock] has a `guidance` field on any authored object, and [`LevelSpec.guidance`][osrlib.crawl.dungeon.LevelSpec] holds whole-level ambience that hangs on no object at all. The TUI barrow's first level reads:

```{.python .no-run}
--8<-- "examples/tui_crawler/content.py:level-guidance"
```

A referee-side narrator reads these straight off the adventure document it is refereeing and never prints them verbatimthe same trust posture as an area's description prose, which already flows into narration. No event carries guidance and no view ships it; it is the author talking to the narrator.
A referee-side narrator reads these straight off the adventure document it is refereeing and never prints them verbatim, the same trust posture as an area's description prose, which already flows into narration. No event includes guidance and no view ships it. It is the author talking to the narrator.

**Authored beats are text to weave, not paraphrase.** A quest's offer and completion, an objective's progress, a gate's success and refusal arrive as structured fields on player-visible events and rejections, with `speaker` attribution when the author wrote one ("the temple almoner"). Those are the table's words: deliver them as written, in the speaker's voice, and put the model's creativity around them rather than over them.

**[`Command.source`][osrlib.crawl.commands.Command] keeps the agent's hands visible.** Every command the interpreter issues is stamped `trigger:{id}` or `quest:{id}`, so an agent that stamps its own referee commandsor simply leaves them unstamped leaves a log where its choices and the adventure's consequences never blur. That attribution is what completes the eval story below: replay a trajectory and the log itself says which grants were the model's ideas and which were the adventure playing out.
**[`Command.source`][osrlib.crawl.commands.Command] keeps the agent's hands visible.** Every command the interpreter issues is stamped `trigger:{id}` or `quest:{id}`, so an agent that stamps its own referee commands, or leaves them unstamped, leaves a log where its choices and the adventure's consequences never blur. That attribution is what completes the eval story below: replay a trajectory and the log itself says which grants were the model's ideas and which were the adventure playing out.

## Determinism is the eval story

Every random draw in osrlib comes from a named stream forked from the master seed, so the same seed plus the same command sequence produces the same game, bit for bit. For agent work this is the property that makes everything else tractable: a trajectory the seed and the list of commands the model choseis a complete, reproducible record of a run. Re-execute it offline and you get the same events to score; change a prompt and replay the same seeds to regression-test the change; diff two models on identical dungeons. [Determinism, saves, and replay](../guides/determinism-saves-replay.md) covers the exact guarantee and its boundary (identical replays are promised only under an identical engine version).
Every random draw in osrlib comes from a named stream forked from the master seed, so the same seed plus the same command sequence produces the same game, bit for bit. For agent work this is the property that makes everything else tractable: a trajectory (the seed and the list of commands the model chose) is a complete, reproducible record of a run. Re-execute it offline and you get the same events to score. Change a prompt and replay the same seeds to regression-test the change. Diff two models on identical dungeons. [Determinism, saves, and replay](../guides/determinism-saves-replay.md) covers the exact guarantee and its boundary (identical replays are promised only under an identical engine version).

```{.python .no-run}
# Determinism is the eval story: same seed, same commands, same trajectory.
Expand Down Expand Up @@ -214,8 +214,8 @@ assert [e.model_dump(mode="json") for e in rerun.events] == [e.model_dump(mode="

## Where next

- [Gates, triggers, and quests](../guides/gates-triggers-quests.md) the authoring side of the guidance and beats this page narrates.
- [Views and visibility](../guides/views-and-visibility.md) the referee/player projection line this page builds on.
- [Determinism, saves, and replay](../guides/determinism-saves-replay.md) the reproducibility guarantee behind the eval story.
- [The FastAPI pattern](fastapi-pattern.md) the other side of the doctrine: serving players who must *not* see what the referee sees.
- [The message code reference](../reference/message-codes.md) every code an event can carry, with its default English template.
- [Gates, triggers, and quests](../guides/gates-triggers-quests.md) - the authoring side of the guidance and beats this page narrates.
- [Views and visibility](../guides/views-and-visibility.md) - the referee/player projection line this page builds on.
- [Determinism, saves, and replay](../guides/determinism-saves-replay.md) - the reproducibility guarantee behind the eval story.
- [The FastAPI pattern](fastapi-pattern.md) - the other side of the doctrine: serving players who must *not* see what the referee sees.
- [The message code reference](../reference/message-codes.md) - every code an event can have, with its default English template.
Loading
Loading