Max Rossi commited on
Commit
cfcfbaf
·
verified ·
1 Parent(s): 29aa94c

Refresh CacheBack website, demos and latent-vector caption

Browse files
.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 booking replay, and a coding demo video. It does not run model inference.
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

  • SHA256: d92494d34e4fbdc2e722dda383de5a30514efaca4c067c056af1e7a9e03223b2
  • Pointer size: 131 Bytes
  • Size of remote file: 315 kB
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 | Booking relay</title>
8
- <style>
9
- * { box-sizing:border-box; }
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
- <section class="question" aria-label="Recorded question">
84
- <h1 id="question">Where should Maya collect her pass on Friday, and what ID should she bring?</h1>
 
 
 
 
 
 
 
 
 
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
- </section>
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"><h2 id="rclc-title">CacheBack</h2><span class="time" id="rclc-time"></span></div>
 
 
 
106
  <div class="row" id="rclc-row"></div>
107
  </section>
108
  <section class="method" aria-labelledby="text-title">
109
- <div class="method-heading"><h2 id="text-title">Text</h2><span class="time" id="text-time"></span></div>
 
 
 
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">Estimated streaming, measured completion times.</span></p>
116
  <a href="https://github.com/agentcacheback/cacheback/tree/main/demo">Code &amp; 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&amp;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&amp;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 &amp; 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 &amp; 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 &amp; 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=tldr-hero-2">
 
 
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>The title’s 94% refers to removing 93.75% of sender positions at 16× compression.</p></div>
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">Three senders read different Wikipedia pages; one receiver combines their messages to answer a question. Each row compares same-size models: higher accuracy and lower time are better. pp = percentage points.</p>
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></div><div class="paired-metrics"><div class="paired-bars"><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>
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></div><div class="paired-metrics"><div class="paired-bars"><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>
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></div><div class="paired-metrics"><div class="paired-bars"><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>
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></div><div class="paired-metrics"><div class="paired-bars"><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>
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="booking-demo" class="section wrap" aria-labelledby="booking-question">
64
- <div class="booking-heading"><div><p class="eyebrow">Interactive demo</p><h2 id="booking-question">Where should Maya collect her pass on Friday, and what ID should she bring?</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>
65
- <p class="visual-note">Press <strong>Run</strong> to replay two Qwen3-8B chains. Agents 1-3 each read a different document and pass a message to the next agent; Agent 4 answers. <strong>Top:</strong> CacheBack highlights source positions whose state is selected for the next agent, guided by the question. Highlights change at each handoff as inherited and new positions are selected. <strong>Bottom:</strong> text messages appear as they are generated. The last column shows each chain’s answer. Timers show recorded elapsed time; streaming is reconstructed.</p>
66
- <div class="recorded-demo">
67
- <div class="demo-window"><iframe id="flagship-demo" src="demo/index.html?v=fit-7" title="Recorded CacheBack and text booking relay comparison, with playback controls" loading="lazy" sandbox="allow-scripts allow-same-origin" referrerpolicy="no-referrer"></iframe></div>
 
68
  </div>
 
 
69
  </section>
