Spaces:
Running
Running
Max Rossi commited on
Refresh CacheBack website, demos and latent-vector caption
Browse files- .gitattributes +2 -0
- README.md +1 -1
- demo/coding/cacheback-coding-4k.mp4 +3 -0
- demo/coding/cacheback-coding-poster.jpg +3 -0
- demo/coding/cacheback-render-receipt.json +16 -0
- demo/communication.css +7 -0
- demo/communication.html +18 -0
- demo/communication.js +281 -0
- demo/index.html +26 -246
- demo/replay.css +130 -0
- demo/replay.js +232 -0
- index.html +47 -34
- website/assets/figures/fanoutqa-nemotron.svg +3 -3
- website/assets/figures/fanoutqa-qwen.svg +6 -6
- website/assets/figures/longbench-nemotron.svg +3 -3
- website/assets/figures/longbench-qwen.svg +3 -3
- website/assets/styles.css +23 -9
- website/check.mjs +25 -11
.gitattributes
CHANGED
|
@@ -35,3 +35,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
demo/coding/rclc-cacheback-4k.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
social-preview.jpg filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
demo/coding/rclc-cacheback-4k.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
social-preview.jpg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
demo/coding/cacheback-coding-4k.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
demo/coding/cacheback-coding-poster.jpg filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -16,6 +16,6 @@ Receiver-Conditioned Latent Communication gives 94% CacheBack.
|
|
| 16 |
|
| 17 |
[Paper](https://arxiv.org/abs/2609.32046) · [Code](https://github.com/agentcacheback/cacheback) · [Website](https://agentcacheback.github.io/)
|
| 18 |
|
| 19 |
-
This static Space mirrors the project website, with benchmark results, an interactive recorded
|
| 20 |
|
| 21 |
The website source is maintained at https://github.com/agentcacheback/agentcacheback.github.io.
|
|
|
|
| 16 |
|
| 17 |
[Paper](https://arxiv.org/abs/2609.32046) · [Code](https://github.com/agentcacheback/cacheback) · [Website](https://agentcacheback.github.io/)
|
| 18 |
|
| 19 |
+
This static Space mirrors the project website, with benchmark results, the architecture animation, an interactive recorded document replay, and the updated coding demo video. It does not run model inference.
|
| 20 |
|
| 21 |
The website source is maintained at https://github.com/agentcacheback/agentcacheback.github.io.
|
demo/coding/cacheback-coding-4k.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f3425292f525489d0238ac2a9109e784961233707c2cfdbb42a8f81acd3f111e
|
| 3 |
+
size 5817953
|
demo/coding/cacheback-coding-poster.jpg
ADDED
|
Git LFS Details
|
demo/coding/cacheback-render-receipt.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"width": 3840,
|
| 3 |
+
"height": 2160,
|
| 4 |
+
"fps": 60,
|
| 5 |
+
"seconds": 36,
|
| 6 |
+
"frames": 2160,
|
| 7 |
+
"claims": {
|
| 8 |
+
"passed": true,
|
| 9 |
+
"run": "bughunt-16333-run-20261001T051520Z",
|
| 10 |
+
"textSeconds": 113.20635836,
|
| 11 |
+
"latentSeconds": 25.66205141,
|
| 12 |
+
"tests": 88,
|
| 13 |
+
"textFinish": 31.69303836875,
|
| 14 |
+
"duration": 36
|
| 15 |
+
}
|
| 16 |
+
}
|
demo/communication.css
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.communication-animation { position:relative; font-family:-apple-system,"Helvetica Neue",Arial,sans-serif; color:#444; }
|
| 2 |
+
.communication-animation svg { display:block; width:100%; height:auto; }
|
| 3 |
+
#animation-caption { min-height:2.7em; padding:8px 0; font-size:16px; line-height:1.4; }
|
| 4 |
+
#animation-play { position:absolute; top:8px; right:8px; border:0; border-radius:50%; width:40px; height:40px; background:#0b7872; color:white; cursor:pointer; font-size:18px; }
|
| 5 |
+
#animation-play:hover { background:#202827; }
|
| 6 |
+
#animation-play:focus-visible { outline:2px solid #0b7872; outline-offset:3px; }
|
| 7 |
+
@media(max-width:600px) { #animation-caption { font-size:14px; min-height:4.2em; } #animation-play { width:32px; height:32px; top:0; right:0; font-size:14px; } }
|
demo/communication.html
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>CacheBack vs. full-KV transfer animation</title>
|
| 7 |
+
<style>body { margin:0; }</style>
|
| 8 |
+
<link rel="stylesheet" href="communication.css?v=inline-1">
|
| 9 |
+
<script type="module" src="communication.js?v=inline-1"></script>
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<div class="communication-animation">
|
| 13 |
+
<button id="animation-play" aria-label="Play animation" title="Play animation">▶</button>
|
| 14 |
+
<svg id="communication-svg" viewBox="0 0 1180 548" role="img" aria-label="Animated comparison of full KV transfer and CacheBack"></svg>
|
| 15 |
+
<div id="animation-caption">Selected source positions can be sent as <span style="color:#6a4fb8"><b>token IDs</b></span>; latent steps must be sent as <span style="color:#3f669f"><b>continuous vectors</b></span>. The receiver prefills both to build its own state.</div>
|
| 16 |
+
</div>
|
| 17 |
+
</body>
|
| 18 |
+
</html>
|
demo/communication.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ===================== configuration ===================== */
|
| 2 |
+
const C = {
|
| 3 |
+
gray:'#d9d9d9', purple:'#c5b3e0', blue:'#6c8ebf', teal:'#a9d7c9',
|
| 4 |
+
red:'#e53935', overflow:'#ef9a9a', overflowBg:'#fdecea',
|
| 5 |
+
text:'#555', purpleText:'#6a4fb8', blueText:'#3f669f', tealText:'#3d8f78', green:'#2e8b57', arrow:'#c4c4c4'
|
| 6 |
+
};
|
| 7 |
+
const P = 19, W = 14, H = 64; // column pitch, column width, column height (= layers)
|
| 8 |
+
const N = 10, LAT = 2; // positions per sender, latent steps per sender
|
| 9 |
+
const SEL = [[2, 7], [3, 6]]; // selected positions per sender
|
| 10 |
+
const SX = 130, SY = [118, 250]; // sender KV origin
|
| 11 |
+
const RX = 620, RY = 118, SEG = 10; // receiver KV row (LatentMAS/KVComm)
|
| 12 |
+
const STX = 400, STY = 250; // staging row (colored entries only)
|
| 13 |
+
const CY = 382, TY = 382; // CacheBack row y, token-ID row y
|
| 14 |
+
const LIMIT_SLOT = 16; // context limit falls before this receiver slot
|
| 15 |
+
const GROUP_GAP = 10; // gap between purple and blue groups in staging
|
| 16 |
+
|
| 17 |
+
const T = { s0:0, s1:1.6, s2:4.0, s3:7.0, s4:11.0, s5:13.4, s6:17.0, s7:20.0, end:23.5 };
|
| 18 |
+
const STAGES = [
|
| 19 |
+
{ t:T.s0, name:'Sender KVs', cap:'Each sender agent holds a <b>KV cache</b> — one column per position, spanning all layers.' },
|
| 20 |
+
{ t:T.s1, name:'Select', cap:'Positions relevant to the receiver\'s query <b>q</b> are <span style="color:#6a4fb8"><b>selected</b></span>.' },
|
| 21 |
+
{ t:T.s2, name:'Latent steps', cap:'Each sender generates <span style="color:#3f669f"><b>latent steps</b></span>, appending new KV entries.' },
|
| 22 |
+
{ t:T.s3, name:'Concatenate', cap:'<b>LatentMAS / KVComm</b>: transfer <i>all</i> positions and concatenate them into the receiver\'s KV, followed by the receiver prompt.' },
|
| 23 |
+
{ t:T.s4, name:'Overflow', cap:'The concatenated cache plus the receiver prompt <span style="color:#e53935"><b>exceeds the context limit</b></span>.' },
|
| 24 |
+
{ t:T.s5, name:'Pick & sort', cap:'<b>CacheBack</b>: keep only the selected positions and latent steps, and organize them.' },
|
| 25 |
+
{ t:T.s6, name:'CacheBack', cap:'The compact set is written into the receiver\'s cache — <span style="color:#2e8b57"><b>well within the context limit</b></span>.' },
|
| 26 |
+
{ t:T.s7, name:'Token IDs', cap:'Selected source positions can be sent as <span style="color:#6a4fb8"><b>token IDs</b></span>; latent steps must be sent as <span style="color:#3f669f"><b>continuous vectors</b></span>. The receiver prefills both to build its own state.' },
|
| 27 |
+
];
|
| 28 |
+
|
| 29 |
+
/* ===================== tiny timeline engine ===================== */
|
| 30 |
+
const NS = 'http://www.w3.org/2000/svg';
|
| 31 |
+
const svg = document.getElementById('communication-svg');
|
| 32 |
+
function el(tag, attrs = {}, parent) {
|
| 33 |
+
const n = document.createElementNS(NS, tag);
|
| 34 |
+
for (const k in attrs) n.setAttribute(k, attrs[k]);
|
| 35 |
+
if (parent) parent.appendChild(n);
|
| 36 |
+
return n;
|
| 37 |
+
}
|
| 38 |
+
const ease = {
|
| 39 |
+
lin: t => t,
|
| 40 |
+
io: t => t < .5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2,
|
| 41 |
+
out: t => 1 - Math.pow(1 - t, 3),
|
| 42 |
+
back:t => { const c1 = 1.70158, c3 = c1 + 1; return 1 + c3*Math.pow(t - 1, 3) + c1*Math.pow(t - 1, 2); },
|
| 43 |
+
};
|
| 44 |
+
const COLOR_PROPS = new Set(['fill', 'stroke']);
|
| 45 |
+
const ATTR = { w:'width', h:'height' };
|
| 46 |
+
const hex = h => [0, 2, 4].map(i => parseInt(h.replace('#', '').substr(i, 2), 16));
|
| 47 |
+
const norm = (k, v) => COLOR_PROPS.has(k) ? hex(v) : v;
|
| 48 |
+
const mix = (a, b, p) => Array.isArray(a) ? a.map((v, i) => v + (b[i] - v) * p) : a + (b - a) * p;
|
| 49 |
+
const actors = [];
|
| 50 |
+
class Actor {
|
| 51 |
+
constructor(node, init) {
|
| 52 |
+
this.node = node; this.init = {}; this.tracks = {}; this.last = {};
|
| 53 |
+
for (const k in init) this.init[k] = norm(k, init[k]);
|
| 54 |
+
actors.push(this);
|
| 55 |
+
}
|
| 56 |
+
to(t0, dur, props, e = 'io') {
|
| 57 |
+
for (const k in props) if (!(k in this.init)) this.init[k] = +this.node.getAttribute(ATTR[k] || k);
|
| 58 |
+
for (const k in props) (this.tracks[k] ||= []).push({ t0, t1: t0 + dur, v: norm(k, props[k]), e: ease[e] });
|
| 59 |
+
return this;
|
| 60 |
+
}
|
| 61 |
+
at(t, props) { return this.to(t, 0, props, 'lin'); }
|
| 62 |
+
eval(t) {
|
| 63 |
+
for (const k in this.init) {
|
| 64 |
+
let v = this.init[k];
|
| 65 |
+
for (const s of (this.tracks[k] || [])) {
|
| 66 |
+
if (t >= s.t1) { v = s.v; continue; }
|
| 67 |
+
if (t > s.t0) v = mix(v, s.v, s.e((t - s.t0) / (s.t1 - s.t0)));
|
| 68 |
+
break;
|
| 69 |
+
}
|
| 70 |
+
const out = COLOR_PROPS.has(k) ? `rgb(${v.map(Math.round).join(',')})` : (+v.toFixed(2));
|
| 71 |
+
if (this.last[k] !== out) { this.node.setAttribute(ATTR[k] || k, out); this.last[k] = out; }
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
finalize() { for (const k in this.tracks) this.tracks[k].sort((a, b) => a.t0 - b.t0); }
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* ===================== scene ===================== */
|
| 78 |
+
const defs = el('defs', {}, svg);
|
| 79 |
+
const mk = el('marker', { id:'ah', viewBox:'0 0 10 10', refX:5, refY:5, markerWidth:3.2, markerHeight:3.2, orient:'auto-start-reverse' }, defs);
|
| 80 |
+
el('path', { d:'M0,0 L10,5 L0,10 z', fill:C.arrow }, mk);
|
| 81 |
+
|
| 82 |
+
const L = {};
|
| 83 |
+
for (const name of ['bg', 'main', 'clone', 'over', 'labels']) L[name] = el('g', {}, svg);
|
| 84 |
+
|
| 85 |
+
const rect = (init, parent = L.main, rx = 2) => new Actor(el('rect', { rx }, parent), init);
|
| 86 |
+
function txt(str, x, y, o = {}, parent = L.labels) {
|
| 87 |
+
const n = el('text', { x, y, 'font-size':o.size || 14, 'font-weight':o.weight || 400,
|
| 88 |
+
fill:o.fill || C.text, 'text-anchor':o.anchor || 'start', ...(o.attrs || {}) }, parent);
|
| 89 |
+
n.innerHTML = str;
|
| 90 |
+
return new Actor(n, { opacity:o.opacity ?? 0, ...(o.init || {}) });
|
| 91 |
+
}
|
| 92 |
+
const group = (parent = L.labels, opacity = 0) => { const g = el('g', {}, parent); return [g, new Actor(g, { opacity })]; };
|
| 93 |
+
|
| 94 |
+
const colX = i => SX + i * P;
|
| 95 |
+
const slotX = g => RX + Math.floor(g / (N + LAT)) * ((N + LAT) * P + SEG) + (g % (N + LAT)) * P;
|
| 96 |
+
const limitX = slotX(LIMIT_SLOT) - (P - W) / 2;
|
| 97 |
+
const recvPromptX = RX + 2 * ((N + LAT) * P + SEG);
|
| 98 |
+
const PROMPT_W = 3 * P - (P - W);
|
| 99 |
+
|
| 100 |
+
/* --- axes --- */
|
| 101 |
+
{
|
| 102 |
+
const [g, a] = group();
|
| 103 |
+
el('line', { x1:SX, y1:76, x2:SX + 200, y2:76, stroke:C.arrow, 'stroke-width':5, 'marker-end':'url(#ah)' }, g);
|
| 104 |
+
const t1 = el('text', { x:SX, y:62, 'font-size':16, fill:'#666' }, g); t1.textContent = 'positions';
|
| 105 |
+
el('line', { x1:SX - 24, y1:SY[0], x2:SX - 24, y2:SY[0] + 110, stroke:C.arrow, 'stroke-width':5, 'marker-end':'url(#ah)' }, g);
|
| 106 |
+
const t2 = el('text', { x:SX - 34, y:SY[0] + 14, 'font-size':16, fill:'#666', 'text-anchor':'end' }, g); t2.textContent = 'layers';
|
| 107 |
+
a.to(0.1, .6, { opacity:1 });
|
| 108 |
+
}
|
| 109 |
+
const senderLabels = [0, 1].map(s => txt(`Sender ${s + 1} KV`, SX, SY[s] - 10, { size:15, weight:700 }).to(0.1 + s * .25, .5, { opacity:1 }));
|
| 110 |
+
|
| 111 |
+
/* --- stage 0/1/2: sender columns --- */
|
| 112 |
+
const senderCols = [[], []];
|
| 113 |
+
const scanStart = T.s1 + 0.3, scanDur = 1.8;
|
| 114 |
+
for (const s of [0, 1]) {
|
| 115 |
+
for (let i = 0; i < N + LAT; i++) {
|
| 116 |
+
const isLat = i >= N, y = SY[s];
|
| 117 |
+
const a = rect({ x:colX(i), y:y + H/2, w:W, h:0, fill:isLat ? C.blue : C.gray, opacity:1 });
|
| 118 |
+
if (!isLat) a.to(0.15 + s * .25 + i * .05, .45, { y, h:H }, 'out');
|
| 119 |
+
if (SEL[s].includes(i)) {
|
| 120 |
+
const ts = scanStart + scanDur * (i + 1) / (N + 1);
|
| 121 |
+
a.to(ts, .25, { fill:C.purple }, 'lin')
|
| 122 |
+
.to(ts, .15, { y:y - 4, h:H + 8 }, 'out').to(ts + .15, .25, { y, h:H });
|
| 123 |
+
}
|
| 124 |
+
senderCols[s].push(a);
|
| 125 |
+
}
|
| 126 |
+
// scan bar with query q
|
| 127 |
+
const scan = rect({ x:colX(-1) - 3, y:SY[s] - 6, w:W + 6, h:H + 12, fill:'#7cc4b0', opacity:0 }, L.over, 4);
|
| 128 |
+
scan.to(scanStart - .15, .15, { opacity:.5 }, 'lin')
|
| 129 |
+
.to(scanStart, scanDur, { x:colX(N) - 3 }, 'lin')
|
| 130 |
+
.to(scanStart + scanDur, .2, { opacity:0 }, 'lin');
|
| 131 |
+
const q = txt('q', colX(-1) + W/2, SY[s] + H + 18, { size:15, weight:700, fill:C.tealText, anchor:'middle', attrs:{ 'font-style':'italic' } });
|
| 132 |
+
q.to(scanStart - .15, .15, { opacity:1 }, 'lin')
|
| 133 |
+
.to(scanStart, scanDur, { x:colX(N) + W/2 }, 'lin')
|
| 134 |
+
.to(scanStart + scanDur, .2, { opacity:0 }, 'lin');
|
| 135 |
+
|
| 136 |
+
// latent step generation: attend over existing cache, then append a blue column
|
| 137 |
+
for (let k = 0; k < LAT; k++) {
|
| 138 |
+
const tk = T.s2 + 0.3 + k * 1.3;
|
| 139 |
+
const flash = rect({ x:SX - 4, y:SY[s] - 4, w:(N + k) * P - (P - W) + 8, h:H + 8, fill:C.blue, opacity:0 }, L.over, 5);
|
| 140 |
+
flash.to(tk, .3, { opacity:.2 }, 'out').to(tk + .35, .35, { opacity:0 });
|
| 141 |
+
senderCols[s][N + k].to(tk + .45, .5, { y:SY[s], h:H }, 'back');
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
const typeOf = (s, i) => i >= N ? 'b' : SEL[s].includes(i) ? 'p' : 'g';
|
| 145 |
+
const fillOf = { p:C.purple, b:C.blue, g:C.gray };
|
| 146 |
+
|
| 147 |
+
/* --- stage 3: LatentMAS / KVComm concatenation --- */
|
| 148 |
+
const recvTitle = txt(`LatentMAS, KVComm <tspan font-weight="400" fill="#999">· all positions</tspan>`, RX, RY - 10, { size:15, weight:700 });
|
| 149 |
+
recvTitle.to(T.s3, .5, { opacity:1 });
|
| 150 |
+
const recv = [];
|
| 151 |
+
for (const s of [0, 1]) for (let i = 0; i < N + LAT; i++) {
|
| 152 |
+
const g = s * (N + LAT) + i;
|
| 153 |
+
const c = rect({ x:colX(i), y:SY[s], w:W, h:H, fill:fillOf[typeOf(s, i)], opacity:0 }, L.clone);
|
| 154 |
+
const t0 = T.s3 + 0.4 + s * 1.1 + i * .06;
|
| 155 |
+
c.at(t0, { opacity:1 }).to(t0, .9, { x:slotX(g), y:RY });
|
| 156 |
+
recv.push(c);
|
| 157 |
+
}
|
| 158 |
+
const recvPrompt = rect({ x:recvPromptX + 60, y:RY, w:PROMPT_W, h:H, fill:C.teal, opacity:0 }, L.clone);
|
| 159 |
+
recvPrompt.to(T.s3 + 3.0, .7, { x:recvPromptX, opacity:1 }, 'out');
|
| 160 |
+
|
| 161 |
+
/* --- stage 4: context limit & overflow --- */
|
| 162 |
+
const overBg = rect({ x:limitX, y:RY - 6, w:recvPromptX + PROMPT_W + 8 - limitX, h:H + 12, fill:C.overflowBg, opacity:0 }, L.bg, 4);
|
| 163 |
+
overBg.to(T.s4 + .8, .4, { opacity:1 });
|
| 164 |
+
const limitLine = new Actor(el('line', { x1:limitX, x2:limitX, y1:92, stroke:C.red, 'stroke-width':2.5, 'stroke-dasharray':'8 6' }, L.labels), { y2:92, opacity:0 });
|
| 165 |
+
limitLine.at(T.s4 + .1, { opacity:1 }).to(T.s4 + .1, .7, { y2:478 }, 'io');
|
| 166 |
+
const limitText = txt('Context Limit', 0, 0, { size:17, fill:C.red, anchor:'middle', attrs:{ transform:`translate(${limitX + 16},${(RY + H + CY) / 2 + 6}) rotate(90)` } });
|
| 167 |
+
limitText.to(T.s4 + .6, .4, { opacity:1 });
|
| 168 |
+
recv.forEach((c, g) => { if (g >= LIMIT_SLOT) c.to(T.s4 + .9 + (g - LIMIT_SLOT) * .05, .3, { fill:C.overflow }, 'lin'); });
|
| 169 |
+
recvPrompt.to(T.s4 + .9 + (recv.length - LIMIT_SLOT) * .05, .3, { fill:C.overflow }, 'lin');
|
| 170 |
+
const overLabel = txt('✕ exceeds context', limitX + 10, RY - 10, { size:14, weight:700, fill:C.red });
|
| 171 |
+
overLabel.to(T.s4 + 1.4, .4, { opacity:1 });
|
| 172 |
+
|
| 173 |
+
/* --- stage 5: CacheBack — pick colored entries, then sort by type --- */
|
| 174 |
+
[...recv, recvPrompt, recvTitle, overBg, overLabel].forEach(a => a.to(T.s5, .5, { opacity:.3 }));
|
| 175 |
+
for (const s of [0, 1]) senderCols[s].forEach((a, i) => { if (typeOf(s, i) === 'g') a.to(T.s5 + .1, .5, { opacity:.3 }); });
|
| 176 |
+
|
| 177 |
+
const picks = [];
|
| 178 |
+
for (const s of [0, 1]) {
|
| 179 |
+
SEL[s].forEach(i => picks.push({ s, i, type:'p' }));
|
| 180 |
+
for (let k = 0; k < LAT; k++) picks.push({ s, i:N + k, type:'b' });
|
| 181 |
+
}
|
| 182 |
+
const purples = picks.filter(p => p.type === 'p'), blues = picks.filter(p => p.type === 'b');
|
| 183 |
+
const sortedX = p => p.type === 'p' ? STX + purples.indexOf(p) * P
|
| 184 |
+
: STX + (purples.length + blues.indexOf(p)) * P + GROUP_GAP;
|
| 185 |
+
const sortT = T.s5 + 2.2;
|
| 186 |
+
picks.forEach((p, j) => {
|
| 187 |
+
const c = rect({ x:colX(p.i), y:SY[p.s], w:W, h:H, fill:fillOf[p.type], opacity:0 }, L.clone);
|
| 188 |
+
c.at(T.s5 + .3, { opacity:1 }).to(T.s5 + .3, .3, { y:SY[p.s] - 8 }, 'out'); // lift out
|
| 189 |
+
c.to(T.s5 + .8 + j * .08, .8, { x:STX + j * P, y:STY }); // gather
|
| 190 |
+
if (p.type === 'b') { // sort: blues dodge down, slide, come back
|
| 191 |
+
c.to(sortT, .3, { y:STY + 40 }).to(sortT + .85, .3, { y:STY });
|
| 192 |
+
}
|
| 193 |
+
c.to(sortT + .35, .45, { x:sortedX(p) });
|
| 194 |
+
p.actor = c;
|
| 195 |
+
});
|
| 196 |
+
txt('selected + latent only', STX, STY - 10, { size:13, fill:'#888' }).to(T.s5 + .8, .5, { opacity:1 });
|
| 197 |
+
|
| 198 |
+
/* --- stage 6: write into CacheBack receiver cache --- */
|
| 199 |
+
txt('CacheBack', RX, CY - 10, { size:15, weight:700 }).to(T.s6, .5, { opacity:1 });
|
| 200 |
+
const perSender = SEL[0].length + LAT;
|
| 201 |
+
const cbX = (p) => {
|
| 202 |
+
const m = p.type === 'p' ? SEL[p.s].indexOf(p.i) : SEL[p.s].length + (p.i - N);
|
| 203 |
+
return RX + p.s * (perSender * P + SEG) + m * P;
|
| 204 |
+
};
|
| 205 |
+
const cbOrder = [...picks]; // per-sender: selected positions, then latent steps
|
| 206 |
+
cbOrder.forEach((p, j) => {
|
| 207 |
+
const c = rect({ x:sortedX(p), y:STY, w:W, h:H, fill:fillOf[p.type], opacity:0 }, L.clone);
|
| 208 |
+
const t0 = T.s6 + .3 + j * .07;
|
| 209 |
+
c.at(t0, { opacity:1 }).to(t0, .9, { x:cbX(p), y:CY });
|
| 210 |
+
});
|
| 211 |
+
const cbPromptX = RX + 2 * (perSender * P + SEG);
|
| 212 |
+
rect({ x:cbPromptX + 40, y:CY, w:PROMPT_W, h:H, fill:C.teal, opacity:0 }, L.clone).to(T.s6 + 1.5, .7, { x:cbPromptX, opacity:1 }, 'out');
|
| 213 |
+
txt('✓ fits', cbPromptX + PROMPT_W + 14, CY + H / 2 + 5, { size:15, weight:700, fill:C.green }).to(T.s6 + 2.3, .4, { opacity:1 });
|
| 214 |
+
|
| 215 |
+
/* --- stage 7: purple entries compress to token IDs --- */
|
| 216 |
+
picks.forEach((p, j) => {
|
| 217 |
+
const c = rect({ x:sortedX(p), y:STY, w:W, h:H, fill:fillOf[p.type], opacity:0 }, L.clone);
|
| 218 |
+
const t0 = T.s7 + .3 + j * .08;
|
| 219 |
+
c.at(t0, { opacity:1 });
|
| 220 |
+
if (p.type === 'p') c.to(t0, .9, { y:TY + H / 2 - 4, h:8 });
|
| 221 |
+
else c.to(t0, .9, { y:TY });
|
| 222 |
+
});
|
| 223 |
+
txt('Token IDs', STX + (purples.length * P - (P - W)) / 2, TY + H / 2 - 12, { size:14, weight:600, fill:C.purpleText, anchor:'middle' })
|
| 224 |
+
.to(T.s7 + 1.6, .4, { opacity:1 });
|
| 225 |
+
txt('+prefill', STX + purples.length * P + GROUP_GAP + (blues.length * P - (P - W)) / 2, TY + H + 20, { size:14, weight:600, fill:C.purpleText, anchor:'middle' })
|
| 226 |
+
.to(T.s7 + 2.0, .4, { opacity:1 });
|
| 227 |
+
|
| 228 |
+
/* --- legend (always visible) --- */
|
| 229 |
+
{
|
| 230 |
+
const ly = 524, items = [
|
| 231 |
+
{ x:130, fill:C.purple, label:'Selected for receiver query q', color:C.purpleText },
|
| 232 |
+
{ x:385, fill:C.gray, label:'Unselected positions', color:'#888' },
|
| 233 |
+
{ x:580, fill:C.blue, label:'Latent steps', color:C.blueText },
|
| 234 |
+
{ x:730, fill:C.teal, label:'Receiver prompt', color:C.tealText },
|
| 235 |
+
];
|
| 236 |
+
for (const it of items) {
|
| 237 |
+
el('rect', { x:it.x, y:ly - 13, width:16, height:16, rx:2, fill:it.fill }, svg);
|
| 238 |
+
const t = el('text', { x:it.x + 24, y:ly, 'font-size':14, fill:it.color }, svg); t.textContent = it.label;
|
| 239 |
+
}
|
| 240 |
+
el('line', { x1:905, y1:ly - 5, x2:935, y2:ly - 5, stroke:C.red, 'stroke-width':2.5, 'stroke-dasharray':'6 4' }, svg);
|
| 241 |
+
const t = el('text', { x:943, y:ly, 'font-size':14, fill:C.red }, svg); t.textContent = 'Context limit';
|
| 242 |
+
}
|
| 243 |
+
actors.forEach(a => a.finalize());
|
| 244 |
+
|
| 245 |
+
/* ===================== playback ===================== */
|
| 246 |
+
const button = document.getElementById('animation-play');
|
| 247 |
+
let t = T.end, playing = false, lastTs = null, stageIdx = -1;
|
| 248 |
+
function render(time) {
|
| 249 |
+
for (const a of actors) a.eval(time);
|
| 250 |
+
let idx = 0;
|
| 251 |
+
for (let i = 0; i < STAGES.length; i++) if (time >= STAGES[i].t) idx = i;
|
| 252 |
+
if (idx !== stageIdx) {
|
| 253 |
+
stageIdx = idx;
|
| 254 |
+
document.getElementById('animation-caption').innerHTML = STAGES[idx].cap;
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
function pause() {
|
| 258 |
+
playing = false;
|
| 259 |
+
button.textContent = '▶';
|
| 260 |
+
button.title = t >= T.end ? 'Replay animation' : 'Play animation';
|
| 261 |
+
button.setAttribute('aria-label', button.title);
|
| 262 |
+
}
|
| 263 |
+
function frame(ts) {
|
| 264 |
+
if (!playing) return;
|
| 265 |
+
if (lastTs !== null) t = Math.min(T.end, t + (ts - lastTs) / 1000);
|
| 266 |
+
lastTs = ts;
|
| 267 |
+
render(t);
|
| 268 |
+
if (t >= T.end) pause();
|
| 269 |
+
else requestAnimationFrame(frame);
|
| 270 |
+
}
|
| 271 |
+
button.onclick = () => {
|
| 272 |
+
if (playing) { pause(); return; }
|
| 273 |
+
if (t >= T.end) t = 0;
|
| 274 |
+
playing = true;
|
| 275 |
+
lastTs = null;
|
| 276 |
+
button.textContent = 'Ⅱ';
|
| 277 |
+
button.title = 'Pause animation';
|
| 278 |
+
button.setAttribute('aria-label', button.title);
|
| 279 |
+
requestAnimationFrame(frame);
|
| 280 |
+
};
|
| 281 |
+
render(t);
|
demo/index.html
CHANGED
|
@@ -4,84 +4,24 @@
|
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
<meta name="color-scheme" content="light">
|
| 7 |
-
<title>CacheBack |
|
| 8 |
-
<
|
| 9 |
-
|
| 10 |
-
body { margin:0; background:#fff; color:#222; font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
|
| 11 |
-
main { width:min(1320px,calc(100% - 64px)); margin:24px auto; }
|
| 12 |
-
header { font-size:18px; font-weight:650; }
|
| 13 |
-
.question { display:flex; align-items:center; justify-content:space-between; gap:24px; margin:14px 0 22px; }
|
| 14 |
-
h1 { max-width:880px; margin:0; font-size:clamp(23px,3vw,28px); font-weight:500; letter-spacing:-.6px; line-height:1.3; }
|
| 15 |
-
h2 { margin:0; font-size:15px; font-weight:600; }
|
| 16 |
-
button,a { -webkit-tap-highlight-color:transparent; }
|
| 17 |
-
button { font:inherit; cursor:pointer; border:0; border-radius:4px; background:#222; color:#fff; padding:10px 20px; white-space:nowrap; }
|
| 18 |
-
button:disabled { cursor:default; opacity:.5; }
|
| 19 |
-
.controls { display:flex; align-items:center; gap:14px; }
|
| 20 |
-
#speed { font:inherit; font-size:13px; color:#666; border:0; background:transparent; padding:8px 2px; cursor:pointer; }
|
| 21 |
-
#clear { background:none; color:#777; padding:8px 0; font-size:13px; }
|
| 22 |
-
:focus-visible { outline:2px solid #222; outline-offset:5px; }
|
| 23 |
-
.topology,.row { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:28px; }
|
| 24 |
-
.topology { margin:0 0 16px; padding:0; list-style:none; }
|
| 25 |
-
.flow-label { margin:0 0 10px; color:#777; font-size:11px; }
|
| 26 |
-
.topology li { position:relative; padding:10px 14px; border:1px solid #ddd; border-radius:5px; font-size:13px; }
|
| 27 |
-
.topology li:not(:last-child)::after { content:"→"; position:absolute; right:-23px; top:50%; transform:translateY(-50%); font-size:20px; color:#777; }
|
| 28 |
-
.topology strong { display:block; }
|
| 29 |
-
.topology span { color:#777; font-size:12px; }
|
| 30 |
-
.method { margin-top:16px; }
|
| 31 |
-
.method-heading { display:flex; align-items:baseline; gap:14px; margin-bottom:12px; }
|
| 32 |
-
.time { font-size:13px; color:#666; font-variant-numeric:tabular-nums; }
|
| 33 |
-
.chunk { border-top:1px solid #ddd; padding:12px 10px 0 0; height:230px; overflow:auto; scrollbar-width:thin; white-space:pre-wrap; font-size:12px; line-height:1.7; overflow-wrap:anywhere; }
|
| 34 |
-
.chunk::before { display:none; content:attr(data-agent); font:600 12px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; white-space:normal; margin-bottom:12px; }
|
| 35 |
-
.source { font-family:Georgia,"Times New Roman",serif; font-size:13px; }
|
| 36 |
-
.answer { font-size:13px; }
|
| 37 |
-
.answer-check { margin:14px 0 0; font-size:12px; font-weight:600; }
|
| 38 |
-
strong { font-weight:600; }
|
| 39 |
-
mark { color:inherit; background:linear-gradient(#bde8e3,#bde8e3) left/100% 100% no-repeat; box-decoration-break:clone; -webkit-box-decoration-break:clone; }
|
| 40 |
-
.revealing mark { animation:highlight .18s ease-out both; }
|
| 41 |
-
@keyframes highlight { from { background-size:0% 100%; } to { background-size:100% 100%; } }
|
| 42 |
-
footer { display:flex; justify-content:space-between; gap:20px; margin-top:28px; font-size:11px; color:#777; }
|
| 43 |
-
footer p { margin:0; }
|
| 44 |
-
a { color:inherit; text-underline-offset:3px; }
|
| 45 |
-
#error { color:#8e2b1d; }
|
| 46 |
-
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
|
| 47 |
-
@media(max-width:800px) {
|
| 48 |
-
main { width:calc(100% - 32px); margin:24px auto; }
|
| 49 |
-
.question { align-items:flex-start; flex-direction:column; gap:18px; margin:24px 0; }
|
| 50 |
-
.topology { display:none; }
|
| 51 |
-
.row { grid-template-columns:repeat(2,minmax(0,1fr)); gap:24px; }
|
| 52 |
-
.chunk::before { display:block; }
|
| 53 |
-
.chunk { height:260px; }
|
| 54 |
-
footer { flex-direction:column; gap:8px; }
|
| 55 |
-
}
|
| 56 |
-
@media(max-width:520px) { .row { grid-template-columns:1fr; } }
|
| 57 |
-
.embedded header,.embedded #question { display:none; }
|
| 58 |
-
.embedded main { margin:16px auto; }
|
| 59 |
-
.embedded .question { flex-direction:row; align-items:center; justify-content:space-between; gap:12px; margin:0 0 12px; }
|
| 60 |
-
.embedded .flow-label { display:none; }
|
| 61 |
-
.embedded .question { padding-bottom:12px; border-bottom:1px solid #ddd; }
|
| 62 |
-
.embedded .topology { display:flex; flex:1; min-width:0; gap:22px; margin:0; }
|
| 63 |
-
.embedded .topology li { display:flex; align-items:center; gap:6px; flex:1; min-width:0; padding:0; border:0; }
|
| 64 |
-
.embedded .topology strong { white-space:nowrap; font-size:12px; }
|
| 65 |
-
.embedded .topology span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:11px; }
|
| 66 |
-
.embedded .topology li:not(:last-child)::after { right:-17px; font-size:15px; }
|
| 67 |
-
.embedded .controls { flex-shrink:0; gap:10px; }
|
| 68 |
-
@media(max-width:800px) {
|
| 69 |
-
.embedded .topology span { display:none; }
|
| 70 |
-
.embedded .topology { gap:16px; }
|
| 71 |
-
.embedded .topology li:not(:last-child)::after { right:-12px; }
|
| 72 |
-
.embedded .topology strong { font-size:10px; }
|
| 73 |
-
.embedded .controls { gap:5px; }
|
| 74 |
-
.embedded button { padding:8px 10px; }
|
| 75 |
-
}
|
| 76 |
-
@media(min-width:801px) { .embedded .chunk { height:260px; } }
|
| 77 |
-
@media(prefers-reduced-motion:reduce) { .revealing mark { animation:none; } }
|
| 78 |
-
</style>
|
| 79 |
</head>
|
| 80 |
<body>
|
| 81 |
<main>
|
| 82 |
-
<header>CacheBack</header>
|
| 83 |
-
<
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
<div class="controls">
|
| 86 |
<select id="speed" aria-label="Playback speed" disabled>
|
| 87 |
<option value="1">1×</option>
|
|
@@ -89,191 +29,31 @@
|
|
| 89 |
<option value="4">4×</option>
|
| 90 |
</select>
|
| 91 |
<button id="clear" type="button" hidden>Clear</button>
|
| 92 |
-
<button id="play" type="button" disabled>Run</button>
|
| 93 |
</div>
|
| 94 |
-
</
|
| 95 |
<p id="error" role="alert" hidden></p>
|
| 96 |
<div class="flow" aria-label="Four-agent relay">
|
| 97 |
-
<p class="flow-label">Chain of agents</p>
|
| 98 |
-
<ol class="topology" aria-label="Agent topology">
|
| 99 |
-
<li><strong>Agent 1</strong><span>booking-confirmation.txt</span></li>
|
| 100 |
-
<li><strong>Agent 2</strong><span>friday-desk-changes.txt</span></li>
|
| 101 |
-
<li><strong>Agent 3</strong><span>collection-id-rules.txt</span></li>
|
| 102 |
-
<li><strong>Agent 4</strong><span>Final answer</span></li>
|
| 103 |
-
</ol>
|
| 104 |
<section class="method" aria-labelledby="rclc-title">
|
| 105 |
-
<div class="method-heading">
|
|
|
|
|
|
|
|
|
|
| 106 |
<div class="row" id="rclc-row"></div>
|
| 107 |
</section>
|
| 108 |
<section class="method" aria-labelledby="text-title">
|
| 109 |
-
<div class="method-heading">
|
|
|
|
|
|
|
|
|
|
| 110 |
<div class="row" id="text-row"></div>
|
| 111 |
</section>
|
| 112 |
</div>
|
| 113 |
<p class="sr-only" id="status" role="status" aria-live="polite"></p>
|
| 114 |
<footer>
|
| 115 |
-
<p><span id="run-info">Recorded replay</span> · <span id="timing-note">
|
| 116 |
<a href="https://github.com/agentcacheback/cacheback/tree/main/demo">Code & recording</a>
|
| 117 |
</footer>
|
| 118 |
</main>
|
| 119 |
-
<script>
|
| 120 |
-
const $ = id => document.getElementById(id);
|
| 121 |
-
const host = window.frameElement;
|
| 122 |
-
if (host) {
|
| 123 |
-
document.body.classList.add("embedded");
|
| 124 |
-
document.querySelector(".question").prepend(document.querySelector(".topology"));
|
| 125 |
-
const main = document.querySelector("main");
|
| 126 |
-
new ResizeObserver(() => {
|
| 127 |
-
const bottomMargin = parseFloat(getComputedStyle(main).marginBottom);
|
| 128 |
-
host.style.height = `${Math.ceil(main.offsetTop + main.offsetHeight + bottomMargin) + 1}px`;
|
| 129 |
-
}).observe(main);
|
| 130 |
-
}
|
| 131 |
-
let trace, playing = false, frame;
|
| 132 |
-
const phases = {rclc:0, text:0};
|
| 133 |
-
const reviewedAnswer = 'Maya should collect her pass at **Harbour Hub** on Friday, and she should bring a **passport** as her ID.\n\n**Evidence:**\n- "Reference J7 has moved to Harbour Hub."\n- "Harbour Hub requires a passport for replacement building pass collections."';
|
| 134 |
-
|
| 135 |
-
function element(tag, text) {
|
| 136 |
-
const node = document.createElement(tag);
|
| 137 |
-
if (text !== undefined) node.textContent = text;
|
| 138 |
-
return node;
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
function segments(source, indices) {
|
| 142 |
-
const chars = Array.from(source.text), mask = Array(chars.length).fill(false);
|
| 143 |
-
for (const index of indices) {
|
| 144 |
-
const [start, end] = source.offsets[index];
|
| 145 |
-
for (let i = start; i < end; i++) mask[i] = true;
|
| 146 |
-
}
|
| 147 |
-
const result = [];
|
| 148 |
-
for (let start = 0; start < chars.length;) {
|
| 149 |
-
let end = start + 1;
|
| 150 |
-
while (end < chars.length && mask[end] === mask[start]) end++;
|
| 151 |
-
result.push({text:chars.slice(start, end).join(""), selected:mask[start]});
|
| 152 |
-
start = end;
|
| 153 |
-
}
|
| 154 |
-
return result;
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
function prose(container, text, fraction = 1) {
|
| 158 |
-
const parts = text.split(/(\*\*[^*\n]+\*\*)/g).map(part => {
|
| 159 |
-
const bold = part.startsWith("**") && part.endsWith("**");
|
| 160 |
-
return {bold, chars:Array.from(bold ? part.slice(2, -2) : part)};
|
| 161 |
-
});
|
| 162 |
-
let remaining = Math.floor(parts.reduce((n, part) => n + part.chars.length, 0) * fraction);
|
| 163 |
-
if (container.dataset.characters === String(remaining)) return;
|
| 164 |
-
container.dataset.characters = String(remaining); container.replaceChildren();
|
| 165 |
-
for (const part of parts) {
|
| 166 |
-
const chars = part.chars.slice(0, remaining); remaining -= chars.length;
|
| 167 |
-
if (chars.length) container.append(element(part.bold ? "strong" : "span", chars.join("")));
|
| 168 |
-
}
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
function render(method) {
|
| 172 |
-
const phase = phases[method], recorded = trace.cases[0], selected = trace.sources.map(() => []);
|
| 173 |
-
if (method === "rclc" && phase) for (const [source, token] of recorded.rclc.hops[Math.min(phase,3) - 1].origins) selected[source].push(token);
|
| 174 |
-
const row = $(`${method}-row`), scroll = [...row.children].map(node => node.scrollTop);
|
| 175 |
-
row.replaceChildren();
|
| 176 |
-
trace.sources.forEach((source, i) => {
|
| 177 |
-
const chunk = element("div"); chunk.className = "chunk";
|
| 178 |
-
const name = document.querySelectorAll(".topology li span")[i].textContent;
|
| 179 |
-
chunk.dataset.agent = `Agent ${i + 1} · ${name}`;
|
| 180 |
-
chunk.tabIndex = 0; chunk.setAttribute("aria-label", `${method === "rclc" ? "Source document" : "Text handoff"} ${i + 1}: ${name}`);
|
| 181 |
-
if (method === "rclc") {
|
| 182 |
-
chunk.classList.add("source");
|
| 183 |
-
chunk.classList.toggle("revealing", playing && phase === i + 1);
|
| 184 |
-
for (const piece of segments(source, selected[i])) chunk.append(element(piece.selected ? "mark" : "span", piece.text));
|
| 185 |
-
} else if (phase > i) {
|
| 186 |
-
const message = recorded.text.hops[i].message;
|
| 187 |
-
prose(chunk, message.text || "(empty handoff)");
|
| 188 |
-
if (message.stop === "limit") chunk.append(element("p", "Handoff reached its token limit."));
|
| 189 |
-
}
|
| 190 |
-
row.append(chunk);
|
| 191 |
-
chunk.scrollTop = method === "text" && phase === i + 1 ? chunk.scrollHeight : scroll[i] || 0;
|
| 192 |
-
});
|
| 193 |
-
const answer = element("div"); answer.className = "chunk answer";
|
| 194 |
-
answer.dataset.agent = "Agent 4 · Final answer";
|
| 195 |
-
answer.tabIndex = 0; answer.setAttribute("aria-label", `${method === "rclc" ? "CacheBack" : "Text"} final answer`);
|
| 196 |
-
if (phase === 4) {
|
| 197 |
-
prose(answer, recorded[method].answer.text || "(empty answer)");
|
| 198 |
-
if (recorded[method].answer.stop === "limit") answer.append(element("p", "Answer reached its token limit."));
|
| 199 |
-
if (method === "rclc" && recorded.question === "Where should Maya collect her pass on Friday, and what ID should she bring?" && recorded.rclc.answer.text === reviewedAnswer) {
|
| 200 |
-
const check = element("p", "✓ Harbour Hub · Passport");
|
| 201 |
-
check.className = "answer-check"; answer.append(check);
|
| 202 |
-
}
|
| 203 |
-
}
|
| 204 |
-
row.append(answer); answer.scrollTop = phase === 4 ? answer.scrollHeight : scroll[3] || 0;
|
| 205 |
-
$(`${method}-time`).textContent = phase === 4 ? `${recorded[method].seconds.toFixed(2)} s` : "";
|
| 206 |
-
$("status").textContent = `${method === "rclc" ? "CacheBack" : "Text"}: ${phase === 4 ? "final answer received." : `${phase} of 3 handoffs received.`}`;
|
| 207 |
-
}
|
| 208 |
-
|
| 209 |
-
function play() {
|
| 210 |
-
if (playing || !trace) return;
|
| 211 |
-
playing = true; $("play").disabled = $("speed").disabled = true;
|
| 212 |
-
$("clear").hidden = false;
|
| 213 |
-
const speed = Number($("speed").value), deadlines = {};
|
| 214 |
-
for (const method of ["rclc", "text"]) {
|
| 215 |
-
const run = trace.cases[0][method];
|
| 216 |
-
let elapsed = 0;
|
| 217 |
-
deadlines[method] = [...run.hops.map(hop => elapsed += hop.seconds * 1000), run.seconds * 1000];
|
| 218 |
-
phases[method] = 0; render(method);
|
| 219 |
-
}
|
| 220 |
-
let start;
|
| 221 |
-
function advance(now) {
|
| 222 |
-
start ??= now;
|
| 223 |
-
const elapsed = (now - start) * speed;
|
| 224 |
-
for (const method of ["rclc", "text"]) {
|
| 225 |
-
const phase = deadlines[method].filter(ms => elapsed >= ms).length;
|
| 226 |
-
if (phase !== phases[method]) { phases[method] = phase; render(method); }
|
| 227 |
-
if (phase < 4) {
|
| 228 |
-
$(`${method}-time`).textContent = `${(elapsed / 1000).toFixed(2)} s`;
|
| 229 |
-
if (method === "text" || phase === 3) {
|
| 230 |
-
const begin = phase ? deadlines[method][phase - 1] : 0;
|
| 231 |
-
const message = phase === 3 ? trace.cases[0][method].answer : trace.cases[0].text.hops[phase].message;
|
| 232 |
-
const chunk = $(`${method}-row`).children[phase];
|
| 233 |
-
prose(chunk, message.text, (elapsed - begin) / (deadlines[method][phase] - begin));
|
| 234 |
-
chunk.scrollTop = chunk.scrollHeight;
|
| 235 |
-
}
|
| 236 |
-
}
|
| 237 |
-
}
|
| 238 |
-
if (phases.rclc < 4 || phases.text < 4) frame = requestAnimationFrame(advance);
|
| 239 |
-
else {
|
| 240 |
-
playing = false; $("play").disabled = $("speed").disabled = false;
|
| 241 |
-
$("play").textContent = "Replay";
|
| 242 |
-
}
|
| 243 |
-
}
|
| 244 |
-
frame = requestAnimationFrame(advance);
|
| 245 |
-
}
|
| 246 |
-
$("play").addEventListener("click", play);
|
| 247 |
-
$("clear").addEventListener("click", () => {
|
| 248 |
-
cancelAnimationFrame(frame); playing = false;
|
| 249 |
-
for (const method of ["rclc", "text"]) {
|
| 250 |
-
phases[method] = 0; render(method);
|
| 251 |
-
for (const chunk of $(`${method}-row`).children) chunk.scrollTop = 0;
|
| 252 |
-
}
|
| 253 |
-
$("play").disabled = $("speed").disabled = false;
|
| 254 |
-
$("play").textContent = "Run"; $("clear").hidden = true;
|
| 255 |
-
$("play").focus();
|
| 256 |
-
});
|
| 257 |
-
$("speed").addEventListener("change", () => {
|
| 258 |
-
$("timing-note").textContent = `${$("speed").value === "1" ? "" : `Playback ${$("speed").value}×. `}Estimated streaming, measured completion times.`;
|
| 259 |
-
});
|
| 260 |
-
|
| 261 |
-
async function load() {
|
| 262 |
-
try {
|
| 263 |
-
const response = await fetch("trace-w4.json");
|
| 264 |
-
if (!response.ok) throw new Error(`Recording unavailable (${response.status}).`);
|
| 265 |
-
trace = await response.json();
|
| 266 |
-
if (trace.schema !== 2 || trace.span_size !== 4 || trace.sources.length !== 3 || !trace.cases.length) throw new Error("Expected a W=4 three-handoff recording.");
|
| 267 |
-
$("question").textContent = trace.cases[0].question;
|
| 268 |
-
const hardware = trace.run.device.startsWith("cuda") ? trace.run.gpu.replace("NVIDIA ", "") : "CPU";
|
| 269 |
-
$("run-info").textContent = `Recorded ${trace.run.model.split("/").pop()} · ${hardware} · r${trace.ratio} · W=4${trace.complete === false ? " · Partial recording" : ""}`;
|
| 270 |
-
render("rclc"); render("text"); $("play").disabled = $("speed").disabled = false;
|
| 271 |
-
} catch (error) {
|
| 272 |
-
$("error").hidden = false;
|
| 273 |
-
$("error").textContent = `${error.message} Serve this folder with python -m http.server.`;
|
| 274 |
-
}
|
| 275 |
-
}
|
| 276 |
-
load();
|
| 277 |
-
</script>
|
| 278 |
</body>
|
| 279 |
</html>
|
|
|
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
<meta name="color-scheme" content="light">
|
| 7 |
+
<title>CacheBack | Multi-hop document Q&A</title>
|
| 8 |
+
<link rel="stylesheet" href="replay.css?v=selection-1">
|
| 9 |
+
<script src="replay.js?v=selection-1" defer></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
<main>
|
| 13 |
+
<header>CacheBack · Multi-hop document Q&A</header>
|
| 14 |
+
<h1 id="question">Where should Maya collect her pass on Friday, and what ID should she bring?</h1>
|
| 15 |
+
<p class="standalone-guide">In multi-agent question answering, a large document collection can be split across agents, each with its own context. Answering a question requires combining information across those contexts.</p>
|
| 16 |
+
<p class="standalone-guide">With text communication, agents write messages summarising what they read. CacheBack lets them pass latent thoughts together with a subset of internal state selected for what the next agent needs. This example shows selected-state handoffs: three agents read separate documents in sequence, and a fourth agent produces the final answer.</p>
|
| 17 |
+
<p class="standalone-guide">The top row shows each agent’s input document: a booking confirmation, a location-change notice, then an ID policy. Agents 2 and 3 also receive the previous agent’s handoff; Agent 4 answers from the final handoff alone. The bottom row shows the messages generated by the text agents.</p>
|
| 18 |
+
<div class="relay-bar">
|
| 19 |
+
<ol class="topology" aria-label="Agent topology">
|
| 20 |
+
<li><strong>1</strong><span>Booking reference</span></li>
|
| 21 |
+
<li><strong>2</strong><span>Collection desk</span></li>
|
| 22 |
+
<li><strong>3</strong><span>Required ID</span></li>
|
| 23 |
+
<li><strong>4</strong><span>Final answer</span></li>
|
| 24 |
+
</ol>
|
| 25 |
<div class="controls">
|
| 26 |
<select id="speed" aria-label="Playback speed" disabled>
|
| 27 |
<option value="1">1×</option>
|
|
|
|
| 29 |
<option value="4">4×</option>
|
| 30 |
</select>
|
| 31 |
<button id="clear" type="button" hidden>Clear</button>
|
| 32 |
+
<button id="play" type="button" disabled>Run <span aria-hidden="true">▶</span></button>
|
| 33 |
</div>
|
| 34 |
+
</div>
|
| 35 |
<p id="error" role="alert" hidden></p>
|
| 36 |
<div class="flow" aria-label="Four-agent relay">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
<section class="method" aria-labelledby="rclc-title">
|
| 38 |
+
<div class="method-heading">
|
| 39 |
+
<div class="method-name"><h2 id="rclc-title">CacheBack</h2><span class="channel-label">Select & pass internal state</span></div>
|
| 40 |
+
<div class="run-status"><span class="activity" id="rclc-activity">Ready</span><span class="time" id="rclc-time">0.00 s</span></div>
|
| 41 |
+
</div>
|
| 42 |
<div class="row" id="rclc-row"></div>
|
| 43 |
</section>
|
| 44 |
<section class="method" aria-labelledby="text-title">
|
| 45 |
+
<div class="method-heading">
|
| 46 |
+
<div class="method-name"><h2 id="text-title">Text</h2><span class="channel-label">Generate & pass messages</span></div>
|
| 47 |
+
<div class="run-status"><span class="activity" id="text-activity">Ready</span><span class="time" id="text-time">0.00 s</span></div>
|
| 48 |
+
</div>
|
| 49 |
<div class="row" id="text-row"></div>
|
| 50 |
</section>
|
| 51 |
</div>
|
| 52 |
<p class="sr-only" id="status" role="status" aria-live="polite"></p>
|
| 53 |
<footer>
|
| 54 |
+
<p><span id="run-info">Recorded replay</span> · <span id="timing-note">Measured times; selection and streaming animated for playback.</span></p>
|
| 55 |
<a href="https://github.com/agentcacheback/cacheback/tree/main/demo">Code & recording</a>
|
| 56 |
</footer>
|
| 57 |
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
</body>
|
| 59 |
</html>
|
demo/replay.css
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* { box-sizing:border-box; }
|
| 2 |
+
:root { --teal:#0f766e; --highlight:#a8e1d9; --line:#e0e8e6; --quiet:#7d8b88; }
|
| 3 |
+
body { margin:0; background:#fff; color:#222c2a; font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
|
| 4 |
+
main { width:min(1320px,calc(100% - 64px)); margin:24px auto; }
|
| 5 |
+
header { color:var(--teal); font-size:18px; font-weight:650; }
|
| 6 |
+
h1 { max-width:980px; margin:16px 0 12px; font-size:clamp(23px,3vw,28px); font-weight:550; letter-spacing:-.6px; line-height:1.3; }
|
| 7 |
+
h2 { margin:0; font-size:17px; font-weight:650; }
|
| 8 |
+
button,a { -webkit-tap-highlight-color:transparent; }
|
| 9 |
+
button { font:inherit; cursor:pointer; border:0; border-radius:8px; background:#222c2a; color:#fff; padding:10px 18px; white-space:nowrap; }
|
| 10 |
+
button:hover { background:var(--teal); }
|
| 11 |
+
button:disabled { cursor:default; opacity:.45; }
|
| 12 |
+
button>span { margin-left:10px; font-size:11px; }
|
| 13 |
+
.controls { display:flex; align-items:center; gap:14px; }
|
| 14 |
+
#speed { font:inherit; font-size:13px; color:#667570; border:0; background:transparent; padding:8px 2px; cursor:pointer; }
|
| 15 |
+
#clear { background:none; color:#667570; padding:8px 0; font-size:13px; }
|
| 16 |
+
:focus-visible { outline:2px solid var(--teal); outline-offset:4px; }
|
| 17 |
+
.standalone-guide { color:#667570; font-size:13px; margin-bottom:28px; }
|
| 18 |
+
.relay-bar { display:flex; align-items:center; gap:24px; padding-bottom:16px; border-bottom:1px solid var(--line); }
|
| 19 |
+
.topology { display:flex; flex:1; min-width:0; gap:25px; margin:0; padding:0; list-style:none; }
|
| 20 |
+
.topology li { position:relative; display:flex; align-items:center; gap:8px; flex:1; min-width:0; font-size:12px; }
|
| 21 |
+
.topology strong { display:grid; place-items:center; flex-shrink:0; width:23px; height:23px; background:#f1f5f4; border-radius:50%; color:#5f706a; font-size:11px; }
|
| 22 |
+
.topology span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 23 |
+
.topology li:not(:last-child)::after { content:"→"; position:absolute; right:-19px; color:#8c9b97; font-size:16px; }
|
| 24 |
+
.topology li.reached strong { background:#d5eeea; color:var(--teal); }
|
| 25 |
+
.method { margin-top:22px; }
|
| 26 |
+
.method-heading { display:flex; align-items:center; justify-content:space-between; gap:14px; margin-bottom:12px; }
|
| 27 |
+
.method-name,.run-status { display:flex; align-items:center; gap:12px; }
|
| 28 |
+
.channel-label { font-size:12px; color:#6b7974; }
|
| 29 |
+
#rclc-title { color:var(--teal); }
|
| 30 |
+
.activity { display:flex; align-items:center; gap:6px; font-size:11px; color:#7d8b88; }
|
| 31 |
+
.activity::before { content:""; width:5px; height:5px; border-radius:50%; background:currentColor; }
|
| 32 |
+
.activity.running::before { animation:pulse 1.2s ease-in-out infinite; }
|
| 33 |
+
.activity.complete { color:var(--teal); }
|
| 34 |
+
.time { min-width:78px; text-align:right; font-size:19px; line-height:1; letter-spacing:-.4px; font-variant-numeric:tabular-nums; }
|
| 35 |
+
#rclc-time { color:var(--teal); }
|
| 36 |
+
.row { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:24px; }
|
| 37 |
+
.chunk { position:relative; height:320px; min-width:0; border-top:1px solid var(--line); padding-top:12px; display:flex; flex-direction:column; }
|
| 38 |
+
.source-title { display:flex; justify-content:space-between; gap:8px; margin:0 0 12px; color:#53665f; font-size:11px; font-weight:600; }
|
| 39 |
+
.source-title>span { color:#90a09a; font-weight:400; white-space:nowrap; }
|
| 40 |
+
.document-body,.message-body { flex:1; min-height:0; overflow:auto; scrollbar-width:thin; scrollbar-color:#c9d4d0 transparent; padding-right:10px; white-space:pre-wrap; overflow-wrap:anywhere; }
|
| 41 |
+
.document-body { font:11.5px/1.85 Georgia,"Times New Roman",serif; color:#68776f; }
|
| 42 |
+
mark { color:#104f47; background:var(--highlight); box-decoration-break:clone; -webkit-box-decoration-break:clone; }
|
| 43 |
+
.source.is-selecting mark { animation:highlight var(--selection-duration,.3s) ease-out both; }
|
| 44 |
+
.source.is-selecting .document-body { background:linear-gradient(180deg,transparent,#eef9f6 50%,transparent) 0 -300px/100% 150px no-repeat; animation:scan var(--selection-duration,.3s) linear both; }
|
| 45 |
+
.position-map { width:100%; height:13px; display:block; margin:12px 0 0; border-radius:2px; }
|
| 46 |
+
.position-map rect { fill:#e7edeb; }
|
| 47 |
+
.position-map rect.selected { fill:var(--teal); }
|
| 48 |
+
.source.is-selecting .position-map { animation:pulse .65s ease-in-out infinite; }
|
| 49 |
+
.source>.position-map { margin:0; flex-shrink:0; }
|
| 50 |
+
.source>.handoff { margin-bottom:12px; }
|
| 51 |
+
.handoff { display:flex; align-items:center; gap:8px; position:relative; height:29px; flex-shrink:0; color:#95a49e; font-size:10px; }
|
| 52 |
+
.handoff>span { position:relative; z-index:1; background:white; padding-right:8px; }
|
| 53 |
+
.handoff::after { content:"→"; position:absolute; right:-18px; color:#b3c2bc; font-size:16px; }
|
| 54 |
+
.handoff-track { position:absolute; left:0; right:-13px; bottom:5px; height:1px; background:#e7eeeb; }
|
| 55 |
+
.packet { position:absolute; left:0; top:-4px; display:flex; gap:2px; opacity:0; }
|
| 56 |
+
.packet i { width:5px; height:9px; border-radius:1px; background:var(--teal); }
|
| 57 |
+
.source.has-passed .handoff { color:var(--teal); }
|
| 58 |
+
.source.has-passed .handoff::after { color:var(--teal); }
|
| 59 |
+
.source.has-passed .packet { animation:pass-state 1.1s cubic-bezier(.2,.6,.3,1) both; }
|
| 60 |
+
.message-body { font-size:12px; line-height:1.75; color:#56665f; }
|
| 61 |
+
.message-title { color:#788980; }
|
| 62 |
+
.message-card.is-writing .message-body::after { content:""; display:inline-block; height:1em; width:2px; background:#55665e; vertical-align:text-bottom; margin-left:3px; animation:cursor .8s steps(1) infinite; }
|
| 63 |
+
.message-card .handoff::after { color:#9ca9a3; }
|
| 64 |
+
.message-card.has-passed .handoff { color:#53665f; }
|
| 65 |
+
.placeholder { color:#a4afa9; font-size:11px; line-height:1.65; white-space:normal; margin:0; }
|
| 66 |
+
.waiting-state { display:flex; gap:5px; margin:24px 0 12px; }
|
| 67 |
+
.waiting-state i { width:6px; height:6px; background:#e2e9e6; border-radius:50%; }
|
| 68 |
+
.is-writing .waiting-state i { animation:pulse .9s ease-in-out infinite; }
|
| 69 |
+
.answer { padding:12px 14px; border:1px solid var(--line); border-radius:8px; background:#f9fbfa; }
|
| 70 |
+
.answer .source-title { margin-bottom:12px; }
|
| 71 |
+
.answer-check { margin:0 0 12px; font-size:16px; font-weight:650; line-height:1.45; color:var(--teal); }
|
| 72 |
+
.answer.complete { background:#f1f9f7; border-color:#c9e7df; }
|
| 73 |
+
.answer.unresolved { background:#fcfaf6; border-color:#ece4d4; }
|
| 74 |
+
.unresolved .answer-check { color:#956724; font-size:14px; }
|
| 75 |
+
.answer.is-writing .source-title { color:var(--teal); }
|
| 76 |
+
.answer .message-body { font-size:12px; }
|
| 77 |
+
strong { font-weight:650; }
|
| 78 |
+
footer { display:flex; justify-content:space-between; gap:20px; margin-top:24px; font-size:10px; color:#8a9992; }
|
| 79 |
+
footer p { margin:0; }
|
| 80 |
+
footer a { flex-shrink:0; }
|
| 81 |
+
a { color:inherit; text-underline-offset:3px; }
|
| 82 |
+
#error { color:#8e2b1d; }
|
| 83 |
+
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
|
| 84 |
+
.embedded header,.embedded #question,.embedded .standalone-guide { display:none; }
|
| 85 |
+
.embedded main { margin:20px auto; }
|
| 86 |
+
@keyframes highlight { from { background-color:transparent; color:#68776f; } to { background-color:var(--highlight); color:#104f47; } }
|
| 87 |
+
@keyframes scan { to { background-position:0 400px; } }
|
| 88 |
+
@keyframes pulse { 50% { opacity:.4; } }
|
| 89 |
+
@keyframes cursor { 50% { opacity:0; } }
|
| 90 |
+
@keyframes pass-state { 0% { left:0; opacity:0; } 15% { opacity:1; } 80% { opacity:1; } 100% { left:calc(100% - 24px); opacity:0; } }
|
| 91 |
+
@media(max-width:900px) {
|
| 92 |
+
main { width:calc(100% - 36px); }
|
| 93 |
+
.relay-bar { gap:16px; }
|
| 94 |
+
.topology { gap:16px; }
|
| 95 |
+
.topology li { gap:5px; font-size:10px; }
|
| 96 |
+
.topology li:not(:last-child)::after { right:-12px; font-size:13px; }
|
| 97 |
+
.controls { gap:8px; }
|
| 98 |
+
button { padding:9px 12px; }
|
| 99 |
+
.row { gap:16px; }
|
| 100 |
+
.handoff::after { right:-13px; }
|
| 101 |
+
.handoff-track { right:-8px; }
|
| 102 |
+
.source-title>span { display:none; }
|
| 103 |
+
}
|
| 104 |
+
@media(max-width:680px) {
|
| 105 |
+
.relay-bar { flex-wrap:wrap; }
|
| 106 |
+
.topology { flex-basis:100%; }
|
| 107 |
+
.controls { margin-left:auto; }
|
| 108 |
+
.method-heading { align-items:flex-start; }
|
| 109 |
+
.method-name { align-items:flex-start; flex-direction:column; gap:2px; }
|
| 110 |
+
.run-status { flex-direction:column-reverse; align-items:flex-end; gap:7px; }
|
| 111 |
+
.time { font-size:18px; }
|
| 112 |
+
.row { grid-template-columns:repeat(2,minmax(0,1fr)); gap:20px; }
|
| 113 |
+
.chunk { height:280px; }
|
| 114 |
+
.chunk:nth-child(2) .handoff::after { right:0; content:"↓"; }
|
| 115 |
+
.chunk:nth-child(2) .handoff-track { right:5px; }
|
| 116 |
+
footer { flex-direction:column; gap:8px; }
|
| 117 |
+
}
|
| 118 |
+
@media(max-width:420px) {
|
| 119 |
+
main { width:calc(100% - 28px); }
|
| 120 |
+
.row { gap:14px; }
|
| 121 |
+
.document-body { font-size:10.5px; }
|
| 122 |
+
.message-body { font-size:11px; }
|
| 123 |
+
.channel-label { font-size:10px; }
|
| 124 |
+
.source-title { font-size:10px; }
|
| 125 |
+
.topology span { font-size:9px; }
|
| 126 |
+
.topology strong { width:18px; height:18px; font-size:9px; }
|
| 127 |
+
.answer { padding-inline:10px; }
|
| 128 |
+
.answer-check { font-size:14px; }
|
| 129 |
+
}
|
| 130 |
+
@media(prefers-reduced-motion:reduce) { *,*::before,*::after { animation:none !important; } }
|
demo/replay.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const $ = id => document.getElementById(id);
|
| 2 |
+
const host = window.frameElement;
|
| 3 |
+
if (host) {
|
| 4 |
+
document.body.classList.add('embedded');
|
| 5 |
+
const main = document.querySelector('main');
|
| 6 |
+
new ResizeObserver(() => {
|
| 7 |
+
const margin = parseFloat(getComputedStyle(main).marginBottom);
|
| 8 |
+
host.style.height = `${Math.ceil(main.offsetTop + main.offsetHeight + margin) + 1}px`;
|
| 9 |
+
}).observe(main);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
let trace, playing = false, frame;
|
| 13 |
+
const phases = {rclc:0, text:0};
|
| 14 |
+
const cards = {rclc:[], text:[]};
|
| 15 |
+
const titles = ['Booking confirmation', 'Location-change notice', 'ID policy'];
|
| 16 |
+
const question = 'Where should Maya collect her pass on Friday, and what ID should she bring?';
|
| 17 |
+
const reviewedAnswer = 'Maya should collect her pass at **Harbour Hub** on Friday, and she should bring a **passport** as her ID.\n\n**Evidence:**\n- "Reference J7 has moved to Harbour Hub."\n- "Harbour Hub requires a passport for replacement building pass collections."';
|
| 18 |
+
|
| 19 |
+
function element(tag, text, className) {
|
| 20 |
+
const node = document.createElement(tag);
|
| 21 |
+
if (text !== undefined) node.textContent = text;
|
| 22 |
+
if (className) node.className = className;
|
| 23 |
+
return node;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function segments(source, indices) {
|
| 27 |
+
const chars = Array.from(source.text), mask = Array(chars.length).fill(false);
|
| 28 |
+
for (const index of indices) {
|
| 29 |
+
const [start, end] = source.offsets[index];
|
| 30 |
+
for (let i = start; i < end; i++) mask[i] = true;
|
| 31 |
+
}
|
| 32 |
+
const result = [];
|
| 33 |
+
for (let start = 0; start < chars.length;) {
|
| 34 |
+
let end = start + 1;
|
| 35 |
+
while (end < chars.length && mask[end] === mask[start]) end++;
|
| 36 |
+
result.push({text:chars.slice(start, end).join(''), selected:mask[start]});
|
| 37 |
+
start = end;
|
| 38 |
+
}
|
| 39 |
+
return result;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function prose(container, text, fraction = 1) {
|
| 43 |
+
const parts = text.split(/(\*\*[^*\n]+\*\*)/g).map(part => {
|
| 44 |
+
const bold = part.startsWith('**') && part.endsWith('**');
|
| 45 |
+
return {bold, chars:Array.from(bold ? part.slice(2, -2) : part)};
|
| 46 |
+
});
|
| 47 |
+
let remaining = Math.floor(parts.reduce((n, part) => n + part.chars.length, 0) * Math.min(1, Math.max(0, fraction)));
|
| 48 |
+
if (container.dataset.characters === String(remaining)) return;
|
| 49 |
+
container.dataset.characters = String(remaining); container.replaceChildren();
|
| 50 |
+
for (const part of parts) {
|
| 51 |
+
const chars = part.chars.slice(0, remaining); remaining -= chars.length;
|
| 52 |
+
if (chars.length) container.append(element(part.bold ? 'strong' : 'span', chars.join('')));
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function positionMap(source) {
|
| 57 |
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
| 58 |
+
svg.classList.add('position-map');
|
| 59 |
+
svg.setAttribute('viewBox', `0 0 ${source.offsets.length} 6`);
|
| 60 |
+
svg.setAttribute('preserveAspectRatio', 'none');
|
| 61 |
+
svg.setAttribute('role', 'img'); svg.setAttribute('aria-label', 'Source positions; teal positions are selected');
|
| 62 |
+
source.offsets.forEach((_, i) => {
|
| 63 |
+
const rect = document.createElementNS(svg.namespaceURI, 'rect');
|
| 64 |
+
rect.setAttribute('x', i); rect.setAttribute('y', 0);
|
| 65 |
+
rect.setAttribute('width', '.8'); rect.setAttribute('height', '6');
|
| 66 |
+
svg.append(rect);
|
| 67 |
+
});
|
| 68 |
+
return svg;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function makeCards(method) {
|
| 72 |
+
for (let i = 0; i < 4; i++) {
|
| 73 |
+
const answer = i === 3, source = method === 'rclc' && !answer;
|
| 74 |
+
const card = element('div', undefined, `chunk ${answer ? 'answer message-card' : source ? 'source' : 'message-card'}`);
|
| 75 |
+
card.setAttribute('aria-label', answer ? `${method === 'rclc' ? 'CacheBack' : 'Text'} final answer` : source ? `Full source document ${i + 1}: ${titles[i]}` : `Generated message from Agent ${i + 1}`);
|
| 76 |
+
const title = element('p', answer ? 'Final answer' : source ? titles[i] : `Agent ${i + 1}’s message`, 'source-title');
|
| 77 |
+
if (source) title.append(element('span', `Agent ${i + 1}`));
|
| 78 |
+
card.append(title);
|
| 79 |
+
const badge = element('p', '', 'answer-check'); badge.hidden = true;
|
| 80 |
+
if (answer) card.append(badge);
|
| 81 |
+
const body = element('div', undefined, source ? 'document-body' : 'message-body');
|
| 82 |
+
body.tabIndex = 0; card.append(body);
|
| 83 |
+
let map, handoff, label;
|
| 84 |
+
if (source) { map = positionMap(trace.sources[i]); card.append(map); }
|
| 85 |
+
if (!answer) {
|
| 86 |
+
handoff = element('div', undefined, 'handoff');
|
| 87 |
+
label = element('span', 'Waiting'); handoff.append(label);
|
| 88 |
+
const track = element('div', undefined, 'handoff-track'); track.setAttribute('aria-hidden', 'true');
|
| 89 |
+
if (source) {
|
| 90 |
+
const packet = element('div', undefined, 'packet');
|
| 91 |
+
for (let n = 0; n < 4; n++) packet.append(element('i'));
|
| 92 |
+
track.append(packet);
|
| 93 |
+
}
|
| 94 |
+
handoff.append(track); card.append(handoff);
|
| 95 |
+
}
|
| 96 |
+
if (source) card.prepend(map, handoff);
|
| 97 |
+
$(`${method}-row`).append(card);
|
| 98 |
+
cards[method].push({card, body, map, badge, label});
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
function render(method) {
|
| 103 |
+
const phase = phases[method], recorded = trace.cases[0];
|
| 104 |
+
const selected = trace.sources.map(() => []);
|
| 105 |
+
if (method === 'rclc' && (playing || phase)) {
|
| 106 |
+
// Reveal the recorded target selection during each handoff's playback interval.
|
| 107 |
+
const hop = playing && phase < 3 ? phase : Math.min(phase, 3) - 1;
|
| 108 |
+
for (const [source, token] of recorded.rclc.hops[hop].origins) selected[source].push(token);
|
| 109 |
+
}
|
| 110 |
+
cards[method].forEach(({card, body, map, badge, label}, i) => {
|
| 111 |
+
const active = playing && phase === i, passed = phase > i;
|
| 112 |
+
card.classList.toggle('is-selecting', method === 'rclc' && i < 3 && active);
|
| 113 |
+
card.classList.toggle('is-writing', active && (method === 'text' || i === 3));
|
| 114 |
+
card.classList.toggle('has-passed', passed);
|
| 115 |
+
if (method === 'rclc' && i < 3) {
|
| 116 |
+
card.style.setProperty('--selection-duration', `${recorded.rclc.hops[i].seconds / Number($('speed').value)}s`);
|
| 117 |
+
const scroll = body.scrollTop;
|
| 118 |
+
body.replaceChildren(...segments(trace.sources[i], selected[i]).map(piece => element(piece.selected ? 'mark' : 'span', piece.text)));
|
| 119 |
+
body.scrollTop = scroll;
|
| 120 |
+
const indices = new Set(selected[i]);
|
| 121 |
+
[...map.children].forEach((rect, n) => rect.classList.toggle('selected', indices.has(n)));
|
| 122 |
+
label.textContent = passed ? 'Selected state passed' : active ? 'Selecting positions' : 'Full source document';
|
| 123 |
+
} else {
|
| 124 |
+
delete body.dataset.characters;
|
| 125 |
+
if (passed) {
|
| 126 |
+
const message = i === 3 ? recorded[method].answer : recorded.text.hops[i].message;
|
| 127 |
+
prose(body, message.text || '(empty message)');
|
| 128 |
+
body.scrollTop = 0;
|
| 129 |
+
} else {
|
| 130 |
+
body.replaceChildren();
|
| 131 |
+
if (!active) {
|
| 132 |
+
const dots = element('div', undefined, 'waiting-state'); dots.setAttribute('aria-hidden', 'true');
|
| 133 |
+
for (let n = 0; n < 3; n++) dots.append(element('i'));
|
| 134 |
+
body.append(dots, element('p', i === 3 ? 'Waiting for the handoffs.' : 'Waiting to generate a message.', 'placeholder'));
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
if (label) label.textContent = passed ? 'Message passed' : active ? 'Generating message' : 'Waiting';
|
| 138 |
+
}
|
| 139 |
+
if (i === 3) {
|
| 140 |
+
const done = phase === 4, reviewed = recorded.question === question;
|
| 141 |
+
card.classList.toggle('complete', done && method === 'rclc');
|
| 142 |
+
const unresolved = done && reviewed && method === 'text' && recorded.text.answer.text.startsWith('Maya should collect her pass at the assigned desk based on her reference J7');
|
| 143 |
+
card.classList.toggle('unresolved', unresolved);
|
| 144 |
+
badge.hidden = true;
|
| 145 |
+
if (done && reviewed && method === 'rclc' && recorded.rclc.answer.text === reviewedAnswer) {
|
| 146 |
+
badge.textContent = '✓ Harbour Hub · Passport'; badge.hidden = false;
|
| 147 |
+
} else if (unresolved) {
|
| 148 |
+
badge.textContent = 'Collection desk unresolved'; badge.hidden = false;
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
});
|
| 152 |
+
const activity = $(`${method}-activity`);
|
| 153 |
+
activity.textContent = phase === 4 ? 'Finished' : playing ? phase === 3 ? 'Generating answer' : method === 'rclc' ? 'Selecting & passing' : 'Generating message' : 'Ready';
|
| 154 |
+
activity.classList.toggle('running', playing && phase < 4);
|
| 155 |
+
activity.classList.toggle('complete', phase === 4);
|
| 156 |
+
$(`${method}-time`).textContent = phase === 4 ? `${recorded[method].seconds.toFixed(2)} s` : '0.00 s';
|
| 157 |
+
if (method === 'rclc') document.querySelectorAll('.topology li').forEach((node, i) => node.classList.toggle('reached', phase > i));
|
| 158 |
+
$('status').textContent = `${method === 'rclc' ? 'CacheBack' : 'Text'}: ${phase === 4 ? 'final answer received.' : `${phase} of 3 handoffs received.`}`;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
function reset() {
|
| 162 |
+
cancelAnimationFrame(frame); playing = false;
|
| 163 |
+
for (const method of ['rclc', 'text']) {
|
| 164 |
+
phases[method] = 0; render(method);
|
| 165 |
+
cards[method].forEach(({body}) => { body.scrollTop = 0; });
|
| 166 |
+
}
|
| 167 |
+
$('play').disabled = $('speed').disabled = false;
|
| 168 |
+
$('play').textContent = 'Run ▶'; $('clear').hidden = true;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
function play() {
|
| 172 |
+
if (playing || !trace) return;
|
| 173 |
+
reset(); playing = true; $('play').disabled = $('speed').disabled = true;
|
| 174 |
+
$('play').textContent = 'Running';
|
| 175 |
+
$('clear').hidden = false;
|
| 176 |
+
const speed = Number($('speed').value), deadlines = {};
|
| 177 |
+
for (const method of ['rclc', 'text']) {
|
| 178 |
+
let elapsed = 0;
|
| 179 |
+
const run = trace.cases[0][method];
|
| 180 |
+
deadlines[method] = [...run.hops.map(hop => elapsed += hop.seconds * 1000), run.seconds * 1000];
|
| 181 |
+
render(method);
|
| 182 |
+
}
|
| 183 |
+
let start;
|
| 184 |
+
function advance(now) {
|
| 185 |
+
start ??= now;
|
| 186 |
+
const elapsed = (now - start) * speed;
|
| 187 |
+
for (const method of ['rclc', 'text']) {
|
| 188 |
+
const phase = deadlines[method].filter(ms => elapsed >= ms).length;
|
| 189 |
+
if (phase !== phases[method]) { phases[method] = phase; render(method); }
|
| 190 |
+
if (phase < 4) {
|
| 191 |
+
$(`${method}-time`).textContent = `${(elapsed / 1000).toFixed(2)} s`;
|
| 192 |
+
if (method === 'text' || phase === 3) {
|
| 193 |
+
const begin = phase ? deadlines[method][phase - 1] : 0;
|
| 194 |
+
const message = phase === 3 ? trace.cases[0][method].answer : trace.cases[0].text.hops[phase].message;
|
| 195 |
+
const body = cards[method][phase].body;
|
| 196 |
+
prose(body, message.text, (elapsed - begin) / (deadlines[method][phase] - begin));
|
| 197 |
+
body.scrollTop = body.scrollHeight;
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
if (phases.rclc < 4 || phases.text < 4) frame = requestAnimationFrame(advance);
|
| 202 |
+
else {
|
| 203 |
+
playing = false; $('play').disabled = $('speed').disabled = false;
|
| 204 |
+
$('play').textContent = 'Replay';
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
frame = requestAnimationFrame(advance);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
$('play').addEventListener('click', play);
|
| 211 |
+
$('clear').addEventListener('click', () => { reset(); $('play').focus(); });
|
| 212 |
+
$('speed').addEventListener('change', () => {
|
| 213 |
+
$('timing-note').textContent = `${$('speed').value === '1' ? '' : `Playback ${$('speed').value}×. `}Measured times; selection and streaming animated for playback.`;
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
+
async function load() {
|
| 217 |
+
try {
|
| 218 |
+
const response = await fetch('trace-w4.json');
|
| 219 |
+
if (!response.ok) throw new Error(`Recording unavailable (${response.status}).`);
|
| 220 |
+
trace = await response.json();
|
| 221 |
+
if (trace.schema !== 2 || trace.span_size !== 4 || trace.sources.length !== 3 || !trace.cases.length) throw new Error('Expected a W=4 three-handoff recording.');
|
| 222 |
+
$('question').textContent = trace.cases[0].question;
|
| 223 |
+
const hardware = trace.run.device.startsWith('cuda') ? trace.run.gpu.replace('NVIDIA ', '') : 'CPU';
|
| 224 |
+
$('run-info').textContent = `Recorded ${trace.run.model.split('/').pop()} · ${hardware} · r${trace.ratio} · W=4${trace.complete === false ? ' · Partial recording' : ''}`;
|
| 225 |
+
for (const method of ['rclc', 'text']) { makeCards(method); render(method); }
|
| 226 |
+
$('play').disabled = $('speed').disabled = false;
|
| 227 |
+
} catch (error) {
|
| 228 |
+
$('error').hidden = false;
|
| 229 |
+
$('error').textContent = `${error.message} Serve this folder with python -m http.server.`;
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
load();
|
index.html
CHANGED
|
@@ -19,7 +19,9 @@
|
|
| 19 |
<link rel="canonical" href="https://agentcacheback.github.io/">
|
| 20 |
<title>Receiver-Conditioned Latent Communication gives 94% CacheBack</title>
|
| 21 |
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%231c211e'/%3E%3Cpath d='M7 7h7v7H7zm11 0h7v7h-7zM7 18h7v7H7z' fill='%230f766e'/%3E%3Cpath d='M18 18h7v7h-7z' fill='%23fff'/%3E%3C/svg%3E">
|
| 22 |
-
<link rel="stylesheet" href="website/assets/styles.css?v=
|
|
|
|
|
|
|
| 23 |
<script type="module" src="website/assets/app.js"></script>
|
| 24 |
</head>
|
| 25 |
<body>
|
|
@@ -37,7 +39,7 @@
|
|
| 37 |
<ul aria-label="Authors"><li>Maximillian Rossi</li><li>Prajwal Raghunath</li><li>Haoqing Xuan</li><li>Yusen Zhang</li><li>Eugene Wu</li></ul>
|
| 38 |
<p class="affiliations"><a href="https://daplab.cs.columbia.edu/"><img src="https://daplab.cs.columbia.edu/files/images/daplab_logo_horiz.png" width="1508" height="331" alt="DAP Lab"></a><span>Columbia University</span></p>
|
| 39 |
</div>
|
| 40 |
-
<div class="tldr"><p><b>TLDR;</b> Agents can share <strong>internal model state</strong> instead of generating text messages. Sending all of it, however, overwhelms the receiver.</p><p><strong>The receiver asks for what it needs.</strong> CacheBack uses attention to that request to select which state gets sent. Simple, robust, and <strong>training-free.</strong></p><p>
|
| 41 |
<nav class="resource-links" aria-label="Project resources">
|
| 42 |
<a href="https://arxiv.org/pdf/2609.32046">Paper <span>PDF</span></a>
|
| 43 |
<a href="https://github.com/agentcacheback/cacheback">Code</a>
|
|
@@ -47,62 +49,73 @@
|
|
| 47 |
</nav>
|
| 48 |
</div>
|
| 49 |
<aside class="hero-results" aria-labelledby="hero-results-title">
|
| 50 |
-
<h2 id="hero-results-title" class="eyebrow">FANOUTQA</h2><p class="visual-note">
|
| 51 |
<figure id="result-summary" class="comparison-plot" aria-labelledby="hero-results-title">
|
| 52 |
<div class="plot-legend"><span><i></i>Text · same-size senders</span><span class="rclc-emphasis"><i></i>CacheBack</span></div>
|
| 53 |
<div class="paired-metrics chart-heading"><div><strong>Strict accuracy (%) ↑</strong></div><div><strong>Median time (s) ↓</strong></div></div>
|
| 54 |
-
<div class="model-row" data-model="qwen"><div class="model-heading paired-metrics"><div class="model-label"><h3>Qwen 3 <small>8B</small></h3><strong class="accuracy-gain">+14.7<abbr title="percentage points">pp</abbr></strong></div><
|
| 55 |
-
<div class="model-row" data-model="nemotron"><div class="model-heading paired-metrics"><div class="model-label"><h3>Nemotron <small>12B</small></h3><strong class="accuracy-gain">+11.3<abbr title="percentage points">pp</abbr></strong></div><
|
| 56 |
-
<div class="model-row" data-model="gemma"><div class="model-heading paired-metrics"><div class="model-label"><h3>Gemma 4 <small>12B</small></h3><strong class="accuracy-gain">+7.3<abbr title="percentage points">pp</abbr></strong></div><
|
| 57 |
-
<div class="model-row" data-model="ministral"><div class="model-heading paired-metrics"><div class="model-label"><h3>Ministral 3 <small>14B</small></h3><strong class="accuracy-gain">+20.7<abbr title="percentage points">pp</abbr></strong></div><
|
| 58 |
<div class="paired-metrics chart-axes" aria-hidden="true"><div class="chart-axis"><span>0</span><span>50</span><span>100</span></div><div class="chart-axis"><span>0</span><span>800</span><span>1,600</span></div></div>
|
| 59 |
</figure>
|
| 60 |
|
| 61 |
</aside>
|
| 62 |
</section>
|
| 63 |
-
<section id="
|
| 64 |
-
<
|
| 65 |
-
<
|
| 66 |
-
|
| 67 |
-
<
|
|
|
|
| 68 |
</div>
|
|
|
|
|
|
|
| 69 |
</section>
|
| 70 |
-
<section id="
|
| 71 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
</section>
|
| 73 |
<section id="results" class="section wrap" aria-labelledby="results-title">
|
| 74 |
<h2 id="results-title">Results</h2>
|
| 75 |
<div class="paper-curves" id="paper-curves">
|
| 76 |
-
<div class="benchmark-setup"><div class="curves-heading"><h3>FanOutQA</h3><p id="compression-guide">Questions require combining evidence across Wikipedia pages. We split at least 120K tokens among three parallel senders; a receiver answers from their messages. Strict accuracy requires every reference-answer group. <strong>Higher is more accurate; left is faster.</strong> Each panel fixes the receiver model. Text labels give sender size; CacheBack uses same-size senders. <strong>
|
| 77 |
<div class="topology-diagram parallel" role="img" aria-label="Three senders read separate Wikipedia pages in parallel. Their messages converge on one receiver, which answers."><div class="senders"><div class="agent-node"><small>Wikipedia pages A</small><strong>Sender 1</strong></div><div class="agent-node"><small>Wikipedia pages B</small><strong>Sender 2</strong></div><div class="agent-node"><small>Wikipedia pages C</small><strong>Sender 3</strong></div></div><div class="merge" aria-hidden="true"><span>messages</span></div><div class="agent-node receiver"><small>Combines 3 messages</small><strong>Receiver</strong><small>↓ Answer</small></div></div></div>
|
| 78 |
-
<div class="curve-legend" aria-label="Curve legend"><span class="rclc-emphasis">● ━ CacheBack</span><span class="text-emphasis">▲ ┄ Text · labels show sender size</span><span>× Question only</span></div>
|
| 79 |
<div class="curve-grid">
|
| 80 |
-
<figure><img src="website/assets/figures/fanoutqa-qwen.svg?v=
|
| 81 |
-
<figure><img src="website/assets/figures/fanoutqa-nemotron.svg?v=
|
| 82 |
</div>
|
| 83 |
-
<div class="chain-results" id="chain-results"><div class="benchmark-setup"><div class="curves-heading"><h3>LongBench v2 Easy</h3><p>Long-document questions test whether evidence survives repeated handoffs. We split each 100K-246K-token document into four equal parts. Each sender reads its part plus the previous message; a final receiver answers from the fourth message.
|
| 84 |
<div class="topology-diagram sequential" role="img" aria-label="Four agents read document parts in order. Each passes a message to the next. A final receiver answers from the fourth message."><div class="agent-node"><small>Document part 1</small><strong>Agent 1</strong></div><div class="agent-node"><small>Document part 2</small><strong>Agent 2</strong></div><div class="agent-node"><small>Document part 3</small><strong>Agent 3</strong></div><div class="agent-node"><small>Document part 4</small><strong>Agent 4</strong></div><div class="agent-node receiver"><small>Final message</small><strong>Receiver</strong><small>↓ Answer</small></div></div></div>
|
| 85 |
-
<div class="curve-legend" aria-label="LongBench curve legend"><span class="rclc-emphasis">● ━ CacheBack ·
|
| 86 |
<div class="curve-grid">
|
| 87 |
-
<figure><img src="website/assets/figures/longbench-qwen.svg?v=
|
| 88 |
-
<figure><img src="website/assets/figures/longbench-nemotron.svg?v=
|
| 89 |
</div>
|
| 90 |
</div>
|
| 91 |
<p class="curve-caption">Lines join the best accuracy-time tradeoffs within each channel; faded points are dominated settings. × means the receiver gets only the question. 50 concurrent tasks on 8 H100s; accuracy averaged over three receiver draws. Completion includes queueing. <a href="https://arxiv.org/pdf/2609.32046">Full setup and results in the paper.</a></p>
|
| 92 |
</div>
|
| 93 |
</section>
|
| 94 |
-
<section id="demo" class="
|
| 95 |
-
<div class="
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
<div class="demo-caption" id="coding-caption"><p><strong>One recorded case:</strong> CacheBack 25.66 s; text 113.21 s. Separate recorded runs are aligned at their start. Startup and test grading excluded. Silent video; playback speed varies. <a href="demo/coding/rclc-cacheback-4k.mp4">Open in full window</a></p>
|
| 104 |
-
</div>
|
| 105 |
-
</article>
|
| 106 |
</div>
|
| 107 |
</section>
|
| 108 |
<section class="code-section" id="code" aria-labelledby="code-title"><div class="wrap code-layout"><div><h2 id="code-title">Use CacheBack</h2><p>Install <code>rclc</code>, bind your agents, and pass the receiver’s request.</p><a class="button primary" href="https://github.com/agentcacheback/cacheback">Code and documentation</a></div><div class="code-card"><div class="code-bar"><span>Python</span><button class="copy-button" data-copy="quickstart" type="button" aria-label="Copy code"><svg class="copy-icon" aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3"/></svg><span class="copy-feedback" aria-hidden="true">Copied</span></button></div><pre><code id="quickstart"><span class="code-keyword">import</span> rclc
|
|
|
|
| 19 |
<link rel="canonical" href="https://agentcacheback.github.io/">
|
| 20 |
<title>Receiver-Conditioned Latent Communication gives 94% CacheBack</title>
|
| 21 |
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%231c211e'/%3E%3Cpath d='M7 7h7v7H7zm11 0h7v7h-7zM7 18h7v7H7z' fill='%230f766e'/%3E%3Cpath d='M18 18h7v7h-7z' fill='%23fff'/%3E%3C/svg%3E">
|
| 22 |
+
<link rel="stylesheet" href="website/assets/styles.css?v=method-order-1">
|
| 23 |
+
<link rel="stylesheet" href="demo/communication.css?v=inline-1">
|
| 24 |
+
<script type="module" src="demo/communication.js?v=inline-1"></script>
|
| 25 |
<script type="module" src="website/assets/app.js"></script>
|
| 26 |
</head>
|
| 27 |
<body>
|
|
|
|
| 39 |
<ul aria-label="Authors"><li>Maximillian Rossi</li><li>Prajwal Raghunath</li><li>Haoqing Xuan</li><li>Yusen Zhang</li><li>Eugene Wu</li></ul>
|
| 40 |
<p class="affiliations"><a href="https://daplab.cs.columbia.edu/"><img src="https://daplab.cs.columbia.edu/files/images/daplab_logo_horiz.png" width="1508" height="331" alt="DAP Lab"></a><span>Columbia University</span></p>
|
| 41 |
</div>
|
| 42 |
+
<div class="tldr"><p><b>TLDR;</b> Agents can share <strong>internal model state</strong> instead of generating text messages. Sending all of it, however, overwhelms the receiver.</p><p><strong>The receiver asks for what it needs.</strong> CacheBack uses attention to that request to select which state gets sent. Simple, robust, and <strong>training-free.</strong></p><p>At 16× compression, CacheBack passes on just 6.25% of sender positions.</p></div>
|
| 43 |
<nav class="resource-links" aria-label="Project resources">
|
| 44 |
<a href="https://arxiv.org/pdf/2609.32046">Paper <span>PDF</span></a>
|
| 45 |
<a href="https://github.com/agentcacheback/cacheback">Code</a>
|
|
|
|
| 49 |
</nav>
|
| 50 |
</div>
|
| 51 |
<aside class="hero-results" aria-labelledby="hero-results-title">
|
| 52 |
+
<h2 id="hero-results-title" class="eyebrow">FANOUTQA</h2><p class="visual-note">FanOutQA asks agents to combine evidence across documents. Each row compares same-size models: higher accuracy and lower time are better. pp = percentage points.</p>
|
| 53 |
<figure id="result-summary" class="comparison-plot" aria-labelledby="hero-results-title">
|
| 54 |
<div class="plot-legend"><span><i></i>Text · same-size senders</span><span class="rclc-emphasis"><i></i>CacheBack</span></div>
|
| 55 |
<div class="paired-metrics chart-heading"><div><strong>Strict accuracy (%) ↑</strong></div><div><strong>Median time (s) ↓</strong></div></div>
|
| 56 |
+
<div class="model-row" data-model="qwen"><div class="model-heading paired-metrics"><div class="model-label"><h3>Qwen 3 <small>8B</small></h3></div></div><div class="paired-metrics"><div class="paired-bars accuracy-bars"><div class="accuracy-guide" style="--bar-end:55.3300%"><span class="gain-bracket" aria-hidden="true"></span><strong class="accuracy-gain">+14.7<abbr title="percentage points">pp</abbr></strong></div><div class="bar-row" role="img" aria-label="Text: 40.7%"><div class="bar-track"><i style="width:40.6700%"></i></div><b>40.7%</b></div><div class="bar-row rclc-bar" role="img" aria-label="CacheBack: 55.3%"><div class="bar-track"><i style="width:55.3300%"></i></div><b>55.3%</b></div></div><div class="paired-bars latency-bars"><div class="speed-guide" style="--cache-end:11.9938%"><strong class="speedup">3.2× faster</strong></div><div class="bar-row" role="img" aria-label="Text: 612 s"><div class="bar-track"><i style="width:38.2437%"></i></div><b>612 s</b></div><div class="bar-row rclc-bar" role="img" aria-label="CacheBack: 192 s"><div class="bar-track"><i style="width:11.9938%"></i></div><b>192 s</b></div></div></div></div>
|
| 57 |
+
<div class="model-row" data-model="nemotron"><div class="model-heading paired-metrics"><div class="model-label"><h3>Nemotron <small>12B</small></h3></div></div><div class="paired-metrics"><div class="paired-bars accuracy-bars"><div class="accuracy-guide" style="--bar-end:50.0000%"><span class="gain-bracket" aria-hidden="true"></span><strong class="accuracy-gain">+11.3<abbr title="percentage points">pp</abbr></strong></div><div class="bar-row" role="img" aria-label="Text: 38.7%"><div class="bar-track"><i style="width:38.6667%"></i></div><b>38.7%</b></div><div class="bar-row rclc-bar" role="img" aria-label="CacheBack: 50.0%"><div class="bar-track"><i style="width:50.0000%"></i></div><b>50.0%</b></div></div><div class="paired-bars latency-bars"><div class="speed-guide" style="--cache-end:6.3516%"><strong class="speedup">1.3× faster</strong></div><div class="bar-row" role="img" aria-label="Text: 131 s"><div class="bar-track"><i style="width:8.2023%"></i></div><b>131 s</b></div><div class="bar-row rclc-bar" role="img" aria-label="CacheBack: 102 s"><div class="bar-track"><i style="width:6.3516%"></i></div><b>102 s</b></div></div></div></div>
|
| 58 |
+
<div class="model-row" data-model="gemma"><div class="model-heading paired-metrics"><div class="model-label"><h3>Gemma 4 <small>12B</small></h3></div></div><div class="paired-metrics"><div class="paired-bars accuracy-bars"><div class="accuracy-guide" style="--bar-end:59.3300%"><span class="gain-bracket" aria-hidden="true"></span><strong class="accuracy-gain">+7.3<abbr title="percentage points">pp</abbr></strong></div><div class="bar-row" role="img" aria-label="Text: 52.0%"><div class="bar-track"><i style="width:52.0000%"></i></div><b>52.0%</b></div><div class="bar-row rclc-bar" role="img" aria-label="CacheBack: 59.3%"><div class="bar-track"><i style="width:59.3300%"></i></div><b>59.3%</b></div></div><div class="paired-bars latency-bars"><div class="speed-guide" style="--cache-end:12.5812%"><strong class="speedup">3× faster</strong></div><div class="bar-row" role="img" aria-label="Text: 603 s"><div class="bar-track"><i style="width:37.6938%"></i></div><b>603 s</b></div><div class="bar-row rclc-bar" role="img" aria-label="CacheBack: 201 s"><div class="bar-track"><i style="width:12.5812%"></i></div><b>201 s</b></div></div></div></div>
|
| 59 |
+
<div class="model-row" data-model="ministral"><div class="model-heading paired-metrics"><div class="model-label"><h3>Ministral 3 <small>14B</small></h3></div></div><div class="paired-metrics"><div class="paired-bars accuracy-bars"><div class="accuracy-guide" style="--bar-end:59.3300%"><span class="gain-bracket" aria-hidden="true"></span><strong class="accuracy-gain">+20.7<abbr title="percentage points">pp</abbr></strong></div><div class="bar-row" role="img" aria-label="Text: 38.7%"><div class="bar-track"><i style="width:38.6700%"></i></div><b>38.7%</b></div><div class="bar-row rclc-bar" role="img" aria-label="CacheBack: 59.3%"><div class="bar-track"><i style="width:59.3300%"></i></div><b>59.3%</b></div></div><div class="paired-bars latency-bars"><div class="speed-guide" style="--cache-end:12.0813%"><strong class="speedup">8× faster</strong></div><div class="bar-row" role="img" aria-label="Text: 1,537 s"><div class="bar-track"><i style="width:96.0563%"></i></div><b>1,537 s</b></div><div class="bar-row rclc-bar" role="img" aria-label="CacheBack: 193 s"><div class="bar-track"><i style="width:12.0813%"></i></div><b>193 s</b></div></div></div></div>
|
| 60 |
<div class="paired-metrics chart-axes" aria-hidden="true"><div class="chart-axis"><span>0</span><span>50</span><span>100</span></div><div class="chart-axis"><span>0</span><span>800</span><span>1,600</span></div></div>
|
| 61 |
</figure>
|
| 62 |
|
| 63 |
</aside>
|
| 64 |
</section>
|
| 65 |
+
<section id="abstract" class="abstract-section wrap" aria-label="Communication overview">
|
| 66 |
+
<figure class="overview-figure">
|
| 67 |
+
<div class="communication-animation">
|
| 68 |
+
<button id="animation-play" aria-label="Play animation" title="Play animation">▶</button>
|
| 69 |
+
<svg id="communication-svg" viewBox="0 0 1180 548" role="img" aria-label="Animated comparison of full KV transfer and CacheBack"></svg>
|
| 70 |
+
<div id="animation-caption">Selected source positions can be sent as <span style="color:#6a4fb8"><b>token IDs</b></span>; latent steps must be sent as <span style="color:#3f669f"><b>continuous vectors</b></span>. The receiver prefills both to build its own state.</div>
|
| 71 |
</div>
|
| 72 |
+
|
| 73 |
+
</figure>
|
| 74 |
</section>
|
| 75 |
+
<section id="demo" class="demo-section" aria-labelledby="demo-title">
|
| 76 |
+
<div class="wrap">
|
| 77 |
+
<div class="demo-heading"><h2 id="demo-title">CacheBack on a coding task</h2></div>
|
| 78 |
+
<article class="recorded-demo coding-demo" id="coding-demo" aria-labelledby="coding-title">
|
| 79 |
+
<div class="demo-description"><div><p class="demo-type">36 seconds · 4K · Recorded coding task</p><h3 id="coding-title">Fixing a Django bug with seven coding agents</h3><p>Seven Qwen3-8B workers inspect the code and send information to a coordinator, which writes the Django fix. The video compares selected latent state with generated text messages. CacheBack produces the patch <strong class="rclc-emphasis">4.41× faster</strong> than with text communication on this case. Both runs make the same one-line code change and pass all 88 tests.</p></div></div>
|
| 80 |
+
<div class="demo-player"><video id="coding-video" controls playsinline preload="auto" tabindex="0" width="3840" height="2160" poster="demo/coding/cacheback-coding-poster.jpg" aria-labelledby="coding-title" aria-describedby="coding-caption">
|
| 81 |
+
<source src="demo/coding/cacheback-coding-4k.mp4" type="video/mp4">
|
| 82 |
+
<p><a href="demo/coding/cacheback-coding-4k.mp4">Watch the demo as an MP4</a>.</p>
|
| 83 |
+
</video><button class="play-demo" id="play-demo" type="button" hidden><span aria-hidden="true">▶</span> Watch the full demo</button></div>
|
| 84 |
+
<div class="demo-caption" id="coding-caption"><p><strong>One recorded case:</strong> CacheBack 25.66 s; text 113.21 s. Separate recorded runs are aligned at their start. Startup and test grading excluded. Silent video; playback speed varies. <a href="demo/coding/cacheback-coding-4k.mp4">Open in full window</a></p>
|
| 85 |
+
</div>
|
| 86 |
+
</article>
|
| 87 |
+
</div>
|
| 88 |
</section>
|
| 89 |
<section id="results" class="section wrap" aria-labelledby="results-title">
|
| 90 |
<h2 id="results-title">Results</h2>
|
| 91 |
<div class="paper-curves" id="paper-curves">
|
| 92 |
+
<div class="benchmark-setup"><div class="curves-heading"><h3>FanOutQA</h3><p id="compression-guide">Questions require combining evidence across Wikipedia pages. We split at least 120K tokens among three parallel senders; a receiver answers from their messages. Strict accuracy requires every reference-answer group. <strong>Higher is more accurate; left is faster.</strong> Each panel fixes the receiver model. Text labels give sender size; CacheBack uses same-size senders. <strong>4× compression retains ¼ of sender positions; 16× retains ¹⁄₁₆.</strong></p></div>
|
| 93 |
<div class="topology-diagram parallel" role="img" aria-label="Three senders read separate Wikipedia pages in parallel. Their messages converge on one receiver, which answers."><div class="senders"><div class="agent-node"><small>Wikipedia pages A</small><strong>Sender 1</strong></div><div class="agent-node"><small>Wikipedia pages B</small><strong>Sender 2</strong></div><div class="agent-node"><small>Wikipedia pages C</small><strong>Sender 3</strong></div></div><div class="merge" aria-hidden="true"><span>messages</span></div><div class="agent-node receiver"><small>Combines 3 messages</small><strong>Receiver</strong><small>↓ Answer</small></div></div></div>
|
| 94 |
+
<div class="curve-legend" aria-label="Curve legend"><span class="rclc-emphasis">● ━ CacheBack · labels show compression factor</span><span class="text-emphasis">▲ ┄ Text · labels show sender size</span><span>× Question only</span></div>
|
| 95 |
<div class="curve-grid">
|
| 96 |
+
<figure><img src="website/assets/figures/fanoutqa-qwen.svg?v=compression-labels" width="560" height="460" loading="lazy" alt="FanOutQA, Qwen 3 8B: CacheBack 4× compression reaches 55.3% strict accuracy at 192 seconds, compared with 40.7% at 612 seconds for same-size text; weaker settings are also plotted."><figcaption>Qwen: 4× compression improves accuracy and completion time over 8B text; 1.7B text is slightly faster but less accurate.</figcaption></figure>
|
| 97 |
+
<figure><img src="website/assets/figures/fanoutqa-nemotron.svg?v=compression-labels" width="560" height="460" loading="lazy" alt="FanOutQA, Nemotron Nano 2 12B: CacheBack 8× compression reaches 50% accuracy near 102 seconds versus 38.7% near 131 seconds for same-size text. A smaller text sender is faster at lower accuracy."><figcaption>Nemotron: 8× compression improves on 12B text; 4B text is faster but less accurate.</figcaption></figure>
|
| 98 |
</div>
|
| 99 |
+
<div class="chain-results" id="chain-results"><div class="benchmark-setup"><div class="curves-heading"><h3>LongBench v2 Easy</h3><p>Long-document questions test whether evidence survives repeated handoffs. We split each 100K-246K-token document into four equal parts. Each sender reads its part plus the previous message; a final receiver answers from the fourth message. Compression factors retain a fraction of accumulated context (4× keeps ¼); fixed budgets (32K, 64K) cap message positions. Axes and text-size labels follow FanOutQA.</p></div>
|
| 100 |
<div class="topology-diagram sequential" role="img" aria-label="Four agents read document parts in order. Each passes a message to the next. A final receiver answers from the fourth message."><div class="agent-node"><small>Document part 1</small><strong>Agent 1</strong></div><div class="agent-node"><small>Document part 2</small><strong>Agent 2</strong></div><div class="agent-node"><small>Document part 3</small><strong>Agent 3</strong></div><div class="agent-node"><small>Document part 4</small><strong>Agent 4</strong></div><div class="agent-node receiver"><small>Final message</small><strong>Receiver</strong><small>↓ Answer</small></div></div></div>
|
| 101 |
+
<div class="curve-legend" aria-label="LongBench curve legend"><span class="rclc-emphasis">● ━ CacheBack · labels show compression factor</span><span class="fixed-emphasis">■ ━ CacheBack · labels show fixed position budget</span><span class="text-emphasis">▲ ┄ Text · labels show sender size</span><span>× Question only</span></div>
|
| 102 |
<div class="curve-grid">
|
| 103 |
+
<figure><img src="website/assets/figures/longbench-qwen.svg?v=compression-labels" width="560" height="460" loading="lazy" alt="LongBench v2 Easy, Qwen 3 8B: both relative and fixed CacheBack budgets include settings faster and more accurate than same-size text. Stronger compression eventually lowers accuracy."><figcaption>Qwen: 4× compression gives the highest plotted accuracy; stronger compression loses accuracy.</figcaption></figure>
|
| 104 |
+
<figure><img src="website/assets/figures/longbench-nemotron.svg?v=compression-labels" width="560" height="460" loading="lazy" alt="LongBench v2 Easy, Nemotron Nano 2 12B: CacheBack 8× compression reaches 48% accuracy near 133 seconds, while fixed-budget settings show a different tradeoff; smaller text senders can be faster."><figcaption>Nemotron: relative 8× compression beats 12B text on both axes; 4B text remains faster but less accurate.</figcaption></figure>
|
| 105 |
</div>
|
| 106 |
</div>
|
| 107 |
<p class="curve-caption">Lines join the best accuracy-time tradeoffs within each channel; faded points are dominated settings. × means the receiver gets only the question. 50 concurrent tasks on 8 H100s; accuracy averaged over three receiver draws. Completion includes queueing. <a href="https://arxiv.org/pdf/2609.32046">Full setup and results in the paper.</a></p>
|
| 108 |
</div>
|
| 109 |
</section>
|
| 110 |
+
<section id="booking-demo" class="section wrap" aria-labelledby="booking-title">
|
| 111 |
+
<div class="booking-heading"><div><p class="eyebrow">Interactive demo · Unstructured documents</p><h2 id="booking-title">Multi-hop document Q&A</h2></div><a class="text-link" href="demo/index.html" target="_blank" rel="noopener noreferrer">Open full window <span aria-hidden="true">↗</span></a></div>
|
| 112 |
+
<p class="demo-scenario">In multi-agent question answering, a large document collection can be split across agents, each with its own context. Answering a question requires combining information across those contexts.</p>
|
| 113 |
+
<p class="demo-scenario">With text communication, agents write messages summarising what they read. CacheBack lets them pass latent thoughts together with a subset of internal state selected for what the next agent needs. This example shows selected-state handoffs: three agents read separate documents in sequence, and a fourth agent produces the final answer.</p>
|
| 114 |
+
<p class="demo-scenario">The top row shows each agent’s input document: a booking confirmation, a location-change notice, then an ID policy. Agents 2 and 3 also receive the previous agent’s handoff; Agent 4 answers from the final handoff alone. The bottom row shows the messages generated by the text agents.</p>
|
| 115 |
+
<div class="question-brief"><span>Question</span><h3 id="booking-question">Where should Maya collect her pass on Friday, and what ID should she bring?</h3></div>
|
| 116 |
+
<p class="visual-note">Press <strong>Run</strong> to watch the handoffs.</p>
|
| 117 |
+
<div class="recorded-demo">
|
| 118 |
+
<div class="demo-window"><iframe id="flagship-demo" src="demo/index.html?v=selection-1" title="Multi-hop document question answering: selected internal state versus generated text messages" loading="lazy" sandbox="allow-scripts allow-same-origin" referrerpolicy="no-referrer"></iframe></div>
|
|
|
|
|
|
|
|
|
|
| 119 |
</div>
|
| 120 |
</section>
|
| 121 |
<section class="code-section" id="code" aria-labelledby="code-title"><div class="wrap code-layout"><div><h2 id="code-title">Use CacheBack</h2><p>Install <code>rclc</code>, bind your agents, and pass the receiver’s request.</p><a class="button primary" href="https://github.com/agentcacheback/cacheback">Code and documentation</a></div><div class="code-card"><div class="code-bar"><span>Python</span><button class="copy-button" data-copy="quickstart" type="button" aria-label="Copy code"><svg class="copy-icon" aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3"/></svg><span class="copy-feedback" aria-hidden="true">Copied</span></button></div><pre><code id="quickstart"><span class="code-keyword">import</span> rclc
|
website/assets/figures/fanoutqa-nemotron.svg
CHANGED
|
|
|
|
website/assets/figures/fanoutqa-qwen.svg
CHANGED
|
|
|
|
website/assets/figures/longbench-nemotron.svg
CHANGED
|
|
|
|
website/assets/figures/longbench-qwen.svg
CHANGED
|
|
|
|
website/assets/styles.css
CHANGED
|
@@ -65,26 +65,32 @@ strong,b { font-weight:650; }
|
|
| 65 |
.plot-legend span { display:flex; align-items:center; gap:7px; }
|
| 66 |
.plot-legend i { width:16px; height:8px; background:#67726b; border-radius:2px; }
|
| 67 |
.plot-legend .rclc-emphasis i { background:var(--rclc); }
|
| 68 |
-
.paired-metrics { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:
|
| 69 |
-
.chart-heading {
|
| 70 |
-
.
|
|
|
|
|
|
|
| 71 |
.model-heading { margin-bottom:8px; }
|
| 72 |
.model-label { display:flex; justify-content:space-between; align-items:baseline; gap:8px; min-width:0; }
|
| 73 |
.accuracy-gain,.speedup { color:var(--rclc); font-size:14px; white-space:nowrap; }
|
| 74 |
.accuracy-gain abbr { text-decoration:none; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
.latency-bars { position:relative; }
|
| 76 |
-
.speed-guide { position:absolute; inset:0
|
| 77 |
.speedup { position:absolute; left:calc(var(--cache-end) + 6px); top:25px; font-size:12px; line-height:20px; }
|
| 78 |
.model-heading h3 { font-size:15px; font-weight:650; letter-spacing:0; }
|
| 79 |
.model-heading small { color:var(--muted); font-size:12px; font-weight:400; }
|
| 80 |
-
.bar-row { line-height:20px; display:grid; grid-template-columns:minmax(0,1fr)
|
| 81 |
.bar-row+.bar-row { margin-top:5px; }
|
| 82 |
-
.bar-row>b { text-align:right; font-variant-numeric:tabular-nums; }
|
| 83 |
-
.bar-track { border-left:1px solid #b5bfb7; background:repeating-linear-gradient(to right,transparent 0 calc(50% - 1px),#dce2dc calc(50% - 1px) 50%); }
|
| 84 |
.bar-track i { display:block; height:17px; background:#67726b; border-radius:0 2px 2px 0; }
|
| 85 |
.rclc-bar { color:var(--rclc); }
|
| 86 |
.rclc-bar i { background:var(--rclc); }
|
| 87 |
-
.chart-axis { margin:8px
|
| 88 |
.chart-axis span { position:absolute; transform:translateX(-50%); white-space:nowrap; }
|
| 89 |
.chart-axis span:nth-child(1) { left:0; }
|
| 90 |
.chart-axis span:nth-child(2) { left:50%; }
|
|
@@ -92,7 +98,7 @@ strong,b { font-weight:650; }
|
|
| 92 |
|
| 93 |
.abstract-section { display:grid; grid-template-columns:1fr; align-items:center; gap:48px; padding-block:64px; border-top:1px solid var(--line); }
|
| 94 |
.tldr { font-size:16px; line-height:1.65; }
|
| 95 |
-
.abstract-section .overview-figure { max-width:900px; margin-inline:auto; }
|
| 96 |
.overview-figure img { width:100%; height:auto; display:block; }
|
| 97 |
.overview-figure figcaption { color:var(--muted); font-size:12px; margin-top:18px; }
|
| 98 |
.tldr p+p { margin-top:12px; }
|
|
@@ -261,3 +267,11 @@ html { scroll-behavior:auto; }
|
|
| 261 |
@media(max-width:900px) { .benchmark-setup { grid-template-columns:1fr; gap:20px; } }
|
| 262 |
|
| 263 |
@media(max-width:600px) { .benchmark-setup .sequential { gap:25px; } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
.plot-legend span { display:flex; align-items:center; gap:7px; }
|
| 66 |
.plot-legend i { width:16px; height:8px; background:#67726b; border-radius:2px; }
|
| 67 |
.plot-legend .rclc-emphasis i { background:var(--rclc); }
|
| 68 |
+
.paired-metrics { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:36px; }
|
| 69 |
+
.chart-heading { font-size:14px; line-height:1.4; }
|
| 70 |
+
.chart-heading>div { border-bottom:1px solid var(--line); padding-bottom:15px; }
|
| 71 |
+
.model-row { padding-top:14px; }
|
| 72 |
+
.paired-bars { padding-bottom:14px; border-bottom:1px solid var(--line); }
|
| 73 |
.model-heading { margin-bottom:8px; }
|
| 74 |
.model-label { display:flex; justify-content:space-between; align-items:baseline; gap:8px; min-width:0; }
|
| 75 |
.accuracy-gain,.speedup { color:var(--rclc); font-size:14px; white-space:nowrap; }
|
| 76 |
.accuracy-gain abbr { text-decoration:none; }
|
| 77 |
+
.accuracy-bars { position:relative; }
|
| 78 |
+
.accuracy-guide { position:absolute; inset:0 0 14px 58px; color:var(--rclc); pointer-events:none; }
|
| 79 |
+
.gain-bracket { position:absolute; top:1.5px; bottom:1.5px; left:calc(var(--bar-end) + 4px); width:7px; border:1px solid currentColor; border-left:0; }
|
| 80 |
+
.accuracy-guide .accuracy-gain { position:absolute; left:calc(var(--bar-end) + 17px); top:50%; transform:translateY(-50%); font-size:12px; line-height:20px; }
|
| 81 |
.latency-bars { position:relative; }
|
| 82 |
+
.speed-guide { position:absolute; inset:0 0 0 58px; color:var(--rclc); pointer-events:none; }
|
| 83 |
.speedup { position:absolute; left:calc(var(--cache-end) + 6px); top:25px; font-size:12px; line-height:20px; }
|
| 84 |
.model-heading h3 { font-size:15px; font-weight:650; letter-spacing:0; }
|
| 85 |
.model-heading small { color:var(--muted); font-size:12px; font-weight:400; }
|
| 86 |
+
.bar-row { line-height:20px; display:grid; grid-template-columns:50px minmax(0,1fr); gap:8px; align-items:center; font-size:12px; }
|
| 87 |
.bar-row+.bar-row { margin-top:5px; }
|
| 88 |
+
.bar-row>b { grid-column:1; grid-row:1; text-align:right; font-variant-numeric:tabular-nums; }
|
| 89 |
+
.bar-track { grid-column:2; grid-row:1; border-left:1px solid #b5bfb7; background:repeating-linear-gradient(to right,transparent 0 calc(50% - 1px),#dce2dc calc(50% - 1px) 50%); }
|
| 90 |
.bar-track i { display:block; height:17px; background:#67726b; border-radius:0 2px 2px 0; }
|
| 91 |
.rclc-bar { color:var(--rclc); }
|
| 92 |
.rclc-bar i { background:var(--rclc); }
|
| 93 |
+
.chart-axis { margin:8px 0 0 58px; color:var(--muted); font-size:12px; position:relative; height:20px; }
|
| 94 |
.chart-axis span { position:absolute; transform:translateX(-50%); white-space:nowrap; }
|
| 95 |
.chart-axis span:nth-child(1) { left:0; }
|
| 96 |
.chart-axis span:nth-child(2) { left:50%; }
|
|
|
|
| 98 |
|
| 99 |
.abstract-section { display:grid; grid-template-columns:1fr; align-items:center; gap:48px; padding-block:64px; border-top:1px solid var(--line); }
|
| 100 |
.tldr { font-size:16px; line-height:1.65; }
|
| 101 |
+
.abstract-section .overview-figure { width:100%; max-width:900px; margin-inline:auto; }
|
| 102 |
.overview-figure img { width:100%; height:auto; display:block; }
|
| 103 |
.overview-figure figcaption { color:var(--muted); font-size:12px; margin-top:18px; }
|
| 104 |
.tldr p+p { margin-top:12px; }
|
|
|
|
| 267 |
@media(max-width:900px) { .benchmark-setup { grid-template-columns:1fr; gap:20px; } }
|
| 268 |
|
| 269 |
@media(max-width:600px) { .benchmark-setup .sequential { gap:25px; } }
|
| 270 |
+
|
| 271 |
+
.overview-figure .communication-animation { background:transparent; }
|
| 272 |
+
|
| 273 |
+
.demo-scenario { color:var(--muted); font-size:16px; max-width:75ch; margin-bottom:22px; }
|
| 274 |
+
.question-brief { display:flex; align-items:baseline; gap:20px; padding:18px 0; border-block:1px solid var(--line); margin-bottom:17px; }
|
| 275 |
+
.question-brief>span { color:var(--rclc); font-size:12px; font-weight:650; }
|
| 276 |
+
.question-brief h3 { font-size:21px; line-height:1.4; letter-spacing:-.3px; }
|
| 277 |
+
@media(max-width:520px) { .paired-metrics { gap:36px; } .accuracy-guide .accuracy-gain,.speedup { font-size:10px; } .question-brief { display:block; } .question-brief h3 { font-size:19px; margin-top:6px; } }
|
website/check.mjs
CHANGED
|
@@ -16,15 +16,15 @@ const ids = [...html.matchAll(/\bid="([^"]+)"/g)].map((match) => match[1]);
|
|
| 16 |
assert.equal(new Set(ids).size, ids.length, 'Duplicate HTML IDs');
|
| 17 |
assert.equal((html.match(/<h1\b/g) || []).length, 1, 'One page heading');
|
| 18 |
assert.match(html, /<h1 id="hero-title">Receiver-Conditioned Latent Communication gives/);
|
| 19 |
-
assert.equal((html.match(/<figcaption>/g) || []).length,
|
| 20 |
assert.match(html, /<html lang="en">/);
|
| 21 |
assert.match(html, /name="viewport"/);
|
| 22 |
assert.match(html, /<title>Receiver-Conditioned Latent Communication gives 94% CacheBack<\/title>/);
|
| 23 |
-
const sectionOrder = ['result-summary', '
|
| 24 |
sectionOrder.slice(1).forEach((id, i) => {
|
| 25 |
assert.ok(html.indexOf(`id="${sectionOrder[i]}"`) < html.indexOf(`id="${id}"`), `Section order: ${id}`);
|
| 26 |
});
|
| 27 |
-
assert.equal((html.match(/<img src="website\/assets\/figures\//g) || []).length,
|
| 28 |
|
| 29 |
for (const [, url] of html.matchAll(/(?:href|src|poster)="([^"]+)"/g)) {
|
| 30 |
if (/^(https:|data:)/.test(url)) continue;
|
|
@@ -68,23 +68,25 @@ assert.match(html, /loading="lazy" sandbox="allow-scripts allow-same-origin"/);
|
|
| 68 |
assert.match(css, /prefers-reduced-motion:reduce/);
|
| 69 |
assert.match(css, /:focus-visible/);
|
| 70 |
assert.match(html, /id="bibtex"/);
|
| 71 |
-
assert.match(html, /<iframe[^>]+src="demo\/index\.html\?v=
|
| 72 |
-
|
|
|
|
| 73 |
// The booking record is rendered by the embedded replay, not duplicated in the page.
|
| 74 |
const replay = readFileSync(resolve(project, 'demo/index.html'), 'utf8');
|
| 75 |
-
assert.match(replay, /
|
| 76 |
-
|
|
|
|
| 77 |
assert.doesNotMatch(html, /Recorded model run|demo-outcomes|hero-description|class="contribution"/);
|
| 78 |
for (const label of ['Copy code', 'Copy citation']) assert.ok(html.includes(`aria-label="${label}"`));
|
| 79 |
|
| 80 |
const coding = JSON.parse(readFileSync(resolve(project, 'demo/coding/evidence.json'), 'utf8')).arms;
|
| 81 |
-
assert.ok(html.indexOf('id="
|
| 82 |
const video = html.match(/<video\b[^>]*>[\s\S]*?<\/video>/)[0];
|
| 83 |
assert.match(video, /controls playsinline preload="auto"/);
|
| 84 |
assert.doesNotMatch(video, /autoplay/);
|
| 85 |
-
assert.match(video, /src="demo\/coding\/
|
| 86 |
-
assert.match(video, /poster="demo\/coding\/poster.jpg"/);
|
| 87 |
-
assert.equal(readFileSync(resolve(project, 'demo/coding/
|
| 88 |
assert.ok(html.includes(`${(coding.text.seconds / coding.latent.seconds).toFixed(2)}× faster`));
|
| 89 |
assert.ok(html.includes(`CacheBack ${coding.latent.seconds.toFixed(2)} s; text ${coding.text.seconds.toFixed(2)} s`));
|
| 90 |
assert.equal(coding.latent.patch, coding.text.patch);
|
|
@@ -104,3 +106,15 @@ for (const name of readdirSync(dist, { recursive: true })) {
|
|
| 104 |
}
|
| 105 |
assert.ok(bytes < 100000, 'Keep first-party static assets below 100 KB');
|
| 106 |
console.log(`Website checks passed: anchors, assets, benchmark points, accessibility hooks; ${bytes} bytes, ${gzipSync(js).length} bytes gzipped JS.`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
assert.equal(new Set(ids).size, ids.length, 'Duplicate HTML IDs');
|
| 17 |
assert.equal((html.match(/<h1\b/g) || []).length, 1, 'One page heading');
|
| 18 |
assert.match(html, /<h1 id="hero-title">Receiver-Conditioned Latent Communication gives/);
|
| 19 |
+
assert.equal((html.match(/<figcaption>/g) || []).length, 4, 'Explain each static paper figure');
|
| 20 |
assert.match(html, /<html lang="en">/);
|
| 21 |
assert.match(html, /name="viewport"/);
|
| 22 |
assert.match(html, /<title>Receiver-Conditioned Latent Communication gives 94% CacheBack<\/title>/);
|
| 23 |
+
const sectionOrder = ['result-summary', 'abstract', 'demo', 'results', 'booking-demo', 'code', 'paper'];
|
| 24 |
sectionOrder.slice(1).forEach((id, i) => {
|
| 25 |
assert.ok(html.indexOf(`id="${sectionOrder[i]}"`) < html.indexOf(`id="${id}"`), `Section order: ${id}`);
|
| 26 |
});
|
| 27 |
+
assert.equal((html.match(/<img src="website\/assets\/figures\//g) || []).length, 4);
|
| 28 |
|
| 29 |
for (const [, url] of html.matchAll(/(?:href|src|poster)="([^"]+)"/g)) {
|
| 30 |
if (/^(https:|data:)/.test(url)) continue;
|
|
|
|
| 68 |
assert.match(css, /prefers-reduced-motion:reduce/);
|
| 69 |
assert.match(css, /:focus-visible/);
|
| 70 |
assert.match(html, /id="bibtex"/);
|
| 71 |
+
assert.match(html, /<iframe[^>]+src="demo\/index\.html\?v=selection-1"/);
|
| 72 |
+
const replayScript = readFileSync(resolve(project, 'demo/replay.js'), 'utf8');
|
| 73 |
+
assert.match(replayScript, /fetch\('trace-w4\.json'\)/);
|
| 74 |
// The booking record is rendered by the embedded replay, not duplicated in the page.
|
| 75 |
const replay = readFileSync(resolve(project, 'demo/index.html'), 'utf8');
|
| 76 |
+
assert.match(replay, /src="replay.js\?v=selection-1"/);
|
| 77 |
+
new Function(replayScript);
|
| 78 |
+
assert.match(html, /aria-labelledby="booking-title"/);
|
| 79 |
assert.doesNotMatch(html, /Recorded model run|demo-outcomes|hero-description|class="contribution"/);
|
| 80 |
for (const label of ['Copy code', 'Copy citation']) assert.ok(html.includes(`aria-label="${label}"`));
|
| 81 |
|
| 82 |
const coding = JSON.parse(readFileSync(resolve(project, 'demo/coding/evidence.json'), 'utf8')).arms;
|
| 83 |
+
assert.ok(html.indexOf('id="coding-demo"') < html.indexOf('id="booking-demo"'), 'Coding video precedes document replay');
|
| 84 |
const video = html.match(/<video\b[^>]*>[\s\S]*?<\/video>/)[0];
|
| 85 |
assert.match(video, /controls playsinline preload="auto"/);
|
| 86 |
assert.doesNotMatch(video, /autoplay/);
|
| 87 |
+
assert.match(video, /src="demo\/coding\/cacheback-coding-4k.mp4"/);
|
| 88 |
+
assert.match(video, /poster="demo\/coding\/cacheback-coding-poster.jpg"/);
|
| 89 |
+
assert.equal(readFileSync(resolve(project, 'demo/coding/cacheback-coding-4k.mp4')).toString('ascii', 4, 8), 'ftyp', 'Real MP4, not a Git LFS pointer');
|
| 90 |
assert.ok(html.includes(`${(coding.text.seconds / coding.latent.seconds).toFixed(2)}× faster`));
|
| 91 |
assert.ok(html.includes(`CacheBack ${coding.latent.seconds.toFixed(2)} s; text ${coding.text.seconds.toFixed(2)} s`));
|
| 92 |
assert.equal(coding.latent.patch, coding.text.patch);
|
|
|
|
| 106 |
}
|
| 107 |
assert.ok(bytes < 100000, 'Keep first-party static assets below 100 KB');
|
| 108 |
console.log(`Website checks passed: anchors, assets, benchmark points, accessibility hooks; ${bytes} bytes, ${gzipSync(js).length} bytes gzipped JS.`);
|
| 109 |
+
|
| 110 |
+
const animation = readFileSync(resolve(project, 'demo/communication.html'), 'utf8');
|
| 111 |
+
const animationScript = readFileSync(resolve(project, 'demo/communication.js'), 'utf8');
|
| 112 |
+
assert.doesNotMatch(html, /<iframe[^>]*communication/, 'Method animation participates in page layout');
|
| 113 |
+
for (const markup of [html, animation]) {
|
| 114 |
+
assert.equal((markup.match(/<svg id="communication-svg"/g) || []).length, 1);
|
| 115 |
+
assert.equal((markup.match(/id="animation-play"/g) || []).length, 1, 'One animation control');
|
| 116 |
+
assert.match(markup, /src="(?:demo\/)?communication.js\?v=inline-1"/);
|
| 117 |
+
}
|
| 118 |
+
assert.doesNotMatch(animationScript, /frameElement|ResizeObserver/, 'No iframe height synchronisation');
|
| 119 |
+
assert.doesNotMatch(animationScript, /setTimeout\(play/, 'Playback starts on user action');
|
| 120 |
+
new Function(animationScript);
|