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/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[];
@@ -514,6 +529,21 @@ declare class CacheFirstLoop {
514
529
  private _turn;
515
530
  private _streamPreference;
516
531
  constructor(opts: CacheFirstLoopOptions);
532
+ /**
533
+ * Shrink the log by re-truncating oversized tool results to a tighter
534
+ * cap, and persist the result back to disk so the next launch doesn't
535
+ * re-inherit a fat session file. Returns a summary the TUI can
536
+ * display.
537
+ *
538
+ * Only tool-role messages are touched (same rationale as
539
+ * {@link healLoadedMessages}). User and assistant messages carry
540
+ * authored intent we can't mechanically shrink without losing
541
+ * meaning.
542
+ */
543
+ compact(tightCapChars?: number): {
544
+ healedCount: number;
545
+ charsSaved: number;
546
+ };
517
547
  private appendAndPersist;
518
548
  /**
519
549
  * Reconfigure model/harvest/branch/stream mid-session. The loop's log,
@@ -524,6 +554,7 @@ declare class CacheFirstLoop {
524
554
  configure(opts: ReconfigurableOptions): void;
525
555
  private buildMessages;
526
556
  step(userInput: string): AsyncGenerator<LoopEvent>;
557
+ private forceSummaryAfterIterLimit;
527
558
  run(userInput: string, onEvent?: (ev: LoopEvent) => void): Promise<string>;
528
559
  private assistantMessage;
529
560
  }
@@ -1246,6 +1277,6 @@ declare function redactKey(key: string): string;
1246
1277
 
1247
1278
  /** Reasonix — DeepSeek-native agent framework. Library entry point. */
1248
1279
 
1249
- declare const VERSION = "0.3.0-alpha.6";
1280
+ declare const VERSION = "0.3.1";
1250
1281
 
1251
1282
  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
  };
