reasonix 0.0.5 → 0.2.0
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 +42 -8
- package/dist/cli/index.js +761 -44
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +253 -2
- package/dist/index.js +570 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -778,6 +778,93 @@ function signature2(call) {
|
|
|
778
778
|
return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
|
|
779
779
|
}
|
|
780
780
|
|
|
781
|
+
// src/session.ts
|
|
782
|
+
import {
|
|
783
|
+
appendFileSync,
|
|
784
|
+
chmodSync,
|
|
785
|
+
existsSync,
|
|
786
|
+
mkdirSync,
|
|
787
|
+
readFileSync,
|
|
788
|
+
readdirSync,
|
|
789
|
+
statSync,
|
|
790
|
+
unlinkSync
|
|
791
|
+
} from "fs";
|
|
792
|
+
import { homedir } from "os";
|
|
793
|
+
import { dirname, join } from "path";
|
|
794
|
+
function sessionsDir() {
|
|
795
|
+
return join(homedir(), ".reasonix", "sessions");
|
|
796
|
+
}
|
|
797
|
+
function sessionPath(name) {
|
|
798
|
+
return join(sessionsDir(), `${sanitizeName(name)}.jsonl`);
|
|
799
|
+
}
|
|
800
|
+
function sanitizeName(name) {
|
|
801
|
+
const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
|
|
802
|
+
return cleaned || "default";
|
|
803
|
+
}
|
|
804
|
+
function loadSessionMessages(name) {
|
|
805
|
+
const path = sessionPath(name);
|
|
806
|
+
if (!existsSync(path)) return [];
|
|
807
|
+
try {
|
|
808
|
+
const raw = readFileSync(path, "utf8");
|
|
809
|
+
const out = [];
|
|
810
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
811
|
+
const trimmed = line.trim();
|
|
812
|
+
if (!trimmed) continue;
|
|
813
|
+
try {
|
|
814
|
+
const msg = JSON.parse(trimmed);
|
|
815
|
+
if (msg && typeof msg === "object" && "role" in msg) out.push(msg);
|
|
816
|
+
} catch {
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return out;
|
|
820
|
+
} catch {
|
|
821
|
+
return [];
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function appendSessionMessage(name, message) {
|
|
825
|
+
const path = sessionPath(name);
|
|
826
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
827
|
+
appendFileSync(path, `${JSON.stringify(message)}
|
|
828
|
+
`, "utf8");
|
|
829
|
+
try {
|
|
830
|
+
chmodSync(path, 384);
|
|
831
|
+
} catch {
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function listSessions() {
|
|
835
|
+
const dir = sessionsDir();
|
|
836
|
+
if (!existsSync(dir)) return [];
|
|
837
|
+
try {
|
|
838
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
839
|
+
return files.map((file) => {
|
|
840
|
+
const path = join(dir, file);
|
|
841
|
+
const stat = statSync(path);
|
|
842
|
+
const name = file.replace(/\.jsonl$/, "");
|
|
843
|
+
const messageCount = countLines(path);
|
|
844
|
+
return { name, path, size: stat.size, messageCount, mtime: stat.mtime };
|
|
845
|
+
}).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
846
|
+
} catch {
|
|
847
|
+
return [];
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function deleteSession(name) {
|
|
851
|
+
const path = sessionPath(name);
|
|
852
|
+
try {
|
|
853
|
+
unlinkSync(path);
|
|
854
|
+
return true;
|
|
855
|
+
} catch {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function countLines(path) {
|
|
860
|
+
try {
|
|
861
|
+
const raw = readFileSync(path, "utf8");
|
|
862
|
+
return raw.split(/\r?\n/).filter((l) => l.trim()).length;
|
|
863
|
+
} catch {
|
|
864
|
+
return 0;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
781
868
|
// src/telemetry.ts
|
|
782
869
|
var DEEPSEEK_PRICING = {
|
|
783
870
|
"deepseek-chat": { inputCacheHit: 0.07, inputCacheMiss: 0.27, output: 1.1 },
|
|
@@ -934,6 +1021,9 @@ var CacheFirstLoop = class {
|
|
|
934
1021
|
harvestOptions;
|
|
935
1022
|
branchEnabled;
|
|
936
1023
|
branchOptions;
|
|
1024
|
+
sessionName;
|
|
1025
|
+
/** Number of messages that were pre-loaded from the session file. */
|
|
1026
|
+
resumedMessageCount;
|
|
937
1027
|
_turn = 0;
|
|
938
1028
|
_streamPreference;
|
|
939
1029
|
constructor(opts) {
|
|
@@ -957,6 +1047,23 @@ var CacheFirstLoop = class {
|
|
|
957
1047
|
this.stream = this.branchEnabled ? false : this._streamPreference;
|
|
958
1048
|
const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
|
|
959
1049
|
this.repair = new ToolCallRepair({ allowedToolNames: allowedNames });
|
|
1050
|
+
this.sessionName = opts.session ?? null;
|
|
1051
|
+
if (this.sessionName) {
|
|
1052
|
+
const prior = loadSessionMessages(this.sessionName);
|
|
1053
|
+
for (const msg of prior) this.log.append(msg);
|
|
1054
|
+
this.resumedMessageCount = prior.length;
|
|
1055
|
+
} else {
|
|
1056
|
+
this.resumedMessageCount = 0;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
appendAndPersist(message) {
|
|
1060
|
+
this.log.append(message);
|
|
1061
|
+
if (this.sessionName) {
|
|
1062
|
+
try {
|
|
1063
|
+
appendSessionMessage(this.sessionName, message);
|
|
1064
|
+
} catch {
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
960
1067
|
}
|
|
961
1068
|
/**
|
|
962
1069
|
* Reconfigure model/harvest/branch/stream mid-session. The loop's log,
|
|
@@ -1144,7 +1251,7 @@ var CacheFirstLoop = class {
|
|
|
1144
1251
|
}
|
|
1145
1252
|
const turnStats = this.stats.record(this._turn, this.model, usage ?? new Usage());
|
|
1146
1253
|
if (pendingUser !== null) {
|
|
1147
|
-
this.
|
|
1254
|
+
this.appendAndPersist({ role: "user", content: pendingUser });
|
|
1148
1255
|
pendingUser = null;
|
|
1149
1256
|
}
|
|
1150
1257
|
this.scratch.reasoning = reasoningContent || null;
|
|
@@ -1153,7 +1260,7 @@ var CacheFirstLoop = class {
|
|
|
1153
1260
|
toolCalls,
|
|
1154
1261
|
reasoningContent || null
|
|
1155
1262
|
);
|
|
1156
|
-
this.
|
|
1263
|
+
this.appendAndPersist(this.assistantMessage(assistantContent, repairedCalls));
|
|
1157
1264
|
yield {
|
|
1158
1265
|
turn: this._turn,
|
|
1159
1266
|
role: "assistant_final",
|
|
@@ -1171,13 +1278,19 @@ var CacheFirstLoop = class {
|
|
|
1171
1278
|
const name = call.function?.name ?? "";
|
|
1172
1279
|
const args = call.function?.arguments ?? "{}";
|
|
1173
1280
|
const result = await this.tools.dispatch(name, args);
|
|
1174
|
-
this.
|
|
1281
|
+
this.appendAndPersist({
|
|
1175
1282
|
role: "tool",
|
|
1176
1283
|
tool_call_id: call.id ?? "",
|
|
1177
1284
|
name,
|
|
1178
1285
|
content: result
|
|
1179
1286
|
});
|
|
1180
|
-
yield {
|
|
1287
|
+
yield {
|
|
1288
|
+
turn: this._turn,
|
|
1289
|
+
role: "tool",
|
|
1290
|
+
content: result,
|
|
1291
|
+
toolName: name,
|
|
1292
|
+
toolArgs: args
|
|
1293
|
+
};
|
|
1181
1294
|
}
|
|
1182
1295
|
}
|
|
1183
1296
|
yield { turn: this._turn, role: "done", content: "[max_tool_iters reached]" };
|
|
@@ -1207,12 +1320,12 @@ function summarizeBranch(chosen, samples) {
|
|
|
1207
1320
|
}
|
|
1208
1321
|
|
|
1209
1322
|
// src/env.ts
|
|
1210
|
-
import { readFileSync } from "fs";
|
|
1323
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1211
1324
|
import { resolve } from "path";
|
|
1212
1325
|
function loadDotenv(path = ".env") {
|
|
1213
1326
|
let raw;
|
|
1214
1327
|
try {
|
|
1215
|
-
raw =
|
|
1328
|
+
raw = readFileSync2(resolve(process.cwd(), path), "utf8");
|
|
1216
1329
|
} catch {
|
|
1217
1330
|
return;
|
|
1218
1331
|
}
|
|
@@ -1230,16 +1343,439 @@ function loadDotenv(path = ".env") {
|
|
|
1230
1343
|
}
|
|
1231
1344
|
}
|
|
1232
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
|
+
|
|
1233
1769
|
// src/config.ts
|
|
1234
|
-
import { chmodSync, mkdirSync, readFileSync as
|
|
1235
|
-
import { homedir } from "os";
|
|
1236
|
-
import { dirname, join } from "path";
|
|
1770
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
1771
|
+
import { homedir as homedir2 } from "os";
|
|
1772
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1237
1773
|
function defaultConfigPath() {
|
|
1238
|
-
return
|
|
1774
|
+
return join2(homedir2(), ".reasonix", "config.json");
|
|
1239
1775
|
}
|
|
1240
1776
|
function readConfig(path = defaultConfigPath()) {
|
|
1241
1777
|
try {
|
|
1242
|
-
const raw =
|
|
1778
|
+
const raw = readFileSync4(path, "utf8");
|
|
1243
1779
|
const parsed = JSON.parse(raw);
|
|
1244
1780
|
if (parsed && typeof parsed === "object") return parsed;
|
|
1245
1781
|
} catch {
|
|
@@ -1247,10 +1783,10 @@ function readConfig(path = defaultConfigPath()) {
|
|
|
1247
1783
|
return {};
|
|
1248
1784
|
}
|
|
1249
1785
|
function writeConfig(cfg, path = defaultConfigPath()) {
|
|
1250
|
-
|
|
1786
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1251
1787
|
writeFileSync(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
1252
1788
|
try {
|
|
1253
|
-
|
|
1789
|
+
chmodSync2(path, 384);
|
|
1254
1790
|
} catch {
|
|
1255
1791
|
}
|
|
1256
1792
|
}
|
|
@@ -1274,7 +1810,7 @@ function redactKey(key) {
|
|
|
1274
1810
|
}
|
|
1275
1811
|
|
|
1276
1812
|
// src/index.ts
|
|
1277
|
-
var VERSION = "0.0
|
|
1813
|
+
var VERSION = "0.2.0";
|
|
1278
1814
|
export {
|
|
1279
1815
|
AppendOnlyLog,
|
|
1280
1816
|
CacheFirstLoop,
|
|
@@ -1289,25 +1825,44 @@ export {
|
|
|
1289
1825
|
VolatileScratch,
|
|
1290
1826
|
aggregateBranchUsage,
|
|
1291
1827
|
analyzeSchema,
|
|
1828
|
+
appendSessionMessage,
|
|
1292
1829
|
claudeEquivalentCost,
|
|
1830
|
+
computeReplayStats,
|
|
1293
1831
|
costUsd,
|
|
1294
1832
|
defaultConfigPath,
|
|
1295
1833
|
defaultSelector,
|
|
1834
|
+
deleteSession,
|
|
1835
|
+
diffTranscripts,
|
|
1296
1836
|
emptyPlanState,
|
|
1297
1837
|
fetchWithRetry,
|
|
1298
1838
|
flattenSchema,
|
|
1299
1839
|
harvest,
|
|
1300
1840
|
isPlanStateEmpty,
|
|
1301
1841
|
isPlausibleKey,
|
|
1842
|
+
listSessions,
|
|
1302
1843
|
loadApiKey,
|
|
1303
1844
|
loadDotenv,
|
|
1845
|
+
loadSessionMessages,
|
|
1304
1846
|
nestArguments,
|
|
1847
|
+
openTranscriptFile,
|
|
1848
|
+
parseTranscript,
|
|
1305
1849
|
readConfig,
|
|
1850
|
+
readTranscript,
|
|
1851
|
+
recordFromLoopEvent,
|
|
1306
1852
|
redactKey,
|
|
1853
|
+
renderMarkdown as renderDiffMarkdown,
|
|
1854
|
+
renderSummaryTable as renderDiffSummary,
|
|
1307
1855
|
repairTruncatedJson,
|
|
1856
|
+
replayFromFile,
|
|
1308
1857
|
runBranches,
|
|
1858
|
+
sanitizeName as sanitizeSessionName,
|
|
1309
1859
|
saveApiKey,
|
|
1310
1860
|
scavengeToolCalls,
|
|
1311
|
-
|
|
1861
|
+
sessionPath,
|
|
1862
|
+
sessionsDir,
|
|
1863
|
+
similarity,
|
|
1864
|
+
writeConfig,
|
|
1865
|
+
writeMeta,
|
|
1866
|
+
writeRecord
|
|
1312
1867
|
};
|
|
1313
1868
|
//# sourceMappingURL=index.js.map
|