# The Yendorbound Agent

> **Objective:** YBA is scored by the **(HP+Pw)-Depth Peak (HDP)** — run games
> in terminal mode (100k-turn safety cap) and score each complete game by
> `max over actions of ((current-HP + current-Pw) x absolute dungeon depth)`.
> This behavior-inert north-star
> supersedes turn-survival. See **`OBJECTIVE.md`**.

**The Yendorbound Agent (YBA)** is Teleport's autonomous JavaScript NetHack
player. It began as a port of **AutoAscend**, the first-place bot in the
NeurIPS 2021 NetHack Challenge (MIT, Maciej and Michal Sypetkowski,
<https://github.com/maciej-sypetkowski/autoascend>). It has since evolved into
a distinct agent with durable goals, observation-derived affordances,
materialized action proposals, shared consequence assessment, and a
deterministic evaluation system.

The source package is `yendorbound/`, the primary class is
`YendorboundAgent`, and the play/capture command is `yba` (or
`node scripts/yba.mjs` from a checkout).

## Why this exists

1. **Build a strong autonomous player.** The immediate objective is reliable
   ascension through fair, deterministic play.
2. **Exercise the public engine boundary.** YBA sees decoded player knowledge
   and emits ordinary commands. It does not read hidden engine state.
3. **Make strategic changes measurable.** Fixed per-level reseeding, paired
   cohorts, causal traces, and independent holdouts separate policy value from
   dungeon-layout noise.
4. **Retain verifiable provenance.** The pinned AutoAscend source remains a
   useful behavioral reference, but YBA's current architecture and policies
   are owned here.

**Design docs:**
- [ARCHITECTURE.md](ARCHITECTURE.md) — the long-term decision architecture:
  goals, methods, proposals, one arbiter, and the migration from today's
  overlapping directive and plan mechanisms.
- [PORTING.md](PORTING.md) — the AutoAscend source repo, how that bot works, and
  the component-by-component plan for porting it onto our API.
- [EPISTEMOLOGY.md](EPISTEMOLOGY.md) — the fairness boundary: every assertion
  about what the observation may or may not reveal, grounded in engine `file:line`.
- [COMMANDS.md](COMMANDS.md) — the command API: the uniform primitive + the
  argument conventions.
- [ACTION_MODEL.md](ACTION_MODEL.md) — tick-scoped, observation-derived
  affordances and why they are not an action catalog.
- [DIVERGENCE_HUNTING.md](DIVERGENCE_HUNTING.md) — using the agent to auto-play
  diverse sessions and surface JS↔C parity divergences (the regime, the
  session-is-the-repro rule, and the triage→issue workflow).
- [SURVIVAL_CAMPAIGN.md](SURVIVAL_CAMPAIGN.md) — the current survival-improvement
  hypothesis: use emblematic deaths to find small systematic policy fixes, then
  test whether they compound on fixed and holdout seed panels.
- [SURVIVAL_PROGRESS.md](SURVIVAL_PROGRESS.md) — chronological measurements for
  that survival campaign.
- [EXPOSURE_HYPOTHESIS.md](EXPOSURE_HYPOTHESIS.md) — the current early-game
  objective-throughput hypothesis. After first reaching main D3, YBA's measured
  death rate per elapsed turn is roughly human-like while its D5 transition
  rate is about 10x lower. This is a diagnosis, not permission to weaken safety
  gates: removing slow owners and correcting pursuit cost have both failed to
  improve holdouts without a complete replacement objective.
- [STRONG_PLAYER_GAP_ANALYSIS.md](STRONG_PLAYER_GAP_ANALYSIS.md) — the full
  human-vs-YBA measurement record the hypothesis is drawn from, including the
  refuted tempo thesis and every closed no-go lead.

## The boundary principle

The engine hands the agent a **decoded, epistemic observation** — the structured form
of exactly what a human can know (screen, `i`, `\`, `^X`, `^O`, `^P`, `;`). Rules:

- **Decode `glyph`, never `typ`.** Expose "what the hero thinks is there," never ground
  truth (`typ`, `level.monsters[][]`, `level.objects[][]`, monster HP).
- **Tiles are the substrate; regions are a derived epistemic lens; rectangles are a
  trap.** Never expose `svr.rooms[]`.
- **Auto-More + structured messages.** Informational `--More--` is auto-dismissed and
  every `pline` is logged structurally; real decisions (`yn`/`getlin`/menu) route to the
  agent.
- **Keylog-equivalence.** Every framework convenience expands to the literal human
  keystream, so a bot session is always a verifiable replay.

This removes the two taxes AutoAscend paid against NLE: **glyph-integer decoding** (its
`glyph/` module) and **tty-text scraping** (its `get_message_and_popup` + issuing the
`M` command and parsing the popup just to learn monster names/peacefulness). Our engine
holds the semantics directly; taking an observation has **no in-game cost**.

## What we keep vs. drop from AutoAscend

(Full discussion — how AutoAscend works and the component→API mapping — is in
[PORTING.md](PORTING.md).)

| Keep (the intelligence) | Drop (NLE plumbing the engine obviates) |
|---|---|
| `strategy.py` behavior framework (generators + `preempt`/`repeat`/`until`) | `glyph/` glyph-integer decoder |
| world model (`level`, `item`, `monster_tracker`) — as our observation/state | `get_message_and_popup` tty scraper + `--More--` pager |
| `objects/data.py` → the knowledge base | `env_wrapper.py` (NLE coupling) |
| strategy catalog (`global_logic`, combat, exploration, `soko_solver`) | `muzero/`, `visualization/` |

## Layout

```
yendorbound/
  README.md
  PORTING.md         — AutoAscend source + how it works + the porting plan
  agent.js           — the YendorboundAgent state, memory, and goal owner
  EPISTEMOLOGY.md    — the fairness boundary (engine file:line)
  COMMANDS.md        — the command API design
  api/
    observation.js   — the decoded epistemic observation schema (engine → agent)
    agent.js         — the agent contract + action & knowledge interfaces
  world/
    boot.js          — boot a live game in PLAYER mode (engine awaits input)
    observe.js       — live engine state → decoded Observation (+ prompt, inventoryByLetter)
    api.js           — the interactive Api: do/answer/cancel/on/off + knowledge (agent → engine)
    actions.js       — action constructors carrying a prompt map: eat(...)/read(...)/wield(...)
    responders.js    — layered, agent-owned prompt→answer tables (per-action / session / global)
    select.js        — inventory queries + invmatch() deferred item selector
  strategy/
    exceptions.js    — inherited strategy control-flow exceptions
    strategy.js      — Strategy.wrap/run/checkCondition + combinators
    agent_hooks.js   — step counters, onUpdate callbacks, atoms, preemption
    wander.js        — a minimal perceive→act smoke agent (proves the loop)
  goals/             — durable objectives and graph-method adapters
  affordances/       — current observation-derived capabilities
  decision/          — proposals, consequence assessment, and arbitration
  knowledge/         — fair static and learned tactical knowledge
```

## Status

Both halves of the boundary are live and tested, and the agent runs **as a
player** (the engine awaits input; an empty queue is the normal "waiting for the
player" state) — not in the `gameFromSession` test harness (which throws on an
empty queue to detect end-of-session).

- **engine → agent** (`world/observe.js`): the decoded epistemic observation,
  including `observation.prompt`, with the non-leakage invariant under test
  (`test/yendorbound/live_observe.test.mjs`).
- **agent → engine** (`world/api.js`): `do(action)` / `answer(prompt)` /
  `cancel()` drive a live game by pushing keys and settling at each input
  boundary. Informational pauses (`--More--`, PICK_NONE text windows) are
  auto-dismissed; the `wander` smoke agent closes the loop and
  `test/yendorbound/live_agent.test.mjs` asserts **keylog-equivalence**.
- **modal routing** (`test/yendorbound/live_modal.test.mjs`): a real decision
  (yn/getlin/menu — detected via `game._quiescent`, which is true only at the
  command-loop `nhgetch`) surfaces on `observation.prompt`. An unprepared agent
  stays safe: `do()` is **guarded** (returns `{ok:false, reason:'prompt-pending'}`
  rather than feeding a move into a `[yn]`), `cancel()` ESCs to a clean state,
  and `onUnhandledPrompt: 'surface' | 'escape' | 'throw'` is configurable.
- **strategy framework** (`strategy/strategy.js`, `strategy/agent_hooks.js`):
  the AutoAscend `Strategy` generator contract, combinators, step/update
  hooks, atomic operations, and preemption driver are ported and unit-tested.
  The live boundary smoke test wraps `wander` as a Strategy and asserts it
  produces the same result and keylog as standalone `wander`.

Modal answers are driven by **agent-owned, layered tables** (`responders.js`),
highest precedence first:

1. a **per-action** prompt map — `do(eat(null, { 'eat it\\?': 'y' }))`
2. the **session** table — `api.on({ 'really.*\\[yn': 'n' })` / `api.off(...)`
3. the **global** table — `import { on, off } from responders.js`
4. the `onUnhandledPrompt` policy for anything unclaimed.

A response is a literal (`'y'`, `{letter:'d'}`) or a deferred `invmatch(query)`
resolved against the live inventory when the prompt fires. The observation is
`inventoryByLetter` (queryable by `oclass`/`price`/text), so an agent can ask
"the scroll the shopkeeper offered 10 for" and answer the getobj prompt with it:
`do(read(invmatch({ oclass:'scrolls', price:10 })))`, or use a prompt map when
the item prompt needs to be disambiguated.
`api.knowledge.object(nameOrOtyp)` gives a-priori type facts from the engine's
`objects[]`. (`test/yendorbound/live_responders.test.mjs`.)

The current decision stack combines the inherited strategy catalogue with
durable goal graphs and a shared proposal receiver. New behavior should reuse
canonical action owners and remain deterministic, RNG-inert while assessing,
and fair with respect to the observation boundary.
