reasonix 0.2.0 → 0.2.2
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.
- package/README.md +9 -9
- package/dist/cli/index.js +259 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,17 +58,17 @@ actually stays byte-stable.
|
|
|
58
58
|
|
|
59
59
|
## Validated numbers
|
|
60
60
|
|
|
61
|
-
**τ-bench-lite** — 8 multi-turn tool-use tasks
|
|
62
|
-
|
|
63
|
-
DeepSeek `deepseek-chat`:
|
|
61
|
+
**τ-bench-lite** — 8 multi-turn tool-use tasks × 3 repeats = 48 runs per
|
|
62
|
+
side. Same tools / same prompt / same client on both sides, sole variable
|
|
63
|
+
is prefix stability. Measured on live DeepSeek `deepseek-chat`:
|
|
64
64
|
|
|
65
65
|
| metric | baseline (cache-hostile) | Reasonix | delta |
|
|
66
66
|
|---|---:|---:|---:|
|
|
67
|
-
| runs |
|
|
68
|
-
| **cache hit** |
|
|
69
|
-
| cost / task | $0.
|
|
67
|
+
| runs | 24 | 24 | — |
|
|
68
|
+
| **cache hit** | 46.6% | **94.4%** | **+47.7pp** |
|
|
69
|
+
| cost / task | $0.002599 | $0.001579 | **−39% (×0.61)** |
|
|
70
70
|
| vs Claude Sonnet 4.6 (token-count estimate) | — | — | **~96% cheaper** |
|
|
71
|
-
| pass rate |
|
|
71
|
+
| pass rate | 96% (23/24) | **100% (24/24)** | Reasonix held the guardrail on every run |
|
|
72
72
|
|
|
73
73
|
**Verify it yourself — no API key, zero cost:**
|
|
74
74
|
|
|
@@ -86,8 +86,8 @@ stays byte-stable across every model call; baseline's prefix churns on
|
|
|
86
86
|
every turn. The cache delta is *mechanically* attributable to log
|
|
87
87
|
stability, not to a different system prompt.
|
|
88
88
|
|
|
89
|
-
Full
|
|
90
|
-
with your own API key: `npx tsx benchmarks/tau-bench/runner.ts`.
|
|
89
|
+
Full 48-run report: [`benchmarks/tau-bench/report.md`][r]. Reproduce
|
|
90
|
+
with your own API key: `npx tsx benchmarks/tau-bench/runner.ts --repeats 3`.
|
|
91
91
|
|
|
92
92
|
[r]: ./benchmarks/tau-bench/report.md
|
|
93
93
|
|
package/dist/cli/index.js
CHANGED
|
@@ -1420,6 +1420,24 @@ function parseTranscript(raw) {
|
|
|
1420
1420
|
}
|
|
1421
1421
|
|
|
1422
1422
|
// src/replay.ts
|
|
1423
|
+
function groupRecordsByTurn(records) {
|
|
1424
|
+
const byTurn = /* @__PURE__ */ new Map();
|
|
1425
|
+
for (const rec of records) {
|
|
1426
|
+
const list = byTurn.get(rec.turn);
|
|
1427
|
+
if (list) list.push(rec);
|
|
1428
|
+
else byTurn.set(rec.turn, [rec]);
|
|
1429
|
+
}
|
|
1430
|
+
return [...byTurn.entries()].sort(([a], [b]) => a - b).map(([turn, records2]) => ({ turn, records: records2 }));
|
|
1431
|
+
}
|
|
1432
|
+
function computeCumulativeStats(pages, upToIdx) {
|
|
1433
|
+
if (upToIdx < 0) return computeReplayStats([]);
|
|
1434
|
+
const flat = [];
|
|
1435
|
+
for (let i = 0; i <= upToIdx && i < pages.length; i++) {
|
|
1436
|
+
const records = pages[i]?.records;
|
|
1437
|
+
if (records) flat.push(...records);
|
|
1438
|
+
}
|
|
1439
|
+
return computeReplayStats(flat);
|
|
1440
|
+
}
|
|
1423
1441
|
function replayFromFile(path) {
|
|
1424
1442
|
const parsed = readTranscript(path);
|
|
1425
1443
|
return { parsed, stats: computeReplayStats(parsed.records) };
|
|
@@ -1491,6 +1509,19 @@ function round2(n, digits) {
|
|
|
1491
1509
|
}
|
|
1492
1510
|
|
|
1493
1511
|
// src/diff.ts
|
|
1512
|
+
function findNextDivergence(pairs, fromIdx) {
|
|
1513
|
+
for (let i = fromIdx + 1; i < pairs.length; i++) {
|
|
1514
|
+
if (pairs[i].kind !== "match") return i;
|
|
1515
|
+
}
|
|
1516
|
+
return -1;
|
|
1517
|
+
}
|
|
1518
|
+
function findPrevDivergence(pairs, fromIdx) {
|
|
1519
|
+
const start = Math.min(fromIdx - 1, pairs.length - 1);
|
|
1520
|
+
for (let i = start; i >= 0; i--) {
|
|
1521
|
+
if (pairs[i].kind !== "match") return i;
|
|
1522
|
+
}
|
|
1523
|
+
return -1;
|
|
1524
|
+
}
|
|
1494
1525
|
function diffTranscripts(a, b) {
|
|
1495
1526
|
const aSide = {
|
|
1496
1527
|
label: a.label,
|
|
@@ -1815,7 +1846,7 @@ function redactKey(key) {
|
|
|
1815
1846
|
}
|
|
1816
1847
|
|
|
1817
1848
|
// src/index.ts
|
|
1818
|
-
var VERSION = "0.2.
|
|
1849
|
+
var VERSION = "0.2.2";
|
|
1819
1850
|
|
|
1820
1851
|
// src/cli/commands/chat.tsx
|
|
1821
1852
|
import { render } from "ink";
|
|
@@ -2566,24 +2597,231 @@ async function chatCommand(opts) {
|
|
|
2566
2597
|
// src/cli/commands/diff.ts
|
|
2567
2598
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
2568
2599
|
import { basename } from "path";
|
|
2569
|
-
|
|
2600
|
+
import { render as render2 } from "ink";
|
|
2601
|
+
import React10 from "react";
|
|
2602
|
+
|
|
2603
|
+
// src/cli/ui/DiffApp.tsx
|
|
2604
|
+
import { Box as Box8, Static as Static2, Text as Text8, useApp as useApp3, useInput } from "ink";
|
|
2605
|
+
import React9, { useState as useState5 } from "react";
|
|
2606
|
+
|
|
2607
|
+
// src/cli/ui/RecordView.tsx
|
|
2608
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
2609
|
+
import React8 from "react";
|
|
2610
|
+
function RecordView({ rec, compact = false }) {
|
|
2611
|
+
const toolArgsMax = compact ? 120 : 200;
|
|
2612
|
+
const toolContentMax = compact ? 200 : 400;
|
|
2613
|
+
if (rec.role === "user") {
|
|
2614
|
+
return /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React8.createElement(Text7, null, rec.content));
|
|
2615
|
+
}
|
|
2616
|
+
if (rec.role === "assistant_final") {
|
|
2617
|
+
return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React8.createElement(CacheBadge, { usage: rec.usage }) : null), rec.content ? /* @__PURE__ */ React8.createElement(Text7, null, rec.content) : /* @__PURE__ */ React8.createElement(Text7, { dimColor: true, italic: true }, "(tool-call response only)"));
|
|
2618
|
+
}
|
|
2619
|
+
if (rec.role === "tool") {
|
|
2620
|
+
return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
|
|
2621
|
+
}
|
|
2622
|
+
if (rec.role === "error") {
|
|
2623
|
+
return /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React8.createElement(Text7, { color: "red" }, rec.error ?? rec.content));
|
|
2624
|
+
}
|
|
2625
|
+
if (rec.role === "done" || rec.role === "assistant_delta") {
|
|
2626
|
+
return null;
|
|
2627
|
+
}
|
|
2628
|
+
return /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, "[", rec.role, "] ", rec.content));
|
|
2629
|
+
}
|
|
2630
|
+
function CacheBadge({ usage }) {
|
|
2631
|
+
const hit = usage.prompt_cache_hit_tokens ?? 0;
|
|
2632
|
+
const miss = usage.prompt_cache_miss_tokens ?? 0;
|
|
2633
|
+
const total = hit + miss;
|
|
2634
|
+
if (total === 0) return null;
|
|
2635
|
+
const pct2 = hit / total * 100;
|
|
2636
|
+
const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
|
|
2637
|
+
return /* @__PURE__ */ React8.createElement(Text7, null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React8.createElement(Text7, { color }, pct2.toFixed(1), "%"));
|
|
2638
|
+
}
|
|
2639
|
+
function truncate3(s, max) {
|
|
2640
|
+
return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// src/cli/ui/DiffApp.tsx
|
|
2644
|
+
function DiffApp({ report }) {
|
|
2645
|
+
const { exit } = useApp3();
|
|
2646
|
+
const maxIdx = Math.max(0, report.pairs.length - 1);
|
|
2647
|
+
const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
|
|
2648
|
+
const [idx, setIdx] = useState5(Math.max(0, initialIdx));
|
|
2649
|
+
useInput((input, key) => {
|
|
2650
|
+
if (input === "q" || key.ctrl && input === "c") {
|
|
2651
|
+
exit();
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
if (input === "j" || key.downArrow || input === " " || key.return) {
|
|
2655
|
+
setIdx((i) => Math.min(maxIdx, i + 1));
|
|
2656
|
+
} else if (input === "k" || key.upArrow) {
|
|
2657
|
+
setIdx((i) => Math.max(0, i - 1));
|
|
2658
|
+
} else if (input === "g") {
|
|
2659
|
+
setIdx(0);
|
|
2660
|
+
} else if (input === "G") {
|
|
2661
|
+
setIdx(maxIdx);
|
|
2662
|
+
} else if (input === "n") {
|
|
2663
|
+
const next = findNextDivergence(report.pairs, idx);
|
|
2664
|
+
if (next !== -1) setIdx(next);
|
|
2665
|
+
} else if (input === "N" || input === "p") {
|
|
2666
|
+
const prev = findPrevDivergence(report.pairs, idx);
|
|
2667
|
+
if (prev !== -1) setIdx(prev);
|
|
2668
|
+
}
|
|
2669
|
+
});
|
|
2670
|
+
const pair = report.pairs[idx];
|
|
2671
|
+
return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(DiffHeader, { report }), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React9.createElement(Text8, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React9.createElement(Text8, null, pair ? /* @__PURE__ */ React9.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React9.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React9.createElement(Text8, null, pair.divergenceNote)) : null, /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "j"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "k"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "N"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "g"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "q"), " ", "quit")));
|
|
2672
|
+
}
|
|
2673
|
+
function DiffHeader({ report }) {
|
|
2674
|
+
const a = report.a;
|
|
2675
|
+
const b = report.b;
|
|
2676
|
+
const cacheDelta = b.stats.cacheHitRatio - a.stats.cacheHitRatio;
|
|
2677
|
+
const costDelta2 = a.stats.totalCostUsd > 0 ? (b.stats.totalCostUsd - a.stats.totalCostUsd) / a.stats.totalCostUsd * 100 : 0;
|
|
2678
|
+
const aStable = a.stats.prefixHashes.length <= 1;
|
|
2679
|
+
const bStable = b.stats.prefixHashes.length <= 1;
|
|
2680
|
+
let prefixLine = null;
|
|
2681
|
+
if (aStable !== bStable) {
|
|
2682
|
+
const stableLabel = aStable ? report.a.label : report.b.label;
|
|
2683
|
+
const churnLabel = aStable ? report.b.label : report.a.label;
|
|
2684
|
+
const churnCount = aStable ? b.stats.prefixHashes.length : a.stats.prefixHashes.length;
|
|
2685
|
+
prefixLine = `${stableLabel} stayed byte-stable; ${churnLabel} churned ${churnCount} distinct prefixes.`;
|
|
2686
|
+
} else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
|
|
2687
|
+
prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
|
|
2688
|
+
}
|
|
2689
|
+
return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React9.createElement(Box8, { justifyContent: "space-between" }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React9.createElement(Text8, { color: "blue" }, a.label), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " vs B="), /* @__PURE__ */ React9.createElement(Text8, { color: "magenta" }, b.label)), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "cache "), /* @__PURE__ */ React9.createElement(Text8, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React9.createElement(Text8, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React9.createElement(Text8, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "cost "), /* @__PURE__ */ React9.createElement(Text8, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React9.createElement(Text8, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React9.createElement(Text8, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "model calls "), /* @__PURE__ */ React9.createElement(Text8, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, italic: true }, prefixLine)) : null);
|
|
2690
|
+
}
|
|
2691
|
+
function Pane({
|
|
2692
|
+
label,
|
|
2693
|
+
headerColor,
|
|
2694
|
+
records
|
|
2695
|
+
}) {
|
|
2696
|
+
return /* @__PURE__ */ React9.createElement(
|
|
2697
|
+
Box8,
|
|
2698
|
+
{
|
|
2699
|
+
flexDirection: "column",
|
|
2700
|
+
flexGrow: 1,
|
|
2701
|
+
paddingX: 1,
|
|
2702
|
+
borderStyle: "single",
|
|
2703
|
+
borderColor: headerColor
|
|
2704
|
+
},
|
|
2705
|
+
/* @__PURE__ */ React9.createElement(Text8, { color: headerColor, bold: true }, label),
|
|
2706
|
+
records.length === 0 ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React9.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React9.createElement(RecordView, { key, rec, compact: true }))
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
function KindBadge({ kind }) {
|
|
2710
|
+
if (kind === "match") {
|
|
2711
|
+
return /* @__PURE__ */ React9.createElement(Text8, { color: "green" }, "\u2713 match");
|
|
2712
|
+
}
|
|
2713
|
+
if (kind === "diverge") {
|
|
2714
|
+
return /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u2605 diverge");
|
|
2715
|
+
}
|
|
2716
|
+
if (kind === "only_in_a") {
|
|
2717
|
+
return /* @__PURE__ */ React9.createElement(Text8, { color: "blue" }, "\u2190 only in A");
|
|
2718
|
+
}
|
|
2719
|
+
return /* @__PURE__ */ React9.createElement(Text8, { color: "magenta" }, "\u2192 only in B");
|
|
2720
|
+
}
|
|
2721
|
+
function paneRecords(pair, side) {
|
|
2722
|
+
if (!pair) return [];
|
|
2723
|
+
const tools = side === "a" ? pair.aTools : pair.bTools;
|
|
2724
|
+
const assistant = side === "a" ? pair.aAssistant : pair.bAssistant;
|
|
2725
|
+
const out = [...tools];
|
|
2726
|
+
if (assistant) out.push(assistant);
|
|
2727
|
+
return out;
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
// src/cli/commands/diff.ts
|
|
2731
|
+
async function diffCommand(opts) {
|
|
2570
2732
|
const aParsed = readTranscript(opts.a);
|
|
2571
2733
|
const bParsed = readTranscript(opts.b);
|
|
2572
2734
|
const report = diffTranscripts(
|
|
2573
2735
|
{ label: opts.labelA ?? basename(opts.a), parsed: aParsed },
|
|
2574
2736
|
{ label: opts.labelB ?? basename(opts.b), parsed: bParsed }
|
|
2575
2737
|
);
|
|
2576
|
-
|
|
2577
|
-
|
|
2738
|
+
const wantMarkdown = !!opts.mdPath;
|
|
2739
|
+
const wantPrint = opts.print || !process.stdout.isTTY;
|
|
2740
|
+
const wantTui = opts.tui || !wantPrint && !wantMarkdown;
|
|
2741
|
+
if (wantMarkdown) {
|
|
2742
|
+
console.log(renderSummaryTable(report));
|
|
2578
2743
|
const md = renderMarkdown(report);
|
|
2579
2744
|
writeFileSync2(opts.mdPath, md, "utf8");
|
|
2580
2745
|
console.log(`
|
|
2581
2746
|
markdown report written to ${opts.mdPath}`);
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
if (wantTui) {
|
|
2750
|
+
const { waitUntilExit } = render2(React10.createElement(DiffApp, { report }), {
|
|
2751
|
+
exitOnCtrlC: true
|
|
2752
|
+
});
|
|
2753
|
+
await waitUntilExit();
|
|
2754
|
+
return;
|
|
2582
2755
|
}
|
|
2756
|
+
console.log(renderSummaryTable(report));
|
|
2583
2757
|
}
|
|
2584
2758
|
|
|
2585
2759
|
// src/cli/commands/replay.ts
|
|
2586
|
-
|
|
2760
|
+
import { render as render3 } from "ink";
|
|
2761
|
+
import React12 from "react";
|
|
2762
|
+
|
|
2763
|
+
// src/cli/ui/ReplayApp.tsx
|
|
2764
|
+
import { Box as Box9, Static as Static3, Text as Text9, useApp as useApp4, useInput as useInput2 } from "ink";
|
|
2765
|
+
import React11, { useMemo as useMemo2, useState as useState6 } from "react";
|
|
2766
|
+
function ReplayApp({ meta, pages }) {
|
|
2767
|
+
const { exit } = useApp4();
|
|
2768
|
+
const maxIdx = Math.max(0, pages.length - 1);
|
|
2769
|
+
const [idx, setIdx] = useState6(maxIdx);
|
|
2770
|
+
useInput2((input, key) => {
|
|
2771
|
+
if (input === "q" || key.ctrl && input === "c") {
|
|
2772
|
+
exit();
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
if (input === "j" || key.downArrow || input === " " || key.return) {
|
|
2776
|
+
setIdx((i) => Math.min(maxIdx, i + 1));
|
|
2777
|
+
} else if (input === "k" || key.upArrow) {
|
|
2778
|
+
setIdx((i) => Math.max(0, i - 1));
|
|
2779
|
+
} else if (input === "g") {
|
|
2780
|
+
setIdx(0);
|
|
2781
|
+
} else if (input === "G") {
|
|
2782
|
+
setIdx(maxIdx);
|
|
2783
|
+
} else if (input === "h" || key.leftArrow) {
|
|
2784
|
+
setIdx(0);
|
|
2785
|
+
} else if (input === "l" || key.rightArrow) {
|
|
2786
|
+
setIdx(maxIdx);
|
|
2787
|
+
}
|
|
2788
|
+
});
|
|
2789
|
+
const cumStats = useMemo2(() => computeCumulativeStats(pages, idx), [pages, idx]);
|
|
2790
|
+
const summary = {
|
|
2791
|
+
turns: cumStats.turns,
|
|
2792
|
+
totalCostUsd: cumStats.totalCostUsd,
|
|
2793
|
+
claudeEquivalentUsd: cumStats.claudeEquivalentUsd,
|
|
2794
|
+
savingsVsClaudePct: cumStats.savingsVsClaudePct,
|
|
2795
|
+
cacheHitRatio: cumStats.cacheHitRatio
|
|
2796
|
+
};
|
|
2797
|
+
const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
|
|
2798
|
+
const currentPage = pages[idx];
|
|
2799
|
+
const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
|
|
2800
|
+
return /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React11.createElement(
|
|
2801
|
+
StatsPanel,
|
|
2802
|
+
{
|
|
2803
|
+
summary,
|
|
2804
|
+
model: cumStats.models[0] ?? meta?.model ?? "?",
|
|
2805
|
+
prefixHash
|
|
2806
|
+
}
|
|
2807
|
+
), /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React11.createElement(Box9, { justifyContent: "space-between" }, /* @__PURE__ */ React11.createElement(Text9, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React11.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React11.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React11.createElement(Text9, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React11.createElement(Box9, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "j"), "/", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "k"), "/", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "q"), " quit")));
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
// src/cli/commands/replay.ts
|
|
2811
|
+
async function replayCommand(opts) {
|
|
2812
|
+
const wantPrint = opts.print || !process.stdout.isTTY || opts.head !== void 0 || opts.tail !== void 0;
|
|
2813
|
+
if (wantPrint) {
|
|
2814
|
+
printReplay(opts);
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
const { parsed } = replayFromFile(opts.path);
|
|
2818
|
+
const pages = groupRecordsByTurn(parsed.records);
|
|
2819
|
+
const { waitUntilExit } = render3(React12.createElement(ReplayApp, { meta: parsed.meta, pages }), {
|
|
2820
|
+
exitOnCtrlC: true
|
|
2821
|
+
});
|
|
2822
|
+
await waitUntilExit();
|
|
2823
|
+
}
|
|
2824
|
+
function printReplay(opts) {
|
|
2587
2825
|
const { parsed, stats } = replayFromFile(opts.path);
|
|
2588
2826
|
if (parsed.meta) {
|
|
2589
2827
|
const m = parsed.meta;
|
|
@@ -2779,16 +3017,27 @@ program.command("stats <transcript>").description("Summarize a JSONL transcript
|
|
|
2779
3017
|
statsCommand({ transcript });
|
|
2780
3018
|
});
|
|
2781
3019
|
program.command("replay <transcript>").description(
|
|
2782
|
-
"
|
|
2783
|
-
).option("--head <n>", "
|
|
2784
|
-
replayCommand({
|
|
3020
|
+
"Interactive Ink TUI to scrub through a transcript + rebuild its session summary (cost, cache, prefix stability). No API calls."
|
|
3021
|
+
).option("--print", "Dump to stdout instead of mounting the TUI (auto when piped)").option("--head <n>", "stdout mode only \u2014 show first N records", (v) => Number.parseInt(v, 10)).option("--tail <n>", "stdout mode only \u2014 show last N records", (v) => Number.parseInt(v, 10)).action(async (transcript, opts) => {
|
|
3022
|
+
await replayCommand({
|
|
2785
3023
|
path: transcript,
|
|
3024
|
+
print: !!opts.print,
|
|
2786
3025
|
head: Number.isFinite(opts.head) ? opts.head : void 0,
|
|
2787
3026
|
tail: Number.isFinite(opts.tail) ? opts.tail : void 0
|
|
2788
3027
|
});
|
|
2789
3028
|
});
|
|
2790
|
-
program.command("diff <a> <b>").description(
|
|
2791
|
-
|
|
3029
|
+
program.command("diff <a> <b>").description(
|
|
3030
|
+
"Compare two transcripts in a split-pane Ink TUI (default) or stdout table. Use n/N to jump across divergences."
|
|
3031
|
+
).option("--md <path>", "Write a markdown report (blog-ready) to this path").option("--print", "Force stdout table instead of the TUI (auto when piped)").option("--tui", "Force the TUI even when piped (rare)").option("--label-a <label>", "Display label for transcript A (default: filename)").option("--label-b <label>", "Display label for transcript B (default: filename)").action(async (a, b, opts) => {
|
|
3032
|
+
await diffCommand({
|
|
3033
|
+
a,
|
|
3034
|
+
b,
|
|
3035
|
+
mdPath: opts.md,
|
|
3036
|
+
labelA: opts.labelA,
|
|
3037
|
+
labelB: opts.labelB,
|
|
3038
|
+
print: !!opts.print,
|
|
3039
|
+
tui: !!opts.tui
|
|
3040
|
+
});
|
|
2792
3041
|
});
|
|
2793
3042
|
program.command("version").description("Print Reasonix version.").action(versionCommand);
|
|
2794
3043
|
program.parseAsync(process.argv).catch((err) => {
|