semif-api-rocm

SemIf HTTP API and rocm flake
Log | Files | Refs | README | LICENSE

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 })();