Wiring, long form
Everything a developer does to put a mind in a character, in the order they do it. The Characters SDK says what a character is; this page says what your engine sends, what it gets back, and what it must execute. Two hours for a first character, if your engine can already move an actor to a point and make it say a line.
lw dev starts the brain on the developer's machine. Your engine talks to one socket:
ws://localhost:8765 the broker — JSON messages, one per line, both directions
Your engine sends the state feed on it, several times a second. The brain sends back commands on it: move this character like this, say this. That is the whole wire. Everything else (perceptions, orders, receipts) is HTTP on lw serve and is optional at first.
If your engine cannot hold a socket (Papyrus cannot), copy the Skyrim adapter's pattern: an HTTP shim on localhost:8766 that takes POST /state and answers with every command queued since the last post. The post is the poll. pipeline/skyrim_bridge.py is that shim, and it is about 500 lines.
One message per tick, 3 to 10 Hz. Positions are in your engine's units on a horizontal plane (x, z); the brain never reads your navmesh.
{
"type": "state",
"tab": "my-game", // one id per running game instance
"world": "my-game", // MUST equal the manifest's "world"
"player": { "x": 0.0, "z": 0.0, "armed": false },
"npcs": [
{ "i": 0, "name": "Hulda", "x": 8.0, "z": 4.0, "handle": 1000,
"armed": false, "activeExpert": "idle",
"identity": { "klass": "CitizenInnkeeper", "city": "Whiterun" }, // optional: seeds an archetype
"location": "The Bannered Mare", "loctype": "inn", "interior": true // optional: place tags (§02)
}
],
"objects": [ { "label": "well", "x": 3.0, "z": -2.0, "kind": "furniture" } ] // optional: things a plan can go to
}
| field | required | what the brain does with it |
|---|---|---|
| npcs[].i | yes | the slot index commands come back on; stable while the character is present |
| npcs[].name | yes | the character's identity: journal, manifest characters, receipts. Nameless crowd rows may have name: null |
npcs[].x, z | yes | distances: to the player, to the guard, to allies, to stations |
| npcs[].handle | recommended | your engine's stable id, so renames and slot shuffles never lose a mind; commands carry it back |
| npcs[].armed | recommended | the threat rung: a drawn weapon near a character outranks everything |
| npcs[].activeExpert | recommended | what the body is doing now (idle, or the last command's expert); lets the brain not repeat itself |
npcs[].identity.klass / .guard | optional | seeds the archetype (guard, owner, elder, child, drifter) when no persona is authored |
npcs[].location, loctype, interior | optional | the place tags never rules use (place.temple, interior) |
| player.armed | yes | the same threat rung, from the player's side |
| objects[] | optional | labelled things: a plan step can navigate to them; the talk lane can refer to them |
The brain keeps nothing you did not send. If a field is missing, the rule that needs it evaluates to false and the character simply does not consider it.
Every command carries npc (the slot) and name; when you sent a handle it carries that too. There are two you cannot skip.
loadScenario — a body movement. modes[].expert is one of the engine actions in the vocabulary (the table in §5). Execute it against the target, keep executing it until the next loadScenario for that slot, and report it back as activeExpert.
{ "cmd": "loadScenario", "npc": 0, "name": "Hulda", "handle": 1000, "title": "plan:report",
"target": "player",
"modes": [ { "cond": "default", "expert": "navigate-to", "target": "guard" } ] }
target is "player", another character's name, or absent when modes[].focus gives a point [x, y, z].
say — a line, aimed at someone.
{ "cmd": "say", "npc": 0, "name": "Hulda", "text": "Guard. I've been robbed.", "title": "plan:report" }
Render it however your game renders speech. It is already the character's decision; do not filter it.
Commands you may ignore at first: castRole, setSituation, setGoal, toast, buildScene. They belong to scenes and the Architect (Characters SDK §12) and a game that never uses those features never receives them.
Your level ships lw.manifest.json (lw init writes one). Its world must equal the feed's. Each verb names the engine action it maps to, and your adapter declares which engine actions it can execute. At start, ask for the report:
python -m pipeline.verb_manifest --check lw.manifest.json --executes approach,evade,navigate-to,say --adapter my-game
or POST /v1/manifest/check on lw serve. It prints one line: every verb executable here, or CANNOT execute here: sit (sit), lean (lean). A verb your adapter cannot execute is still planned and spoken; your adapter no-ops it. Fix it by binding the engine action, not by editing the brain.
| engine action | what your engine must do | verbs that map to it |
|---|---|---|
| approach | walk to within speaking distance of the target and face it | greet, serve, investigate |
| navigate-to | walk to the target or point; stop there | report, go_station, seek_station, buy_from, ask_friend |
| follow | stay within a few units of the target as it moves | guard_player |
| guard | hold position, face the target, keep eyes on it | watch |
| evade | move away from the target, fast | flee |
| retreat | back off a few steps, then hold | call_help, avoid |
| confront | close to arm's length, square up, hold | confront |
| lead | walk ahead of the target toward a point, waiting when it lags | lead |
| say | speak the line; no movement | warn, complain, demand, refuse_service, gratitude, claim_task, resume_task, say |
sit, eat, lean | use the nearest matching furniture | sit, eat, lean |
| wander2 | roam locally | sandbox |
A first adapter needs approach, navigate-to, evade, retreat, guard, follow, confront and say. That is enough for every reflex and every shipped plan; the rest are bodies you add when your level lists the verb.
The brain does not see your world. You tell a character what it witnessed, in the first person, in your words:
POST http://localhost:8767/v1/perceive { "char": "Hulda", "text": "the newcomer took a purse of 40 septims right off me" }
lw serve is that endpoint; without it, send {"kind": "perceive", "char": "...", "text": "..."} on the socket. The gate classes the line (theft, attack, threat, slight, debt, gift, amends, need, or ambient), stamps its weight, and the deliberator decides whether it is worth planning about. Your engine's hooks are the senses: on-hit, on-steal, on-gift, on-insult, each one line. Phrasing does not matter; meaning does. Level-specific phrasings you want classed a fixed way go in the manifest's events section (Characters SDK §02).
POST /v1/command { "char": "Hulda", "text": "follow me" }. A two-minute lease that a strong enough grievance can break, aloud (Characters SDK §11). Step by step: Give orders.GET /v1/receipts streams every decision as it lands; lw watch and lw why read the same stream. Wire it to a debug overlay and you can see a character's mind while you play.localhost:8094 (POST /start, then POST /turn { session_id, player_input }; the reply streams as server-sent events, the line arrives in response_generated.text). Bring your own model or none: the lane is handed the brain's memory either way, and anything it invents is rejected before it is spoken.In a shipped build the local half is the same code, pointed at the hosted deliberator. lw bundle --target windows|mac --build-key <key> packs it as one folder (embedded Python, the embedder, your weights, the manifest, a launcher) that your installer carries and your game starts with lwbrain --parent <pid>; the player installs nothing. The shipped brain wakes with a build key (lw licence buildkey create), never your licence key. Reflexes, perception, memory and running a plan happen on the player's machine; one small request per wake goes to the service. Nothing else leaves the machine. Pull the cable and the town keeps living; it stops forming new long plans until the wire is back. Step by step: Ship with a licence.
living-world-web3/src/main.js — opens the socket, posts the feed from the scene, executes loadScenario and say on the meshes.pipeline/skyrim_bridge.py (the HTTP shim) with the Papyrus side in the mod; the place tags come from the engine's location keywords.dist/unity/ — a C# runtime that holds the socket, dispatches the command vocabulary and posts the feed; see its README for the install.pipeline/skyrim_mock.py sends a feed and executes commands with no engine at all. Read it as the smallest possible adapter: it is what lw dev --mock runs.