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/) →