# Semantic protocol v1 A request names a method. It does not mix an action with a reply to a previous choice. All transports invoke the same validating C boundary. Creation returns an opaque `sessionId`: a 16-character URL-safe token minted from 96 random bits independently of the game seed. Pass it back on subsequent run operations and resume. It identifies a run; it is not an authentication credential. Treat IDs as opaque rather than depending on their length or prefix. ```json {"version":1,"method":"session.create","params":{"name":"Ada","seed":42}} {"version":1,"method":"game.move","params":{"sessionId":"g-...","requestId":"step-1","expectedRevision":0,"direction":"south"}} {"version":1,"method":"decision.answer","params":{"sessionId":"g-...","requestId":"answer-1","expectedRevision":1,"decisionId":"decision-3","answer":{"kind":"confirmation","confirm":false}}} ``` The exact request grammar is [request.schema.json](../protocol/request.schema.json). The exact supported methods and their descriptions are in [catalog.json](../protocol/catalog.json), also returned by `protocol.describe`. These artifacts and C validation data are generated together by `node scripts/generate.ts`; CI must reject stale generated files. Optional `harness_name` and `model_name` creation labels (up to 120 UTF-8 bytes) carry caller-supplied run attribution. They do not affect the world, identify an authenticated model, or become perceived game facts. Browser adventure metadata and input recordings retain them; the WebMCP `create` tool exposes the same fields. `session.create` accepts `{}`: C selects a random seed and generates a hero name, and the engine selects a compatible role, race, gender and alignment. Supplied fields remain explicit choices. An omitted name is derived from the seed using a separate mixer, without consuming the engine or host's global random stream. The concrete name and seed are recorded before `new_game`; resume reuses them. Generated names are ASCII, at most 31 bytes, and contain no identity suffixes. MCP/WebMCP use the navigation vocabulary generated from `protocol/agent.ts`: `session`, `observe`, `interact`, `use`, `travel`, `equipment`, `inventory`, `plan`, `lookup`, `respond` and `help`. The low library and NDJSON retain this document's precise dotted methods and tagged answers. Low JavaScript tool discovery uses underscores, such as `session_observe`. These are deliberately separate interfaces; obsolete MCP names have no aliases. MCP item actions take `itemId` and an optional `quantity`; omit both to request the engine's selection. A readable name is never accepted as an item ID. MCP answers use `respond({action:"answer", sessionId, decisionId, value})`. The adapter checks the exact standing ID before interpreting `value` using that question's kind. Every question includes nested `decision.reply` with the tool name, bound arguments and `valueSchema`, plus `decision.cancel` when cancellable. The low decision semantics are unchanged; the agent projection adds this syntax. See the [DecisionFrame target](#mcp-observation-presentation) for view binding and rollout scope. Confirmation values are booleans; text and directions are strings; choices are arrays of returned names/IDs; item values are `{itemId, quantity?}`; positions are `{x,y}` or the cursor commands. An answer is never inferred or selected. Actionless `interact` also returns `attempts` with MCP tool/argument templates beside the unchanged low-level cell facts. An offered call does not promise success or safety. ## Method families | Family | Methods | |---|---| | Discovery | `protocol.describe` | | Session | `session.create`, `session.observe`, `session.resume`, `session.close` | | Movement | `game.move`, `game.run`, `game.moveWithoutAttack`, `game.attack`, `game.wait`, `game.climb` | | Environment | `game.search`, `game.kick`, `game.open`, `game.close`, `game.pray`, `game.quit` | | Items | `game.pickup`, `game.eat`, `game.drink`, `game.wield`, `game.equip`, `game.remove`, `game.read`, `game.apply`, `game.drop`, `game.zap` | | Containers | `game.loot` | | Ranged and weapons | `game.throw`, `game.fire`, `game.quiver`, `game.swap`, `game.twoWeapon` | | Spells and skills | `game.cast`, `game.enhance`; inspect `observation.knowledge` | | Manual interactions | `game.offer`, `game.pay`, `game.chat`, `game.dip`, `game.rub`, `game.invoke`, `game.engrave` | | Automatic ground pickup | `game.configurePickup`; read `observation.automaticPickup` | | Continuation | `decision.answer`, `decision.cancel` | There is deliberately no generic `act`, raw key, travel-until-success, or menu reply method. Inspecting self/here/inventory is a projection of the observation, not a gameplay action. Unsupported actions are not advertised. ### Targets and items `move` takes eight compass directions. `climb` takes `up`/`down`. `kick`/`open`/`close` accept an adjacent `{direction}` target; omitting it offers a choice. `zap`, `throw` and `fire` additionally permit `"self"` and vertical aiming. Applying a tool accepts an item; tool-specific targets arrive as subsequent decisions. An item is `{id:"item-..."}` or a perceived-name query. Omission requests this command's selection flow; `offer` can first ask about floor corpses, and `wield` and `quiver` include the explicit `hands` option. No arbitrary first match, slot-letter fallback, hidden-property filter, or navigation is permitted. Eligibility does not imply safety. IDs are opaque and session-scoped. `drop({id, quantity})` accepts an explicit positive partial-stack count. Other initial commands reject counts; a standing item decision accepts them only when `counted:true`. Its observed stack quantity is the maximum. Omission preserves the engine default. Counts select engine objects, never repeated requests. Throw/fire execute one normal engine command, including any engine-controlled multishot; this is not a caller-selected volley count. Engine `getobj` decisions bind real object pointers to opaque references. They offer the ordinary whole-inventory `*` view rather than disclosing hidden eligibility rankings. The engine validates the actual attempt. Applying and reading therefore accept non-tool/non-book uses. Equipment includes recognized blindfolds, towels, lenses and meat rings. A perceived non-food-eating polymorph form permits broad non-food attempts without revealing hidden edibility. Initial item/direction arguments answer only their first matching selection; subsequent selections remain explicit. Floor sacrifice selections use engine object bindings, independently of genuine danger confirmations. `cast` uses the real learned-spell menu; spell targets, failure and costs come from NetHack. `enhance` uses the real earned-advancement menu. `invoke` activates an artifact power; the invocation ritual still uses explicit candle attachment, Candelabrum lighting, Bell application and Book reading. `offer` executes the sacrifice command and can produce the genuine ascended terminal result. These are separate operations, with no ritual or ascension macro. ### Decisions At most one decision is standing. Submit its exact ID and one answer: ```json {"kind":"item","item":{"id":"item-1"}} {"kind":"target","target":"self"} {"kind":"position","position":"help"} {"kind":"position","position":{"x":42,"y":10}} {"kind":"confirmation","confirm":false} {"kind":"choice","choose":[0,2]} {"kind":"text","text":"Elbereth"} ``` ### Directional movement and counts | Intent | Operation and parameters | Native command | | --- | --- | --- | | One ordinary step | game.move {direction} | lowercase direction | | Step without pickup or fighting | game.moveWithoutAttack {direction} | m + direction | | Force one attack | game.attack {direction} | F + direction | | Ordinary running | game.run {direction, mode:"normal"} | uppercase direction | | Stop on something interesting, including forks | game.run {direction, mode:"untilInteresting"} | g + direction | | Ignore corridor forks as stops | game.run {direction, mode:"pastBranches"} | G + direction | | Suppress pickup and fighting during any run mode | game.run {direction, mode, noPickup:true} | m + selected running command | | Counted search/rest | game.search / game.rest {turns:1..1000} | native counted occupation | All inputs also carry sessionId, requestId and expectedRevision. Running mode omits to normal; noPickup omits to false. False preserves current automatic pickup configuration; it does not enable pickup. Force attack remains distinct and cannot be combined with running. Counts belong to search/rest here, not a generic repeat parameter or a count of successful movement squares. Each call executes one native command and settles at its next engine input boundary, reporting actual turns and any standing decision. Runs may follow corridors and interact with peaceful creatures under engine rules. These modes are not safety guarantees. No client loop, automatic warning answer or resumed occupation is implied. Retry only the exact original request; free queries and resume cannot restart it. A `position` decision exposes the engine cursor and `mode` (`browse` or `select`). Answer with a compass direction to move the cursor, `help` for engine instructions, `finish` to finish at the cursor, or `{x,y}` to select a map square (x 1–79, y 0–20). These continue the standing map prompt; they are not hero movement operations. Detection scrolls remain suspended until an explicit finish, selection or cancellation. The decision and cursor survive close/resume with the pinned runtime. Low-level choice IDs are the returned **integers**, not keyboard letters. Each choice also has a readable `name` derived from its displayed label. These aliases can collide and are bound to the current question; MCP resolves them or returns candidate clarification, while the low protocol still accepts IDs. Headers are not options. Cancellation is `decision.cancel`, not an empty choice array, arbitrary text, or a confirmation default. It cannot undo time already spent. Wrong answer shapes and stale IDs do not advance the engine. No client should restart the initiating action to continue a pending decision. MCP/WebMCP also require the exact returned `decisionId` on answer and cancel; the adapter never substitutes a newer standing question for a caller’s old answer. Use the returned `decision.id` as `decision.answer.params.decisionId` or `decision.cancel.params.decisionId`; the standing neighborhood `inputGate.decisionId` matches it. An explicit `confirm:false` answers the engine's yes/no question; `decision.cancel` cancels the standing decision and reports `cancelled`. Both preserve any time the initiating operation already spent. Declining does not request the initiating action again. Observe/replan, then choose a new operation deliberately; a later reattempt can raise a new genuine warning with a different ID. Never convert repeated declines or an unrecognized decision kind into consent. Confirmation decisions may include `context: {action, direction?, itemId?}`. These are only fields from the initiating public request, retained across close/resume. Unspecified directions and name-selected or subsequently selected item IDs are omitted. Context does not infer a warning reason or promise safety; `about` preserves the original engine warning. No hidden trap, curse, nutrition prediction or teleport destination is added. See the explicit decline and deliberate-reattempt handlers in the [manual reference example](AGENT_BROWSER.md#manual-reference-consumer). ## Responses Every accepted gameplay operation/answer returns: - `version:1`, `sessionId`, `requestId`, `revision`; - `outcome`: action, status, actual `turnsElapsed`, `positionChanged`, effects, and an optional reason; - `observation`: turn, stable level identity, position, named vitals, perceived inventory, underfoot items, layered world, recent heard narration, and perception freshness; - `events`: ordered changes from this operation, not a drained global stream; - `decision`: a typed choice or null; - `ended` and `end`: an authoritative terminal result or null; - optional `error`, `storage` and `recording` diagnostics. Protocol/shape errors can contain only `version` and `error`, without an observation. New full semantic rejection frames retain the still-standing engine or item choice when its identity, knowledge and storage state remain safe; invalid answers do not silently dismiss it. They do not synthesize a choice for an unloaded world, unknown item knowledge or known failed storage. A diagnostic requiring resume takes priority over any choice in a previously recorded frame. Historical receipts—including older error frames—are returned unchanged, not repaired to look like today's current state. Do not assume an error means the operation definitely did not execute: `unknown`, `incompleteRequest` and storage diagnostics require explicit recovery. [response.schema.json](../protocol/response.schema.json) describes all surfaces, including discovery and error-only responses. New output properties may be added within v1; requests remain closed to unknown fields. Clients must not act on an unknown decision kind or outcome. ### Perception, not omniscience World cells layer remembered terrain and currently rendered occupants/objects. For perceived `openDoor` and `closedDoor` terrain, `terrain.orientation` is `horizontal` (frame runs east–west) or `vertical` (frame runs north–south). This is the frame/wall axis, not the direction of an open leaf or of travel. It comes from disclosed engine symbols, including perceived backgrounds beneath occupants, never unseen map structure or neighboring tiles. It is retained with terrain memory and omitted when unknown or when the terrain is not an intact door. Neighborhood and cell-action terrain expose the same orientation. `terrain.mark` is the engine's displayed character for that square when nothing covers it: no occupant, no object, the cell is present, and the display char is a single printable non-space ASCII character. Occupant and object `mark` are unchanged. A covering layer omits `terrain.mark`. Optional `cell.visible` reports the engine's current sight of that square. `false` retains remembered terrain; it does not imply an empty square or absence of a creature perceived through another sense. Older engine packages omit this field: clients must not guess visibility from distance or map updates. Darkness does not erase previously perceived terrain. Undiscovered terrain remains unknown, including on invisible squares. `terrain.freshness` in world, neighborhood and cell-action views is `current` when known terrain is in sight, `remembered` when known but out of sight, and `unknown` for undisclosed/dark terrain. World `terrain.knowledge` still records the terrain-memory layer; it is not a visibility test. A known trap is the world terrain overlay (`type: "trap"`); local views additionally retain a previously perceived base beneath it and include `hazards: ["trap"]`. Those layers need not have identical terrain types. Neither layer infers terrain from failed movement. World, neighborhood and `session.actions` share displayed occupant `kind`, `appearance`, `mark`, `color` and disclosed `attitude`, plus displayed `objects`. An already perceived boulder carries `objects[].kind: "boulder"`, independently of any `movement.intent: "possiblePush"` offer. This does not reveal whether a push will succeed or what is beyond it. Rendered weapons, rigid containers (chest, large box, ice box) and statues may also carry `category` and `known.appearance`. A statue may carry `known.depictedCreature`, the subject shown by its rendered glyph, not a living occupant. These facts describe the apparent shape, including disguises and remembered display; they are withheld during hallucination. Other generic objects have no inferred identity or category. Optional display fields may be absent on an older pinned package. The map is not a query of undiscovered level state. Replace the full observation on each response; never carry future terrain backward through a replay. Labels, marks and colors are presentation data, not object identity or game rules. Missing properties are unknown. `inventoryKnown`, `here.known` and `perception.{inventory,here,equipment}` distinguish current-at-boundary, last-known and unavailable information. Empty known arrays mean empty; unknown does not. Existing engine pins can have fewer perception capabilities. Health/energy/etc. are numeric where the engine supplies numeric values. Some legacy/formatted vitals can be strings; exceptional strength is not flattened. `vitals.hunger` is a canonical `HungerState`: `satiated`, `not_hungry`, `hungry`, `weak`, `fainting`, `fainted`, `starved` or `unknown`. `vitals.burden` is a `BurdenState`: `unencumbered`, `burdened`, `stressed`, `strained`, `overtaxed`, `overloaded` or `unknown`. Display padding and case are normalized; the engine's empty normal labels mean `not_hungry` and `unencumbered`. Missing perception stays absent and unfamiliar text is `unknown`. `hungerLabel` and `burdenLabel` preserve the original display strings. The public types are generated from the response schema. These states disclose no exact nutrition, prayer timer, piety or undiscovered intrinsic properties. `vitals.condition` uses the engine's self-state tests independently of optional visual status toggles. In particular, `held` appears when the hero is held (or swallowed), `grabbed` retains the engine's drowning-grab distinction, and `grasping` means a sticky hero is holding a monster. Acquisition and release are reflected at the next observed input boundary. These conditions do not identify an unseen holder. Bounded navigation stops when conditions change. An operation can spend many turns without another opportunity for input. `turnsElapsed` reports the complete interval to the returned input boundary, including sleep or paralysis. Read ordered `heard` and `felt` events to narrate disclosed loss and recovery of control; the final ready state does not mean the hero could act during those intervening turns. For example a sleeping-potion drink can include “You suddenly fall asleep!”, intervening turn events, then “You wake up.” Do not attribute every long interval to paralysis or infer a remaining-duration countdown. Multi-turn meals and interruptions retain their existing authoritative activity events. See [outcomes and safe recovery](SEMANTIC_OUTCOMES.md) for deliberate waiting, guarded waiting, time spent and uncertain receipts. `lifeSaved` is a witnessed event, not game over. Only engine terminal facts establish death, escape or ascent; process failure is never a victory or death. ## Revisions, retries and storage Every gameplay and decision request requires `requestId` and `expectedRevision`. Once recorded, the same ID and canonical payload return the original receipt. Rejections before reservation (for example, `staleRevision`) are not durable receipts and can be evaluated anew on an explicit caller retry. Do not infer reservation or authorize an automatic retry from the presence of a request ID. A changed payload under an already-reserved ID is an error. Known retries are checked **before** stale revisions, so a successful but timed-out operation remains retrievable. A retry receipt is historical and must not rewind a newer local observation. Revision is an interaction boundary, not turn count. A zero-turn choice may or may not change revision according to whether the core accepted new input. `session.observe` sends no input, spends no turn, changes no revision and consumes no events. It does not start or resume an unloaded game. Native sessions retain validated input journals, semantic metadata, request reservations, public receipts, and pinned engines. Resume replays the input with that pin and requires exact prompt/offer agreement before rebinding a pending choice. Profile 1 records a fixed UTC creation calendar and isolates user options/configuration; unprofiled history is refused without inventing settings. See [runtime profiles and replay limits](REPLAY.md). Prompt agreement alone is not a universal original-world equivalence proof. An exclusive lease prevents multiple owners. Missing receipts are uncertainty, not authorization to execute twice. Corrupt or torn history is not automatically truncated. Closing retires the engine; it does not delete or in-game quit. Interrupted meals/study return `outcome.status="interrupted"` from actual engine activity events. Observing, resuming or retrying a receipt never continues them: issue a new explicit `game.eat`/`game.read` for the remaining perceived item. A `game.wait` request can be refused near danger; zero elapsed turns with no progress are `blocked`, not a completed waiting turn or forced consent. Closing a loaded terminal world succeeds as a no-op if its engine already retired; it does not restart it or invent a view for an unloaded session. **Creation is not retry-idempotent.** Do not blindly repeat `session.create` after losing its response. Lifecycle methods do not share gameplay receipt semantics. Storage failure does not undo a deed: retain its request ID. `protocol.describe.capabilities` distinguishes native filesystem/fsync, isolated WASM memory with no durability, and explicitly requested IndexedDB transactions with origin Web Lock ownership. WASM resume requires the same package identity. See [WASM guarantees and tests](WASM.md). ## Limits and framing Native CLI: one UTF-8 JSON request per LF-terminated line, one JSON response per line; diagnostics only on stderr. Embedded NUL, malformed UTF-8, duplicate, escaped, unknown or inapplicable field names are rejected before engine input. No automatic retry occurs at the transport layer. Public request frames are capped at 4096 bytes; responses include full observations and can be larger. The current private semantic operation storage imposes an additional **4094-byte translated-request limit**; the C core rejects larger combinations explicitly. Individual limits are in the schemas: 64-byte session/item/decision IDs, 128-byte request IDs/text answers, 127-byte item-name queries, 31-byte identity strings, 64 distinct choice IDs, JavaScript-safe integer guards. `x-maxBytes` supplements JSON Schema character lengths for UTF-8 byte bounds. This release does not claim every original NetHack action, arbitrary legacy historical equivalence, automatic crash recovery, or general power-loss proof. The protocol stabilizes supported operations without inventing missing behavior. ### Perceived actions and neighborhood (resolver version 1) `protocol.describe.capabilities.affordanceVersion: 1` describes the library resolver. Engine support is separate: every newly emitted full observation carries `neighborhood`, either `available` or `unavailable` with `unsupportedPerception`, `unknownPosition`, or `recoveryRequired`. Old receipts are returned unchanged and may omit it. A resumed game uses its original pinned engine/package. `session.actions({sessionId, expectedRevision, target})` accepts `"here"`, an adjacent `{x,y}` coordinate, or `{direction: Compass}`. It returns an `ActionsResponse` (`kind: "actions"`), never an observation. It rejects a stale revision before resolving and never resumes an unloaded engine. It sends no engine input, consumes no events or random numbers, reserves no receipt and writes no storage. The SDK's `game.actions()` does not accept this result as a snapshot or clear an uncertain operation. Available neighborhoods have a shared `basis: {revision, levelId, origin}`, `inputGate`, radius 4 and exactly 81 row-major cells, dy/dx -4 through 4. The nine query targets equal the corresponding neighborhood cells. Playable bounds are x=1..79, y=0..20; out-of-map entries have no terrain assertion or actions and false walkability. Swallowing does not invent a surrounding normal map. `walkable` describes last-known terrain traversal for ordinary locomotion, including auto-opening a known unlocked door. It is independent of occupants, hazards, source-dependent movement restrictions, and permission to issue input. Unknown doors, water/lava and unusual forms return null conservatively. Manual movement remains available even toward false/null terrain. `movement` describes an adjacent attempt (including creature/ally bumps or possible pushes), and known intact-door diagonal restrictions. `requiresSqueeze:true` identifies a diagonal between two perceived rock/tree obstacles, without claiming the hero can or cannot fit. No success, safety or hidden squeeze test runs. ### Known walking routes `session.route({sessionId, expectedRevision, to:{x,y}})` (typed client: `game.route({x,y})`) is an optional, free query over the perceived current level. It returns `kind:"route"`, revision/level/origin `basis`, `inputGate`, the target, `policy:"knownWalking"`, `distance` and ordered `steps` excluding the origin. The origin has distance zero. A null distance and empty steps mean no route is known under this policy, not that the destination is physically unreachable. When distance is null, `why` is `targetOccupied`, `targetUnknown`, `closedDoor` or `disconnected` from perceived knowledge of that square. The shared C resolver computes the shortest path in steps, with a stable compass tie-break order. The policy excludes unknown terrain, closed doors (even known unlocked doors), hostile or peaceful occupants, boulders, known hazards, intact doorway diagonals, uncertain corners and tight squeezes. A displayed tame ally (`occupant.kind:"ally"`) is displaceable: ordinary movement swaps places under the engine's normal rules, so a route may pass through or end on its square. The swap is still an attempt; when the engine refuses ("You stop. … is in your way."), the step reports `positionChanged:false`. The policy allows remembered terrain but does not promise it remains unchanged. Unusual locomotion or unreliable direction returns `unsupportedMovement`. No path executes movement, opens a door, answers a question, resumes an unloaded run or changes a receipt. Inspect the input gate and re-query after an action; a route is a plan based on its revision, not a success guarantee. `session.navigation({sessionId,expectedRevision})` (`game.navigation()`) returns `frontiers`, `waysDown` and `doors` under the same policy and revision/level basis. Frontiers are reachable unvisited squares next to unknown cardinal neighbors. The C driver remembers actual disclosed standing positions by level, including the initial square; targeting cursor motion does not count. Pinned replay rebuilds this history. Known downward stairs are included even when their distance is null. Door entries carry remembered lock knowledge, the nearest reachable cardinal `approach`, its walking `distance`, and the named `direction` from that approach. Unreachable doors have null distance and no approach/direction. The query does not open them. Neither query searches an unseen level or spends input/randomness. The optional high-level [Navigator](HERO.md#optional-navigation) executes bounded travel/explore/descend legs. Route steps include C-resolved named directions. Native MCP and WebMCP expose the same navigation vocabulary. Exploration defaults to one frontier; `maxFrontiers` explicitly permits successive perceived frontiers within one total `maxActions` budget. Real decisions, interruptions, damage, newly perceived creatures and a door attempt stop the call. A failed later substep preserves confirmed progress; condition and hunger changes also stop continued navigation. Zero-action navigation reports zero turns without repeating the last input's events or receipt ID. Door locks come only from player-facing disclosures at the actual engine target. They are `unknown`, `locked` or `unlocked`, with independent `unknown`, `witnessed` or `remembered` freshness and an observation turn. Seeing a closed door again cannot refresh or invalidate remembered lock evidence. An observed replacement or opening clears obsolete closed-door advice. Tool Lock/Unlock confirmations disclose knowledge even on decline, but are never answered automatically. Bounded per-level records are reconstructed by integrity-checked pinned input replay, not hidden save state. Public `doorWitness` events carry level, coordinates, fact and turn. Offers use named methods only, with a closed availability union: `attemptable`, `uncertain`, `needsSelection`, `outOfReach` or `knownBlocked`. The last two carry no runnable arguments; `knownBlocked` requires a reason. Tool selection uses current perceived classes, never hidden powers or guessed identity. `context` is explanatory and must not be passed to `game.apply`; item, direction and confirmation remain separate genuine decisions. Cost is variable, not a promised turn count. SDK named methods accept optional `{expectedRevision}` as their final argument. Copy the offer's basis revision when opening a control; queued operations preserve that revision and copied arguments. Omitting it retains ordinary sequential input. The session-wide input gate takes precedence over all offers: a standing decision allows only its answers/cancellation, and uncertainty requires receipt recovery. Storage diagnostics always override a historical input gate. A failure while publishing/checkpointing a completed input can be discovered after its exact receipt was formed. Preserve that receipt's neighborhood unchanged; do not rewrite it on the original reply or later retries. A degraded `storage`/`recording` result blocks new operations regardless of the receipt's old gate. A fresh observe/query reports the current recovery requirement, and the SDK refuses discovery while it has an unresolved request. ### Perceived creature appearance `occupant.appearance` in world, neighborhood and cell-action views, when present, names the monster type represented by the engine's displayed glyph (for example `kitten` or `newt`). This is available without spending a turn or attacking. It is an apparent description, not proof of a shapeshifter's true form, an unseen monster lookup, or an entity ID. It is omitted during hallucination and by older engine packages. Clients must not infer it from message prose, symbol/color pairs or remembered occupants. Refresh it from each observation; absence must clear an earlier description. Saved games continue to use their pinned package and may lack this optional field. `game.quit` requests NetHack’s own quit confirmation. Declining keeps the run active; an explicit affirmative decision answer ends it. Its journal and terminal state remain available on resume. `session.close` only unloads a session. The current cell's action offers include item eligibility computed by the same C candidate resolver used by operations. `knownBlocked` with `noPerceivedItems` means a client can disable that action without sending input. Unknown or stale knowledge remains `uncertain`, not a claim that no item exists. Eligibility does not imply safety: curses, unknown potion effects and warnings remain engine decisions. `game.drink()` on a perceived fountain or sink underfoot asks the engine's genuine confirmation even without carried potions. Adjacent water features do not qualify. Current inventory and underfoot item references include optional `actions`: named candidate actions computed by the shared C item resolver, using the same perceived class and equipment accessibility as explicit operations. The list is omitted for stale perception. It is eligibility, not a safety guarantee or an input gate; clients must still respect decisions, recovery and the observation revision, and send the opaque item ID unchanged. No hidden curse, potion effect or corpse safety is exposed. `game.loot()` opens perceived containers on the current square through NetHack's loot command. It requires current floor perception and a recognizable container; it never substitutes an adjacent creature interaction or picks up the container. Multiple containers, inspection, transfer selection and warnings remain engine decisions. The neighborhood offers `game.loot` only underfoot; container appearance does not disclose contents, locks, traps, or magical powers. Contents become available only through the engine's explicit container choices, not the ground inventory. Answer those choice IDs, rather than starting a second operation or reusing ground item IDs for contents. Container choices carry `containerPhase: "inspect" | "transfer"`. Inspection of unknown contents is explicit and costs time; known contents open directly into the transfer decision. Transfer options carry `transfer: "take" | "put"`, authored by C from the actual object binding. Clients may stage both groups locally and submit their choice IDs together, including selecting all take options. Each selection moves the offered whole stack. Taking happens before putting; putting uses the originally offered amount even if a taken stack merged into it. Original engine transfer rules, capacity/shop confirmations and interruption apply. This is one reviewed plan, not an atomic rollback transaction. Cancellation does not undo inspection or transfers already performed. Unsubmitted UI drafts are not engine state; resume restores the exact standing decision with no selected items. ### Automatic ground pickup Creation accepts `automaticPickup`; `game.configurePickup({automaticPickup})` replaces the complete configuration at a free command boundary. TypeScript uses `game.configurePickup(settings)`. The C convenience API uses `nnh_automatic_pickup`, `nnh_identity.automatic_pickup`, and `nnh_game_configure_pickup`. The object has five required fields: ```json {"enabled":true,"itemTypes":["gold"],"arrows":true,"leaveCorpses":true,"leaveKnownCursed":true} ``` Types are gold, food, potions, scrolls, weapons, armor, rings, amulets, tools, spellbooks, wands, gems, rocks, balls and chains. Deliberately select all names for all categories. An empty array means no categories; C maps it to NetHack's nonempty venom sentinel, so only an enabled arrow or loot-pattern inclusion can collect items. Off preserves the configured filters. The pixel client defaults to enabled Gold + arrows; omitted library creation settings leave automatic pickup off, with gold/arrow filters ready to enable. No client defaults are injected on resume. The settings arrive before engine game creation and initial pickup. Updates spend zero turns but advance revision. Both use the input journal, pre-input reservation, and durable receipts. Read actual active settings from the observation, including after resume. An outstanding decision must be answered/cancelled before updating. Never retry an uncertain update with a fresh request ID. Both native and WASM use POSIX extended regex matching against NetHack's singular perceived description. Arrow inclusion uses `(^| )arrow( named .*)?$`, corpse exclusion uses ` corpse( named .*)?$`, and known-curse exclusion uses `^(an? |the |[0-9]+ )?cursed `. Exclusions are inserted last because the engine prepends rules and uses the first match. The curse rule only matches disclosed curse wording, never hidden object flags. Recovery overrides `pickup_thrown`, `pickup_stolen`, and `nopick_dropped` are deliberately disabled: recovered, thrown and dropped items follow these same filters. Shop exclusions, weight rules, engine warnings, and explicit manual pickup remain intact. These settings never open or transfer a container. Optional `lootPatterns` and `ignorePatterns` arrays each accept up to 16 nonblank literal substrings (1–64 UTF-8 bytes, no control characters). Matching uses NetHack's case-insensitive substring search against the singular perceived item name, including disclosed modifiers and user-assigned names. It does not interpret regex or wildcard syntax, nor match undiscovered true identities. Loot matches add to category/arrow inclusion; ignore matches and leave rules override all inclusions. Omitting either list clears it when replacing settings. Patterns are retained in observations, creation inputs, configuration receipts and replay. For example: `lootPatterns: ["ration", "dagger"], ignorePatterns: ["corpse", "cursed"]`. Clear `itemTypes` and disable `arrows` to select using loot patterns alone. Optional `review: true` pauses matching automatic pickup at a real choice before transfer. `decision.pickupReview` marks that menu and each option carries a `suggested` boolean from the configured filters. No choice is preselected. The caller can override suggestions or cancel before issuing a different action. Omitted review resets it to false on replacement; configuration and pending review are retained through the existing journal and exact receipt machinery. Public `itemLooted` events witness actual acquisition, including transferred quantity, resulting inventory stack ID/size, floor/container/engulfer source and container when present. `containerOpened` describes an actually inspected container and its disclosed contents, never a failed locked/trapped attempt. These witnessed outcomes cannot be vetoed; the standing transfer decision can. They are exposed identically through native, WASM, TypeScript, MCP and WebMCP. ## MCP observation presentation This section specifies the source MCP contract. Runtime schemas establish build support; this is not a deployment claim. The C/low TypeScript/WASM protocol and exact durable receipts retain their existing semantics. Bun/WASM stdio/HTTP MCP and WebMCP share one agent projection, schema and target resolver. Every game-scoped result uses a version-2 `DecisionFrame`. The following is a field outline, not a captured response or a replacement for generated schemas: ```text DecisionFrame { version: 2, basis: {sessionId, revision, viewId, levelId, turn} | null, control: {gate}, outcome: { submission, status, actionsTaken, turnsElapsed, positionChanged, sceneChanged, operationId? } | null, scene: { kind: "snapshot", self, nearby, entities, features, map, inventory, here, automaticPickup, perception, world? } | null, decision, // null when absent; nested reply and permitted cancel when present events, result?, // query data, not another current-state envelope error?, diagnostics } ``` There are no legacy top-level `state`, `observation`, `messages`, `context` or `reply` fields. Canonical self-state lives in `scene.self`; query-specific data lives in `result`. Basis/scene may be null when no current session view is available, including failed session entry. Never fabricate a current scene from a historical receipt or replace unavailable knowledge with an empty list. Admission, outcome and input authority are distinct. `outcome.submission` distinguishes confirmed acceptance, rejection and uncertainty; `status` describes what actually happened. `control.gate` distinguishes readiness, a standing decision, required recovery, lost ownership and an ended game. Elapsed time and position are not execution proofs. Integrity revision advances at accepted interaction boundaries, including zero-turn decisions; `viewId` binds an agent's perceived view, including its question. ### Scene completeness and events Routine replies contain compact but self-contained `scene.kind: "snapshot"` views sufficient for the next decision, including nearby attempts, perceived entities/features, inventory, underfoot knowledge, automatic pickup, perception and a bounded map. They require neither model-side delta merging nor a mandatory action–sync loop. `observe({sessionId})` additionally returns full `scene.world` for cell-layer inspection or resynchronization. It costs no game turn or randomness. Terminal details belong in `scene.end`; `control.gate` still determines whether further input is allowed. Preserve `inventoryKnown` and perception freshness rather than equating an empty list with known absence. The evolving catalog also defines optional programmatic encoding controls: `includeWorld` requests full world and `sinceViewId` requests a delta against the last sent baseline. These are separate from the default snapshot examples above, not different gameplay profiles. A delta carries `baseViewId`; a shared reader must validate session, level and base before applying changes and removals. A missing sender baseline returns a snapshot; a missing or out-of-order receiver baseline requires resynchronization. `sinceViewId` does not replace the mutation's authorizing `viewId`. Check the generated response schema and matching reader before opting in. Schema availability is not evidence of completed integration tests. Pagination and proposed byte/latency targets are not guaranteed here. `scene.map.text` draws perceived display marks with x/y rulers; `scene.map.legend` describes their disclosed meanings. Bounds, hero position and the symbol-position index avoid manual column counting. Duplicate marks keep all matches. A glyph is presentation, not identity, safety or a lossless encoding of terrain, objects, occupants and remembered hazards. Keep structured layers and their freshness; missing knowledge never becomes inferred floor. `events` carries fresh witnessed evidence for the call. Navigation can aggregate confirmed substeps; response-boundary turn labels do not establish every internal event's turn. Read-only sync does not replay rolling narration as fresh events. Enrichment failure must preserve the actual outcome and mark unavailable data without adopting another revision, retrying gameplay or clearing a question. ### Free discovery and strict variants The eleven families and examples are in [agent play](AGENT_PLAY.md). Actionless use discovers current perceived item actions; actionless interact queries a perceived square (self by default). Neither submits engine input, spends turns/randomness or changes the standing decision, even with one offer. Explicit action executes only the requested operation. No label-based verb inference or automatic question chaining. General supported commands are labelled separately from C eligibility. Discovery offers include executable family arguments bound to the returned viewId. Unavailable item perception withholds offers instead of inventing capabilities. Family schemas are strict unions, not interchangeable optional parameters. Mixed query/mutation tools advertise readOnlyHint false; readonly connections publish and enforce query-only variants. Grouping does not change low semantics, exact receipts, source pins or action costs. ### Targets and decisions Gameplay mutations against a live view require explicit `sessionId` and `viewId`. Session variants (action create, resume, suspend) do not require `viewId`. `respond` actions answer and cancel use the exact returned `decision.id` instead. Stale views and ambiguous targets submit no input; an adapter must not refresh then silently retarget the same intent. The shared operation target grammar is `{direction}`, `{position:{x,y}}`, `{ref}` or `{symbol,scope:'adjacent'|'level'}`. Here/self uses `{direction:'self'}` where permitted. Each tool advertises its valid subset and spatial restrictions. Compass words are not vectors. Resolve symbols only in the captured perceived view and explicit scope: zero matches is not found, multiple matches is ambiguous. Do not select the nearest match or discard a tame ally to manufacture uniqueness. View-bound creature refs do not assert persistent identity after disappearance. `travel({sessionId,viewId,mode:"step",target:{direction:"east"}})` is one adjacent ordinary attempt; `travel({sessionId,viewId,mode:"to",to:{x,y}})` is destination-only navigation. Keep tame-ally swaps, genuine warnings and deliberate `interact action attack` distinct. Neither eligibility nor route policy guarantees safety against unseen hazards. Read-only planning is separate from explicitly requested bounded execution. The first standing `decision` includes `decision.reply.arguments`, `decision.reply.valueSchema` and permitted `decision.cancel` syntax. Use the exact bound question ID and explicit value, preserving option IDs, cardinality and quantity rules. No defaults or replacement-question substitution. Operation targets and contextual answer values are different schemas. Item actions use opaque top-level `itemId`; at the low level an opaque item reference remains `{"item":{"id":"ground-20"}}`. Neither an inventory letter nor a decision choice ID substitutes for an item ID. ### Recovery and historical evidence `observe` checks a retained uncertain operation's exact receipt with read-only queries, then returns the current scene and decision. It never resends gameplay or continues a leg. Missing or mismatched verification keeps input blocked. Normal confirmed navigation stops and rejected arguments need no recovery ritual. There is no MCP `recover` tool. MCP `observe({sessionId,operationId})` is historical query data in `result`, never a replacement for the frame's current scene or decision. Historical questions have no executable reply/cancel suggestions. The low `session.receipt` response remains exact. Retain operation and bridge invocation IDs; a lost outer reply must not cause the adapter to guess which historical operation to verify. ### Perceived attitude and normalized hunger `occupant.attitude` in world, neighborhood and cell-action views is optional: `hostile`, `peaceful`, or `tame`. The shared headless engine discloses it only for a currently visible, spotted, undisguised creature while not hallucinating or swallowed. Omission means unknown, not hostile. Remembered glyphs and hidden monsters disclose no attitude. Like appearance, this is a boundary observation rather than persistent identity. The C semantic driver strips terminal padding from hunger/burden words. A received blank hunger status means `not_hungry`, as displayed by the engine; an absent status remains unknown. Clients must not derive numerical nutrition from it. The [Hero facade](HERO.md) uses these public observations and existing named operations; it does not extend the request protocol or infer item identities. ## Player-known information and final results `observation.knowledge` contains engine-disclosed information at `observedTurn`. Check `observation.perception.knowledge` before treating it as current. It is omitted when unavailable. Pure observe/actions queries neither refresh engine knowledge nor consume input or randomness. - `spells`: known name, level, category, ordinary menu failure percentage and rounded retention text. Raw memory counters and unknown spells are absent. - `skills`: displayed skill rank and advancement state (`available`, `needsExperience`, `peaked`, `practice`). No hidden practice counters. - `levels`: remembered dungeon overview entries with canonical level IDs, branch/depth, existing annotation and remembered feature counts. Counts are the overview's bounded values: **3 means three or more**. Forgotten levels are omitted. This is not the current unseen map or a portal/trap oracle. - `conduct`: engine counters for food, unvegan, unvegetarian, gnostic, weaponHits, kills, literacy, objectPolymorphs, selfPolymorphs, wishes, artifactWishes, sokobanViolations and pets. - `achievements`: engine-issued, non-spoiler achievement names and stable IDs, including attained rank titles and final roleplay achievements. Hidden prize identities are withheld; this is not the entire interactive chronicle. Inventory and perceived floor objects have a `known` object. Individual fields are omitted when unknown: perceived `appearance`, a statue’s perceived `depictedCreature`, identified `identity`, known `beatitude`, identified `charges`/`recharges` or `enchantment`, and known `erosionProof`. Hallucination withholds these properties. Absence of a curse/charge field means unknown, not uncursed/empty. Nutrition, prayer timers, hidden traps and unseen monster state are never supplied by this projection or candidate filtering. `end.score`, when supplied, is the final score calculated by the engine after its terminal scoring phase, including its real bonuses. No score is synthesized from turn count or experience. A terminal observation retains the last live perception plus engine final knowledge; postmortem identification does not leak back into the live inventory. Repeated queries and exact lost-response retries retain the same terminal result. Clients can summarize the genuine outcome, score, achievements, conduct and remembered places without inventing progress. See [COMMAND_COVERAGE.md](COMMAND_COVERAGE.md) for tested scenarios, intentional omissions and the distinction between endgame fixtures and a completed run. Text decisions may include `purpose: "consumedPotionNickname"`. This explicit engine context means drinking has occurred without conclusively identifying the potion type, and the engine is offering an optional user label for that type. Display the received `heard` events as the witnessed effects; do not infer the potion's true identity. Answer with the normal text decision contract or cancel naming. Cancellation does not undo consumption or its consequences. Other text prompts do not acquire this meaning from the initiating action or prompt wording. ### Perceived item appearance Item `known.appearance` is the engine's undecorated appearance description once that appearance is known (`dknown`), omitted during hallucination. It uses the current shuffled object description, or the base name for types without one. It is independent of `known.identity`: riding gloves remain riding gloves when their magical identity is unknown. Names, nicknames, charges and enchantments do not enter this field. It is presentation information, never an operation target. ### Inventory equipment slots Inventory items expose optional `equipmentSlots: EquipmentSlot[]` alongside `usage`. These are actual observed assignments from the engine, not places the item could be equipped. `[]` means no assigned slot; omission means unknown. Read `observation.perception.equipment` for `current`, `lastKnown` or `unknown` freshness. Ground items do not carry this inventory-only field. | Slots | Meaning | | --- | --- | | `shirt`, `bodyArmor`, `cloak` | Separate clothing layers, including covered layers | | `helmet`, `gloves`, `boots`, `shield` | Worn armor positions | | `leftRing`, `rightRing`, `amulet`, `eyewear` | Worn accessories; eyewear includes lenses, towels and blindfolds | | `weapon` | Primary wielded item, which need not be a weapon | | `offhand`, `alternateWeapon` | Secondary item in active two-weapon use, or stored for swapping, respectively | | `quiver` | Readied ammunition/item | | `skin` | Armor merged into the hero's polymorphed skin, separate from ordinary body armor | | `ball`, `chain` | Attached punishment objects, if present in inventory | Arrays support multiple actual assignments. Two-handed wielding does not invent an `offhand` assignment. Empty assignments do not imply the hero has an available body part, and worn slots do not promise successful removal. Known placement is independent of item identification and remains available during hallucination. Artifact carrying/invocation property masks are never equipment slots. For an equipped shield, for example: ```json {"id":"item-17","label":"an uncursed +3 small shield (being worn)","location":"inventory","quantity":1,"category":"armor","usage":["worn"],"equipmentSlots":["shield"]} ``` The TypeScript clients export `EquipmentSlot` and `ItemRef`. The slot vocabulary and driver lookup table are generated from the response contract; native JSON, WASM, MCP and WebMCP preserve the same inventory fields. Free observation does not change assignments or spend a turn. Equipment actions retain their existing explicit decisions, revision checks and opaque item references. ### Perceived equipment destinations and passages Current carried items may expose `equipmentTargets: [{slot, action}]`. These are physical destinations for named attempts, derived in the engine from perceived shape and current use. They do not disclose hidden curses, magical identity or predict that wearing will succeed. An occupied ring hand is not offered as a destination; removal remains a separate explicit action. `game.equip` accepts optional `slot` with an explicit item. A mismatched destination is rejected before input. A selected ring hand supplies that particular hand choice; omitted hand choices, warnings and subsequent unrelated decisions remain explicit. No equipment is automatically removed. TypeScript: `await game.equip({id: item.id}, {slot: 'leftRing'})`. The C convenience is `nnh_game_equip_at`; ordinary `nnh_game_equip` remains the slot-unspecified form. `passage` events carry the exact text of a displayed engine text window, including paragraph breaks. Associated `heard` lines have `textWindow: true`; consumers can show the passage once instead of duplicating those lines. Ordinary messages remain `heard` events, regardless of their length. Text windows are presentation facts, not inferred quest significance. Opening a client reading panel costs no input. ## Encyclopedia lore `session.lookup({sessionId,name})` returns `{kind:"lore",name,found,lines}` with the protocol version and session ID. A live, resumed engine uses its pinned encyclopedia and actual typed-name matcher. A missing entry returns `found:false` and an empty line list. Matching follows the engine’s rules, including their limits; it is not a general search engine. This reference channel does not identify anything on the map, reveal hidden properties, or classify safety. It performs no gameplay input, journal append, revision change or RNG draw, and leaves the standing decision unchanged. The query uses an out-of-band engine response while input remains suspended. A lost or malformed transport reply retires that runtime so a late query response cannot be mistaken for the next gameplay response. Resume replays the intact input journal and restores its standing decision; a failed free query does not invent an uncertain gameplay input. Current runtimes also compare the reply's private RNG evidence with the last engine input boundary. A mismatch retires the runtime without exposing that evidence through the public result. ## Counted search and rest `game.search` and `game.rest` accept optional `turns` (integer 1–1000, default 1). This is NetHack’s native command count, submitted as one journaled input and handled by its timed occupation logic. `game.wait` remains a single waiting attempt. Engine safety refusals, discoveries, approaching monsters, hunger and other interruption rules remain in effect; no warning is automatically answered. The count is not a promise about elapsed world time: hero speed or involuntary delays can make it differ. `outcome.turnsElapsed` reports actual elapsed time. Execution witnesses distinguish completed and interrupted occupations from the requested count; a refused search does not emit `searched`. Standing decisions, including client-facing item selections, block a new occupation. Exact request retries and close/resume retain the original counted command and receipt. ### Witnessed creature deaths `creatureDied` is an action-local visual event with `levelId`, `x`, `y`, `turn`, and optional perceived `appearance`. It is emitted after lifesaving and form-recovery checks, before hidden true-form restoration. It requires direct visibility of the creature and its square; unseen, disguised, undetected, hallucinatory and swallowed scenes do not disclose it. Disappearance and `saw` events are never death evidence. The event does not promise a corpse object, loot, a particular cause or a stable monster identity. Clients may show a labelled cosmetic aftereffect; actual items remain authoritative.