@@ -1083,7 +1107,7 @@ var CacheFirstLoop = class {
1083
1107
  this.prefix = opts.prefix;
1084
1108
  this.tools = opts.tools ?? new ToolRegistry();
1085
1109
  this.model = opts.model ?? "deepseek-chat";
1086
- this.maxToolIters = opts.maxToolIters ?? 8;
1110
+ this.maxToolIters = opts.maxToolIters ?? 24;
1087
1111
  if (typeof opts.branch === "number") {
1088
1112
  this.branchOptions = { budget: opts.branch };
1089
1113
  } else if (opts.branch && typeof opts.branch === "object") {
@@ -1118,6 +1142,33 @@ var CacheFirstLoop = class {
1118
1142
  this.resumedMessageCount = 0;
1119
1143
  }
1120
1144
  }
1145
+ /**
1146
+ * Shrink the log by re-truncating oversized tool results to a tighter
1147
+ * cap, and persist the result back to disk so the next launch doesn't
1148
+ * re-inherit a fat session file. Returns a summary the TUI can
1149
+ * display.
1150
+ *
1151
+ * Only tool-role messages are touched (same rationale as
1152
+ * {@link healLoadedMessages}). User and assistant messages carry
1153
+ * authored intent we can't mechanically shrink without losing
1154
+ * meaning.
1155
+ */
1156
+ compact(tightCapChars = 4e3) {
1157
+ const before = this.log.toMessages();
1158
+ const { messages, healedCount, healedFrom } = healLoadedMessages(before, tightCapChars);
1159
+ const afterBytes = messages.filter((m) => m.role === "tool").reduce((s, m) => s + (typeof m.content === "string" ? m.content.length : 0), 0);
1160
+ const charsSaved = healedFrom - afterBytes;
1161
+ if (healedCount > 0) {
1162
+ this.log.compactInPlace(messages);
1163
+ if (this.sessionName) {
1164
+ try {
1165
+ rewriteSession(this.sessionName, messages);
1166
+ } catch {
1167
+ }
1168
+ }
1169
+ }
1170
+ return { healedCount, charsSaved };
1171
+ }
1121
1172
  appendAndPersist(message) {
1122
1173
  this.log.append(message);
1123
1174
  if (this.sessionName) {
@@ -1355,7 +1406,38 @@ var CacheFirstLoop = class {
1355
1406
  };
1356
1407
  }
1357
1408
  }
1358
- yield { turn: this._turn, role: "done", content: "[max_tool_iters reached]" };
1409
+ yield* this.forceSummaryAfterIterLimit();
1410
+ }
1411
+ async *forceSummaryAfterIterLimit() {
1412
+ try {
1413
+ const messages = this.buildMessages(null);
1414
+ const resp = await this.client.chat({
1415
+ model: this.model,
1416
+ messages
1417
+ // no tools → model is forced to answer in text
1418
+ });
1419
+ const summary = resp.content?.trim() || "(model returned no text; try a narrower question or raise --max-tool-iters)";
1420
+ const annotated = `[tool-call budget (${this.maxToolIters}) reached \u2014 forcing summary from what I found]
1421
+
1422
+ ${summary}`;
1423
+ const summaryStats = this.stats.record(this._turn, this.model, resp.usage ?? new Usage());
1424
+ this.appendAndPersist({ role: "assistant", content: summary });
1425
+ yield {
1426
+ turn: this._turn,
1427
+ role: "assistant_final",
1428
+ content: annotated,
1429
+ stats: summaryStats
1430
+ };
1431
+ yield { turn: this._turn, role: "done", content: summary };
1432
+ } catch (err) {
1433
+ yield {
1434
+ turn: this._turn,
1435
+ role: "error",
1436
+ content: "",
1437
+ 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.`
1438
+ };
1439
+ yield { turn: this._turn, role: "done", content: "" };
1440
+ }
1359
1441
  }
1360
1442
  async run(userInput, onEvent) {
1361
1443
  let final = "";
@@ -1578,12 +1660,14 @@ function summarizeTurns(turns) {
1578
1660
  }
1579
1661
  const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
1580
1662
  const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
1663
+ const lastTurn = turns[turns.length - 1];
1581
1664
  return {
1582
1665
  turns: turns.length,
1583
1666
  totalCostUsd: round2(totalCost, 6),
1584
1667
  claudeEquivalentUsd: round2(totalClaude, 6),
1585
1668
  savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
1586
- cacheHitRatio: round2(cacheHitRatio, 4)
1669
+ cacheHitRatio: round2(cacheHitRatio, 4),
1670
+ lastPromptTokens: lastTurn?.usage.promptTokens ?? 0
1587
1671
  };
1588
1672
  }
1589
1673
  function round2(n, digits) {
@@ -2365,7 +2449,7 @@ function parseMcpSpec(input) {
2365
2449
  }
2366
2450
 
2367
2451
  // src/config.ts
2368
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
2452
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
2369
2453
  import { homedir as homedir2 } from "os";
2370
2454
  import { dirname as dirname2, join as join2 } from "path";
2371
2455
  function defaultConfigPath() {
@@ -2382,7 +2466,7 @@ function readConfig(path = defaultConfigPath()) {
2382
2466
  }
2383
2467
  function writeConfig(cfg, path = defaultConfigPath()) {
2384
2468
  mkdirSync2(dirname2(path), { recursive: true });
2385
- writeFileSync(path, JSON.stringify(cfg, null, 2), "utf8");
2469
+ writeFileSync2(path, JSON.stringify(cfg, null, 2), "utf8");
2386
2470
  try {
2387
2471
  chmodSync2(path, 384);
2388
2472
  } catch {
@@ -2408,7 +2492,7 @@ function redactKey(key) {
2408
2492
  }
2409
2493
 
2410
2494
  // src/index.ts
2411
- var VERSION = "0.3.0-alpha.6";
2495
+ var VERSION = "0.3.1";
2412
2496
  export {
2413
2497
  AppendOnlyLog,
2414
2498
  CacheFirstLoop,