transcript-viewer 0.11.0__tar.gz → 0.11.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/PKG-INFO +1 -1
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/pyproject.toml +1 -1
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/src/transcript_viewer/page.html +129 -16
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/page.test.js +96 -4
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/uv.lock +1 -1
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/.coverage +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/.github/workflows/publish.yml +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/.github/workflows/test.yml +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/.gitignore +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/README.md +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/design/Anchor.dc.html +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/design/Dense.dc.html +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/design/Hover.dc.html +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/design/Main.dc.html +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/design/Quiet.dc.html +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/design/canvas.json +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/design/delegation-timeline.html +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/src/transcript_viewer/__init__.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/src/transcript_viewer/ai.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/src/transcript_viewer/cli.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/src/transcript_viewer/config.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/src/transcript_viewer/corpus.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/src/transcript_viewer/fetch.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/src/transcript_viewer/library.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/src/transcript_viewer/store.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/src/transcript_viewer/viewer.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/fixtures/big-command.json +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_ai.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_cli.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_config.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_corpus.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_fetch.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_library.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_page.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_readme.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_store.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_stream.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_style.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_tidiness.py +0 -0
- {transcript_viewer-0.11.0 → transcript_viewer-0.11.1}/tests/test_viewer.py +0 -0
|
@@ -420,6 +420,9 @@ pre{background:var(--sunk);border-radius:7px;padding:10px;overflow:auto;font-siz
|
|
|
420
420
|
.gapmark:after{content:"";position:absolute;left:-2px;top:0;bottom:0;width:5px;
|
|
421
421
|
background:var(--bg);opacity:.9;border-left:1px dashed var(--muted);
|
|
422
422
|
border-right:1px dashed var(--muted);opacity:.35}
|
|
423
|
+
/* Conversations that belong to the record rather than to any step. */
|
|
424
|
+
.loose{margin-top:18px;padding-top:12px;border-top:1px solid var(--line)}
|
|
425
|
+
.loose-head{font:11px var(--font-mono);color:var(--muted);margin-bottom:8px}
|
|
423
426
|
.lanes-legend{display:flex;align-items:center;gap:14px;margin-top:10px;padding-top:9px;
|
|
424
427
|
border-top:1px solid var(--line);font-size:11px;color:var(--muted);flex-wrap:wrap}
|
|
425
428
|
.lanes-legend i{display:inline-block;width:16px;height:8px;border-radius:2px;
|
|
@@ -2366,6 +2369,9 @@ function haystack(s){
|
|
|
2366
2369
|
Where they do not, the same lanes are laid out by step position and the axis
|
|
2367
2370
|
says so, rather than inventing a duration. */
|
|
2368
2371
|
|
|
2372
|
+
/* "1 subagent" and "2 subagents", said once rather than at each call site. */
|
|
2373
|
+
const many = (n, one, more) => `${num(n)} ${n === 1 ? one : more || one + "s"}`;
|
|
2374
|
+
|
|
2369
2375
|
const at = (s) => {
|
|
2370
2376
|
const t = s && s.timestamp;
|
|
2371
2377
|
if (!t) return null;
|
|
@@ -2419,6 +2425,36 @@ function laneIndex(t) {
|
|
|
2419
2425
|
});
|
|
2420
2426
|
};
|
|
2421
2427
|
walk(t, 0);
|
|
2428
|
+
|
|
2429
|
+
// A subagent that no call refers to. ATIF links delegation from the result of
|
|
2430
|
+
// the call that spawned it, but not every format has a call to link from:
|
|
2431
|
+
// MALT's records carry an advisor, an agent and its raters as trajectories in
|
|
2432
|
+
// their own right, spawned by nothing. Walking only the references missed all
|
|
2433
|
+
// of them, and a record with ten subagents drew no timeline at all.
|
|
2434
|
+
const linked = new Set(lanes.map((l) => l.id));
|
|
2435
|
+
const loose = (parent, depth) => {
|
|
2436
|
+
for (const sub of parent.subagent_trajectories || []) {
|
|
2437
|
+
if (!linked.has(sub.trajectory_id)) {
|
|
2438
|
+
const x = sub.extra || {};
|
|
2439
|
+
lanes.push({
|
|
2440
|
+
id: sub.trajectory_id,
|
|
2441
|
+
kind: x.agentType || sub.agent?.name || "subagent",
|
|
2442
|
+
said: x.description || "",
|
|
2443
|
+
steps: (sub.steps || []).length,
|
|
2444
|
+
tools: [...new Set((sub.steps || []).flatMap(
|
|
2445
|
+
(s) => (s.tool_calls || []).map((c) => c.function_name)))],
|
|
2446
|
+
span: spanMs(sub.steps),
|
|
2447
|
+
fromStep: null,
|
|
2448
|
+
fromIndex: 0,
|
|
2449
|
+
depth,
|
|
2450
|
+
});
|
|
2451
|
+
linked.add(sub.trajectory_id);
|
|
2452
|
+
}
|
|
2453
|
+
loose(sub, depth + 1);
|
|
2454
|
+
}
|
|
2455
|
+
};
|
|
2456
|
+
loose(t, 0);
|
|
2457
|
+
|
|
2422
2458
|
return lanes;
|
|
2423
2459
|
}
|
|
2424
2460
|
|
|
@@ -2529,10 +2565,17 @@ function drawLanes(t) {
|
|
|
2529
2565
|
width:${Math.max(100 / Math.max(1, steps), 0.6)}%" title="${esc(lane.kind)}"
|
|
2530
2566
|
onmouseenter="laneCard(event,'${esc(lane.id)}')" onmouseleave="laneCard(null)"
|
|
2531
2567
|
onclick="openLane('${esc(lane.id)}')"></button>`)).join("");
|
|
2532
|
-
return laneShell(
|
|
2533
|
-
|
|
2568
|
+
return laneShell({
|
|
2569
|
+
count: LANES.length,
|
|
2570
|
+
rows,
|
|
2571
|
+
mainRow: laneRow({ kind: "main thread", steps, main: true }, tight,
|
|
2534
2572
|
`<span class="bar main" style="left:0;width:100%"></span>`),
|
|
2535
|
-
"",
|
|
2573
|
+
marks: "",
|
|
2574
|
+
// No clock to lay anything against, so no duration is claimed and no
|
|
2575
|
+
// overlap either: without times, two bars sitting side by side would
|
|
2576
|
+
// say something the log never recorded.
|
|
2577
|
+
note: "laid out by step · these steps carry no times",
|
|
2578
|
+
});
|
|
2536
2579
|
}
|
|
2537
2580
|
|
|
2538
2581
|
// Everything the run touched, parent and children alike.
|
|
@@ -2579,13 +2622,24 @@ function drawLanes(t) {
|
|
|
2579
2622
|
return { pos: (m.pos / 100) * scale.room, label: m.label };
|
|
2580
2623
|
});
|
|
2581
2624
|
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2625
|
+
// What the axis leaves out, in the reader's terms. How many separate gaps
|
|
2626
|
+
// were collapsed is a fact about the drawing, not about the run, so it is not
|
|
2627
|
+
// said; how much time is missing from the picture very much is.
|
|
2628
|
+
const hidden = scale.gaps.length ? `${duration(scale.skipped)} of waiting hidden` : "";
|
|
2629
|
+
const note = [hidden, "click a bar to open it"].filter(Boolean).join(" · ");
|
|
2630
|
+
|
|
2631
|
+
const [shownAs, wholeAs] = durationPair(scale.kept, hi - lo);
|
|
2632
|
+
return laneShell({
|
|
2633
|
+
count: LANES.length,
|
|
2634
|
+
shown: shownAs,
|
|
2635
|
+
whole: wholeAs,
|
|
2636
|
+
rows,
|
|
2637
|
+
mainRow,
|
|
2638
|
+
marks: marks.map((m) => `<span style="left:${m.pos}%">${esc(m.label)}</span>`).join(""),
|
|
2639
|
+
note,
|
|
2640
|
+
layer: bandLayer + breaks,
|
|
2641
|
+
overlaps: bands.length,
|
|
2642
|
+
});
|
|
2589
2643
|
}
|
|
2590
2644
|
|
|
2591
2645
|
/* One row: who, how many steps, and the track it sits on. */
|
|
@@ -2602,14 +2656,35 @@ function laneRow(lane, tight, inner) {
|
|
|
2602
2656
|
}
|
|
2603
2657
|
|
|
2604
2658
|
/* The frame around the lanes: what it is, the axis, and what was left out. */
|
|
2605
|
-
|
|
2659
|
+
/* What the header says about a run. Every clause is computed, and each is true
|
|
2660
|
+
of any transcript rather than of the one it was written against:
|
|
2661
|
+
|
|
2662
|
+
- the count of subagents;
|
|
2663
|
+
- how much time the axis is showing, and — only when they differ — the whole
|
|
2664
|
+
stretch it was drawn from. Saying "31h" of a run that ran ten days would be
|
|
2665
|
+
wrong, and it is the axis that was shortened, not the run;
|
|
2666
|
+
- how many times two or more were going at once, which is what the view is
|
|
2667
|
+
for. Called "overlapped" rather than "stretches", because the skipped
|
|
2668
|
+
stretches below are the opposite thing and the word was doing both jobs.
|
|
2669
|
+
*/
|
|
2670
|
+
function laneHeader({ count, shown, whole, overlaps }) {
|
|
2671
|
+
const parts = [many(count, "subagent")];
|
|
2672
|
+
if (shown) parts.push(whole && whole !== shown ? `${shown} shown of ${whole}` : shown);
|
|
2673
|
+
if (overlaps) {
|
|
2674
|
+
parts.push(
|
|
2675
|
+
`<em>overlapped ${overlaps === 1 ? "once" : many(overlaps, "time")}</em>`,
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
return parts;
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
function laneShell({ count, shown, whole, rows, mainRow, marks, note, layer, overlaps }) {
|
|
2682
|
+
const parts = laneHeader({ count, shown, whole, overlaps });
|
|
2606
2683
|
return `
|
|
2607
2684
|
<details id="lanes" open>
|
|
2608
2685
|
<summary>
|
|
2609
2686
|
<b>Delegation</b>
|
|
2610
|
-
<span class="fine">${
|
|
2611
|
-
overlaps ? ` · <em>${num(overlaps)} stretch${overlaps === 1 ? "" : "es"} with two or more at once</em>` : ""
|
|
2612
|
-
}</span>
|
|
2687
|
+
<span class="fine">${parts.join(" · ")}</span>
|
|
2613
2688
|
</summary>
|
|
2614
2689
|
<div class="lanes-body">
|
|
2615
2690
|
<div class="lane-axis${rows.includes("lane tight") ? " tight" : ""}">
|
|
@@ -2623,7 +2698,7 @@ function laneShell(count, span, rows, mainRow, marks, note, layer, overlaps) {
|
|
|
2623
2698
|
<div class="lanes-legend">
|
|
2624
2699
|
<span><i style="background:var(--ink);opacity:.85"></i>main thread</span>
|
|
2625
2700
|
<span><i style="background:var(--accent);opacity:.85"></i>subagent</span>
|
|
2626
|
-
${overlaps ? `<span><i style="background:var(--hl)"></i>
|
|
2701
|
+
${overlaps ? `<span><i style="background:var(--hl)"></i>running at the same time</span>` : ""}
|
|
2627
2702
|
<span class="lanes-note">${esc(note)}</span>
|
|
2628
2703
|
</div>
|
|
2629
2704
|
</div>
|
|
@@ -2681,6 +2756,25 @@ function openLane(id) {
|
|
|
2681
2756
|
setTimeout(() => box.classList.remove("found"), 1400);
|
|
2682
2757
|
}
|
|
2683
2758
|
|
|
2759
|
+
/* Subagents that no call refers to. They are drawn after the run rather than
|
|
2760
|
+
inside it, because there is no step they belong to: MALT records an advisor,
|
|
2761
|
+
an agent and its raters as trajectories spawned by nothing, and until now the
|
|
2762
|
+
viewer held them without ever showing them. The timeline made that visible by
|
|
2763
|
+
pointing at boxes that were not there. */
|
|
2764
|
+
function looseSubagents(t){
|
|
2765
|
+
const linked=new Set();
|
|
2766
|
+
const mark=p=>{
|
|
2767
|
+
for(const s of p.steps||[]){
|
|
2768
|
+
for(const r of s.observation?.results||[]){
|
|
2769
|
+
for(const ref of r.subagent_trajectory_ref||[])linked.add(ref.trajectory_id);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
for(const sub of p.subagent_trajectories||[])mark(sub);
|
|
2773
|
+
};
|
|
2774
|
+
mark(t);
|
|
2775
|
+
return (t.subagent_trajectories||[]).filter(s=>!linked.has(s.trajectory_id));
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2684
2778
|
function branchIndex(t){
|
|
2685
2779
|
const out=[];
|
|
2686
2780
|
t.steps.forEach((s,i)=>{
|
|
@@ -2787,7 +2881,16 @@ function trajectoryTab(t,e,branches,branchSteps,counts,q){
|
|
|
2787
2881
|
${shown.map(({s,i})=>step(s,t,0,i,"")).join("")
|
|
2788
2882
|
||`<div class="empty">Nothing matches.</div>`}
|
|
2789
2883
|
${total>shown.length?`<div class="more"><button onclick="more()">Show more —
|
|
2790
|
-
${num(shown.length)} of ${num(total)} steps</button></div>`:""}
|
|
2884
|
+
${num(shown.length)} of ${num(total)} steps</button></div>`:""}
|
|
2885
|
+
${(()=>{
|
|
2886
|
+
// Only in the plain view: a lens or a search is a question about the
|
|
2887
|
+
// steps of this run, and answering it with whole other conversations
|
|
2888
|
+
// would be answering something else.
|
|
2889
|
+
if(lens!=="all"||q)return "";
|
|
2890
|
+
const loose=looseSubagents(t);
|
|
2891
|
+
return loose.length?`<div class="loose"><div class="loose-head">
|
|
2892
|
+
${many(loose.length,"other conversation")} in this record — no step spawned them</div>
|
|
2893
|
+
${loose.map(sub=>branch({trajectory_id:sub.trajectory_id},t,0)).join("")}</div>`:""})()}`;
|
|
2791
2894
|
}
|
|
2792
2895
|
|
|
2793
2896
|
/* The switch stays visible when AI is off for a transcript — otherwise there
|
|
@@ -2936,6 +3039,16 @@ function spanOf(steps){
|
|
|
2936
3039
|
return ms>0?ms:null;
|
|
2937
3040
|
}
|
|
2938
3041
|
|
|
3042
|
+
/* Two durations that will be read against each other, in one unit, so "75m
|
|
3043
|
+
shown of 1.6h" cannot happen — the pair is scaled by the larger of them. */
|
|
3044
|
+
function durationPair(small, large){
|
|
3045
|
+
if(small==null||large==null)return [duration(small),duration(large)];
|
|
3046
|
+
const unit=large/1000<90?1000:large/60000<90?60000:large/3600000<48?3600000:86400000;
|
|
3047
|
+
const suffix=unit===1000?"s":unit===60000?"m":unit===3600000?"h":"d";
|
|
3048
|
+
const show=v=>{const n=v/unit;return (n<10?n.toFixed(1).replace(/\.0$/,""):String(Math.round(n)))+suffix};
|
|
3049
|
+
return [show(small),show(large)];
|
|
3050
|
+
}
|
|
3051
|
+
|
|
2939
3052
|
function duration(ms){
|
|
2940
3053
|
if(ms==null)return null;
|
|
2941
3054
|
// A whole number reads better without the decimal: 2h, not 2.0h.
|
|
@@ -1840,10 +1840,30 @@ test("the timeline says how many ran at once", () => {
|
|
|
1840
1840
|
const html = run(`return drawLanes(${JSON.stringify(TWO_AT_ONCE)})`);
|
|
1841
1841
|
assert.match(html, /id="lanes"/);
|
|
1842
1842
|
assert.match(html, /2 subagents/);
|
|
1843
|
-
assert.match(html, /
|
|
1843
|
+
assert.match(html, /overlapped once/, "one overlap reads as once, not '1 times'");
|
|
1844
1844
|
assert.match(html, /class="band"/, "the overlap is drawn, not only counted");
|
|
1845
1845
|
});
|
|
1846
1846
|
|
|
1847
|
+
test("the header counts, and counts in English", () => {
|
|
1848
|
+
const one = run(`return laneHeader({count: 1, shown: "5m", overlaps: 0}).join(" · ")`);
|
|
1849
|
+
assert.strictEqual(one, "1 subagent · 5m");
|
|
1850
|
+
|
|
1851
|
+
const some = run(`return laneHeader({count: 12, shown: "2h", overlaps: 4}).join(" · ")`);
|
|
1852
|
+
assert.match(some, /^12 subagents · 2h · /);
|
|
1853
|
+
assert.match(some, /overlapped 4 times/);
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
test("the header never passes off the drawn time as the whole run", () => {
|
|
1857
|
+
// The axis skips the waiting, so a run of ten days can show thirty-one hours.
|
|
1858
|
+
// Saying "31h" alone would be false about the run.
|
|
1859
|
+
const elided = run(`return laneHeader({count: 3, shown: "31h", whole: "10d"}).join(" · ")`);
|
|
1860
|
+
assert.match(elided, /31h shown of 10d/);
|
|
1861
|
+
|
|
1862
|
+
// Where nothing was skipped the two are the same and it says it once.
|
|
1863
|
+
const straight = run(`return laneHeader({count: 3, shown: "20m", whole: "20m"}).join(" · ")`);
|
|
1864
|
+
assert.strictEqual(straight, "3 subagents · 20m");
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1847
1867
|
test("a transcript that delegated nothing gets no timeline at all", () => {
|
|
1848
1868
|
const html = run(`return drawLanes({steps:[{step_id:1,source:"user",message:"hi"}]})`);
|
|
1849
1869
|
assert.strictEqual(html, "", "an empty chart says less than no chart");
|
|
@@ -1856,8 +1876,10 @@ test("without timestamps the lanes fall back to step position and say so", () =>
|
|
|
1856
1876
|
for (const s of sub.steps) delete s.timestamp;
|
|
1857
1877
|
}
|
|
1858
1878
|
const html = run(`return drawLanes(${JSON.stringify(untimed)})`);
|
|
1859
|
-
assert.match(html, /no
|
|
1879
|
+
assert.match(html, /carry no times/, "it must not imply a duration it does not have");
|
|
1860
1880
|
assert.ok(!/class="band"/.test(html), "and must not claim overlap it cannot know");
|
|
1881
|
+
assert.ok(!/overlapped/.test(html), "nor say two things ran together");
|
|
1882
|
+
assert.ok(!/hidden/.test(html), "nor that anything was left out");
|
|
1861
1883
|
});
|
|
1862
1884
|
|
|
1863
1885
|
test("the axis is read in whole units, not seven of them", () => {
|
|
@@ -1946,7 +1968,77 @@ test("what was skipped is stated, not swallowed", () => {
|
|
|
1946
1968
|
] }],
|
|
1947
1969
|
};
|
|
1948
1970
|
const html = run(`return drawLanes(${JSON.stringify(t)})`);
|
|
1949
|
-
assert.match(html, /
|
|
1950
|
-
assert.match(html, /
|
|
1971
|
+
assert.match(html, /of waiting hidden/, "a reader must be told time was left out");
|
|
1972
|
+
assert.match(html, /shown of/, "and that the axis is shorter than the run");
|
|
1951
1973
|
assert.match(html, /class="gapmark"/, "and see where");
|
|
1974
|
+
// How many gaps were collapsed is a fact about the drawing, not the run.
|
|
1975
|
+
assert.ok(!/\d+ quiet stretch/.test(html), "that is not the reader's business");
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
test("every phrase in the timeline is computed, none of it written per run", () => {
|
|
1979
|
+
// The header and note are built from counts and durations alone: the same run
|
|
1980
|
+
// renders the same words, and a different one renders different ones for the
|
|
1981
|
+
// same reason. Nothing here is specific to any transcript.
|
|
1982
|
+
const twice = [1, 2].map(() =>
|
|
1983
|
+
run(`return drawLanes(${JSON.stringify(TWO_AT_ONCE)})`),
|
|
1984
|
+
);
|
|
1985
|
+
assert.strictEqual(twice[0], twice[1], "the same run must read the same way");
|
|
1986
|
+
|
|
1987
|
+
const bigger = JSON.parse(JSON.stringify(TWO_AT_ONCE));
|
|
1988
|
+
bigger.subagent_trajectories[1].steps = [
|
|
1989
|
+
{ step_id: 1, source: "agent", timestamp: "2026-01-01T10:30:00Z" },
|
|
1990
|
+
{ step_id: 2, source: "agent", timestamp: "2026-01-01T10:40:00Z" },
|
|
1991
|
+
];
|
|
1992
|
+
const changed = run(`return drawLanes(${JSON.stringify(bigger)})`);
|
|
1993
|
+
assert.notStrictEqual(changed, twice[0], "a different run must read differently");
|
|
1994
|
+
assert.ok(!/overlapped/.test(changed), "and this one no longer overlaps");
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
test("a subagent no call spawned still gets a lane and a box", () => {
|
|
1998
|
+
// MALT records an advisor, an agent and its raters as trajectories in their
|
|
1999
|
+
// own right. Walking only the call references found none of them, so a record
|
|
2000
|
+
// with ten subagents drew no timeline and showed none of them anywhere.
|
|
2001
|
+
const t = {
|
|
2002
|
+
steps: [{ step_id: 1, source: "user", message: "do the thing",
|
|
2003
|
+
timestamp: "2026-01-01T10:00:00Z" }],
|
|
2004
|
+
subagent_trajectories: [
|
|
2005
|
+
{ trajectory_id: "advisor", agent: { name: "metr-malt" }, extra: {},
|
|
2006
|
+
steps: [{ step_id: 1, source: "agent", timestamp: "2026-01-01T10:00:05Z" },
|
|
2007
|
+
{ step_id: 2, source: "agent", timestamp: "2026-01-01T10:02:00Z" }] },
|
|
2008
|
+
{ trajectory_id: "rater", agent: { name: "metr-malt" }, extra: {},
|
|
2009
|
+
steps: [{ step_id: 1, source: "agent", timestamp: "2026-01-01T10:01:00Z" },
|
|
2010
|
+
{ step_id: 2, source: "agent", timestamp: "2026-01-01T10:03:00Z" }] },
|
|
2011
|
+
],
|
|
2012
|
+
};
|
|
2013
|
+
|
|
2014
|
+
const lanes = run(`return laneIndex(${JSON.stringify(t)})`);
|
|
2015
|
+
assert.strictEqual(lanes.length, 2, "both belong on the timeline");
|
|
2016
|
+
assert.deepStrictEqual(lanes.map((l) => l.fromStep), [null, null],
|
|
2017
|
+
"and neither claims a step spawned it");
|
|
2018
|
+
|
|
2019
|
+
const loose = run(`return looseSubagents(${JSON.stringify(t)}).map(s=>s.trajectory_id)`);
|
|
2020
|
+
assert.deepStrictEqual(loose, ["advisor", "rater"],
|
|
2021
|
+
"the transcript must show them somewhere, or the lanes point at nothing");
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
test("a subagent a call did spawn is not also shown loose", () => {
|
|
2025
|
+
const t = {
|
|
2026
|
+
steps: [{ step_id: 1, source: "agent",
|
|
2027
|
+
tool_calls: [{ tool_call_id: "c1", function_name: "Task" }],
|
|
2028
|
+
observation: { results: [{ source_call_id: "c1",
|
|
2029
|
+
subagent_trajectory_ref: [{ trajectory_id: "a" }] }] } }],
|
|
2030
|
+
subagent_trajectories: [{ trajectory_id: "a", agent: { name: "claude-code" },
|
|
2031
|
+
extra: {}, steps: [] }],
|
|
2032
|
+
};
|
|
2033
|
+
const loose = run(`return looseSubagents(${JSON.stringify(t)})`);
|
|
2034
|
+
assert.deepStrictEqual(loose, [], "it already has a home in the run");
|
|
2035
|
+
});
|
|
2036
|
+
|
|
2037
|
+
test("two durations read against each other share a unit", () => {
|
|
2038
|
+
// "75m shown of 1.6h" is two scales in one sentence.
|
|
2039
|
+
const pair = run(`return durationPair(75*60000, 96*60000)`);
|
|
2040
|
+
assert.deepStrictEqual(pair, ["1.3h", "1.6h"]);
|
|
2041
|
+
|
|
2042
|
+
const wide = run(`return durationPair(31*3600000, 10*86400000)`);
|
|
2043
|
+
assert.deepStrictEqual(wide, ["1.3d", "10d"]);
|
|
1952
2044
|
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|