reasonix 0.3.0-alpha.6 → 0.3.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 +84 -79
- package/dist/cli/index.js +193 -24
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +47 -2
- package/dist/index.js +130 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -244,6 +244,14 @@ declare class AppendOnlyLog {
|
|
|
244
244
|
private _entries;
|
|
245
245
|
append(message: ChatMessage): void;
|
|
246
246
|
extend(messages: ChatMessage[]): void;
|
|
247
|
+
/**
|
|
248
|
+
* Bulk-replace entries. Intentionally named to be hard to reach for —
|
|
249
|
+
* this is the one mutation path that breaks the log's append-only
|
|
250
|
+
* spirit, reserved for compaction flows (`/compact`) and recovery
|
|
251
|
+
* where the caller has consciously decided to drop old history. Any
|
|
252
|
+
* other use is almost certainly wrong; append() is what you want.
|
|
253
|
+
*/
|
|
254
|
+
compactInPlace(replacement: ChatMessage[]): void;
|
|
247
255
|
get entries(): readonly ChatMessage[];
|
|
248
256
|
toMessages(): ChatMessage[];
|
|
249
257
|
get length(): number;
|
|
@@ -379,6 +387,13 @@ interface SessionSummary {
|
|
|
379
387
|
claudeEquivalentUsd: number;
|
|
380
388
|
savingsVsClaudePct: number;
|
|
381
389
|
cacheHitRatio: number;
|
|
390
|
+
/**
|
|
391
|
+
* Most recent turn's prompt-token count. Used by the TUI's context
|
|
392
|
+
* gauge: we can't know the next call's cost without making it, but
|
|
393
|
+
* the last turn's prompt tokens is the floor (next call is last
|
|
394
|
+
* prompt + user delta + any new tool outputs).
|
|
395
|
+
*/
|
|
396
|
+
lastPromptTokens: number;
|
|
382
397
|
}
|
|
383
398
|
declare class SessionStats {
|
|
384
399
|
readonly turns: TurnStats[];
|
|
@@ -418,7 +433,7 @@ declare class ToolRegistry {
|
|
|
418
433
|
dispatch(name: string, argumentsRaw: string | Record<string, unknown>): Promise<string>;
|
|
419
434
|
}
|
|
420
435
|
|
|
421
|
-
type EventRole = "assistant_delta" | "assistant_final" | "tool" | "done" | "error" | "branch_start" | "branch_progress" | "branch_done";
|
|
436
|
+
type EventRole = "assistant_delta" | "assistant_final" | "tool" | "done" | "error" | "warning" | "branch_start" | "branch_progress" | "branch_done";
|
|
422
437
|
interface BranchSummary {
|
|
423
438
|
budget: number;
|
|
424
439
|
chosenIndex: number;
|
|
@@ -513,7 +528,28 @@ declare class CacheFirstLoop {
|
|
|
513
528
|
readonly resumedMessageCount: number;
|
|
514
529
|
private _turn;
|
|
515
530
|
private _streamPreference;
|
|
531
|
+
/**
|
|
532
|
+
* Set by {@link abort} to short-circuit the tool-call loop after the
|
|
533
|
+
* current iteration. Reset at the start of each `step()` so an Esc
|
|
534
|
+
* during one turn doesn't poison the next.
|
|
535
|
+
*/
|
|
536
|
+
private _aborted;
|
|
516
537
|
constructor(opts: CacheFirstLoopOptions);
|
|
538
|
+
/**
|
|
539
|
+
* Shrink the log by re-truncating oversized tool results to a tighter
|
|
540
|
+
* cap, and persist the result back to disk so the next launch doesn't
|
|
541
|
+
* re-inherit a fat session file. Returns a summary the TUI can
|
|
542
|
+
* display.
|
|
543
|
+
*
|
|
544
|
+
* Only tool-role messages are touched (same rationale as
|
|
545
|
+
* {@link healLoadedMessages}). User and assistant messages carry
|
|
546
|
+
* authored intent we can't mechanically shrink without losing
|
|
547
|
+
* meaning.
|
|
548
|
+
*/
|
|
549
|
+
compact(tightCapChars?: number): {
|
|
550
|
+
healedCount: number;
|
|
551
|
+
charsSaved: number;
|
|
552
|
+
};
|
|
517
553
|
private appendAndPersist;
|
|
518
554
|
/**
|
|
519
555
|
* Reconfigure model/harvest/branch/stream mid-session. The loop's log,
|
|
@@ -523,7 +559,16 @@ declare class CacheFirstLoop {
|
|
|
523
559
|
*/
|
|
524
560
|
configure(opts: ReconfigurableOptions): void;
|
|
525
561
|
private buildMessages;
|
|
562
|
+
/**
|
|
563
|
+
* Signal the currently-running {@link step} that the user wants to
|
|
564
|
+
* stop exploring. Takes effect at the next iteration boundary — if a
|
|
565
|
+
* tool call is mid-flight it will be allowed to finish, then the
|
|
566
|
+
* loop diverts to the forced-summary path so the user gets an
|
|
567
|
+
* answer instead of a cliff. Called by the TUI on Esc.
|
|
568
|
+
*/
|
|
569
|
+
abort(): void;
|
|
526
570
|
step(userInput: string): AsyncGenerator<LoopEvent>;
|
|
571
|
+
private forceSummaryAfterIterLimit;
|
|
527
572
|
run(userInput: string, onEvent?: (ev: LoopEvent) => void): Promise<string>;
|
|
528
573
|
private assistantMessage;
|
|
529
574
|
}
|
|
@@ -1246,6 +1291,6 @@ declare function redactKey(key: string): string;
|
|
|
1246
1291
|
|
|
1247
1292
|
/** Reasonix — DeepSeek-native agent framework. Library entry point. */
|
|
1248
1293
|
|
|
1249
|
-
declare const VERSION = "0.3.
|
|
1294
|
+
declare const VERSION = "0.3.2";
|
|
1250
1295
|
|
|
1251
1296
|
export { AppendOnlyLog, type BranchOptions, type BranchProgress, type BranchResult, type BranchSample, type BranchSelector, type BranchSummary, type BridgeOptions, type BridgeResult, CacheFirstLoop, type CacheFirstLoopOptions, type CallToolResult, type ChatMessage, type ChatResponse, DEFAULT_MAX_RESULT_CHARS, DeepSeekClient, type DeepSeekClientOptions, type RenderOptions as DiffRenderOptions, type DiffReport, type DiffSide, type EventRole, type FlattenDecision, type FlattenOptions, type HarvestOptions, ImmutablePrefix, type ImmutablePrefixOptions, type InitializeResult, type JSONSchema, type JsonRpcMessage, type JsonRpcRequest, type JsonRpcResponse, type ListToolsResult, type LoopEvent, MCP_PROTOCOL_VERSION, McpClient, type McpClientOptions, type McpContentBlock, type McpSpec, type McpTool, type McpToolSchema, type McpTransport, type ReadTranscriptResult, type ReasonixConfig, type ReconfigurableOptions, type RepairReport, type ReplayStats, type RetryInfo, type RetryOptions, type Role, type ScavengeOptions, type ScavengeResult, type SessionInfo, SessionStats, type SessionSummary, type SseMcpSpec, SseTransport, type SseTransportOptions, type StdioMcpSpec, StdioTransport, type StdioTransportOptions, StormBreaker, type StreamChunk, type ToolCall, ToolCallRepair, type ToolCallRepairOptions, type ToolDefinition, type ToolFunctionSpec, ToolRegistry, type ToolSpec, type TranscriptMeta, type TranscriptRecord, type TruncationRepairResult, type TurnPair, type TurnStats, type TypedPlanState, Usage, VERSION, VolatileScratch, aggregateBranchUsage, analyzeSchema, appendSessionMessage, bridgeMcpTools, claudeEquivalentCost, computeReplayStats, costUsd, defaultConfigPath, defaultSelector, deleteSession, diffTranscripts, emptyPlanState, fetchWithRetry, flattenMcpResult, flattenSchema, formatLoopError, harvest, healLoadedMessages, isJsonRpcError, isPlanStateEmpty, isPlausibleKey, listSessions, loadApiKey, loadDotenv, loadSessionMessages, nestArguments, openTranscriptFile, parseMcpSpec, parseTranscript, readConfig, readTranscript, recordFromLoopEvent, redactKey, renderMarkdown as renderDiffMarkdown, renderSummaryTable as renderDiffSummary, repairTruncatedJson, replayFromFile, runBranches, sanitizeName as sanitizeSessionName, saveApiKey, scavengeToolCalls, sessionPath, sessionsDir, similarity, truncateForModel, writeConfig, writeMeta, writeRecord };
|
package/dist/index.js
CHANGED
|
@@ -636,6 +636,16 @@ var AppendOnlyLog = class {
|
|
|
636
636
|
extend(messages) {
|
|
637
637
|
for (const m of messages) this.append(m);
|
|
638
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* Bulk-replace entries. Intentionally named to be hard to reach for —
|
|
641
|
+
* this is the one mutation path that breaks the log's append-only
|
|
642
|
+
* spirit, reserved for compaction flows (`/compact`) and recovery
|
|
643
|
+
* where the caller has consciously decided to drop old history. Any
|
|
644
|
+
* other use is almost certainly wrong; append() is what you want.
|
|
645
|
+
*/
|
|
646
|
+
compactInPlace(replacement) {
|
|
647
|
+
this._entries = [...replacement];
|
|
648
|
+
}
|
|
639
649
|
get entries() {
|
|
640
650
|
return this._entries;
|
|
641
651
|
}
|
|
@@ -914,7 +924,8 @@ import {
|
|
|
914
924
|
readFileSync,
|
|
915
925
|
readdirSync,
|
|
916
926
|
statSync,
|
|
917
|
-
unlinkSync
|
|
927
|
+
unlinkSync,
|
|
928
|
+
writeFileSync
|
|
918
929
|
} from "fs";
|
|
919
930
|
import { homedir } from "os";
|
|
920
931
|
import { dirname, join } from "path";
|
|
@@ -983,6 +994,17 @@ function deleteSession(name) {
|
|
|
983
994
|
return false;
|
|
984
995
|
}
|
|
985
996
|
}
|
|
997
|
+
function rewriteSession(name, messages) {
|
|
998
|
+
const path = sessionPath(name);
|
|
999
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1000
|
+
const body = messages.map((m) => JSON.stringify(m)).join("\n");
|
|
1001
|
+
writeFileSync(path, body ? `${body}
|
|
1002
|
+
` : "", "utf8");
|
|
1003
|
+
try {
|
|
1004
|
+
chmodSync(path, 384);
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
986
1008
|
function countLines(path) {
|
|
987
1009
|
try {
|
|
988
1010
|
const raw = readFileSync(path, "utf8");
|
|
@@ -1041,12 +1063,14 @@ var SessionStats = class {
|
|
|
1041
1063
|
return denom > 0 ? hit / denom : 0;
|
|
1042
1064
|
}
|
|
1043
1065
|
summary() {
|
|
1066
|
+
const last = this.turns[this.turns.length - 1];
|
|
1044
1067
|
return {
|
|
1045
1068
|
turns: this.turns.length,
|
|
1046
1069
|
totalCostUsd: round(this.totalCost, 6),
|
|
1047
1070
|
claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
|
|
1048
1071
|
savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
|
|
1049
|
-
cacheHitRatio: round(this.aggregateCacheHitRatio, 4)
|
|
1072
|
+
cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
|
|
1073
|
+
lastPromptTokens: last?.usage.promptTokens ?? 0
|
|
1050
1074
|
};
|
|
1051
1075
|
}
|
|
1052
1076
|
};
|
|
@@ -1078,12 +1102,18 @@ var CacheFirstLoop = class {
|
|
|
1078
1102
|
resumedMessageCount;
|
|
1079
1103
|
_turn = 0;
|
|
1080
1104
|
_streamPreference;
|
|
1105
|
+
/**
|
|
1106
|
+
* Set by {@link abort} to short-circuit the tool-call loop after the
|
|
1107
|
+
* current iteration. Reset at the start of each `step()` so an Esc
|
|
1108
|
+
* during one turn doesn't poison the next.
|
|
1109
|
+
*/
|
|
1110
|
+
_aborted = false;
|
|
1081
1111
|
constructor(opts) {
|
|
1082
1112
|
this.client = opts.client;
|
|
1083
1113
|
this.prefix = opts.prefix;
|
|
1084
1114
|
this.tools = opts.tools ?? new ToolRegistry();
|
|
1085
1115
|
this.model = opts.model ?? "deepseek-chat";
|
|
1086
|
-
this.maxToolIters = opts.maxToolIters ??
|
|
1116
|
+
this.maxToolIters = opts.maxToolIters ?? 24;
|
|
1087
1117
|
if (typeof opts.branch === "number") {
|
|
1088
1118
|
this.branchOptions = { budget: opts.branch };
|
|
1089
1119
|
} else if (opts.branch && typeof opts.branch === "object") {
|
|
@@ -1118,6 +1148,33 @@ var CacheFirstLoop = class {
|
|
|
1118
1148
|
this.resumedMessageCount = 0;
|
|
1119
1149
|
}
|
|
1120
1150
|
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Shrink the log by re-truncating oversized tool results to a tighter
|
|
1153
|
+
* cap, and persist the result back to disk so the next launch doesn't
|
|
1154
|
+
* re-inherit a fat session file. Returns a summary the TUI can
|
|
1155
|
+
* display.
|
|
1156
|
+
*
|
|
1157
|
+
* Only tool-role messages are touched (same rationale as
|
|
1158
|
+
* {@link healLoadedMessages}). User and assistant messages carry
|
|
1159
|
+
* authored intent we can't mechanically shrink without losing
|
|
1160
|
+
* meaning.
|
|
1161
|
+
*/
|
|
1162
|
+
compact(tightCapChars = 4e3) {
|
|
1163
|
+
const before = this.log.toMessages();
|
|
1164
|
+
const { messages, healedCount, healedFrom } = healLoadedMessages(before, tightCapChars);
|
|
1165
|
+
const afterBytes = messages.filter((m) => m.role === "tool").reduce((s, m) => s + (typeof m.content === "string" ? m.content.length : 0), 0);
|
|
1166
|
+
const charsSaved = healedFrom - afterBytes;
|
|
1167
|
+
if (healedCount > 0) {
|
|
1168
|
+
this.log.compactInPlace(messages);
|
|
1169
|
+
if (this.sessionName) {
|
|
1170
|
+
try {
|
|
1171
|
+
rewriteSession(this.sessionName, messages);
|
|
1172
|
+
} catch {
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return { healedCount, charsSaved };
|
|
1177
|
+
}
|
|
1121
1178
|
appendAndPersist(message) {
|
|
1122
1179
|
this.log.append(message);
|
|
1123
1180
|
if (this.sessionName) {
|
|
@@ -1162,12 +1219,42 @@ var CacheFirstLoop = class {
|
|
|
1162
1219
|
if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
|
|
1163
1220
|
return msgs;
|
|
1164
1221
|
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Signal the currently-running {@link step} that the user wants to
|
|
1224
|
+
* stop exploring. Takes effect at the next iteration boundary — if a
|
|
1225
|
+
* tool call is mid-flight it will be allowed to finish, then the
|
|
1226
|
+
* loop diverts to the forced-summary path so the user gets an
|
|
1227
|
+
* answer instead of a cliff. Called by the TUI on Esc.
|
|
1228
|
+
*/
|
|
1229
|
+
abort() {
|
|
1230
|
+
this._aborted = true;
|
|
1231
|
+
}
|
|
1165
1232
|
async *step(userInput) {
|
|
1166
1233
|
this._turn++;
|
|
1167
1234
|
this.scratch.reset();
|
|
1235
|
+
this._aborted = false;
|
|
1168
1236
|
let pendingUser = userInput;
|
|
1169
1237
|
const toolSpecs = this.prefix.tools();
|
|
1238
|
+
const warnAt = Math.max(1, Math.floor(this.maxToolIters * 0.7));
|
|
1239
|
+
let warnedForIterBudget = false;
|
|
1170
1240
|
for (let iter = 0; iter < this.maxToolIters; iter++) {
|
|
1241
|
+
if (this._aborted) {
|
|
1242
|
+
yield {
|
|
1243
|
+
turn: this._turn,
|
|
1244
|
+
role: "warning",
|
|
1245
|
+
content: `aborted at iter ${iter}/${this.maxToolIters} \u2014 forcing summary from what was gathered`
|
|
1246
|
+
};
|
|
1247
|
+
yield* this.forceSummaryAfterIterLimit({ reason: "aborted" });
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (!warnedForIterBudget && iter >= warnAt) {
|
|
1251
|
+
warnedForIterBudget = true;
|
|
1252
|
+
yield {
|
|
1253
|
+
turn: this._turn,
|
|
1254
|
+
role: "warning",
|
|
1255
|
+
content: `${iter}/${this.maxToolIters} tool calls used \u2014 approaching budget. Press Esc to force a summary now.`
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1171
1258
|
const messages = this.buildMessages(pendingUser);
|
|
1172
1259
|
let assistantContent = "";
|
|
1173
1260
|
let reasoningContent = "";
|
|
@@ -1355,7 +1442,40 @@ var CacheFirstLoop = class {
|
|
|
1355
1442
|
};
|
|
1356
1443
|
}
|
|
1357
1444
|
}
|
|
1358
|
-
yield
|
|
1445
|
+
yield* this.forceSummaryAfterIterLimit({ reason: "budget" });
|
|
1446
|
+
}
|
|
1447
|
+
async *forceSummaryAfterIterLimit(opts = { reason: "budget" }) {
|
|
1448
|
+
try {
|
|
1449
|
+
const messages = this.buildMessages(null);
|
|
1450
|
+
const resp = await this.client.chat({
|
|
1451
|
+
model: this.model,
|
|
1452
|
+
messages
|
|
1453
|
+
// no tools → model is forced to answer in text
|
|
1454
|
+
});
|
|
1455
|
+
const summary = resp.content?.trim() || "(model returned no text; try a narrower question or raise --max-tool-iters)";
|
|
1456
|
+
const reasonPrefix = opts.reason === "aborted" ? "[aborted by user (Esc) \u2014 summarizing what I found so far]" : `[tool-call budget (${this.maxToolIters}) reached \u2014 forcing summary from what I found]`;
|
|
1457
|
+
const annotated = `${reasonPrefix}
|
|
1458
|
+
|
|
1459
|
+
${summary}`;
|
|
1460
|
+
const summaryStats = this.stats.record(this._turn, this.model, resp.usage ?? new Usage());
|
|
1461
|
+
this.appendAndPersist({ role: "assistant", content: summary });
|
|
1462
|
+
yield {
|
|
1463
|
+
turn: this._turn,
|
|
1464
|
+
role: "assistant_final",
|
|
1465
|
+
content: annotated,
|
|
1466
|
+
stats: summaryStats
|
|
1467
|
+
};
|
|
1468
|
+
yield { turn: this._turn, role: "done", content: summary };
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
const label = opts.reason === "aborted" ? "aborted by user" : `tool-call budget (${this.maxToolIters}) reached`;
|
|
1471
|
+
yield {
|
|
1472
|
+
turn: this._turn,
|
|
1473
|
+
role: "error",
|
|
1474
|
+
content: "",
|
|
1475
|
+
error: `${label} and the fallback summary call failed: ${err.message}. Run /clear and retry with a narrower question, or raise --max-tool-iters.`
|
|
1476
|
+
};
|
|
1477
|
+
yield { turn: this._turn, role: "done", content: "" };
|
|
1478
|
+
}
|
|
1359
1479
|
}
|
|
1360
1480
|
async run(userInput, onEvent) {
|
|
1361
1481
|
let final = "";
|
|
@@ -1578,12 +1698,14 @@ function summarizeTurns(turns) {
|
|
|
1578
1698
|
}
|
|
1579
1699
|
const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
|
|
1580
1700
|
const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
|
|
1701
|
+
const lastTurn = turns[turns.length - 1];
|
|
1581
1702
|
return {
|
|
1582
1703
|
turns: turns.length,
|
|
1583
1704
|
totalCostUsd: round2(totalCost, 6),
|
|
1584
1705
|
claudeEquivalentUsd: round2(totalClaude, 6),
|
|
1585
1706
|
savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
|
|
1586
|
-
cacheHitRatio: round2(cacheHitRatio, 4)
|
|
1707
|
+
cacheHitRatio: round2(cacheHitRatio, 4),
|
|
1708
|
+
lastPromptTokens: lastTurn?.usage.promptTokens ?? 0
|
|
1587
1709
|
};
|
|
1588
1710
|
}
|
|
1589
1711
|
function round2(n, digits) {
|
|
@@ -2365,7 +2487,7 @@ function parseMcpSpec(input) {
|
|
|
2365
2487
|
}
|
|
2366
2488
|
|
|
2367
2489
|
// src/config.ts
|
|
2368
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
2490
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
2369
2491
|
import { homedir as homedir2 } from "os";
|
|
2370
2492
|
import { dirname as dirname2, join as join2 } from "path";
|
|
2371
2493
|
function defaultConfigPath() {
|
|
@@ -2382,7 +2504,7 @@ function readConfig(path = defaultConfigPath()) {
|
|
|
2382
2504
|
}
|
|
2383
2505
|
function writeConfig(cfg, path = defaultConfigPath()) {
|
|
2384
2506
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
2385
|
-
|
|
2507
|
+
writeFileSync2(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
2386
2508
|
try {
|
|
2387
2509
|
chmodSync2(path, 384);
|
|
2388
2510
|
} catch {
|
|
@@ -2408,7 +2530,7 @@ function redactKey(key) {
|
|
|
2408
2530
|
}
|
|
2409
2531
|
|
|
2410
2532
|
// src/index.ts
|
|
2411
|
-
var VERSION = "0.3.
|
|
2533
|
+
var VERSION = "0.3.2";
|
|
2412
2534
|
export {
|
|
2413
2535
|
AppendOnlyLog,
|
|
2414
2536
|
CacheFirstLoop,
|