Skip to content

Instantly share code, notes, and snippets.

@adamgen
Last active May 13, 2026 10:26
Show Gist options
  • Select an option

  • Save adamgen/5a14beabad8b72f34f8bb8f693632f0f to your computer and use it in GitHub Desktop.

Select an option

Save adamgen/5a14beabad8b72f34f8bb8f693632f0f to your computer and use it in GitHub Desktop.
A single HTML for viewing LangChain Streams in a better way.

This is a LangChain viewer. It allows a user to upload a JSON Lines file that has a LangChain stream to better understand what is going on inside of it. It is a work in progress and has not been reviewed carefully for precision, and it could have a significantly better user experience. However, it is the best tool I have come across that helps understand what is going on inside LangChain.

Written as a part of my work at definity, you can see it in this temp storage: https://langchain-viewer.tom-5de.workers.dev/.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LangChain JSONL Trace Viewer</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f1117;
--surface: #161b27;
--surface2: #1c2130;
--border: #2a3045;
--border-sub: #212840;
--text: #e2e8f0;
--text-sec: #7c8ba1;
--text-dim: #4a5568;
--parent-bg: rgba(99,132,186,0.10);
--parent-bdr: rgba(99,132,186,0.45);
--sub-bg: rgba(186,130,90,0.10);
--sub-bdr: rgba(186,130,90,0.45);
--pill-info: #1e3a5f;
--pill-info-t: #63a3d4;
--pill-warn: #3d2a0e;
--pill-warn-t: #d4943b;
--pill-ok: #0e3023;
--pill-ok-t: #3db87a;
--pill-err: #3a1020;
--pill-err-t: #d45f7c;
--accent: #4f83cc;
--code-bg: #1e2438;
--code-t: #9db8e0;
--mono: 'Fira Mono', 'Cascadia Code', 'Consolas', monospace;
--sans: system-ui, -apple-system, sans-serif;
--meta-w: 340px;
}
body {
font-family: var(--sans);
background: var(--bg);
color: var(--text);
min-height: 100vh;
font-size: 13px;
line-height: 1.5;
}
/* ── Drop zone ─────────────────────────────────────────────────────────── */
#drop-zone {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
min-height: 100vh;
padding: 40px;
text-align: center;
}
#drop-zone.dragging {
background: rgba(79,131,204,0.06);
outline: 2px dashed var(--accent);
outline-offset: -16px;
}
#drop-zone h1 { font-size: 20px; font-weight: 600; color: var(--text); }
#drop-zone p { color: var(--text-sec); font-size: 13px; max-width: 420px; }
.file-btn {
display: inline-block;
padding: 9px 20px;
background: var(--accent);
color: #fff;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: opacity .15s;
border: none;
}
.file-btn:hover { opacity: .85; }
#file-input { display: none; }
/* ── Main layout ───────────────────────────────────────────────────────── */
#app { display: none; }
#app.visible { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
.page-wrap {
max-width: 100%;
padding: 20px 40px 0;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.page-section { max-width: 1120px; flex-shrink: 0; }
/* ── Typography ─────────────────────────────────────────────────────────── */
h1.page-title { font-size: 18px; font-weight: 600; margin-bottom: 2px; }
h2.section-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; color: var(--text); }
.subtitle { color: var(--text-sec); font-size: 12px; }
/* ── Stats row ──────────────────────────────────────────────────────────── */
.stats-row { display: flex; gap: 24px; margin: 20px 0; flex-wrap: wrap; }
.stat { min-width: 90px; }
.stat-value { font-size: 22px; font-weight: 600; color: var(--text); line-height: 1.1; }
.stat-label { font-size: 11px; color: var(--text-sec); margin-top: 2px; text-transform: uppercase; letter-spacing: .04em; }
/* ── Divider ────────────────────────────────────────────────────────────── */
hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
/* ── Observations ───────────────────────────────────────────────────────── */
.obs-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 24px; }
.obs-item {
background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
padding: 12px 14px; display: flex; gap: 10px; align-items: flex-start;
}
.obs-body { font-size: 12px; color: var(--text-sec); line-height: 1.6; flex: 1; }
/* ── Pills ──────────────────────────────────────────────────────────────── */
.pill { display: inline-block; padding: 2px 7px; border-radius: 4px; font-size: 11px; font-weight: 500; white-space: nowrap; line-height: 1.5; }
.pill-info { background: var(--pill-info); color: var(--pill-info-t); }
.pill-warn { background: var(--pill-warn); color: var(--pill-warn-t); }
.pill-ok { background: var(--pill-ok); color: var(--pill-ok-t); }
.pill-err { background: var(--pill-err); color: var(--pill-err-t); }
.pill-neutral { background: var(--surface2); color: var(--text-sec); }
/* ── Code spans ─────────────────────────────────────────────────────────── */
code {
font-family: var(--mono); background: var(--code-bg); color: var(--code-t);
padding: 1px 5px; border-radius: 3px; font-size: 11px; white-space: nowrap;
}
/* ── Inventory table ────────────────────────────────────────────────────── */
.inventory-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.inventory-table th {
text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--border);
font-size: 11px; font-weight: 600; color: var(--text-sec); text-transform: uppercase; letter-spacing: .04em;
}
.inventory-table td { padding: 5px 10px; border-bottom: 1px solid var(--border-sub); vertical-align: middle; }
.inventory-table tr.parent-row { background: var(--parent-bg); }
.inventory-table tr.subagent-row { background: var(--sub-bg); }
.inventory-table tr:last-child td { border-bottom: none; }
.mono { font-family: var(--mono); color: var(--text-sec); font-size: 11px; }
.mono-dim { font-family: var(--mono); color: var(--text-dim); font-size: 11px; }
/* ── Tool params ────────────────────────────────────────────────────────── */
.tool-params { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 3px; }
.tool-param {
font-family: var(--mono); font-size: 10px; color: var(--text-sec);
background: var(--surface2); border: 1px solid var(--border-sub); border-radius: 3px;
padding: 1px 5px; max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tool-param-key { color: var(--text-dim); }
.tool-param-val { color: var(--code-t); }
/* ── Recent files ───────────────────────────────────────────────────────── */
#recent-section { margin-top: 28px; width: 100%; max-width: 480px; }
#recent-section h2 {
font-size: 11px; font-weight: 600; color: var(--text-dim);
text-transform: uppercase; letter-spacing: .05em; margin-bottom: 8px;
}
.recent-list { display: flex; flex-direction: column; gap: 4px; }
.recent-item {
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
background: var(--surface); border: 1px solid var(--border); border-radius: 5px;
cursor: pointer; transition: border-color .12s;
}
.recent-item:hover { border-color: var(--accent); }
.recent-name { font-size: 13px; color: var(--text); flex: 1; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recent-meta { font-size: 11px; color: var(--text-dim); white-space: nowrap; flex-shrink: 0; }
.recent-del { font-size: 14px; color: var(--text-dim); background: none; border: none; cursor: pointer; padding: 0 2px; line-height: 1; flex-shrink: 0; }
.recent-del:hover { color: var(--pill-err-t); }
/* back button */
#back-btn { display: inline-block; margin-bottom: 14px; font-size: 12px; color: var(--text-sec); cursor: pointer; background: none; border: none; padding: 0; }
#back-btn:hover { color: var(--accent); }
.copy-btn {
display: inline-flex; align-items: center; gap: 3px; font-size: 10px; font-family: var(--sans);
color: var(--text-dim); background: none; border: 1px solid var(--border); border-radius: 3px;
padding: 1px 5px; cursor: pointer; transition: color .12s, border-color .12s; white-space: nowrap;
}
.copy-btn:hover { color: var(--accent); border-color: var(--accent); }
.copy-btn.copied { color: var(--pill-ok-t); border-color: var(--pill-ok-t); }
/* ── Error message ──────────────────────────────────────────────────────── */
.error-box {
background: var(--pill-err); border: 1px solid var(--pill-err-t);
border-radius: 6px; padding: 14px 18px; color: var(--pill-err-t); font-size: 13px; margin-top: 16px;
}
/* ════════════════════════════════════════════════════════════════════════════
FLAT LINE LIST — primary view
Each .fl-row = one JSONL line. Left gutter (line-no) + right JSON cell.
When a group is "active", the metadata panel overlays to the left.
════════════════════════════════════════════════════════════════════════════ */
/* Outer container: metadata panel sits to the left of the line list */
.lines-view {
display: flex;
align-items: stretch;
}
/* ── Metadata panel (left, hidden until a group is activated) ──────────── */
.meta-panel {
flex: 0 0 var(--meta-w);
width: var(--meta-w);
display: none; /* hidden by default */
flex-direction: column;
border-right: 2px solid var(--border);
background: var(--surface);
align-self: stretch;
}
.meta-panel.visible { display: flex; }
.meta-group {
padding: 10px 12px;
border-bottom: 1px solid var(--border-sub);
border-left: 3px solid transparent;
cursor: pointer;
transition: background .1s;
}
.meta-group:last-child { border-bottom: none; }
.meta-group.parent { border-left-color: var(--parent-bdr); }
.meta-group.subagent { border-left-color: var(--sub-bdr); }
.meta-group:hover { filter: brightness(1.1); }
.meta-group.active.parent { background: var(--parent-bg); }
.meta-group.active.subagent { background: var(--sub-bg); }
.meta-group-header {
display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 4px;
}
.meta-lines { font-family: var(--mono); font-size: 10px; color: var(--text-dim); }
.meta-runid { font-family: var(--mono); font-size: 10px; color: var(--text-sec); word-break: break-all; margin: 3px 0; }
.meta-tools { margin-top: 4px; display: flex; flex-direction: column; gap: 3px; }
.meta-tool { font-family: var(--mono); font-size: 10px; color: var(--text-sec); }
.meta-tool-result { font-size: 10px; color: var(--text-dim); }
.meta-snip { font-family: var(--mono); font-size: 10px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ── Line list ─────────────────────────────────────────────────────────── */
.line-list {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.fl-row {
display: grid;
grid-template-columns: 46px 1fr;
border-bottom: 1px solid var(--border-sub);
font-family: var(--mono);
font-size: 11px;
line-height: 1.55;
min-height: fit-content;
cursor: pointer;
}
.fl-row:last-child { border-bottom: none; }
/* When a group is active, lines inside it lose their border and get a background */
.fl-row.group-active {
border-bottom-color: transparent;
}
.fl-row.group-active.parent { background: var(--parent-bg); }
.fl-row.group-active.subagent { background: var(--sub-bg); }
/* Restore bottom border only on the last line of an active group */
.fl-row.group-active.group-last {
border-bottom: 1px solid var(--border-sub);
}
.fl-lineno {
padding: 4px 6px;
color: var(--text-dim);
text-align: right;
border-right: 1px solid var(--border-sub);
user-select: none;
flex-shrink: 0;
}
.fl-json {
padding: 4px 8px;
color: var(--text-sec);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
height: fit-content;
}
.fl-json:hover { background: var(--surface2); }
.fl-json.pretty {
white-space: pre-wrap;
word-break: break-all;
background: var(--surface2);
}
/* ── Lines section title bar ────────────────────────────────────────────── */
.lines-bar {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
padding: 10px 0;
flex-wrap: wrap;
}
.lines-bar h2 { margin: 0; font-size: 14px; font-weight: 600; color: var(--text); }
.lines-bar .hint { font-size: 11px; color: var(--text-dim); font-weight: 400; flex: 1; }
.bar-btn {
font-size: 11px;
font-family: var(--sans);
color: var(--accent);
background: none;
border: 1px solid var(--border);
border-radius: 4px;
padding: 4px 12px;
cursor: pointer;
transition: border-color .12s;
white-space: nowrap;
}
.bar-btn:hover { border-color: var(--accent); }
/* ── Lines view fills remaining viewport height ─────────────────────────── */
.lines-view {
flex: 1;
min-height: 0;
overflow: hidden;
}
.meta-panel { overflow-y: auto; height: 100%; }
.line-list { overflow-y: auto; height: 100%; }
/* ── Metadata modal ─────────────────────────────────────────────────────── */
.modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,.65);
z-index: 100;
align-items: center;
justify-content: center;
padding: 32px;
}
.modal-backdrop.open { display: flex; }
.modal {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
width: 100%;
max-width: 820px;
max-height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-header {
display: flex;
align-items: center;
padding: 14px 18px;
border-bottom: 1px solid var(--border);
gap: 10px;
}
.modal-title { font-size: 14px; font-weight: 600; flex: 1; }
.modal-close {
font-size: 18px;
line-height: 1;
color: var(--text-dim);
background: none;
border: none;
cursor: pointer;
padding: 2px 6px;
}
.modal-close:hover { color: var(--text); }
.modal-body {
overflow-y: auto;
padding: 18px;
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.modal-stats { display: flex; gap: 20px; flex-wrap: wrap; }
.modal-obs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.modal-inventory { width: 100%; border-collapse: collapse; font-size: 12px; }
.modal-inventory th {
text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--border);
font-size: 11px; font-weight: 600; color: var(--text-sec); text-transform: uppercase; letter-spacing: .04em;
}
.modal-inventory td { padding: 5px 10px; border-bottom: 1px solid var(--border-sub); vertical-align: middle; }
.modal-inventory tr.parent-row { background: var(--parent-bg); }
.modal-inventory tr.subagent-row { background: var(--sub-bg); }
.modal-inventory tr:last-child td { border-bottom: none; }
/* ── Responsive ─────────────────────────────────────────────────────────── */
@media (max-width: 700px) {
:root { --meta-w: 220px; }
.obs-grid { grid-template-columns: 1fr; }
.modal-obs { grid-template-columns: 1fr; }
.modal-backdrop { padding: 12px; }
}
</style>
</head>
<body>
<!-- ── Drop / pick zone ────────────────────────────────────────────────────── -->
<div id="drop-zone">
<h1>LangChain JSONL Trace Viewer</h1>
<p>Drop a <code>.jsonl</code> LangChain stream file here, or pick one from disk.</p>
<label class="file-btn">
Open file
<input type="file" id="file-input" accept=".jsonl,.json,.txt">
</label>
<p style="font-size:11px; color:var(--text-dim);">
Works entirely in your browser — nothing is uploaded anywhere.
</p>
<div id="recent-section" style="display:none">
<h2>Recent files</h2>
<div class="recent-list" id="recent-list"></div>
</div>
</div>
<!-- ── Main app view ───────────────────────────────────────────────────────── -->
<div id="app">
<div class="page-wrap">
<div class="page-section">
<h1 class="page-title">LangChain JSONL — turn &amp; tool call map</h1>
<div class="subtitle" id="file-info"></div>
</div>
<div class="lines-bar">
<h2>Lines</h2>
<span class="hint">— click any line to reveal its group metadata</span>
<button class="bar-btn" onclick="showDropZone()">&#8592; open file</button>
<button class="bar-btn" onclick="showMetadataModal()">show metadata</button>
</div>
<div class="lines-view">
<div class="meta-panel" id="meta-panel"></div>
<div class="line-list" id="line-list"></div>
</div>
<div id="error-area"></div>
</div>
</div>
<!-- ── Metadata modal ──────────────────────────────────────────────────────── -->
<div class="modal-backdrop" id="modal-backdrop" onclick="closeMetadataModal(event)">
<div class="modal">
<div class="modal-header">
<span class="modal-title">File metadata</span>
<button class="modal-close" onclick="closeMetadataModal()">&#215;</button>
</div>
<div class="modal-body" id="modal-body"></div>
</div>
</div>
<script>
// ─────────────────────────────────────────────────────────────────────────────
// Recent files — stored in localStorage
// ─────────────────────────────────────────────────────────────────────────────
const RECENT_KEY = 'trace-viewer-recent';
const SELECTED_KEY = 'trace-viewer-selected';
const RECENT_MAX = 5;
const CONTENT_LIMIT = 4 * 1024 * 1024;
function loadRecent() {
try { return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]'); }
catch (_) { return []; }
}
function saveRecent(list) {
try { localStorage.setItem(RECENT_KEY, JSON.stringify(list)); } catch (_) {}
}
function addToRecent(name, size, content) {
let list = loadRecent().filter(r => r.name !== name);
const entry = { name, size, openedAt: Date.now() };
if (size <= CONTENT_LIMIT) entry.content = content;
list.unshift(entry);
list = list.slice(0, RECENT_MAX);
saveRecent(list);
renderRecentList();
}
function removeFromRecent(name) {
saveRecent(loadRecent().filter(r => r.name !== name));
renderRecentList();
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
}
function formatAge(ts) {
const s = Math.floor((Date.now() - ts) / 1000);
if (s < 60) return 'just now';
if (s < 3600) return Math.floor(s / 60) + 'm ago';
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
return Math.floor(s / 86400) + 'd ago';
}
function renderRecentList() {
const list = loadRecent();
const section = document.getElementById('recent-section');
const ul = document.getElementById('recent-list');
if (list.length === 0) { section.style.display = 'none'; return; }
section.style.display = '';
ul.innerHTML = list.map(r => {
const hasContent = !!r.content;
const itemAttrs = hasContent
? `onclick="openFromRecent('${CSS.escape(r.name)}')" title="Click to re-open"`
: `style="opacity:.5;cursor:default" title="Content too large to cache"`;
return `
<div class="recent-item" ${itemAttrs}>
<span class="recent-name">${esc(r.name)}</span>
<span class="recent-meta">${formatSize(r.size)} &middot; ${formatAge(r.openedAt)}</span>
<button class="recent-del" title="Remove" onclick="event.stopPropagation(); removeFromRecent('${CSS.escape(r.name)}')">&#215;</button>
</div>`;
}).join('');
}
function openFromRecent(name) {
const entry = loadRecent().find(r => r.name === name);
if (entry && entry.content) parseAndRender(entry.content, entry.name);
}
function saveSelected(name) { try { localStorage.setItem(SELECTED_KEY, name); } catch (_) {} }
function clearSelected() { try { localStorage.removeItem(SELECTED_KEY); } catch (_) {} }
function loadSelected() { try { return localStorage.getItem(SELECTED_KEY) || null; } catch (_) { return null; } }
// ─────────────────────────────────────────────────────────────────────────────
// File input / drag-drop
// ─────────────────────────────────────────────────────────────────────────────
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const appEl = document.getElementById('app');
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragging'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragging'));
dropZone.addEventListener('drop', e => {
e.preventDefault(); dropZone.classList.remove('dragging');
const file = e.dataTransfer.files[0];
if (file) readFile(file);
});
fileInput.addEventListener('change', () => { if (fileInput.files[0]) readFile(fileInput.files[0]); });
function readFile(file) {
const reader = new FileReader();
reader.onload = e => { addToRecent(file.name, file.size, e.target.result); parseAndRender(e.target.result, file.name); };
reader.readAsText(file);
}
function showDropZone() {
clearSelected();
appEl.classList.remove('visible');
dropZone.style.display = '';
renderRecentList();
}
renderRecentList();
// ─────────────────────────────────────────────────────────────────────────────
// JSONL parser → turn model
// ─────────────────────────────────────────────────────────────────────────────
function parseJSONL(text) {
const lines = text.split('\n').filter(l => l.trim());
const parsed = [];
for (let i = 0; i < lines.length; i++) {
try { parsed.push({ lineNo: i + 1, data: JSON.parse(lines[i]), raw: lines[i] }); }
catch (_) { /* skip malformed */ }
}
return parsed;
}
function buildTurns(entries) {
const turnsMap = new Map();
const turnOrder = [];
for (const { lineNo, data, raw } of entries) {
const runId = data.id || '';
const shortId = runId.replace('lc_run--', '');
if (!turnsMap.has(shortId)) {
turnsMap.set(shortId, {
runId: shortId, kind: 'parent',
startLine: lineNo, endLine: lineNo, aiLastLine: lineNo,
stopReason: null, inputTokens: 0, outputTokens: 0,
textBefore: false, text: '',
toolCalls: [], toolResults: [], entries: [], _tcAccum: {},
});
turnOrder.push(shortId);
}
const turn = turnsMap.get(shortId);
turn.endLine = lineNo;
turn.entries.push({ lineNo, data, raw });
const type = data.type;
if (type === 'AIMessageChunk') {
for (const chunk of (data.tool_call_chunks || [])) {
if (chunk.name && chunk.id) {
turn.toolCalls.push({ line: lineNo, name: chunk.name, toolCallId: chunk.id, index: chunk.index ?? 0, pattern: null, _argsRaw: '' });
turn._tcAccum[chunk.id] = '';
}
}
for (const chunk of (data.tool_call_chunks || [])) {
if (!chunk.name && chunk.args && chunk.index != null) {
const tc = [...turn.toolCalls].reverse().find(t => t.index === chunk.index);
if (tc) tc._argsRaw = (tc._argsRaw || '') + chunk.args;
}
}
const u = data.usage_metadata;
if (u) {
if (u.input_tokens) turn.inputTokens = u.input_tokens;
if (u.output_tokens) turn.outputTokens = u.output_tokens;
}
if (data.chunk_position === 'last') {
turn.aiLastLine = lineNo;
const rm = data.response_metadata || {};
turn.stopReason = rm.stop_reason || rm.finish_reason || 'unknown';
}
for (const item of (data.content || [])) {
if (item.type === 'text' && item.text) {
turn.text += item.text;
if (turn.toolCalls.length === 0) turn.textBefore = true;
}
}
}
if (type === 'tool') {
const resultText = typeof data.content === 'string' ? data.content : JSON.stringify(data.content).slice(0, 200);
turn.toolResults.push({ line: lineNo, name: data.name || '?', toolCallId: data.tool_call_id || '', status: data.status || 'success', snippet: resultText.slice(0, 160) });
turn.endLine = lineNo;
}
}
let inSubagentBlock = false;
let parentTaskRunId = null;
for (const id of turnOrder) {
const turn = turnsMap.get(id);
for (const tc of turn.toolCalls) {
if (tc._argsRaw) {
try {
const args = JSON.parse(tc._argsRaw);
tc.args = args;
if (tc.name === 'grep' && args.pattern) tc.pattern = args.pattern;
if (tc.name === 'task' && args.subagent_type) tc.subagentType = args.subagent_type;
if (tc.name === 'read_file'&& args.path) tc.path = args.path;
} catch (_) {}
}
}
const callsTask = turn.toolCalls.some(tc => tc.name === 'task');
if (callsTask) {
inSubagentBlock = true; parentTaskRunId = id; turn.kind = 'parent';
} else if (inSubagentBlock && id !== parentTaskRunId) {
const hasTaskResult = turn.toolResults.some(tr => tr.name === 'task');
if (hasTaskResult) { turn.kind = 'parent'; inSubagentBlock = false; }
else if (turn.toolResults.length === 0 && turn.toolCalls.length === 0 && !turn.stopReason) { turn.kind = 'parent'; }
else { turn.kind = 'subagent'; }
}
delete turn._tcAccum;
for (const tc of turn.toolCalls) delete tc._argsRaw;
}
return turnOrder.map(id => turnsMap.get(id));
}
// ─────────────────────────────────────────────────────────────────────────────
// Render helpers
// ─────────────────────────────────────────────────────────────────────────────
function pill(text, cls) { return `<span class="pill ${cls}">${esc(text)}</span>`; }
function code(text) { return `<code>${esc(text)}</code>`; }
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function stopPillClass(r) {
if (!r || r === 'unknown') return 'pill-neutral';
if (r === 'end_turn') return 'pill-ok';
if (r === 'tool_use') return 'pill-info';
if (r === 'max_tokens')return 'pill-err';
return 'pill-neutral';
}
const PARAM_SKIP = new Set(['command', 'contents', 'new_string', 'old_string']);
const PARAM_VAL_MAX = 60;
function renderToolParams(tc) {
if (!tc.args || typeof tc.args !== 'object') return '';
const chips = Object.entries(tc.args)
.filter(([k]) => !PARAM_SKIP.has(k))
.map(([k, v]) => {
let display = typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v);
if (display.length > PARAM_VAL_MAX) display = display.slice(0, PARAM_VAL_MAX) + '…';
return `<span class="tool-param"><span class="tool-param-key">${esc(k)}=</span><span class="tool-param-val">${esc(display)}</span></span>`;
});
return chips.length ? `<div class="tool-params">${chips.join('')}</div>` : '';
}
function copyRunId(btn, id) {
navigator.clipboard.writeText(id).then(() => {
btn.textContent = 'copied!'; btn.classList.add('copied');
setTimeout(() => { btn.textContent = 'copy'; btn.classList.remove('copied'); }, 1500);
});
}
// ─────────────────────────────────────────────────────────────────────────────
// Pretty-print toggle on individual JSON lines
// ─────────────────────────────────────────────────────────────────────────────
function toggleJsonPretty(el) {
el.classList.toggle('pretty');
if (el.classList.contains('pretty')) {
try { el.textContent = JSON.stringify(JSON.parse(el.dataset.raw), null, 2); } catch (_) {}
// Highlight the group without toggling meta panel visibility
const row = el.closest('.fl-row');
if (row) highlightGroup(row.dataset.groupId);
} else {
el.textContent = el.dataset.raw;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Group (turn) activation — show/hide metadata and highlight lines
// ─────────────────────────────────────────────────────────────────────────────
let _activeGroupId = null;
function highlightGroup(groupId) {
document.querySelectorAll('.meta-group').forEach(g => {
g.classList.toggle('active', g.dataset.groupId === groupId);
});
let firstActiveRow = null;
let lastActiveRow = null;
document.querySelectorAll('.fl-row').forEach(r => {
r.classList.remove('group-active', 'group-last', 'parent', 'subagent');
if (r.dataset.groupId === groupId) {
r.classList.add('group-active', r.dataset.kind);
if (!firstActiveRow) firstActiveRow = r;
lastActiveRow = r;
}
});
if (lastActiveRow) lastActiveRow.classList.add('group-last');
return firstActiveRow;
}
// scrollIntoView within a scrollable container
function scrollIntoContainer(el, container) {
if (!el || !container) return;
container.scrollTop = el.offsetTop - (container.clientHeight - el.offsetHeight) / 2;
}
// from='left' → selected via meta panel → scroll line-list to first matching fl-row
// from='right' → selected via fl-row → scroll meta-panel to matching meta-group
function activateGroup(groupId, from) {
const metaPanel = document.getElementById('meta-panel');
const lineList = document.getElementById('line-list');
// Toggle off if clicking the already-active group from the same side
if (_activeGroupId === groupId) {
_activeGroupId = null;
document.querySelectorAll('.fl-row').forEach(r => {
r.classList.remove('group-active', 'group-last', 'parent', 'subagent');
});
document.querySelectorAll('.meta-group').forEach(g => g.classList.remove('active'));
metaPanel.classList.remove('visible');
return;
}
_activeGroupId = groupId;
metaPanel.classList.add('visible');
const firstRow = highlightGroup(groupId);
if (from === 'left') {
// Selected from meta panel → scroll line-list to first matching row
scrollIntoContainer(firstRow, lineList);
} else {
// Selected from line list → scroll meta-panel to matching meta-group
const metaGroup = metaPanel.querySelector(`.meta-group[data-group-id="${groupId}"]`);
scrollIntoContainer(metaGroup, metaPanel);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Build & inject the flat line list + meta panel
// ─────────────────────────────────────────────────────────────────────────────
function renderLines(turns, allEntries) {
// Build a lineNo → turn index map for O(1) lookup
const lineToTurn = new Map();
turns.forEach((turn, i) => {
for (const e of turn.entries) lineToTurn.set(e.lineNo, i);
});
// ── Line list HTML ────────────────────────────────────────────────────────
const lineHTML = allEntries.map(e => {
const turnIdx = lineToTurn.get(e.lineNo);
const groupId = turnIdx != null ? String(turnIdx) : 'none';
const kind = turnIdx != null ? turns[turnIdx].kind : 'parent';
return `<div class="fl-row" data-group-id="${groupId}" data-kind="${kind}"
onclick="activateGroup('${groupId}','right')">
<div class="fl-lineno">${e.lineNo}</div>
<div class="fl-json" data-raw="${esc(e.raw)}"
onclick="event.stopPropagation(); toggleJsonPretty(this)">${esc(e.raw)}</div>
</div>`;
}).join('');
// ── Meta panel HTML ───────────────────────────────────────────────────────
const metaHTML = turns.map((turn, i) => {
const lineSpan = turn.endLine - turn.startLine + 1;
const kindPill = turn.kind === 'parent'
? pill('parent', 'pill-info')
: pill('subagent', 'pill-warn');
const stopPill = turn.stopReason
? pill(turn.stopReason, stopPillClass(turn.stopReason))
: `<span class="mono-dim">—</span>`;
let toolsHTML = '';
for (const tc of turn.toolCalls) {
let head = `<span class="meta-tool">${code(tc.name)}`;
if (tc.pattern) head += ` ${code(tc.pattern)}`;
else if (tc.subagentType) head += ` ${code(tc.subagentType)}`;
else if (tc.path) head += ` <span class="mono-dim">${esc(tc.path.slice(-32))}</span>`;
head += `</span>`;
toolsHTML += head + renderToolParams(tc);
}
for (const tr of turn.toolResults) {
const cls = tr.status === 'error' ? 'pill-err' : 'pill-ok';
toolsHTML += `<div class="meta-tool-result">${pill('↳ ' + tr.name, cls)}</div>`;
if (tr.snippet) toolsHTML += `<div class="meta-snip">${esc(tr.snippet.slice(0, 80))}</div>`;
}
return `<div class="meta-group ${turn.kind}" data-group-id="${i}"
onclick="activateGroup('${i}','left')">
<div class="meta-group-header">
${kindPill}
${stopPill}
<span class="meta-lines">${turn.startLine}${turn.endLine} (${lineSpan})</span>
</div>
<div class="meta-runid">
${esc(turn.runId.slice(0, 20))}
<button class="copy-btn" onclick="event.stopPropagation(); copyRunId(this,'${esc(turn.runId)}')">copy</button>
</div>
${toolsHTML ? `<div class="meta-tools">${toolsHTML}</div>` : ''}
</div>`;
}).join('');
document.getElementById('line-list').innerHTML = lineHTML;
document.getElementById('meta-panel').innerHTML = metaHTML;
// Panel starts hidden; _activeGroupId is null
_activeGroupId = null;
document.getElementById('meta-panel').classList.remove('visible');
}
// ─────────────────────────────────────────────────────────────────────────────
// Metadata modal
// ─────────────────────────────────────────────────────────────────────────────
let _modalHTML = '';
function showMetadataModal() {
if (!_modalHTML) return;
document.getElementById('modal-body').innerHTML = _modalHTML;
document.getElementById('modal-backdrop').classList.add('open');
}
function closeMetadataModal(e) {
// Close on backdrop click or explicit call (no event / close button)
if (e && e.target !== document.getElementById('modal-backdrop')) return;
document.getElementById('modal-backdrop').classList.remove('open');
}
document.addEventListener('keydown', e => {
if (e.key === 'Escape') document.getElementById('modal-backdrop').classList.remove('open');
});
// ─────────────────────────────────────────────────────────────────────────────
// Main parse + render
// ─────────────────────────────────────────────────────────────────────────────
function parseAndRender(text, fileName) {
const errorArea = document.getElementById('error-area');
errorArea.innerHTML = '';
let entries;
try { entries = parseJSONL(text); }
catch (e) { errorArea.innerHTML = `<div class="error-box">Parse error: ${esc(e.message)}</div>`; return; }
if (entries.length === 0) {
errorArea.innerHTML = `<div class="error-box">No valid JSONL lines found in file.</div>`;
return;
}
const turns = buildTurns(entries);
const parentTurns = turns.filter(t => t.kind === 'parent');
const subagentTurns = turns.filter(t => t.kind === 'subagent');
const allToolCalls = turns.flatMap(t => t.toolCalls);
const toolNames = [...new Set(allToolCalls.map(tc => tc.name))];
document.getElementById('file-info').textContent = `${fileName} · ${entries.length} lines`;
// ── Build modal content ──────────────────────────────────────────────────
const statsHTML = [
[turns.length, 'turns'],
[parentTurns.length, 'parent'],
[subagentTurns.length, 'subagent'],
[allToolCalls.length, 'tool calls'],
[toolNames.join(', ') || '—', 'tool names'],
].map(([v, l]) =>
`<div class="stat"><div class="stat-value">${esc(String(v))}</div><div class="stat-label">${l}</div></div>`
).join('');
const grepCalls = allToolCalls.filter(tc => tc.name === 'grep');
const obsHTML = [
{ pill: 'subagent', cls: 'pill-warn',
body: `Each subagent turn gets its own <code>lc_run--</code> id. The ${subagentTurns.length} subagent turns appear as a flat sequence — no nesting.` },
{ pill: 'task', cls: 'pill-info',
body: `A subagent call is a normal <code>tool_use</code> named <code>task</code>. The distinguishing info lives inside the streamed arg JSON (e.g. <code>subagent_type</code>).` },
{ pill: 'grep', cls: 'pill-neutral',
body: `${grepCalls.length} grep call${grepCalls.length !== 1 ? 's' : ''} detected. ${grepCalls.map(tc => `<code>${esc(tc.pattern || '?')}</code>`).join(' ')}` },
{ pill: 'result', cls: 'pill-ok',
body: `The subagent's final response is delivered as a plain <code>type:"tool"</code> message, identical in structure to any other tool result.` },
].map(o => `<div class="obs-item"><span class="pill ${o.cls}">${o.pill}</span><div class="obs-body">${o.body}</div></div>`).join('');
const inventoryHTML = turns.flatMap(turn =>
turn.toolCalls.map(tc => {
const patternCell = tc.pattern
? `<code>${esc(tc.pattern)}</code>`
: tc.subagentType ? `<code>${esc(tc.subagentType)}</code>`
: tc.path ? `<span class="mono-dim">${esc(tc.path.slice(-50))}</span>`
: tc.args && Object.keys(tc.args).length > 0
? (() => {
const es = Object.entries(tc.args).filter(([k]) => !PARAM_SKIP.has(k));
if (!es.length) return '<span class="mono-dim">—</span>';
return es.map(([k, v]) => {
let d = typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v);
if (d.length > 50) d = d.slice(0, 50) + '…';
return `<span class="tool-param"><span class="tool-param-key">${esc(k)}=</span><span class="tool-param-val">${esc(d)}</span></span>`;
}).join(' ');
})()
: '<span class="mono-dim">—</span>';
return `<tr class="${turn.kind}-row">
<td class="mono">${tc.line}</td>
<td>${turn.kind === 'parent' ? pill('parent','pill-info') : pill('subagent','pill-warn')}</td>
<td class="mono">${esc(turn.runId.slice(0, 17))}</td>
<td>${code(tc.name)}</td>
<td>${patternCell}</td>
<td class="mono-dim">${esc(tc.toolCallId)}</td>
</tr>`;
})
).join('');
_modalHTML = `
<div class="modal-stats">${statsHTML}</div>
<div>
<h2 class="section-title" style="margin-bottom:10px">Observations</h2>
<div class="modal-obs obs-grid">${obsHTML}</div>
</div>
<div>
<h2 class="section-title" style="margin-bottom:10px">Tool call inventory</h2>
${inventoryHTML
? `<table class="modal-inventory">
<thead><tr><th>line</th><th>kind</th><th>run id</th><th>tool</th><th>pattern / args</th><th>tool call id</th></tr></thead>
<tbody>${inventoryHTML}</tbody>
</table>`
: `<span style="color:var(--text-dim);font-size:12px">No tool calls found.</span>`}
</div>`;
renderLines(turns, entries);
saveSelected(fileName);
dropZone.style.display = 'none';
appEl.classList.add('visible');
window.scrollTo(0, 0);
}
// Restore previously selected trace
(function restoreSelected() {
const name = loadSelected();
if (!name) return;
const entry = loadRecent().find(r => r.name === name);
if (entry && entry.content) parseAndRender(entry.content, entry.name);
})();
</script>
</body>
</html>

LangChain Streaming Response Glossary (entirely AI-generated)

Reference glossary for every key/property that appears in the LangChain server-sent event stream (JSONL format) used by the AI Toolbox chat. Derived from subagent-with-tool-calls.jsonl.


Root-level properties

Each line in the JSONL stream is a single JSON object. Two type variants exist:

AIMessageChunk

Streamed fragment of an AI (LLM) response — either text tokens or tool-call deltas.

Key Type Description
type "AIMessageChunk" LangChain message type discriminator. Indicates this object is a streamed chunk from the language model, not a complete message. Part of LangChain's BaseMessageChunk hierarchy.
id string Unique run identifier, prefixed lc_run-- followed by a UUID. Groups all chunks belonging to the same LLM invocation. Corresponds to LangChain's run ID for tracing/callbacks.
name string | null Optional human-readable name for the message. Always null in observed data; LangChain supports naming messages for multi-agent tracing.
content ContentItem[] Array of content blocks in this chunk. Can be empty (for metadata-only chunks like the first and last), or contain one block of type text, tool_use, or input_json_delta.
additional_kwargs object Passthrough bag for provider-specific extra data not modeled by LangChain. Always {} in observed data. Used by LangChain to carry provider extras like function_call (OpenAI) without polluting the core schema.
response_metadata ResponseMetadata Model and provider metadata attached by LangChain's chat model wrapper. See sub-table below.
tool_calls ToolCall[] Array of validated, fully-parsed tool calls accumulated so far. LangChain populates this when it can successfully parse the streaming tool-call JSON.
invalid_tool_calls InvalidToolCall[] Array of tool call fragments that could not be parsed into valid calls. LangChain places partial/malformed argument fragments here during streaming, moving them to tool_calls once parseable.
tool_call_chunks ToolCallChunk[] Raw, unprocessed tool-call deltas exactly as received from the model provider. These are the low-level streaming fragments before LangChain attempts JSON parsing.
usage_metadata UsageMetadata | null Token usage statistics. null on all chunks except the final one (chunk_position: "last"), where it contains the complete token counts for the entire LLM call.
chunk_position "last" | null Sentinel indicating stream position. null for all chunks except the very last one of an LLM invocation, which is "last". The last chunk typically has empty content, carries usage_metadata, and includes stop_reason in response_metadata.

tool

Result returned from executing a tool call (injected into the stream by the backend agent framework).

Key Type Description
type "tool" LangChain message type discriminator. Indicates this is a ToolMessage — the result of executing a tool.
id string Unique message ID (UUID).
name string Name of the tool that was executed (e.g. "get_tasks", "get_task_run_tfs"). Matches the name from the originating tool_use content block.
content string | (string | object)[] The tool's return value. In practice always a serialized string, but the LangChain ToolMessage type also accepts list[str | dict] | None. Can be very large (entire query result sets).
tool_call_id string ID linking this result back to the specific tool_use request that triggered it (matches content[].id of the tool_use block).
additional_kwargs object Provider passthrough — always {}.
response_metadata object Always {} for tool messages.
artifact null Reserved for structured tool artifacts (e.g. images, files). Always null in observed data. LangChain supports returning artifacts alongside string content.
status string Execution status of the tool call. Observed value: "success". Can also be "error" when tool execution fails.

content[] — Content block types

The content array in an AIMessageChunk contains typed blocks. Three types are observed:

"text"

A fragment of the model's natural-language text response.

Key Type Description
type "text" Discriminator — this block carries streamed text output from the LLM.
text string The actual text token(s) in this chunk. Concatenate across chunks sharing the same id to reconstruct the full text.
index number Zero-based position of this content block within the overall message. Text blocks are always at index 0 when they're the only content.

"tool_use"

The start of a tool call — appears once per tool invocation, carrying the tool name and ID.

Key Type Description
type "tool_use" Discriminator — the model is initiating a tool call. This is Anthropic's native content block type for tool use.
id string Unique tool-call ID generated by the model (e.g. "toolu_01Bf92tBughkLkeUBe9dyzoN"). Used to correlate with the subsequent tool message's tool_call_id. Anthropic prefixes these with toolu_.
name string The name of the tool being called (e.g. "get_tasks", "task").
input object Initial (usually empty {}) input object. The actual arguments arrive incrementally via subsequent input_json_delta blocks.
index number Position of this content block in the message. Multiple tool calls in the same turn have different indices.
caller object Metadata about what triggered this tool call. Contains type (observed: "direct" — meaning the LLM itself initiated the call) and tool_id (null when type is "direct", or a reference to the parent block's ID when a server tool triggers the call).

"input_json_delta"

An incremental fragment of the JSON arguments for a tool call, streamed token-by-token.

Key Type Description
type "input_json_delta" Discriminator — this block is a partial JSON fragment for a tool call's input arguments. Anthropic streams tool arguments as a series of these deltas.
partial_json string A raw fragment of JSON text. Concatenate all partial_json values (in order) for the same tool-call index to reconstruct the full JSON arguments string.
index number Index identifying which tool call this delta belongs to (matches the index of the originating tool_use block).

response_metadata

Metadata about the model response, attached to AIMessageChunk objects.

Key Type Description
model_name string The specific model identifier used (e.g. "claude-sonnet-4-6"). Only present on the first chunk of an invocation.
model_provider string The LLM provider name (e.g. "anthropic"). Present on every chunk. LangChain uses this for provider-specific deserialization.
stop_reason string Why the model stopped generating. Only present on the last chunk. Observed values: "tool_use" (stopped to execute a tool), "end_turn" (natural completion), "max_tokens" (hit token limit). Other possible Anthropic values not observed in this data: "stop_sequence" (custom stop sequence triggered), "refusal" (policy violation), "pause_turn" (long-running turn paused).
stop_sequence string | null The specific stop sequence that triggered generation halt, if any. Always null in observed data (Anthropic uses stop_reason instead).

tool_calls[]

Validated, parsed tool calls. LangChain accumulates these as streaming JSON becomes parseable.

Key Type Description
name string Tool name. Non-empty on the initial chunk (from the tool_use block), empty string "" on subsequent delta chunks.
args object Parsed arguments object. {} while still streaming (JSON incomplete). Populated once LangChain can parse the accumulated partial_json.
id string | null Tool call ID. Non-null on the first chunk (matches content[].id from tool_use), null on delta chunks.
type "tool_call" Discriminator indicating this is a fully validated tool call (as opposed to an invalid one).

invalid_tool_calls[]

Tool call fragments that failed JSON parsing. During streaming, most delta chunks land here because the accumulated JSON is incomplete.

Key Type Description
name string | null Tool name, or null if not yet known.
args string Raw argument fragment string (not parsed JSON). This is the partial_json content that couldn't be parsed yet.
id string | null Tool call ID, or null.
error string | null Parse error description. Always null in observed data — these are "invalid" only because JSON is incomplete during streaming, not because of actual errors.
type "invalid_tool_call" Discriminator for invalid/unparseable tool call entries.

tool_call_chunks[]

Raw streaming deltas for tool calls, exactly as received from the provider before any parsing.

Key Type Description
name string | null Tool name on the initial chunk, null on subsequent deltas.
args string Raw argument fragment. Empty string "" on the initial chunk, then JSON fragments on subsequent chunks. These are the raw partial_json values.
id string | null Tool call ID on the initial chunk, null on subsequent deltas.
index number Zero-based index identifying which tool call this chunk belongs to. Matches the index in the content[] blocks. Crucial for interleaved multi-tool-call streams.
type "tool_call_chunk" Discriminator for raw, unprocessed tool call streaming fragments.

usage_metadata

Token consumption statistics for the entire LLM invocation. Only populated on the final chunk (chunk_position: "last").

Key Type Description
input_tokens number Total input (prompt) tokens consumed by this LLM call.
output_tokens number Total output (completion) tokens generated.
total_tokens number Sum of input_tokens + output_tokens.
input_token_details object Breakdown of input token processing. Provider-specific — for Anthropic, contains caching details.
input_token_details.cache_creation number Tokens written to Anthropic's prompt cache in this call. These tokens were not previously cached and incur cache-write cost.
input_token_details.cache_read number Tokens read from Anthropic's prompt cache (previously cached). These are cheaper than non-cached input tokens.

Enum / sentinel values summary

Field Observed Values Meaning
Root type "AIMessageChunk", "tool" Streamed LLM chunk vs tool execution result
content[].type "text", "tool_use", "input_json_delta" Text output, tool call start, tool argument fragment
chunk_position null, "last" Mid-stream vs final chunk of an invocation
stop_reason "tool_use", "end_turn", "max_tokens", "stop_sequence", "refusal", "pause_turn" Model stopped for tool call, natural end, token limit, custom stop sequence, policy refusal, or paused turn
tool_calls[].type "tool_call" Successfully parsed tool call
invalid_tool_calls[].type "invalid_tool_call" Unparseable (usually incomplete) tool call fragment
tool_call_chunks[].type "tool_call_chunk" Raw streaming delta
content[].caller.type "direct" Tool call initiated directly by the LLM
status "success", "error" Tool executed successfully, or tool execution failed

Stream lifecycle

A typical tool-calling turn flows as follows:

  1. Opening chunkAIMessageChunk with empty content[], carries model_name
  2. Text chunksAIMessageChunk with content[].type = "text", carrying reasoning tokens
  3. Tool-use startAIMessageChunk with content[].type = "tool_use", tool name and id
  4. Argument deltas — Series of AIMessageChunk with content[].type = "input_json_delta", building up the tool's JSON arguments
  5. Closing chunkAIMessageChunk with chunk_position = "last", stop_reason = "tool_use", usage_metadata populated
  6. Tool resulttype = "tool" message with the execution result
  7. Next invocation — Cycle repeats from step 1 as the model processes the tool result
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment