reasonix 0.0.6 → 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 +34 -7
- package/dist/cli/index.js +801 -24
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +211 -2
- package/dist/index.js +446 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1284,7 +1284,13 @@ var CacheFirstLoop = class {
|
|
|
1284
1284
|
name,
|
|
1285
1285
|
content: result
|
|
1286
1286
|
});
|
|
1287
|
-
yield {
|
|
1287
|
+
yield {
|
|
1288
|
+
turn: this._turn,
|
|
1289
|
+
role: "tool",
|
|
1290
|
+
content: result,
|
|
1291
|
+
toolName: name,
|
|
1292
|
+
toolArgs: args
|
|
1293
|
+
};
|
|
1288
1294
|
}
|
|
1289
1295
|
}
|
|
1290
1296
|
yield { turn: this._turn, role: "done", content: "[max_tool_iters reached]" };
|
|
@@ -1337,8 +1343,431 @@ function loadDotenv(path = ".env") {
|
|
|
1337
1343
|
}
|
|
1338
1344
|
}
|
|
1339
1345
|
|
|
1346
|
+
// src/transcript.ts
|
|
1347
|
+
import { createWriteStream, readFileSync as readFileSync3 } from "fs";
|
|
1348
|
+
function recordFromLoopEvent(ev, extra) {
|
|
1349
|
+
const rec = {
|
|
1350
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1351
|
+
turn: ev.turn,
|
|
1352
|
+
role: ev.role,
|
|
1353
|
+
content: ev.content
|
|
1354
|
+
};
|
|
1355
|
+
if (ev.toolName !== void 0) rec.tool = ev.toolName;
|
|
1356
|
+
if (ev.toolArgs !== void 0) rec.args = ev.toolArgs;
|
|
1357
|
+
if (ev.error !== void 0) rec.error = ev.error;
|
|
1358
|
+
if (ev.stats) {
|
|
1359
|
+
rec.usage = {
|
|
1360
|
+
prompt_tokens: ev.stats.usage.promptTokens,
|
|
1361
|
+
completion_tokens: ev.stats.usage.completionTokens,
|
|
1362
|
+
total_tokens: ev.stats.usage.totalTokens,
|
|
1363
|
+
prompt_cache_hit_tokens: ev.stats.usage.promptCacheHitTokens,
|
|
1364
|
+
prompt_cache_miss_tokens: ev.stats.usage.promptCacheMissTokens
|
|
1365
|
+
};
|
|
1366
|
+
rec.cost = ev.stats.cost;
|
|
1367
|
+
rec.model = ev.stats.model;
|
|
1368
|
+
rec.prefixHash = extra.prefixHash;
|
|
1369
|
+
} else if (ev.role === "assistant_final") {
|
|
1370
|
+
rec.model = extra.model;
|
|
1371
|
+
rec.prefixHash = extra.prefixHash;
|
|
1372
|
+
}
|
|
1373
|
+
return rec;
|
|
1374
|
+
}
|
|
1375
|
+
function writeRecord(stream, record) {
|
|
1376
|
+
stream.write(`${JSON.stringify(record)}
|
|
1377
|
+
`);
|
|
1378
|
+
}
|
|
1379
|
+
function writeMeta(stream, meta) {
|
|
1380
|
+
const line = { role: "_meta", meta };
|
|
1381
|
+
stream.write(`${JSON.stringify(line)}
|
|
1382
|
+
`);
|
|
1383
|
+
}
|
|
1384
|
+
function openTranscriptFile(path, meta) {
|
|
1385
|
+
const stream = createWriteStream(path, { flags: "a" });
|
|
1386
|
+
writeMeta(stream, meta);
|
|
1387
|
+
return stream;
|
|
1388
|
+
}
|
|
1389
|
+
function readTranscript(path) {
|
|
1390
|
+
const raw = readFileSync3(path, "utf8");
|
|
1391
|
+
return parseTranscript(raw);
|
|
1392
|
+
}
|
|
1393
|
+
function parseTranscript(raw) {
|
|
1394
|
+
const out = { meta: null, records: [] };
|
|
1395
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1396
|
+
const trimmed = line.trim();
|
|
1397
|
+
if (!trimmed) continue;
|
|
1398
|
+
let obj;
|
|
1399
|
+
try {
|
|
1400
|
+
obj = JSON.parse(trimmed);
|
|
1401
|
+
} catch {
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
if (!obj || typeof obj !== "object") continue;
|
|
1405
|
+
const rec = obj;
|
|
1406
|
+
if (rec.role === "_meta" && rec.meta && typeof rec.meta === "object") {
|
|
1407
|
+
out.meta = rec.meta;
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
if (typeof rec.ts === "string" && typeof rec.turn === "number" && typeof rec.role === "string" && typeof rec.content === "string") {
|
|
1411
|
+
out.records.push(rec);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
return out;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// src/replay.ts
|
|
1418
|
+
function replayFromFile(path) {
|
|
1419
|
+
const parsed = readTranscript(path);
|
|
1420
|
+
return { parsed, stats: computeReplayStats(parsed.records) };
|
|
1421
|
+
}
|
|
1422
|
+
function computeReplayStats(records) {
|
|
1423
|
+
const turns = [];
|
|
1424
|
+
const models = /* @__PURE__ */ new Set();
|
|
1425
|
+
const prefixHashes = /* @__PURE__ */ new Set();
|
|
1426
|
+
let userTurns = 0;
|
|
1427
|
+
let toolCalls = 0;
|
|
1428
|
+
for (const rec of records) {
|
|
1429
|
+
if (rec.role === "user") userTurns++;
|
|
1430
|
+
else if (rec.role === "tool") toolCalls++;
|
|
1431
|
+
else if (rec.role === "assistant_final") {
|
|
1432
|
+
if (rec.model) models.add(rec.model);
|
|
1433
|
+
if (rec.prefixHash) prefixHashes.add(rec.prefixHash);
|
|
1434
|
+
if (rec.usage && rec.model) {
|
|
1435
|
+
const u = new Usage(
|
|
1436
|
+
rec.usage.prompt_tokens ?? 0,
|
|
1437
|
+
rec.usage.completion_tokens ?? 0,
|
|
1438
|
+
rec.usage.total_tokens ?? 0,
|
|
1439
|
+
rec.usage.prompt_cache_hit_tokens ?? 0,
|
|
1440
|
+
rec.usage.prompt_cache_miss_tokens ?? 0
|
|
1441
|
+
);
|
|
1442
|
+
turns.push({
|
|
1443
|
+
turn: rec.turn,
|
|
1444
|
+
model: rec.model,
|
|
1445
|
+
usage: u,
|
|
1446
|
+
// `rec.cost` wins when present — honors whatever the writer computed
|
|
1447
|
+
// even if pricing tables have since changed. Only recompute when
|
|
1448
|
+
// the transcript didn't record it (old format).
|
|
1449
|
+
cost: rec.cost ?? costUsd(rec.model, u),
|
|
1450
|
+
cacheHitRatio: u.cacheHitRatio
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return {
|
|
1456
|
+
perTurn: turns,
|
|
1457
|
+
models: [...models],
|
|
1458
|
+
prefixHashes: [...prefixHashes],
|
|
1459
|
+
userTurns,
|
|
1460
|
+
toolCalls,
|
|
1461
|
+
...summarizeTurns(turns)
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function summarizeTurns(turns) {
|
|
1465
|
+
const totalCost = turns.reduce((s, t) => s + t.cost, 0);
|
|
1466
|
+
const totalClaude = turns.reduce((s, t) => s + claudeEquivalentCost(t.usage), 0);
|
|
1467
|
+
let hit = 0;
|
|
1468
|
+
let miss = 0;
|
|
1469
|
+
for (const t of turns) {
|
|
1470
|
+
hit += t.usage.promptCacheHitTokens;
|
|
1471
|
+
miss += t.usage.promptCacheMissTokens;
|
|
1472
|
+
}
|
|
1473
|
+
const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
|
|
1474
|
+
const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
|
|
1475
|
+
return {
|
|
1476
|
+
turns: turns.length,
|
|
1477
|
+
totalCostUsd: round2(totalCost, 6),
|
|
1478
|
+
claudeEquivalentUsd: round2(totalClaude, 6),
|
|
1479
|
+
savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
|
|
1480
|
+
cacheHitRatio: round2(cacheHitRatio, 4)
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function round2(n, digits) {
|
|
1484
|
+
const f = 10 ** digits;
|
|
1485
|
+
return Math.round(n * f) / f;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// src/diff.ts
|
|
1489
|
+
function diffTranscripts(a, b) {
|
|
1490
|
+
const aSide = {
|
|
1491
|
+
label: a.label,
|
|
1492
|
+
meta: a.parsed.meta,
|
|
1493
|
+
records: a.parsed.records,
|
|
1494
|
+
stats: computeReplayStats(a.parsed.records)
|
|
1495
|
+
};
|
|
1496
|
+
const bSide = {
|
|
1497
|
+
label: b.label,
|
|
1498
|
+
meta: b.parsed.meta,
|
|
1499
|
+
records: b.parsed.records,
|
|
1500
|
+
stats: computeReplayStats(b.parsed.records)
|
|
1501
|
+
};
|
|
1502
|
+
const aByTurn = groupByTurn(a.parsed.records);
|
|
1503
|
+
const bByTurn = groupByTurn(b.parsed.records);
|
|
1504
|
+
const turns = [.../* @__PURE__ */ new Set([...aByTurn.keys(), ...bByTurn.keys()])].sort((x, y) => x - y);
|
|
1505
|
+
const pairs = [];
|
|
1506
|
+
let firstDivergenceTurn = null;
|
|
1507
|
+
for (const turn of turns) {
|
|
1508
|
+
const aGroup = aByTurn.get(turn) ?? { assistant: void 0, tools: [] };
|
|
1509
|
+
const bGroup = bByTurn.get(turn) ?? { assistant: void 0, tools: [] };
|
|
1510
|
+
const aAssistant = aGroup.assistant;
|
|
1511
|
+
const bAssistant = bGroup.assistant;
|
|
1512
|
+
const aTools = aGroup.tools;
|
|
1513
|
+
const bTools = bGroup.tools;
|
|
1514
|
+
let kind;
|
|
1515
|
+
let divergenceNote;
|
|
1516
|
+
if (!aAssistant && bAssistant) kind = "only_in_b";
|
|
1517
|
+
else if (aAssistant && !bAssistant) kind = "only_in_a";
|
|
1518
|
+
else if (!aAssistant && !bAssistant)
|
|
1519
|
+
kind = "diverge";
|
|
1520
|
+
else {
|
|
1521
|
+
divergenceNote = classifyDivergence(aAssistant, bAssistant, aTools, bTools);
|
|
1522
|
+
kind = divergenceNote ? "diverge" : "match";
|
|
1523
|
+
}
|
|
1524
|
+
if (kind !== "match" && firstDivergenceTurn === null) firstDivergenceTurn = turn;
|
|
1525
|
+
pairs.push({ turn, aAssistant, bAssistant, aTools, bTools, kind, divergenceNote });
|
|
1526
|
+
}
|
|
1527
|
+
return { a: aSide, b: bSide, pairs, firstDivergenceTurn };
|
|
1528
|
+
}
|
|
1529
|
+
function classifyDivergence(a, b, aTools, bTools) {
|
|
1530
|
+
const aNames = aTools.map((t) => t.tool ?? "").sort();
|
|
1531
|
+
const bNames = bTools.map((t) => t.tool ?? "").sort();
|
|
1532
|
+
if (aNames.join(",") !== bNames.join(",")) {
|
|
1533
|
+
return `tool calls differ: A=[${aNames.join(",") || "\u2014"}] B=[${bNames.join(",") || "\u2014"}]`;
|
|
1534
|
+
}
|
|
1535
|
+
for (let i = 0; i < aTools.length; i++) {
|
|
1536
|
+
const at = aTools[i];
|
|
1537
|
+
const bt = bTools[i];
|
|
1538
|
+
if (at.tool !== bt.tool) continue;
|
|
1539
|
+
if ((at.args ?? "") !== (bt.args ?? "")) {
|
|
1540
|
+
return `"${at.tool}" args differ`;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
const simRatio = similarity(a.content, b.content);
|
|
1544
|
+
if (simRatio < 0.75) return `text similarity ${(simRatio * 100).toFixed(0)}%`;
|
|
1545
|
+
return void 0;
|
|
1546
|
+
}
|
|
1547
|
+
function similarity(a, b) {
|
|
1548
|
+
if (a === b) return 1;
|
|
1549
|
+
if (!a && !b) return 1;
|
|
1550
|
+
if (!a || !b) return 0;
|
|
1551
|
+
const maxLen = Math.max(a.length, b.length);
|
|
1552
|
+
if (maxLen > 2e3) return tokenOverlap(a, b);
|
|
1553
|
+
const dist = levenshtein(a, b);
|
|
1554
|
+
return 1 - dist / maxLen;
|
|
1555
|
+
}
|
|
1556
|
+
function tokenOverlap(a, b) {
|
|
1557
|
+
const ta = new Set(a.toLowerCase().split(/\s+/).filter(Boolean));
|
|
1558
|
+
const tb = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
|
|
1559
|
+
if (ta.size === 0 && tb.size === 0) return 1;
|
|
1560
|
+
let shared = 0;
|
|
1561
|
+
for (const t of ta) if (tb.has(t)) shared++;
|
|
1562
|
+
return 2 * shared / (ta.size + tb.size);
|
|
1563
|
+
}
|
|
1564
|
+
function levenshtein(a, b) {
|
|
1565
|
+
const m = a.length;
|
|
1566
|
+
const n = b.length;
|
|
1567
|
+
if (m === 0) return n;
|
|
1568
|
+
if (n === 0) return m;
|
|
1569
|
+
let prev = new Array(n + 1);
|
|
1570
|
+
let curr = new Array(n + 1);
|
|
1571
|
+
for (let j = 0; j <= n; j++) prev[j] = j;
|
|
1572
|
+
for (let i = 1; i <= m; i++) {
|
|
1573
|
+
curr[0] = i;
|
|
1574
|
+
for (let j = 1; j <= n; j++) {
|
|
1575
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1576
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
1577
|
+
}
|
|
1578
|
+
[prev, curr] = [curr, prev];
|
|
1579
|
+
}
|
|
1580
|
+
return prev[n];
|
|
1581
|
+
}
|
|
1582
|
+
function groupByTurn(records) {
|
|
1583
|
+
const out = /* @__PURE__ */ new Map();
|
|
1584
|
+
for (const rec of records) {
|
|
1585
|
+
if (rec.role === "user") continue;
|
|
1586
|
+
const g = out.get(rec.turn) ?? { tools: [] };
|
|
1587
|
+
if (rec.role === "assistant_final") g.assistant = rec;
|
|
1588
|
+
else if (rec.role === "tool") g.tools.push(rec);
|
|
1589
|
+
out.set(rec.turn, g);
|
|
1590
|
+
}
|
|
1591
|
+
return out;
|
|
1592
|
+
}
|
|
1593
|
+
function renderSummaryTable(report, _opts = {}) {
|
|
1594
|
+
const a = report.a;
|
|
1595
|
+
const b = report.b;
|
|
1596
|
+
const lines = [];
|
|
1597
|
+
lines.push("Comparing:");
|
|
1598
|
+
lines.push(` A ${a.label}`);
|
|
1599
|
+
lines.push(` B ${b.label}`);
|
|
1600
|
+
lines.push("");
|
|
1601
|
+
lines.push(row(["", "A", "B", "\u0394"], [20, 14, 14, 14]));
|
|
1602
|
+
lines.push(
|
|
1603
|
+
row(["\u2500".repeat(20), "\u2500".repeat(14), "\u2500".repeat(14), "\u2500".repeat(14)], [20, 14, 14, 14])
|
|
1604
|
+
);
|
|
1605
|
+
lines.push(statRow("model calls", a.stats.turns, b.stats.turns));
|
|
1606
|
+
lines.push(statRow("user turns", a.stats.userTurns, b.stats.userTurns));
|
|
1607
|
+
lines.push(statRow("tool calls", a.stats.toolCalls, b.stats.toolCalls));
|
|
1608
|
+
lines.push(
|
|
1609
|
+
row(
|
|
1610
|
+
[
|
|
1611
|
+
"cache hit",
|
|
1612
|
+
`${pct(a.stats.cacheHitRatio)}`,
|
|
1613
|
+
`${pct(b.stats.cacheHitRatio)}`,
|
|
1614
|
+
signPct(b.stats.cacheHitRatio - a.stats.cacheHitRatio)
|
|
1615
|
+
],
|
|
1616
|
+
[20, 14, 14, 14]
|
|
1617
|
+
)
|
|
1618
|
+
);
|
|
1619
|
+
lines.push(
|
|
1620
|
+
row(
|
|
1621
|
+
[
|
|
1622
|
+
"cost (USD)",
|
|
1623
|
+
`$${a.stats.totalCostUsd.toFixed(6)}`,
|
|
1624
|
+
`$${b.stats.totalCostUsd.toFixed(6)}`,
|
|
1625
|
+
costDelta(a.stats.totalCostUsd, b.stats.totalCostUsd)
|
|
1626
|
+
],
|
|
1627
|
+
[20, 14, 14, 14]
|
|
1628
|
+
)
|
|
1629
|
+
);
|
|
1630
|
+
lines.push(statRow("prefix hashes", a.stats.prefixHashes.length, b.stats.prefixHashes.length));
|
|
1631
|
+
lines.push("");
|
|
1632
|
+
const aPrefixStable = a.stats.prefixHashes.length <= 1;
|
|
1633
|
+
const bPrefixStable = b.stats.prefixHashes.length <= 1;
|
|
1634
|
+
if (aPrefixStable !== bPrefixStable) {
|
|
1635
|
+
const stable = aPrefixStable ? "A" : "B";
|
|
1636
|
+
const churn = aPrefixStable ? "B" : "A";
|
|
1637
|
+
const churnCount = aPrefixStable ? b.stats.prefixHashes.length : a.stats.prefixHashes.length;
|
|
1638
|
+
lines.push(
|
|
1639
|
+
`prefix stability: ${stable} stayed byte-stable across ${Math.max(
|
|
1640
|
+
a.stats.turns,
|
|
1641
|
+
b.stats.turns
|
|
1642
|
+
)} turns; ${churn} churned ${churnCount} distinct prefixes.`
|
|
1643
|
+
);
|
|
1644
|
+
lines.push("");
|
|
1645
|
+
} else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
|
|
1646
|
+
lines.push(
|
|
1647
|
+
`prefix: A and B share the same prefix hash (${a.stats.prefixHashes[0].slice(0, 12)}\u2026) \u2014 cache delta is attributable to log stability, not prompt change.`
|
|
1648
|
+
);
|
|
1649
|
+
lines.push("");
|
|
1650
|
+
}
|
|
1651
|
+
if (report.firstDivergenceTurn !== null) {
|
|
1652
|
+
const p = report.pairs.find((p2) => p2.turn === report.firstDivergenceTurn);
|
|
1653
|
+
lines.push(
|
|
1654
|
+
`first divergence: turn ${report.firstDivergenceTurn} \u2014 ${p?.divergenceNote ?? "?"}`
|
|
1655
|
+
);
|
|
1656
|
+
if (p?.aAssistant) lines.push(` A \u2192 ${truncate(p.aAssistant.content, 100)}`);
|
|
1657
|
+
if (p?.bAssistant) lines.push(` B \u2192 ${truncate(p.bAssistant.content, 100)}`);
|
|
1658
|
+
} else {
|
|
1659
|
+
lines.push("no material divergence detected (texts within similarity threshold).");
|
|
1660
|
+
}
|
|
1661
|
+
return lines.join("\n");
|
|
1662
|
+
}
|
|
1663
|
+
function renderMarkdown(report) {
|
|
1664
|
+
const a = report.a;
|
|
1665
|
+
const b = report.b;
|
|
1666
|
+
const out = [];
|
|
1667
|
+
out.push(`# Transcript diff: ${a.label} vs ${b.label}`);
|
|
1668
|
+
out.push("");
|
|
1669
|
+
if (a.meta || b.meta) {
|
|
1670
|
+
out.push("## Meta");
|
|
1671
|
+
out.push("");
|
|
1672
|
+
out.push(`| | ${a.label} | ${b.label} |`);
|
|
1673
|
+
out.push("|---|---|---|");
|
|
1674
|
+
out.push(`| source | ${a.meta?.source ?? "\u2014"} | ${b.meta?.source ?? "\u2014"} |`);
|
|
1675
|
+
out.push(`| model | ${a.meta?.model ?? "\u2014"} | ${b.meta?.model ?? "\u2014"} |`);
|
|
1676
|
+
out.push(`| task | ${a.meta?.task ?? "\u2014"} | ${b.meta?.task ?? "\u2014"} |`);
|
|
1677
|
+
out.push(`| startedAt | ${a.meta?.startedAt ?? "\u2014"} | ${b.meta?.startedAt ?? "\u2014"} |`);
|
|
1678
|
+
out.push("");
|
|
1679
|
+
}
|
|
1680
|
+
out.push("## Summary");
|
|
1681
|
+
out.push("");
|
|
1682
|
+
out.push(`| metric | ${a.label} | ${b.label} | delta |`);
|
|
1683
|
+
out.push("|---|---:|---:|---:|");
|
|
1684
|
+
out.push(
|
|
1685
|
+
`| model calls | ${a.stats.turns} | ${b.stats.turns} | ${signed(b.stats.turns - a.stats.turns)} |`
|
|
1686
|
+
);
|
|
1687
|
+
out.push(
|
|
1688
|
+
`| user turns | ${a.stats.userTurns} | ${b.stats.userTurns} | ${signed(b.stats.userTurns - a.stats.userTurns)} |`
|
|
1689
|
+
);
|
|
1690
|
+
out.push(
|
|
1691
|
+
`| tool calls | ${a.stats.toolCalls} | ${b.stats.toolCalls} | ${signed(b.stats.toolCalls - a.stats.toolCalls)} |`
|
|
1692
|
+
);
|
|
1693
|
+
out.push(
|
|
1694
|
+
`| cache hit | ${pct(a.stats.cacheHitRatio)} | ${pct(b.stats.cacheHitRatio)} | **${signPct(b.stats.cacheHitRatio - a.stats.cacheHitRatio)}** |`
|
|
1695
|
+
);
|
|
1696
|
+
out.push(
|
|
1697
|
+
`| cost (USD) | $${a.stats.totalCostUsd.toFixed(6)} | $${b.stats.totalCostUsd.toFixed(6)} | ${costDelta(a.stats.totalCostUsd, b.stats.totalCostUsd)} |`
|
|
1698
|
+
);
|
|
1699
|
+
out.push(
|
|
1700
|
+
`| prefix hashes | ${a.stats.prefixHashes.length} | ${b.stats.prefixHashes.length} | \u2014 |`
|
|
1701
|
+
);
|
|
1702
|
+
out.push("");
|
|
1703
|
+
out.push("## Turn-by-turn");
|
|
1704
|
+
out.push("");
|
|
1705
|
+
out.push(`| turn | kind | ${a.label} tool calls | ${b.label} tool calls | note |`);
|
|
1706
|
+
out.push("|---:|:---:|---|---|---|");
|
|
1707
|
+
for (const p of report.pairs) {
|
|
1708
|
+
const aTools = p.aTools.map((t) => t.tool).filter(Boolean).join(", ") || "\u2014";
|
|
1709
|
+
const bTools = p.bTools.map((t) => t.tool).filter(Boolean).join(", ") || "\u2014";
|
|
1710
|
+
out.push(`| ${p.turn} | ${p.kind} | ${aTools} | ${bTools} | ${p.divergenceNote ?? ""} |`);
|
|
1711
|
+
}
|
|
1712
|
+
out.push("");
|
|
1713
|
+
if (report.firstDivergenceTurn !== null) {
|
|
1714
|
+
const p = report.pairs.find((x) => x.turn === report.firstDivergenceTurn);
|
|
1715
|
+
out.push(`## First divergence (turn ${report.firstDivergenceTurn})`);
|
|
1716
|
+
out.push("");
|
|
1717
|
+
out.push(p?.divergenceNote ?? "");
|
|
1718
|
+
out.push("");
|
|
1719
|
+
if (p?.aAssistant) {
|
|
1720
|
+
out.push(`**${a.label}:**`);
|
|
1721
|
+
out.push("");
|
|
1722
|
+
out.push("```");
|
|
1723
|
+
out.push(p.aAssistant.content);
|
|
1724
|
+
out.push("```");
|
|
1725
|
+
out.push("");
|
|
1726
|
+
}
|
|
1727
|
+
if (p?.bAssistant) {
|
|
1728
|
+
out.push(`**${b.label}:**`);
|
|
1729
|
+
out.push("");
|
|
1730
|
+
out.push("```");
|
|
1731
|
+
out.push(p.bAssistant.content);
|
|
1732
|
+
out.push("```");
|
|
1733
|
+
out.push("");
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return out.join("\n");
|
|
1737
|
+
}
|
|
1738
|
+
function row(cols, widths) {
|
|
1739
|
+
return cols.map((c, i) => padRight(c, widths[i] ?? c.length)).join(" ");
|
|
1740
|
+
}
|
|
1741
|
+
function statRow(label, av, bv) {
|
|
1742
|
+
return row([label, `${av}`, `${bv}`, signed(bv - av)], [20, 14, 14, 14]);
|
|
1743
|
+
}
|
|
1744
|
+
function padRight(s, w) {
|
|
1745
|
+
return s.length >= w ? s : s + " ".repeat(w - s.length);
|
|
1746
|
+
}
|
|
1747
|
+
function signed(n) {
|
|
1748
|
+
if (n === 0) return "0";
|
|
1749
|
+
return `${n > 0 ? "+" : ""}${n}`;
|
|
1750
|
+
}
|
|
1751
|
+
function signPct(diff) {
|
|
1752
|
+
if (diff === 0) return "0pp";
|
|
1753
|
+
const s = (diff * 100).toFixed(1);
|
|
1754
|
+
return `${diff > 0 ? "+" : ""}${s}pp`;
|
|
1755
|
+
}
|
|
1756
|
+
function pct(x) {
|
|
1757
|
+
return `${(x * 100).toFixed(1)}%`;
|
|
1758
|
+
}
|
|
1759
|
+
function costDelta(a, b) {
|
|
1760
|
+
if (a === 0 && b === 0) return "\u2014";
|
|
1761
|
+
if (a === 0) return "new";
|
|
1762
|
+
const pctChange = (b - a) / a * 100;
|
|
1763
|
+
return `${pctChange > 0 ? "+" : ""}${pctChange.toFixed(1)}%`;
|
|
1764
|
+
}
|
|
1765
|
+
function truncate(s, n) {
|
|
1766
|
+
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1340
1769
|
// src/config.ts
|
|
1341
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as
|
|
1770
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
1342
1771
|
import { homedir as homedir2 } from "os";
|
|
1343
1772
|
import { dirname as dirname2, join as join2 } from "path";
|
|
1344
1773
|
function defaultConfigPath() {
|
|
@@ -1346,7 +1775,7 @@ function defaultConfigPath() {
|
|
|
1346
1775
|
}
|
|
1347
1776
|
function readConfig(path = defaultConfigPath()) {
|
|
1348
1777
|
try {
|
|
1349
|
-
const raw =
|
|
1778
|
+
const raw = readFileSync4(path, "utf8");
|
|
1350
1779
|
const parsed = JSON.parse(raw);
|
|
1351
1780
|
if (parsed && typeof parsed === "object") return parsed;
|
|
1352
1781
|
} catch {
|
|
@@ -1381,7 +1810,7 @@ function redactKey(key) {
|
|
|
1381
1810
|
}
|
|
1382
1811
|
|
|
1383
1812
|
// src/index.ts
|
|
1384
|
-
var VERSION = "0.
|
|
1813
|
+
var VERSION = "0.2.2";
|
|
1385
1814
|
export {
|
|
1386
1815
|
AppendOnlyLog,
|
|
1387
1816
|
CacheFirstLoop,
|
|
@@ -1398,10 +1827,12 @@ export {
|
|
|
1398
1827
|
analyzeSchema,
|
|
1399
1828
|
appendSessionMessage,
|
|
1400
1829
|
claudeEquivalentCost,
|
|
1830
|
+
computeReplayStats,
|
|
1401
1831
|
costUsd,
|
|
1402
1832
|
defaultConfigPath,
|
|
1403
1833
|
defaultSelector,
|
|
1404
1834
|
deleteSession,
|
|
1835
|
+
diffTranscripts,
|
|
1405
1836
|
emptyPlanState,
|
|
1406
1837
|
fetchWithRetry,
|
|
1407
1838
|
flattenSchema,
|
|
@@ -1413,15 +1844,25 @@ export {
|
|
|
1413
1844
|
loadDotenv,
|
|
1414
1845
|
loadSessionMessages,
|
|
1415
1846
|
nestArguments,
|
|
1847
|
+
openTranscriptFile,
|
|
1848
|
+
parseTranscript,
|
|
1416
1849
|
readConfig,
|
|
1850
|
+
readTranscript,
|
|
1851
|
+
recordFromLoopEvent,
|
|
1417
1852
|
redactKey,
|
|
1853
|
+
renderMarkdown as renderDiffMarkdown,
|
|
1854
|
+
renderSummaryTable as renderDiffSummary,
|
|
1418
1855
|
repairTruncatedJson,
|
|
1856
|
+
replayFromFile,
|
|
1419
1857
|
runBranches,
|
|
1420
1858
|
sanitizeName as sanitizeSessionName,
|
|
1421
1859
|
saveApiKey,
|
|
1422
1860
|
scavengeToolCalls,
|
|
1423
1861
|
sessionPath,
|
|
1424
1862
|
sessionsDir,
|
|
1425
|
-
|
|
1863
|
+
similarity,
|
|
1864
|
+
writeConfig,
|
|
1865
|
+
writeMeta,
|
|
1866
|
+
writeRecord
|
|
1426
1867
|
};
|
|
1427
1868
|
//# sourceMappingURL=index.js.map
|