planner.js (3231B)
1 "use strict"; 2 3 /* Shared client-side helpers for the /plan learning loop. 4 * 5 * Game-agnostic by design: a demo records each completed action as a transcript 6 * turn, and Planner.triggered() decides when a decision result demands new 7 * rules (a confident "insufficient" choice). The no-rules platformer uses this 8 * today; the puzzle room can adopt the same trigger policy and wire format. 9 */ 10 const Planner = (() => { 11 const INSUFFICIENT_ID = "insufficient"; 12 // Plan only on a confident "I cannot decide"; a weak insufficient just loses 13 // the argmax to the best real action (self-game.js handles that fallback). 14 const INSUFFICIENT_THRESHOLD = 0.99; 15 const TRANSCRIPT_KEEP = 24; // completed actions retained for planning 16 17 const fresh = () => []; 18 19 // One completed action + observed outcome, as the user turn the planner reads. 20 const record = (transcript, content) => { 21 transcript.push({ role: "user", content }); 22 while (transcript.length > TRANSCRIPT_KEEP) transcript.shift(); 23 }; 24 25 // [optionId, probability] of the server's choice, mirroring its argmax. 26 const best = (result) => { 27 let top = 0; 28 result.probabilities.forEach((p, i) => { if (p > result.probabilities[top]) top = i; }); 29 return [result.option_ids[top], result.probabilities[top]]; 30 }; 31 32 // A confident "I cannot decide" demands new rules. The threshold is the 33 // caller's policy: the platformer wants real confidence (0.99) before it 34 // spends a planner run, while the puzzle room passes 0 — any plurality win 35 // for "insufficient" plans, since it has no failure signal until the step 36 // budget runs out and a wandering model must be able to ask for rules early. 37 const triggered = (result, threshold = INSUFFICIENT_THRESHOLD) => { 38 const [id, p] = best(result); 39 return id === INSUFFICIENT_ID && p >= threshold; 40 }; 41 42 // Composes the /plan user prompt from facts the orchestrator owns: the 43 // simulation's one-line goal, a note on what triggered planning, and the 44 // rules currently in effect ("" on the first plan). Game-agnostic — every 45 // simulation supplies the same three facts, so no game needs its own prompt. 46 const context = (goal, trigger, rules) => { 47 const parts = [`Objective: ${goal}`, `Trigger: ${trigger}`]; 48 if (rules) { 49 parts.push( 50 "Previous rules — the actor failed while these were in force. " + 51 "That failure indicts the rules (their facts, priorities, or " + 52 "framing), not the actor's comprehension of them. Never resubmit " + 53 "a reworded or lightly edited version: change the substance, or " + 54 "discard the set and write a fresh one.\n" + rules); 55 } 56 return parts.join("\n"); 57 }; 58 59 async function request(body) { 60 const response = await fetch("/plan", { 61 method: "POST", headers: { "Content-Type": "application/json" }, 62 body: JSON.stringify(body), 63 }); 64 const payload = JSON.parse(await response.text()); 65 if (!response.ok) { 66 throw new Error(typeof payload.detail === "string" ? payload.detail : response.statusText); 67 } 68 return payload; 69 } 70 71 return { INSUFFICIENT_ID, INSUFFICIENT_THRESHOLD, TRANSCRIPT_KEEP, 72 fresh, record, best, triggered, context, request }; 73 })();