File size: 12,765 Bytes
d99996b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"use strict";
// "Try it": replays a REAL recorded run (data/experience.js, made by scripts/make_experience_data.py).
// The visitor picks what goes wrong and whether Savepoint is on; nothing is staged by hand.
const FAULTS = [
  ["none", "Nothing goes wrong", "The agent follows the instructions exactly."],
  ["stop_at", "It stops halfway", "The agent gives up before finishing the job."],
  ["wrong_value", "It types a wrong value", "A slipped decimal: 125000.00 instead of 1250.00."],
  ["skip_save", "It skips the last Save", "It types the note but never saves it, then says done."],
  ["done_early", "It says done too early", "It reports done after doing only half the work."],
];
const PROTECTION = [
  ["off", "No protection", "The agent runs on its own, with nothing watching."],
  ["savepoint", "Savepoint", "Saves checkpoints, checks the result, and restores on any failure."],
  ["retry", "Savepoint + retry", "Same, and the agent gets another try from the last good state."],
];
const CHECK_TEXT = {
  amount_under_cap: "the amount stays under 100,000", no_negative_amounts: "no amount is negative",
  no_orphan_notes: "no note is left without a claim", amount_set: "the amount is 1250.00",
  note_added: "a note was added", frame: "nothing else changed",
};
const WANT = { amount: 1250.0, noteWord: "adjusted" };
const money = (n) => Number(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const tryState = { fault: "skip_save", prot: "off", timer: null, token: 0 };

function problems(st) {
  const p = [];
  if (Math.abs(st.amount - WANT.amount) > 0.001) p.push(st.amount === X.initial.amount ? "the amount was never changed" : `the amount is ${money(st.amount)}, not ${money(WANT.amount)}`);
  if (!st.notes.some((n) => n.toLowerCase().includes(WANT.noteWord))) p.push("the note was never saved");
  return p;
}

function verdict(run) {
  const wrong = problems(run.events[run.events.length - 1].state);
  const cls = run.oracle;
  let head, body, notice;
  if (cls === "VS" && run.retries) { head = "Caught, undone, and finished correctly."; body = "Savepoint noticed the problem, restored the last good state, and the agent finished the job on its second try."; notice = "Without retry this run would have ended as a safe fail. Retry turned it into a correct result."; }
  else if (cls === "VS") { head = "The job was done correctly."; body = "The record has the new amount and the note. Savepoint checked both before accepting the agent’s “done”."; notice = "Nothing went wrong here, so Savepoint stayed out of the way. Pick a failure above to see it act."; }
  else if (cls === "VF") { head = "It failed, but safely."; body = "The job did not finish, but Savepoint restored the record to exactly how it was. Nothing is left half-done."; notice = "Compare the record before and after: they are identical. Rows different after restore: 0."; }
  else if (cls === "IS") { head = "The agent said “done”, but the record is wrong."; body = `Nobody was warned. In the record: ${wrong.join(" and ")}. This is the failure Savepoint exists to stop.`; notice = "Turn Savepoint on and replay the same failure to see the difference."; }
  else { head = "It failed and left a mess."; body = `The agent stopped and the record is half-finished: ${wrong.join(" and ")}. Someone has to clean it up by hand.`; notice = "Turn Savepoint on and replay the same failure to see the difference."; }
  return { cls, head, body, notice };
}

function recHtml(st, flash, banner) {
  const prev = X.initial;
  return `<div class="rec" role="group" aria-label="The claims record">

    <div class="rec-title">ClaimDesk 98 — Claim ${esc(X.claim)}</div>

    <div class="rec-row"><span>Amount</span><b class="${flash && st.amount !== prev.amount ? "flash" : ""}">${money(st.amount)}</b></div>

    <div class="rec-row"><span>Status</span><b>${esc(st.status)}</b></div>

    <div class="rec-row"><span>Notes (${st.notes.length})</span><b class="${flash && st.notes.length ? "flash" : ""}">${st.notes.length ? esc(st.notes.join(" · ")) : "none"}</b></div>

    <div class="rec-banner">${banner || ""}</div></div>`;
}

function viewTry(query) {
  if (!X) return pageShell({ title: "Try it", body: `<div class="callout bad">${icon("alert")}<p><b>EXPERIENCE_MISSING</b> data/experience.js did not load.</p></div>` });
  if (query && FAULTS.some((f) => f[0] === query.fault)) tryState.fault = query.fault;
  if (query && PROTECTION.some((p) => p[0] === query.prot)) tryState.prot = query.prot;
  const seg = (items, key) => items.map(([v, label]) => `<button type="button" data-${key}="${v}" aria-pressed="${tryState[key] === v}">${esc(label)}</button>`).join("");
  const body = `

  <div class="callout">${icon("info")}<div><p><b>The task the agent was given</b></p><p>${esc(X.instruction)}</p><p class="muted">Below is a recording of the real Savepoint software running this task on a real (fake-data) claims app. You choose what goes wrong. The replay is real, not an animation.</p></div></div>

  <section class="sect"><div class="ctl">

    <div class="field"><span class="label">Step 1. What goes wrong?</span><div class="seg" id="seg-fault" role="group" aria-label="What goes wrong">${seg(FAULTS, "fault")}</div><span class="hintline" id="hint-fault"></span></div>

    <div class="field"><span class="label">Step 2. Protection</span><div class="seg" id="seg-prot" role="group" aria-label="Protection">${seg(PROTECTION, "prot")}</div><span class="hintline" id="hint-prot"></span></div></div>

    <div class="actions"><button type="button" class="btn primary" id="try-play">${icon("play", "s")} <span>Play</span></button><button type="button" class="btn" id="try-skip">Skip to the end</button><span class="hintline" id="try-status" aria-live="polite"></span></div></section>

  <div id="try-anchor"></div>

  <section class="sect"><div class="two">

    <div><h2>The record</h2><p class="lede" style="margin-bottom:12px">What is stored in the app. It changes as the agent works.</p><div id="rec-wrap"></div></div>

    <div><h2>What happened</h2><p class="lede" style="margin-bottom:12px">Blue lines are Savepoint. Grey lines are the agent. Green and red lines are checks.</p><ol class="ev" id="ev" aria-live="polite"></ol></div></div></section>

  <section class="sect"><div class="panel" id="try-verdict" aria-live="polite"><p class="muted" style="margin:0">The result appears here when the replay finishes.</p></div></section>

  <section class="sect"><h2>Keep going</h2><div class="actions" style="margin-top:8px"><a class="btn" href="#/results">See all 600 test runs ${icon("arrowRight", "s")}</a><a class="btn" href="#/benchmark">See the numbers ${icon("arrowRight", "s")}</a><a class="btn" href="#/how">How it works ${icon("arrowRight", "s")}</a></div>

    <p class="hintline" style="margin-top:12px">The agent here is a script that fails on purpose, not an AI model. The same checks ran on two real open-source apps, OpenMRS and iDempiere, in the test results.</p></section>`;
  return pageShell({ title: "Try it: break an AI agent, then undo it", lede: "Pick a failure, switch Savepoint on or off, and watch what happens to the data.", body });
}

function evHtml(e) {
  const li = (cls, ic, word, text) => `<li class="evi ${cls}"><span class="evw">${icon(ic, "s")}${word}</span><span>${text}</span></li>`;
  if (e.t === "step") return li("", "arrowRight", "Agent", esc(e.text));
  if (e.t === "checkpoint") return li("cp", "diamond", "Savepoint", e.kind === "initial" ? "saved a checkpoint of the record at the start" : "saved a checkpoint before a risky click");
  if (e.t === "agent") return li("", e.status === "done" ? "check" : "x", "Agent", e.status === "done" ? "says <b>done</b>" : "<b>gives up</b>: it cannot finish");
  if (e.t === "restore") return li("rs", "undo", "Savepoint", `restored the record to ${esc(e.to)}. Rows different after restore: <b>${e.rows_differ}</b>${e.exact ? " (exact)" : ""}`);
  if (e.t === "check") {
    const bad = e.results.filter((r) => !r.passed);
    const what = e.phase === "invariant" ? "safety checks after the change" : e.phase === "goal" ? "checking the result against the task" : "checking that nothing else changed";
    if (!bad.length) return li("ok", "check", "Check passed", `${what}: all passed`);
    return li("bad", "x", "Check failed", `${what} — ${bad.map((r) => esc(CHECK_TEXT[r.id] || r.id) + (r.observed != null && r.id !== "frame" ? ` (found ${esc(Array.isArray(r.observed) ? r.observed.join(", ") : r.observed)})` : "")).join("; ")}`);
  }
  return "";
}

function shotsHtml(run) {
  if (!run.shots || run.shots.length < 2) return "";
  return `<details class="disc" style="margin-top:12px"><summary>${icon("chevronDown", "s")} Show the real screens before and after the restore</summary><div class="body"><div class="shotgrid">

    <figure><img alt="Screen before the restore" src="experience/${esc(run.shots[0])}"><figcaption>Before the restore</figcaption></figure>

    <figure><img alt="Screen after the restore" src="experience/${esc(run.shots[1])}"><figcaption>After the restore</figcaption></figure></div></div></details>`;
}

function showVerdict(run) {
  const v = verdict(run);
  const again = run.protection === "off" && v.cls !== "VS" ? `<div class="actions" style="margin-top:12px"><button type="button" class="btn primary" id="try-on">Turn Savepoint on and replay this failure</button></div>` : "";
  $("#try-verdict").innerHTML = `<div class="verdict"><div class="tile o-${v.cls}">${icon(OUT[v.cls].icon)}<span>${OUT[v.cls].word}</span></div>

    <div><h3>${esc(v.head)}</h3><p style="margin:4px 0 0">${esc(v.body)}</p><p class="hintline" style="margin:6px 0 0">${esc(v.notice)}</p></div></div>${again}${shotsHtml(run)}`;
  const on = $("#try-on"); if (on) on.addEventListener("click", () => { tryState.prot = "savepoint"; syncSeg(); playTry(false); });
}

function syncSeg() {
  $$("[data-fault]").forEach((b) => b.setAttribute("aria-pressed", String(b.dataset.fault === tryState.fault)));
  $$("[data-prot]").forEach((b) => b.setAttribute("aria-pressed", String(b.dataset.prot === tryState.prot)));
  $("#hint-fault").textContent = FAULTS.find((f) => f[0] === tryState.fault)[2];
  $("#hint-prot").textContent = PROTECTION.find((p) => p[0] === tryState.prot)[2];
  history.replaceState(null, "", `#/try?fault=${tryState.fault}&prot=${tryState.prot}`);
}

function playTry(instant) {
  const run = X.runs[`${tryState.fault}|${tryState.prot}`];
  clearTimeout(tryState.timer);
  const token = ++tryState.token;
  const list = $("#ev"), wrap = $("#rec-wrap");
  const label = $("#try-play span"), status = $("#try-status");
  label.textContent = "Replaying…"; status.textContent = "Watch the record and the log below.";
  if (!instant) $("#try-anchor").scrollIntoView({ behavior: "smooth", block: "start" });
  list.innerHTML = ""; $("#try-verdict").innerHTML = `<p class="muted" style="margin:0">Replaying…</p>`;
  wrap.innerHTML = recHtml(run.events[0].state, false, "Original record");
  const events = run.events.slice(1);
  const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  let i = 0, last = run.events[0].state, lastBanner = "";
  const step = () => {
    if (token !== tryState.token || i >= events.length) return;
    const e = events[i++];
    const html = evHtml(e);
    if (html) list.insertAdjacentHTML("beforeend", html);
    if (e.state) {
      const changed = JSON.stringify(e.state) !== JSON.stringify(last);
      const banner = e.t === "restore" ? `${icon("undo", "s")} Restored by Savepoint` : e.t === "checkpoint" ? `${icon("diamond", "s")} Checkpoint saved` : "";
      wrap.innerHTML = recHtml(e.state, changed, banner); last = e.state;
      if (e.t === "restore") lastBanner = banner;
    }
    if (e.t === "end") { wrap.innerHTML = recHtml(e.state, false, lastBanner); showVerdict(run); label.textContent = "Play again"; status.textContent = "Done. Change a choice above, or play again."; return; }
    if (instant || reduce) step(); else tryState.timer = setTimeout(step, e.t === "check" || e.t === "restore" ? 1100 : 700);
  };
  step();
}

function bindTry() {
  if (!X) return;
  $$("[data-fault]").forEach((b) => b.addEventListener("click", () => { tryState.fault = b.dataset.fault; syncSeg(); playTry(false); }));
  $$("[data-prot]").forEach((b) => b.addEventListener("click", () => { tryState.prot = b.dataset.prot; syncSeg(); playTry(false); }));
  $("#try-play").addEventListener("click", () => playTry(false));
  $("#try-skip").addEventListener("click", () => playTry(true));
  syncSeg();
  playTry(false);
}