reasonix 0.11.2 → 0.12.6

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.js CHANGED
@@ -1358,25 +1358,32 @@ function coerceToToolCall(candidateJson, allowedNames) {
1358
1358
  var StormBreaker = class {
1359
1359
  windowSize;
1360
1360
  threshold;
1361
+ isMutating;
1361
1362
  recent = [];
1362
- constructor(windowSize = 6, threshold = 3) {
1363
+ constructor(windowSize = 6, threshold = 3, isMutating) {
1363
1364
  this.windowSize = windowSize;
1364
1365
  this.threshold = threshold;
1366
+ this.isMutating = isMutating;
1365
1367
  }
1366
1368
  inspect(call) {
1367
- const sig = signature(call);
1368
- if (!sig) return { suppress: false };
1369
- const count = this.recent.reduce(
1370
- (n, [name, args]) => name === sig[0] && args === sig[1] ? n + 1 : n,
1371
- 0
1372
- );
1369
+ const name = call.function?.name;
1370
+ if (!name) return { suppress: false };
1371
+ const args = call.function?.arguments ?? "";
1372
+ const mutating = this.isMutating ? this.isMutating(call) : false;
1373
+ const readOnly = !mutating;
1374
+ if (mutating) {
1375
+ for (let i = this.recent.length - 1; i >= 0; i--) {
1376
+ if (this.recent[i].readOnly) this.recent.splice(i, 1);
1377
+ }
1378
+ }
1379
+ const count = this.recent.reduce((n, e) => e.name === name && e.args === args ? n + 1 : n, 0);
1373
1380
  if (count >= this.threshold - 1) {
1374
1381
  return {
1375
1382
  suppress: true,
1376
- reason: `call-storm suppressed: ${sig[0]} called with identical args ${count + 1} times within window=${this.windowSize}`
1383
+ reason: `call-storm suppressed: ${name} called with identical args ${count + 1} times within window=${this.windowSize}`
1377
1384
  };
1378
1385
  }
1379
- this.recent.push(sig);
1386
+ this.recent.push({ name, args, readOnly });
1380
1387
  while (this.recent.length > this.windowSize) this.recent.shift();
1381
1388
  return { suppress: false };
1382
1389
  }
@@ -1384,11 +1391,6 @@ var StormBreaker = class {
1384
1391
  this.recent.length = 0;
1385
1392
  }
1386
1393
  };
1387
- function signature(call) {
1388
- const name = call.function?.name;
1389
- if (!name) return null;
1390
- return [name, call.function?.arguments ?? ""];
1391
- }
1392
1394
 
1393
1395
  // src/repair/truncation.ts
1394
1396
  function repairTruncatedJson(input) {
@@ -1466,7 +1468,7 @@ var ToolCallRepair = class {
1466
1468
  opts;
1467
1469
  constructor(opts) {
1468
1470
  this.opts = opts;
1469
- this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3);
1471
+ this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3, opts.isMutating);
1470
1472
  }
1471
1473
  /**
1472
1474
  * Drop the StormBreaker's sliding window of recent (name, args)
@@ -1490,13 +1492,13 @@ var ToolCallRepair = class {
1490
1492
  allowedNames: this.opts.allowedToolNames,
1491
1493
  maxCalls: this.opts.maxScavenge ?? 4
1492
1494
  });
1493
- const seenSignatures = new Set(declaredCalls.map(signature2));
1495
+ const seenSignatures = new Set(declaredCalls.map(signature));
1494
1496
  const merged = [...declaredCalls];
1495
1497
  for (const sc of scavenged.calls) {
1496
- if (!seenSignatures.has(signature2(sc))) {
1498
+ if (!seenSignatures.has(signature(sc))) {
1497
1499
  merged.push(sc);
1498
1500
  report.scavenged++;
1499
- seenSignatures.add(signature2(sc));
1501
+ seenSignatures.add(signature(sc));
1500
1502
  }
1501
1503
  }
1502
1504
  report.notes.push(...scavenged.notes);
@@ -1522,7 +1524,7 @@ var ToolCallRepair = class {
1522
1524
  return { calls: filtered, report };
1523
1525
  }
1524
1526
  };
1525
- function signature2(call) {
1527
+ function signature(call) {
1526
1528
  return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
1527
1529
  }
1528
1530
 
@@ -1661,6 +1663,12 @@ function outputCostUsd(model, usage) {
1661
1663
  if (!p) return 0;
1662
1664
  return usage.completionTokens * p.output / 1e6;
1663
1665
  }
1666
+ function cacheSavingsUsd(model, hitTokens) {
1667
+ if (hitTokens <= 0) return 0;
1668
+ const p = DEEPSEEK_PRICING[model];
1669
+ if (!p) return 0;
1670
+ return hitTokens * (p.inputCacheMiss - p.inputCacheHit) / 1e6;
1671
+ }
1664
1672
  function claudeEquivalentCost(usage) {
1665
1673
  return (usage.promptTokens * CLAUDE_SONNET_PRICING.input + usage.completionTokens * CLAUDE_SONNET_PRICING.output) / 1e6;
1666
1674
  }
@@ -1751,6 +1759,13 @@ var CacheFirstLoop = class {
1751
1759
  branchOptions;
1752
1760
  /** See ReconfigurableOptions — mutable so `/effort` can flip mid-session. */
1753
1761
  reasoningEffort;
1762
+ /**
1763
+ * Auto-escalation toggle. `true` lets the loop self-promote to pro
1764
+ * mid-turn (NEEDS_PRO marker / failure threshold); `false` keeps it
1765
+ * pinned to `model`. Mutable so the dashboard's preset switcher can
1766
+ * flip it live alongside `model`.
1767
+ */
1768
+ autoEscalate = true;
1754
1769
  sessionName;
1755
1770
  /**
1756
1771
  * Hook list, mutable so `/hooks reload` can swap it without
@@ -1815,6 +1830,7 @@ var CacheFirstLoop = class {
1815
1830
  this.tools = opts.tools ?? new ToolRegistry();
1816
1831
  this.model = opts.model ?? "deepseek-v4-flash";
1817
1832
  this.reasoningEffort = opts.reasoningEffort ?? "max";
1833
+ if (opts.autoEscalate !== void 0) this.autoEscalate = opts.autoEscalate;
1818
1834
  this.maxToolIters = opts.maxToolIters ?? 64;
1819
1835
  this.hooks = opts.hooks ?? [];
1820
1836
  this.hookCwd = opts.hookCwd ?? process.cwd();
@@ -1832,7 +1848,26 @@ var CacheFirstLoop = class {
1832
1848
  this._streamPreference = opts.stream ?? true;
1833
1849
  this.stream = this.branchEnabled ? false : this._streamPreference;
1834
1850
  const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
1835
- this.repair = new ToolCallRepair({ allowedToolNames: allowedNames });
1851
+ const registry = this.tools;
1852
+ const isMutating = (call) => {
1853
+ const name = call.function?.name;
1854
+ if (!name) return false;
1855
+ const def = registry.get(name);
1856
+ if (!def) return false;
1857
+ if (def.readOnlyCheck) {
1858
+ let args = {};
1859
+ try {
1860
+ args = JSON.parse(call.function?.arguments ?? "{}") ?? {};
1861
+ } catch {
1862
+ }
1863
+ try {
1864
+ if (def.readOnlyCheck(args)) return false;
1865
+ } catch {
1866
+ }
1867
+ }
1868
+ return def.readOnly !== true;
1869
+ };
1870
+ this.repair = new ToolCallRepair({ allowedToolNames: allowedNames, isMutating });
1836
1871
  this.sessionName = opts.session ?? null;
1837
1872
  if (this.sessionName) {
1838
1873
  const prior = loadSessionMessages(this.sessionName);
@@ -2013,6 +2048,7 @@ var CacheFirstLoop = class {
2013
2048
  if (opts.model !== void 0) this.model = opts.model;
2014
2049
  if (opts.stream !== void 0) this._streamPreference = opts.stream;
2015
2050
  if (opts.reasoningEffort !== void 0) this.reasoningEffort = opts.reasoningEffort;
2051
+ if (opts.autoEscalate !== void 0) this.autoEscalate = opts.autoEscalate;
2016
2052
  if (opts.branch !== void 0) {
2017
2053
  if (typeof opts.branch === "number") {
2018
2054
  this.branchOptions = { budget: opts.branch };
@@ -2128,7 +2164,7 @@ var CacheFirstLoop = class {
2128
2164
  if (repair.truncationsFixed > 0) bump("truncated", repair.truncationsFixed);
2129
2165
  if (repair.stormsBroken > 0) bump("storm-broken", repair.stormsBroken);
2130
2166
  }
2131
- if (bumped && !this._escalateThisTurn && this._turnFailureCount >= FAILURE_ESCALATION_THRESHOLD) {
2167
+ if (bumped && !this._escalateThisTurn && this.autoEscalate && this._turnFailureCount >= FAILURE_ESCALATION_THRESHOLD) {
2132
2168
  this._escalateThisTurn = true;
2133
2169
  return true;
2134
2170
  }
@@ -2373,7 +2409,7 @@ var CacheFirstLoop = class {
2373
2409
  const callBuf = /* @__PURE__ */ new Map();
2374
2410
  const readyIndices = /* @__PURE__ */ new Set();
2375
2411
  const callModel = this.modelForCurrentCall();
2376
- const bufferForEscalation = callModel !== ESCALATION_MODEL;
2412
+ const bufferForEscalation = this.autoEscalate && callModel !== ESCALATION_MODEL;
2377
2413
  let escalationBuf = "";
2378
2414
  let escalationBufFlushed = false;
2379
2415
  for await (const chunk of this.client.stream({
@@ -2485,7 +2521,7 @@ var CacheFirstLoop = class {
2485
2521
  };
2486
2522
  return;
2487
2523
  }
2488
- if (this.modelForCurrentCall() !== ESCALATION_MODEL && this.isEscalationRequest(assistantContent)) {
2524
+ if (this.autoEscalate && this.modelForCurrentCall() !== ESCALATION_MODEL && this.isEscalationRequest(assistantContent)) {
2489
2525
  const { reason } = this.parseEscalationMarker(assistantContent);
2490
2526
  this._escalateThisTurn = true;
2491
2527
  const reasonSuffix = reason ? ` \u2014 ${reason}` : "";
@@ -7325,6 +7361,159 @@ var SseTransport = class {
7325
7361
  }
7326
7362
  };
7327
7363
 
7364
+ // src/mcp/streamable-http.ts
7365
+ import { createParser as createParser3 } from "eventsource-parser";
7366
+ var SESSION_HEADER = "mcp-session-id";
7367
+ var StreamableHttpTransport = class {
7368
+ url;
7369
+ extraHeaders;
7370
+ queue = [];
7371
+ waiters = [];
7372
+ controller = new AbortController();
7373
+ /** Session id minted by server on (typically) the initialize response. */
7374
+ sessionId = null;
7375
+ closed = false;
7376
+ /** Background SSE read-loops kicked off by send(); awaited on close(). */
7377
+ streams = /* @__PURE__ */ new Set();
7378
+ constructor(opts) {
7379
+ this.url = opts.url;
7380
+ this.extraHeaders = opts.headers ?? {};
7381
+ }
7382
+ async send(message) {
7383
+ if (this.closed) throw new Error("MCP Streamable HTTP transport is closed");
7384
+ const headers = {
7385
+ "content-type": "application/json",
7386
+ // Both accepted — server picks. application/json first signals a
7387
+ // mild preference for the simpler shape when the response is a
7388
+ // single message.
7389
+ accept: "application/json, text/event-stream",
7390
+ ...this.extraHeaders
7391
+ };
7392
+ if (this.sessionId !== null) headers["mcp-session-id"] = this.sessionId;
7393
+ let res;
7394
+ try {
7395
+ res = await fetch(this.url, {
7396
+ method: "POST",
7397
+ headers,
7398
+ body: JSON.stringify(message),
7399
+ signal: this.controller.signal
7400
+ });
7401
+ } catch (err) {
7402
+ throw new Error(`MCP Streamable HTTP POST ${this.url} failed: ${err.message}`);
7403
+ }
7404
+ const serverSessionId = res.headers.get(SESSION_HEADER);
7405
+ if (serverSessionId && this.sessionId === null) {
7406
+ this.sessionId = serverSessionId;
7407
+ }
7408
+ if (res.status === 404 && this.sessionId !== null) {
7409
+ await res.body?.cancel().catch(() => void 0);
7410
+ throw new Error(
7411
+ `MCP Streamable HTTP session expired (server returned 404 with Mcp-Session-Id "${this.sessionId}"). Reinitialize the client.`
7412
+ );
7413
+ }
7414
+ if (!res.ok) {
7415
+ const body = await res.text().catch(() => "");
7416
+ throw new Error(
7417
+ `MCP Streamable HTTP POST ${this.url} \u2192 ${res.status} ${res.statusText}${body ? `: ${body}` : ""}`
7418
+ );
7419
+ }
7420
+ if (res.status === 202) {
7421
+ await res.body?.cancel().catch(() => void 0);
7422
+ return;
7423
+ }
7424
+ const ct = (res.headers.get("content-type") ?? "").toLowerCase();
7425
+ if (ct.includes("application/json")) {
7426
+ let parsed;
7427
+ try {
7428
+ parsed = await res.json();
7429
+ } catch (err) {
7430
+ throw new Error(`MCP Streamable HTTP body wasn't valid JSON: ${err.message}`);
7431
+ }
7432
+ if (Array.isArray(parsed)) {
7433
+ for (const item of parsed) this.pushMessage(item);
7434
+ } else {
7435
+ this.pushMessage(parsed);
7436
+ }
7437
+ return;
7438
+ }
7439
+ if (ct.includes("text/event-stream")) {
7440
+ if (!res.body) {
7441
+ throw new Error("MCP Streamable HTTP SSE response had no body");
7442
+ }
7443
+ const stream = this.consumeStream(res.body);
7444
+ this.streams.add(stream);
7445
+ stream.finally(() => this.streams.delete(stream));
7446
+ return;
7447
+ }
7448
+ await res.body?.cancel().catch(() => void 0);
7449
+ }
7450
+ async *messages() {
7451
+ while (true) {
7452
+ if (this.queue.length > 0) {
7453
+ yield this.queue.shift();
7454
+ continue;
7455
+ }
7456
+ if (this.closed) return;
7457
+ const next = await new Promise((resolve9) => {
7458
+ this.waiters.push(resolve9);
7459
+ });
7460
+ if (next === null) return;
7461
+ yield next;
7462
+ }
7463
+ }
7464
+ async close() {
7465
+ if (this.closed) return;
7466
+ this.closed = true;
7467
+ while (this.waiters.length > 0) this.waiters.shift()(null);
7468
+ try {
7469
+ this.controller.abort();
7470
+ } catch {
7471
+ }
7472
+ await Promise.allSettled(Array.from(this.streams));
7473
+ }
7474
+ /** Visible for tests — confirm session header round-trip. */
7475
+ getSessionId() {
7476
+ return this.sessionId;
7477
+ }
7478
+ // ---------- internals ----------
7479
+ async consumeStream(body) {
7480
+ const parser = createParser3({
7481
+ onEvent: (ev) => {
7482
+ const type = ev.event ?? "message";
7483
+ if (type !== "message") return;
7484
+ try {
7485
+ const parsed = JSON.parse(ev.data);
7486
+ this.pushMessage(parsed);
7487
+ } catch {
7488
+ }
7489
+ }
7490
+ });
7491
+ const decoder = new TextDecoder();
7492
+ try {
7493
+ for await (const chunk of body) {
7494
+ if (this.closed) break;
7495
+ parser.feed(decoder.decode(chunk, { stream: true }));
7496
+ }
7497
+ } catch (err) {
7498
+ if (!this.closed) {
7499
+ this.pushMessage({
7500
+ jsonrpc: "2.0",
7501
+ id: null,
7502
+ error: {
7503
+ code: -32e3,
7504
+ message: `Streamable HTTP stream error: ${err.message}`
7505
+ }
7506
+ });
7507
+ }
7508
+ }
7509
+ }
7510
+ pushMessage(msg) {
7511
+ const waiter = this.waiters.shift();
7512
+ if (waiter) waiter(msg);
7513
+ else this.queue.push(msg);
7514
+ }
7515
+ };
7516
+
7328
7517
  // src/mcp/shell-split.ts
7329
7518
  function shellSplit(input) {
7330
7519
  const tokens = [];
@@ -7377,6 +7566,7 @@ function shellSplit(input) {
7377
7566
  // src/mcp/spec.ts
7378
7567
  var NAME_PREFIX = /^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$/;
7379
7568
  var HTTP_URL = /^https?:\/\//i;
7569
+ var STREAMABLE_PREFIX = /^streamable\+(https?:\/\/.+)$/i;
7380
7570
  function parseMcpSpec(input) {
7381
7571
  const trimmed = input.trim();
7382
7572
  if (!trimmed) {
@@ -7388,6 +7578,10 @@ function parseMcpSpec(input) {
7388
7578
  if (!body) {
7389
7579
  throw new Error(`MCP spec has name but no command: ${input}`);
7390
7580
  }
7581
+ const streamMatch = STREAMABLE_PREFIX.exec(body);
7582
+ if (streamMatch) {
7583
+ return { transport: "streamable-http", name, url: streamMatch[1] };
7584
+ }
7391
7585
  if (HTTP_URL.test(body)) {
7392
7586
  return { transport: "sse", name, url: body };
7393
7587
  }
@@ -8000,7 +8194,8 @@ function emptyBucket(label, since) {
8000
8194
  cacheHitTokens: 0,
8001
8195
  cacheMissTokens: 0,
8002
8196
  costUsd: 0,
8003
- claudeEquivUsd: 0
8197
+ claudeEquivUsd: 0,
8198
+ cacheSavingsUsd: 0
8004
8199
  };
8005
8200
  }
8006
8201
  function addToBucket(b, r) {
@@ -8011,6 +8206,7 @@ function addToBucket(b, r) {
8011
8206
  b.cacheMissTokens += r.cacheMissTokens;
8012
8207
  b.costUsd += r.costUsd;
8013
8208
  b.claudeEquivUsd += r.claudeEquivUsd;
8209
+ b.cacheSavingsUsd += cacheSavingsUsd(r.model, r.cacheHitTokens);
8014
8210
  }
8015
8211
  function aggregateUsage(records, opts = {}) {
8016
8212
  const now = opts.now ?? Date.now();
@@ -8112,6 +8308,7 @@ export {
8112
8308
  SseTransport,
8113
8309
  StdioTransport,
8114
8310
  StormBreaker,
8311
+ StreamableHttpTransport,
8115
8312
  ToolCallRepair,
8116
8313
  ToolRegistry,
8117
8314
  USER_MEMORY_DIR,