README.mdpreviewREADME.mdsource125 lines · 6.9 KB · raw
1# Chapter 5: decisions, the one place Jev is asked anything
2
3The bot of chapter 4 has one real decision. Every so often it scores all the
4goals it knows about (open this chest, go through that door, talk to that
5man) by roughly how far away they are, plus a penalty for each time a goal
6has already failed, and pursues the lowest. That is sensible and a bit
7short-sighted: the nearest pot always beats the dungeon two screens away.
8
9A carried patch (0002, chapter 18) lets the host make that pick instead. The
10shim gathers the goals that are within reach and scored within a margin of
11the cheapest (at most six, upstream's own pick first) and hands them over.
12This crate says each one as a sentence, puts them to Jev as one `Choice`,
13and hands a pick back. Here is a real request, the golden copy a test holds
14the code to (the Castle Yard, two goals on offer):
15
16```json
17{
18  "model": "jev-1.13.0",
19  "questions": {
20    "next": {
21      "type": "choice",
22      "instructions": {
23        "question": "Link is playing The Legend of Zelda: A Link to the Past, out to find every item and clear every dungeon. Which of these should he do next?",
24        "focus": "Weigh what each is likely to gain - an item, a new area, progress toward a dungeon - against how far away it is and whether it has already failed."
25      },
26      "criteria": {
27        "A": "Lift the pot in the north side of this screen, 6 steps west and 2 steps north of Link. It is about 62 steps away.",
28        "B": "Open the unopened chest in the north-east corner of this screen, right beside Link. It is about 3 steps away."
29      }
30    }
31  },
32  "state": {
33    "game": "The Legend of Zelda: A Link to the Past (open mode: Zelda is already rescued, the castle gate is open)",
34    "link_is": "outdoors, in Castle Yard",
35    "link_has": ["a sword", "the Pegasus Boots", "the Moon Pearl"],
36    "hearts": 14.0,
37    "heart_pieces_of_4": 2,
38    "pendants": ["Wisdom"],
39    "crystals": 2,
40    "current_dungeon_small_keys": 1,
41    "big_keys_held": ["Eastern Palace"],
42    "progress_so_far": 9
43  }
44}
45```
46
47Jev answers with a probability for each option, and the pick is drawn from
48those. A clear favourite (70% or more) is simply taken; below that, the
49distribution is flattened a little (a floor of 0.05, then a softmax at
50temperature 2) and sampled, so a close call still explores.
51
52> **Aside: Jev is never in charge of whether the game goes on.** Every way
53> a question can fail to happen (nothing worth asking, the same question
54> asked a moment ago, the rate limiter saying "not now", the network
55> erroring, this run's spending limit reached) ends the same way: upstream's
56> own pick, and the bot plays on. With no chooser installed at all the hook
57> is NULL and the bot is upstream's byte for byte.
58
59Here is every path through `Engine::ask`, and the `How` each one is logged
60as:
61
62```mermaid
63flowchart TD
64  start["a goal choice"] --> q{"two or more distinct options?"}
65  q -- no --> nq["NoQuestion: upstream's pick, nothing spent"]
66  q -- yes --> r{"same options asked within 1,800 frames?"}
67  r -- yes --> re["Reused: the remembered answer, resampled"]
68  r -- no --> lim{"this run's dollar limit reached?"}
69  lim -- yes --> d1["Default: upstream's pick"]
70  lim -- no --> g{"the shared spend guards admit it?"}
71  g -- "not now" --> th["Throttled: upstream's pick, try again later"]
72  g -- yes --> ask["ask Jev"]
73  ask -- answered --> ok["Asked: Jev's pick, with cost and the exact request"]
74  ask -- failed --> d2["Default: upstream's pick, and why"]
75```
76
77The guards are `jev-http`'s (chapter 19 has where it comes from): a spend
78ledger on disk that every process on the machine shares, with a bucket of
79questions per minute and a breaker on dollars per hour. They clear by
80themselves; there is no lifetime cap, only the vendor's own "out of credit".
81
82**One kind today, a shape for more.** The crate is built so that a second
83kind of question would be a new module, not a new code path. A kind owns
84five things (its Facts, how they become a Question, the typed Answer, the
85Fallback without Jev, and the Record it logs), and implements the `Decision`
86trait. Everything shared (the guards, the call, sampling, the reuse cache,
87the totals, the history log) lives once, in `Engine`.
88
89Every decision is a line of `jev.jsonl` (the window's is
90`run/zbanks-window/jev.jsonl`). The window's Jev panel and the `jev_history`
91MCP tool read the last 200 back, and a restart reloads them, so the history
92survives the process.
93
94> **Try it.** `nix develop -c buck2 test //packages/decisions:test` runs
95> offline: the sampling rule, the flattening, the request held byte-equal to
96> a golden copy, the dungeon lore, and every older shape of log line still
97> loading. To see one real round trip to Jev, run chapter 13's probe.
98
99## For the people who maintain it
100
101### In this folder
102
103| Path | What |
104| --- | --- |
105| [src/lib.rs](src/lib.rs) | The `Decision` trait; `Engine<D>` (guards, the call, `sample`, the reuse cache, `Totals`, the history log, `load_history`); `How`, `Record<A>`, `SampleRule`; the shared `Config`; `FAVOURITE_THRESHOLD` (0.70); `HISTORY_CAP` (200); `MODEL`, pinned to `jev-1.13.0`. |
106| [src/goal_choice.rs](src/goal_choice.rs) | The first decision: `Facts` (the offers, where Link is, a `Snapshot` of inventory, hearts, pendants, crystals and keys, refreshed by `Chooser::set_snapshot` once a frame), `Answer`, the `GoalChoice` impl, `DUNGEON_LORE` (vanilla rewards appended to a sentence that already names a dungeon), and `Chooser`/`Shared`, which wire an `Engine<GoalChoice>` into `zbanks::GoalChooser`. Its `Config` adds `margin` (512), which goes straight to the C shim. |
107| [src/goal_choice/words.rs](src/goal_choice/words.rs) | The offered goals as options Jev can tell apart, from the bot's own records only: what, where (the screen by name and which third of it; steps east/west and north/south of Link on his screen; elsewhere, the compass point and the exit the bot's path leaves by), whether it is on the way to another option, failures so far, the path length. Goals that read the same are one option (`offers`); no two options share a sentence (tested). |
108| [BUCK](BUCK) | The `rust_library` and `//packages/decisions:test`. |
109
110```rust
111let client = jev_http::Jev::from_env("native", decisions::model()).expect("key")?;
112let mut chooser = decisions::goal_choice::Chooser::new(client, decisions::goal_choice::Config::default())?;
113chooser.load_history(&log_path)?; // before log_to, so a restart keeps the panel's history
114chooser.log_to(&log_path)?;
115let chooser = decisions::goal_choice::Shared::new(chooser);
116bot.set_goal_chooser(Some(Box::new(chooser.clone())), decisions::goal_choice::Config::default().margin);
117```
118
119Who uses it: `apps/zbanks --jev` (headless) and `apps/native --jev` (the
120window), and `apps/jevprobe` for `decisions::model()`. Measurements of cost
121and of how Jev's picks differ from upstream's:
122[research/zbanks-alttp.md](../../research/zbanks-alttp.md), "Jev at the goal
123choice".
124
125← Previous: [Chapter 4, zbanks/](../zbanks/) · Up: [packages](../) · Next: [Chapter 6, replay/](../replay/) →