AgentStateGraph / app /src /content /embeds /trace-explorer.html
seonglae's picture
Automata from Agent Traces — interactive article
59027a2
Raw
History Blame Contribute Delete
13.2 kB
<!-- Trace Explorer: Interactive trace-to-symbol mapper -->
<div class="trace-explorer"></div>
<style>
.trace-explorer { position: relative; width: 100%; font-family: inherit; }
.trace-explorer .seg-control {
display: inline-flex; background: var(--surface-bg);
border: 1px solid var(--border-color); border-radius: 8px;
padding: 3px; gap: 2px;
}
.trace-explorer .seg-control button {
padding: 5px 14px; border-radius: 6px; border: none;
background: transparent; font-size: 12px; font-weight: 500;
color: var(--text-color); cursor: pointer; transition: all 0.15s ease;
opacity: 0.6;
}
.trace-explorer .seg-control button:hover { opacity: 0.8; }
.trace-explorer .seg-control button.active {
background: var(--text-color); color: var(--page-bg);
opacity: 1; font-weight: 600;
}
.trace-explorer .top-bar {
display: flex; align-items: center; gap: 12px; margin-bottom: 14px; flex-wrap: wrap;
}
.trace-explorer .step-info {
font-size: 12px; color: var(--text-color); opacity: 0.5; margin-left: auto;
font-variant-numeric: tabular-nums;
}
.trace-explorer .progress-track {
width: 100%; height: 3px; border-radius: 2px;
background: var(--border-color); margin-bottom: 14px; overflow: hidden;
}
.trace-explorer .progress-fill {
height: 100%; border-radius: 2px; background: var(--text-color);
transition: width 0.3s ease; opacity: 0.5;
}
.trace-explorer .panels {
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
}
@media (max-width: 700px) { .trace-explorer .panels { grid-template-columns: 1fr; } }
.trace-explorer .panel {
border: 1px solid var(--border-color); border-radius: 10px;
background: var(--surface-bg); overflow: hidden;
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
}
.trace-explorer .panel-header {
padding: 10px 14px; font-size: 11px; font-weight: 700; text-transform: uppercase;
letter-spacing: 1px; border-bottom: 1px solid var(--border-color);
color: var(--text-color); opacity: 0.5;
}
.trace-explorer .panel-body {
padding: 10px 12px; font-size: 12px; line-height: 1.6; max-height: 340px;
overflow-y: auto; color: var(--text-color);
}
.trace-explorer .msg {
padding: 7px 10px; margin: 2px 0; border-radius: 6px; transition: all .25s ease;
border-left: 3px solid transparent; cursor: default;
}
.trace-explorer .msg.active-system { background: rgba(107, 143, 113, 0.08); border-left-color: #6b8f71; }
.trace-explorer .msg.active-user { background: rgba(61, 90, 128, 0.08); border-left-color: #8fa6c4; }
.trace-explorer .msg.active-assistant { background: rgba(61, 90, 128, 0.08); border-left-color: #3d5a80; }
.trace-explorer .msg.active-tool { background: rgba(196, 154, 58, 0.08); border-left-color: #c49a3a; }
.trace-explorer .msg .role {
font-weight: 700; font-size: 10px; text-transform: uppercase; letter-spacing: 0.6px;
margin-bottom: 2px;
}
.trace-explorer .msg .role.system { color: #6b8f71; }
.trace-explorer .msg .role.user { color: #8fa6c4; }
.trace-explorer .msg .role.assistant { color: #3d5a80; }
.trace-explorer .msg .role.tool { color: #c49a3a; }
.trace-explorer .msg .content {
font-size: 11px; opacity: 0.55; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.trace-explorer .symbol-flow {
display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
}
.trace-explorer .symbol {
padding: 4px 10px; border-radius: 6px; font-size: 10px; font-weight: 600;
font-family: var(--font-mono, monospace); background: rgba(150,150,150,0.06);
border: 1px solid var(--border-color); opacity: 0.2; transition: all .25s ease;
transform: scale(0.92);
}
.trace-explorer .symbol.revealed { opacity: 1; transform: scale(1); }
.trace-explorer .symbol.current {
border-color: var(--text-color);
box-shadow: 0 0 0 2px rgba(61, 90, 128, 0.15);
}
.trace-explorer .symbol.sym-system { border-color: #6b8f71; }
.trace-explorer .symbol.sym-user { border-color: #8fa6c4; }
.trace-explorer .symbol.sym-assistant { border-color: #3d5a80; }
.trace-explorer .symbol.sym-tool { border-color: #c49a3a; }
.trace-explorer .symbol.revealed.sym-system { background: rgba(107, 143, 113, 0.1); }
.trace-explorer .symbol.revealed.sym-user { background: rgba(61, 90, 128, 0.1); }
.trace-explorer .symbol.revealed.sym-assistant { background: rgba(61, 90, 128, 0.1); }
.trace-explorer .symbol.revealed.sym-tool { background: rgba(196, 154, 58, 0.1); }
.trace-explorer .arrow {
color: var(--text-color); opacity: 0.15; font-size: 12px;
transition: opacity .25s ease;
}
.trace-explorer .arrow.revealed { opacity: 0.4; }
.trace-explorer .rule-callout {
margin-top: 12px; padding: 10px 14px; border-radius: 8px;
background: var(--surface-bg); border: 1px solid var(--border-color);
font-size: 11px; line-height: 1.5; color: var(--text-color);
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
transition: opacity .2s ease;
}
.trace-explorer .rule-callout .rule-icon {
font-weight: 700; margin-right: 6px; opacity: 0.6;
}
</style>
<script>
(() => {
const container = document.querySelector('.trace-explorer:not([data-mounted])');
if (!container) return;
container.dataset.mounted = 'true';
const trace = [
{ role: 'system', content: 'You are a coding assistant. Use bash and str_replace_editor tools...', activity: 'system:text', rule: 'Default: role:content_type' },
{ role: 'user', content: 'Fix the failing test in tests/test_parser.py. The error is IndexError on line 42.', activity: 'user:text', rule: 'Default: role:content_type' },
{ role: 'assistant', content: 'tool_call: bash({"command": "cat tests/test_parser.py"})', activity: 'assistant:tool:bash', rule: 'Tool call: function name = bash' },
{ role: 'tool', content: '# test_parser.py\ndef test_parse_input():\n result = parse("hello world")\n assert result[5] == ...', activity: 'tool:text', rule: 'Default: role:content_type' },
{ role: 'assistant', content: 'tool_call: str_replace_editor({"file": "parser.py", "old": "result[5]", "new": "result[min(5,len(result)-1)]"})', activity: 'assistant:tool:str_replace_editor', rule: 'Tool call: function name = str_replace_editor' },
{ role: 'tool', content: 'File edited successfully.', activity: 'tool:text', rule: 'Default: role:content_type' },
{ role: 'assistant', content: 'tool_call: bash({"command": "python -m pytest tests/test_parser.py -v"})', activity: 'assistant:tool:bash', rule: 'Tool call: function name = bash' },
{ role: 'tool', content: 'tests/test_parser.py::test_parse_input PASSED\n1 passed in 0.02s', activity: 'tool:text', rule: 'Default: role:content_type' },
{ role: 'assistant', content: 'tool_call: submit()', activity: 'assistant:tool:submit', rule: 'Tool call: function name = submit' },
{ role: 'tool', content: 'Submission accepted.', activity: 'tool:text', rule: 'Default: role:content_type' },
];
let step = -1;
// Top bar with segmented control
const topBar = document.createElement('div');
topBar.className = 'top-bar';
const seg = document.createElement('div');
seg.className = 'seg-control';
const prevBtn = document.createElement('button');
prevBtn.textContent = '\u2190 Prev';
const nextBtn = document.createElement('button');
nextBtn.textContent = 'Next \u2192';
nextBtn.className = 'active';
const playBtn = document.createElement('button');
playBtn.textContent = '\u25B6 Play';
const resetBtn = document.createElement('button');
resetBtn.textContent = 'Reset';
seg.append(prevBtn, nextBtn, playBtn, resetBtn);
const stepInfo = document.createElement('span');
stepInfo.className = 'step-info';
topBar.append(seg, stepInfo);
container.appendChild(topBar);
// Progress bar
const progressTrack = document.createElement('div');
progressTrack.className = 'progress-track';
const progressFill = document.createElement('div');
progressFill.className = 'progress-fill';
progressFill.style.width = '0%';
progressTrack.appendChild(progressFill);
container.appendChild(progressTrack);
// Panels
const panels = document.createElement('div');
panels.className = 'panels';
// Left: Raw trace
const leftPanel = document.createElement('div');
leftPanel.className = 'panel';
leftPanel.innerHTML = '<div class="panel-header">Raw Trace Messages</div>';
const leftBody = document.createElement('div');
leftBody.className = 'panel-body';
trace.forEach((msg, i) => {
const div = document.createElement('div');
div.className = 'msg';
div.dataset.idx = i;
div.innerHTML = '<div class="role ' + msg.role + '">' + msg.role + '</div><div class="content">' + msg.content.replace(/</g,'&lt;').slice(0, 80) + (msg.content.length > 80 ? '...' : '') + '</div>';
leftBody.appendChild(div);
});
leftPanel.appendChild(leftBody);
// Right: Symbol sequence + rule
const rightPanel = document.createElement('div');
rightPanel.className = 'panel';
rightPanel.innerHTML = '<div class="panel-header">Activity Sequence</div>';
const rightBody = document.createElement('div');
rightBody.className = 'panel-body';
const flow = document.createElement('div');
flow.className = 'symbol-flow';
trace.forEach((msg, i) => {
if (i > 0) {
const arrow = document.createElement('span');
arrow.className = 'arrow';
arrow.textContent = '\u2192';
arrow.dataset.idx = i;
flow.appendChild(arrow);
}
const sym = document.createElement('span');
sym.className = 'symbol sym-' + msg.role;
sym.textContent = msg.activity;
sym.dataset.idx = i;
flow.appendChild(sym);
});
rightBody.appendChild(flow);
const ruleBox = document.createElement('div');
ruleBox.className = 'rule-callout';
ruleBox.innerHTML = '<span class="rule-icon">f(x)</span> Click "Next" to step through the trace';
rightBody.appendChild(ruleBox);
rightPanel.appendChild(rightBody);
panels.append(leftPanel, rightPanel);
container.appendChild(panels);
function update() {
const pct = step < 0 ? 0 : Math.max(0, ((step + 1) / trace.length) * 100);
progressFill.style.width = pct + '%';
stepInfo.textContent = step < 0 ? 'Ready' : 'Step ' + (step + 1) + ' / ' + trace.length;
// Update active button highlights
prevBtn.classList.toggle('active', false);
nextBtn.classList.toggle('active', step < trace.length - 1);
// Highlight messages with role-specific accent
leftBody.querySelectorAll('.msg').forEach(m => {
const idx = parseInt(m.dataset.idx);
m.className = 'msg';
if (idx === step) {
m.classList.add('active-' + trace[idx].role);
}
});
// Reveal symbols
flow.querySelectorAll('.symbol').forEach(s => {
const idx = parseInt(s.dataset.idx);
s.classList.toggle('revealed', idx <= step);
s.classList.toggle('current', idx === step);
});
flow.querySelectorAll('.arrow').forEach(a => {
const idx = parseInt(a.dataset.idx);
a.classList.toggle('revealed', idx <= step);
});
// Rule callout
if (step >= 0 && step < trace.length) {
ruleBox.innerHTML = '<span class="rule-icon">f(x)</span> ' + trace[step].rule;
ruleBox.style.opacity = '1';
const activeMsg = leftBody.querySelector('.msg[class*="active-"]');
if (activeMsg) {
const msgTop = activeMsg.offsetTop - leftBody.offsetTop;
const msgBot = msgTop + activeMsg.offsetHeight;
const viewTop = leftBody.scrollTop;
const viewBot = viewTop + leftBody.clientHeight;
if (msgTop < viewTop || msgBot > viewBot) {
leftBody.scrollTo({ top: msgTop - leftBody.clientHeight / 2 + activeMsg.offsetHeight / 2, behavior: 'smooth' });
}
}
} else if (step >= trace.length - 1 && step >= 0) {
ruleBox.innerHTML = '<span class="rule-icon">\u2713</span> Extraction complete: 10 messages \u2192 10 symbols from alphabet of 5';
ruleBox.style.opacity = '1';
} else {
ruleBox.innerHTML = '<span class="rule-icon">f(x)</span> Click "Next" to step through the trace';
ruleBox.style.opacity = '0.6';
}
}
prevBtn.addEventListener('click', () => { if (step > -1) { step--; update(); } });
nextBtn.addEventListener('click', () => { if (step < trace.length - 1) { step++; update(); } });
resetBtn.addEventListener('click', () => { step = -1; update(); clearInterval(playTimer); playBtn.textContent = '\u25B6 Play'; playBtn.classList.remove('active'); });
let playTimer = null;
playBtn.addEventListener('click', () => {
if (playTimer) { clearInterval(playTimer); playTimer = null; playBtn.textContent = '\u25B6 Play'; playBtn.classList.remove('active'); return; }
playBtn.textContent = '\u23F8 Pause';
playBtn.classList.add('active');
playTimer = setInterval(() => {
if (step < trace.length - 1) { step++; update(); }
else { clearInterval(playTimer); playTimer = null; playBtn.textContent = '\u25B6 Play'; playBtn.classList.remove('active'); }
}, 1000);
});
update();
})();
</script>