70
- <section id="abstract" class="abstract-section wrap" aria-label="Communication overview">
71
- <figure class="overview-figure"><a href="https://arxiv.org/pdf/2609.32046" aria-label="Read Figure 2 in the paper"><img src="website/assets/figures/communication-overview.svg" width="1240" height="510" loading="lazy" alt="Figure 2: sending every agent's full cache exceeds the receiver's context limit. CacheBack sends selected positions and latent steps, leaving room for the receiver's prompt."></a><figcaption>Figure 2. Full KV caches (stored model state) accumulate source context at the receiver and can exceed its context limit. Receiver-conditioning selects source and latent positions, leaving room for the receiver’s prompt. Token IDs represent source positions; continuous vectors represent latent steps.</figcaption></figure>
 
 
 
 
 
 
 
 
 
 
 
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>rX retains 1/X of sender positions; r4 keeps 25%.</strong></p></div>
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=sender-sizes" width="560" height="460" loading="lazy" alt="FanOutQA, Qwen 3 8B: CacheBack r4 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: r4 improves accuracy and completion time over 8B text; 1.7B text is slightly faster but less accurate.</figcaption></figure>
81
- <figure><img src="website/assets/figures/fanoutqa-nemotron.svg?v=sender-sizes" width="560" height="460" loading="lazy" alt="FanOutQA, Nemotron Nano 2 12B: CacheBack r8 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: r8 improves on 12B text; 4B text is faster but less accurate.</figcaption></figure>
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. Relative budgets (rX) grow with accumulated context; fixed budgets (32K, 64K) cap message positions. Axes and text-size labels follow FanOutQA.</p></div>
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 · relative budget</span><span class="fixed-emphasis">■ ━ CacheBack · fixed budget</span><span class="text-emphasis">▲ ┄ Text · labels show sender size</span><span>× Question only</span></div>
86
  <div class="curve-grid">
87
- <figure><img src="website/assets/figures/longbench-qwen.svg?v=sender-sizes" 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: relative r4 gives the highest plotted accuracy; stronger compression loses accuracy.</figcaption></figure>
88
- <figure><img src="website/assets/figures/longbench-nemotron.svg?v=sender-sizes" width="560" height="460" loading="lazy" alt="LongBench v2 Easy, Nemotron Nano 2 12B: CacheBack r8 reaches 48% accuracy near 133 seconds, while fixed-budget settings show a different tradeoff; smaller text senders can be faster."><figcaption>Nemotron: relative r8 beats 12B text on both axes; 4B text remains faster but less accurate.</figcaption></figure>
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="demo-section" aria-labelledby="demo-title">
95
- <div class="wrap">
96
- <div class="demo-heading"><h2 id="demo-title">Watch the demo</h2></div>
97
- <article class="recorded-demo coding-demo" id="coding-demo" aria-labelledby="coding-title">
98
- <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>
99
- <div class="demo-player"><video id="coding-video" controls playsinline preload="auto" tabindex="0" width="3840" height="2160" poster="demo/coding/poster.jpg" aria-labelledby="coding-title" aria-describedby="coding-caption">
100
- <source src="demo/coding/rclc-cacheback-4k.mp4" type="video/mp4">
101
- <p><a href="demo/coding/rclc-cacheback-4k.mp4">Watch the demo as an MP4</a>.</p>
102
- </video><button class="play-demo" id="play-demo" type="button" hidden><span aria-hidden="true">▶</span> Watch the full demo</button></div>
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&amp;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:24px; }
69
- .chart-heading { border-bottom:1px solid var(--line); padding-bottom:15px; font-size:14px; line-height:1.4; }
70
- .model-row { padding:14px 0; border-bottom:1px solid var(--line); }
 
 
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 58px 0 0; color:var(--rclc); pointer-events:none; }
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) 50px; gap:8px; align-items:center; font-size:12px; }
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 58px 0 0; color:var(--muted); font-size:12px; position:relative; height:20px; }
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, 5, 'Explain each 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', 'booking-demo', 'abstract', 'results', '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, 5);
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=fit-7"/);
72
- assert.match(readFileSync(resolve(project, 'demo/index.html'), 'utf8'), /fetch\("trace-w4\.json"\)/);
 
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, /trace-w4\.json/);
76
- assert.match(html, /aria-labelledby="booking-question"/);
 
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="booking-demo"') < html.indexOf('id="coding-demo"'), 'Booking replay precedes coding video');
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\/rclc-cacheback-4k.mp4"/);
86
- assert.match(video, /poster="demo\/coding\/poster.jpg"/);
87
- assert.equal(readFileSync(resolve(project, 'demo/coding/rclc-cacheback-4k.mp4')).toString('ascii', 4, 8), 'ftyp', 'Real MP4, not a Git LFS pointer');
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);