reasonix 0.3.0-alpha.6 → 0.3.1
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/dist/cli/index.js +126 -12
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +92 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -684,6 +684,16 @@ var AppendOnlyLog = class {
|
|
|
684
684
|
extend(messages) {
|
|
685
685
|
for (const m of messages) this.append(m);
|
|
686
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* Bulk-replace entries. Intentionally named to be hard to reach for —
|
|
689
|
+
* this is the one mutation path that breaks the log's append-only
|
|
690
|
+
* spirit, reserved for compaction flows (`/compact`) and recovery
|
|
691
|
+
* where the caller has consciously decided to drop old history. Any
|
|
692
|
+
* other use is almost certainly wrong; append() is what you want.
|
|
693
|
+
*/
|
|
694
|
+
compactInPlace(replacement) {
|
|
695
|
+
this._entries = [...replacement];
|
|
696
|
+
}
|
|
687
697
|
get entries() {
|
|
688
698
|
return this._entries;
|
|
689
699
|
}
|
|
@@ -962,7 +972,8 @@ import {
|
|
|
962
972
|
readFileSync as readFileSync2,
|
|
963
973
|
readdirSync,
|
|
964
974
|
statSync,
|
|
965
|
-
unlinkSync
|
|
975
|
+
unlinkSync,
|
|
976
|
+
writeFileSync as writeFileSync2
|
|
966
977
|
} from "fs";
|
|
967
978
|
import { homedir as homedir2 } from "os";
|
|
968
979
|
import { dirname as dirname2, join as join2 } from "path";
|
|
@@ -1031,6 +1042,17 @@ function deleteSession(name) {
|
|
|
1031
1042
|
return false;
|
|
1032
1043
|
}
|
|
1033
1044
|
}
|
|
1045
|
+
function rewriteSession(name, messages) {
|
|
1046
|
+
const path = sessionPath(name);
|
|
1047
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1048
|
+
const body = messages.map((m) => JSON.stringify(m)).join("\n");
|
|
1049
|
+
writeFileSync2(path, body ? `${body}
|
|
1050
|
+
` : "", "utf8");
|
|
1051
|
+
try {
|
|
1052
|
+
chmodSync2(path, 384);
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1034
1056
|
function countLines(path) {
|
|
1035
1057
|
try {
|
|
1036
1058
|
const raw = readFileSync2(path, "utf8");
|
|
@@ -1046,6 +1068,11 @@ var DEEPSEEK_PRICING = {
|
|
|
1046
1068
|
"deepseek-reasoner": { inputCacheHit: 0.14, inputCacheMiss: 0.55, output: 2.19 }
|
|
1047
1069
|
};
|
|
1048
1070
|
var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
|
|
1071
|
+
var DEEPSEEK_CONTEXT_TOKENS = {
|
|
1072
|
+
"deepseek-chat": 131072,
|
|
1073
|
+
"deepseek-reasoner": 131072
|
|
1074
|
+
};
|
|
1075
|
+
var DEFAULT_CONTEXT_TOKENS = 131072;
|
|
1049
1076
|
function costUsd(model, usage) {
|
|
1050
1077
|
const p = DEEPSEEK_PRICING[model];
|
|
1051
1078
|
if (!p) return 0;
|
|
@@ -1089,12 +1116,14 @@ var SessionStats = class {
|
|
|
1089
1116
|
return denom > 0 ? hit / denom : 0;
|
|
1090
1117
|
}
|
|
1091
1118
|
summary() {
|
|
1119
|
+
const last = this.turns[this.turns.length - 1];
|
|
1092
1120
|
return {
|
|
1093
1121
|
turns: this.turns.length,
|
|
1094
1122
|
totalCostUsd: round(this.totalCost, 6),
|
|
1095
1123
|
claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
|
|
1096
1124
|
savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
|
|
1097
|
-
cacheHitRatio: round(this.aggregateCacheHitRatio, 4)
|
|
1125
|
+
cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
|
|
1126
|
+
lastPromptTokens: last?.usage.promptTokens ?? 0
|
|
1098
1127
|
};
|
|
1099
1128
|
}
|
|
1100
1129
|
};
|
|
@@ -1131,7 +1160,7 @@ var CacheFirstLoop = class {
|
|
|
1131
1160
|
this.prefix = opts.prefix;
|
|
1132
1161
|
this.tools = opts.tools ?? new ToolRegistry();
|
|
1133
1162
|
this.model = opts.model ?? "deepseek-chat";
|
|
1134
|
-
this.maxToolIters = opts.maxToolIters ??
|
|
1163
|
+
this.maxToolIters = opts.maxToolIters ?? 24;
|
|
1135
1164
|
if (typeof opts.branch === "number") {
|
|
1136
1165
|
this.branchOptions = { budget: opts.branch };
|
|
1137
1166
|
} else if (opts.branch && typeof opts.branch === "object") {
|
|
@@ -1166,6 +1195,33 @@ var CacheFirstLoop = class {
|
|
|
1166
1195
|
this.resumedMessageCount = 0;
|
|
1167
1196
|
}
|
|
1168
1197
|
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Shrink the log by re-truncating oversized tool results to a tighter
|
|
1200
|
+
* cap, and persist the result back to disk so the next launch doesn't
|
|
1201
|
+
* re-inherit a fat session file. Returns a summary the TUI can
|
|
1202
|
+
* display.
|
|
1203
|
+
*
|
|
1204
|
+
* Only tool-role messages are touched (same rationale as
|
|
1205
|
+
* {@link healLoadedMessages}). User and assistant messages carry
|
|
1206
|
+
* authored intent we can't mechanically shrink without losing
|
|
1207
|
+
* meaning.
|
|
1208
|
+
*/
|
|
1209
|
+
compact(tightCapChars = 4e3) {
|
|
1210
|
+
const before = this.log.toMessages();
|
|
1211
|
+
const { messages, healedCount, healedFrom } = healLoadedMessages(before, tightCapChars);
|
|
1212
|
+
const afterBytes = messages.filter((m) => m.role === "tool").reduce((s, m) => s + (typeof m.content === "string" ? m.content.length : 0), 0);
|
|
1213
|
+
const charsSaved = healedFrom - afterBytes;
|
|
1214
|
+
if (healedCount > 0) {
|
|
1215
|
+
this.log.compactInPlace(messages);
|
|
1216
|
+
if (this.sessionName) {
|
|
1217
|
+
try {
|
|
1218
|
+
rewriteSession(this.sessionName, messages);
|
|
1219
|
+
} catch {
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return { healedCount, charsSaved };
|
|
1224
|
+
}
|
|
1169
1225
|
appendAndPersist(message) {
|
|
1170
1226
|
this.log.append(message);
|
|
1171
1227
|
if (this.sessionName) {
|
|
@@ -1403,7 +1459,38 @@ var CacheFirstLoop = class {
|
|
|
1403
1459
|
};
|
|
1404
1460
|
}
|
|
1405
1461
|
}
|
|
1406
|
-
yield
|
|
1462
|
+
yield* this.forceSummaryAfterIterLimit();
|
|
1463
|
+
}
|
|
1464
|
+
async *forceSummaryAfterIterLimit() {
|
|
1465
|
+
try {
|
|
1466
|
+
const messages = this.buildMessages(null);
|
|
1467
|
+
const resp = await this.client.chat({
|
|
1468
|
+
model: this.model,
|
|
1469
|
+
messages
|
|
1470
|
+
// no tools → model is forced to answer in text
|
|
1471
|
+
});
|
|
1472
|
+
const summary = resp.content?.trim() || "(model returned no text; try a narrower question or raise --max-tool-iters)";
|
|
1473
|
+
const annotated = `[tool-call budget (${this.maxToolIters}) reached \u2014 forcing summary from what I found]
|
|
1474
|
+
|
|
1475
|
+
${summary}`;
|
|
1476
|
+
const summaryStats = this.stats.record(this._turn, this.model, resp.usage ?? new Usage());
|
|
1477
|
+
this.appendAndPersist({ role: "assistant", content: summary });
|
|
1478
|
+
yield {
|
|
1479
|
+
turn: this._turn,
|
|
1480
|
+
role: "assistant_final",
|
|
1481
|
+
content: annotated,
|
|
1482
|
+
stats: summaryStats
|
|
1483
|
+
};
|
|
1484
|
+
yield { turn: this._turn, role: "done", content: summary };
|
|
1485
|
+
} catch (err) {
|
|
1486
|
+
yield {
|
|
1487
|
+
turn: this._turn,
|
|
1488
|
+
role: "error",
|
|
1489
|
+
content: "",
|
|
1490
|
+
error: `tool-call budget (${this.maxToolIters}) reached and the fallback summary call failed: ${err.message}. Run /clear and retry with a narrower question, or pass --max-tool-iters higher.`
|
|
1491
|
+
};
|
|
1492
|
+
yield { turn: this._turn, role: "done", content: "" };
|
|
1493
|
+
}
|
|
1407
1494
|
}
|
|
1408
1495
|
async run(userInput, onEvent) {
|
|
1409
1496
|
let final = "";
|
|
@@ -1644,12 +1731,14 @@ function summarizeTurns(turns) {
|
|
|
1644
1731
|
}
|
|
1645
1732
|
const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
|
|
1646
1733
|
const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
|
|
1734
|
+
const lastTurn = turns[turns.length - 1];
|
|
1647
1735
|
return {
|
|
1648
1736
|
turns: turns.length,
|
|
1649
1737
|
totalCostUsd: round2(totalCost, 6),
|
|
1650
1738
|
claudeEquivalentUsd: round2(totalClaude, 6),
|
|
1651
1739
|
savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
|
|
1652
|
-
cacheHitRatio: round2(cacheHitRatio, 4)
|
|
1740
|
+
cacheHitRatio: round2(cacheHitRatio, 4),
|
|
1741
|
+
lastPromptTokens: lastTurn?.usage.promptTokens ?? 0
|
|
1653
1742
|
};
|
|
1654
1743
|
}
|
|
1655
1744
|
function round2(n, digits) {
|
|
@@ -2444,7 +2533,7 @@ function parseMcpSpec(input) {
|
|
|
2444
2533
|
}
|
|
2445
2534
|
|
|
2446
2535
|
// src/index.ts
|
|
2447
|
-
var VERSION = "0.3.
|
|
2536
|
+
var VERSION = "0.3.1";
|
|
2448
2537
|
|
|
2449
2538
|
// src/cli/commands/chat.tsx
|
|
2450
2539
|
import { render } from "ink";
|
|
@@ -2772,7 +2861,15 @@ function StatsPanel({
|
|
|
2772
2861
|
const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
|
|
2773
2862
|
const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
|
|
2774
2863
|
const branchOn = (branchBudget ?? 1) > 1;
|
|
2775
|
-
|
|
2864
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
2865
|
+
const ctxRatio = summary.lastPromptTokens / ctxMax;
|
|
2866
|
+
const ctxColor = ctxRatio >= 0.8 ? "red" : ctxRatio >= 0.5 ? "yellow" : void 0;
|
|
2867
|
+
return /* @__PURE__ */ React5.createElement(Box5, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React5.createElement(Box5, { justifyContent: "space-between" }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, model), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React5.createElement(Text5, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "cache hit "), /* @__PURE__ */ React5.createElement(Text5, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "cost "), /* @__PURE__ */ React5.createElement(Text5, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React5.createElement(Text5, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "saving "), /* @__PURE__ */ React5.createElement(Text5, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%")), summary.lastPromptTokens > 0 ? /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "ctx "), /* @__PURE__ */ React5.createElement(Text5, { color: ctxColor, bold: ctxColor !== void 0 }, formatTokens(summary.lastPromptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " (", (ctxRatio * 100).toFixed(0), "%)"), ctxRatio >= 0.8 ? /* @__PURE__ */ React5.createElement(Text5, { color: "red", bold: true }, " ", "\xB7 /compact") : null) : null));
|
|
2868
|
+
}
|
|
2869
|
+
function formatTokens(n) {
|
|
2870
|
+
if (n < 1e3) return String(n);
|
|
2871
|
+
const k = n / 1e3;
|
|
2872
|
+
return k >= 100 ? `${k.toFixed(0)}k` : `${k.toFixed(1)}k`;
|
|
2776
2873
|
}
|
|
2777
2874
|
|
|
2778
2875
|
// src/cli/ui/slash.ts
|
|
@@ -2803,6 +2900,7 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
2803
2900
|
" /branch <N|off> run N parallel samples (N>=2), pick most confident",
|
|
2804
2901
|
" /mcp list MCP servers + tools attached to this session",
|
|
2805
2902
|
" /setup (exit + reconfigure) \u2192 run `reasonix setup`",
|
|
2903
|
+
" /compact [cap] shrink large tool results in history (default 4k/result)",
|
|
2806
2904
|
" /sessions list saved sessions (current is marked with \u25B8)",
|
|
2807
2905
|
" /forget delete the current session from disk",
|
|
2808
2906
|
" /clear clear displayed history (log + session kept)",
|
|
@@ -2844,6 +2942,19 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
2844
2942
|
return {
|
|
2845
2943
|
info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
|
|
2846
2944
|
};
|
|
2945
|
+
case "compact": {
|
|
2946
|
+
const tight = Number.parseInt(args[0] ?? "", 10);
|
|
2947
|
+
const cap = Number.isFinite(tight) && tight >= 500 ? tight : 4e3;
|
|
2948
|
+
const { healedCount, charsSaved } = loop.compact(cap);
|
|
2949
|
+
if (healedCount === 0) {
|
|
2950
|
+
return {
|
|
2951
|
+
info: `\u25B8 nothing to compact \u2014 no tool result in history exceeds ${cap.toLocaleString()} chars.`
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
return {
|
|
2955
|
+
info: `\u25B8 compacted ${healedCount} tool result(s), saved ${charsSaved.toLocaleString()} chars (~${Math.round(charsSaved / 4).toLocaleString()} tokens). Session file rewritten.`
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2847
2958
|
case "sessions": {
|
|
2848
2959
|
const items = listSessions();
|
|
2849
2960
|
if (items.length === 0) {
|
|
@@ -2953,7 +3064,8 @@ function App({
|
|
|
2953
3064
|
totalCostUsd: 0,
|
|
2954
3065
|
claudeEquivalentUsd: 0,
|
|
2955
3066
|
savingsVsClaudePct: 0,
|
|
2956
|
-
cacheHitRatio: 0
|
|
3067
|
+
cacheHitRatio: 0,
|
|
3068
|
+
lastPromptTokens: 0
|
|
2957
3069
|
});
|
|
2958
3070
|
const transcriptRef = useRef(null);
|
|
2959
3071
|
if (transcript && !transcriptRef.current) {
|
|
@@ -3155,7 +3267,7 @@ function App({
|
|
|
3155
3267
|
), /* @__PURE__ */ React6.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React6.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React6.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React6.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React6.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React6.createElement(CommandStrip, null));
|
|
3156
3268
|
}
|
|
3157
3269
|
function CommandStrip() {
|
|
3158
|
-
return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"));
|
|
3270
|
+
return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /compact \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"));
|
|
3159
3271
|
}
|
|
3160
3272
|
function describeRepair(repair) {
|
|
3161
3273
|
const parts = [];
|
|
@@ -3286,7 +3398,7 @@ async function chatCommand(opts) {
|
|
|
3286
3398
|
}
|
|
3287
3399
|
|
|
3288
3400
|
// src/cli/commands/diff.ts
|
|
3289
|
-
import { writeFileSync as
|
|
3401
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
3290
3402
|
import { basename } from "path";
|
|
3291
3403
|
import { render as render2 } from "ink";
|
|
3292
3404
|
import React11 from "react";
|
|
@@ -3432,7 +3544,7 @@ async function diffCommand(opts) {
|
|
|
3432
3544
|
if (wantMarkdown) {
|
|
3433
3545
|
console.log(renderSummaryTable(report));
|
|
3434
3546
|
const md = renderMarkdown(report);
|
|
3435
|
-
|
|
3547
|
+
writeFileSync3(opts.mdPath, md, "utf8");
|
|
3436
3548
|
console.log(`
|
|
3437
3549
|
markdown report written to ${opts.mdPath}`);
|
|
3438
3550
|
return;
|
|
@@ -3548,7 +3660,9 @@ function ReplayApp({ meta, pages }) {
|
|
|
3548
3660
|
totalCostUsd: cumStats.totalCostUsd,
|
|
3549
3661
|
claudeEquivalentUsd: cumStats.claudeEquivalentUsd,
|
|
3550
3662
|
savingsVsClaudePct: cumStats.savingsVsClaudePct,
|
|
3551
|
-
cacheHitRatio: cumStats.cacheHitRatio
|
|
3663
|
+
cacheHitRatio: cumStats.cacheHitRatio,
|
|
3664
|
+
// Replay is read-only — no live last-turn prompt tokens to show.
|
|
3665
|
+
lastPromptTokens: 0
|
|
3552
3666
|
};
|
|
3553
3667
|
const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
|
|
3554
3668
|
const currentPage = pages[idx];
|