reasonix 0.27.1 → 0.27.3
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 +2054 -1469
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +166 -139
- package/dist/index.js +1203 -915
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -116,7 +116,9 @@ var DeepSeekClient = class {
|
|
|
116
116
|
);
|
|
117
117
|
}
|
|
118
118
|
this.apiKey = apiKey;
|
|
119
|
-
|
|
119
|
+
let url = opts.baseUrl ?? process.env.DEEPSEEK_BASE_URL ?? "https://api.deepseek.com";
|
|
120
|
+
while (url.endsWith("/")) url = url.slice(0, -1);
|
|
121
|
+
this.baseUrl = url;
|
|
120
122
|
this.timeoutMs = opts.timeoutMs ?? 66e4;
|
|
121
123
|
this._fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
122
124
|
this.retry = opts.retry ?? {};
|
|
@@ -456,6 +458,7 @@ var PauseGate = class {
|
|
|
456
458
|
_nextId = 0;
|
|
457
459
|
_pending = /* @__PURE__ */ new Map();
|
|
458
460
|
_listeners = /* @__PURE__ */ new Set();
|
|
461
|
+
_auditListener = null;
|
|
459
462
|
/** Block until the user responds. Takes a named options object so the
|
|
460
463
|
* kind and payload fields don't get confused at the call site. */
|
|
461
464
|
ask(opts) {
|
|
@@ -482,8 +485,12 @@ var PauseGate = class {
|
|
|
482
485
|
const p = this._pending.get(id);
|
|
483
486
|
if (!p) return;
|
|
484
487
|
this._pending.delete(id);
|
|
488
|
+
this.emitAuditEvent(p.request, data);
|
|
485
489
|
p.resolve(data);
|
|
486
490
|
}
|
|
491
|
+
setAuditListener(fn) {
|
|
492
|
+
this._auditListener = fn;
|
|
493
|
+
}
|
|
487
494
|
/** Subscribe to new pause requests. Returns an unsubscribe function. */
|
|
488
495
|
on(fn) {
|
|
489
496
|
this._listeners.add(fn);
|
|
@@ -496,6 +503,43 @@ var PauseGate = class {
|
|
|
496
503
|
for (const [, p] of this._pending) return p.request;
|
|
497
504
|
return null;
|
|
498
505
|
}
|
|
506
|
+
emitAuditEvent(request, data) {
|
|
507
|
+
if (!this._auditListener) return;
|
|
508
|
+
if (request.kind !== "run_command" && request.kind !== "run_background") return;
|
|
509
|
+
if (!data || typeof data !== "object") return;
|
|
510
|
+
const choice = data;
|
|
511
|
+
try {
|
|
512
|
+
switch (choice.type) {
|
|
513
|
+
case "run_once":
|
|
514
|
+
this._auditListener({
|
|
515
|
+
type: "tool.confirm.allow",
|
|
516
|
+
kind: request.kind,
|
|
517
|
+
payload: request.payload
|
|
518
|
+
});
|
|
519
|
+
break;
|
|
520
|
+
case "deny":
|
|
521
|
+
this._auditListener({
|
|
522
|
+
type: "tool.confirm.deny",
|
|
523
|
+
kind: request.kind,
|
|
524
|
+
payload: request.payload,
|
|
525
|
+
denyContext: choice.denyContext
|
|
526
|
+
});
|
|
527
|
+
break;
|
|
528
|
+
case "always_allow":
|
|
529
|
+
if (typeof choice.prefix !== "string") return;
|
|
530
|
+
this._auditListener({
|
|
531
|
+
type: "tool.confirm.always_allow",
|
|
532
|
+
kind: request.kind,
|
|
533
|
+
payload: request.payload,
|
|
534
|
+
prefix: choice.prefix
|
|
535
|
+
});
|
|
536
|
+
break;
|
|
537
|
+
default:
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
} catch {
|
|
541
|
+
}
|
|
542
|
+
}
|
|
499
543
|
};
|
|
500
544
|
var pauseGate = new PauseGate();
|
|
501
545
|
|
|
@@ -954,6 +998,7 @@ var ToolRegistry = class {
|
|
|
954
998
|
_autoFlatten;
|
|
955
999
|
_planMode = false;
|
|
956
1000
|
_interceptor = null;
|
|
1001
|
+
_auditListener = null;
|
|
957
1002
|
constructor(opts = {}) {
|
|
958
1003
|
this._autoFlatten = opts.autoFlatten !== false;
|
|
959
1004
|
}
|
|
@@ -969,6 +1014,9 @@ var ToolRegistry = class {
|
|
|
969
1014
|
setToolInterceptor(fn) {
|
|
970
1015
|
this._interceptor = fn;
|
|
971
1016
|
}
|
|
1017
|
+
setAuditListener(fn) {
|
|
1018
|
+
this._auditListener = fn;
|
|
1019
|
+
}
|
|
972
1020
|
register(def) {
|
|
973
1021
|
if (!def.name) throw new Error("tool requires a name");
|
|
974
1022
|
const internal = { ...def };
|
|
@@ -1041,6 +1089,10 @@ var ToolRegistry = class {
|
|
|
1041
1089
|
}
|
|
1042
1090
|
}
|
|
1043
1091
|
try {
|
|
1092
|
+
try {
|
|
1093
|
+
this._auditListener?.({ name, args });
|
|
1094
|
+
} catch {
|
|
1095
|
+
}
|
|
1044
1096
|
const result = await tool.fn(args, {
|
|
1045
1097
|
signal: opts.signal,
|
|
1046
1098
|
confirmationGate: opts.confirmationGate
|
|
@@ -1618,169 +1670,513 @@ var ContextManager = class {
|
|
|
1618
1670
|
}
|
|
1619
1671
|
};
|
|
1620
1672
|
|
|
1621
|
-
// src/
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
toMessages() {
|
|
1639
|
-
return [{ role: "system", content: this.system }, ...this.fewShots.map((m) => ({ ...m }))];
|
|
1673
|
+
// src/loop/branch.ts
|
|
1674
|
+
function summarizeBranch(chosen, samples) {
|
|
1675
|
+
return {
|
|
1676
|
+
budget: samples.length,
|
|
1677
|
+
chosenIndex: chosen.index,
|
|
1678
|
+
uncertainties: samples.map((s) => s.planState.uncertainties.length),
|
|
1679
|
+
temperatures: samples.map((s) => s.temperature)
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// src/loop/errors.ts
|
|
1684
|
+
function formatLoopError(err) {
|
|
1685
|
+
const msg = err.message ?? "";
|
|
1686
|
+
if (msg.includes("maximum context length")) {
|
|
1687
|
+
const reqMatch = msg.match(/requested\s+(\d+)\s+tokens/);
|
|
1688
|
+
const requested = reqMatch ? `${Number(reqMatch[1]).toLocaleString()} tokens` : "too many tokens";
|
|
1689
|
+
return `Context overflow (DeepSeek 400): session history is ${requested}, past the model's prompt limit (V4: 1M tokens; legacy chat/reasoner: 131k). Usually a single tool result grew too big. Reasonix caps new tool results at 8k tokens and auto-heals oversized history on session load \u2014 a restart often clears it. If it still overflows, run /forget (delete the session) or /clear (drop the displayed history) to start fresh.`;
|
|
1640
1690
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1691
|
+
const m = /^DeepSeek (\d{3}):\s*([\s\S]*)$/.exec(msg);
|
|
1692
|
+
if (!m) return msg;
|
|
1693
|
+
const status = m[1] ?? "";
|
|
1694
|
+
const body = m[2] ?? "";
|
|
1695
|
+
const inner = extractDeepSeekErrorMessage(body);
|
|
1696
|
+
if (status === "401") {
|
|
1697
|
+
return `Authentication failed (DeepSeek 401): ${inner}. Your API key is rejected. Fix with \`reasonix setup\` or \`export DEEPSEEK_API_KEY=sk-...\`. Get one at https://platform.deepseek.com/api_keys.`;
|
|
1643
1698
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
if (!name) return false;
|
|
1647
|
-
if (this._toolSpecs.some((t) => t.function?.name === name)) return false;
|
|
1648
|
-
this._toolSpecs.push(spec);
|
|
1649
|
-
this._fingerprintCache = null;
|
|
1650
|
-
return true;
|
|
1699
|
+
if (status === "402") {
|
|
1700
|
+
return `Out of balance (DeepSeek 402): ${inner}. Top up at https://platform.deepseek.com/top_up \u2014 the panel header shows your balance once it's non-zero.`;
|
|
1651
1701
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
const idx = this._toolSpecs.findIndex((t) => t.function?.name === name);
|
|
1655
|
-
if (idx < 0) return false;
|
|
1656
|
-
this._toolSpecs.splice(idx, 1);
|
|
1657
|
-
this._fingerprintCache = null;
|
|
1658
|
-
return true;
|
|
1702
|
+
if (status === "422") {
|
|
1703
|
+
return `Invalid parameter (DeepSeek 422): ${inner}`;
|
|
1659
1704
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
this._fingerprintCache = this.computeFingerprint();
|
|
1663
|
-
return this._fingerprintCache;
|
|
1705
|
+
if (status === "400") {
|
|
1706
|
+
return `Bad request (DeepSeek 400): ${inner}`;
|
|
1664
1707
|
}
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
);
|
|
1672
|
-
}
|
|
1673
|
-
this._fingerprintCache = fresh;
|
|
1674
|
-
return fresh;
|
|
1708
|
+
return msg;
|
|
1709
|
+
}
|
|
1710
|
+
function reasonPrefixFor(reason, iterCap) {
|
|
1711
|
+
if (reason === "aborted") return "[aborted by user (Esc) \u2014 summarizing what I found so far]";
|
|
1712
|
+
if (reason === "context-guard") {
|
|
1713
|
+
return "[context budget running low \u2014 summarizing before the next call would overflow]";
|
|
1675
1714
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
system: this.system,
|
|
1679
|
-
tools: this._toolSpecs,
|
|
1680
|
-
shots: this.fewShots
|
|
1681
|
-
});
|
|
1682
|
-
return createHash("sha256").update(blob).digest("hex").slice(0, 16);
|
|
1715
|
+
if (reason === "stuck") {
|
|
1716
|
+
return "[stuck on a repeated tool call \u2014 explaining what was tried and what's blocking progress]";
|
|
1683
1717
|
}
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1718
|
+
return `[tool-call budget (${iterCap}) reached \u2014 forcing summary from what I found]`;
|
|
1719
|
+
}
|
|
1720
|
+
function errorLabelFor(reason, iterCap) {
|
|
1721
|
+
if (reason === "aborted") return "aborted by user";
|
|
1722
|
+
if (reason === "context-guard") return "context-guard triggered (prompt > 80% of window)";
|
|
1723
|
+
if (reason === "stuck") return "stuck (repeated tool call suppressed by storm-breaker)";
|
|
1724
|
+
return `tool-call budget (${iterCap}) reached`;
|
|
1725
|
+
}
|
|
1726
|
+
function extractDeepSeekErrorMessage(body) {
|
|
1727
|
+
const trimmed = body.trim();
|
|
1728
|
+
if (!trimmed) return "(no message)";
|
|
1729
|
+
try {
|
|
1730
|
+
const parsed = JSON.parse(trimmed);
|
|
1731
|
+
if (parsed && typeof parsed === "object") {
|
|
1732
|
+
const obj = parsed;
|
|
1733
|
+
if (obj.error && typeof obj.error.message === "string") return obj.error.message;
|
|
1734
|
+
if (typeof obj.message === "string") return obj.message;
|
|
1690
1735
|
}
|
|
1691
|
-
|
|
1692
|
-
}
|
|
1693
|
-
extend(messages) {
|
|
1694
|
-
for (const m of messages) this.append(m);
|
|
1695
|
-
}
|
|
1696
|
-
/** The one append-only-breaking path — reserved for `/compact` + recovery. Use `append()` otherwise. */
|
|
1697
|
-
compactInPlace(replacement) {
|
|
1698
|
-
this._entries = [...replacement];
|
|
1699
|
-
}
|
|
1700
|
-
get entries() {
|
|
1701
|
-
return this._entries;
|
|
1702
|
-
}
|
|
1703
|
-
toMessages() {
|
|
1704
|
-
return this._entries.map((e) => ({ ...e }));
|
|
1705
|
-
}
|
|
1706
|
-
get length() {
|
|
1707
|
-
return this._entries.length;
|
|
1708
|
-
}
|
|
1709
|
-
};
|
|
1710
|
-
var VolatileScratch = class {
|
|
1711
|
-
reasoning = null;
|
|
1712
|
-
planState = null;
|
|
1713
|
-
notes = [];
|
|
1714
|
-
reset() {
|
|
1715
|
-
this.reasoning = null;
|
|
1716
|
-
this.planState = null;
|
|
1717
|
-
this.notes = [];
|
|
1736
|
+
} catch {
|
|
1718
1737
|
}
|
|
1719
|
-
|
|
1738
|
+
return trimmed;
|
|
1739
|
+
}
|
|
1720
1740
|
|
|
1721
|
-
// src/
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
notes.push(`scavenged call: ${call.function.name}`);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
return { calls: out, notes };
|
|
1741
|
+
// src/loop/escalation.ts
|
|
1742
|
+
var NEEDS_PRO_MARKER_PREFIX = "<<<NEEDS_PRO";
|
|
1743
|
+
var NEEDS_PRO_MARKER_RE = /^<<<NEEDS_PRO(?::\s*([^>]*))?>>>/;
|
|
1744
|
+
var NEEDS_PRO_BUFFER_CHARS = 256;
|
|
1745
|
+
function parseEscalationMarker(content) {
|
|
1746
|
+
const m = NEEDS_PRO_MARKER_RE.exec(content.trimStart());
|
|
1747
|
+
if (!m) return { matched: false };
|
|
1748
|
+
const reason = m[1]?.trim();
|
|
1749
|
+
return { matched: true, reason: reason || void 0 };
|
|
1750
|
+
}
|
|
1751
|
+
function isEscalationRequest(content) {
|
|
1752
|
+
return parseEscalationMarker(content).matched;
|
|
1753
|
+
}
|
|
1754
|
+
function looksLikePartialEscalationMarker(buf) {
|
|
1755
|
+
const t = buf.trimStart();
|
|
1756
|
+
if (t.length === 0) return true;
|
|
1757
|
+
if (t.length <= NEEDS_PRO_MARKER_PREFIX.length) {
|
|
1758
|
+
return NEEDS_PRO_MARKER_PREFIX.startsWith(t);
|
|
1759
|
+
}
|
|
1760
|
+
if (!t.startsWith(NEEDS_PRO_MARKER_PREFIX)) return false;
|
|
1761
|
+
const rest = t.slice(NEEDS_PRO_MARKER_PREFIX.length);
|
|
1762
|
+
if (rest[0] !== ">" && rest[0] !== ":") return false;
|
|
1763
|
+
return true;
|
|
1748
1764
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
return
|
|
1765
|
+
|
|
1766
|
+
// src/loop/thinking.ts
|
|
1767
|
+
function isThinkingModeModel(model) {
|
|
1768
|
+
if (model.includes("reasoner")) return true;
|
|
1769
|
+
if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return true;
|
|
1770
|
+
return false;
|
|
1754
1771
|
}
|
|
1755
|
-
function
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1772
|
+
function thinkingModeForModel(model) {
|
|
1773
|
+
if (model === "deepseek-chat") return "disabled";
|
|
1774
|
+
if (model.includes("reasoner")) return "enabled";
|
|
1775
|
+
if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return "enabled";
|
|
1776
|
+
return void 0;
|
|
1777
|
+
}
|
|
1778
|
+
function stripHallucinatedToolMarkup(s) {
|
|
1779
|
+
let out = s;
|
|
1780
|
+
out = out.replace(/<|DSML|function_calls>[\s\S]*?<\/?|DSML|function_calls>/g, "");
|
|
1781
|
+
out = out.replace(/<\|DSML\|function_calls>[\s\S]*?<\/?\|DSML\|function_calls>/g, "");
|
|
1782
|
+
out = out.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "");
|
|
1783
|
+
out = out.replace(/<|DSML|[\s\S]*$/g, "");
|
|
1784
|
+
return out.trim();
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// src/loop/messages.ts
|
|
1788
|
+
function buildAssistantMessage(content, toolCalls, producingModel, reasoningContent) {
|
|
1789
|
+
const msg = { role: "assistant", content };
|
|
1790
|
+
if (toolCalls.length > 0) msg.tool_calls = toolCalls;
|
|
1791
|
+
if (isThinkingModeModel(producingModel) || reasoningContent && reasoningContent.length > 0) {
|
|
1792
|
+
msg.reasoning_content = reasoningContent ?? "";
|
|
1762
1793
|
}
|
|
1794
|
+
return msg;
|
|
1763
1795
|
}
|
|
1764
|
-
function
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1796
|
+
function buildSyntheticAssistantMessage(content, fallbackModel) {
|
|
1797
|
+
return buildAssistantMessage(content, [], fallbackModel, "");
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// src/loop/force-summary.ts
|
|
1801
|
+
async function* forceSummaryAfterIterLimit(ctx, opts = { reason: "budget" }) {
|
|
1802
|
+
try {
|
|
1803
|
+
yield { turn: ctx.turn, role: "status", content: "summarizing what was gathered\u2026" };
|
|
1804
|
+
const messages = ctx.buildMessages();
|
|
1805
|
+
messages.push({
|
|
1806
|
+
role: "user",
|
|
1807
|
+
content: "I'm out of tool-call budget for this turn. Summarize in plain prose what you learned from the tool results above. Do NOT emit any tool calls, function-call markup, DSML invocations, or SEARCH/REPLACE edit blocks \u2014 they will be silently discarded. Just plain text."
|
|
1808
|
+
});
|
|
1809
|
+
const summaryModel = "deepseek-v4-flash";
|
|
1810
|
+
const summaryEffort = "high";
|
|
1811
|
+
const resp = await ctx.client.chat({
|
|
1812
|
+
model: summaryModel,
|
|
1813
|
+
messages,
|
|
1814
|
+
signal: ctx.signal,
|
|
1815
|
+
thinking: thinkingModeForModel(summaryModel),
|
|
1816
|
+
reasoningEffort: summaryEffort
|
|
1817
|
+
});
|
|
1818
|
+
const rawContent = resp.content?.trim() ?? "";
|
|
1819
|
+
const cleaned = stripHallucinatedToolMarkup(rawContent);
|
|
1820
|
+
const summary = cleaned || "(model emitted fake tool-call markup instead of a prose summary \u2014 try /retry with a narrower question, or /think to inspect R1's reasoning)";
|
|
1821
|
+
const reasonPrefix = reasonPrefixFor(opts.reason, ctx.maxToolIters);
|
|
1822
|
+
const annotated = `${reasonPrefix}
|
|
1823
|
+
|
|
1824
|
+
${summary}`;
|
|
1825
|
+
const summaryStats = ctx.recordStats(summaryModel, resp.usage ?? new Usage());
|
|
1826
|
+
ctx.appendAndPersist(buildAssistantMessage(summary, [], summaryModel, resp.reasoningContent));
|
|
1827
|
+
yield {
|
|
1828
|
+
turn: ctx.turn,
|
|
1829
|
+
role: "assistant_final",
|
|
1830
|
+
content: annotated,
|
|
1831
|
+
stats: summaryStats,
|
|
1832
|
+
forcedSummary: true
|
|
1833
|
+
};
|
|
1834
|
+
yield { turn: ctx.turn, role: "done", content: summary };
|
|
1835
|
+
} catch (err) {
|
|
1836
|
+
const label = errorLabelFor(opts.reason, ctx.maxToolIters);
|
|
1837
|
+
yield {
|
|
1838
|
+
turn: ctx.turn,
|
|
1839
|
+
role: "error",
|
|
1840
|
+
content: "",
|
|
1841
|
+
error: `${label} and the fallback summary call failed: ${err.message}. Run /clear and retry with a narrower question, or raise --max-tool-iters.`
|
|
1842
|
+
};
|
|
1843
|
+
yield { turn: ctx.turn, role: "done", content: "" };
|
|
1780
1844
|
}
|
|
1781
|
-
return args;
|
|
1782
1845
|
}
|
|
1783
|
-
|
|
1846
|
+
|
|
1847
|
+
// src/loop/shrink.ts
|
|
1848
|
+
function looksLikeCompleteJson(s) {
|
|
1849
|
+
if (!s || !s.trim()) return false;
|
|
1850
|
+
try {
|
|
1851
|
+
JSON.parse(s);
|
|
1852
|
+
return true;
|
|
1853
|
+
} catch {
|
|
1854
|
+
return false;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
function shrinkOversizedToolResults(messages, maxChars) {
|
|
1858
|
+
let healedCount = 0;
|
|
1859
|
+
let healedFrom = 0;
|
|
1860
|
+
const out = messages.map((msg) => {
|
|
1861
|
+
if (msg.role !== "tool") return msg;
|
|
1862
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
1863
|
+
if (content.length <= maxChars) return msg;
|
|
1864
|
+
healedCount += 1;
|
|
1865
|
+
healedFrom += content.length;
|
|
1866
|
+
return { ...msg, content: truncateForModel(content, maxChars) };
|
|
1867
|
+
});
|
|
1868
|
+
return { messages: out, healedCount, healedFrom };
|
|
1869
|
+
}
|
|
1870
|
+
function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
|
|
1871
|
+
let healedCount = 0;
|
|
1872
|
+
let tokensSaved = 0;
|
|
1873
|
+
let charsSaved = 0;
|
|
1874
|
+
const out = messages.map((msg) => {
|
|
1875
|
+
if (msg.role !== "tool") return msg;
|
|
1876
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
1877
|
+
if (content.length <= maxTokens) return msg;
|
|
1878
|
+
const beforeTokens = countTokens(content);
|
|
1879
|
+
if (beforeTokens <= maxTokens) return msg;
|
|
1880
|
+
const truncated = truncateForModelByTokens(content, maxTokens);
|
|
1881
|
+
const afterTokens = countTokens(truncated);
|
|
1882
|
+
healedCount += 1;
|
|
1883
|
+
tokensSaved += Math.max(0, beforeTokens - afterTokens);
|
|
1884
|
+
charsSaved += Math.max(0, content.length - truncated.length);
|
|
1885
|
+
return { ...msg, content: truncated };
|
|
1886
|
+
});
|
|
1887
|
+
return { messages: out, healedCount, tokensSaved, charsSaved };
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// src/loop/healing.ts
|
|
1891
|
+
function fixToolCallPairing(messages) {
|
|
1892
|
+
const out = [];
|
|
1893
|
+
let droppedAssistantCalls = 0;
|
|
1894
|
+
let droppedStrayTools = 0;
|
|
1895
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1896
|
+
const msg = messages[i];
|
|
1897
|
+
if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
1898
|
+
const needed = /* @__PURE__ */ new Set();
|
|
1899
|
+
for (const call of msg.tool_calls) {
|
|
1900
|
+
if (call?.id) needed.add(call.id);
|
|
1901
|
+
}
|
|
1902
|
+
const candidates = [];
|
|
1903
|
+
let j = i + 1;
|
|
1904
|
+
while (j < messages.length && needed.size > 0) {
|
|
1905
|
+
const nxt = messages[j];
|
|
1906
|
+
if (nxt.role !== "tool") break;
|
|
1907
|
+
const id = nxt.tool_call_id ?? "";
|
|
1908
|
+
if (!needed.has(id)) break;
|
|
1909
|
+
needed.delete(id);
|
|
1910
|
+
candidates.push(nxt);
|
|
1911
|
+
j++;
|
|
1912
|
+
}
|
|
1913
|
+
if (needed.size === 0) {
|
|
1914
|
+
out.push(msg);
|
|
1915
|
+
for (const r of candidates) out.push(r);
|
|
1916
|
+
i = j - 1;
|
|
1917
|
+
} else {
|
|
1918
|
+
droppedAssistantCalls += 1;
|
|
1919
|
+
droppedStrayTools += candidates.length;
|
|
1920
|
+
i = j - 1;
|
|
1921
|
+
}
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
if (msg.role === "tool") {
|
|
1925
|
+
droppedStrayTools += 1;
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
out.push(msg);
|
|
1929
|
+
}
|
|
1930
|
+
return { messages: out, droppedAssistantCalls, droppedStrayTools };
|
|
1931
|
+
}
|
|
1932
|
+
function healLoadedMessages(messages, maxChars) {
|
|
1933
|
+
const shrunk = shrinkOversizedToolResults(messages, maxChars);
|
|
1934
|
+
const paired = fixToolCallPairing(shrunk.messages);
|
|
1935
|
+
const healedCount = shrunk.healedCount + paired.droppedAssistantCalls + paired.droppedStrayTools;
|
|
1936
|
+
return { messages: paired.messages, healedCount, healedFrom: shrunk.healedFrom };
|
|
1937
|
+
}
|
|
1938
|
+
function stampMissingReasoningForThinkingMode(messages, model) {
|
|
1939
|
+
if (!isThinkingModeModel(model)) {
|
|
1940
|
+
return { messages, stampedCount: 0 };
|
|
1941
|
+
}
|
|
1942
|
+
let stampedCount = 0;
|
|
1943
|
+
const out = messages.map((msg) => {
|
|
1944
|
+
if (msg.role !== "assistant") return msg;
|
|
1945
|
+
if (Object.hasOwn(msg, "reasoning_content")) return msg;
|
|
1946
|
+
stampedCount += 1;
|
|
1947
|
+
return { ...msg, reasoning_content: "" };
|
|
1948
|
+
});
|
|
1949
|
+
return { messages: out, stampedCount };
|
|
1950
|
+
}
|
|
1951
|
+
function healLoadedMessagesByTokens(messages, maxTokens) {
|
|
1952
|
+
const shrunk = shrinkOversizedToolResultsByTokens(messages, maxTokens);
|
|
1953
|
+
const paired = fixToolCallPairing(shrunk.messages);
|
|
1954
|
+
const healedCount = shrunk.healedCount + paired.droppedAssistantCalls + paired.droppedStrayTools;
|
|
1955
|
+
return {
|
|
1956
|
+
messages: paired.messages,
|
|
1957
|
+
healedCount,
|
|
1958
|
+
tokensSaved: shrunk.tokensSaved,
|
|
1959
|
+
charsSaved: shrunk.charsSaved
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// src/loop/hook-events.ts
|
|
1964
|
+
function safeParseToolArgs(raw) {
|
|
1965
|
+
try {
|
|
1966
|
+
return JSON.parse(raw);
|
|
1967
|
+
} catch {
|
|
1968
|
+
return raw;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
function* hookWarnings(outcomes, turn) {
|
|
1972
|
+
for (const o of outcomes) {
|
|
1973
|
+
if (o.decision === "pass") continue;
|
|
1974
|
+
yield { turn, role: "warning", content: formatHookOutcomeMessage(o) };
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/loop/turn-failure-tracker.ts
|
|
1979
|
+
var FAILURE_ESCALATION_THRESHOLD = 3;
|
|
1980
|
+
var TurnFailureTracker = class {
|
|
1981
|
+
count = 0;
|
|
1982
|
+
types = {};
|
|
1983
|
+
reset() {
|
|
1984
|
+
this.count = 0;
|
|
1985
|
+
this.types = {};
|
|
1986
|
+
}
|
|
1987
|
+
/** True ONLY on the call where the count crosses FAILURE_ESCALATION_THRESHOLD. */
|
|
1988
|
+
noteAndCrossedThreshold(resultJson, repair) {
|
|
1989
|
+
const before = this.count;
|
|
1990
|
+
const bump = (kind, by = 1) => {
|
|
1991
|
+
this.count += by;
|
|
1992
|
+
this.types[kind] = (this.types[kind] ?? 0) + by;
|
|
1993
|
+
};
|
|
1994
|
+
if (resultJson.includes('"error"') && resultJson.includes("search text not found")) {
|
|
1995
|
+
bump("search-mismatch");
|
|
1996
|
+
}
|
|
1997
|
+
if (repair) {
|
|
1998
|
+
if (repair.scavenged > 0) bump("scavenged", repair.scavenged);
|
|
1999
|
+
if (repair.truncationsFixed > 0) bump("truncated", repair.truncationsFixed);
|
|
2000
|
+
if (repair.stormsBroken > 0) bump("repeat-loop", repair.stormsBroken);
|
|
2001
|
+
}
|
|
2002
|
+
return before < FAILURE_ESCALATION_THRESHOLD && this.count >= FAILURE_ESCALATION_THRESHOLD;
|
|
2003
|
+
}
|
|
2004
|
+
formatBreakdown() {
|
|
2005
|
+
const parts = Object.entries(this.types).filter(([, n]) => n > 0).map(([kind, n]) => `${n}\xD7 ${kind}`);
|
|
2006
|
+
return parts.length > 0 ? parts.join(", ") : `${this.count} repair/error signal(s)`;
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
|
|
2010
|
+
// src/memory/runtime.ts
|
|
2011
|
+
import { createHash } from "crypto";
|
|
2012
|
+
var ImmutablePrefix = class {
|
|
2013
|
+
system;
|
|
2014
|
+
/** Each `addTool` costs one cache-miss turn — DeepSeek's prefix cache is keyed by full tool list. */
|
|
2015
|
+
_toolSpecs;
|
|
2016
|
+
fewShots;
|
|
2017
|
+
/** Invalidated only via `addTool`; bypassing it leaves cache stale → fingerprint diverges from sent prefix. */
|
|
2018
|
+
_fingerprintCache = null;
|
|
2019
|
+
constructor(opts) {
|
|
2020
|
+
this.system = opts.system;
|
|
2021
|
+
this._toolSpecs = [...opts.toolSpecs ?? []];
|
|
2022
|
+
this.fewShots = Object.freeze([...opts.fewShots ?? []]);
|
|
2023
|
+
}
|
|
2024
|
+
get toolSpecs() {
|
|
2025
|
+
return this._toolSpecs;
|
|
2026
|
+
}
|
|
2027
|
+
toMessages() {
|
|
2028
|
+
return [{ role: "system", content: this.system }, ...this.fewShots.map((m) => ({ ...m }))];
|
|
2029
|
+
}
|
|
2030
|
+
tools() {
|
|
2031
|
+
return this._toolSpecs.map((t) => structuredClone(t));
|
|
2032
|
+
}
|
|
2033
|
+
addTool(spec) {
|
|
2034
|
+
const name = spec.function?.name;
|
|
2035
|
+
if (!name) return false;
|
|
2036
|
+
if (this._toolSpecs.some((t) => t.function?.name === name)) return false;
|
|
2037
|
+
this._toolSpecs.push(spec);
|
|
2038
|
+
this._fingerprintCache = null;
|
|
2039
|
+
return true;
|
|
2040
|
+
}
|
|
2041
|
+
/** Mirror of addTool for MCP hot-unbridge. Same cache-miss cost — prefix changes shape. */
|
|
2042
|
+
removeTool(name) {
|
|
2043
|
+
const idx = this._toolSpecs.findIndex((t) => t.function?.name === name);
|
|
2044
|
+
if (idx < 0) return false;
|
|
2045
|
+
this._toolSpecs.splice(idx, 1);
|
|
2046
|
+
this._fingerprintCache = null;
|
|
2047
|
+
return true;
|
|
2048
|
+
}
|
|
2049
|
+
get fingerprint() {
|
|
2050
|
+
if (this._fingerprintCache !== null) return this._fingerprintCache;
|
|
2051
|
+
this._fingerprintCache = this.computeFingerprint();
|
|
2052
|
+
return this._fingerprintCache;
|
|
2053
|
+
}
|
|
2054
|
+
/** Dev/test only — throws on cache drift, which always means a non-`addTool` mutation slipped in. */
|
|
2055
|
+
verifyFingerprint() {
|
|
2056
|
+
const fresh = this.computeFingerprint();
|
|
2057
|
+
if (this._fingerprintCache !== null && this._fingerprintCache !== fresh) {
|
|
2058
|
+
throw new Error(
|
|
2059
|
+
`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.`
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
this._fingerprintCache = fresh;
|
|
2063
|
+
return fresh;
|
|
2064
|
+
}
|
|
2065
|
+
computeFingerprint() {
|
|
2066
|
+
const blob = JSON.stringify({
|
|
2067
|
+
system: this.system,
|
|
2068
|
+
tools: this._toolSpecs,
|
|
2069
|
+
shots: this.fewShots
|
|
2070
|
+
});
|
|
2071
|
+
return createHash("sha256").update(blob).digest("hex").slice(0, 16);
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
var AppendOnlyLog = class {
|
|
2075
|
+
_entries = [];
|
|
2076
|
+
append(message) {
|
|
2077
|
+
if (!message || typeof message !== "object" || !("role" in message)) {
|
|
2078
|
+
throw new Error(`invalid log entry: ${JSON.stringify(message)}`);
|
|
2079
|
+
}
|
|
2080
|
+
this._entries.push(message);
|
|
2081
|
+
}
|
|
2082
|
+
extend(messages) {
|
|
2083
|
+
for (const m of messages) this.append(m);
|
|
2084
|
+
}
|
|
2085
|
+
/** The one append-only-breaking path — reserved for `/compact` + recovery. Use `append()` otherwise. */
|
|
2086
|
+
compactInPlace(replacement) {
|
|
2087
|
+
this._entries = [...replacement];
|
|
2088
|
+
}
|
|
2089
|
+
get entries() {
|
|
2090
|
+
return this._entries;
|
|
2091
|
+
}
|
|
2092
|
+
toMessages() {
|
|
2093
|
+
return this._entries.map((e) => ({ ...e }));
|
|
2094
|
+
}
|
|
2095
|
+
get length() {
|
|
2096
|
+
return this._entries.length;
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
var VolatileScratch = class {
|
|
2100
|
+
reasoning = null;
|
|
2101
|
+
planState = null;
|
|
2102
|
+
notes = [];
|
|
2103
|
+
reset() {
|
|
2104
|
+
this.reasoning = null;
|
|
2105
|
+
this.planState = null;
|
|
2106
|
+
this.notes = [];
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
|
|
2110
|
+
// src/repair/scavenge.ts
|
|
2111
|
+
var MAX_SCAVENGE_INPUT = 100 * 1024;
|
|
2112
|
+
function scavengeToolCalls(reasoningContent, opts) {
|
|
2113
|
+
if (!reasoningContent) return { calls: [], notes: [] };
|
|
2114
|
+
if (reasoningContent.length > MAX_SCAVENGE_INPUT) {
|
|
2115
|
+
return {
|
|
2116
|
+
calls: [],
|
|
2117
|
+
notes: [`scavenge skipped: reasoning_content too large (${reasoningContent.length} chars)`]
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
const max = opts.maxCalls ?? 4;
|
|
2121
|
+
const notes = [];
|
|
2122
|
+
const out = [];
|
|
2123
|
+
for (const invoke of iterateDsmlInvokes(reasoningContent)) {
|
|
2124
|
+
if (out.length >= max) break;
|
|
2125
|
+
if (!opts.allowedNames.has(invoke.name)) continue;
|
|
2126
|
+
out.push({
|
|
2127
|
+
function: {
|
|
2128
|
+
name: invoke.name,
|
|
2129
|
+
arguments: JSON.stringify(invoke.args)
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
notes.push(`scavenged DSML call: ${invoke.name}`);
|
|
2133
|
+
}
|
|
2134
|
+
const nonDsml = stripDsmlBlocks(reasoningContent);
|
|
2135
|
+
for (const candidate of iterateJsonObjects(nonDsml)) {
|
|
2136
|
+
if (out.length >= max) break;
|
|
2137
|
+
const call = coerceToToolCall(candidate, opts.allowedNames);
|
|
2138
|
+
if (call) {
|
|
2139
|
+
out.push(call);
|
|
2140
|
+
notes.push(`scavenged call: ${call.function.name}`);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return { calls: out, notes };
|
|
2144
|
+
}
|
|
2145
|
+
function stripDsmlBlocks(text) {
|
|
2146
|
+
let out = text;
|
|
2147
|
+
out = out.replace(/<[||]DSML[||]function_calls>[\s\S]*?<\/?[||]DSML[||]function_calls>/g, "");
|
|
2148
|
+
out = out.replace(/<[||]DSML[||]invoke\s+[^>]*>[\s\S]*?<\/[||]DSML[||]invoke>/g, "");
|
|
2149
|
+
return out;
|
|
2150
|
+
}
|
|
2151
|
+
function* iterateDsmlInvokes(text) {
|
|
2152
|
+
const INVOKE_RE = /<[||]DSML[||]invoke\s+name="([^"]+)">([\s\S]*?)<\/[||]DSML[||]invoke>/g;
|
|
2153
|
+
for (const match of text.matchAll(INVOKE_RE)) {
|
|
2154
|
+
const name = match[1];
|
|
2155
|
+
const body = match[2];
|
|
2156
|
+
if (!name || body === void 0) continue;
|
|
2157
|
+
yield { name, args: parseDsmlParameters(body) };
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
function parseDsmlParameters(body) {
|
|
2161
|
+
const PARAM_RE = /<[||]DSML[||]parameter\s+name="([^"]+)"(?:\s+string="(true|false)")?\s*>([\s\S]*?)<\/[||]DSML[||]parameter>/g;
|
|
2162
|
+
const args = {};
|
|
2163
|
+
for (const m of body.matchAll(PARAM_RE)) {
|
|
2164
|
+
const key = m[1];
|
|
2165
|
+
const stringFlag = m[2];
|
|
2166
|
+
const raw = (m[3] ?? "").trim();
|
|
2167
|
+
if (!key) continue;
|
|
2168
|
+
if (stringFlag === "false") {
|
|
2169
|
+
try {
|
|
2170
|
+
args[key] = JSON.parse(raw);
|
|
2171
|
+
continue;
|
|
2172
|
+
} catch {
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
args[key] = raw;
|
|
2176
|
+
}
|
|
2177
|
+
return args;
|
|
2178
|
+
}
|
|
2179
|
+
function* iterateJsonObjects(text) {
|
|
1784
2180
|
for (let i = 0; i < text.length; i++) {
|
|
1785
2181
|
if (text[i] !== "{") continue;
|
|
1786
2182
|
let depth = 0;
|
|
@@ -2020,11 +2416,7 @@ function signature(call) {
|
|
|
2020
2416
|
}
|
|
2021
2417
|
|
|
2022
2418
|
// src/loop.ts
|
|
2023
|
-
var FAILURE_ESCALATION_THRESHOLD = 3;
|
|
2024
2419
|
var ESCALATION_MODEL = "deepseek-v4-pro";
|
|
2025
|
-
var NEEDS_PRO_MARKER_PREFIX = "<<<NEEDS_PRO";
|
|
2026
|
-
var NEEDS_PRO_MARKER_RE = /^<<<NEEDS_PRO(?::\s*([^>]*))?>>>/;
|
|
2027
|
-
var NEEDS_PRO_BUFFER_CHARS = 256;
|
|
2028
2420
|
var CacheFirstLoop = class {
|
|
2029
2421
|
client;
|
|
2030
2422
|
prefix;
|
|
@@ -2060,11 +2452,13 @@ var CacheFirstLoop = class {
|
|
|
2060
2452
|
_turnAbort = new AbortController();
|
|
2061
2453
|
_proArmedForNextTurn = false;
|
|
2062
2454
|
_escalateThisTurn = false;
|
|
2063
|
-
|
|
2064
|
-
_turnFailureTypes = {};
|
|
2455
|
+
_turnFailures = new TurnFailureTracker();
|
|
2065
2456
|
_turnSelfCorrected = false;
|
|
2066
2457
|
_foldedThisTurn = false;
|
|
2067
2458
|
context;
|
|
2459
|
+
get currentTurn() {
|
|
2460
|
+
return this._turn;
|
|
2461
|
+
}
|
|
2068
2462
|
constructor(opts) {
|
|
2069
2463
|
this.client = opts.client;
|
|
2070
2464
|
this.prefix = opts.prefix;
|
|
@@ -2232,61 +2626,19 @@ var CacheFirstLoop = class {
|
|
|
2232
2626
|
get proArmed() {
|
|
2233
2627
|
return this._proArmedForNextTurn;
|
|
2234
2628
|
}
|
|
2235
|
-
/** UI surface — true while the current turn is running on pro (armed or auto-escalated). */
|
|
2236
|
-
get escalatedThisTurn() {
|
|
2237
|
-
return this._escalateThisTurn;
|
|
2238
|
-
}
|
|
2239
|
-
modelForCurrentCall() {
|
|
2240
|
-
return this._escalateThisTurn ? ESCALATION_MODEL : this.model;
|
|
2241
|
-
}
|
|
2242
|
-
/** Anchored to lead — mid-text matches are normal content (user asking about the marker). */
|
|
2243
|
-
parseEscalationMarker(content) {
|
|
2244
|
-
const m = NEEDS_PRO_MARKER_RE.exec(content.trimStart());
|
|
2245
|
-
if (!m) return { matched: false };
|
|
2246
|
-
const reason = m[1]?.trim();
|
|
2247
|
-
return { matched: true, reason: reason || void 0 };
|
|
2248
|
-
}
|
|
2249
|
-
/** Convenience boolean — same gate the streaming path used to call. */
|
|
2250
|
-
isEscalationRequest(content) {
|
|
2251
|
-
return this.parseEscalationMarker(content).matched;
|
|
2252
|
-
}
|
|
2253
|
-
/** Drives streaming flush — while plausibly partial, keep accumulating; else flush. */
|
|
2254
|
-
looksLikePartialEscalationMarker(buf) {
|
|
2255
|
-
const t = buf.trimStart();
|
|
2256
|
-
if (t.length === 0) return true;
|
|
2257
|
-
if (t.length <= NEEDS_PRO_MARKER_PREFIX.length) {
|
|
2258
|
-
return NEEDS_PRO_MARKER_PREFIX.startsWith(t);
|
|
2259
|
-
}
|
|
2260
|
-
if (!t.startsWith(NEEDS_PRO_MARKER_PREFIX)) return false;
|
|
2261
|
-
const rest = t.slice(NEEDS_PRO_MARKER_PREFIX.length);
|
|
2262
|
-
if (rest[0] !== ">" && rest[0] !== ":") return false;
|
|
2263
|
-
return true;
|
|
2264
|
-
}
|
|
2265
|
-
/** Returns true ONLY on the tipping call — caller surfaces a one-shot warning. */
|
|
2266
|
-
noteToolFailureSignal(resultJson, repair) {
|
|
2267
|
-
let bumped = false;
|
|
2268
|
-
const bump = (kind, by = 1) => {
|
|
2269
|
-
this._turnFailureCount += by;
|
|
2270
|
-
this._turnFailureTypes[kind] = (this._turnFailureTypes[kind] ?? 0) + by;
|
|
2271
|
-
bumped = true;
|
|
2272
|
-
};
|
|
2273
|
-
if (resultJson.includes('"error"') && resultJson.includes("search text not found")) {
|
|
2274
|
-
bump("search-mismatch");
|
|
2275
|
-
}
|
|
2276
|
-
if (repair) {
|
|
2277
|
-
if (repair.scavenged > 0) bump("scavenged", repair.scavenged);
|
|
2278
|
-
if (repair.truncationsFixed > 0) bump("truncated", repair.truncationsFixed);
|
|
2279
|
-
if (repair.stormsBroken > 0) bump("repeat-loop", repair.stormsBroken);
|
|
2280
|
-
}
|
|
2281
|
-
if (bumped && !this._escalateThisTurn && this.autoEscalate && this._turnFailureCount >= FAILURE_ESCALATION_THRESHOLD) {
|
|
2282
|
-
this._escalateThisTurn = true;
|
|
2283
|
-
return true;
|
|
2284
|
-
}
|
|
2285
|
-
return false;
|
|
2629
|
+
/** UI surface — true while the current turn is running on pro (armed or auto-escalated). */
|
|
2630
|
+
get escalatedThisTurn() {
|
|
2631
|
+
return this._escalateThisTurn;
|
|
2632
|
+
}
|
|
2633
|
+
modelForCurrentCall() {
|
|
2634
|
+
return this._escalateThisTurn ? ESCALATION_MODEL : this.model;
|
|
2286
2635
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2636
|
+
/** Returns true ONLY on the tipping call — caller surfaces a one-shot warning. */
|
|
2637
|
+
noteToolFailureSignal(resultJson, repair) {
|
|
2638
|
+
if (!this._turnFailures.noteAndCrossedThreshold(resultJson, repair)) return false;
|
|
2639
|
+
if (this._escalateThisTurn || !this.autoEscalate) return false;
|
|
2640
|
+
this._escalateThisTurn = true;
|
|
2641
|
+
return true;
|
|
2290
2642
|
}
|
|
2291
2643
|
buildMessages(pendingUser) {
|
|
2292
2644
|
const healed = healLoadedMessages(this.log.toMessages(), DEFAULT_MAX_RESULT_CHARS);
|
|
@@ -2344,8 +2696,7 @@ var CacheFirstLoop = class {
|
|
|
2344
2696
|
this._turn++;
|
|
2345
2697
|
this.scratch.reset();
|
|
2346
2698
|
this.repair.resetStorm();
|
|
2347
|
-
this.
|
|
2348
|
-
this._turnFailureTypes = {};
|
|
2699
|
+
this._turnFailures.reset();
|
|
2349
2700
|
this._turnSelfCorrected = false;
|
|
2350
2701
|
this._escalateThisTurn = false;
|
|
2351
2702
|
this._foldedThisTurn = false;
|
|
@@ -2378,7 +2729,7 @@ var CacheFirstLoop = class {
|
|
|
2378
2729
|
content: `aborted at iter ${iter}/${this.maxToolIters} \u2014 stopped without producing a summary (press \u2191 + Enter or /retry to resume)`
|
|
2379
2730
|
};
|
|
2380
2731
|
const stoppedMsg = "[aborted by user (Esc) \u2014 no summary produced. Ask again or /retry when ready; prior tool output is still in the log.]";
|
|
2381
|
-
this.appendAndPersist(this.
|
|
2732
|
+
this.appendAndPersist(buildSyntheticAssistantMessage(stoppedMsg, this.model));
|
|
2382
2733
|
yield {
|
|
2383
2734
|
turn: this._turn,
|
|
2384
2735
|
role: "assistant_final",
|
|
@@ -2540,10 +2891,10 @@ var CacheFirstLoop = class {
|
|
|
2540
2891
|
assistantContent += chunk.contentDelta;
|
|
2541
2892
|
if (bufferForEscalation && !escalationBufFlushed) {
|
|
2542
2893
|
escalationBuf += chunk.contentDelta;
|
|
2543
|
-
if (
|
|
2894
|
+
if (isEscalationRequest(escalationBuf)) {
|
|
2544
2895
|
break;
|
|
2545
2896
|
}
|
|
2546
|
-
if (escalationBuf.length >= NEEDS_PRO_BUFFER_CHARS || !
|
|
2897
|
+
if (escalationBuf.length >= NEEDS_PRO_BUFFER_CHARS || !looksLikePartialEscalationMarker(escalationBuf)) {
|
|
2547
2898
|
escalationBufFlushed = true;
|
|
2548
2899
|
yield {
|
|
2549
2900
|
turn: this._turn,
|
|
@@ -2600,7 +2951,7 @@ var CacheFirstLoop = class {
|
|
|
2600
2951
|
}
|
|
2601
2952
|
toolCalls = [...callBuf.values()];
|
|
2602
2953
|
if (bufferForEscalation && !escalationBufFlushed && escalationBuf.length > 0) {
|
|
2603
|
-
if (!
|
|
2954
|
+
if (!isEscalationRequest(escalationBuf)) {
|
|
2604
2955
|
yield {
|
|
2605
2956
|
turn: this._turn,
|
|
2606
2957
|
role: "assistant_delta",
|
|
@@ -2637,8 +2988,8 @@ var CacheFirstLoop = class {
|
|
|
2637
2988
|
};
|
|
2638
2989
|
return;
|
|
2639
2990
|
}
|
|
2640
|
-
if (this.autoEscalate && this.modelForCurrentCall() !== ESCALATION_MODEL &&
|
|
2641
|
-
const { reason } =
|
|
2991
|
+
if (this.autoEscalate && this.modelForCurrentCall() !== ESCALATION_MODEL && isEscalationRequest(assistantContent)) {
|
|
2992
|
+
const { reason } = parseEscalationMarker(assistantContent);
|
|
2642
2993
|
this._escalateThisTurn = true;
|
|
2643
2994
|
const reasonSuffix = reason ? ` \u2014 ${reason}` : "";
|
|
2644
2995
|
yield {
|
|
@@ -2679,7 +3030,7 @@ var CacheFirstLoop = class {
|
|
|
2679
3030
|
assistantContent || null
|
|
2680
3031
|
);
|
|
2681
3032
|
this.appendAndPersist(
|
|
2682
|
-
|
|
3033
|
+
buildAssistantMessage(
|
|
2683
3034
|
assistantContent,
|
|
2684
3035
|
repairedCalls,
|
|
2685
3036
|
this.modelForCurrentCall(),
|
|
@@ -2699,14 +3050,14 @@ var CacheFirstLoop = class {
|
|
|
2699
3050
|
yield {
|
|
2700
3051
|
turn: this._turn,
|
|
2701
3052
|
role: "warning",
|
|
2702
|
-
content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this.
|
|
3053
|
+
content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this._turnFailures.formatBreakdown()}. Next turn falls back to ${this.model} unless /pro is armed.`
|
|
2703
3054
|
};
|
|
2704
3055
|
}
|
|
2705
3056
|
const allSuppressed = report.stormsBroken > 0 && repairedCalls.length === 0 && toolCalls.length > 0;
|
|
2706
3057
|
if (allSuppressed && !this._turnSelfCorrected) {
|
|
2707
3058
|
this._turnSelfCorrected = true;
|
|
2708
3059
|
this.replaceTailAssistantMessage(
|
|
2709
|
-
|
|
3060
|
+
buildAssistantMessage(
|
|
2710
3061
|
assistantContent,
|
|
2711
3062
|
toolCalls,
|
|
2712
3063
|
this.modelForCurrentCall(),
|
|
@@ -2739,7 +3090,7 @@ var CacheFirstLoop = class {
|
|
|
2739
3090
|
}
|
|
2740
3091
|
if (repairedCalls.length === 0) {
|
|
2741
3092
|
if (allSuppressed) {
|
|
2742
|
-
yield* this.
|
|
3093
|
+
yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "stuck" });
|
|
2743
3094
|
return;
|
|
2744
3095
|
}
|
|
2745
3096
|
yield { turn: this._turn, role: "done", content: assistantContent };
|
|
@@ -2759,383 +3110,121 @@ var CacheFirstLoop = class {
|
|
|
2759
3110
|
const result = await this.compactHistory({ keepRecentTokens: decision.tailBudget });
|
|
2760
3111
|
if (result.folded) {
|
|
2761
3112
|
yield {
|
|
2762
|
-
turn: this._turn,
|
|
2763
|
-
role: "warning",
|
|
2764
|
-
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
|
|
2765
|
-
before / ctxMax * 100
|
|
2766
|
-
)}%) \u2014 ${decision.aggressive ? "aggressively folded" : "folded"} ${result.beforeMessages} messages \u2192 ${result.afterMessages} (summary ${result.summaryChars} chars). Continuing.`
|
|
2767
|
-
};
|
|
2768
|
-
}
|
|
2769
|
-
} else if (decision.kind === "exit-with-summary") {
|
|
2770
|
-
const before = decision.promptTokens;
|
|
2771
|
-
const ctxMax = decision.ctxMax;
|
|
2772
|
-
yield {
|
|
2773
|
-
turn: this._turn,
|
|
2774
|
-
role: "warning",
|
|
2775
|
-
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
|
|
2776
|
-
before / ctxMax * 100
|
|
2777
|
-
)}%) \u2014 forcing summary from what was gathered. Run /compact, /clear, or /new to reset.`
|
|
2778
|
-
};
|
|
2779
|
-
this.context.trimTrailingToolCalls();
|
|
2780
|
-
yield* this.
|
|
2781
|
-
return;
|
|
2782
|
-
}
|
|
2783
|
-
for (const call of repairedCalls) {
|
|
2784
|
-
const name = call.function?.name ?? "";
|
|
2785
|
-
const args = call.function?.arguments ?? "{}";
|
|
2786
|
-
yield {
|
|
2787
|
-
turn: this._turn,
|
|
2788
|
-
role: "tool_start",
|
|
2789
|
-
content: "",
|
|
2790
|
-
toolName: name,
|
|
2791
|
-
toolArgs: args
|
|
2792
|
-
};
|
|
2793
|
-
const parsedArgs = safeParseToolArgs(args);
|
|
2794
|
-
const preReport = await runHooks({
|
|
2795
|
-
hooks: this.hooks,
|
|
2796
|
-
payload: {
|
|
2797
|
-
event: "PreToolUse",
|
|
2798
|
-
cwd: this.hookCwd,
|
|
2799
|
-
toolName: name,
|
|
2800
|
-
toolArgs: parsedArgs
|
|
2801
|
-
}
|
|
2802
|
-
});
|
|
2803
|
-
for (const w of hookWarnings(preReport.outcomes, this._turn)) yield w;
|
|
2804
|
-
let result;
|
|
2805
|
-
if (preReport.blocked) {
|
|
2806
|
-
const blocking = preReport.outcomes[preReport.outcomes.length - 1];
|
|
2807
|
-
const reason = (blocking?.stderr || blocking?.stdout || "blocked by PreToolUse hook").trim();
|
|
2808
|
-
result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
|
|
2809
|
-
${reason}`;
|
|
2810
|
-
} else {
|
|
2811
|
-
result = await this.tools.dispatch(name, args, {
|
|
2812
|
-
signal,
|
|
2813
|
-
maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
|
|
2814
|
-
confirmationGate: this.confirmationGate
|
|
2815
|
-
});
|
|
2816
|
-
const postReport = await runHooks({
|
|
2817
|
-
hooks: this.hooks,
|
|
2818
|
-
payload: {
|
|
2819
|
-
event: "PostToolUse",
|
|
2820
|
-
cwd: this.hookCwd,
|
|
2821
|
-
toolName: name,
|
|
2822
|
-
toolArgs: parsedArgs,
|
|
2823
|
-
toolResult: result
|
|
2824
|
-
}
|
|
2825
|
-
});
|
|
2826
|
-
for (const w of hookWarnings(postReport.outcomes, this._turn)) yield w;
|
|
2827
|
-
}
|
|
2828
|
-
this.appendAndPersist({
|
|
2829
|
-
role: "tool",
|
|
2830
|
-
tool_call_id: call.id ?? "",
|
|
2831
|
-
name,
|
|
2832
|
-
content: result
|
|
2833
|
-
});
|
|
2834
|
-
if (this.noteToolFailureSignal(result)) {
|
|
2835
|
-
yield {
|
|
2836
|
-
turn: this._turn,
|
|
2837
|
-
role: "warning",
|
|
2838
|
-
content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this.formatFailureBreakdown()}. Next turn falls back to ${this.model} unless /pro is armed.`
|
|
2839
|
-
};
|
|
2840
|
-
}
|
|
2841
|
-
yield {
|
|
2842
|
-
turn: this._turn,
|
|
2843
|
-
role: "tool",
|
|
2844
|
-
content: result,
|
|
2845
|
-
toolName: name,
|
|
2846
|
-
toolArgs: args
|
|
2847
|
-
};
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
yield* this.forceSummaryAfterIterLimit({ reason: "budget" });
|
|
2851
|
-
}
|
|
2852
|
-
async *forceSummaryAfterIterLimit(opts = { reason: "budget" }) {
|
|
2853
|
-
try {
|
|
2854
|
-
yield {
|
|
2855
|
-
turn: this._turn,
|
|
2856
|
-
role: "status",
|
|
2857
|
-
content: "summarizing what was gathered\u2026"
|
|
2858
|
-
};
|
|
2859
|
-
const messages = this.buildMessages(null);
|
|
2860
|
-
messages.push({
|
|
2861
|
-
role: "user",
|
|
2862
|
-
content: "I'm out of tool-call budget for this turn. Summarize in plain prose what you learned from the tool results above. Do NOT emit any tool calls, function-call markup, DSML invocations, or SEARCH/REPLACE edit blocks \u2014 they will be silently discarded. Just plain text."
|
|
2863
|
-
});
|
|
2864
|
-
const summaryModel = "deepseek-v4-flash";
|
|
2865
|
-
const summaryEffort = "high";
|
|
2866
|
-
const resp = await this.client.chat({
|
|
2867
|
-
model: summaryModel,
|
|
2868
|
-
messages,
|
|
2869
|
-
// no tools → model is forced to answer in text
|
|
2870
|
-
signal: this._turnAbort.signal,
|
|
2871
|
-
thinking: thinkingModeForModel(summaryModel),
|
|
2872
|
-
reasoningEffort: summaryEffort
|
|
2873
|
-
});
|
|
2874
|
-
const rawContent = resp.content?.trim() ?? "";
|
|
2875
|
-
const cleaned = stripHallucinatedToolMarkup(rawContent);
|
|
2876
|
-
const summary = cleaned || "(model emitted fake tool-call markup instead of a prose summary \u2014 try /retry with a narrower question, or /think to inspect R1's reasoning)";
|
|
2877
|
-
const reasonPrefix = reasonPrefixFor(opts.reason, this.maxToolIters);
|
|
2878
|
-
const annotated = `${reasonPrefix}
|
|
2879
|
-
|
|
2880
|
-
${summary}`;
|
|
2881
|
-
const summaryStats = this.stats.record(this._turn, summaryModel, resp.usage ?? new Usage());
|
|
2882
|
-
this.appendAndPersist(
|
|
2883
|
-
this.assistantMessage(summary, [], summaryModel, resp.reasoningContent)
|
|
2884
|
-
);
|
|
2885
|
-
yield {
|
|
2886
|
-
turn: this._turn,
|
|
2887
|
-
role: "assistant_final",
|
|
2888
|
-
content: annotated,
|
|
2889
|
-
stats: summaryStats,
|
|
2890
|
-
forcedSummary: true
|
|
2891
|
-
};
|
|
2892
|
-
yield { turn: this._turn, role: "done", content: summary };
|
|
2893
|
-
} catch (err) {
|
|
2894
|
-
const label = errorLabelFor(opts.reason, this.maxToolIters);
|
|
2895
|
-
yield {
|
|
2896
|
-
turn: this._turn,
|
|
2897
|
-
role: "error",
|
|
2898
|
-
content: "",
|
|
2899
|
-
error: `${label} and the fallback summary call failed: ${err.message}. Run /clear and retry with a narrower question, or raise --max-tool-iters.`
|
|
2900
|
-
};
|
|
2901
|
-
yield { turn: this._turn, role: "done", content: "" };
|
|
2902
|
-
}
|
|
2903
|
-
}
|
|
2904
|
-
async run(userInput, onEvent) {
|
|
2905
|
-
let final = "";
|
|
2906
|
-
for await (const ev of this.step(userInput)) {
|
|
2907
|
-
onEvent?.(ev);
|
|
2908
|
-
if (ev.role === "assistant_final") final = ev.content;
|
|
2909
|
-
if (ev.role === "done") break;
|
|
2910
|
-
}
|
|
2911
|
-
return final;
|
|
2912
|
-
}
|
|
2913
|
-
/** Thinking-mode producer ⇒ reasoning_content MUST be set (even ""), or next call 400s. */
|
|
2914
|
-
assistantMessage(content, toolCalls, producingModel, reasoningContent) {
|
|
2915
|
-
const msg = { role: "assistant", content };
|
|
2916
|
-
if (toolCalls.length > 0) msg.tool_calls = toolCalls;
|
|
2917
|
-
if (isThinkingModeModel(producingModel) || reasoningContent && reasoningContent.length > 0) {
|
|
2918
|
-
msg.reasoning_content = reasoningContent ?? "";
|
|
2919
|
-
}
|
|
2920
|
-
return msg;
|
|
2921
|
-
}
|
|
2922
|
-
/** Abort notices etc — uses this.model as stand-in producer for the thinking-mode stamp. */
|
|
2923
|
-
syntheticAssistantMessage(content) {
|
|
2924
|
-
return this.assistantMessage(content, [], this.model, "");
|
|
2925
|
-
}
|
|
2926
|
-
};
|
|
2927
|
-
function isThinkingModeModel(model) {
|
|
2928
|
-
if (model.includes("reasoner")) return true;
|
|
2929
|
-
if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return true;
|
|
2930
|
-
return false;
|
|
2931
|
-
}
|
|
2932
|
-
function thinkingModeForModel(model) {
|
|
2933
|
-
if (model === "deepseek-chat") return "disabled";
|
|
2934
|
-
if (model.includes("reasoner")) return "enabled";
|
|
2935
|
-
if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return "enabled";
|
|
2936
|
-
return void 0;
|
|
2937
|
-
}
|
|
2938
|
-
function stripHallucinatedToolMarkup(s) {
|
|
2939
|
-
let out = s;
|
|
2940
|
-
out = out.replace(/<|DSML|function_calls>[\s\S]*?<\/?|DSML|function_calls>/g, "");
|
|
2941
|
-
out = out.replace(/<\|DSML\|function_calls>[\s\S]*?<\/?\|DSML\|function_calls>/g, "");
|
|
2942
|
-
out = out.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "");
|
|
2943
|
-
out = out.replace(/<|DSML|[\s\S]*$/g, "");
|
|
2944
|
-
return out.trim();
|
|
2945
|
-
}
|
|
2946
|
-
function parsePositiveIntEnv(raw) {
|
|
2947
|
-
if (!raw) return void 0;
|
|
2948
|
-
const n = Number.parseInt(raw, 10);
|
|
2949
|
-
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
2950
|
-
}
|
|
2951
|
-
function safeParseToolArgs(raw) {
|
|
2952
|
-
try {
|
|
2953
|
-
return JSON.parse(raw);
|
|
2954
|
-
} catch {
|
|
2955
|
-
return raw;
|
|
2956
|
-
}
|
|
2957
|
-
}
|
|
2958
|
-
function looksLikeCompleteJson(s) {
|
|
2959
|
-
if (!s || !s.trim()) return false;
|
|
2960
|
-
try {
|
|
2961
|
-
JSON.parse(s);
|
|
2962
|
-
return true;
|
|
2963
|
-
} catch {
|
|
2964
|
-
return false;
|
|
2965
|
-
}
|
|
2966
|
-
}
|
|
2967
|
-
function* hookWarnings(outcomes, turn) {
|
|
2968
|
-
for (const o of outcomes) {
|
|
2969
|
-
if (o.decision === "pass") continue;
|
|
2970
|
-
yield { turn, role: "warning", content: formatHookOutcomeMessage(o) };
|
|
2971
|
-
}
|
|
2972
|
-
}
|
|
2973
|
-
function reasonPrefixFor(reason, iterCap) {
|
|
2974
|
-
if (reason === "aborted") return "[aborted by user (Esc) \u2014 summarizing what I found so far]";
|
|
2975
|
-
if (reason === "context-guard") {
|
|
2976
|
-
return "[context budget running low \u2014 summarizing before the next call would overflow]";
|
|
2977
|
-
}
|
|
2978
|
-
if (reason === "stuck") {
|
|
2979
|
-
return "[stuck on a repeated tool call \u2014 explaining what was tried and what's blocking progress]";
|
|
2980
|
-
}
|
|
2981
|
-
return `[tool-call budget (${iterCap}) reached \u2014 forcing summary from what I found]`;
|
|
2982
|
-
}
|
|
2983
|
-
function errorLabelFor(reason, iterCap) {
|
|
2984
|
-
if (reason === "aborted") return "aborted by user";
|
|
2985
|
-
if (reason === "context-guard") return "context-guard triggered (prompt > 80% of window)";
|
|
2986
|
-
if (reason === "stuck") return "stuck (repeated tool call suppressed by storm-breaker)";
|
|
2987
|
-
return `tool-call budget (${iterCap}) reached`;
|
|
2988
|
-
}
|
|
2989
|
-
function summarizeBranch(chosen, samples) {
|
|
2990
|
-
return {
|
|
2991
|
-
budget: samples.length,
|
|
2992
|
-
chosenIndex: chosen.index,
|
|
2993
|
-
uncertainties: samples.map((s) => s.planState.uncertainties.length),
|
|
2994
|
-
temperatures: samples.map((s) => s.temperature)
|
|
2995
|
-
};
|
|
2996
|
-
}
|
|
2997
|
-
function shrinkOversizedToolResults(messages, maxChars) {
|
|
2998
|
-
let healedCount = 0;
|
|
2999
|
-
let healedFrom = 0;
|
|
3000
|
-
const out = messages.map((msg) => {
|
|
3001
|
-
if (msg.role !== "tool") return msg;
|
|
3002
|
-
const content = typeof msg.content === "string" ? msg.content : "";
|
|
3003
|
-
if (content.length <= maxChars) return msg;
|
|
3004
|
-
healedCount += 1;
|
|
3005
|
-
healedFrom += content.length;
|
|
3006
|
-
return { ...msg, content: truncateForModel(content, maxChars) };
|
|
3007
|
-
});
|
|
3008
|
-
return { messages: out, healedCount, healedFrom };
|
|
3009
|
-
}
|
|
3010
|
-
function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
|
|
3011
|
-
let healedCount = 0;
|
|
3012
|
-
let tokensSaved = 0;
|
|
3013
|
-
let charsSaved = 0;
|
|
3014
|
-
const out = messages.map((msg) => {
|
|
3015
|
-
if (msg.role !== "tool") return msg;
|
|
3016
|
-
const content = typeof msg.content === "string" ? msg.content : "";
|
|
3017
|
-
if (content.length <= maxTokens) return msg;
|
|
3018
|
-
const beforeTokens = countTokens(content);
|
|
3019
|
-
if (beforeTokens <= maxTokens) return msg;
|
|
3020
|
-
const truncated = truncateForModelByTokens(content, maxTokens);
|
|
3021
|
-
const afterTokens = countTokens(truncated);
|
|
3022
|
-
healedCount += 1;
|
|
3023
|
-
tokensSaved += Math.max(0, beforeTokens - afterTokens);
|
|
3024
|
-
charsSaved += Math.max(0, content.length - truncated.length);
|
|
3025
|
-
return { ...msg, content: truncated };
|
|
3026
|
-
});
|
|
3027
|
-
return { messages: out, healedCount, tokensSaved, charsSaved };
|
|
3028
|
-
}
|
|
3029
|
-
function fixToolCallPairing(messages) {
|
|
3030
|
-
const out = [];
|
|
3031
|
-
let droppedAssistantCalls = 0;
|
|
3032
|
-
let droppedStrayTools = 0;
|
|
3033
|
-
for (let i = 0; i < messages.length; i++) {
|
|
3034
|
-
const msg = messages[i];
|
|
3035
|
-
if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
3036
|
-
const needed = /* @__PURE__ */ new Set();
|
|
3037
|
-
for (const call of msg.tool_calls) {
|
|
3038
|
-
if (call?.id) needed.add(call.id);
|
|
3039
|
-
}
|
|
3040
|
-
const candidates = [];
|
|
3041
|
-
let j = i + 1;
|
|
3042
|
-
while (j < messages.length && needed.size > 0) {
|
|
3043
|
-
const nxt = messages[j];
|
|
3044
|
-
if (nxt.role !== "tool") break;
|
|
3045
|
-
const id = nxt.tool_call_id ?? "";
|
|
3046
|
-
if (!needed.has(id)) break;
|
|
3047
|
-
needed.delete(id);
|
|
3048
|
-
candidates.push(nxt);
|
|
3049
|
-
j++;
|
|
3113
|
+
turn: this._turn,
|
|
3114
|
+
role: "warning",
|
|
3115
|
+
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
|
|
3116
|
+
before / ctxMax * 100
|
|
3117
|
+
)}%) \u2014 ${decision.aggressive ? "aggressively folded" : "folded"} ${result.beforeMessages} messages \u2192 ${result.afterMessages} (summary ${result.summaryChars} chars). Continuing.`
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
} else if (decision.kind === "exit-with-summary") {
|
|
3121
|
+
const before = decision.promptTokens;
|
|
3122
|
+
const ctxMax = decision.ctxMax;
|
|
3123
|
+
yield {
|
|
3124
|
+
turn: this._turn,
|
|
3125
|
+
role: "warning",
|
|
3126
|
+
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
|
|
3127
|
+
before / ctxMax * 100
|
|
3128
|
+
)}%) \u2014 forcing summary from what was gathered. Run /compact, /clear, or /new to reset.`
|
|
3129
|
+
};
|
|
3130
|
+
this.context.trimTrailingToolCalls();
|
|
3131
|
+
yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "context-guard" });
|
|
3132
|
+
return;
|
|
3050
3133
|
}
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3134
|
+
for (const call of repairedCalls) {
|
|
3135
|
+
const name = call.function?.name ?? "";
|
|
3136
|
+
const args = call.function?.arguments ?? "{}";
|
|
3137
|
+
yield {
|
|
3138
|
+
turn: this._turn,
|
|
3139
|
+
role: "tool_start",
|
|
3140
|
+
content: "",
|
|
3141
|
+
toolName: name,
|
|
3142
|
+
toolArgs: args
|
|
3143
|
+
};
|
|
3144
|
+
const parsedArgs = safeParseToolArgs(args);
|
|
3145
|
+
const preReport = await runHooks({
|
|
3146
|
+
hooks: this.hooks,
|
|
3147
|
+
payload: {
|
|
3148
|
+
event: "PreToolUse",
|
|
3149
|
+
cwd: this.hookCwd,
|
|
3150
|
+
toolName: name,
|
|
3151
|
+
toolArgs: parsedArgs
|
|
3152
|
+
}
|
|
3153
|
+
});
|
|
3154
|
+
for (const w of hookWarnings(preReport.outcomes, this._turn)) yield w;
|
|
3155
|
+
let result;
|
|
3156
|
+
if (preReport.blocked) {
|
|
3157
|
+
const blocking = preReport.outcomes[preReport.outcomes.length - 1];
|
|
3158
|
+
const reason = (blocking?.stderr || blocking?.stdout || "blocked by PreToolUse hook").trim();
|
|
3159
|
+
result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
|
|
3160
|
+
${reason}`;
|
|
3161
|
+
} else {
|
|
3162
|
+
result = await this.tools.dispatch(name, args, {
|
|
3163
|
+
signal,
|
|
3164
|
+
maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
|
|
3165
|
+
confirmationGate: this.confirmationGate
|
|
3166
|
+
});
|
|
3167
|
+
const postReport = await runHooks({
|
|
3168
|
+
hooks: this.hooks,
|
|
3169
|
+
payload: {
|
|
3170
|
+
event: "PostToolUse",
|
|
3171
|
+
cwd: this.hookCwd,
|
|
3172
|
+
toolName: name,
|
|
3173
|
+
toolArgs: parsedArgs,
|
|
3174
|
+
toolResult: result
|
|
3175
|
+
}
|
|
3176
|
+
});
|
|
3177
|
+
for (const w of hookWarnings(postReport.outcomes, this._turn)) yield w;
|
|
3178
|
+
}
|
|
3179
|
+
this.appendAndPersist({
|
|
3180
|
+
role: "tool",
|
|
3181
|
+
tool_call_id: call.id ?? "",
|
|
3182
|
+
name,
|
|
3183
|
+
content: result
|
|
3184
|
+
});
|
|
3185
|
+
if (this.noteToolFailureSignal(result)) {
|
|
3186
|
+
yield {
|
|
3187
|
+
turn: this._turn,
|
|
3188
|
+
role: "warning",
|
|
3189
|
+
content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this._turnFailures.formatBreakdown()}. Next turn falls back to ${this.model} unless /pro is armed.`
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
yield {
|
|
3193
|
+
turn: this._turn,
|
|
3194
|
+
role: "tool",
|
|
3195
|
+
content: result,
|
|
3196
|
+
toolName: name,
|
|
3197
|
+
toolArgs: args
|
|
3198
|
+
};
|
|
3059
3199
|
}
|
|
3060
|
-
continue;
|
|
3061
|
-
}
|
|
3062
|
-
if (msg.role === "tool") {
|
|
3063
|
-
droppedStrayTools += 1;
|
|
3064
|
-
continue;
|
|
3065
3200
|
}
|
|
3066
|
-
|
|
3067
|
-
}
|
|
3068
|
-
return { messages: out, droppedAssistantCalls, droppedStrayTools };
|
|
3069
|
-
}
|
|
3070
|
-
function healLoadedMessages(messages, maxChars) {
|
|
3071
|
-
const shrunk = shrinkOversizedToolResults(messages, maxChars);
|
|
3072
|
-
const paired = fixToolCallPairing(shrunk.messages);
|
|
3073
|
-
const healedCount = shrunk.healedCount + paired.droppedAssistantCalls + paired.droppedStrayTools;
|
|
3074
|
-
return { messages: paired.messages, healedCount, healedFrom: shrunk.healedFrom };
|
|
3075
|
-
}
|
|
3076
|
-
function stampMissingReasoningForThinkingMode(messages, model) {
|
|
3077
|
-
if (!isThinkingModeModel(model)) {
|
|
3078
|
-
return { messages, stampedCount: 0 };
|
|
3079
|
-
}
|
|
3080
|
-
let stampedCount = 0;
|
|
3081
|
-
const out = messages.map((msg) => {
|
|
3082
|
-
if (msg.role !== "assistant") return msg;
|
|
3083
|
-
if (Object.hasOwn(msg, "reasoning_content")) return msg;
|
|
3084
|
-
stampedCount += 1;
|
|
3085
|
-
return { ...msg, reasoning_content: "" };
|
|
3086
|
-
});
|
|
3087
|
-
return { messages: out, stampedCount };
|
|
3088
|
-
}
|
|
3089
|
-
function healLoadedMessagesByTokens(messages, maxTokens) {
|
|
3090
|
-
const shrunk = shrinkOversizedToolResultsByTokens(messages, maxTokens);
|
|
3091
|
-
const paired = fixToolCallPairing(shrunk.messages);
|
|
3092
|
-
const healedCount = shrunk.healedCount + paired.droppedAssistantCalls + paired.droppedStrayTools;
|
|
3093
|
-
return {
|
|
3094
|
-
messages: paired.messages,
|
|
3095
|
-
healedCount,
|
|
3096
|
-
tokensSaved: shrunk.tokensSaved,
|
|
3097
|
-
charsSaved: shrunk.charsSaved
|
|
3098
|
-
};
|
|
3099
|
-
}
|
|
3100
|
-
function formatLoopError(err) {
|
|
3101
|
-
const msg = err.message ?? "";
|
|
3102
|
-
if (msg.includes("maximum context length")) {
|
|
3103
|
-
const reqMatch = msg.match(/requested\s+(\d+)\s+tokens/);
|
|
3104
|
-
const requested = reqMatch ? `${Number(reqMatch[1]).toLocaleString()} tokens` : "too many tokens";
|
|
3105
|
-
return `Context overflow (DeepSeek 400): session history is ${requested}, past the model's prompt limit (V4: 1M tokens; legacy chat/reasoner: 131k). Usually a single tool result grew too big. Reasonix caps new tool results at 8k tokens and auto-heals oversized history on session load \u2014 a restart often clears it. If it still overflows, run /forget (delete the session) or /clear (drop the displayed history) to start fresh.`;
|
|
3106
|
-
}
|
|
3107
|
-
const m = /^DeepSeek (\d{3}):\s*([\s\S]*)$/.exec(msg);
|
|
3108
|
-
if (!m) return msg;
|
|
3109
|
-
const status = m[1] ?? "";
|
|
3110
|
-
const body = m[2] ?? "";
|
|
3111
|
-
const inner = extractDeepSeekErrorMessage(body);
|
|
3112
|
-
if (status === "401") {
|
|
3113
|
-
return `Authentication failed (DeepSeek 401): ${inner}. Your API key is rejected. Fix with \`reasonix setup\` or \`export DEEPSEEK_API_KEY=sk-...\`. Get one at https://platform.deepseek.com/api_keys.`;
|
|
3201
|
+
yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "budget" });
|
|
3114
3202
|
}
|
|
3115
|
-
|
|
3116
|
-
return
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3203
|
+
summaryContext() {
|
|
3204
|
+
return {
|
|
3205
|
+
client: this.client,
|
|
3206
|
+
signal: this._turnAbort.signal,
|
|
3207
|
+
buildMessages: () => this.buildMessages(null),
|
|
3208
|
+
appendAndPersist: (m) => this.appendAndPersist(m),
|
|
3209
|
+
recordStats: (model, usage) => this.stats.record(this._turn, model, usage),
|
|
3210
|
+
turn: this._turn,
|
|
3211
|
+
maxToolIters: this.maxToolIters
|
|
3212
|
+
};
|
|
3123
3213
|
}
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
const parsed = JSON.parse(trimmed);
|
|
3131
|
-
if (parsed && typeof parsed === "object") {
|
|
3132
|
-
const obj = parsed;
|
|
3133
|
-
if (obj.error && typeof obj.error.message === "string") return obj.error.message;
|
|
3134
|
-
if (typeof obj.message === "string") return obj.message;
|
|
3214
|
+
async run(userInput, onEvent) {
|
|
3215
|
+
let final = "";
|
|
3216
|
+
for await (const ev of this.step(userInput)) {
|
|
3217
|
+
onEvent?.(ev);
|
|
3218
|
+
if (ev.role === "assistant_final") final = ev.content;
|
|
3219
|
+
if (ev.role === "done") break;
|
|
3135
3220
|
}
|
|
3136
|
-
|
|
3221
|
+
return final;
|
|
3137
3222
|
}
|
|
3138
|
-
|
|
3223
|
+
};
|
|
3224
|
+
function parsePositiveIntEnv(raw) {
|
|
3225
|
+
if (!raw) return void 0;
|
|
3226
|
+
const n = Number.parseInt(raw, 10);
|
|
3227
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
3139
3228
|
}
|
|
3140
3229
|
|
|
3141
3230
|
// src/at-mentions.ts
|
|
@@ -3356,17 +3445,18 @@ function rankPickerCandidates(files, query, limitOrOpts) {
|
|
|
3356
3445
|
var AT_MENTION_PATTERN = /(?<=^|\s)@([a-zA-Z0-9_./\\-]+)/g;
|
|
3357
3446
|
function expandAtMentions(text, rootDir, opts = {}) {
|
|
3358
3447
|
const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
|
|
3359
|
-
const
|
|
3448
|
+
const fs4 = opts.fs ?? defaultFs;
|
|
3360
3449
|
const root = resolve(rootDir);
|
|
3361
3450
|
const seen = /* @__PURE__ */ new Map();
|
|
3362
3451
|
const expansions = [];
|
|
3363
3452
|
for (const match of text.matchAll(AT_MENTION_PATTERN)) {
|
|
3364
3453
|
const rawPath = match[1] ?? "";
|
|
3365
|
-
|
|
3454
|
+
let cleaned = rawPath;
|
|
3455
|
+
while (cleaned.endsWith(".")) cleaned = cleaned.slice(0, -1);
|
|
3366
3456
|
if (!cleaned) continue;
|
|
3367
3457
|
const token = `@${cleaned}`;
|
|
3368
3458
|
if (seen.has(token)) continue;
|
|
3369
|
-
const expansion = resolveMention(cleaned, root, maxBytes,
|
|
3459
|
+
const expansion = resolveMention(cleaned, root, maxBytes, fs4);
|
|
3370
3460
|
seen.set(token, expansion);
|
|
3371
3461
|
expansions.push(expansion);
|
|
3372
3462
|
}
|
|
@@ -3374,7 +3464,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
|
|
|
3374
3464
|
const blocks = [];
|
|
3375
3465
|
for (const ex of expansions) {
|
|
3376
3466
|
if (ex.ok) {
|
|
3377
|
-
const content = readSafe(root, ex.path,
|
|
3467
|
+
const content = readSafe(root, ex.path, fs4);
|
|
3378
3468
|
blocks.push(`<file path="${ex.path}">
|
|
3379
3469
|
${content}
|
|
3380
3470
|
</file>`);
|
|
@@ -3388,7 +3478,7 @@ ${content}
|
|
|
3388
3478
|
${blocks.join("\n\n")}`;
|
|
3389
3479
|
return { text: augmented, expansions };
|
|
3390
3480
|
}
|
|
3391
|
-
function resolveMention(rawPath, root, maxBytes,
|
|
3481
|
+
function resolveMention(rawPath, root, maxBytes, fs4) {
|
|
3392
3482
|
if (isAbsolute(rawPath)) {
|
|
3393
3483
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
|
|
3394
3484
|
}
|
|
@@ -3397,22 +3487,22 @@ function resolveMention(rawPath, root, maxBytes, fs2) {
|
|
|
3397
3487
|
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
3398
3488
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
|
|
3399
3489
|
}
|
|
3400
|
-
if (!
|
|
3490
|
+
if (!fs4.exists(resolved)) {
|
|
3401
3491
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "missing" };
|
|
3402
3492
|
}
|
|
3403
|
-
if (!
|
|
3493
|
+
if (!fs4.isFile(resolved)) {
|
|
3404
3494
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
|
|
3405
3495
|
}
|
|
3406
|
-
const size =
|
|
3496
|
+
const size = fs4.size(resolved);
|
|
3407
3497
|
if (size > maxBytes) {
|
|
3408
3498
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "too-large", bytes: size };
|
|
3409
3499
|
}
|
|
3410
3500
|
return { token: `@${rawPath}`, path: rawPath, ok: true, bytes: size };
|
|
3411
3501
|
}
|
|
3412
|
-
function readSafe(root, rawPath,
|
|
3502
|
+
function readSafe(root, rawPath, fs4) {
|
|
3413
3503
|
const resolved = resolve(root, rawPath);
|
|
3414
3504
|
try {
|
|
3415
|
-
return
|
|
3505
|
+
return fs4.read(resolved);
|
|
3416
3506
|
} catch {
|
|
3417
3507
|
return "(read failed)";
|
|
3418
3508
|
}
|
|
@@ -4161,8 +4251,9 @@ function applyMemoryStack(basePrompt, rootDir) {
|
|
|
4161
4251
|
}
|
|
4162
4252
|
|
|
4163
4253
|
// src/tools/filesystem.ts
|
|
4164
|
-
import { promises as
|
|
4165
|
-
import * as
|
|
4254
|
+
import { promises as fs3 } from "fs";
|
|
4255
|
+
import * as pathMod3 from "path";
|
|
4256
|
+
import picomatch2 from "picomatch";
|
|
4166
4257
|
|
|
4167
4258
|
// src/index/config.ts
|
|
4168
4259
|
import picomatch from "picomatch";
|
|
@@ -4249,6 +4340,214 @@ var DEFAULT_INDEX_EXCLUDES = {
|
|
|
4249
4340
|
};
|
|
4250
4341
|
var DEFAULT_MAX_FILE_BYTES = 256 * 1024;
|
|
4251
4342
|
|
|
4343
|
+
// src/tools/fs/edit.ts
|
|
4344
|
+
import { promises as fs } from "fs";
|
|
4345
|
+
import * as pathMod from "path";
|
|
4346
|
+
function displayRel(rootDir, full) {
|
|
4347
|
+
return pathMod.relative(rootDir, full).replaceAll("\\", "/");
|
|
4348
|
+
}
|
|
4349
|
+
async function applyEdit(rootDir, abs, args) {
|
|
4350
|
+
if (args.search.length === 0) {
|
|
4351
|
+
throw new Error("edit_file: search cannot be empty");
|
|
4352
|
+
}
|
|
4353
|
+
const before = await fs.readFile(abs, "utf8");
|
|
4354
|
+
const le = before.includes("\r\n") ? "\r\n" : "\n";
|
|
4355
|
+
const adaptedSearch = args.search.replace(/\r?\n/g, le);
|
|
4356
|
+
const adaptedReplace = args.replace.replace(/\r?\n/g, le);
|
|
4357
|
+
const firstIdx = before.indexOf(adaptedSearch);
|
|
4358
|
+
if (firstIdx < 0) {
|
|
4359
|
+
throw new Error(`edit_file: search text not found in ${displayRel(rootDir, abs)}`);
|
|
4360
|
+
}
|
|
4361
|
+
const nextIdx = before.indexOf(adaptedSearch, firstIdx + 1);
|
|
4362
|
+
if (nextIdx >= 0) {
|
|
4363
|
+
throw new Error(
|
|
4364
|
+
`edit_file: search text appears multiple times in ${displayRel(rootDir, abs)} \u2014 include more context to disambiguate`
|
|
4365
|
+
);
|
|
4366
|
+
}
|
|
4367
|
+
const after = before.slice(0, firstIdx) + adaptedReplace + before.slice(firstIdx + adaptedSearch.length);
|
|
4368
|
+
await fs.writeFile(abs, after, "utf8");
|
|
4369
|
+
const rel = displayRel(rootDir, abs);
|
|
4370
|
+
const header = `edited ${rel} (${adaptedSearch.length}\u2192${adaptedReplace.length} chars)`;
|
|
4371
|
+
const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
|
|
4372
|
+
const diff = renderEditDiff(adaptedSearch, adaptedReplace, startLine);
|
|
4373
|
+
return `${header}
|
|
4374
|
+
${diff}`;
|
|
4375
|
+
}
|
|
4376
|
+
function renderEditDiff(search, replace, startLine) {
|
|
4377
|
+
const a = search.split(/\r?\n/);
|
|
4378
|
+
const b = replace.split(/\r?\n/);
|
|
4379
|
+
const diff = lineDiff(a, b);
|
|
4380
|
+
const hunk = `@@ -${startLine},${a.length} +${startLine},${b.length} @@`;
|
|
4381
|
+
const body = diff.map((d) => `${d.op === " " ? " " : d.op} ${d.line}`).join("\n");
|
|
4382
|
+
return `${hunk}
|
|
4383
|
+
${body}`;
|
|
4384
|
+
}
|
|
4385
|
+
function lineDiff(a, b) {
|
|
4386
|
+
const n = a.length;
|
|
4387
|
+
const m = b.length;
|
|
4388
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
4389
|
+
for (let i2 = 1; i2 <= n; i2++) {
|
|
4390
|
+
for (let j2 = 1; j2 <= m; j2++) {
|
|
4391
|
+
if (a[i2 - 1] === b[j2 - 1]) dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
4392
|
+
else dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
const out = [];
|
|
4396
|
+
let i = n;
|
|
4397
|
+
let j = m;
|
|
4398
|
+
while (i > 0 && j > 0) {
|
|
4399
|
+
if (a[i - 1] === b[j - 1]) {
|
|
4400
|
+
out.unshift({ op: " ", line: a[i - 1] });
|
|
4401
|
+
i--;
|
|
4402
|
+
j--;
|
|
4403
|
+
} else if ((dp[i - 1][j] ?? 0) > (dp[i][j - 1] ?? 0)) {
|
|
4404
|
+
out.unshift({ op: "-", line: a[i - 1] });
|
|
4405
|
+
i--;
|
|
4406
|
+
} else {
|
|
4407
|
+
out.unshift({ op: "+", line: b[j - 1] });
|
|
4408
|
+
j--;
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
while (i > 0) {
|
|
4412
|
+
out.unshift({ op: "-", line: a[i - 1] });
|
|
4413
|
+
i--;
|
|
4414
|
+
}
|
|
4415
|
+
while (j > 0) {
|
|
4416
|
+
out.unshift({ op: "+", line: b[j - 1] });
|
|
4417
|
+
j--;
|
|
4418
|
+
}
|
|
4419
|
+
return out;
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
// src/tools/fs/search.ts
|
|
4423
|
+
import { promises as fs2 } from "fs";
|
|
4424
|
+
import * as pathMod2 from "path";
|
|
4425
|
+
function displayRel2(rootDir, full) {
|
|
4426
|
+
return pathMod2.relative(rootDir, full).replaceAll("\\", "/");
|
|
4427
|
+
}
|
|
4428
|
+
async function searchFiles(ctx, startAbs, args) {
|
|
4429
|
+
const needle = args.pattern.toLowerCase();
|
|
4430
|
+
const includeDeps = args.include_deps === true;
|
|
4431
|
+
let re = null;
|
|
4432
|
+
try {
|
|
4433
|
+
re = new RegExp(args.pattern, "i");
|
|
4434
|
+
} catch {
|
|
4435
|
+
re = null;
|
|
4436
|
+
}
|
|
4437
|
+
const matches = [];
|
|
4438
|
+
let totalBytes = 0;
|
|
4439
|
+
const walk2 = async (dir) => {
|
|
4440
|
+
let entries;
|
|
4441
|
+
try {
|
|
4442
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
4443
|
+
} catch {
|
|
4444
|
+
return;
|
|
4445
|
+
}
|
|
4446
|
+
for (const e of entries) {
|
|
4447
|
+
const full = pathMod2.join(dir, e.name);
|
|
4448
|
+
const lower = e.name.toLowerCase();
|
|
4449
|
+
const hit = re ? re.test(e.name) : lower.includes(needle);
|
|
4450
|
+
if (hit) {
|
|
4451
|
+
const rel = displayRel2(ctx.rootDir, full);
|
|
4452
|
+
if (totalBytes + rel.length + 1 > ctx.maxListBytes) {
|
|
4453
|
+
matches.push("[\u2026 search truncated \u2014 refine pattern \u2026]");
|
|
4454
|
+
return;
|
|
4455
|
+
}
|
|
4456
|
+
matches.push(rel);
|
|
4457
|
+
totalBytes += rel.length + 1;
|
|
4458
|
+
}
|
|
4459
|
+
if (e.isDirectory()) {
|
|
4460
|
+
if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
|
|
4461
|
+
await walk2(full);
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
};
|
|
4465
|
+
await walk2(startAbs);
|
|
4466
|
+
return matches.length === 0 ? "(no matches)" : matches.join("\n");
|
|
4467
|
+
}
|
|
4468
|
+
async function searchContent(ctx, startAbs, args) {
|
|
4469
|
+
const caseSensitive = args.case_sensitive === true;
|
|
4470
|
+
const includeDeps = args.include_deps === true;
|
|
4471
|
+
let re = null;
|
|
4472
|
+
try {
|
|
4473
|
+
re = new RegExp(args.pattern, caseSensitive ? "" : "i");
|
|
4474
|
+
} catch {
|
|
4475
|
+
re = null;
|
|
4476
|
+
}
|
|
4477
|
+
const needle = caseSensitive ? args.pattern : args.pattern.toLowerCase();
|
|
4478
|
+
const matches = [];
|
|
4479
|
+
let totalBytes = 0;
|
|
4480
|
+
let scanned = 0;
|
|
4481
|
+
let truncated = false;
|
|
4482
|
+
const walk2 = async (dir) => {
|
|
4483
|
+
if (truncated) return;
|
|
4484
|
+
let entries;
|
|
4485
|
+
try {
|
|
4486
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
4487
|
+
} catch {
|
|
4488
|
+
return;
|
|
4489
|
+
}
|
|
4490
|
+
for (const e of entries) {
|
|
4491
|
+
if (truncated) return;
|
|
4492
|
+
if (e.isDirectory()) {
|
|
4493
|
+
if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
|
|
4494
|
+
await walk2(pathMod2.join(dir, e.name));
|
|
4495
|
+
continue;
|
|
4496
|
+
}
|
|
4497
|
+
if (!e.isFile()) continue;
|
|
4498
|
+
const full = pathMod2.join(dir, e.name);
|
|
4499
|
+
if (ctx.nameMatch && !ctx.nameMatch(e.name, displayRel2(ctx.rootDir, full))) continue;
|
|
4500
|
+
if (ctx.isBinaryByName(e.name)) continue;
|
|
4501
|
+
let fh;
|
|
4502
|
+
try {
|
|
4503
|
+
fh = await fs2.open(full, "r");
|
|
4504
|
+
} catch {
|
|
4505
|
+
continue;
|
|
4506
|
+
}
|
|
4507
|
+
let raw;
|
|
4508
|
+
try {
|
|
4509
|
+
const st = await fh.stat();
|
|
4510
|
+
if (st.size > 2 * 1024 * 1024) {
|
|
4511
|
+
await fh.close();
|
|
4512
|
+
continue;
|
|
4513
|
+
}
|
|
4514
|
+
raw = await fh.readFile();
|
|
4515
|
+
} catch {
|
|
4516
|
+
await fh.close().catch(() => {
|
|
4517
|
+
});
|
|
4518
|
+
continue;
|
|
4519
|
+
}
|
|
4520
|
+
await fh.close();
|
|
4521
|
+
const firstNul = raw.indexOf(0);
|
|
4522
|
+
if (firstNul !== -1 && firstNul < 8 * 1024) continue;
|
|
4523
|
+
const text = raw.toString("utf8");
|
|
4524
|
+
const rel = displayRel2(ctx.rootDir, full);
|
|
4525
|
+
const lines = text.split(/\r?\n/);
|
|
4526
|
+
for (let li = 0; li < lines.length; li++) {
|
|
4527
|
+
const line = lines[li];
|
|
4528
|
+
const lineForCheck = caseSensitive ? line : line.toLowerCase();
|
|
4529
|
+
const hit = re ? re.test(line) : lineForCheck.includes(needle);
|
|
4530
|
+
if (!hit) continue;
|
|
4531
|
+
const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
|
|
4532
|
+
const out = `${rel}:${li + 1}: ${display}`;
|
|
4533
|
+
if (totalBytes + out.length + 1 > ctx.maxListBytes) {
|
|
4534
|
+
matches.push(`[\u2026 truncated at ${ctx.maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
|
|
4535
|
+
truncated = true;
|
|
4536
|
+
return;
|
|
4537
|
+
}
|
|
4538
|
+
matches.push(out);
|
|
4539
|
+
totalBytes += out.length + 1;
|
|
4540
|
+
}
|
|
4541
|
+
scanned++;
|
|
4542
|
+
}
|
|
4543
|
+
};
|
|
4544
|
+
await walk2(startAbs);
|
|
4545
|
+
if (matches.length === 0) {
|
|
4546
|
+
return scanned === 0 ? "(no files scanned \u2014 path empty or all files filtered out)" : `(no matches across ${scanned} file${scanned === 1 ? "" : "s"})`;
|
|
4547
|
+
}
|
|
4548
|
+
return matches.join("\n");
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4252
4551
|
// src/tools/filesystem.ts
|
|
4253
4552
|
var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
|
|
4254
4553
|
var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
|
|
@@ -4257,13 +4556,27 @@ var AUTO_PREVIEW_HEAD_LINES = 80;
|
|
|
4257
4556
|
var AUTO_PREVIEW_TAIL_LINES = 40;
|
|
4258
4557
|
var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
|
|
4259
4558
|
var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
|
|
4559
|
+
function displayRel3(rootDir, full) {
|
|
4560
|
+
return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
|
|
4561
|
+
}
|
|
4562
|
+
var GLOB_METACHARS = /[*?{[]/;
|
|
4563
|
+
function compileNameFilter(filter) {
|
|
4564
|
+
if (!filter) return null;
|
|
4565
|
+
if (!GLOB_METACHARS.test(filter)) {
|
|
4566
|
+
const needle = filter.toLowerCase();
|
|
4567
|
+
return (name) => name.toLowerCase().includes(needle);
|
|
4568
|
+
}
|
|
4569
|
+
const matchPath = filter.includes("/");
|
|
4570
|
+
const isMatch = picomatch2(filter, { dot: true, nocase: true });
|
|
4571
|
+
return matchPath ? (_n, rel) => isMatch(rel) : (name) => isMatch(name);
|
|
4572
|
+
}
|
|
4260
4573
|
function isLikelyBinaryByName(name) {
|
|
4261
4574
|
const dot = name.lastIndexOf(".");
|
|
4262
4575
|
if (dot < 0) return false;
|
|
4263
4576
|
return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
|
|
4264
4577
|
}
|
|
4265
4578
|
function registerFilesystemTools(registry, opts) {
|
|
4266
|
-
const rootDir =
|
|
4579
|
+
const rootDir = pathMod3.resolve(opts.rootDir);
|
|
4267
4580
|
const allowWriting = opts.allowWriting !== false;
|
|
4268
4581
|
const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
4269
4582
|
const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
|
|
@@ -4276,10 +4589,10 @@ function registerFilesystemTools(registry, opts) {
|
|
|
4276
4589
|
normalized = normalized.slice(1);
|
|
4277
4590
|
}
|
|
4278
4591
|
if (normalized.length === 0) normalized = ".";
|
|
4279
|
-
const resolved =
|
|
4280
|
-
const normRoot =
|
|
4281
|
-
const rel =
|
|
4282
|
-
if (rel.startsWith("..") ||
|
|
4592
|
+
const resolved = pathMod3.resolve(rootDir, normalized);
|
|
4593
|
+
const normRoot = pathMod3.resolve(rootDir);
|
|
4594
|
+
const rel = pathMod3.relative(normRoot, resolved);
|
|
4595
|
+
if (rel.startsWith("..") || pathMod3.isAbsolute(rel)) {
|
|
4283
4596
|
throw new Error(`path escapes sandbox root (${normRoot}): ${raw}`);
|
|
4284
4597
|
}
|
|
4285
4598
|
return resolved;
|
|
@@ -4307,11 +4620,17 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
|
|
|
4307
4620
|
},
|
|
4308
4621
|
fn: async (args) => {
|
|
4309
4622
|
const abs = safePath(args.path);
|
|
4310
|
-
const
|
|
4311
|
-
|
|
4312
|
-
|
|
4623
|
+
const fh = await fs3.open(abs, "r");
|
|
4624
|
+
let raw;
|
|
4625
|
+
try {
|
|
4626
|
+
const stat2 = await fh.stat();
|
|
4627
|
+
if (stat2.isDirectory()) {
|
|
4628
|
+
throw new Error(`not a file: ${args.path} (it's a directory)`);
|
|
4629
|
+
}
|
|
4630
|
+
raw = await fh.readFile();
|
|
4631
|
+
} finally {
|
|
4632
|
+
await fh.close();
|
|
4313
4633
|
}
|
|
4314
|
-
const raw = await fs.readFile(abs);
|
|
4315
4634
|
if (raw.length > maxReadBytes) {
|
|
4316
4635
|
const headBytes = raw.slice(0, maxReadBytes).toString("utf8");
|
|
4317
4636
|
return `${headBytes}
|
|
@@ -4373,7 +4692,7 @@ ${slice.join("\n")}`;
|
|
|
4373
4692
|
},
|
|
4374
4693
|
fn: async (args) => {
|
|
4375
4694
|
const abs = safePath(args.path ?? ".");
|
|
4376
|
-
const entries = await
|
|
4695
|
+
const entries = await fs3.readdir(abs, { withFileTypes: true });
|
|
4377
4696
|
const lines = [];
|
|
4378
4697
|
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
4379
4698
|
lines.push(e.isDirectory() ? `${e.name}/` : e.name);
|
|
@@ -4416,7 +4735,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4416
4735
|
if (depth > maxDepth) return;
|
|
4417
4736
|
let entries;
|
|
4418
4737
|
try {
|
|
4419
|
-
entries = await
|
|
4738
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
4420
4739
|
} catch {
|
|
4421
4740
|
return;
|
|
4422
4741
|
}
|
|
@@ -4451,7 +4770,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4451
4770
|
lines.push(line);
|
|
4452
4771
|
emitted++;
|
|
4453
4772
|
if (e.isDirectory() && !skip) {
|
|
4454
|
-
await walk2(
|
|
4773
|
+
await walk2(pathMod3.join(dir, e.name), depth + 1);
|
|
4455
4774
|
}
|
|
4456
4775
|
}
|
|
4457
4776
|
};
|
|
@@ -4461,7 +4780,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4461
4780
|
});
|
|
4462
4781
|
registry.register({
|
|
4463
4782
|
name: "search_files",
|
|
4464
|
-
description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line.",
|
|
4783
|
+
description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line. Skips dependency / VCS / build directories (node_modules, .git, dist, build, .next, target, .venv) by default.",
|
|
4465
4784
|
readOnly: true,
|
|
4466
4785
|
parameters: {
|
|
4467
4786
|
type: "object",
|
|
@@ -4470,47 +4789,19 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4470
4789
|
pattern: {
|
|
4471
4790
|
type: "string",
|
|
4472
4791
|
description: "Substring (or regex) to match against filenames."
|
|
4792
|
+
},
|
|
4793
|
+
include_deps: {
|
|
4794
|
+
type: "boolean",
|
|
4795
|
+
description: "When true, also walk node_modules / .git / dist / build / etc. Off by default \u2014 most filename searches are about the user's own code."
|
|
4473
4796
|
}
|
|
4474
4797
|
},
|
|
4475
4798
|
required: ["pattern"]
|
|
4476
4799
|
},
|
|
4477
|
-
fn: async (args) =>
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
re = new RegExp(args.pattern, "i");
|
|
4483
|
-
} catch {
|
|
4484
|
-
re = null;
|
|
4485
|
-
}
|
|
4486
|
-
const matches = [];
|
|
4487
|
-
let totalBytes = 0;
|
|
4488
|
-
const walk2 = async (dir) => {
|
|
4489
|
-
let entries;
|
|
4490
|
-
try {
|
|
4491
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
4492
|
-
} catch {
|
|
4493
|
-
return;
|
|
4494
|
-
}
|
|
4495
|
-
for (const e of entries) {
|
|
4496
|
-
const full = pathMod.join(dir, e.name);
|
|
4497
|
-
const lower = e.name.toLowerCase();
|
|
4498
|
-
const hit = re ? re.test(e.name) : lower.includes(needle);
|
|
4499
|
-
if (hit) {
|
|
4500
|
-
const rel = pathMod.relative(rootDir, full);
|
|
4501
|
-
if (totalBytes + rel.length + 1 > maxListBytes) {
|
|
4502
|
-
matches.push("[\u2026 search truncated \u2014 refine pattern \u2026]");
|
|
4503
|
-
return;
|
|
4504
|
-
}
|
|
4505
|
-
matches.push(rel);
|
|
4506
|
-
totalBytes += rel.length + 1;
|
|
4507
|
-
}
|
|
4508
|
-
if (e.isDirectory()) await walk2(full);
|
|
4509
|
-
}
|
|
4510
|
-
};
|
|
4511
|
-
await walk2(startAbs);
|
|
4512
|
-
return matches.length === 0 ? "(no matches)" : matches.join("\n");
|
|
4513
|
-
}
|
|
4800
|
+
fn: async (args) => searchFiles(
|
|
4801
|
+
{ rootDir, maxListBytes, skipDirNames: SKIP_DIR_NAMES },
|
|
4802
|
+
safePath(args.path ?? "."),
|
|
4803
|
+
args
|
|
4804
|
+
)
|
|
4514
4805
|
});
|
|
4515
4806
|
registry.register({
|
|
4516
4807
|
name: "search_content",
|
|
@@ -4529,7 +4820,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4529
4820
|
},
|
|
4530
4821
|
glob: {
|
|
4531
4822
|
type: "string",
|
|
4532
|
-
description: "Optional
|
|
4823
|
+
description: "Optional filename filter. Real glob when the value contains `*`, `?`, `{`, or `[` \u2014 e.g. '*.ts', '**/*.tsx', 'src/**/*.{ts,tsx}'. Plain substring otherwise \u2014 e.g. '.ts' (suffix), 'test' (anywhere in the name). Patterns containing `/` match against the path relative to the search root; otherwise just the basename."
|
|
4533
4824
|
},
|
|
4534
4825
|
case_sensitive: {
|
|
4535
4826
|
type: "boolean",
|
|
@@ -4542,83 +4833,17 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4542
4833
|
},
|
|
4543
4834
|
required: ["pattern"]
|
|
4544
4835
|
},
|
|
4545
|
-
fn: async (args) =>
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
const needle = caseSensitive ? args.pattern : args.pattern.toLowerCase();
|
|
4557
|
-
const matches = [];
|
|
4558
|
-
let totalBytes = 0;
|
|
4559
|
-
let scanned = 0;
|
|
4560
|
-
let truncated = false;
|
|
4561
|
-
const walk2 = async (dir) => {
|
|
4562
|
-
if (truncated) return;
|
|
4563
|
-
let entries;
|
|
4564
|
-
try {
|
|
4565
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
4566
|
-
} catch {
|
|
4567
|
-
return;
|
|
4568
|
-
}
|
|
4569
|
-
for (const e of entries) {
|
|
4570
|
-
if (truncated) return;
|
|
4571
|
-
if (e.isDirectory()) {
|
|
4572
|
-
if (!includeDeps && SKIP_DIR_NAMES.has(e.name)) continue;
|
|
4573
|
-
await walk2(pathMod.join(dir, e.name));
|
|
4574
|
-
continue;
|
|
4575
|
-
}
|
|
4576
|
-
if (!e.isFile()) continue;
|
|
4577
|
-
if (nameFilter && !e.name.toLowerCase().includes(nameFilter)) continue;
|
|
4578
|
-
if (isLikelyBinaryByName(e.name)) continue;
|
|
4579
|
-
const full = pathMod.join(dir, e.name);
|
|
4580
|
-
let stat2;
|
|
4581
|
-
try {
|
|
4582
|
-
stat2 = await fs.stat(full);
|
|
4583
|
-
} catch {
|
|
4584
|
-
continue;
|
|
4585
|
-
}
|
|
4586
|
-
if (stat2.size > 2 * 1024 * 1024) continue;
|
|
4587
|
-
let raw;
|
|
4588
|
-
try {
|
|
4589
|
-
raw = await fs.readFile(full);
|
|
4590
|
-
} catch {
|
|
4591
|
-
continue;
|
|
4592
|
-
}
|
|
4593
|
-
const firstNul = raw.indexOf(0);
|
|
4594
|
-
if (firstNul !== -1 && firstNul < 8 * 1024) continue;
|
|
4595
|
-
const text = raw.toString("utf8");
|
|
4596
|
-
const rel = pathMod.relative(rootDir, full);
|
|
4597
|
-
const lines = text.split(/\r?\n/);
|
|
4598
|
-
for (let li = 0; li < lines.length; li++) {
|
|
4599
|
-
const line = lines[li];
|
|
4600
|
-
const lineForCheck = caseSensitive ? line : line.toLowerCase();
|
|
4601
|
-
const hit = re ? re.test(line) : lineForCheck.includes(needle);
|
|
4602
|
-
if (!hit) continue;
|
|
4603
|
-
const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
|
|
4604
|
-
const out = `${rel}:${li + 1}: ${display}`;
|
|
4605
|
-
if (totalBytes + out.length + 1 > maxListBytes) {
|
|
4606
|
-
matches.push(`[\u2026 truncated at ${maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
|
|
4607
|
-
truncated = true;
|
|
4608
|
-
return;
|
|
4609
|
-
}
|
|
4610
|
-
matches.push(out);
|
|
4611
|
-
totalBytes += out.length + 1;
|
|
4612
|
-
}
|
|
4613
|
-
scanned++;
|
|
4614
|
-
}
|
|
4615
|
-
};
|
|
4616
|
-
await walk2(startAbs);
|
|
4617
|
-
if (matches.length === 0) {
|
|
4618
|
-
return scanned === 0 ? "(no files scanned \u2014 path empty or all files filtered out)" : `(no matches across ${scanned} file${scanned === 1 ? "" : "s"})`;
|
|
4619
|
-
}
|
|
4620
|
-
return matches.join("\n");
|
|
4621
|
-
}
|
|
4836
|
+
fn: async (args) => searchContent(
|
|
4837
|
+
{
|
|
4838
|
+
rootDir,
|
|
4839
|
+
maxListBytes,
|
|
4840
|
+
skipDirNames: SKIP_DIR_NAMES,
|
|
4841
|
+
isBinaryByName: isLikelyBinaryByName,
|
|
4842
|
+
nameMatch: compileNameFilter(typeof args.glob === "string" ? args.glob : null)
|
|
4843
|
+
},
|
|
4844
|
+
safePath(args.path ?? "."),
|
|
4845
|
+
args
|
|
4846
|
+
)
|
|
4622
4847
|
});
|
|
4623
4848
|
registry.register({
|
|
4624
4849
|
name: "get_file_info",
|
|
@@ -4633,7 +4858,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4633
4858
|
},
|
|
4634
4859
|
fn: async (args) => {
|
|
4635
4860
|
const abs = safePath(args.path);
|
|
4636
|
-
const st = await
|
|
4861
|
+
const st = await fs3.lstat(abs);
|
|
4637
4862
|
const type = st.isDirectory() ? "directory" : st.isSymbolicLink() ? "symlink" : "file";
|
|
4638
4863
|
return JSON.stringify({
|
|
4639
4864
|
type,
|
|
@@ -4656,9 +4881,9 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4656
4881
|
},
|
|
4657
4882
|
fn: async (args) => {
|
|
4658
4883
|
const abs = safePath(args.path);
|
|
4659
|
-
await
|
|
4660
|
-
await
|
|
4661
|
-
return `wrote ${args.content.length} chars to ${
|
|
4884
|
+
await fs3.mkdir(pathMod3.dirname(abs), { recursive: true });
|
|
4885
|
+
await fs3.writeFile(abs, args.content, "utf8");
|
|
4886
|
+
return `wrote ${args.content.length} chars to ${displayRel3(rootDir, abs)}`;
|
|
4662
4887
|
}
|
|
4663
4888
|
});
|
|
4664
4889
|
registry.register({
|
|
@@ -4673,34 +4898,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4673
4898
|
},
|
|
4674
4899
|
required: ["path", "search", "replace"]
|
|
4675
4900
|
},
|
|
4676
|
-
fn: async (args) =>
|
|
4677
|
-
const abs = safePath(args.path);
|
|
4678
|
-
const before = await fs.readFile(abs, "utf8");
|
|
4679
|
-
if (args.search.length === 0) {
|
|
4680
|
-
throw new Error("edit_file: search cannot be empty");
|
|
4681
|
-
}
|
|
4682
|
-
const le = before.includes("\r\n") ? "\r\n" : "\n";
|
|
4683
|
-
const adaptedSearch = args.search.replace(/\r?\n/g, le);
|
|
4684
|
-
const adaptedReplace = args.replace.replace(/\r?\n/g, le);
|
|
4685
|
-
const firstIdx = before.indexOf(adaptedSearch);
|
|
4686
|
-
if (firstIdx < 0) {
|
|
4687
|
-
throw new Error(`edit_file: search text not found in ${pathMod.relative(rootDir, abs)}`);
|
|
4688
|
-
}
|
|
4689
|
-
const nextIdx = before.indexOf(adaptedSearch, firstIdx + 1);
|
|
4690
|
-
if (nextIdx >= 0) {
|
|
4691
|
-
throw new Error(
|
|
4692
|
-
`edit_file: search text appears multiple times in ${pathMod.relative(rootDir, abs)} \u2014 include more context to disambiguate`
|
|
4693
|
-
);
|
|
4694
|
-
}
|
|
4695
|
-
const after = before.slice(0, firstIdx) + adaptedReplace + before.slice(firstIdx + adaptedSearch.length);
|
|
4696
|
-
await fs.writeFile(abs, after, "utf8");
|
|
4697
|
-
const rel = pathMod.relative(rootDir, abs);
|
|
4698
|
-
const header = `edited ${rel} (${adaptedSearch.length}\u2192${adaptedReplace.length} chars)`;
|
|
4699
|
-
const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
|
|
4700
|
-
const diff = renderEditDiff(adaptedSearch, adaptedReplace, startLine);
|
|
4701
|
-
return `${header}
|
|
4702
|
-
${diff}`;
|
|
4703
|
-
}
|
|
4901
|
+
fn: async (args) => applyEdit(rootDir, safePath(args.path), args)
|
|
4704
4902
|
});
|
|
4705
4903
|
registry.register({
|
|
4706
4904
|
name: "create_directory",
|
|
@@ -4712,8 +4910,8 @@ ${diff}`;
|
|
|
4712
4910
|
},
|
|
4713
4911
|
fn: async (args) => {
|
|
4714
4912
|
const abs = safePath(args.path);
|
|
4715
|
-
await
|
|
4716
|
-
return `created ${
|
|
4913
|
+
await fs3.mkdir(abs, { recursive: true });
|
|
4914
|
+
return `created ${displayRel3(rootDir, abs)}/`;
|
|
4717
4915
|
}
|
|
4718
4916
|
});
|
|
4719
4917
|
registry.register({
|
|
@@ -4730,58 +4928,13 @@ ${diff}`;
|
|
|
4730
4928
|
fn: async (args) => {
|
|
4731
4929
|
const src = safePath(args.source);
|
|
4732
4930
|
const dst = safePath(args.destination);
|
|
4733
|
-
await
|
|
4734
|
-
await
|
|
4735
|
-
return `moved ${
|
|
4931
|
+
await fs3.mkdir(pathMod3.dirname(dst), { recursive: true });
|
|
4932
|
+
await fs3.rename(src, dst);
|
|
4933
|
+
return `moved ${displayRel3(rootDir, src)} \u2192 ${displayRel3(rootDir, dst)}`;
|
|
4736
4934
|
}
|
|
4737
4935
|
});
|
|
4738
4936
|
return registry;
|
|
4739
4937
|
}
|
|
4740
|
-
function renderEditDiff(search, replace, startLine) {
|
|
4741
|
-
const a = search.split(/\r?\n/);
|
|
4742
|
-
const b = replace.split(/\r?\n/);
|
|
4743
|
-
const diff = lineDiff(a, b);
|
|
4744
|
-
const hunk = `@@ -${startLine},${a.length} +${startLine},${b.length} @@`;
|
|
4745
|
-
const body = diff.map((d) => `${d.op === " " ? " " : d.op} ${d.line}`).join("\n");
|
|
4746
|
-
return `${hunk}
|
|
4747
|
-
${body}`;
|
|
4748
|
-
}
|
|
4749
|
-
function lineDiff(a, b) {
|
|
4750
|
-
const n = a.length;
|
|
4751
|
-
const m = b.length;
|
|
4752
|
-
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
4753
|
-
for (let i2 = 1; i2 <= n; i2++) {
|
|
4754
|
-
for (let j2 = 1; j2 <= m; j2++) {
|
|
4755
|
-
if (a[i2 - 1] === b[j2 - 1]) dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
4756
|
-
else dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
4757
|
-
}
|
|
4758
|
-
}
|
|
4759
|
-
const out = [];
|
|
4760
|
-
let i = n;
|
|
4761
|
-
let j = m;
|
|
4762
|
-
while (i > 0 && j > 0) {
|
|
4763
|
-
if (a[i - 1] === b[j - 1]) {
|
|
4764
|
-
out.unshift({ op: " ", line: a[i - 1] });
|
|
4765
|
-
i--;
|
|
4766
|
-
j--;
|
|
4767
|
-
} else if ((dp[i - 1][j] ?? 0) > (dp[i][j - 1] ?? 0)) {
|
|
4768
|
-
out.unshift({ op: "-", line: a[i - 1] });
|
|
4769
|
-
i--;
|
|
4770
|
-
} else {
|
|
4771
|
-
out.unshift({ op: "+", line: b[j - 1] });
|
|
4772
|
-
j--;
|
|
4773
|
-
}
|
|
4774
|
-
}
|
|
4775
|
-
while (i > 0) {
|
|
4776
|
-
out.unshift({ op: "-", line: a[i - 1] });
|
|
4777
|
-
i--;
|
|
4778
|
-
}
|
|
4779
|
-
while (j > 0) {
|
|
4780
|
-
out.unshift({ op: "+", line: b[j - 1] });
|
|
4781
|
-
j--;
|
|
4782
|
-
}
|
|
4783
|
-
return out;
|
|
4784
|
-
}
|
|
4785
4938
|
|
|
4786
4939
|
// src/tools/memory.ts
|
|
4787
4940
|
function registerMemoryTools(registry, opts = {}) {
|
|
@@ -5499,16 +5652,14 @@ function forkRegistryExcluding(parent, exclude) {
|
|
|
5499
5652
|
}
|
|
5500
5653
|
|
|
5501
5654
|
// src/tools/shell.ts
|
|
5502
|
-
import
|
|
5503
|
-
import { existsSync as existsSync8, statSync as statSync4 } from "fs";
|
|
5504
|
-
import * as pathMod4 from "path";
|
|
5655
|
+
import * as pathMod7 from "path";
|
|
5505
5656
|
|
|
5506
5657
|
// src/config.ts
|
|
5507
5658
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
|
|
5508
5659
|
import { homedir as homedir5 } from "os";
|
|
5509
|
-
import { dirname as dirname4, join as
|
|
5660
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
5510
5661
|
function defaultConfigPath() {
|
|
5511
|
-
return
|
|
5662
|
+
return join10(homedir5(), ".reasonix", "config.json");
|
|
5512
5663
|
}
|
|
5513
5664
|
function readConfig(path2 = defaultConfigPath()) {
|
|
5514
5665
|
try {
|
|
@@ -5559,7 +5710,7 @@ function redactKey(key) {
|
|
|
5559
5710
|
|
|
5560
5711
|
// src/tools/jobs.ts
|
|
5561
5712
|
import { spawn as spawn2 } from "child_process";
|
|
5562
|
-
import * as
|
|
5713
|
+
import * as pathMod4 from "path";
|
|
5563
5714
|
function killProcessTree(pid, signal) {
|
|
5564
5715
|
if (process.platform === "win32") {
|
|
5565
5716
|
const args = ["/pid", String(pid), "/T"];
|
|
@@ -5619,7 +5770,7 @@ var JobRegistry = class {
|
|
|
5619
5770
|
const maxBytes = opts.maxBufferBytes ?? DEFAULT_OUTPUT_CAP_BYTES;
|
|
5620
5771
|
const { bin, args, spawnOverrides } = prepareSpawn(argv);
|
|
5621
5772
|
const spawnOpts = {
|
|
5622
|
-
cwd:
|
|
5773
|
+
cwd: pathMod4.resolve(opts.cwd),
|
|
5623
5774
|
shell: false,
|
|
5624
5775
|
windowsHide: true,
|
|
5625
5776
|
env: process.env,
|
|
@@ -5650,6 +5801,9 @@ var JobRegistry = class {
|
|
|
5650
5801
|
child: null,
|
|
5651
5802
|
readyPromise: Promise.resolve(),
|
|
5652
5803
|
signalReady: () => {
|
|
5804
|
+
},
|
|
5805
|
+
closedPromise: Promise.resolve(),
|
|
5806
|
+
signalClosed: () => {
|
|
5653
5807
|
}
|
|
5654
5808
|
};
|
|
5655
5809
|
this.jobs.set(id2, job2);
|
|
@@ -5668,6 +5822,11 @@ var JobRegistry = class {
|
|
|
5668
5822
|
const readyPromise = new Promise((res) => {
|
|
5669
5823
|
readyResolve = res;
|
|
5670
5824
|
});
|
|
5825
|
+
let closedResolve = () => {
|
|
5826
|
+
};
|
|
5827
|
+
const closedPromise = new Promise((res) => {
|
|
5828
|
+
closedResolve = res;
|
|
5829
|
+
});
|
|
5671
5830
|
const job = {
|
|
5672
5831
|
id,
|
|
5673
5832
|
command: trimmed,
|
|
@@ -5679,7 +5838,9 @@ var JobRegistry = class {
|
|
|
5679
5838
|
running: true,
|
|
5680
5839
|
child,
|
|
5681
5840
|
readyPromise,
|
|
5682
|
-
signalReady: readyResolve
|
|
5841
|
+
signalReady: readyResolve,
|
|
5842
|
+
closedPromise,
|
|
5843
|
+
signalClosed: closedResolve
|
|
5683
5844
|
};
|
|
5684
5845
|
this.jobs.set(id, job);
|
|
5685
5846
|
let readyMatched = false;
|
|
@@ -5713,11 +5874,13 @@ ${job.output.slice(start)}`;
|
|
|
5713
5874
|
job.running = false;
|
|
5714
5875
|
job.spawnError = err.message;
|
|
5715
5876
|
job.signalReady();
|
|
5877
|
+
job.signalClosed();
|
|
5716
5878
|
});
|
|
5717
5879
|
child.on("close", (code) => {
|
|
5718
5880
|
job.running = false;
|
|
5719
5881
|
job.exitCode = code;
|
|
5720
5882
|
job.signalReady();
|
|
5883
|
+
job.signalClosed();
|
|
5721
5884
|
});
|
|
5722
5885
|
const onAbort = () => this.stop(id, { graceMs: 100 });
|
|
5723
5886
|
if (opts.signal?.aborted) {
|
|
@@ -5779,7 +5942,7 @@ ${job.output.slice(start)}`;
|
|
|
5779
5942
|
} catch {
|
|
5780
5943
|
}
|
|
5781
5944
|
}
|
|
5782
|
-
await Promise.race([job.
|
|
5945
|
+
await Promise.race([job.closedPromise, new Promise((res) => setTimeout(res, graceMs))]);
|
|
5783
5946
|
if (job.running) {
|
|
5784
5947
|
if (job.pid !== null) {
|
|
5785
5948
|
killProcessTree(job.pid, "SIGKILL");
|
|
@@ -5789,7 +5952,7 @@ ${job.output.slice(start)}`;
|
|
|
5789
5952
|
} catch {
|
|
5790
5953
|
}
|
|
5791
5954
|
}
|
|
5792
|
-
await new Promise((res) => setTimeout(res,
|
|
5955
|
+
await Promise.race([job.closedPromise, new Promise((res) => setTimeout(res, 5e3))]);
|
|
5793
5956
|
}
|
|
5794
5957
|
return snapshot(job);
|
|
5795
5958
|
}
|
|
@@ -5845,10 +6008,15 @@ function snapshot(job) {
|
|
|
5845
6008
|
};
|
|
5846
6009
|
}
|
|
5847
6010
|
|
|
6011
|
+
// src/tools/shell/exec.ts
|
|
6012
|
+
import { spawn as spawn4, spawnSync } from "child_process";
|
|
6013
|
+
import { existsSync as existsSync8, statSync as statSync4 } from "fs";
|
|
6014
|
+
import * as pathMod6 from "path";
|
|
6015
|
+
|
|
5848
6016
|
// src/tools/shell-chain.ts
|
|
5849
6017
|
import { spawn as spawn3 } from "child_process";
|
|
5850
6018
|
import { closeSync, openSync } from "fs";
|
|
5851
|
-
import * as
|
|
6019
|
+
import * as pathMod5 from "path";
|
|
5852
6020
|
var UnsupportedSyntaxError = class extends Error {
|
|
5853
6021
|
constructor(detail) {
|
|
5854
6022
|
super(`run_command: ${detail}`);
|
|
@@ -6044,6 +6212,14 @@ function parseCommandChain(cmd) {
|
|
|
6044
6212
|
}
|
|
6045
6213
|
segments.push(parseSegment(trimmed));
|
|
6046
6214
|
}
|
|
6215
|
+
for (const seg of segments) {
|
|
6216
|
+
const cmdName = seg.argv[0] ?? "";
|
|
6217
|
+
if (cmdName.toLowerCase() === "cd") {
|
|
6218
|
+
throw new UnsupportedSyntaxError(
|
|
6219
|
+
"cd in parsed command chains does not change cwd for later segments. Use a command-native cwd flag instead, such as `npm --prefix <dir> run <script>`, `git -C <dir> ...`, or `cargo -C <dir> ...`."
|
|
6220
|
+
);
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6047
6223
|
if (ops.length === 0 && segments[0].redirects.length === 0) return null;
|
|
6048
6224
|
return { segments, ops };
|
|
6049
6225
|
}
|
|
@@ -6107,7 +6283,7 @@ function openRedirects(redirects, cwd) {
|
|
|
6107
6283
|
let bothFd = null;
|
|
6108
6284
|
const toClose = [];
|
|
6109
6285
|
const open = (target, flags) => {
|
|
6110
|
-
const resolved =
|
|
6286
|
+
const resolved = pathMod5.resolve(cwd, target);
|
|
6111
6287
|
const fd = openSync(resolved, flags);
|
|
6112
6288
|
toClose.push(fd);
|
|
6113
6289
|
return fd;
|
|
@@ -6256,31 +6432,7 @@ var OutputBuffer = class {
|
|
|
6256
6432
|
}
|
|
6257
6433
|
};
|
|
6258
6434
|
|
|
6259
|
-
// src/tools/shell.ts
|
|
6260
|
-
function killProcessTree2(child) {
|
|
6261
|
-
if (!child.pid || child.killed) return;
|
|
6262
|
-
if (process.platform === "win32") {
|
|
6263
|
-
try {
|
|
6264
|
-
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
6265
|
-
stdio: "ignore",
|
|
6266
|
-
windowsHide: true
|
|
6267
|
-
});
|
|
6268
|
-
return;
|
|
6269
|
-
} catch {
|
|
6270
|
-
}
|
|
6271
|
-
}
|
|
6272
|
-
try {
|
|
6273
|
-
process.kill(-child.pid, "SIGKILL");
|
|
6274
|
-
return;
|
|
6275
|
-
} catch {
|
|
6276
|
-
}
|
|
6277
|
-
try {
|
|
6278
|
-
child.kill("SIGKILL");
|
|
6279
|
-
} catch {
|
|
6280
|
-
}
|
|
6281
|
-
}
|
|
6282
|
-
var DEFAULT_TIMEOUT_SEC = 60;
|
|
6283
|
-
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
6435
|
+
// src/tools/shell/parse.ts
|
|
6284
6436
|
var BUILTIN_ALLOWLIST = [
|
|
6285
6437
|
// Repo inspection
|
|
6286
6438
|
"git status",
|
|
@@ -6493,6 +6645,32 @@ function isCommandAllowed(cmd, extra = []) {
|
|
|
6493
6645
|
if (chain === null) return isAllowed(cmd, extra);
|
|
6494
6646
|
return chainAllowed(chain, (seg) => isAllowed(seg, extra));
|
|
6495
6647
|
}
|
|
6648
|
+
|
|
6649
|
+
// src/tools/shell/exec.ts
|
|
6650
|
+
var DEFAULT_TIMEOUT_SEC = 60;
|
|
6651
|
+
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
6652
|
+
function killProcessTree2(child) {
|
|
6653
|
+
if (!child.pid || child.killed) return;
|
|
6654
|
+
if (process.platform === "win32") {
|
|
6655
|
+
try {
|
|
6656
|
+
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
6657
|
+
stdio: "ignore",
|
|
6658
|
+
windowsHide: true
|
|
6659
|
+
});
|
|
6660
|
+
return;
|
|
6661
|
+
} catch {
|
|
6662
|
+
}
|
|
6663
|
+
}
|
|
6664
|
+
try {
|
|
6665
|
+
process.kill(-child.pid, "SIGKILL");
|
|
6666
|
+
return;
|
|
6667
|
+
} catch {
|
|
6668
|
+
}
|
|
6669
|
+
try {
|
|
6670
|
+
child.kill("SIGKILL");
|
|
6671
|
+
} catch {
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6496
6674
|
async function runCommand(cmd, opts) {
|
|
6497
6675
|
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
|
|
6498
6676
|
const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
@@ -6601,16 +6779,16 @@ function resolveExecutable(cmd, opts = {}) {
|
|
|
6601
6779
|
const platform = opts.platform ?? process.platform;
|
|
6602
6780
|
if (platform !== "win32") return cmd;
|
|
6603
6781
|
if (!cmd) return cmd;
|
|
6604
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
6605
|
-
if (
|
|
6782
|
+
if (cmd.includes("/") || cmd.includes("\\") || pathMod6.isAbsolute(cmd)) return cmd;
|
|
6783
|
+
if (pathMod6.extname(cmd)) return cmd;
|
|
6606
6784
|
const env = opts.env ?? process.env;
|
|
6607
6785
|
const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
|
|
6608
|
-
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" :
|
|
6786
|
+
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod6.delimiter);
|
|
6609
6787
|
const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
|
|
6610
6788
|
const isFile = opts.isFile ?? defaultIsFile;
|
|
6611
6789
|
for (const dir of pathDirs) {
|
|
6612
6790
|
for (const ext of pathExt) {
|
|
6613
|
-
const full =
|
|
6791
|
+
const full = pathMod6.win32.join(dir, cmd + ext);
|
|
6614
6792
|
if (isFile(full)) return full;
|
|
6615
6793
|
}
|
|
6616
6794
|
}
|
|
@@ -6680,8 +6858,8 @@ function withUtf8Codepage(cmdline) {
|
|
|
6680
6858
|
function isBareWindowsName(s) {
|
|
6681
6859
|
if (!s) return false;
|
|
6682
6860
|
if (s.includes("/") || s.includes("\\")) return false;
|
|
6683
|
-
if (
|
|
6684
|
-
if (
|
|
6861
|
+
if (pathMod6.isAbsolute(s)) return false;
|
|
6862
|
+
if (pathMod6.extname(s)) return false;
|
|
6685
6863
|
return true;
|
|
6686
6864
|
}
|
|
6687
6865
|
function quoteForCmdExe(arg) {
|
|
@@ -6689,6 +6867,8 @@ function quoteForCmdExe(arg) {
|
|
|
6689
6867
|
if (!/[\s"&|<>^%(),;!]/.test(arg)) return arg;
|
|
6690
6868
|
return `"${arg.replace(/"/g, '""')}"`;
|
|
6691
6869
|
}
|
|
6870
|
+
|
|
6871
|
+
// src/tools/shell.ts
|
|
6692
6872
|
var NeedsConfirmationError = class extends Error {
|
|
6693
6873
|
command;
|
|
6694
6874
|
constructor(command) {
|
|
@@ -6700,7 +6880,7 @@ var NeedsConfirmationError = class extends Error {
|
|
|
6700
6880
|
}
|
|
6701
6881
|
};
|
|
6702
6882
|
function registerShellTools(registry, opts) {
|
|
6703
|
-
const rootDir =
|
|
6883
|
+
const rootDir = pathMod7.resolve(opts.rootDir);
|
|
6704
6884
|
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
|
|
6705
6885
|
const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
6706
6886
|
const jobs = opts.jobs ?? new JobRegistry();
|
|
@@ -6711,7 +6891,7 @@ function registerShellTools(registry, opts) {
|
|
|
6711
6891
|
const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
|
|
6712
6892
|
registry.register({
|
|
6713
6893
|
name: "run_command",
|
|
6714
|
-
description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022 Chain operators `|`, `||`, `&&`, `;` ARE supported \u2014 parsed natively, no shell invoked, so semantics are identical on Windows / macOS / Linux. Each chain segment is allowlist-checked individually: `git status | grep main` runs if both halves are allowed.\n\u2022 File redirects ARE supported: `>` truncate, `>>` append, `<` stdin from file, `2>` / `2>>` stderr to file, `2>&1` merge stderr\u2192stdout, `&>` both to file. Targets resolve relative to the project root. At most one redirect per fd per segment.\n\u2022 Background `&`, heredoc `<<`, command substitution `$(\u2026)`, subshells `(\u2026)`, and process substitution `<(\u2026)` are NOT supported. Wrap a literal `&` arg in quotes; for input use a `<` file or the binary's own --input flag.\n\u2022 Env-var expansion `$VAR` is NOT performed \u2014 `$VAR` is passed as a literal string. Use the binary's own --env flag or substitute the value yourself.\n\u2022 `cd` DOES NOT PERSIST between calls \u2014 each call spawns a fresh process rooted at the project.
|
|
6894
|
+
description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022 Chain operators `|`, `||`, `&&`, `;` ARE supported \u2014 parsed natively, no shell invoked, so semantics are identical on Windows / macOS / Linux. Each chain segment is allowlist-checked individually: `git status | grep main` runs if both halves are allowed.\n\u2022 File redirects ARE supported: `>` truncate, `>>` append, `<` stdin from file, `2>` / `2>>` stderr to file, `2>&1` merge stderr\u2192stdout, `&>` both to file. Targets resolve relative to the project root. At most one redirect per fd per segment.\n\u2022 Background `&`, heredoc `<<`, command substitution `$(\u2026)`, subshells `(\u2026)`, and process substitution `<(\u2026)` are NOT supported. Wrap a literal `&` arg in quotes; for input use a `<` file or the binary's own --input flag.\n\u2022 Env-var expansion `$VAR` is NOT performed \u2014 `$VAR` is passed as a literal string. Use the binary's own --env flag or substitute the value yourself.\n\u2022 `cd` DOES NOT PERSIST between calls \u2014 each call spawns a fresh process rooted at the project. `cd` also does not persist within parsed chains like `cd dir && command`. Use a command-native cwd flag instead: `npm --prefix <dir> run <script>`, `npm --prefix <dir> exec -- <bin>`, `git -C <dir> ...`, `cargo -C <dir> ...`, `pytest <dir>/tests`.\n\u2022 Glob patterns (`*.ts`) are passed through as literal arguments \u2014 no shell expansion. Use `grep -r`, `rg`, `find -name`, etc.\n\u2022 Avoid commands with unbounded output (`netstat -ano`, `find /`, etc.) \u2014 they waste tokens. Filter at source: `netstat -ano -p TCP`, `find src -name '*.ts'`, `grep -c`, `wc -l`.\n\nCommon read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
|
|
6715
6895
|
// Plan-mode gate: allow allowlisted commands through (git status,
|
|
6716
6896
|
// cargo check, ls, grep …) so the model can actually investigate
|
|
6717
6897
|
// during planning. Anything that would otherwise trigger a
|
|
@@ -6899,6 +7079,7 @@ ${r.output}` : header;
|
|
|
6899
7079
|
}
|
|
6900
7080
|
|
|
6901
7081
|
// src/tools/web.ts
|
|
7082
|
+
import { parse as parseHtml } from "node-html-parser";
|
|
6902
7083
|
var DEFAULT_FETCH_MAX_CHARS = 32e3;
|
|
6903
7084
|
var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
|
|
6904
7085
|
var DEFAULT_TOPK = 5;
|
|
@@ -7028,28 +7209,70 @@ async function readBodyCapped(resp, maxBytes) {
|
|
|
7028
7209
|
}
|
|
7029
7210
|
return out;
|
|
7030
7211
|
}
|
|
7212
|
+
var MAX_HTML_INPUT = 5 * 1024 * 1024;
|
|
7213
|
+
var STRIP_BLOCK_TAGS = "script, style, noscript, nav, footer, aside, svg";
|
|
7214
|
+
var BLOCK_BREAK_TAGS = /* @__PURE__ */ new Set([
|
|
7215
|
+
"p",
|
|
7216
|
+
"div",
|
|
7217
|
+
"br",
|
|
7218
|
+
"h1",
|
|
7219
|
+
"h2",
|
|
7220
|
+
"h3",
|
|
7221
|
+
"h4",
|
|
7222
|
+
"h5",
|
|
7223
|
+
"h6",
|
|
7224
|
+
"li",
|
|
7225
|
+
"tr",
|
|
7226
|
+
"section",
|
|
7227
|
+
"article"
|
|
7228
|
+
]);
|
|
7031
7229
|
function htmlToText(html) {
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
s =
|
|
7038
|
-
s = s.replace(/<aside[\s\S]*?<\/aside>/gi, "");
|
|
7039
|
-
s = s.replace(/<svg[\s\S]*?<\/svg>/gi, "");
|
|
7040
|
-
s = s.replace(/<\/?(p|div|br|h[1-6]|li|tr|section|article)\b[^>]*>/gi, "\n");
|
|
7041
|
-
s = s.replace(/<[^>]+>/g, "");
|
|
7230
|
+
const input = html.length > MAX_HTML_INPUT ? html.slice(0, MAX_HTML_INPUT) : html;
|
|
7231
|
+
const root = parseHtml(input);
|
|
7232
|
+
for (const node of root.querySelectorAll(STRIP_BLOCK_TAGS)) node.remove();
|
|
7233
|
+
const out = [];
|
|
7234
|
+
walkExtract(root, out);
|
|
7235
|
+
let s = out.join("");
|
|
7042
7236
|
s = decodeHtmlEntities(s);
|
|
7043
7237
|
s = s.replace(/[ \t]+/g, " ");
|
|
7044
7238
|
s = s.replace(/\n[ \t]+/g, "\n");
|
|
7045
7239
|
s = s.replace(/\n{3,}/g, "\n\n");
|
|
7046
7240
|
return s.trim();
|
|
7047
7241
|
}
|
|
7048
|
-
function
|
|
7049
|
-
|
|
7242
|
+
function walkExtract(node, out) {
|
|
7243
|
+
if (node.nodeType === 3) {
|
|
7244
|
+
out.push(node.rawText ?? node.text ?? "");
|
|
7245
|
+
return;
|
|
7246
|
+
}
|
|
7247
|
+
const tag = node.rawTagName?.toLowerCase();
|
|
7248
|
+
const isBreak = tag !== void 0 && BLOCK_BREAK_TAGS.has(tag);
|
|
7249
|
+
if (isBreak) out.push("\n");
|
|
7250
|
+
for (const child of node.childNodes) walkExtract(child, out);
|
|
7251
|
+
if (isBreak) out.push("\n");
|
|
7050
7252
|
}
|
|
7253
|
+
function stripHtml(s) {
|
|
7254
|
+
return parseHtml(s).text;
|
|
7255
|
+
}
|
|
7256
|
+
var HTML_ENTITIES = {
|
|
7257
|
+
amp: "&",
|
|
7258
|
+
lt: "<",
|
|
7259
|
+
gt: ">",
|
|
7260
|
+
quot: '"',
|
|
7261
|
+
apos: "'",
|
|
7262
|
+
nbsp: " "
|
|
7263
|
+
};
|
|
7051
7264
|
function decodeHtmlEntities(s) {
|
|
7052
|
-
return s.replace(/&
|
|
7265
|
+
return s.replace(/&(#\d+|#x[0-9a-fA-F]+|\w+);/g, (raw, name) => {
|
|
7266
|
+
if (name.startsWith("#x") || name.startsWith("#X")) {
|
|
7267
|
+
const code = Number.parseInt(name.slice(2), 16);
|
|
7268
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : raw;
|
|
7269
|
+
}
|
|
7270
|
+
if (name.startsWith("#")) {
|
|
7271
|
+
const code = Number.parseInt(name.slice(1), 10);
|
|
7272
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : raw;
|
|
7273
|
+
}
|
|
7274
|
+
return HTML_ENTITIES[name.toLowerCase()] ?? raw;
|
|
7275
|
+
});
|
|
7053
7276
|
}
|
|
7054
7277
|
function extractTitle(html) {
|
|
7055
7278
|
const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
@@ -7644,7 +7867,7 @@ function truncate(s, n) {
|
|
|
7644
7867
|
// src/version.ts
|
|
7645
7868
|
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
|
|
7646
7869
|
import { homedir as homedir6 } from "os";
|
|
7647
|
-
import { dirname as dirname5, join as
|
|
7870
|
+
import { dirname as dirname5, join as join11 } from "path";
|
|
7648
7871
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7649
7872
|
var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
|
|
7650
7873
|
var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -7653,7 +7876,7 @@ function readPackageVersion() {
|
|
|
7653
7876
|
try {
|
|
7654
7877
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
7655
7878
|
for (let i = 0; i < 6; i++) {
|
|
7656
|
-
const p =
|
|
7879
|
+
const p = join11(dir, "package.json");
|
|
7657
7880
|
if (existsSync9(p)) {
|
|
7658
7881
|
const pkg = JSON.parse(readFileSync12(p, "utf8"));
|
|
7659
7882
|
if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
|
|
@@ -7670,7 +7893,7 @@ function readPackageVersion() {
|
|
|
7670
7893
|
}
|
|
7671
7894
|
var VERSION = readPackageVersion();
|
|
7672
7895
|
function cachePath(homeDirOverride) {
|
|
7673
|
-
return
|
|
7896
|
+
return join11(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
|
|
7674
7897
|
}
|
|
7675
7898
|
function readCache(homeDirOverride) {
|
|
7676
7899
|
try {
|
|
@@ -8486,7 +8709,19 @@ async function trySection(load) {
|
|
|
8486
8709
|
}
|
|
8487
8710
|
|
|
8488
8711
|
// src/code/edit-blocks.ts
|
|
8489
|
-
import {
|
|
8712
|
+
import {
|
|
8713
|
+
closeSync as closeSync2,
|
|
8714
|
+
existsSync as existsSync10,
|
|
8715
|
+
fstatSync,
|
|
8716
|
+
ftruncateSync,
|
|
8717
|
+
mkdirSync as mkdirSync5,
|
|
8718
|
+
openSync as openSync2,
|
|
8719
|
+
readFileSync as readFileSync13,
|
|
8720
|
+
readSync,
|
|
8721
|
+
unlinkSync as unlinkSync3,
|
|
8722
|
+
writeFileSync as writeFileSync5,
|
|
8723
|
+
writeSync
|
|
8724
|
+
} from "fs";
|
|
8490
8725
|
import { dirname as dirname6, resolve as resolve9 } from "path";
|
|
8491
8726
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
8492
8727
|
function parseEditBlocks(text) {
|
|
@@ -8515,42 +8750,76 @@ function applyEditBlock(block, rootDir) {
|
|
|
8515
8750
|
};
|
|
8516
8751
|
}
|
|
8517
8752
|
const searchEmpty = block.search.length === 0;
|
|
8518
|
-
|
|
8753
|
+
if (searchEmpty) {
|
|
8754
|
+
try {
|
|
8755
|
+
mkdirSync5(dirname6(absTarget), { recursive: true });
|
|
8756
|
+
const fd = openSync2(absTarget, "wx");
|
|
8757
|
+
try {
|
|
8758
|
+
writeSync(fd, block.replace);
|
|
8759
|
+
} finally {
|
|
8760
|
+
closeSync2(fd);
|
|
8761
|
+
}
|
|
8762
|
+
return { path: block.path, status: "created" };
|
|
8763
|
+
} catch (err) {
|
|
8764
|
+
const e = err;
|
|
8765
|
+
if (e.code === "EEXIST") {
|
|
8766
|
+
return {
|
|
8767
|
+
path: block.path,
|
|
8768
|
+
status: "not-found",
|
|
8769
|
+
message: "empty SEARCH only creates new files \u2014 this file already exists"
|
|
8770
|
+
};
|
|
8771
|
+
}
|
|
8772
|
+
return { path: block.path, status: "error", message: e.message };
|
|
8773
|
+
}
|
|
8774
|
+
}
|
|
8519
8775
|
try {
|
|
8520
|
-
|
|
8521
|
-
|
|
8776
|
+
let fd;
|
|
8777
|
+
try {
|
|
8778
|
+
fd = openSync2(absTarget, "r+");
|
|
8779
|
+
} catch (err) {
|
|
8780
|
+
if (err.code === "ENOENT") {
|
|
8522
8781
|
return {
|
|
8523
8782
|
path: block.path,
|
|
8524
8783
|
status: "file-missing",
|
|
8525
8784
|
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
8526
8785
|
};
|
|
8527
8786
|
}
|
|
8528
|
-
|
|
8529
|
-
writeFileSync5(absTarget, block.replace, "utf8");
|
|
8530
|
-
return { path: block.path, status: "created" };
|
|
8531
|
-
}
|
|
8532
|
-
const content = readFileSync13(absTarget, "utf8");
|
|
8533
|
-
if (searchEmpty) {
|
|
8534
|
-
return {
|
|
8535
|
-
path: block.path,
|
|
8536
|
-
status: "not-found",
|
|
8537
|
-
message: "empty SEARCH only creates new files \u2014 this file already exists"
|
|
8538
|
-
};
|
|
8787
|
+
throw err;
|
|
8539
8788
|
}
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8789
|
+
try {
|
|
8790
|
+
const stat2 = fstatSync(fd);
|
|
8791
|
+
const inBuf = Buffer.alloc(stat2.size);
|
|
8792
|
+
let readBytes = 0;
|
|
8793
|
+
while (readBytes < stat2.size) {
|
|
8794
|
+
const n = readSync(fd, inBuf, readBytes, stat2.size - readBytes, readBytes);
|
|
8795
|
+
if (n <= 0) break;
|
|
8796
|
+
readBytes += n;
|
|
8797
|
+
}
|
|
8798
|
+
const content = inBuf.toString("utf8", 0, readBytes);
|
|
8799
|
+
const le = lineEndingOf(content);
|
|
8800
|
+
const adaptedSearch = block.search.replace(/\r?\n/g, le);
|
|
8801
|
+
const adaptedReplace = block.replace.replace(/\r?\n/g, le);
|
|
8802
|
+
const idx = content.indexOf(adaptedSearch);
|
|
8803
|
+
if (idx === -1) {
|
|
8804
|
+
return {
|
|
8805
|
+
path: block.path,
|
|
8806
|
+
status: "not-found",
|
|
8807
|
+
message: "SEARCH text does not match the current file content exactly"
|
|
8808
|
+
};
|
|
8809
|
+
}
|
|
8810
|
+
const replaced = `${content.slice(0, idx)}${adaptedReplace}${content.slice(idx + adaptedSearch.length)}`;
|
|
8811
|
+
const outBuf = Buffer.from(replaced, "utf8");
|
|
8812
|
+
ftruncateSync(fd, outBuf.length);
|
|
8813
|
+
let written = 0;
|
|
8814
|
+
while (written < outBuf.length) {
|
|
8815
|
+
const n = writeSync(fd, outBuf, written, outBuf.length - written, written);
|
|
8816
|
+
if (n <= 0) break;
|
|
8817
|
+
written += n;
|
|
8818
|
+
}
|
|
8819
|
+
return { path: block.path, status: "applied" };
|
|
8820
|
+
} finally {
|
|
8821
|
+
closeSync2(fd);
|
|
8550
8822
|
}
|
|
8551
|
-
const replaced = `${content.slice(0, idx)}${adaptedReplace}${content.slice(idx + adaptedSearch.length)}`;
|
|
8552
|
-
writeFileSync5(absTarget, replaced, "utf8");
|
|
8553
|
-
return { path: block.path, status: "applied" };
|
|
8554
8823
|
} catch (err) {
|
|
8555
8824
|
return { path: block.path, status: "error", message: err.message };
|
|
8556
8825
|
}
|
|
@@ -8618,7 +8887,7 @@ function lineEndingOf(text) {
|
|
|
8618
8887
|
|
|
8619
8888
|
// src/code/prompt.ts
|
|
8620
8889
|
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
8621
|
-
import { join as
|
|
8890
|
+
import { join as join12 } from "path";
|
|
8622
8891
|
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.
|
|
8623
8892
|
|
|
8624
8893
|
# Cite or shut up \u2014 non-negotiable
|
|
@@ -8820,7 +9089,7 @@ If \`semantic_search\` returns nothing useful (low scores, off-topic), THEN fall
|
|
|
8820
9089
|
function codeSystemPrompt(rootDir, opts = {}) {
|
|
8821
9090
|
const base = opts.hasSemanticSearch ? `${CODE_SYSTEM_PROMPT}${SEMANTIC_SEARCH_ROUTING}` : CODE_SYSTEM_PROMPT;
|
|
8822
9091
|
const withMemory = applyMemoryStack(base, rootDir);
|
|
8823
|
-
const gitignorePath =
|
|
9092
|
+
const gitignorePath = join12(rootDir, ".gitignore");
|
|
8824
9093
|
let result = withMemory;
|
|
8825
9094
|
if (existsSync11(gitignorePath)) {
|
|
8826
9095
|
let content;
|
|
@@ -8858,34 +9127,47 @@ ${appendParts.join("\n\n")}`;
|
|
|
8858
9127
|
// src/telemetry/usage.ts
|
|
8859
9128
|
import {
|
|
8860
9129
|
appendFileSync as appendFileSync2,
|
|
9130
|
+
closeSync as closeSync3,
|
|
8861
9131
|
existsSync as existsSync12,
|
|
9132
|
+
fstatSync as fstatSync2,
|
|
8862
9133
|
mkdirSync as mkdirSync6,
|
|
9134
|
+
openSync as openSync3,
|
|
8863
9135
|
readFileSync as readFileSync15,
|
|
9136
|
+
readSync as readSync2,
|
|
9137
|
+
renameSync as renameSync2,
|
|
8864
9138
|
statSync as statSync5,
|
|
9139
|
+
unlinkSync as unlinkSync4,
|
|
8865
9140
|
writeFileSync as writeFileSync6
|
|
8866
9141
|
} from "fs";
|
|
8867
9142
|
import { homedir as homedir7 } from "os";
|
|
8868
|
-
import { dirname as dirname7, join as
|
|
9143
|
+
import { dirname as dirname7, join as join13 } from "path";
|
|
8869
9144
|
function defaultUsageLogPath(homeDirOverride) {
|
|
8870
|
-
return
|
|
9145
|
+
return join13(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
|
|
8871
9146
|
}
|
|
8872
9147
|
var USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;
|
|
8873
9148
|
var USAGE_RETENTION_DAYS = 365;
|
|
8874
9149
|
function compactUsageLogIfLarge(path2, now) {
|
|
8875
|
-
let size;
|
|
8876
|
-
try {
|
|
8877
|
-
size = statSync5(path2).size;
|
|
8878
|
-
} catch {
|
|
8879
|
-
return;
|
|
8880
|
-
}
|
|
8881
|
-
if (size < USAGE_COMPACTION_THRESHOLD_BYTES) return;
|
|
8882
|
-
const cutoff = now - USAGE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
8883
9150
|
let raw;
|
|
8884
9151
|
try {
|
|
8885
|
-
|
|
9152
|
+
const fd = openSync3(path2, "r");
|
|
9153
|
+
try {
|
|
9154
|
+
const stat2 = fstatSync2(fd);
|
|
9155
|
+
if (stat2.size < USAGE_COMPACTION_THRESHOLD_BYTES) return;
|
|
9156
|
+
const buf = Buffer.alloc(stat2.size);
|
|
9157
|
+
let read = 0;
|
|
9158
|
+
while (read < stat2.size) {
|
|
9159
|
+
const n = readSync2(fd, buf, read, stat2.size - read, read);
|
|
9160
|
+
if (n <= 0) break;
|
|
9161
|
+
read += n;
|
|
9162
|
+
}
|
|
9163
|
+
raw = buf.toString("utf8", 0, read);
|
|
9164
|
+
} finally {
|
|
9165
|
+
closeSync3(fd);
|
|
9166
|
+
}
|
|
8886
9167
|
} catch {
|
|
8887
9168
|
return;
|
|
8888
9169
|
}
|
|
9170
|
+
const cutoff = now - USAGE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
8889
9171
|
const lines = raw.split(/\r?\n/);
|
|
8890
9172
|
const kept = [];
|
|
8891
9173
|
for (const line of lines) {
|
|
@@ -8897,10 +9179,16 @@ function compactUsageLogIfLarge(path2, now) {
|
|
|
8897
9179
|
}
|
|
8898
9180
|
}
|
|
8899
9181
|
if (kept.length === lines.filter((l) => l.trim()).length) return;
|
|
9182
|
+
const tmp = `${path2}.compacting`;
|
|
8900
9183
|
try {
|
|
8901
|
-
writeFileSync6(
|
|
9184
|
+
writeFileSync6(tmp, kept.length > 0 ? `${kept.join("\n")}
|
|
8902
9185
|
` : "", "utf8");
|
|
9186
|
+
renameSync2(tmp, path2);
|
|
8903
9187
|
} catch {
|
|
9188
|
+
try {
|
|
9189
|
+
unlinkSync4(tmp);
|
|
9190
|
+
} catch {
|
|
9191
|
+
}
|
|
8904
9192
|
}
|
|
8905
9193
|
}
|
|
8906
9194
|
function appendUsage(input) {
|