reasonix 0.3.0-alpha.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +981 -323
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +114 -3
- package/dist/index.js +326 -193
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -404,6 +404,201 @@ function resolveTemperatures(budget, custom) {
|
|
|
404
404
|
return out;
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
// src/repair/flatten.ts
|
|
408
|
+
function analyzeSchema(schema) {
|
|
409
|
+
if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
|
|
410
|
+
let leafCount = 0;
|
|
411
|
+
let maxDepth = 0;
|
|
412
|
+
walk(schema, 0, (depth, isLeaf) => {
|
|
413
|
+
if (isLeaf) leafCount++;
|
|
414
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
shouldFlatten: leafCount > 10 || maxDepth > 2,
|
|
418
|
+
leafCount,
|
|
419
|
+
maxDepth
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function flattenSchema(schema) {
|
|
423
|
+
const flatProps = {};
|
|
424
|
+
const required = [];
|
|
425
|
+
collect("", schema, flatProps, required, true);
|
|
426
|
+
return {
|
|
427
|
+
type: "object",
|
|
428
|
+
properties: flatProps,
|
|
429
|
+
required
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function nestArguments(flatArgs) {
|
|
433
|
+
const out = {};
|
|
434
|
+
for (const [key, value] of Object.entries(flatArgs)) {
|
|
435
|
+
setByPath(out, key.split("."), value);
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
function walk(schema, depth, visit) {
|
|
440
|
+
if (schema.type === "object" && schema.properties) {
|
|
441
|
+
for (const child of Object.values(schema.properties)) {
|
|
442
|
+
walk(child, depth + 1, visit);
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (schema.type === "array" && schema.items) {
|
|
447
|
+
walk(schema.items, depth + 1, visit);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
visit(depth, true);
|
|
451
|
+
}
|
|
452
|
+
function collect(prefix, schema, out, required, isRootRequired) {
|
|
453
|
+
if (schema.type === "object" && schema.properties) {
|
|
454
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
455
|
+
for (const [key, child] of Object.entries(schema.properties)) {
|
|
456
|
+
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
457
|
+
const childRequired = isRootRequired && requiredSet.has(key);
|
|
458
|
+
collect(nextPrefix, child, out, required, childRequired);
|
|
459
|
+
}
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
out[prefix] = schema;
|
|
463
|
+
if (isRootRequired) required.push(prefix);
|
|
464
|
+
}
|
|
465
|
+
function setByPath(target, path, value) {
|
|
466
|
+
let cur = target;
|
|
467
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
468
|
+
const key = path[i];
|
|
469
|
+
if (typeof cur[key] !== "object" || cur[key] === null) cur[key] = {};
|
|
470
|
+
cur = cur[key];
|
|
471
|
+
}
|
|
472
|
+
cur[path[path.length - 1]] = value;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/tools.ts
|
|
476
|
+
var ToolRegistry = class {
|
|
477
|
+
_tools = /* @__PURE__ */ new Map();
|
|
478
|
+
_autoFlatten;
|
|
479
|
+
constructor(opts = {}) {
|
|
480
|
+
this._autoFlatten = opts.autoFlatten !== false;
|
|
481
|
+
}
|
|
482
|
+
register(def) {
|
|
483
|
+
if (!def.name) throw new Error("tool requires a name");
|
|
484
|
+
const internal = { ...def };
|
|
485
|
+
if (this._autoFlatten && def.parameters) {
|
|
486
|
+
const decision = analyzeSchema(def.parameters);
|
|
487
|
+
if (decision.shouldFlatten) {
|
|
488
|
+
internal.flatSchema = flattenSchema(def.parameters);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
this._tools.set(def.name, internal);
|
|
492
|
+
return this;
|
|
493
|
+
}
|
|
494
|
+
has(name) {
|
|
495
|
+
return this._tools.has(name);
|
|
496
|
+
}
|
|
497
|
+
get(name) {
|
|
498
|
+
return this._tools.get(name);
|
|
499
|
+
}
|
|
500
|
+
get size() {
|
|
501
|
+
return this._tools.size;
|
|
502
|
+
}
|
|
503
|
+
/** True if a registered tool's schema was flattened for the model. */
|
|
504
|
+
wasFlattened(name) {
|
|
505
|
+
return Boolean(this._tools.get(name)?.flatSchema);
|
|
506
|
+
}
|
|
507
|
+
specs() {
|
|
508
|
+
return [...this._tools.values()].map((t) => ({
|
|
509
|
+
type: "function",
|
|
510
|
+
function: {
|
|
511
|
+
name: t.name,
|
|
512
|
+
description: t.description ?? "",
|
|
513
|
+
parameters: t.flatSchema ?? t.parameters ?? { type: "object", properties: {} }
|
|
514
|
+
}
|
|
515
|
+
}));
|
|
516
|
+
}
|
|
517
|
+
async dispatch(name, argumentsRaw) {
|
|
518
|
+
const tool = this._tools.get(name);
|
|
519
|
+
if (!tool) {
|
|
520
|
+
return JSON.stringify({ error: `unknown tool: ${name}` });
|
|
521
|
+
}
|
|
522
|
+
let args;
|
|
523
|
+
try {
|
|
524
|
+
args = typeof argumentsRaw === "string" ? argumentsRaw.trim() ? JSON.parse(argumentsRaw) ?? {} : {} : argumentsRaw ?? {};
|
|
525
|
+
} catch (err) {
|
|
526
|
+
return JSON.stringify({
|
|
527
|
+
error: `invalid tool arguments JSON: ${err.message}`
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
if (tool.flatSchema && args && typeof args === "object" && hasDotKey(args)) {
|
|
531
|
+
args = nestArguments(args);
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
const result = await tool.fn(args);
|
|
535
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
536
|
+
} catch (err) {
|
|
537
|
+
return JSON.stringify({
|
|
538
|
+
error: `${err.name}: ${err.message}`
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
function hasDotKey(obj) {
|
|
544
|
+
for (const k of Object.keys(obj)) {
|
|
545
|
+
if (k.includes(".")) return true;
|
|
546
|
+
}
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/mcp/registry.ts
|
|
551
|
+
var DEFAULT_MAX_RESULT_CHARS = 32e3;
|
|
552
|
+
async function bridgeMcpTools(client, opts = {}) {
|
|
553
|
+
const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
|
|
554
|
+
const prefix = opts.namePrefix ?? "";
|
|
555
|
+
const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS;
|
|
556
|
+
const result = { registry, registeredNames: [], skipped: [] };
|
|
557
|
+
const listed = await client.listTools();
|
|
558
|
+
for (const mcpTool of listed.tools) {
|
|
559
|
+
if (!mcpTool.name) {
|
|
560
|
+
result.skipped.push({ name: "?", reason: "empty tool name" });
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const registeredName = `${prefix}${mcpTool.name}`;
|
|
564
|
+
registry.register({
|
|
565
|
+
name: registeredName,
|
|
566
|
+
description: mcpTool.description ?? "",
|
|
567
|
+
parameters: mcpTool.inputSchema,
|
|
568
|
+
fn: async (args) => {
|
|
569
|
+
const toolResult = await client.callTool(mcpTool.name, args);
|
|
570
|
+
return flattenMcpResult(toolResult, { maxChars: maxResultChars });
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
result.registeredNames.push(registeredName);
|
|
574
|
+
}
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
577
|
+
function flattenMcpResult(result, opts = {}) {
|
|
578
|
+
const parts = result.content.map(blockToString);
|
|
579
|
+
const joined = parts.join("\n").trim();
|
|
580
|
+
const prefixed = result.isError ? `ERROR: ${joined || "(no error message from server)"}` : joined;
|
|
581
|
+
return opts.maxChars ? truncateForModel(prefixed, opts.maxChars) : prefixed;
|
|
582
|
+
}
|
|
583
|
+
function truncateForModel(s, maxChars) {
|
|
584
|
+
if (s.length <= maxChars) return s;
|
|
585
|
+
const tailBudget = Math.min(1024, Math.floor(maxChars * 0.1));
|
|
586
|
+
const headBudget = Math.max(0, maxChars - tailBudget);
|
|
587
|
+
const head = s.slice(0, headBudget);
|
|
588
|
+
const tail = s.slice(-tailBudget);
|
|
589
|
+
const dropped = s.length - head.length - tail.length;
|
|
590
|
+
return `${head}
|
|
591
|
+
|
|
592
|
+
[\u2026truncated ${dropped} chars \u2014 raise BridgeOptions.maxResultChars, or call the tool with a narrower scope (filter, head, pagination)\u2026]
|
|
593
|
+
|
|
594
|
+
${tail}`;
|
|
595
|
+
}
|
|
596
|
+
function blockToString(block) {
|
|
597
|
+
if (block.type === "text") return block.text;
|
|
598
|
+
if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
|
|
599
|
+
return `[unknown block: ${JSON.stringify(block)}]`;
|
|
600
|
+
}
|
|
601
|
+
|
|
407
602
|
// src/memory.ts
|
|
408
603
|
import { createHash } from "crypto";
|
|
409
604
|
var ImmutablePrefix = class {
|
|
@@ -441,6 +636,16 @@ var AppendOnlyLog = class {
|
|
|
441
636
|
extend(messages) {
|
|
442
637
|
for (const m of messages) this.append(m);
|
|
443
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
|
+
}
|
|
444
649
|
get entries() {
|
|
445
650
|
return this._entries;
|
|
446
651
|
}
|
|
@@ -655,74 +860,6 @@ function repairTruncatedJson(input) {
|
|
|
655
860
|
}
|
|
656
861
|
}
|
|
657
862
|
|
|
658
|
-
// src/repair/flatten.ts
|
|
659
|
-
function analyzeSchema(schema) {
|
|
660
|
-
if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
|
|
661
|
-
let leafCount = 0;
|
|
662
|
-
let maxDepth = 0;
|
|
663
|
-
walk(schema, 0, (depth, isLeaf) => {
|
|
664
|
-
if (isLeaf) leafCount++;
|
|
665
|
-
if (depth > maxDepth) maxDepth = depth;
|
|
666
|
-
});
|
|
667
|
-
return {
|
|
668
|
-
shouldFlatten: leafCount > 10 || maxDepth > 2,
|
|
669
|
-
leafCount,
|
|
670
|
-
maxDepth
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
function flattenSchema(schema) {
|
|
674
|
-
const flatProps = {};
|
|
675
|
-
const required = [];
|
|
676
|
-
collect("", schema, flatProps, required, true);
|
|
677
|
-
return {
|
|
678
|
-
type: "object",
|
|
679
|
-
properties: flatProps,
|
|
680
|
-
required
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
function nestArguments(flatArgs) {
|
|
684
|
-
const out = {};
|
|
685
|
-
for (const [key, value] of Object.entries(flatArgs)) {
|
|
686
|
-
setByPath(out, key.split("."), value);
|
|
687
|
-
}
|
|
688
|
-
return out;
|
|
689
|
-
}
|
|
690
|
-
function walk(schema, depth, visit) {
|
|
691
|
-
if (schema.type === "object" && schema.properties) {
|
|
692
|
-
for (const child of Object.values(schema.properties)) {
|
|
693
|
-
walk(child, depth + 1, visit);
|
|
694
|
-
}
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
if (schema.type === "array" && schema.items) {
|
|
698
|
-
walk(schema.items, depth + 1, visit);
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
visit(depth, true);
|
|
702
|
-
}
|
|
703
|
-
function collect(prefix, schema, out, required, isRootRequired) {
|
|
704
|
-
if (schema.type === "object" && schema.properties) {
|
|
705
|
-
const requiredSet = new Set(schema.required ?? []);
|
|
706
|
-
for (const [key, child] of Object.entries(schema.properties)) {
|
|
707
|
-
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
708
|
-
const childRequired = isRootRequired && requiredSet.has(key);
|
|
709
|
-
collect(nextPrefix, child, out, required, childRequired);
|
|
710
|
-
}
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
out[prefix] = schema;
|
|
714
|
-
if (isRootRequired) required.push(prefix);
|
|
715
|
-
}
|
|
716
|
-
function setByPath(target, path, value) {
|
|
717
|
-
let cur = target;
|
|
718
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
719
|
-
const key = path[i];
|
|
720
|
-
if (typeof cur[key] !== "object" || cur[key] === null) cur[key] = {};
|
|
721
|
-
cur = cur[key];
|
|
722
|
-
}
|
|
723
|
-
cur[path[path.length - 1]] = value;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
863
|
// src/repair/index.ts
|
|
727
864
|
var ToolCallRepair = class {
|
|
728
865
|
storm;
|
|
@@ -787,7 +924,8 @@ import {
|
|
|
787
924
|
readFileSync,
|
|
788
925
|
readdirSync,
|
|
789
926
|
statSync,
|
|
790
|
-
unlinkSync
|
|
927
|
+
unlinkSync,
|
|
928
|
+
writeFileSync
|
|
791
929
|
} from "fs";
|
|
792
930
|
import { homedir } from "os";
|
|
793
931
|
import { dirname, join } from "path";
|
|
@@ -856,6 +994,17 @@ function deleteSession(name) {
|
|
|
856
994
|
return false;
|
|
857
995
|
}
|
|
858
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
|
+
}
|
|
859
1008
|
function countLines(path) {
|
|
860
1009
|
try {
|
|
861
1010
|
const raw = readFileSync(path, "utf8");
|
|
@@ -914,12 +1063,14 @@ var SessionStats = class {
|
|
|
914
1063
|
return denom > 0 ? hit / denom : 0;
|
|
915
1064
|
}
|
|
916
1065
|
summary() {
|
|
1066
|
+
const last = this.turns[this.turns.length - 1];
|
|
917
1067
|
return {
|
|
918
1068
|
turns: this.turns.length,
|
|
919
1069
|
totalCostUsd: round(this.totalCost, 6),
|
|
920
1070
|
claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
|
|
921
1071
|
savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
|
|
922
|
-
cacheHitRatio: round(this.aggregateCacheHitRatio, 4)
|
|
1072
|
+
cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
|
|
1073
|
+
lastPromptTokens: last?.usage.promptTokens ?? 0
|
|
923
1074
|
};
|
|
924
1075
|
}
|
|
925
1076
|
};
|
|
@@ -928,81 +1079,6 @@ function round(n, digits) {
|
|
|
928
1079
|
return Math.round(n * f) / f;
|
|
929
1080
|
}
|
|
930
1081
|
|
|
931
|
-
// src/tools.ts
|
|
932
|
-
var ToolRegistry = class {
|
|
933
|
-
_tools = /* @__PURE__ */ new Map();
|
|
934
|
-
_autoFlatten;
|
|
935
|
-
constructor(opts = {}) {
|
|
936
|
-
this._autoFlatten = opts.autoFlatten !== false;
|
|
937
|
-
}
|
|
938
|
-
register(def) {
|
|
939
|
-
if (!def.name) throw new Error("tool requires a name");
|
|
940
|
-
const internal = { ...def };
|
|
941
|
-
if (this._autoFlatten && def.parameters) {
|
|
942
|
-
const decision = analyzeSchema(def.parameters);
|
|
943
|
-
if (decision.shouldFlatten) {
|
|
944
|
-
internal.flatSchema = flattenSchema(def.parameters);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
this._tools.set(def.name, internal);
|
|
948
|
-
return this;
|
|
949
|
-
}
|
|
950
|
-
has(name) {
|
|
951
|
-
return this._tools.has(name);
|
|
952
|
-
}
|
|
953
|
-
get(name) {
|
|
954
|
-
return this._tools.get(name);
|
|
955
|
-
}
|
|
956
|
-
get size() {
|
|
957
|
-
return this._tools.size;
|
|
958
|
-
}
|
|
959
|
-
/** True if a registered tool's schema was flattened for the model. */
|
|
960
|
-
wasFlattened(name) {
|
|
961
|
-
return Boolean(this._tools.get(name)?.flatSchema);
|
|
962
|
-
}
|
|
963
|
-
specs() {
|
|
964
|
-
return [...this._tools.values()].map((t) => ({
|
|
965
|
-
type: "function",
|
|
966
|
-
function: {
|
|
967
|
-
name: t.name,
|
|
968
|
-
description: t.description ?? "",
|
|
969
|
-
parameters: t.flatSchema ?? t.parameters ?? { type: "object", properties: {} }
|
|
970
|
-
}
|
|
971
|
-
}));
|
|
972
|
-
}
|
|
973
|
-
async dispatch(name, argumentsRaw) {
|
|
974
|
-
const tool = this._tools.get(name);
|
|
975
|
-
if (!tool) {
|
|
976
|
-
return JSON.stringify({ error: `unknown tool: ${name}` });
|
|
977
|
-
}
|
|
978
|
-
let args;
|
|
979
|
-
try {
|
|
980
|
-
args = typeof argumentsRaw === "string" ? argumentsRaw.trim() ? JSON.parse(argumentsRaw) ?? {} : {} : argumentsRaw ?? {};
|
|
981
|
-
} catch (err) {
|
|
982
|
-
return JSON.stringify({
|
|
983
|
-
error: `invalid tool arguments JSON: ${err.message}`
|
|
984
|
-
});
|
|
985
|
-
}
|
|
986
|
-
if (tool.flatSchema && args && typeof args === "object" && hasDotKey(args)) {
|
|
987
|
-
args = nestArguments(args);
|
|
988
|
-
}
|
|
989
|
-
try {
|
|
990
|
-
const result = await tool.fn(args);
|
|
991
|
-
return typeof result === "string" ? result : JSON.stringify(result);
|
|
992
|
-
} catch (err) {
|
|
993
|
-
return JSON.stringify({
|
|
994
|
-
error: `${err.name}: ${err.message}`
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
};
|
|
999
|
-
function hasDotKey(obj) {
|
|
1000
|
-
for (const k of Object.keys(obj)) {
|
|
1001
|
-
if (k.includes(".")) return true;
|
|
1002
|
-
}
|
|
1003
|
-
return false;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
1082
|
// src/loop.ts
|
|
1007
1083
|
var CacheFirstLoop = class {
|
|
1008
1084
|
client;
|
|
@@ -1031,7 +1107,7 @@ var CacheFirstLoop = class {
|
|
|
1031
1107
|
this.prefix = opts.prefix;
|
|
1032
1108
|
this.tools = opts.tools ?? new ToolRegistry();
|
|
1033
1109
|
this.model = opts.model ?? "deepseek-chat";
|
|
1034
|
-
this.maxToolIters = opts.maxToolIters ??
|
|
1110
|
+
this.maxToolIters = opts.maxToolIters ?? 24;
|
|
1035
1111
|
if (typeof opts.branch === "number") {
|
|
1036
1112
|
this.branchOptions = { budget: opts.branch };
|
|
1037
1113
|
} else if (opts.branch && typeof opts.branch === "object") {
|
|
@@ -1050,12 +1126,49 @@ var CacheFirstLoop = class {
|
|
|
1050
1126
|
this.sessionName = opts.session ?? null;
|
|
1051
1127
|
if (this.sessionName) {
|
|
1052
1128
|
const prior = loadSessionMessages(this.sessionName);
|
|
1053
|
-
|
|
1054
|
-
|
|
1129
|
+
const { messages, healedCount, healedFrom } = healLoadedMessages(
|
|
1130
|
+
prior,
|
|
1131
|
+
DEFAULT_MAX_RESULT_CHARS
|
|
1132
|
+
);
|
|
1133
|
+
for (const msg of messages) this.log.append(msg);
|
|
1134
|
+
this.resumedMessageCount = messages.length;
|
|
1135
|
+
if (healedCount > 0) {
|
|
1136
|
+
process.stderr.write(
|
|
1137
|
+
`\u25B8 session "${this.sessionName}": healed ${healedCount} oversized tool result(s) (was ${healedFrom.toLocaleString()} chars total). Old payloads were truncated to fit DeepSeek's context window; the conversation is preserved.
|
|
1138
|
+
`
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1055
1141
|
} else {
|
|
1056
1142
|
this.resumedMessageCount = 0;
|
|
1057
1143
|
}
|
|
1058
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
|
+
}
|
|
1059
1172
|
appendAndPersist(message) {
|
|
1060
1173
|
this.log.append(message);
|
|
1061
1174
|
if (this.sessionName) {
|
|
@@ -1245,7 +1358,7 @@ var CacheFirstLoop = class {
|
|
|
1245
1358
|
turn: this._turn,
|
|
1246
1359
|
role: "error",
|
|
1247
1360
|
content: "",
|
|
1248
|
-
error: err
|
|
1361
|
+
error: formatLoopError(err)
|
|
1249
1362
|
};
|
|
1250
1363
|
return;
|
|
1251
1364
|
}
|
|
@@ -1293,7 +1406,38 @@ var CacheFirstLoop = class {
|
|
|
1293
1406
|
};
|
|
1294
1407
|
}
|
|
1295
1408
|
}
|
|
1296
|
-
yield
|
|
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
|
+
}
|
|
1297
1441
|
}
|
|
1298
1442
|
async run(userInput, onEvent) {
|
|
1299
1443
|
let final = "";
|
|
@@ -1318,6 +1462,28 @@ function summarizeBranch(chosen, samples) {
|
|
|
1318
1462
|
temperatures: samples.map((s) => s.temperature)
|
|
1319
1463
|
};
|
|
1320
1464
|
}
|
|
1465
|
+
function healLoadedMessages(messages, maxChars) {
|
|
1466
|
+
let healedCount = 0;
|
|
1467
|
+
let healedFrom = 0;
|
|
1468
|
+
const out = messages.map((msg) => {
|
|
1469
|
+
if (msg.role !== "tool") return msg;
|
|
1470
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
1471
|
+
if (content.length <= maxChars) return msg;
|
|
1472
|
+
healedCount += 1;
|
|
1473
|
+
healedFrom += content.length;
|
|
1474
|
+
return { ...msg, content: truncateForModel(content, maxChars) };
|
|
1475
|
+
});
|
|
1476
|
+
return { messages: out, healedCount, healedFrom };
|
|
1477
|
+
}
|
|
1478
|
+
function formatLoopError(err) {
|
|
1479
|
+
const msg = err.message ?? "";
|
|
1480
|
+
if (msg.includes("maximum context length")) {
|
|
1481
|
+
const reqMatch = msg.match(/requested\s+(\d+)\s+tokens/);
|
|
1482
|
+
const requested = reqMatch ? `${Number(reqMatch[1]).toLocaleString()} tokens` : "too many tokens";
|
|
1483
|
+
return `Context overflow (DeepSeek 400): session history is ${requested}, past the 131,072-token limit. Usually this means a single tool call returned a huge payload. v0.3.0-alpha.6+ caps new tool results at 32k chars, AND auto-heals oversized history on session load \u2014 restart Reasonix and this session should come back trimmed. If it still overflows, run /forget (delete the session) or /clear (drop the displayed history) to start fresh.`;
|
|
1484
|
+
}
|
|
1485
|
+
return msg;
|
|
1486
|
+
}
|
|
1321
1487
|
|
|
1322
1488
|
// src/env.ts
|
|
1323
1489
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -1494,12 +1660,14 @@ function summarizeTurns(turns) {
|
|
|
1494
1660
|
}
|
|
1495
1661
|
const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
|
|
1496
1662
|
const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
|
|
1663
|
+
const lastTurn = turns[turns.length - 1];
|
|
1497
1664
|
return {
|
|
1498
1665
|
turns: turns.length,
|
|
1499
1666
|
totalCostUsd: round2(totalCost, 6),
|
|
1500
1667
|
claudeEquivalentUsd: round2(totalClaude, 6),
|
|
1501
1668
|
savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
|
|
1502
|
-
cacheHitRatio: round2(cacheHitRatio, 4)
|
|
1669
|
+
cacheHitRatio: round2(cacheHitRatio, 4),
|
|
1670
|
+
lastPromptTokens: lastTurn?.usage.promptTokens ?? 0
|
|
1503
1671
|
};
|
|
1504
1672
|
}
|
|
1505
1673
|
function round2(n, digits) {
|
|
@@ -2206,45 +2374,6 @@ var SseTransport = class {
|
|
|
2206
2374
|
}
|
|
2207
2375
|
};
|
|
2208
2376
|
|
|
2209
|
-
// src/mcp/registry.ts
|
|
2210
|
-
async function bridgeMcpTools(client, opts = {}) {
|
|
2211
|
-
const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
|
|
2212
|
-
const prefix = opts.namePrefix ?? "";
|
|
2213
|
-
const result = { registry, registeredNames: [], skipped: [] };
|
|
2214
|
-
const listed = await client.listTools();
|
|
2215
|
-
for (const mcpTool of listed.tools) {
|
|
2216
|
-
if (!mcpTool.name) {
|
|
2217
|
-
result.skipped.push({ name: "?", reason: "empty tool name" });
|
|
2218
|
-
continue;
|
|
2219
|
-
}
|
|
2220
|
-
const registeredName = `${prefix}${mcpTool.name}`;
|
|
2221
|
-
registry.register({
|
|
2222
|
-
name: registeredName,
|
|
2223
|
-
description: mcpTool.description ?? "",
|
|
2224
|
-
parameters: mcpTool.inputSchema,
|
|
2225
|
-
fn: async (args) => {
|
|
2226
|
-
const toolResult = await client.callTool(mcpTool.name, args);
|
|
2227
|
-
return flattenMcpResult(toolResult);
|
|
2228
|
-
}
|
|
2229
|
-
});
|
|
2230
|
-
result.registeredNames.push(registeredName);
|
|
2231
|
-
}
|
|
2232
|
-
return result;
|
|
2233
|
-
}
|
|
2234
|
-
function flattenMcpResult(result) {
|
|
2235
|
-
const parts = result.content.map(blockToString);
|
|
2236
|
-
const joined = parts.join("\n").trim();
|
|
2237
|
-
if (result.isError) {
|
|
2238
|
-
return `ERROR: ${joined || "(no error message from server)"}`;
|
|
2239
|
-
}
|
|
2240
|
-
return joined;
|
|
2241
|
-
}
|
|
2242
|
-
function blockToString(block) {
|
|
2243
|
-
if (block.type === "text") return block.text;
|
|
2244
|
-
if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
|
|
2245
|
-
return `[unknown block: ${JSON.stringify(block)}]`;
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
2377
|
// src/mcp/shell-split.ts
|
|
2249
2378
|
function shellSplit(input) {
|
|
2250
2379
|
const tokens = [];
|
|
@@ -2320,7 +2449,7 @@ function parseMcpSpec(input) {
|
|
|
2320
2449
|
}
|
|
2321
2450
|
|
|
2322
2451
|
// src/config.ts
|
|
2323
|
-
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";
|
|
2324
2453
|
import { homedir as homedir2 } from "os";
|
|
2325
2454
|
import { dirname as dirname2, join as join2 } from "path";
|
|
2326
2455
|
function defaultConfigPath() {
|
|
@@ -2337,7 +2466,7 @@ function readConfig(path = defaultConfigPath()) {
|
|
|
2337
2466
|
}
|
|
2338
2467
|
function writeConfig(cfg, path = defaultConfigPath()) {
|
|
2339
2468
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
2340
|
-
|
|
2469
|
+
writeFileSync2(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
2341
2470
|
try {
|
|
2342
2471
|
chmodSync2(path, 384);
|
|
2343
2472
|
} catch {
|
|
@@ -2363,10 +2492,11 @@ function redactKey(key) {
|
|
|
2363
2492
|
}
|
|
2364
2493
|
|
|
2365
2494
|
// src/index.ts
|
|
2366
|
-
var VERSION = "0.3.
|
|
2495
|
+
var VERSION = "0.3.1";
|
|
2367
2496
|
export {
|
|
2368
2497
|
AppendOnlyLog,
|
|
2369
2498
|
CacheFirstLoop,
|
|
2499
|
+
DEFAULT_MAX_RESULT_CHARS,
|
|
2370
2500
|
DeepSeekClient,
|
|
2371
2501
|
ImmutablePrefix,
|
|
2372
2502
|
MCP_PROTOCOL_VERSION,
|
|
@@ -2395,7 +2525,9 @@ export {
|
|
|
2395
2525
|
fetchWithRetry,
|
|
2396
2526
|
flattenMcpResult,
|
|
2397
2527
|
flattenSchema,
|
|
2528
|
+
formatLoopError,
|
|
2398
2529
|
harvest,
|
|
2530
|
+
healLoadedMessages,
|
|
2399
2531
|
isJsonRpcError,
|
|
2400
2532
|
isPlanStateEmpty,
|
|
2401
2533
|
isPlausibleKey,
|
|
@@ -2422,6 +2554,7 @@ export {
|
|
|
2422
2554
|
sessionPath,
|
|
2423
2555
|
sessionsDir,
|
|
2424
2556
|
similarity,
|
|
2557
|
+
truncateForModel,
|
|
2425
2558
|
writeConfig,
|
|
2426
2559
|
writeMeta,
|
|
2427
2560
|
writeRecord
|