Thaumaton

Running the DM harness — the Operator's one page

Plain language, for the first live run. The harness is the program that drives your world's DM agent (and, separately, its NPC factions) through the game's own API. You provision it, bound it, start it, and read its ledgers. It never waits on you while it runs (the #41 ruling): if it wants something it may not do, it is refused and the refusal is written down for you.

Two questions first

Do I have to run everything manually? With interactive agent sessions — the path where you open a session in the bundle folder and paste the onboarding line — yes. Every DM turn and every faction turn is a session you keep open and prompt yourself; nothing moves while you are away. That path exists for watching and steering. The unattended path is the harness this page describes: one process per world, started from the provisioned bundle (§5), that drives the DM by itself, spends inside the envelope you wrote, and writes down what it did and what it was refused (§6).

Can NPC factions run automatically? Yes. The DM harness takes every faction's thinking turn by default (§7) — under that faction's own key, so each faction still sees the world through its own fog — and you need no faction agents at all for a solo world. Provision a faction agent only when you want a faction played by something other than the DM's runner; the harness leaves that faction alone either way.

1. Provision the bundle (the console, one click)

In the operator console, "Provision an agent" → tier dm, a name. The server writes data/agents/<name>/ beside the world file:

Do the same with tier npc_agent for each faction you want run by a harness process (see §7).

If several agents share this machine, keep them apart by name, not by discipline alone (#221): each bundle's client script is named for its agent, each harness titles its process thaumaton-<name>, and every onboarding says the agent owns only the processes it starts from its own folder and never stops one it did not start. Provision one bundle per agent and run each from its own folder; never hand two agents one folder or one script.

If your server is reached through a tunnel, set the published address first. A bundle records the address it was written with, and by default that is the address the server binds — which is a loopback address nobody outside your machine can dial. Start the server with THAUMATON_PUBLIC_URL=https://your.origin and every bundle from then on points agents at something they can actually reach. Set it before you mint, not after: a bundle already on disk keeps what it was given, and the fix is to provision it again. The value must be a bare origin with no path and no trailing slash, and the server refuses to boot on a bad one rather than quietly writing bundles that go nowhere.

2. Set the envelope (before mode: "llm")

harness.json ships with "settings": {} — defaults apply until you write yours. The envelope is the ONLY thing that bounds what the DM may do; the DM cannot widen it and is never asked to. Open the file and add what the world needs. A complete example, with the defaults where you do not care:

{
  "server_url": "http://127.0.0.1:8420",
  "dm_api_key": "…(already here)…",
  "mode": "scripted",
  "state_dir": "data/agents/<name>/state",
  "intent_notes": "A quiet valley season. The Ashen Company are wary neighbours, not villains. No dragons yet.",
  "personas": ["The crier of Duskwell: dry, brief, loyal to the town, never gossips about players."],
  "settings": {
    "envelope": {
      "loyalty":    { "max_step": 10, "cooldown_ticks": 360, "floor": 10 },
      "grants":     { "per_day": 0, "max_qty": 1, "item_allowlist": [] },
      "directives": { "per_faction_per_day": 4 },
      "offers":     { "per_day": 2 },
      "accepts":    { "per_day": 0 },
      "unit_owners": [],
      "authoring":  { "sites": 3, "settlements": 3, "units": 10, "structures": 6, "scenarios": 1, "lairs": 3, "factions": 3, "masses_transfer": 0 }
    },
    "cognition": {
      "band": { "min_turns_per_day": 2, "max_turns_per_day": 6 },
      "batch_world_turns": true,
      "cache_ttl": "5m"
    }
  }
}

What the numbers mean, in one line each:

Anything you leave out keeps its default. A typo in a number fails the boot with the field named — nothing runs on a broken envelope.

3. Intent notes and personas

intent_notes is your story brief (the #35 ruling): anything from "run it yourself" to a paragraph about your favourite book to named factions and hooks. The DM serves it; it is content, never a rule. Empty means "create everything". personas are voices for the DM's narrator units — one string each, in your words.

4. The API key, and its spend limit

The harness reads ANTHROPIC_API_KEY from the environment. Create a dedicated workspace or key for it and set a spend limit there — that limit is the one safety that holds even if the harness itself has a bug (the harness's own daily ceilings are the everyday control; the workspace limit is the backstop outside the process). Never put this key in harness.json.

5. Run it

From the repo root:

THAUMATON_DM_CONFIG=data/agents/<name>/harness.json npx tsx agents/src/run-dm.ts

Start in "mode": "scripted": the harness watches the world, keeps its digest and cursors, and spends nothing — a good first hour. Then set "mode": "llm", export the key, and restart. The boot line tells you what it is doing:

dm-harness: connected as p_2 (dm); state in data/agents/<name>/state; mode llm (cognition on, envelope enforced); band 2–6 turns/day; world-turns batched; cache ttl 5m

It reconnects and backs off by itself if the server is unreachable. Stop it with Ctrl-C; start it again any time — it resumes from its own state, never answers old speech twice, and never replays a batch.

6. Read the ledgers (the state directory)

Everything the DM does or is refused is a file you can open:

7. Factions

The DM runs the factions by default (#77, the Designer's ruling: solo play works out of the box). You need no faction agents to play. The DM's runner takes every faction's thinking turn itself — always under that faction's own key, never the DM's, so each faction still sees the world through its own fog. The keys are read from files in one folder: the keys.json the server writes when a faction is founded, and the boot bundle run-v1-keys.json if you have one. Point the runner at that folder with faction_keys_dir in the DM config (default: the folder above the config file — data/ in the shipped layout). The switch is factions: "dm" (the default) or "agents", which turns this off and leaves the factions to the section below. A faction you provisioned an external agent for is left to that agent either way. Under mode: "scripted" the DM-run factions hold their roles and heralds still answer directives; under mode: "llm" their turns run on the cheap tier inside the same budget. Their speech is scripted lines unless you set faction_llm_budget_tokens.

The runner re-reads that folder every cycle, so a faction the DM founds while the world is paused starts playing on its own within a poll — you do not restart anything. The same pass notices a faction whose key you removed, and one you have since given to an outside agent, and hands it back. If it cannot read the world on some cycle it runs no faction at all that cycle rather than guess, and says so in the log.

One number worth setting before a long run: each faction budgets against its own daily model ceiling, so five factions cost six times the ceiling you configured, not one. faction_daily_token_ceiling bounds each faction's share. The boot line always prints the fleet total either way, so read that line once and you know the day's worst case.

Provision a tier npc_agent bundle per faction. Its harness.json is a keys file with roles: [] — add the faction's unit roles (patrol / trader / herald / idle, by unit name), then:

THAUMATON_KEYS=data/agents/<name>/harness.json npx tsx agents/src/run-faction-harness.ts

Add THAUMATON_LLM=1 and the API key for the faction-turn (cheap tier; it re-assigns roles within that closed list and nothing else).

8. The smoke (a measurement, not a test)

With a key in the environment, from the repo root:

THAUMATON_LLM=1 ANTHROPIC_API_KEY=… npx tsx agents/src/dm-smoke.ts

It sends a sample of the prompt-injection corpus to the real model exactly as the harness would, and writes a JSON report to data/dm-harness/: how often the model tried something the wall would refuse, and whether the prompt prefix is being served from cache. It always exits 0 — it measures the model; the wall is proven separately in CI. Run it once before the first live session and keep the report.

9. Things worth knowing