reasonix 0.25.1 → 0.26.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/dist/cli/index.js +1835 -1682
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +124 -9
- package/dist/index.js +651 -499
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
package/dist/index.js
CHANGED
|
@@ -451,6 +451,54 @@ function resolveTemperatures(budget, custom) {
|
|
|
451
451
|
return out;
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
+
// src/core/pause-gate.ts
|
|
455
|
+
var PauseGate = class {
|
|
456
|
+
_nextId = 0;
|
|
457
|
+
_pending = /* @__PURE__ */ new Map();
|
|
458
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
459
|
+
/** Block until the user responds. Takes a named options object so the
|
|
460
|
+
* kind and payload fields don't get confused at the call site. */
|
|
461
|
+
ask(opts) {
|
|
462
|
+
const { kind, payload } = opts;
|
|
463
|
+
if (this._listeners.size === 0) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`${kind}: no confirmation listener registered \u2014 cannot prompt the user. This tool can only be used inside an interactive Reasonix session.`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return new Promise((resolve10) => {
|
|
469
|
+
const id = this._nextId++;
|
|
470
|
+
const request = { id, kind, payload };
|
|
471
|
+
this._pending.set(id, { resolve: resolve10, request });
|
|
472
|
+
for (const fn of this._listeners) {
|
|
473
|
+
try {
|
|
474
|
+
fn(request);
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
/** Resolve a pending request. Called by the App's modal callback. */
|
|
481
|
+
resolve(id, data) {
|
|
482
|
+
const p = this._pending.get(id);
|
|
483
|
+
if (!p) return;
|
|
484
|
+
this._pending.delete(id);
|
|
485
|
+
p.resolve(data);
|
|
486
|
+
}
|
|
487
|
+
/** Subscribe to new pause requests. Returns an unsubscribe function. */
|
|
488
|
+
on(fn) {
|
|
489
|
+
this._listeners.add(fn);
|
|
490
|
+
return () => {
|
|
491
|
+
this._listeners.delete(fn);
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/** Current pending request, if any (polling fallback). */
|
|
495
|
+
get current() {
|
|
496
|
+
for (const [, p] of this._pending) return p.request;
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
var pauseGate = new PauseGate();
|
|
501
|
+
|
|
454
502
|
// src/hooks.ts
|
|
455
503
|
import { spawn } from "child_process";
|
|
456
504
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -989,7 +1037,10 @@ var ToolRegistry = class {
|
|
|
989
1037
|
}
|
|
990
1038
|
}
|
|
991
1039
|
try {
|
|
992
|
-
const result = await tool.fn(args, {
|
|
1040
|
+
const result = await tool.fn(args, {
|
|
1041
|
+
signal: opts.signal,
|
|
1042
|
+
confirmationGate: opts.confirmationGate
|
|
1043
|
+
});
|
|
993
1044
|
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
994
1045
|
let clipped = str;
|
|
995
1046
|
if (opts.maxResultTokens !== void 0) {
|
|
@@ -1193,98 +1244,6 @@ function blockToString(block) {
|
|
|
1193
1244
|
return `[unknown block: ${JSON.stringify(block)}]`;
|
|
1194
1245
|
}
|
|
1195
1246
|
|
|
1196
|
-
// src/memory/runtime.ts
|
|
1197
|
-
import { createHash } from "crypto";
|
|
1198
|
-
var ImmutablePrefix = class {
|
|
1199
|
-
system;
|
|
1200
|
-
/** Each `addTool` costs one cache-miss turn — DeepSeek's prefix cache is keyed by full tool list. */
|
|
1201
|
-
_toolSpecs;
|
|
1202
|
-
fewShots;
|
|
1203
|
-
/** Invalidated only via `addTool`; bypassing it leaves cache stale → fingerprint diverges from sent prefix. */
|
|
1204
|
-
_fingerprintCache = null;
|
|
1205
|
-
constructor(opts) {
|
|
1206
|
-
this.system = opts.system;
|
|
1207
|
-
this._toolSpecs = [...opts.toolSpecs ?? []];
|
|
1208
|
-
this.fewShots = Object.freeze([...opts.fewShots ?? []]);
|
|
1209
|
-
}
|
|
1210
|
-
get toolSpecs() {
|
|
1211
|
-
return this._toolSpecs;
|
|
1212
|
-
}
|
|
1213
|
-
toMessages() {
|
|
1214
|
-
return [{ role: "system", content: this.system }, ...this.fewShots.map((m) => ({ ...m }))];
|
|
1215
|
-
}
|
|
1216
|
-
tools() {
|
|
1217
|
-
return this._toolSpecs.map((t) => structuredClone(t));
|
|
1218
|
-
}
|
|
1219
|
-
addTool(spec) {
|
|
1220
|
-
const name = spec.function?.name;
|
|
1221
|
-
if (!name) return false;
|
|
1222
|
-
if (this._toolSpecs.some((t) => t.function?.name === name)) return false;
|
|
1223
|
-
this._toolSpecs.push(spec);
|
|
1224
|
-
this._fingerprintCache = null;
|
|
1225
|
-
return true;
|
|
1226
|
-
}
|
|
1227
|
-
get fingerprint() {
|
|
1228
|
-
if (this._fingerprintCache !== null) return this._fingerprintCache;
|
|
1229
|
-
this._fingerprintCache = this.computeFingerprint();
|
|
1230
|
-
return this._fingerprintCache;
|
|
1231
|
-
}
|
|
1232
|
-
/** Dev/test only — throws on cache drift, which always means a non-`addTool` mutation slipped in. */
|
|
1233
|
-
verifyFingerprint() {
|
|
1234
|
-
const fresh = this.computeFingerprint();
|
|
1235
|
-
if (this._fingerprintCache !== null && this._fingerprintCache !== fresh) {
|
|
1236
|
-
throw new Error(
|
|
1237
|
-
`ImmutablePrefix fingerprint drift: cached=${this._fingerprintCache}, fresh=${fresh}. A mutation path bypassed addTool's cache invalidation \u2014 DeepSeek will see prefix churn that the TUI / transcript log don't know about.`
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
this._fingerprintCache = fresh;
|
|
1241
|
-
return fresh;
|
|
1242
|
-
}
|
|
1243
|
-
computeFingerprint() {
|
|
1244
|
-
const blob = JSON.stringify({
|
|
1245
|
-
system: this.system,
|
|
1246
|
-
tools: this._toolSpecs,
|
|
1247
|
-
shots: this.fewShots
|
|
1248
|
-
});
|
|
1249
|
-
return createHash("sha256").update(blob).digest("hex").slice(0, 16);
|
|
1250
|
-
}
|
|
1251
|
-
};
|
|
1252
|
-
var AppendOnlyLog = class {
|
|
1253
|
-
_entries = [];
|
|
1254
|
-
append(message) {
|
|
1255
|
-
if (!message || typeof message !== "object" || !("role" in message)) {
|
|
1256
|
-
throw new Error(`invalid log entry: ${JSON.stringify(message)}`);
|
|
1257
|
-
}
|
|
1258
|
-
this._entries.push(message);
|
|
1259
|
-
}
|
|
1260
|
-
extend(messages) {
|
|
1261
|
-
for (const m of messages) this.append(m);
|
|
1262
|
-
}
|
|
1263
|
-
/** The one append-only-breaking path — reserved for `/compact` + recovery. Use `append()` otherwise. */
|
|
1264
|
-
compactInPlace(replacement) {
|
|
1265
|
-
this._entries = [...replacement];
|
|
1266
|
-
}
|
|
1267
|
-
get entries() {
|
|
1268
|
-
return this._entries;
|
|
1269
|
-
}
|
|
1270
|
-
toMessages() {
|
|
1271
|
-
return this._entries.map((e) => ({ ...e }));
|
|
1272
|
-
}
|
|
1273
|
-
get length() {
|
|
1274
|
-
return this._entries.length;
|
|
1275
|
-
}
|
|
1276
|
-
};
|
|
1277
|
-
var VolatileScratch = class {
|
|
1278
|
-
reasoning = null;
|
|
1279
|
-
planState = null;
|
|
1280
|
-
notes = [];
|
|
1281
|
-
reset() {
|
|
1282
|
-
this.reasoning = null;
|
|
1283
|
-
this.planState = null;
|
|
1284
|
-
this.notes = [];
|
|
1285
|
-
}
|
|
1286
|
-
};
|
|
1287
|
-
|
|
1288
1247
|
// src/memory/session.ts
|
|
1289
1248
|
import { execFileSync } from "child_process";
|
|
1290
1249
|
import {
|
|
@@ -1415,6 +1374,338 @@ function countLines(path2) {
|
|
|
1415
1374
|
}
|
|
1416
1375
|
}
|
|
1417
1376
|
|
|
1377
|
+
// src/telemetry/stats.ts
|
|
1378
|
+
var DEEPSEEK_PRICING = {
|
|
1379
|
+
"deepseek-v4-flash": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
|
|
1380
|
+
"deepseek-v4-pro": { inputCacheHit: 0.139, inputCacheMiss: 1.667, output: 3.333 },
|
|
1381
|
+
// Compat aliases — priced as v4-flash per the deprecation notice.
|
|
1382
|
+
"deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
|
|
1383
|
+
"deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 }
|
|
1384
|
+
};
|
|
1385
|
+
var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
|
|
1386
|
+
var DEEPSEEK_CONTEXT_TOKENS = {
|
|
1387
|
+
"deepseek-v4-flash": 1e6,
|
|
1388
|
+
"deepseek-v4-pro": 1e6,
|
|
1389
|
+
"deepseek-chat": 1e6,
|
|
1390
|
+
"deepseek-reasoner": 1e6
|
|
1391
|
+
};
|
|
1392
|
+
var DEFAULT_CONTEXT_TOKENS = 131072;
|
|
1393
|
+
function costUsd(model, usage) {
|
|
1394
|
+
const p = DEEPSEEK_PRICING[model];
|
|
1395
|
+
if (!p) return 0;
|
|
1396
|
+
return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss + usage.completionTokens * p.output) / 1e6;
|
|
1397
|
+
}
|
|
1398
|
+
function inputCostUsd(model, usage) {
|
|
1399
|
+
const p = DEEPSEEK_PRICING[model];
|
|
1400
|
+
if (!p) return 0;
|
|
1401
|
+
return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss) / 1e6;
|
|
1402
|
+
}
|
|
1403
|
+
function outputCostUsd(model, usage) {
|
|
1404
|
+
const p = DEEPSEEK_PRICING[model];
|
|
1405
|
+
if (!p) return 0;
|
|
1406
|
+
return usage.completionTokens * p.output / 1e6;
|
|
1407
|
+
}
|
|
1408
|
+
function cacheSavingsUsd(model, hitTokens) {
|
|
1409
|
+
if (hitTokens <= 0) return 0;
|
|
1410
|
+
const p = DEEPSEEK_PRICING[model];
|
|
1411
|
+
if (!p) return 0;
|
|
1412
|
+
return hitTokens * (p.inputCacheMiss - p.inputCacheHit) / 1e6;
|
|
1413
|
+
}
|
|
1414
|
+
function claudeEquivalentCost(usage) {
|
|
1415
|
+
return (usage.promptTokens * CLAUDE_SONNET_PRICING.input + usage.completionTokens * CLAUDE_SONNET_PRICING.output) / 1e6;
|
|
1416
|
+
}
|
|
1417
|
+
var SessionStats = class {
|
|
1418
|
+
turns = [];
|
|
1419
|
+
record(turn, model, usage) {
|
|
1420
|
+
const cost = costUsd(model, usage);
|
|
1421
|
+
const stats = {
|
|
1422
|
+
turn,
|
|
1423
|
+
model,
|
|
1424
|
+
usage,
|
|
1425
|
+
cost,
|
|
1426
|
+
cacheHitRatio: usage.cacheHitRatio
|
|
1427
|
+
};
|
|
1428
|
+
this.turns.push(stats);
|
|
1429
|
+
return stats;
|
|
1430
|
+
}
|
|
1431
|
+
get totalCost() {
|
|
1432
|
+
return this.turns.reduce((sum, t) => sum + t.cost, 0);
|
|
1433
|
+
}
|
|
1434
|
+
get totalClaudeEquivalent() {
|
|
1435
|
+
return this.turns.reduce((sum, t) => sum + claudeEquivalentCost(t.usage), 0);
|
|
1436
|
+
}
|
|
1437
|
+
get savingsVsClaude() {
|
|
1438
|
+
const c = this.totalClaudeEquivalent;
|
|
1439
|
+
return c > 0 ? 1 - this.totalCost / c : 0;
|
|
1440
|
+
}
|
|
1441
|
+
get totalInputCost() {
|
|
1442
|
+
return this.turns.reduce((sum, t) => sum + inputCostUsd(t.model, t.usage), 0);
|
|
1443
|
+
}
|
|
1444
|
+
get totalOutputCost() {
|
|
1445
|
+
return this.turns.reduce((sum, t) => sum + outputCostUsd(t.model, t.usage), 0);
|
|
1446
|
+
}
|
|
1447
|
+
get aggregateCacheHitRatio() {
|
|
1448
|
+
let hit = 0;
|
|
1449
|
+
let miss = 0;
|
|
1450
|
+
for (const t of this.turns) {
|
|
1451
|
+
hit += t.usage.promptCacheHitTokens;
|
|
1452
|
+
miss += t.usage.promptCacheMissTokens;
|
|
1453
|
+
}
|
|
1454
|
+
const denom = hit + miss;
|
|
1455
|
+
return denom > 0 ? hit / denom : 0;
|
|
1456
|
+
}
|
|
1457
|
+
summary() {
|
|
1458
|
+
const last = this.turns[this.turns.length - 1];
|
|
1459
|
+
return {
|
|
1460
|
+
turns: this.turns.length,
|
|
1461
|
+
totalCostUsd: round(this.totalCost, 6),
|
|
1462
|
+
totalInputCostUsd: round(this.totalInputCost, 6),
|
|
1463
|
+
totalOutputCostUsd: round(this.totalOutputCost, 6),
|
|
1464
|
+
claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
|
|
1465
|
+
savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
|
|
1466
|
+
cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
|
|
1467
|
+
lastPromptTokens: last?.usage.promptTokens ?? 0,
|
|
1468
|
+
lastTurnCostUsd: round(last?.cost ?? 0, 6)
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
function round(n, digits) {
|
|
1473
|
+
const f = 10 ** digits;
|
|
1474
|
+
return Math.round(n * f) / f;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/context-manager.ts
|
|
1478
|
+
var HISTORY_FOLD_THRESHOLD = 0.5;
|
|
1479
|
+
var HISTORY_FOLD_TAIL_FRACTION = 0.2;
|
|
1480
|
+
var HISTORY_FOLD_AGGRESSIVE_THRESHOLD = 0.7;
|
|
1481
|
+
var HISTORY_FOLD_AGGRESSIVE_TAIL_FRACTION = 0.1;
|
|
1482
|
+
var HISTORY_FOLD_MIN_SAVINGS_FRACTION = 0.3;
|
|
1483
|
+
var FORCE_SUMMARY_THRESHOLD = 0.8;
|
|
1484
|
+
var PREFLIGHT_EMERGENCY_THRESHOLD = 0.95;
|
|
1485
|
+
var HISTORY_FOLD_MARKER = "[CONVERSATION HISTORY SUMMARY \u2014 earlier turns folded for context efficiency]\n\n";
|
|
1486
|
+
var ContextManager = class {
|
|
1487
|
+
constructor(deps) {
|
|
1488
|
+
this.deps = deps;
|
|
1489
|
+
}
|
|
1490
|
+
deps;
|
|
1491
|
+
/** Decision after a turn's response — fold, exit with summary, or carry on. */
|
|
1492
|
+
decideAfterUsage(usage, model, alreadyFoldedThisTurn) {
|
|
1493
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
1494
|
+
if (!usage) return { kind: "none", promptTokens: 0, ctxMax, ratio: 0 };
|
|
1495
|
+
const ratio = usage.promptTokens / ctxMax;
|
|
1496
|
+
const base = { promptTokens: usage.promptTokens, ctxMax, ratio };
|
|
1497
|
+
if (ratio > FORCE_SUMMARY_THRESHOLD) {
|
|
1498
|
+
return { kind: "exit-with-summary", ...base };
|
|
1499
|
+
}
|
|
1500
|
+
if (alreadyFoldedThisTurn) return { kind: "none", ...base };
|
|
1501
|
+
if (ratio > HISTORY_FOLD_AGGRESSIVE_THRESHOLD) {
|
|
1502
|
+
return {
|
|
1503
|
+
kind: "fold",
|
|
1504
|
+
...base,
|
|
1505
|
+
tailBudget: Math.floor(ctxMax * HISTORY_FOLD_AGGRESSIVE_TAIL_FRACTION),
|
|
1506
|
+
aggressive: true
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
if (ratio > HISTORY_FOLD_THRESHOLD) {
|
|
1510
|
+
return {
|
|
1511
|
+
kind: "fold",
|
|
1512
|
+
...base,
|
|
1513
|
+
tailBudget: Math.floor(ctxMax * HISTORY_FOLD_TAIL_FRACTION),
|
|
1514
|
+
aggressive: false
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
return { kind: "none", ...base };
|
|
1518
|
+
}
|
|
1519
|
+
/** Local-side preflight before sending a request — catches oversized payloads early. */
|
|
1520
|
+
decidePreflight(messages, toolSpecs, model) {
|
|
1521
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
1522
|
+
const estimate = estimateRequestTokens(messages, toolSpecs ?? null);
|
|
1523
|
+
return {
|
|
1524
|
+
needsAction: estimate / ctxMax > PREFLIGHT_EMERGENCY_THRESHOLD,
|
|
1525
|
+
estimateTokens: estimate,
|
|
1526
|
+
ctxMax
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
/** Replace older turns with one summary message; keep tail within keepRecentTokens budget. */
|
|
1530
|
+
async fold(model, opts) {
|
|
1531
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
1532
|
+
const tailBudget = opts?.keepRecentTokens ?? Math.floor(ctxMax * HISTORY_FOLD_TAIL_FRACTION);
|
|
1533
|
+
const all = this.deps.log.toMessages();
|
|
1534
|
+
const noop = {
|
|
1535
|
+
folded: false,
|
|
1536
|
+
beforeMessages: all.length,
|
|
1537
|
+
afterMessages: all.length,
|
|
1538
|
+
summaryChars: 0
|
|
1539
|
+
};
|
|
1540
|
+
if (all.length === 0) return noop;
|
|
1541
|
+
const tokenCounts = all.map((m) => estimateConversationTokens([m]));
|
|
1542
|
+
const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);
|
|
1543
|
+
let cumTokens = 0;
|
|
1544
|
+
let boundary = all.length;
|
|
1545
|
+
for (let i = all.length - 1; i >= 0; i--) {
|
|
1546
|
+
if (cumTokens + tokenCounts[i] > tailBudget) break;
|
|
1547
|
+
cumTokens += tokenCounts[i];
|
|
1548
|
+
if (all[i].role === "user") boundary = i;
|
|
1549
|
+
}
|
|
1550
|
+
if (boundary <= 0) return noop;
|
|
1551
|
+
const head = all.slice(0, boundary);
|
|
1552
|
+
const tail = all.slice(boundary);
|
|
1553
|
+
const headTokens = totalTokens - cumTokens;
|
|
1554
|
+
if (headTokens < totalTokens * HISTORY_FOLD_MIN_SAVINGS_FRACTION) return noop;
|
|
1555
|
+
const summary = await this.summarizeForFold(head);
|
|
1556
|
+
if (!summary) return noop;
|
|
1557
|
+
const summaryMsg = {
|
|
1558
|
+
role: "assistant",
|
|
1559
|
+
content: HISTORY_FOLD_MARKER + summary
|
|
1560
|
+
};
|
|
1561
|
+
const replacement = [summaryMsg, ...tail];
|
|
1562
|
+
this.deps.log.compactInPlace(replacement);
|
|
1563
|
+
this.persistRewrite(replacement);
|
|
1564
|
+
return {
|
|
1565
|
+
folded: true,
|
|
1566
|
+
beforeMessages: all.length,
|
|
1567
|
+
afterMessages: replacement.length,
|
|
1568
|
+
summaryChars: summary.length
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
/** Drop a trailing in-flight assistant-with-tool_calls before a forced summary. Tail-only mutation; prefix cache safe. */
|
|
1572
|
+
trimTrailingToolCalls() {
|
|
1573
|
+
const tail = this.deps.log.entries[this.deps.log.entries.length - 1];
|
|
1574
|
+
if (!tail || tail.role !== "assistant" || !Array.isArray(tail.tool_calls) || tail.tool_calls.length === 0) {
|
|
1575
|
+
return false;
|
|
1576
|
+
}
|
|
1577
|
+
const kept = this.deps.log.entries.slice(0, -1);
|
|
1578
|
+
this.deps.log.compactInPlace([...kept]);
|
|
1579
|
+
this.persistRewrite([...kept]);
|
|
1580
|
+
return true;
|
|
1581
|
+
}
|
|
1582
|
+
async summarizeForFold(messagesToSummarize) {
|
|
1583
|
+
const summaryModel = "deepseek-v4-flash";
|
|
1584
|
+
const systemPrompt = "You compress conversation history for a coding agent. Output one prose recap that preserves: the user's overall goal, decisions and conclusions reached, files inspected or modified, important tool results still relevant to ongoing work, and any open todos. Skip turn-by-turn play-by-play. No tool calls, no markdown headings, no SEARCH/REPLACE blocks \u2014 plain prose only.";
|
|
1585
|
+
const healed = healLoadedMessages(messagesToSummarize, DEFAULT_MAX_RESULT_CHARS).messages;
|
|
1586
|
+
const messages = [
|
|
1587
|
+
{ role: "system", content: systemPrompt },
|
|
1588
|
+
...healed,
|
|
1589
|
+
{
|
|
1590
|
+
role: "user",
|
|
1591
|
+
content: "Summarize the conversation above as plain prose. This summary replaces the original turns to free context \u2014 make it self-contained."
|
|
1592
|
+
}
|
|
1593
|
+
];
|
|
1594
|
+
try {
|
|
1595
|
+
const resp = await this.deps.client.chat({
|
|
1596
|
+
model: summaryModel,
|
|
1597
|
+
messages,
|
|
1598
|
+
signal: this.deps.getAbortSignal(),
|
|
1599
|
+
thinking: thinkingModeForModel(summaryModel),
|
|
1600
|
+
reasoningEffort: "high"
|
|
1601
|
+
});
|
|
1602
|
+
this.deps.stats.record(this.deps.getCurrentTurn(), summaryModel, resp.usage ?? new Usage());
|
|
1603
|
+
return stripHallucinatedToolMarkup((resp.content ?? "").trim());
|
|
1604
|
+
} catch {
|
|
1605
|
+
return "";
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
persistRewrite(messages) {
|
|
1609
|
+
if (!this.deps.sessionName) return;
|
|
1610
|
+
try {
|
|
1611
|
+
rewriteSession(this.deps.sessionName, messages);
|
|
1612
|
+
} catch {
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
// src/memory/runtime.ts
|
|
1618
|
+
import { createHash } from "crypto";
|
|
1619
|
+
var ImmutablePrefix = class {
|
|
1620
|
+
system;
|
|
1621
|
+
/** Each `addTool` costs one cache-miss turn — DeepSeek's prefix cache is keyed by full tool list. */
|
|
1622
|
+
_toolSpecs;
|
|
1623
|
+
fewShots;
|
|
1624
|
+
/** Invalidated only via `addTool`; bypassing it leaves cache stale → fingerprint diverges from sent prefix. */
|
|
1625
|
+
_fingerprintCache = null;
|
|
1626
|
+
constructor(opts) {
|
|
1627
|
+
this.system = opts.system;
|
|
1628
|
+
this._toolSpecs = [...opts.toolSpecs ?? []];
|
|
1629
|
+
this.fewShots = Object.freeze([...opts.fewShots ?? []]);
|
|
1630
|
+
}
|
|
1631
|
+
get toolSpecs() {
|
|
1632
|
+
return this._toolSpecs;
|
|
1633
|
+
}
|
|
1634
|
+
toMessages() {
|
|
1635
|
+
return [{ role: "system", content: this.system }, ...this.fewShots.map((m) => ({ ...m }))];
|
|
1636
|
+
}
|
|
1637
|
+
tools() {
|
|
1638
|
+
return this._toolSpecs.map((t) => structuredClone(t));
|
|
1639
|
+
}
|
|
1640
|
+
addTool(spec) {
|
|
1641
|
+
const name = spec.function?.name;
|
|
1642
|
+
if (!name) return false;
|
|
1643
|
+
if (this._toolSpecs.some((t) => t.function?.name === name)) return false;
|
|
1644
|
+
this._toolSpecs.push(spec);
|
|
1645
|
+
this._fingerprintCache = null;
|
|
1646
|
+
return true;
|
|
1647
|
+
}
|
|
1648
|
+
get fingerprint() {
|
|
1649
|
+
if (this._fingerprintCache !== null) return this._fingerprintCache;
|
|
1650
|
+
this._fingerprintCache = this.computeFingerprint();
|
|
1651
|
+
return this._fingerprintCache;
|
|
1652
|
+
}
|
|
1653
|
+
/** Dev/test only — throws on cache drift, which always means a non-`addTool` mutation slipped in. */
|
|
1654
|
+
verifyFingerprint() {
|
|
1655
|
+
const fresh = this.computeFingerprint();
|
|
1656
|
+
if (this._fingerprintCache !== null && this._fingerprintCache !== fresh) {
|
|
1657
|
+
throw new Error(
|
|
1658
|
+
`ImmutablePrefix fingerprint drift: cached=${this._fingerprintCache}, fresh=${fresh}. A mutation path bypassed addTool's cache invalidation \u2014 DeepSeek will see prefix churn that the TUI / transcript log don't know about.`
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
this._fingerprintCache = fresh;
|
|
1662
|
+
return fresh;
|
|
1663
|
+
}
|
|
1664
|
+
computeFingerprint() {
|
|
1665
|
+
const blob = JSON.stringify({
|
|
1666
|
+
system: this.system,
|
|
1667
|
+
tools: this._toolSpecs,
|
|
1668
|
+
shots: this.fewShots
|
|
1669
|
+
});
|
|
1670
|
+
return createHash("sha256").update(blob).digest("hex").slice(0, 16);
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
var AppendOnlyLog = class {
|
|
1674
|
+
_entries = [];
|
|
1675
|
+
append(message) {
|
|
1676
|
+
if (!message || typeof message !== "object" || !("role" in message)) {
|
|
1677
|
+
throw new Error(`invalid log entry: ${JSON.stringify(message)}`);
|
|
1678
|
+
}
|
|
1679
|
+
this._entries.push(message);
|
|
1680
|
+
}
|
|
1681
|
+
extend(messages) {
|
|
1682
|
+
for (const m of messages) this.append(m);
|
|
1683
|
+
}
|
|
1684
|
+
/** The one append-only-breaking path — reserved for `/compact` + recovery. Use `append()` otherwise. */
|
|
1685
|
+
compactInPlace(replacement) {
|
|
1686
|
+
this._entries = [...replacement];
|
|
1687
|
+
}
|
|
1688
|
+
get entries() {
|
|
1689
|
+
return this._entries;
|
|
1690
|
+
}
|
|
1691
|
+
toMessages() {
|
|
1692
|
+
return this._entries.map((e) => ({ ...e }));
|
|
1693
|
+
}
|
|
1694
|
+
get length() {
|
|
1695
|
+
return this._entries.length;
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
var VolatileScratch = class {
|
|
1699
|
+
reasoning = null;
|
|
1700
|
+
planState = null;
|
|
1701
|
+
notes = [];
|
|
1702
|
+
reset() {
|
|
1703
|
+
this.reasoning = null;
|
|
1704
|
+
this.planState = null;
|
|
1705
|
+
this.notes = [];
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1418
1709
|
// src/repair/scavenge.ts
|
|
1419
1710
|
function scavengeToolCalls(reasoningContent, opts) {
|
|
1420
1711
|
if (!reasoningContent) return { calls: [], notes: [] };
|
|
@@ -1643,182 +1934,80 @@ function repairTruncatedJson(input) {
|
|
|
1643
1934
|
}
|
|
1644
1935
|
while (stack.length > 0) {
|
|
1645
1936
|
const top = stack.pop();
|
|
1646
|
-
if (top === "{") s += "}";
|
|
1647
|
-
else if (top === "[") s += "]";
|
|
1648
|
-
else if (top === '"') s += '"';
|
|
1649
|
-
}
|
|
1650
|
-
try {
|
|
1651
|
-
JSON.parse(s);
|
|
1652
|
-
return { repaired: s, changed: true, notes };
|
|
1653
|
-
} catch (err) {
|
|
1654
|
-
notes.push(`fallback to {}: ${err.message}`);
|
|
1655
|
-
return { repaired: "{}", changed: true, notes };
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
// src/repair/index.ts
|
|
1660
|
-
var ToolCallRepair = class {
|
|
1661
|
-
storm;
|
|
1662
|
-
opts;
|
|
1663
|
-
constructor(opts) {
|
|
1664
|
-
this.opts = opts;
|
|
1665
|
-
this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3, opts.isMutating);
|
|
1666
|
-
}
|
|
1667
|
-
/** Called at start of every user turn — fresh intent shouldn't inherit old repetition state. */
|
|
1668
|
-
resetStorm() {
|
|
1669
|
-
this.storm.reset();
|
|
1670
|
-
}
|
|
1671
|
-
process(declaredCalls, reasoningContent, content = null) {
|
|
1672
|
-
const report = {
|
|
1673
|
-
scavenged: 0,
|
|
1674
|
-
truncationsFixed: 0,
|
|
1675
|
-
stormsBroken: 0,
|
|
1676
|
-
notes: []
|
|
1677
|
-
};
|
|
1678
|
-
const combined = [reasoningContent ?? "", content ?? ""].filter(Boolean).join("\n");
|
|
1679
|
-
const scavenged = scavengeToolCalls(combined || null, {
|
|
1680
|
-
allowedNames: this.opts.allowedToolNames,
|
|
1681
|
-
maxCalls: this.opts.maxScavenge ?? 4
|
|
1682
|
-
});
|
|
1683
|
-
const seenSignatures = new Set(declaredCalls.map(signature));
|
|
1684
|
-
const merged = [...declaredCalls];
|
|
1685
|
-
for (const sc of scavenged.calls) {
|
|
1686
|
-
if (!seenSignatures.has(signature(sc))) {
|
|
1687
|
-
merged.push(sc);
|
|
1688
|
-
report.scavenged++;
|
|
1689
|
-
seenSignatures.add(signature(sc));
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
report.notes.push(...scavenged.notes);
|
|
1693
|
-
for (const call of merged) {
|
|
1694
|
-
const args = call.function?.arguments ?? "";
|
|
1695
|
-
const r = repairTruncatedJson(args);
|
|
1696
|
-
if (r.changed) {
|
|
1697
|
-
call.function.arguments = r.repaired;
|
|
1698
|
-
report.truncationsFixed++;
|
|
1699
|
-
report.notes.push(...r.notes.map((n) => `[${call.function.name}] ${n}`));
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
const filtered = [];
|
|
1703
|
-
for (const call of merged) {
|
|
1704
|
-
const verdict = this.storm.inspect(call);
|
|
1705
|
-
if (verdict.suppress) {
|
|
1706
|
-
report.stormsBroken++;
|
|
1707
|
-
if (verdict.reason) report.notes.push(verdict.reason);
|
|
1708
|
-
continue;
|
|
1709
|
-
}
|
|
1710
|
-
filtered.push(call);
|
|
1711
|
-
}
|
|
1712
|
-
return { calls: filtered, report };
|
|
1713
|
-
}
|
|
1714
|
-
};
|
|
1715
|
-
function signature(call) {
|
|
1716
|
-
return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
// src/telemetry/stats.ts
|
|
1720
|
-
var DEEPSEEK_PRICING = {
|
|
1721
|
-
"deepseek-v4-flash": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
|
|
1722
|
-
"deepseek-v4-pro": { inputCacheHit: 0.139, inputCacheMiss: 1.667, output: 3.333 },
|
|
1723
|
-
// Compat aliases — priced as v4-flash per the deprecation notice.
|
|
1724
|
-
"deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
|
|
1725
|
-
"deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 }
|
|
1726
|
-
};
|
|
1727
|
-
var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
|
|
1728
|
-
var DEEPSEEK_CONTEXT_TOKENS = {
|
|
1729
|
-
"deepseek-v4-flash": 1e6,
|
|
1730
|
-
"deepseek-v4-pro": 1e6,
|
|
1731
|
-
"deepseek-chat": 1e6,
|
|
1732
|
-
"deepseek-reasoner": 1e6
|
|
1733
|
-
};
|
|
1734
|
-
var DEFAULT_CONTEXT_TOKENS = 131072;
|
|
1735
|
-
function costUsd(model, usage) {
|
|
1736
|
-
const p = DEEPSEEK_PRICING[model];
|
|
1737
|
-
if (!p) return 0;
|
|
1738
|
-
return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss + usage.completionTokens * p.output) / 1e6;
|
|
1739
|
-
}
|
|
1740
|
-
function inputCostUsd(model, usage) {
|
|
1741
|
-
const p = DEEPSEEK_PRICING[model];
|
|
1742
|
-
if (!p) return 0;
|
|
1743
|
-
return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss) / 1e6;
|
|
1744
|
-
}
|
|
1745
|
-
function outputCostUsd(model, usage) {
|
|
1746
|
-
const p = DEEPSEEK_PRICING[model];
|
|
1747
|
-
if (!p) return 0;
|
|
1748
|
-
return usage.completionTokens * p.output / 1e6;
|
|
1749
|
-
}
|
|
1750
|
-
function cacheSavingsUsd(model, hitTokens) {
|
|
1751
|
-
if (hitTokens <= 0) return 0;
|
|
1752
|
-
const p = DEEPSEEK_PRICING[model];
|
|
1753
|
-
if (!p) return 0;
|
|
1754
|
-
return hitTokens * (p.inputCacheMiss - p.inputCacheHit) / 1e6;
|
|
1755
|
-
}
|
|
1756
|
-
function claudeEquivalentCost(usage) {
|
|
1757
|
-
return (usage.promptTokens * CLAUDE_SONNET_PRICING.input + usage.completionTokens * CLAUDE_SONNET_PRICING.output) / 1e6;
|
|
1758
|
-
}
|
|
1759
|
-
var SessionStats = class {
|
|
1760
|
-
turns = [];
|
|
1761
|
-
record(turn, model, usage) {
|
|
1762
|
-
const cost = costUsd(model, usage);
|
|
1763
|
-
const stats = {
|
|
1764
|
-
turn,
|
|
1765
|
-
model,
|
|
1766
|
-
usage,
|
|
1767
|
-
cost,
|
|
1768
|
-
cacheHitRatio: usage.cacheHitRatio
|
|
1769
|
-
};
|
|
1770
|
-
this.turns.push(stats);
|
|
1771
|
-
return stats;
|
|
1772
|
-
}
|
|
1773
|
-
get totalCost() {
|
|
1774
|
-
return this.turns.reduce((sum, t) => sum + t.cost, 0);
|
|
1775
|
-
}
|
|
1776
|
-
get totalClaudeEquivalent() {
|
|
1777
|
-
return this.turns.reduce((sum, t) => sum + claudeEquivalentCost(t.usage), 0);
|
|
1778
|
-
}
|
|
1779
|
-
get savingsVsClaude() {
|
|
1780
|
-
const c = this.totalClaudeEquivalent;
|
|
1781
|
-
return c > 0 ? 1 - this.totalCost / c : 0;
|
|
1937
|
+
if (top === "{") s += "}";
|
|
1938
|
+
else if (top === "[") s += "]";
|
|
1939
|
+
else if (top === '"') s += '"';
|
|
1782
1940
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1941
|
+
try {
|
|
1942
|
+
JSON.parse(s);
|
|
1943
|
+
return { repaired: s, changed: true, notes };
|
|
1944
|
+
} catch (err) {
|
|
1945
|
+
notes.push(`fallback to {}: ${err.message}`);
|
|
1946
|
+
return { repaired: "{}", changed: true, notes };
|
|
1785
1947
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/repair/index.ts
|
|
1951
|
+
var ToolCallRepair = class {
|
|
1952
|
+
storm;
|
|
1953
|
+
opts;
|
|
1954
|
+
constructor(opts) {
|
|
1955
|
+
this.opts = opts;
|
|
1956
|
+
this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3, opts.isMutating);
|
|
1788
1957
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
for (const t of this.turns) {
|
|
1793
|
-
hit += t.usage.promptCacheHitTokens;
|
|
1794
|
-
miss += t.usage.promptCacheMissTokens;
|
|
1795
|
-
}
|
|
1796
|
-
const denom = hit + miss;
|
|
1797
|
-
return denom > 0 ? hit / denom : 0;
|
|
1958
|
+
/** Called at start of every user turn — fresh intent shouldn't inherit old repetition state. */
|
|
1959
|
+
resetStorm() {
|
|
1960
|
+
this.storm.reset();
|
|
1798
1961
|
}
|
|
1799
|
-
|
|
1800
|
-
const
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
totalOutputCostUsd: round(this.totalOutputCost, 6),
|
|
1806
|
-
claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
|
|
1807
|
-
savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
|
|
1808
|
-
cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
|
|
1809
|
-
lastPromptTokens: last?.usage.promptTokens ?? 0,
|
|
1810
|
-
lastTurnCostUsd: round(last?.cost ?? 0, 6)
|
|
1962
|
+
process(declaredCalls, reasoningContent, content = null) {
|
|
1963
|
+
const report = {
|
|
1964
|
+
scavenged: 0,
|
|
1965
|
+
truncationsFixed: 0,
|
|
1966
|
+
stormsBroken: 0,
|
|
1967
|
+
notes: []
|
|
1811
1968
|
};
|
|
1969
|
+
const combined = [reasoningContent ?? "", content ?? ""].filter(Boolean).join("\n");
|
|
1970
|
+
const scavenged = scavengeToolCalls(combined || null, {
|
|
1971
|
+
allowedNames: this.opts.allowedToolNames,
|
|
1972
|
+
maxCalls: this.opts.maxScavenge ?? 4
|
|
1973
|
+
});
|
|
1974
|
+
const seenSignatures = new Set(declaredCalls.map(signature));
|
|
1975
|
+
const merged = [...declaredCalls];
|
|
1976
|
+
for (const sc of scavenged.calls) {
|
|
1977
|
+
if (!seenSignatures.has(signature(sc))) {
|
|
1978
|
+
merged.push(sc);
|
|
1979
|
+
report.scavenged++;
|
|
1980
|
+
seenSignatures.add(signature(sc));
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
report.notes.push(...scavenged.notes);
|
|
1984
|
+
for (const call of merged) {
|
|
1985
|
+
const args = call.function?.arguments ?? "";
|
|
1986
|
+
const r = repairTruncatedJson(args);
|
|
1987
|
+
if (r.changed) {
|
|
1988
|
+
call.function.arguments = r.repaired;
|
|
1989
|
+
report.truncationsFixed++;
|
|
1990
|
+
report.notes.push(...r.notes.map((n) => `[${call.function.name}] ${n}`));
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
const filtered = [];
|
|
1994
|
+
for (const call of merged) {
|
|
1995
|
+
const verdict = this.storm.inspect(call);
|
|
1996
|
+
if (verdict.suppress) {
|
|
1997
|
+
report.stormsBroken++;
|
|
1998
|
+
if (verdict.reason) report.notes.push(verdict.reason);
|
|
1999
|
+
continue;
|
|
2000
|
+
}
|
|
2001
|
+
filtered.push(call);
|
|
2002
|
+
}
|
|
2003
|
+
return { calls: filtered, report };
|
|
1812
2004
|
}
|
|
1813
2005
|
};
|
|
1814
|
-
function
|
|
1815
|
-
|
|
1816
|
-
return Math.round(n * f) / f;
|
|
2006
|
+
function signature(call) {
|
|
2007
|
+
return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
|
|
1817
2008
|
}
|
|
1818
2009
|
|
|
1819
2010
|
// src/loop.ts
|
|
1820
|
-
var ARGS_COMPACT_THRESHOLD_TOKENS = 800;
|
|
1821
|
-
var TURN_END_RESULT_CAP_TOKENS = 3e3;
|
|
1822
2011
|
var FAILURE_ESCALATION_THRESHOLD = 3;
|
|
1823
2012
|
var ESCALATION_MODEL = "deepseek-v4-pro";
|
|
1824
2013
|
var NEEDS_PRO_MARKER_PREFIX = "<<<NEEDS_PRO";
|
|
@@ -1849,6 +2038,8 @@ var CacheFirstLoop = class {
|
|
|
1849
2038
|
sessionName;
|
|
1850
2039
|
hooks;
|
|
1851
2040
|
hookCwd;
|
|
2041
|
+
/** PauseGate bridge — defaults to singleton, injectable for tests. */
|
|
2042
|
+
confirmationGate;
|
|
1852
2043
|
/** Number of messages that were pre-loaded from the session file. */
|
|
1853
2044
|
resumedMessageCount;
|
|
1854
2045
|
_turn = 0;
|
|
@@ -1860,6 +2051,8 @@ var CacheFirstLoop = class {
|
|
|
1860
2051
|
_turnFailureCount = 0;
|
|
1861
2052
|
_turnFailureTypes = {};
|
|
1862
2053
|
_turnSelfCorrected = false;
|
|
2054
|
+
_foldedThisTurn = false;
|
|
2055
|
+
context;
|
|
1863
2056
|
constructor(opts) {
|
|
1864
2057
|
this.client = opts.client;
|
|
1865
2058
|
this.prefix = opts.prefix;
|
|
@@ -1871,6 +2064,7 @@ var CacheFirstLoop = class {
|
|
|
1871
2064
|
this.maxToolIters = opts.maxToolIters ?? 64;
|
|
1872
2065
|
this.hooks = opts.hooks ?? [];
|
|
1873
2066
|
this.hookCwd = opts.hookCwd ?? process.cwd();
|
|
2067
|
+
this.confirmationGate = opts.confirmationGate ?? pauseGate;
|
|
1874
2068
|
if (typeof opts.branch === "number") {
|
|
1875
2069
|
this.branchOptions = { budget: opts.branch };
|
|
1876
2070
|
} else if (opts.branch && typeof opts.branch === "object") {
|
|
@@ -1933,54 +2127,18 @@ var CacheFirstLoop = class {
|
|
|
1933
2127
|
} else {
|
|
1934
2128
|
this.resumedMessageCount = 0;
|
|
1935
2129
|
}
|
|
2130
|
+
this.context = new ContextManager({
|
|
2131
|
+
client: this.client,
|
|
2132
|
+
log: this.log,
|
|
2133
|
+
stats: this.stats,
|
|
2134
|
+
sessionName: this.sessionName,
|
|
2135
|
+
getAbortSignal: () => this._turnAbort.signal,
|
|
2136
|
+
getCurrentTurn: () => this._turn
|
|
2137
|
+
});
|
|
1936
2138
|
}
|
|
1937
|
-
/**
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
const { messages, healedCount } = shrinkOversizedToolCallArgsByTokens(
|
|
1941
|
-
before,
|
|
1942
|
-
ARGS_COMPACT_THRESHOLD_TOKENS
|
|
1943
|
-
);
|
|
1944
|
-
if (healedCount === 0) return;
|
|
1945
|
-
this.log.compactInPlace(messages);
|
|
1946
|
-
if (this.sessionName) {
|
|
1947
|
-
try {
|
|
1948
|
-
rewriteSession(this.sessionName, messages);
|
|
1949
|
-
} catch {
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
/** Preventive end-of-turn shrink — trim big results before they ride into the next prompt. */
|
|
1954
|
-
autoCompactToolResultsOnTurnEnd() {
|
|
1955
|
-
const before = this.log.toMessages();
|
|
1956
|
-
const shrunk = shrinkOversizedToolResultsByTokens(before, TURN_END_RESULT_CAP_TOKENS);
|
|
1957
|
-
if (shrunk.healedCount === 0) return;
|
|
1958
|
-
this.log.compactInPlace(shrunk.messages);
|
|
1959
|
-
if (this.sessionName) {
|
|
1960
|
-
try {
|
|
1961
|
-
rewriteSession(this.sessionName, shrunk.messages);
|
|
1962
|
-
} catch {
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
compact(maxTokens = 4e3) {
|
|
1967
|
-
const before = this.log.toMessages();
|
|
1968
|
-
const resultsPass = shrinkOversizedToolResultsByTokens(before, maxTokens);
|
|
1969
|
-
const argsPass = shrinkOversizedToolCallArgsByTokens(resultsPass.messages, maxTokens);
|
|
1970
|
-
const messages = argsPass.messages;
|
|
1971
|
-
const healedCount = resultsPass.healedCount + argsPass.healedCount;
|
|
1972
|
-
const tokensSaved = resultsPass.tokensSaved + argsPass.tokensSaved;
|
|
1973
|
-
const charsSaved = resultsPass.charsSaved + argsPass.charsSaved;
|
|
1974
|
-
if (healedCount > 0) {
|
|
1975
|
-
this.log.compactInPlace(messages);
|
|
1976
|
-
if (this.sessionName) {
|
|
1977
|
-
try {
|
|
1978
|
-
rewriteSession(this.sessionName, messages);
|
|
1979
|
-
} catch {
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
return { healedCount, tokensSaved, charsSaved };
|
|
2139
|
+
/** Replace older turns with one summary message; keep tail within keepRecentTokens budget. */
|
|
2140
|
+
async compactHistory(opts) {
|
|
2141
|
+
return this.context.fold(this.model, opts);
|
|
1984
2142
|
}
|
|
1985
2143
|
appendAndPersist(message) {
|
|
1986
2144
|
this.log.append(message);
|
|
@@ -2178,6 +2336,7 @@ var CacheFirstLoop = class {
|
|
|
2178
2336
|
this._turnFailureTypes = {};
|
|
2179
2337
|
this._turnSelfCorrected = false;
|
|
2180
2338
|
this._escalateThisTurn = false;
|
|
2339
|
+
this._foldedThisTurn = false;
|
|
2181
2340
|
let armedConsumed = false;
|
|
2182
2341
|
if (this._proArmedForNextTurn) {
|
|
2183
2342
|
this._escalateThisTurn = true;
|
|
@@ -2214,7 +2373,6 @@ var CacheFirstLoop = class {
|
|
|
2214
2373
|
content: stoppedMsg,
|
|
2215
2374
|
forcedSummary: true
|
|
2216
2375
|
};
|
|
2217
|
-
this.autoCompactToolResultsOnTurnEnd();
|
|
2218
2376
|
yield { turn: this._turn, role: "done", content: stoppedMsg };
|
|
2219
2377
|
this._turnAbort = new AbortController();
|
|
2220
2378
|
return;
|
|
@@ -2236,26 +2394,31 @@ var CacheFirstLoop = class {
|
|
|
2236
2394
|
}
|
|
2237
2395
|
let messages = this.buildMessages(pendingUser);
|
|
2238
2396
|
{
|
|
2239
|
-
const
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2397
|
+
const decision2 = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
|
|
2398
|
+
if (decision2.needsAction) {
|
|
2399
|
+
const { estimateTokens: estimate, ctxMax } = decision2;
|
|
2400
|
+
yield {
|
|
2401
|
+
turn: this._turn,
|
|
2402
|
+
role: "status",
|
|
2403
|
+
content: "preflight: context near full, attempting fold\u2026"
|
|
2404
|
+
};
|
|
2405
|
+
const result = await this.context.fold(this.model);
|
|
2406
|
+
if (result.folded) {
|
|
2244
2407
|
yield {
|
|
2245
2408
|
turn: this._turn,
|
|
2246
2409
|
role: "warning",
|
|
2247
|
-
content: `preflight: request ~${estimate.toLocaleString()}/${
|
|
2248
|
-
estimate /
|
|
2249
|
-
)}%) \u2014
|
|
2410
|
+
content: `preflight: request ~${estimate.toLocaleString()}/${ctxMax.toLocaleString()} tokens (${Math.round(
|
|
2411
|
+
estimate / ctxMax * 100
|
|
2412
|
+
)}%) \u2014 folded ${result.beforeMessages} messages \u2192 ${result.afterMessages} (summary ${result.summaryChars} chars). Sending.`
|
|
2250
2413
|
};
|
|
2251
2414
|
messages = this.buildMessages(pendingUser);
|
|
2252
2415
|
} else {
|
|
2253
2416
|
yield {
|
|
2254
2417
|
turn: this._turn,
|
|
2255
2418
|
role: "warning",
|
|
2256
|
-
content: `preflight: request ~${estimate.toLocaleString()}/${
|
|
2257
|
-
estimate /
|
|
2258
|
-
)}%) and nothing to
|
|
2419
|
+
content: `preflight: request ~${estimate.toLocaleString()}/${ctxMax.toLocaleString()} tokens (${Math.round(
|
|
2420
|
+
estimate / ctxMax * 100
|
|
2421
|
+
)}%) and nothing left to fold \u2014 DeepSeek will likely 400. Run /clear or /new to start fresh.`
|
|
2259
2422
|
};
|
|
2260
2423
|
}
|
|
2261
2424
|
}
|
|
@@ -2450,7 +2613,6 @@ var CacheFirstLoop = class {
|
|
|
2450
2613
|
}
|
|
2451
2614
|
} catch (err) {
|
|
2452
2615
|
if (signal.aborted) {
|
|
2453
|
-
this.autoCompactToolResultsOnTurnEnd();
|
|
2454
2616
|
yield { turn: this._turn, role: "done", content: "" };
|
|
2455
2617
|
this._turnAbort = new AbortController();
|
|
2456
2618
|
return;
|
|
@@ -2568,60 +2730,43 @@ var CacheFirstLoop = class {
|
|
|
2568
2730
|
yield* this.forceSummaryAfterIterLimit({ reason: "stuck" });
|
|
2569
2731
|
return;
|
|
2570
2732
|
}
|
|
2571
|
-
this.autoCompactToolResultsOnTurnEnd();
|
|
2572
2733
|
yield { turn: this._turn, role: "done", content: assistantContent };
|
|
2573
2734
|
return;
|
|
2574
2735
|
}
|
|
2575
|
-
const
|
|
2576
|
-
if (
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
)}%) \u2014 proactively compacted ${soft.healedCount} tool result(s) to 4k tokens, saved ${soft.tokensSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Staying ahead of the 80% guard.`
|
|
2589
|
-
};
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
}
|
|
2593
|
-
if (usage && usage.promptTokens / ctxMax > 0.8) {
|
|
2594
|
-
const before = usage.promptTokens;
|
|
2595
|
-
const compactResult = this.compact(1e3);
|
|
2596
|
-
if (compactResult.healedCount > 0) {
|
|
2597
|
-
const after = Math.max(0, before - compactResult.tokensSaved);
|
|
2598
|
-
yield {
|
|
2599
|
-
turn: this._turn,
|
|
2600
|
-
role: "warning",
|
|
2601
|
-
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} \u2014 auto-compacted ${compactResult.healedCount} oversized tool result(s), saved ${compactResult.tokensSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Continuing.`
|
|
2602
|
-
};
|
|
2603
|
-
} else {
|
|
2736
|
+
const decision = this.context.decideAfterUsage(usage, this.model, this._foldedThisTurn);
|
|
2737
|
+
if (decision.kind === "fold") {
|
|
2738
|
+
this._foldedThisTurn = true;
|
|
2739
|
+
const before = decision.promptTokens;
|
|
2740
|
+
const ctxMax = decision.ctxMax;
|
|
2741
|
+
const aggressiveTag = decision.aggressive ? " (aggressive)" : "";
|
|
2742
|
+
yield {
|
|
2743
|
+
turn: this._turn,
|
|
2744
|
+
role: "status",
|
|
2745
|
+
content: `compacting history${aggressiveTag}\u2026`
|
|
2746
|
+
};
|
|
2747
|
+
const result = await this.compactHistory({ keepRecentTokens: decision.tailBudget });
|
|
2748
|
+
if (result.folded) {
|
|
2604
2749
|
yield {
|
|
2605
2750
|
turn: this._turn,
|
|
2606
2751
|
role: "warning",
|
|
2607
2752
|
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
|
|
2608
2753
|
before / ctxMax * 100
|
|
2609
|
-
)}%) \u2014
|
|
2754
|
+
)}%) \u2014 ${decision.aggressive ? "aggressively folded" : "folded"} ${result.beforeMessages} messages \u2192 ${result.afterMessages} (summary ${result.summaryChars} chars). Continuing.`
|
|
2610
2755
|
};
|
|
2611
|
-
const tail = this.log.entries[this.log.entries.length - 1];
|
|
2612
|
-
if (tail && tail.role === "assistant" && Array.isArray(tail.tool_calls) && tail.tool_calls.length > 0) {
|
|
2613
|
-
const kept = this.log.entries.slice(0, -1);
|
|
2614
|
-
this.log.compactInPlace([...kept]);
|
|
2615
|
-
if (this.sessionName) {
|
|
2616
|
-
try {
|
|
2617
|
-
rewriteSession(this.sessionName, kept);
|
|
2618
|
-
} catch {
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
}
|
|
2622
|
-
yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
|
|
2623
|
-
return;
|
|
2624
2756
|
}
|
|
2757
|
+
} else if (decision.kind === "exit-with-summary") {
|
|
2758
|
+
const before = decision.promptTokens;
|
|
2759
|
+
const ctxMax = decision.ctxMax;
|
|
2760
|
+
yield {
|
|
2761
|
+
turn: this._turn,
|
|
2762
|
+
role: "warning",
|
|
2763
|
+
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
|
|
2764
|
+
before / ctxMax * 100
|
|
2765
|
+
)}%) \u2014 forcing summary from what was gathered. Run /compact, /clear, or /new to reset.`
|
|
2766
|
+
};
|
|
2767
|
+
this.context.trimTrailingToolCalls();
|
|
2768
|
+
yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
|
|
2769
|
+
return;
|
|
2625
2770
|
}
|
|
2626
2771
|
for (const call of repairedCalls) {
|
|
2627
2772
|
const name = call.function?.name ?? "";
|
|
@@ -2653,7 +2798,8 @@ ${reason}`;
|
|
|
2653
2798
|
} else {
|
|
2654
2799
|
result = await this.tools.dispatch(name, args, {
|
|
2655
2800
|
signal,
|
|
2656
|
-
maxResultTokens: DEFAULT_MAX_RESULT_TOKENS
|
|
2801
|
+
maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
|
|
2802
|
+
confirmationGate: this.confirmationGate
|
|
2657
2803
|
});
|
|
2658
2804
|
const postReport = await runHooks({
|
|
2659
2805
|
hooks: this.hooks,
|
|
@@ -2673,7 +2819,6 @@ ${reason}`;
|
|
|
2673
2819
|
name,
|
|
2674
2820
|
content: result
|
|
2675
2821
|
});
|
|
2676
|
-
this.compactToolCallArgsAfterResponse();
|
|
2677
2822
|
if (this.noteToolFailureSignal(result)) {
|
|
2678
2823
|
yield {
|
|
2679
2824
|
turn: this._turn,
|
|
@@ -2732,7 +2877,6 @@ ${summary}`;
|
|
|
2732
2877
|
stats: summaryStats,
|
|
2733
2878
|
forcedSummary: true
|
|
2734
2879
|
};
|
|
2735
|
-
this.autoCompactToolResultsOnTurnEnd();
|
|
2736
2880
|
yield { turn: this._turn, role: "done", content: summary };
|
|
2737
2881
|
} catch (err) {
|
|
2738
2882
|
const label = errorLabelFor(opts.reason, this.maxToolIters);
|
|
@@ -2742,7 +2886,6 @@ ${summary}`;
|
|
|
2742
2886
|
content: "",
|
|
2743
2887
|
error: `${label} and the fallback summary call failed: ${err.message}. Run /clear and retry with a narrower question, or raise --max-tool-iters.`
|
|
2744
2888
|
};
|
|
2745
|
-
this.autoCompactToolResultsOnTurnEnd();
|
|
2746
2889
|
yield { turn: this._turn, role: "done", content: "" };
|
|
2747
2890
|
}
|
|
2748
2891
|
}
|
|
@@ -2871,56 +3014,6 @@ function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
|
|
|
2871
3014
|
});
|
|
2872
3015
|
return { messages: out, healedCount, tokensSaved, charsSaved };
|
|
2873
3016
|
}
|
|
2874
|
-
function shrinkOversizedToolCallArgsByTokens(messages, maxTokens) {
|
|
2875
|
-
let healedCount = 0;
|
|
2876
|
-
let tokensSaved = 0;
|
|
2877
|
-
let charsSaved = 0;
|
|
2878
|
-
const out = messages.map((msg) => {
|
|
2879
|
-
if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls)) return msg;
|
|
2880
|
-
let changed = false;
|
|
2881
|
-
const newCalls = msg.tool_calls.map((call) => {
|
|
2882
|
-
const args = call.function?.arguments;
|
|
2883
|
-
if (typeof args !== "string" || args.length <= maxTokens) return call;
|
|
2884
|
-
const beforeTokens = countTokens(args);
|
|
2885
|
-
if (beforeTokens <= maxTokens) return call;
|
|
2886
|
-
const shrunk = shrinkJsonLongStrings(args);
|
|
2887
|
-
const afterTokens = countTokens(shrunk);
|
|
2888
|
-
if (afterTokens >= beforeTokens) return call;
|
|
2889
|
-
changed = true;
|
|
2890
|
-
healedCount += 1;
|
|
2891
|
-
tokensSaved += beforeTokens - afterTokens;
|
|
2892
|
-
charsSaved += args.length - shrunk.length;
|
|
2893
|
-
return { ...call, function: { ...call.function, arguments: shrunk } };
|
|
2894
|
-
});
|
|
2895
|
-
if (!changed) return msg;
|
|
2896
|
-
return { ...msg, tool_calls: newCalls };
|
|
2897
|
-
});
|
|
2898
|
-
return { messages: out, healedCount, tokensSaved, charsSaved };
|
|
2899
|
-
}
|
|
2900
|
-
function shrinkJsonLongStrings(jsonStr) {
|
|
2901
|
-
let parsed;
|
|
2902
|
-
try {
|
|
2903
|
-
parsed = JSON.parse(jsonStr);
|
|
2904
|
-
} catch {
|
|
2905
|
-
const head = jsonStr.slice(0, 200);
|
|
2906
|
-
return `${head}\u2026[shrunk: ${jsonStr.length} chars, unparsed]`;
|
|
2907
|
-
}
|
|
2908
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2909
|
-
return jsonStr;
|
|
2910
|
-
}
|
|
2911
|
-
const LONG_THRESHOLD = 300;
|
|
2912
|
-
const input = parsed;
|
|
2913
|
-
const output = {};
|
|
2914
|
-
for (const [k, v] of Object.entries(input)) {
|
|
2915
|
-
if (typeof v === "string" && v.length > LONG_THRESHOLD) {
|
|
2916
|
-
const newlines = v.match(/\n/g)?.length ?? 0;
|
|
2917
|
-
output[k] = `[\u2026shrunk: ${v.length} chars, ${newlines} lines \u2014 tool already responded, see result]`;
|
|
2918
|
-
} else {
|
|
2919
|
-
output[k] = v;
|
|
2920
|
-
}
|
|
2921
|
-
}
|
|
2922
|
-
return JSON.stringify(output);
|
|
2923
|
-
}
|
|
2924
3017
|
function fixToolCallPairing(messages) {
|
|
2925
3018
|
const out = [];
|
|
2926
3019
|
let droppedAssistantCalls = 0;
|
|
@@ -4876,7 +4969,7 @@ function registerChoiceTool(registry, opts = {}) {
|
|
|
4876
4969
|
},
|
|
4877
4970
|
required: ["question", "options"]
|
|
4878
4971
|
},
|
|
4879
|
-
fn: async (args) => {
|
|
4972
|
+
fn: async (args, ctx) => {
|
|
4880
4973
|
const question = (args?.question ?? "").trim();
|
|
4881
4974
|
if (!question) {
|
|
4882
4975
|
throw new Error(
|
|
@@ -4896,7 +4989,13 @@ function registerChoiceTool(registry, opts = {}) {
|
|
|
4896
4989
|
}
|
|
4897
4990
|
const allowCustom = args?.allowCustom === true;
|
|
4898
4991
|
opts.onChoiceRequested?.(question, options);
|
|
4899
|
-
|
|
4992
|
+
const verdict = await (ctx?.confirmationGate ?? pauseGate).ask({
|
|
4993
|
+
kind: "choice",
|
|
4994
|
+
payload: { question, options, allowCustom }
|
|
4995
|
+
});
|
|
4996
|
+
if (verdict.type === "pick") return `user picked: ${verdict.optionId}`;
|
|
4997
|
+
if (verdict.type === "text") return `user answered: ${verdict.text}`;
|
|
4998
|
+
return "user cancelled the choice";
|
|
4900
4999
|
}
|
|
4901
5000
|
});
|
|
4902
5001
|
return registry;
|
|
@@ -5013,7 +5112,7 @@ function registerSubmitPlan(registry, opts) {
|
|
|
5013
5112
|
},
|
|
5014
5113
|
required: ["plan"]
|
|
5015
5114
|
},
|
|
5016
|
-
fn: async (args) => {
|
|
5115
|
+
fn: async (args, ctx) => {
|
|
5017
5116
|
const plan = (args?.plan ?? "").trim();
|
|
5018
5117
|
if (!plan) {
|
|
5019
5118
|
throw new Error("submit_plan: empty plan \u2014 write a markdown plan and try again.");
|
|
@@ -5021,7 +5120,13 @@ function registerSubmitPlan(registry, opts) {
|
|
|
5021
5120
|
const steps = sanitizeSteps(args?.steps);
|
|
5022
5121
|
const summary = typeof args?.summary === "string" ? args.summary.trim() || void 0 : void 0;
|
|
5023
5122
|
opts.onPlanSubmitted?.(plan, steps);
|
|
5024
|
-
|
|
5123
|
+
const verdict = await (ctx?.confirmationGate ?? pauseGate).ask({
|
|
5124
|
+
kind: "plan_proposed",
|
|
5125
|
+
payload: { plan, steps, summary }
|
|
5126
|
+
});
|
|
5127
|
+
if (verdict.type === "approve") return "plan approved";
|
|
5128
|
+
if (verdict.type === "refine") throw new Error("user requested refinement");
|
|
5129
|
+
throw new Error("plan cancelled");
|
|
5025
5130
|
}
|
|
5026
5131
|
});
|
|
5027
5132
|
}
|
|
@@ -5052,7 +5157,7 @@ function registerMarkStepComplete(registry, opts) {
|
|
|
5052
5157
|
},
|
|
5053
5158
|
required: ["stepId", "result"]
|
|
5054
5159
|
},
|
|
5055
|
-
fn: async (args) => {
|
|
5160
|
+
fn: async (args, ctx) => {
|
|
5056
5161
|
const stepId = (args?.stepId ?? "").trim();
|
|
5057
5162
|
const result = (args?.result ?? "").trim();
|
|
5058
5163
|
if (!stepId) {
|
|
@@ -5069,7 +5174,16 @@ function registerMarkStepComplete(registry, opts) {
|
|
|
5069
5174
|
if (title) update.title = title;
|
|
5070
5175
|
if (notes) update.notes = notes;
|
|
5071
5176
|
opts.onStepCompleted?.(update);
|
|
5072
|
-
|
|
5177
|
+
const verdict = await (ctx?.confirmationGate ?? pauseGate).ask({
|
|
5178
|
+
kind: "plan_checkpoint",
|
|
5179
|
+
payload: { stepId, title, result, notes }
|
|
5180
|
+
});
|
|
5181
|
+
if (verdict.type === "continue") return JSON.stringify(update);
|
|
5182
|
+
if (verdict.type === "revise") {
|
|
5183
|
+
if (verdict.feedback) return `revision requested: ${verdict.feedback}`;
|
|
5184
|
+
throw new Error("user requested revision at checkpoint");
|
|
5185
|
+
}
|
|
5186
|
+
throw new Error("user stopped at checkpoint");
|
|
5073
5187
|
}
|
|
5074
5188
|
});
|
|
5075
5189
|
}
|
|
@@ -5097,7 +5211,7 @@ function registerRevisePlan(registry, opts) {
|
|
|
5097
5211
|
},
|
|
5098
5212
|
required: ["reason", "remainingSteps"]
|
|
5099
5213
|
},
|
|
5100
|
-
fn: async (args) => {
|
|
5214
|
+
fn: async (args, ctx) => {
|
|
5101
5215
|
const reason = (args?.reason ?? "").trim();
|
|
5102
5216
|
if (!reason) {
|
|
5103
5217
|
throw new Error(
|
|
@@ -5112,7 +5226,13 @@ function registerRevisePlan(registry, opts) {
|
|
|
5112
5226
|
}
|
|
5113
5227
|
const summary = typeof args?.summary === "string" ? args.summary.trim() || void 0 : void 0;
|
|
5114
5228
|
opts.onPlanRevisionProposed?.(reason, remainingSteps, summary);
|
|
5115
|
-
|
|
5229
|
+
const verdict = await (ctx?.confirmationGate ?? pauseGate).ask({
|
|
5230
|
+
kind: "plan_revision",
|
|
5231
|
+
payload: { reason, remainingSteps, summary }
|
|
5232
|
+
});
|
|
5233
|
+
if (verdict.type === "accepted") return "revision accepted";
|
|
5234
|
+
if (verdict.type === "rejected") throw new Error("revision rejected");
|
|
5235
|
+
throw new Error("revision cancelled");
|
|
5116
5236
|
}
|
|
5117
5237
|
});
|
|
5118
5238
|
}
|
|
@@ -5371,6 +5491,60 @@ import { spawn as spawn4, spawnSync } from "child_process";
|
|
|
5371
5491
|
import { existsSync as existsSync8, statSync as statSync4 } from "fs";
|
|
5372
5492
|
import * as pathMod4 from "path";
|
|
5373
5493
|
|
|
5494
|
+
// src/config.ts
|
|
5495
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
|
|
5496
|
+
import { homedir as homedir5 } from "os";
|
|
5497
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
5498
|
+
function defaultConfigPath() {
|
|
5499
|
+
return join9(homedir5(), ".reasonix", "config.json");
|
|
5500
|
+
}
|
|
5501
|
+
function readConfig(path2 = defaultConfigPath()) {
|
|
5502
|
+
try {
|
|
5503
|
+
const raw = readFileSync9(path2, "utf8");
|
|
5504
|
+
const parsed = JSON.parse(raw);
|
|
5505
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
5506
|
+
} catch {
|
|
5507
|
+
}
|
|
5508
|
+
return {};
|
|
5509
|
+
}
|
|
5510
|
+
function writeConfig(cfg, path2 = defaultConfigPath()) {
|
|
5511
|
+
mkdirSync3(dirname4(path2), { recursive: true });
|
|
5512
|
+
writeFileSync3(path2, JSON.stringify(cfg, null, 2), "utf8");
|
|
5513
|
+
try {
|
|
5514
|
+
chmodSync2(path2, 384);
|
|
5515
|
+
} catch {
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5518
|
+
function loadApiKey(path2 = defaultConfigPath()) {
|
|
5519
|
+
if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
|
|
5520
|
+
return readConfig(path2).apiKey;
|
|
5521
|
+
}
|
|
5522
|
+
function saveApiKey(key, path2 = defaultConfigPath()) {
|
|
5523
|
+
const cfg = readConfig(path2);
|
|
5524
|
+
cfg.apiKey = key.trim();
|
|
5525
|
+
writeConfig(cfg, path2);
|
|
5526
|
+
}
|
|
5527
|
+
function addProjectShellAllowed(rootDir, prefix, path2 = defaultConfigPath()) {
|
|
5528
|
+
const trimmed = prefix.trim();
|
|
5529
|
+
if (!trimmed) return;
|
|
5530
|
+
const cfg = readConfig(path2);
|
|
5531
|
+
if (!cfg.projects) cfg.projects = {};
|
|
5532
|
+
if (!cfg.projects[rootDir]) cfg.projects[rootDir] = {};
|
|
5533
|
+
const existing = cfg.projects[rootDir].shellAllowed ?? [];
|
|
5534
|
+
if (existing.includes(trimmed)) return;
|
|
5535
|
+
cfg.projects[rootDir].shellAllowed = [...existing, trimmed];
|
|
5536
|
+
writeConfig(cfg, path2);
|
|
5537
|
+
}
|
|
5538
|
+
function isPlausibleKey(key) {
|
|
5539
|
+
const trimmed = key.trim();
|
|
5540
|
+
return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
|
|
5541
|
+
}
|
|
5542
|
+
function redactKey(key) {
|
|
5543
|
+
if (!key) return "";
|
|
5544
|
+
if (key.length <= 12) return "****";
|
|
5545
|
+
return `${key.slice(0, 6)}\u2026${key.slice(-4)}`;
|
|
5546
|
+
}
|
|
5547
|
+
|
|
5374
5548
|
// src/tools/jobs.ts
|
|
5375
5549
|
import { spawn as spawn2 } from "child_process";
|
|
5376
5550
|
import * as pathMod2 from "path";
|
|
@@ -6498,7 +6672,16 @@ function registerShellTools(registry, opts) {
|
|
|
6498
6672
|
const cmd = args.command.trim();
|
|
6499
6673
|
if (!cmd) throw new Error("run_command: empty command");
|
|
6500
6674
|
if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed())) {
|
|
6501
|
-
|
|
6675
|
+
const gate = ctx?.confirmationGate ?? pauseGate;
|
|
6676
|
+
const choice = await gate.ask({ kind: "run_command", payload: { command: cmd } });
|
|
6677
|
+
if (choice.type === "deny") {
|
|
6678
|
+
throw new Error(
|
|
6679
|
+
`user denied: ${cmd}${choice.denyContext ? ` \u2014 ${choice.denyContext}` : ""}`
|
|
6680
|
+
);
|
|
6681
|
+
}
|
|
6682
|
+
if (choice.type === "always_allow") {
|
|
6683
|
+
addProjectShellAllowed(rootDir, choice.prefix);
|
|
6684
|
+
}
|
|
6502
6685
|
}
|
|
6503
6686
|
const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
|
|
6504
6687
|
const result = await runCommand(cmd, {
|
|
@@ -6530,8 +6713,17 @@ function registerShellTools(registry, opts) {
|
|
|
6530
6713
|
fn: async (args, ctx) => {
|
|
6531
6714
|
const cmd = args.command.trim();
|
|
6532
6715
|
if (!cmd) throw new Error("run_background: empty command");
|
|
6533
|
-
if (!isAllowAll() && !
|
|
6534
|
-
|
|
6716
|
+
if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed())) {
|
|
6717
|
+
const gate = ctx?.confirmationGate ?? pauseGate;
|
|
6718
|
+
const choice = await gate.ask({ kind: "run_background", payload: { command: cmd } });
|
|
6719
|
+
if (choice.type === "deny") {
|
|
6720
|
+
throw new Error(
|
|
6721
|
+
`user denied: ${cmd}${choice.denyContext ? ` \u2014 ${choice.denyContext}` : ""}`
|
|
6722
|
+
);
|
|
6723
|
+
}
|
|
6724
|
+
if (choice.type === "always_allow") {
|
|
6725
|
+
addProjectShellAllowed(rootDir, choice.prefix);
|
|
6726
|
+
}
|
|
6535
6727
|
}
|
|
6536
6728
|
const result = await jobs.start(cmd, {
|
|
6537
6729
|
cwd: rootDir,
|
|
@@ -6860,12 +7052,12 @@ ${i + 1}. ${r.title}`);
|
|
|
6860
7052
|
}
|
|
6861
7053
|
|
|
6862
7054
|
// src/env.ts
|
|
6863
|
-
import { readFileSync as
|
|
7055
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
6864
7056
|
import { resolve as resolve8 } from "path";
|
|
6865
7057
|
function loadDotenv(path2 = ".env") {
|
|
6866
7058
|
let raw;
|
|
6867
7059
|
try {
|
|
6868
|
-
raw =
|
|
7060
|
+
raw = readFileSync10(resolve8(process.cwd(), path2), "utf8");
|
|
6869
7061
|
} catch {
|
|
6870
7062
|
return;
|
|
6871
7063
|
}
|
|
@@ -6884,7 +7076,7 @@ function loadDotenv(path2 = ".env") {
|
|
|
6884
7076
|
}
|
|
6885
7077
|
|
|
6886
7078
|
// src/transcript/log.ts
|
|
6887
|
-
import { createWriteStream, readFileSync as
|
|
7079
|
+
import { createWriteStream, readFileSync as readFileSync11 } from "fs";
|
|
6888
7080
|
function recordFromLoopEvent(ev, extra) {
|
|
6889
7081
|
const rec = {
|
|
6890
7082
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6935,7 +7127,7 @@ function openTranscriptFile(path2, meta) {
|
|
|
6935
7127
|
return stream;
|
|
6936
7128
|
}
|
|
6937
7129
|
function readTranscript(path2) {
|
|
6938
|
-
const raw =
|
|
7130
|
+
const raw = readFileSync11(path2, "utf8");
|
|
6939
7131
|
return parseTranscript(raw);
|
|
6940
7132
|
}
|
|
6941
7133
|
function isPlanStateEmptyShape(s) {
|
|
@@ -7382,25 +7574,25 @@ function truncate(s, n) {
|
|
|
7382
7574
|
}
|
|
7383
7575
|
|
|
7384
7576
|
// src/version.ts
|
|
7385
|
-
import { existsSync as existsSync9, mkdirSync as
|
|
7386
|
-
import { homedir as
|
|
7387
|
-
import { dirname as
|
|
7577
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
|
|
7578
|
+
import { homedir as homedir6 } from "os";
|
|
7579
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
7388
7580
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7389
7581
|
var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
|
|
7390
7582
|
var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
7391
7583
|
var LATEST_FETCH_TIMEOUT_MS = 2e3;
|
|
7392
7584
|
function readPackageVersion() {
|
|
7393
7585
|
try {
|
|
7394
|
-
let dir =
|
|
7586
|
+
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
7395
7587
|
for (let i = 0; i < 6; i++) {
|
|
7396
|
-
const p =
|
|
7588
|
+
const p = join10(dir, "package.json");
|
|
7397
7589
|
if (existsSync9(p)) {
|
|
7398
|
-
const pkg = JSON.parse(
|
|
7590
|
+
const pkg = JSON.parse(readFileSync12(p, "utf8"));
|
|
7399
7591
|
if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
|
|
7400
7592
|
return pkg.version;
|
|
7401
7593
|
}
|
|
7402
7594
|
}
|
|
7403
|
-
const parent =
|
|
7595
|
+
const parent = dirname5(dir);
|
|
7404
7596
|
if (parent === dir) break;
|
|
7405
7597
|
dir = parent;
|
|
7406
7598
|
}
|
|
@@ -7410,11 +7602,11 @@ function readPackageVersion() {
|
|
|
7410
7602
|
}
|
|
7411
7603
|
var VERSION = readPackageVersion();
|
|
7412
7604
|
function cachePath(homeDirOverride) {
|
|
7413
|
-
return
|
|
7605
|
+
return join10(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
|
|
7414
7606
|
}
|
|
7415
7607
|
function readCache(homeDirOverride) {
|
|
7416
7608
|
try {
|
|
7417
|
-
const raw =
|
|
7609
|
+
const raw = readFileSync12(cachePath(homeDirOverride), "utf8");
|
|
7418
7610
|
const parsed = JSON.parse(raw);
|
|
7419
7611
|
if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
|
|
7420
7612
|
return parsed;
|
|
@@ -7426,8 +7618,8 @@ function readCache(homeDirOverride) {
|
|
|
7426
7618
|
function writeCache(entry, homeDirOverride) {
|
|
7427
7619
|
try {
|
|
7428
7620
|
const p = cachePath(homeDirOverride);
|
|
7429
|
-
|
|
7430
|
-
|
|
7621
|
+
mkdirSync4(dirname5(p), { recursive: true });
|
|
7622
|
+
writeFileSync4(p, JSON.stringify(entry), "utf8");
|
|
7431
7623
|
} catch {
|
|
7432
7624
|
}
|
|
7433
7625
|
}
|
|
@@ -7653,9 +7845,12 @@ var McpClient = class {
|
|
|
7653
7845
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
7654
7846
|
}
|
|
7655
7847
|
});
|
|
7848
|
+
promise.catch(() => void 0);
|
|
7656
7849
|
try {
|
|
7657
|
-
await this.transport.send(frame);
|
|
7850
|
+
await Promise.race([this.transport.send(frame), promise.then(() => void 0)]);
|
|
7658
7851
|
} catch (err) {
|
|
7852
|
+
const pending = this.pending.get(id);
|
|
7853
|
+
if (pending) clearTimeout(pending.timeout);
|
|
7659
7854
|
this.pending.delete(id);
|
|
7660
7855
|
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
7661
7856
|
throw err;
|
|
@@ -8223,8 +8418,8 @@ async function trySection(load) {
|
|
|
8223
8418
|
}
|
|
8224
8419
|
|
|
8225
8420
|
// src/code/edit-blocks.ts
|
|
8226
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
8227
|
-
import { dirname as
|
|
8421
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync13, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
8422
|
+
import { dirname as dirname6, resolve as resolve9 } from "path";
|
|
8228
8423
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
8229
8424
|
function parseEditBlocks(text) {
|
|
8230
8425
|
const out = [];
|
|
@@ -8262,11 +8457,11 @@ function applyEditBlock(block, rootDir) {
|
|
|
8262
8457
|
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
8263
8458
|
};
|
|
8264
8459
|
}
|
|
8265
|
-
|
|
8266
|
-
|
|
8460
|
+
mkdirSync5(dirname6(absTarget), { recursive: true });
|
|
8461
|
+
writeFileSync5(absTarget, block.replace, "utf8");
|
|
8267
8462
|
return { path: block.path, status: "created" };
|
|
8268
8463
|
}
|
|
8269
|
-
const content =
|
|
8464
|
+
const content = readFileSync13(absTarget, "utf8");
|
|
8270
8465
|
if (searchEmpty) {
|
|
8271
8466
|
return {
|
|
8272
8467
|
path: block.path,
|
|
@@ -8286,7 +8481,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
8286
8481
|
};
|
|
8287
8482
|
}
|
|
8288
8483
|
const replaced = `${content.slice(0, idx)}${adaptedReplace}${content.slice(idx + adaptedSearch.length)}`;
|
|
8289
|
-
|
|
8484
|
+
writeFileSync5(absTarget, replaced, "utf8");
|
|
8290
8485
|
return { path: block.path, status: "applied" };
|
|
8291
8486
|
} catch (err) {
|
|
8292
8487
|
return { path: block.path, status: "error", message: err.message };
|
|
@@ -8308,7 +8503,7 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
8308
8503
|
continue;
|
|
8309
8504
|
}
|
|
8310
8505
|
try {
|
|
8311
|
-
snapshots.push({ path: b.path, prevContent:
|
|
8506
|
+
snapshots.push({ path: b.path, prevContent: readFileSync13(abs, "utf8") });
|
|
8312
8507
|
} catch {
|
|
8313
8508
|
snapshots.push({ path: b.path, prevContent: null });
|
|
8314
8509
|
}
|
|
@@ -8335,7 +8530,7 @@ function restoreSnapshots(snapshots, rootDir) {
|
|
|
8335
8530
|
message: "removed (the edit had created it)"
|
|
8336
8531
|
};
|
|
8337
8532
|
}
|
|
8338
|
-
|
|
8533
|
+
writeFileSync5(abs, snap.prevContent, "utf8");
|
|
8339
8534
|
return {
|
|
8340
8535
|
path: snap.path,
|
|
8341
8536
|
status: "applied",
|
|
@@ -8354,8 +8549,8 @@ function lineEndingOf(text) {
|
|
|
8354
8549
|
}
|
|
8355
8550
|
|
|
8356
8551
|
// src/code/prompt.ts
|
|
8357
|
-
import { existsSync as existsSync11, readFileSync as
|
|
8358
|
-
import { join as
|
|
8552
|
+
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
8553
|
+
import { join as join11 } from "path";
|
|
8359
8554
|
var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, edit_file, list_directory, directory_tree, search_files, search_content, get_file_info) rooted at the user's working directory, plus run_command / run_background for shell.
|
|
8360
8555
|
|
|
8361
8556
|
# Cite or shut up \u2014 non-negotiable
|
|
@@ -8557,12 +8752,12 @@ If \`semantic_search\` returns nothing useful (low scores, off-topic), THEN fall
|
|
|
8557
8752
|
function codeSystemPrompt(rootDir, opts = {}) {
|
|
8558
8753
|
const base = opts.hasSemanticSearch ? `${CODE_SYSTEM_PROMPT}${SEMANTIC_SEARCH_ROUTING}` : CODE_SYSTEM_PROMPT;
|
|
8559
8754
|
const withMemory = applyMemoryStack(base, rootDir);
|
|
8560
|
-
const gitignorePath =
|
|
8755
|
+
const gitignorePath = join11(rootDir, ".gitignore");
|
|
8561
8756
|
let result = withMemory;
|
|
8562
8757
|
if (existsSync11(gitignorePath)) {
|
|
8563
8758
|
let content;
|
|
8564
8759
|
try {
|
|
8565
|
-
content =
|
|
8760
|
+
content = readFileSync14(gitignorePath, "utf8");
|
|
8566
8761
|
} catch {
|
|
8567
8762
|
}
|
|
8568
8763
|
if (content !== void 0) {
|
|
@@ -8592,49 +8787,6 @@ ${appendParts.join("\n\n")}`;
|
|
|
8592
8787
|
return result;
|
|
8593
8788
|
}
|
|
8594
8789
|
|
|
8595
|
-
// src/config.ts
|
|
8596
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync5 } from "fs";
|
|
8597
|
-
import { homedir as homedir6 } from "os";
|
|
8598
|
-
import { dirname as dirname6, join as join11 } from "path";
|
|
8599
|
-
function defaultConfigPath() {
|
|
8600
|
-
return join11(homedir6(), ".reasonix", "config.json");
|
|
8601
|
-
}
|
|
8602
|
-
function readConfig(path2 = defaultConfigPath()) {
|
|
8603
|
-
try {
|
|
8604
|
-
const raw = readFileSync14(path2, "utf8");
|
|
8605
|
-
const parsed = JSON.parse(raw);
|
|
8606
|
-
if (parsed && typeof parsed === "object") return parsed;
|
|
8607
|
-
} catch {
|
|
8608
|
-
}
|
|
8609
|
-
return {};
|
|
8610
|
-
}
|
|
8611
|
-
function writeConfig(cfg, path2 = defaultConfigPath()) {
|
|
8612
|
-
mkdirSync5(dirname6(path2), { recursive: true });
|
|
8613
|
-
writeFileSync5(path2, JSON.stringify(cfg, null, 2), "utf8");
|
|
8614
|
-
try {
|
|
8615
|
-
chmodSync2(path2, 384);
|
|
8616
|
-
} catch {
|
|
8617
|
-
}
|
|
8618
|
-
}
|
|
8619
|
-
function loadApiKey(path2 = defaultConfigPath()) {
|
|
8620
|
-
if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
|
|
8621
|
-
return readConfig(path2).apiKey;
|
|
8622
|
-
}
|
|
8623
|
-
function saveApiKey(key, path2 = defaultConfigPath()) {
|
|
8624
|
-
const cfg = readConfig(path2);
|
|
8625
|
-
cfg.apiKey = key.trim();
|
|
8626
|
-
writeConfig(cfg, path2);
|
|
8627
|
-
}
|
|
8628
|
-
function isPlausibleKey(key) {
|
|
8629
|
-
const trimmed = key.trim();
|
|
8630
|
-
return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
|
|
8631
|
-
}
|
|
8632
|
-
function redactKey(key) {
|
|
8633
|
-
if (!key) return "";
|
|
8634
|
-
if (key.length <= 12) return "****";
|
|
8635
|
-
return `${key.slice(0, 6)}\u2026${key.slice(-4)}`;
|
|
8636
|
-
}
|
|
8637
|
-
|
|
8638
8790
|
// src/telemetry/usage.ts
|
|
8639
8791
|
import {
|
|
8640
8792
|
appendFileSync as appendFileSync2,
|