AgentStateGraph / app /src /content /embeds /failure-features.html
seonglae's picture
Automata from Agent Traces — interactive article
59027a2
Raw
History Blame Contribute Delete
11.2 kB
<!-- Failure Features: Dumbbell chart with CV + Holdout AUROC and top features -->
<div class="failure-features"></div>
<style>
.failure-features { position: relative; width: 100%; min-height: 420px; }
.failure-features svg { display: block; width: 100%; }
.failure-features .tick text { fill: var(--text-color); font-size: 11px; }
.failure-features .tick line, .failure-features .domain { stroke: var(--border-color); }
.failure-features .grid line { stroke: var(--border-color); opacity: 0.15; }
.failure-features .grid .domain { display: none; }
.failure-features .axis-label { fill: var(--text-color); font-size: 13px; font-weight: 500; }
.failure-features .tooltip {
position: absolute; top: 0; left: 0; pointer-events: none; padding: 10px 14px; border-radius: 8px;
font-size: 12px; line-height: 1.6; border: 1px solid var(--border-color);
background: var(--surface-bg); color: var(--text-color);
box-shadow: 0 4px 20px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
opacity: 0; transition: opacity 0.15s ease;
z-index: 100; max-width: 260px;
font-variant-numeric: tabular-nums;
}
.failure-features .ref-line { stroke-dasharray: 6,4; }
.failure-features .feature-pill {
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
font-size: 10px;
}
.failure-features .legend-item text { fill: var(--text-color); font-size: 11px; }
@media (max-width: 600px) {
.failure-features { min-height: 340px; }
.failure-features .tooltip { max-width: 200px; }
.failure-features .feature-pill { display: none; }
}
</style>
<script>
(() => {
const ensureD3 = (cb) => {
if (window.d3 && typeof window.d3.select === 'function') return cb();
let s = document.getElementById('d3-cdn-script');
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
s.addEventListener('load', () => cb(), { once: true });
};
const bootstrap = () => {
const container = document.querySelector('.failure-features:not([data-mounted])');
if (!container) return;
container.dataset.mounted = 'true';
const d3 = window.d3;
const data = [
{ name: 'tau2 telecom', states: 43, cvAuroc: 0.946, holdout: 0.941, topFeature: 'message length before human handoff', features: 129 },
{ name: 'WebArena', states: 25, cvAuroc: 0.882, holdout: 0.903, topFeature: 'whether the run ends on a click', features: 50 },
{ name: 'ATBench', states: 15, cvAuroc: 0.864, holdout: 0.894, topFeature: 'whether the run ends on an assistant turn', features: 36 },
{ name: 'AgentNet', states: 25, cvAuroc: 0.886, holdout: 0.890, topFeature: 'a write-then-click step', features: 73 },
{ name: 'tau2 airline', states: 18, cvAuroc: 0.826, holdout: 0.864, topFeature: 'a cancel-reservation step', features: 70 },
{ name: 'SWE-agent', states: 25, cvAuroc: 0.806, holdout: 0.799, topFeature: 'edit message length', features: 39 },
{ name: 'tau2 retail', states: 19, cvAuroc: 0.752, holdout: 0.779, topFeature: 'longest user message', features: 52 },
{ name: 'OSWorld', states: 27, cvAuroc: 0.779, holdout: 0.774, topFeature: 'trace length', features: 79 },
{ name: 'SWE-smith', states: 10, cvAuroc: 0.636, holdout: 0.703, topFeature: 'longest tool message', features: 54 },
].sort((a, b) => b.holdout - a.holdout);
const ours = '#3d5a80';
const slate = '#8fa6c4';
const margin = { top: 38, right: 150, bottom: 48, left: 110 };
const tip = document.createElement('div');
tip.className = 'tooltip';
container.appendChild(tip);
let hasAnimated = false;
function render() {
container.querySelectorAll('svg').forEach(s => s.remove());
const rect = container.getBoundingClientRect();
const W = Math.max(400, Math.round(rect.width));
const barH = 34;
const H = margin.top + margin.bottom + data.length * (barH + 5);
const w = Math.max(0, W - margin.left - margin.right);
const h = Math.max(0, H - margin.top - margin.bottom);
const svg = d3.select(container).insert('svg', '.tooltip')
.attr('width', W).attr('height', Math.max(0, H));
const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);
const x = d3.scaleLinear().domain([0.5, 1.0]).range([0, w]);
const y = d3.scaleBand().domain(data.map(d => d.name)).range([0, h]).padding(0.3);
// Grid
g.append('g').attr('class', 'grid')
.attr('transform', `translate(0,${h})`)
.call(d3.axisBottom(x).ticks(5).tickSize(Math.max(0, -h)).tickFormat(''));
// Random baseline (0.5) dashed line
g.append('line').attr('class', 'ref-line')
.attr('x1', x(0.5)).attr('x2', x(0.5)).attr('y1', -8).attr('y2', h)
.attr('stroke', 'var(--text-color)').attr('stroke-width', 1).attr('stroke-opacity', 0.3);
g.append('text')
.attr('x', x(0.5)).attr('y', -12)
.attr('fill', 'var(--text-color)').attr('font-size', '10px')
.attr('text-anchor', 'middle').attr('opacity', 0.45).text('random (0.5)');
// Bottom axis
g.append('g').attr('transform', `translate(0,${h})`).call(d3.axisBottom(x).ticks(5));
// Left axis
g.append('g').call(d3.axisLeft(y).tickSize(0)).select('.domain').remove();
g.append('text').attr('class', 'axis-label')
.attr('x', w / 2).attr('y', h + 40).attr('text-anchor', 'middle')
.text('AUROC');
// --- Horizontal legend at top ---
const leg = g.append('g').attr('transform', `translate(0, -26)`);
// Holdout
leg.append('circle').attr('cx', 0).attr('cy', 0).attr('r', 5).attr('fill', ours);
leg.append('text').attr('class', 'legend-item').attr('x', 10).attr('y', 4)
.attr('fill', 'var(--text-color)').attr('font-size', '11px').text('Holdout AUROC');
// CV
leg.append('circle').attr('cx', 112).attr('cy', 0).attr('r', 5)
.attr('fill', 'none').attr('stroke', slate).attr('stroke-width', 2);
leg.append('text').attr('class', 'legend-item').attr('x', 122).attr('y', 4)
.attr('fill', 'var(--text-color)').attr('font-size', '11px').text('5-fold CV AUROC');
// Connector
leg.append('line').attr('x1', 230).attr('x2', 254).attr('y1', 0).attr('y2', 0)
.attr('stroke', slate).attr('stroke-width', 2).attr('stroke-opacity', 0.4);
leg.append('text').attr('class', 'legend-item').attr('x', 260).attr('y', 4)
.attr('fill', 'var(--text-color)').attr('font-size', '11px').attr('opacity', 0.55).text('gap');
const animate = !hasAnimated;
const dur = 500;
// --- Dumbbell rows ---
data.forEach((d, idx) => {
const cy = y(d.name) + y.bandwidth() / 2;
const delay = idx * 60;
const xCV = x(d.cvAuroc);
const xHO = x(d.holdout);
const x0 = x(0.5);
// Connecting line (CV to Holdout)
const line = g.append('line')
.attr('y1', cy).attr('y2', cy)
.attr('stroke', slate).attr('stroke-width', 2).attr('stroke-opacity', 0.35)
.attr('stroke-linecap', 'round');
if (animate) {
line.attr('x1', x0).attr('x2', x0)
.transition().duration(dur).delay(delay).ease(d3.easeCubicOut)
.attr('x1', Math.min(xCV, xHO)).attr('x2', Math.max(xCV, xHO));
} else {
line.attr('x1', Math.min(xCV, xHO)).attr('x2', Math.max(xCV, xHO));
}
// CV AUROC (open circle)
const cvCircle = g.append('circle')
.attr('cx', animate ? x0 : xCV).attr('cy', cy)
.attr('fill', 'var(--page-bg)').attr('stroke', slate).attr('stroke-width', 2)
.attr('cursor', 'pointer');
if (animate) {
cvCircle.attr('r', 0)
.transition().duration(dur).delay(delay).ease(d3.easeCubicOut)
.attr('cx', xCV).attr('r', 5);
} else {
cvCircle.attr('r', 5);
}
// Holdout AUROC (filled circle)
const hoCircle = g.append('circle')
.attr('cx', animate ? x0 : xHO).attr('cy', cy)
.attr('fill', ours).attr('stroke', 'var(--page-bg)').attr('stroke-width', 2)
.attr('cursor', 'pointer');
if (animate) {
hoCircle.attr('r', 0)
.transition().duration(dur).delay(delay + 80).ease(d3.easeCubicOut)
.attr('cx', xHO).attr('r', 6);
} else {
hoCircle.attr('r', 6);
}
// Hover target (invisible wide rect)
const hitArea = g.append('rect')
.attr('x', 0).attr('y', cy - y.bandwidth() / 2)
.attr('width', Math.max(0, w)).attr('height', Math.max(0, y.bandwidth()))
.attr('fill', 'transparent').attr('cursor', 'pointer');
hitArea
.on('mouseenter', function(ev) {
cvCircle.attr('r', 7); hoCircle.attr('r', 8);
line.attr('stroke-opacity', 0.6).attr('stroke-width', 3);
tip.innerHTML = `<strong>${d.name}</strong><br/>States: ${d.states} | Features: ${d.features}<br/>CV AUROC: ${d.cvAuroc.toFixed(3)}<br/>Holdout AUROC: <strong>${d.holdout.toFixed(3)}</strong><br/>Top feature: <code style="font-size:11px;background:color-mix(in srgb, var(--text-color) 8%, transparent);padding:2px 5px;border-radius:4px">${d.topFeature}</code>`;
tip.style.opacity = '1';
})
.on('mousemove', function(ev) {
const [mx, my] = d3.pointer(ev, container);
const flipX = mx > rect.width * 0.6;
tip.style.transform = `translate(${flipX ? mx - 200 : mx + 14}px, ${my - 14}px)`;
})
.on('mouseleave', function() {
cvCircle.attr('r', 5); hoCircle.attr('r', 6);
line.attr('stroke-opacity', 0.35).attr('stroke-width', 2);
tip.style.opacity = '0';
});
// Feature pill on right
const pillG = g.append('g')
.attr('transform', `translate(${w + 10}, ${cy})`);
const pillText = pillG.append('text')
.attr('class', 'feature-pill')
.attr('x', 6).attr('y', 4)
.attr('fill', 'var(--text-color)').attr('opacity', 0.65)
.text(d.topFeature);
// Compute pill background after text renders
setTimeout(() => {
const bbox = pillText.node().getBBox();
pillG.insert('rect', 'text')
.attr('x', bbox.x - 5).attr('y', bbox.y - 2)
.attr('width', Math.max(0, bbox.width + 10)).attr('height', Math.max(0, bbox.height + 4))
.attr('rx', 4).attr('fill', 'var(--text-color)').attr('opacity', 0.05);
}, 0);
});
hasAnimated = true;
}
render();
if (window.ResizeObserver) {
let resizeTimer;
new ResizeObserver(() => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(render, 80);
}).observe(container);
}
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
else ensureD3(bootstrap);
})();
</script>