reasonix 0.27.2 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/{chunk-R2L5YEEF.js → chunk-COFBA5FV.js} +7 -2
- package/dist/cli/chunk-COFBA5FV.js.map +1 -0
- package/dist/cli/index.js +1871 -1234
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-YUL7CYKY.js → prompt-VF7B6BWR.js} +2 -2
- package/dist/index.d.ts +166 -139
- package/dist/index.js +1097 -722
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/cli/chunk-R2L5YEEF.js.map +0 -1
- /package/dist/cli/{prompt-YUL7CYKY.js.map → prompt-VF7B6BWR.js.map} +0 -0
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,6 +1670,343 @@ var ContextManager = class {
|
|
|
1618
1670
|
}
|
|
1619
1671
|
};
|
|
1620
1672
|
|
|
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.`;
|
|
1690
|
+
}
|
|
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.`;
|
|
1698
|
+
}
|
|
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.`;
|
|
1701
|
+
}
|
|
1702
|
+
if (status === "422") {
|
|
1703
|
+
return `Invalid parameter (DeepSeek 422): ${inner}`;
|
|
1704
|
+
}
|
|
1705
|
+
if (status === "400") {
|
|
1706
|
+
return `Bad request (DeepSeek 400): ${inner}`;
|
|
1707
|
+
}
|
|
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]";
|
|
1714
|
+
}
|
|
1715
|
+
if (reason === "stuck") {
|
|
1716
|
+
return "[stuck on a repeated tool call \u2014 explaining what was tried and what's blocking progress]";
|
|
1717
|
+
}
|
|
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;
|
|
1735
|
+
}
|
|
1736
|
+
} catch {
|
|
1737
|
+
}
|
|
1738
|
+
return trimmed;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
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;
|
|
1764
|
+
}
|
|
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;
|
|
1771
|
+
}
|
|
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 ?? "";
|
|
1793
|
+
}
|
|
1794
|
+
return msg;
|
|
1795
|
+
}
|
|
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: "" };
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
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
|
+
|
|
1621
2010
|
// src/memory/runtime.ts
|
|
1622
2011
|
import { createHash } from "crypto";
|
|
1623
2012
|
var ImmutablePrefix = class {
|
|
@@ -1719,8 +2108,15 @@ var VolatileScratch = class {
|
|
|
1719
2108
|
};
|
|
1720
2109
|
|
|
1721
2110
|
// src/repair/scavenge.ts
|
|
2111
|
+
var MAX_SCAVENGE_INPUT = 100 * 1024;
|
|
1722
2112
|
function scavengeToolCalls(reasoningContent, opts) {
|
|
1723
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
|
+
}
|
|
1724
2120
|
const max = opts.maxCalls ?? 4;
|
|
1725
2121
|
const notes = [];
|
|
1726
2122
|
const out = [];
|
|
@@ -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;
|
|
@@ -2239,54 +2633,12 @@ var CacheFirstLoop = class {
|
|
|
2239
2633
|
modelForCurrentCall() {
|
|
2240
2634
|
return this._escalateThisTurn ? ESCALATION_MODEL : this.model;
|
|
2241
2635
|
}
|
|
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
2636
|
/** Returns true ONLY on the tipping call — caller surfaces a one-shot warning. */
|
|
2266
2637
|
noteToolFailureSignal(resultJson, repair) {
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
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;
|
|
2286
|
-
}
|
|
2287
|
-
formatFailureBreakdown() {
|
|
2288
|
-
const parts = Object.entries(this._turnFailureTypes).filter(([, n]) => n > 0).map(([kind, n]) => `${n}\xD7 ${kind}`);
|
|
2289
|
-
return parts.length > 0 ? parts.join(", ") : `${this._turnFailureCount} repair/error signal(s)`;
|
|
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 };
|
|
@@ -2777,7 +3128,7 @@ var CacheFirstLoop = class {
|
|
|
2777
3128
|
)}%) \u2014 forcing summary from what was gathered. Run /compact, /clear, or /new to reset.`
|
|
2778
3129
|
};
|
|
2779
3130
|
this.context.trimTrailingToolCalls();
|
|
2780
|
-
yield* this.
|
|
3131
|
+
yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "context-guard" });
|
|
2781
3132
|
return;
|
|
2782
3133
|
}
|
|
2783
3134
|
for (const call of repairedCalls) {
|
|
@@ -2819,323 +3170,61 @@ ${reason}`;
|
|
|
2819
3170
|
event: "PostToolUse",
|
|
2820
3171
|
cwd: this.hookCwd,
|
|
2821
3172
|
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.
|
|
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++;
|
|
3050
|
-
}
|
|
3051
|
-
if (needed.size === 0) {
|
|
3052
|
-
out.push(msg);
|
|
3053
|
-
for (const r of candidates) out.push(r);
|
|
3054
|
-
i = j - 1;
|
|
3055
|
-
} else {
|
|
3056
|
-
droppedAssistantCalls += 1;
|
|
3057
|
-
droppedStrayTools += candidates.length;
|
|
3058
|
-
i = j - 1;
|
|
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.`;
|
|
3114
|
-
}
|
|
3115
|
-
if (status === "402") {
|
|
3116
|
-
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.`;
|
|
3117
|
-
}
|
|
3118
|
-
if (status === "422") {
|
|
3119
|
-
return `Invalid parameter (DeepSeek 422): ${inner}`;
|
|
3201
|
+
yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "budget" });
|
|
3120
3202
|
}
|
|
3121
|
-
|
|
3122
|
-
return
|
|
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
|
}
|
|
@@ -3546,6 +3636,11 @@ function parseFrontmatter(raw) {
|
|
|
3546
3636
|
function isValidSkillName(name) {
|
|
3547
3637
|
return VALID_SKILL_NAME.test(name);
|
|
3548
3638
|
}
|
|
3639
|
+
function parseAllowedTools(raw) {
|
|
3640
|
+
if (raw === void 0) return void 0;
|
|
3641
|
+
const names = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3642
|
+
return names.length > 0 ? Object.freeze(names) : void 0;
|
|
3643
|
+
}
|
|
3549
3644
|
var SkillStore = class {
|
|
3550
3645
|
homeDir;
|
|
3551
3646
|
projectRoot;
|
|
@@ -3645,7 +3740,7 @@ var SkillStore = class {
|
|
|
3645
3740
|
body: body.trim(),
|
|
3646
3741
|
scope,
|
|
3647
3742
|
path: path2,
|
|
3648
|
-
allowedTools: data["allowed-tools"],
|
|
3743
|
+
allowedTools: parseAllowedTools(data["allowed-tools"]),
|
|
3649
3744
|
runAs: parseRunAs(data.runAs),
|
|
3650
3745
|
model: data.model?.startsWith("deepseek-") ? data.model : void 0
|
|
3651
3746
|
};
|
|
@@ -4161,8 +4256,8 @@ function applyMemoryStack(basePrompt, rootDir) {
|
|
|
4161
4256
|
}
|
|
4162
4257
|
|
|
4163
4258
|
// src/tools/filesystem.ts
|
|
4164
|
-
import { promises as
|
|
4165
|
-
import * as
|
|
4259
|
+
import { promises as fs3 } from "fs";
|
|
4260
|
+
import * as pathMod3 from "path";
|
|
4166
4261
|
import picomatch2 from "picomatch";
|
|
4167
4262
|
|
|
4168
4263
|
// src/index/config.ts
|
|
@@ -4250,6 +4345,214 @@ var DEFAULT_INDEX_EXCLUDES = {
|
|
|
4250
4345
|
};
|
|
4251
4346
|
var DEFAULT_MAX_FILE_BYTES = 256 * 1024;
|
|
4252
4347
|
|
|
4348
|
+
// src/tools/fs/edit.ts
|
|
4349
|
+
import { promises as fs } from "fs";
|
|
4350
|
+
import * as pathMod from "path";
|
|
4351
|
+
function displayRel(rootDir, full) {
|
|
4352
|
+
return pathMod.relative(rootDir, full).replaceAll("\\", "/");
|
|
4353
|
+
}
|
|
4354
|
+
async function applyEdit(rootDir, abs, args) {
|
|
4355
|
+
if (args.search.length === 0) {
|
|
4356
|
+
throw new Error("edit_file: search cannot be empty");
|
|
4357
|
+
}
|
|
4358
|
+
const before = await fs.readFile(abs, "utf8");
|
|
4359
|
+
const le = before.includes("\r\n") ? "\r\n" : "\n";
|
|
4360
|
+
const adaptedSearch = args.search.replace(/\r?\n/g, le);
|
|
4361
|
+
const adaptedReplace = args.replace.replace(/\r?\n/g, le);
|
|
4362
|
+
const firstIdx = before.indexOf(adaptedSearch);
|
|
4363
|
+
if (firstIdx < 0) {
|
|
4364
|
+
throw new Error(`edit_file: search text not found in ${displayRel(rootDir, abs)}`);
|
|
4365
|
+
}
|
|
4366
|
+
const nextIdx = before.indexOf(adaptedSearch, firstIdx + 1);
|
|
4367
|
+
if (nextIdx >= 0) {
|
|
4368
|
+
throw new Error(
|
|
4369
|
+
`edit_file: search text appears multiple times in ${displayRel(rootDir, abs)} \u2014 include more context to disambiguate`
|
|
4370
|
+
);
|
|
4371
|
+
}
|
|
4372
|
+
const after = before.slice(0, firstIdx) + adaptedReplace + before.slice(firstIdx + adaptedSearch.length);
|
|
4373
|
+
await fs.writeFile(abs, after, "utf8");
|
|
4374
|
+
const rel = displayRel(rootDir, abs);
|
|
4375
|
+
const header = `edited ${rel} (${adaptedSearch.length}\u2192${adaptedReplace.length} chars)`;
|
|
4376
|
+
const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
|
|
4377
|
+
const diff = renderEditDiff(adaptedSearch, adaptedReplace, startLine);
|
|
4378
|
+
return `${header}
|
|
4379
|
+
${diff}`;
|
|
4380
|
+
}
|
|
4381
|
+
function renderEditDiff(search, replace, startLine) {
|
|
4382
|
+
const a = search.split(/\r?\n/);
|
|
4383
|
+
const b = replace.split(/\r?\n/);
|
|
4384
|
+
const diff = lineDiff(a, b);
|
|
4385
|
+
const hunk = `@@ -${startLine},${a.length} +${startLine},${b.length} @@`;
|
|
4386
|
+
const body = diff.map((d) => `${d.op === " " ? " " : d.op} ${d.line}`).join("\n");
|
|
4387
|
+
return `${hunk}
|
|
4388
|
+
${body}`;
|
|
4389
|
+
}
|
|
4390
|
+
function lineDiff(a, b) {
|
|
4391
|
+
const n = a.length;
|
|
4392
|
+
const m = b.length;
|
|
4393
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
4394
|
+
for (let i2 = 1; i2 <= n; i2++) {
|
|
4395
|
+
for (let j2 = 1; j2 <= m; j2++) {
|
|
4396
|
+
if (a[i2 - 1] === b[j2 - 1]) dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
4397
|
+
else dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
const out = [];
|
|
4401
|
+
let i = n;
|
|
4402
|
+
let j = m;
|
|
4403
|
+
while (i > 0 && j > 0) {
|
|
4404
|
+
if (a[i - 1] === b[j - 1]) {
|
|
4405
|
+
out.unshift({ op: " ", line: a[i - 1] });
|
|
4406
|
+
i--;
|
|
4407
|
+
j--;
|
|
4408
|
+
} else if ((dp[i - 1][j] ?? 0) > (dp[i][j - 1] ?? 0)) {
|
|
4409
|
+
out.unshift({ op: "-", line: a[i - 1] });
|
|
4410
|
+
i--;
|
|
4411
|
+
} else {
|
|
4412
|
+
out.unshift({ op: "+", line: b[j - 1] });
|
|
4413
|
+
j--;
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
while (i > 0) {
|
|
4417
|
+
out.unshift({ op: "-", line: a[i - 1] });
|
|
4418
|
+
i--;
|
|
4419
|
+
}
|
|
4420
|
+
while (j > 0) {
|
|
4421
|
+
out.unshift({ op: "+", line: b[j - 1] });
|
|
4422
|
+
j--;
|
|
4423
|
+
}
|
|
4424
|
+
return out;
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
// src/tools/fs/search.ts
|
|
4428
|
+
import { promises as fs2 } from "fs";
|
|
4429
|
+
import * as pathMod2 from "path";
|
|
4430
|
+
function displayRel2(rootDir, full) {
|
|
4431
|
+
return pathMod2.relative(rootDir, full).replaceAll("\\", "/");
|
|
4432
|
+
}
|
|
4433
|
+
async function searchFiles(ctx, startAbs, args) {
|
|
4434
|
+
const needle = args.pattern.toLowerCase();
|
|
4435
|
+
const includeDeps = args.include_deps === true;
|
|
4436
|
+
let re = null;
|
|
4437
|
+
try {
|
|
4438
|
+
re = new RegExp(args.pattern, "i");
|
|
4439
|
+
} catch {
|
|
4440
|
+
re = null;
|
|
4441
|
+
}
|
|
4442
|
+
const matches = [];
|
|
4443
|
+
let totalBytes = 0;
|
|
4444
|
+
const walk2 = async (dir) => {
|
|
4445
|
+
let entries;
|
|
4446
|
+
try {
|
|
4447
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
4448
|
+
} catch {
|
|
4449
|
+
return;
|
|
4450
|
+
}
|
|
4451
|
+
for (const e of entries) {
|
|
4452
|
+
const full = pathMod2.join(dir, e.name);
|
|
4453
|
+
const lower = e.name.toLowerCase();
|
|
4454
|
+
const hit = re ? re.test(e.name) : lower.includes(needle);
|
|
4455
|
+
if (hit) {
|
|
4456
|
+
const rel = displayRel2(ctx.rootDir, full);
|
|
4457
|
+
if (totalBytes + rel.length + 1 > ctx.maxListBytes) {
|
|
4458
|
+
matches.push("[\u2026 search truncated \u2014 refine pattern \u2026]");
|
|
4459
|
+
return;
|
|
4460
|
+
}
|
|
4461
|
+
matches.push(rel);
|
|
4462
|
+
totalBytes += rel.length + 1;
|
|
4463
|
+
}
|
|
4464
|
+
if (e.isDirectory()) {
|
|
4465
|
+
if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
|
|
4466
|
+
await walk2(full);
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
};
|
|
4470
|
+
await walk2(startAbs);
|
|
4471
|
+
return matches.length === 0 ? "(no matches)" : matches.join("\n");
|
|
4472
|
+
}
|
|
4473
|
+
async function searchContent(ctx, startAbs, args) {
|
|
4474
|
+
const caseSensitive = args.case_sensitive === true;
|
|
4475
|
+
const includeDeps = args.include_deps === true;
|
|
4476
|
+
let re = null;
|
|
4477
|
+
try {
|
|
4478
|
+
re = new RegExp(args.pattern, caseSensitive ? "" : "i");
|
|
4479
|
+
} catch {
|
|
4480
|
+
re = null;
|
|
4481
|
+
}
|
|
4482
|
+
const needle = caseSensitive ? args.pattern : args.pattern.toLowerCase();
|
|
4483
|
+
const matches = [];
|
|
4484
|
+
let totalBytes = 0;
|
|
4485
|
+
let scanned = 0;
|
|
4486
|
+
let truncated = false;
|
|
4487
|
+
const walk2 = async (dir) => {
|
|
4488
|
+
if (truncated) return;
|
|
4489
|
+
let entries;
|
|
4490
|
+
try {
|
|
4491
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
4492
|
+
} catch {
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
4495
|
+
for (const e of entries) {
|
|
4496
|
+
if (truncated) return;
|
|
4497
|
+
if (e.isDirectory()) {
|
|
4498
|
+
if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
|
|
4499
|
+
await walk2(pathMod2.join(dir, e.name));
|
|
4500
|
+
continue;
|
|
4501
|
+
}
|
|
4502
|
+
if (!e.isFile()) continue;
|
|
4503
|
+
const full = pathMod2.join(dir, e.name);
|
|
4504
|
+
if (ctx.nameMatch && !ctx.nameMatch(e.name, displayRel2(ctx.rootDir, full))) continue;
|
|
4505
|
+
if (ctx.isBinaryByName(e.name)) continue;
|
|
4506
|
+
let fh;
|
|
4507
|
+
try {
|
|
4508
|
+
fh = await fs2.open(full, "r");
|
|
4509
|
+
} catch {
|
|
4510
|
+
continue;
|
|
4511
|
+
}
|
|
4512
|
+
let raw;
|
|
4513
|
+
try {
|
|
4514
|
+
const st = await fh.stat();
|
|
4515
|
+
if (st.size > 2 * 1024 * 1024) {
|
|
4516
|
+
await fh.close();
|
|
4517
|
+
continue;
|
|
4518
|
+
}
|
|
4519
|
+
raw = await fh.readFile();
|
|
4520
|
+
} catch {
|
|
4521
|
+
await fh.close().catch(() => {
|
|
4522
|
+
});
|
|
4523
|
+
continue;
|
|
4524
|
+
}
|
|
4525
|
+
await fh.close();
|
|
4526
|
+
const firstNul = raw.indexOf(0);
|
|
4527
|
+
if (firstNul !== -1 && firstNul < 8 * 1024) continue;
|
|
4528
|
+
const text = raw.toString("utf8");
|
|
4529
|
+
const rel = displayRel2(ctx.rootDir, full);
|
|
4530
|
+
const lines = text.split(/\r?\n/);
|
|
4531
|
+
for (let li = 0; li < lines.length; li++) {
|
|
4532
|
+
const line = lines[li];
|
|
4533
|
+
const lineForCheck = caseSensitive ? line : line.toLowerCase();
|
|
4534
|
+
const hit = re ? re.test(line) : lineForCheck.includes(needle);
|
|
4535
|
+
if (!hit) continue;
|
|
4536
|
+
const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
|
|
4537
|
+
const out = `${rel}:${li + 1}: ${display}`;
|
|
4538
|
+
if (totalBytes + out.length + 1 > ctx.maxListBytes) {
|
|
4539
|
+
matches.push(`[\u2026 truncated at ${ctx.maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
|
|
4540
|
+
truncated = true;
|
|
4541
|
+
return;
|
|
4542
|
+
}
|
|
4543
|
+
matches.push(out);
|
|
4544
|
+
totalBytes += out.length + 1;
|
|
4545
|
+
}
|
|
4546
|
+
scanned++;
|
|
4547
|
+
}
|
|
4548
|
+
};
|
|
4549
|
+
await walk2(startAbs);
|
|
4550
|
+
if (matches.length === 0) {
|
|
4551
|
+
return scanned === 0 ? "(no files scanned \u2014 path empty or all files filtered out)" : `(no matches across ${scanned} file${scanned === 1 ? "" : "s"})`;
|
|
4552
|
+
}
|
|
4553
|
+
return matches.join("\n");
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4253
4556
|
// src/tools/filesystem.ts
|
|
4254
4557
|
var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
|
|
4255
4558
|
var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
|
|
@@ -4258,8 +4561,8 @@ var AUTO_PREVIEW_HEAD_LINES = 80;
|
|
|
4258
4561
|
var AUTO_PREVIEW_TAIL_LINES = 40;
|
|
4259
4562
|
var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
|
|
4260
4563
|
var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
|
|
4261
|
-
function
|
|
4262
|
-
return
|
|
4564
|
+
function displayRel3(rootDir, full) {
|
|
4565
|
+
return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
|
|
4263
4566
|
}
|
|
4264
4567
|
var GLOB_METACHARS = /[*?{[]/;
|
|
4265
4568
|
function compileNameFilter(filter) {
|
|
@@ -4278,7 +4581,7 @@ function isLikelyBinaryByName(name) {
|
|
|
4278
4581
|
return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
|
|
4279
4582
|
}
|
|
4280
4583
|
function registerFilesystemTools(registry, opts) {
|
|
4281
|
-
const rootDir =
|
|
4584
|
+
const rootDir = pathMod3.resolve(opts.rootDir);
|
|
4282
4585
|
const allowWriting = opts.allowWriting !== false;
|
|
4283
4586
|
const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
4284
4587
|
const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
|
|
@@ -4291,10 +4594,10 @@ function registerFilesystemTools(registry, opts) {
|
|
|
4291
4594
|
normalized = normalized.slice(1);
|
|
4292
4595
|
}
|
|
4293
4596
|
if (normalized.length === 0) normalized = ".";
|
|
4294
|
-
const resolved =
|
|
4295
|
-
const normRoot =
|
|
4296
|
-
const rel =
|
|
4297
|
-
if (rel.startsWith("..") ||
|
|
4597
|
+
const resolved = pathMod3.resolve(rootDir, normalized);
|
|
4598
|
+
const normRoot = pathMod3.resolve(rootDir);
|
|
4599
|
+
const rel = pathMod3.relative(normRoot, resolved);
|
|
4600
|
+
if (rel.startsWith("..") || pathMod3.isAbsolute(rel)) {
|
|
4298
4601
|
throw new Error(`path escapes sandbox root (${normRoot}): ${raw}`);
|
|
4299
4602
|
}
|
|
4300
4603
|
return resolved;
|
|
@@ -4322,11 +4625,17 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
|
|
|
4322
4625
|
},
|
|
4323
4626
|
fn: async (args) => {
|
|
4324
4627
|
const abs = safePath(args.path);
|
|
4325
|
-
const
|
|
4326
|
-
|
|
4327
|
-
|
|
4628
|
+
const fh = await fs3.open(abs, "r");
|
|
4629
|
+
let raw;
|
|
4630
|
+
try {
|
|
4631
|
+
const stat2 = await fh.stat();
|
|
4632
|
+
if (stat2.isDirectory()) {
|
|
4633
|
+
throw new Error(`not a file: ${args.path} (it's a directory)`);
|
|
4634
|
+
}
|
|
4635
|
+
raw = await fh.readFile();
|
|
4636
|
+
} finally {
|
|
4637
|
+
await fh.close();
|
|
4328
4638
|
}
|
|
4329
|
-
const raw = await fs.readFile(abs);
|
|
4330
4639
|
if (raw.length > maxReadBytes) {
|
|
4331
4640
|
const headBytes = raw.slice(0, maxReadBytes).toString("utf8");
|
|
4332
4641
|
return `${headBytes}
|
|
@@ -4388,7 +4697,7 @@ ${slice.join("\n")}`;
|
|
|
4388
4697
|
},
|
|
4389
4698
|
fn: async (args) => {
|
|
4390
4699
|
const abs = safePath(args.path ?? ".");
|
|
4391
|
-
const entries = await
|
|
4700
|
+
const entries = await fs3.readdir(abs, { withFileTypes: true });
|
|
4392
4701
|
const lines = [];
|
|
4393
4702
|
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
4394
4703
|
lines.push(e.isDirectory() ? `${e.name}/` : e.name);
|
|
@@ -4431,7 +4740,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4431
4740
|
if (depth > maxDepth) return;
|
|
4432
4741
|
let entries;
|
|
4433
4742
|
try {
|
|
4434
|
-
entries = await
|
|
4743
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
4435
4744
|
} catch {
|
|
4436
4745
|
return;
|
|
4437
4746
|
}
|
|
@@ -4466,7 +4775,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4466
4775
|
lines.push(line);
|
|
4467
4776
|
emitted++;
|
|
4468
4777
|
if (e.isDirectory() && !skip) {
|
|
4469
|
-
await walk2(
|
|
4778
|
+
await walk2(pathMod3.join(dir, e.name), depth + 1);
|
|
4470
4779
|
}
|
|
4471
4780
|
}
|
|
4472
4781
|
};
|
|
@@ -4479,61 +4788,25 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4479
4788
|
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.",
|
|
4480
4789
|
readOnly: true,
|
|
4481
4790
|
parameters: {
|
|
4482
|
-
type: "object",
|
|
4483
|
-
properties: {
|
|
4484
|
-
path: { type: "string", description: "Directory to start the search at (default: root)." },
|
|
4485
|
-
pattern: {
|
|
4486
|
-
type: "string",
|
|
4487
|
-
description: "Substring (or regex) to match against filenames."
|
|
4488
|
-
},
|
|
4489
|
-
include_deps: {
|
|
4490
|
-
type: "boolean",
|
|
4491
|
-
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."
|
|
4492
|
-
}
|
|
4493
|
-
},
|
|
4494
|
-
required: ["pattern"]
|
|
4495
|
-
},
|
|
4496
|
-
fn: async (args) => {
|
|
4497
|
-
const startAbs = safePath(args.path ?? ".");
|
|
4498
|
-
const needle = args.pattern.toLowerCase();
|
|
4499
|
-
const includeDeps = args.include_deps === true;
|
|
4500
|
-
let re = null;
|
|
4501
|
-
try {
|
|
4502
|
-
re = new RegExp(args.pattern, "i");
|
|
4503
|
-
} catch {
|
|
4504
|
-
re = null;
|
|
4505
|
-
}
|
|
4506
|
-
const matches = [];
|
|
4507
|
-
let totalBytes = 0;
|
|
4508
|
-
const walk2 = async (dir) => {
|
|
4509
|
-
let entries;
|
|
4510
|
-
try {
|
|
4511
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
4512
|
-
} catch {
|
|
4513
|
-
return;
|
|
4514
|
-
}
|
|
4515
|
-
for (const e of entries) {
|
|
4516
|
-
const full = pathMod.join(dir, e.name);
|
|
4517
|
-
const lower = e.name.toLowerCase();
|
|
4518
|
-
const hit = re ? re.test(e.name) : lower.includes(needle);
|
|
4519
|
-
if (hit) {
|
|
4520
|
-
const rel = displayRel(rootDir, full);
|
|
4521
|
-
if (totalBytes + rel.length + 1 > maxListBytes) {
|
|
4522
|
-
matches.push("[\u2026 search truncated \u2014 refine pattern \u2026]");
|
|
4523
|
-
return;
|
|
4524
|
-
}
|
|
4525
|
-
matches.push(rel);
|
|
4526
|
-
totalBytes += rel.length + 1;
|
|
4527
|
-
}
|
|
4528
|
-
if (e.isDirectory()) {
|
|
4529
|
-
if (!includeDeps && SKIP_DIR_NAMES.has(e.name)) continue;
|
|
4530
|
-
await walk2(full);
|
|
4531
|
-
}
|
|
4791
|
+
type: "object",
|
|
4792
|
+
properties: {
|
|
4793
|
+
path: { type: "string", description: "Directory to start the search at (default: root)." },
|
|
4794
|
+
pattern: {
|
|
4795
|
+
type: "string",
|
|
4796
|
+
description: "Substring (or regex) to match against filenames."
|
|
4797
|
+
},
|
|
4798
|
+
include_deps: {
|
|
4799
|
+
type: "boolean",
|
|
4800
|
+
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."
|
|
4532
4801
|
}
|
|
4533
|
-
}
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4802
|
+
},
|
|
4803
|
+
required: ["pattern"]
|
|
4804
|
+
},
|
|
4805
|
+
fn: async (args) => searchFiles(
|
|
4806
|
+
{ rootDir, maxListBytes, skipDirNames: SKIP_DIR_NAMES },
|
|
4807
|
+
safePath(args.path ?? "."),
|
|
4808
|
+
args
|
|
4809
|
+
)
|
|
4537
4810
|
});
|
|
4538
4811
|
registry.register({
|
|
4539
4812
|
name: "search_content",
|
|
@@ -4565,83 +4838,17 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4565
4838
|
},
|
|
4566
4839
|
required: ["pattern"]
|
|
4567
4840
|
},
|
|
4568
|
-
fn: async (args) =>
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
const needle = caseSensitive ? args.pattern : args.pattern.toLowerCase();
|
|
4580
|
-
const matches = [];
|
|
4581
|
-
let totalBytes = 0;
|
|
4582
|
-
let scanned = 0;
|
|
4583
|
-
let truncated = false;
|
|
4584
|
-
const walk2 = async (dir) => {
|
|
4585
|
-
if (truncated) return;
|
|
4586
|
-
let entries;
|
|
4587
|
-
try {
|
|
4588
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
4589
|
-
} catch {
|
|
4590
|
-
return;
|
|
4591
|
-
}
|
|
4592
|
-
for (const e of entries) {
|
|
4593
|
-
if (truncated) return;
|
|
4594
|
-
if (e.isDirectory()) {
|
|
4595
|
-
if (!includeDeps && SKIP_DIR_NAMES.has(e.name)) continue;
|
|
4596
|
-
await walk2(pathMod.join(dir, e.name));
|
|
4597
|
-
continue;
|
|
4598
|
-
}
|
|
4599
|
-
if (!e.isFile()) continue;
|
|
4600
|
-
const full = pathMod.join(dir, e.name);
|
|
4601
|
-
if (nameMatch && !nameMatch(e.name, displayRel(rootDir, full))) continue;
|
|
4602
|
-
if (isLikelyBinaryByName(e.name)) continue;
|
|
4603
|
-
let stat2;
|
|
4604
|
-
try {
|
|
4605
|
-
stat2 = await fs.stat(full);
|
|
4606
|
-
} catch {
|
|
4607
|
-
continue;
|
|
4608
|
-
}
|
|
4609
|
-
if (stat2.size > 2 * 1024 * 1024) continue;
|
|
4610
|
-
let raw;
|
|
4611
|
-
try {
|
|
4612
|
-
raw = await fs.readFile(full);
|
|
4613
|
-
} catch {
|
|
4614
|
-
continue;
|
|
4615
|
-
}
|
|
4616
|
-
const firstNul = raw.indexOf(0);
|
|
4617
|
-
if (firstNul !== -1 && firstNul < 8 * 1024) continue;
|
|
4618
|
-
const text = raw.toString("utf8");
|
|
4619
|
-
const rel = displayRel(rootDir, full);
|
|
4620
|
-
const lines = text.split(/\r?\n/);
|
|
4621
|
-
for (let li = 0; li < lines.length; li++) {
|
|
4622
|
-
const line = lines[li];
|
|
4623
|
-
const lineForCheck = caseSensitive ? line : line.toLowerCase();
|
|
4624
|
-
const hit = re ? re.test(line) : lineForCheck.includes(needle);
|
|
4625
|
-
if (!hit) continue;
|
|
4626
|
-
const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
|
|
4627
|
-
const out = `${rel}:${li + 1}: ${display}`;
|
|
4628
|
-
if (totalBytes + out.length + 1 > maxListBytes) {
|
|
4629
|
-
matches.push(`[\u2026 truncated at ${maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
|
|
4630
|
-
truncated = true;
|
|
4631
|
-
return;
|
|
4632
|
-
}
|
|
4633
|
-
matches.push(out);
|
|
4634
|
-
totalBytes += out.length + 1;
|
|
4635
|
-
}
|
|
4636
|
-
scanned++;
|
|
4637
|
-
}
|
|
4638
|
-
};
|
|
4639
|
-
await walk2(startAbs);
|
|
4640
|
-
if (matches.length === 0) {
|
|
4641
|
-
return scanned === 0 ? "(no files scanned \u2014 path empty or all files filtered out)" : `(no matches across ${scanned} file${scanned === 1 ? "" : "s"})`;
|
|
4642
|
-
}
|
|
4643
|
-
return matches.join("\n");
|
|
4644
|
-
}
|
|
4841
|
+
fn: async (args) => searchContent(
|
|
4842
|
+
{
|
|
4843
|
+
rootDir,
|
|
4844
|
+
maxListBytes,
|
|
4845
|
+
skipDirNames: SKIP_DIR_NAMES,
|
|
4846
|
+
isBinaryByName: isLikelyBinaryByName,
|
|
4847
|
+
nameMatch: compileNameFilter(typeof args.glob === "string" ? args.glob : null)
|
|
4848
|
+
},
|
|
4849
|
+
safePath(args.path ?? "."),
|
|
4850
|
+
args
|
|
4851
|
+
)
|
|
4645
4852
|
});
|
|
4646
4853
|
registry.register({
|
|
4647
4854
|
name: "get_file_info",
|
|
@@ -4656,7 +4863,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4656
4863
|
},
|
|
4657
4864
|
fn: async (args) => {
|
|
4658
4865
|
const abs = safePath(args.path);
|
|
4659
|
-
const st = await
|
|
4866
|
+
const st = await fs3.lstat(abs);
|
|
4660
4867
|
const type = st.isDirectory() ? "directory" : st.isSymbolicLink() ? "symlink" : "file";
|
|
4661
4868
|
return JSON.stringify({
|
|
4662
4869
|
type,
|
|
@@ -4679,9 +4886,9 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4679
4886
|
},
|
|
4680
4887
|
fn: async (args) => {
|
|
4681
4888
|
const abs = safePath(args.path);
|
|
4682
|
-
await
|
|
4683
|
-
await
|
|
4684
|
-
return `wrote ${args.content.length} chars to ${
|
|
4889
|
+
await fs3.mkdir(pathMod3.dirname(abs), { recursive: true });
|
|
4890
|
+
await fs3.writeFile(abs, args.content, "utf8");
|
|
4891
|
+
return `wrote ${args.content.length} chars to ${displayRel3(rootDir, abs)}`;
|
|
4685
4892
|
}
|
|
4686
4893
|
});
|
|
4687
4894
|
registry.register({
|
|
@@ -4696,34 +4903,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4696
4903
|
},
|
|
4697
4904
|
required: ["path", "search", "replace"]
|
|
4698
4905
|
},
|
|
4699
|
-
fn: async (args) =>
|
|
4700
|
-
const abs = safePath(args.path);
|
|
4701
|
-
const before = await fs.readFile(abs, "utf8");
|
|
4702
|
-
if (args.search.length === 0) {
|
|
4703
|
-
throw new Error("edit_file: search cannot be empty");
|
|
4704
|
-
}
|
|
4705
|
-
const le = before.includes("\r\n") ? "\r\n" : "\n";
|
|
4706
|
-
const adaptedSearch = args.search.replace(/\r?\n/g, le);
|
|
4707
|
-
const adaptedReplace = args.replace.replace(/\r?\n/g, le);
|
|
4708
|
-
const firstIdx = before.indexOf(adaptedSearch);
|
|
4709
|
-
if (firstIdx < 0) {
|
|
4710
|
-
throw new Error(`edit_file: search text not found in ${displayRel(rootDir, abs)}`);
|
|
4711
|
-
}
|
|
4712
|
-
const nextIdx = before.indexOf(adaptedSearch, firstIdx + 1);
|
|
4713
|
-
if (nextIdx >= 0) {
|
|
4714
|
-
throw new Error(
|
|
4715
|
-
`edit_file: search text appears multiple times in ${displayRel(rootDir, abs)} \u2014 include more context to disambiguate`
|
|
4716
|
-
);
|
|
4717
|
-
}
|
|
4718
|
-
const after = before.slice(0, firstIdx) + adaptedReplace + before.slice(firstIdx + adaptedSearch.length);
|
|
4719
|
-
await fs.writeFile(abs, after, "utf8");
|
|
4720
|
-
const rel = displayRel(rootDir, abs);
|
|
4721
|
-
const header = `edited ${rel} (${adaptedSearch.length}\u2192${adaptedReplace.length} chars)`;
|
|
4722
|
-
const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
|
|
4723
|
-
const diff = renderEditDiff(adaptedSearch, adaptedReplace, startLine);
|
|
4724
|
-
return `${header}
|
|
4725
|
-
${diff}`;
|
|
4726
|
-
}
|
|
4906
|
+
fn: async (args) => applyEdit(rootDir, safePath(args.path), args)
|
|
4727
4907
|
});
|
|
4728
4908
|
registry.register({
|
|
4729
4909
|
name: "create_directory",
|
|
@@ -4735,8 +4915,8 @@ ${diff}`;
|
|
|
4735
4915
|
},
|
|
4736
4916
|
fn: async (args) => {
|
|
4737
4917
|
const abs = safePath(args.path);
|
|
4738
|
-
await
|
|
4739
|
-
return `created ${
|
|
4918
|
+
await fs3.mkdir(abs, { recursive: true });
|
|
4919
|
+
return `created ${displayRel3(rootDir, abs)}/`;
|
|
4740
4920
|
}
|
|
4741
4921
|
});
|
|
4742
4922
|
registry.register({
|
|
@@ -4753,58 +4933,13 @@ ${diff}`;
|
|
|
4753
4933
|
fn: async (args) => {
|
|
4754
4934
|
const src = safePath(args.source);
|
|
4755
4935
|
const dst = safePath(args.destination);
|
|
4756
|
-
await
|
|
4757
|
-
await
|
|
4758
|
-
return `moved ${
|
|
4936
|
+
await fs3.mkdir(pathMod3.dirname(dst), { recursive: true });
|
|
4937
|
+
await fs3.rename(src, dst);
|
|
4938
|
+
return `moved ${displayRel3(rootDir, src)} \u2192 ${displayRel3(rootDir, dst)}`;
|
|
4759
4939
|
}
|
|
4760
4940
|
});
|
|
4761
4941
|
return registry;
|
|
4762
4942
|
}
|
|
4763
|
-
function renderEditDiff(search, replace, startLine) {
|
|
4764
|
-
const a = search.split(/\r?\n/);
|
|
4765
|
-
const b = replace.split(/\r?\n/);
|
|
4766
|
-
const diff = lineDiff(a, b);
|
|
4767
|
-
const hunk = `@@ -${startLine},${a.length} +${startLine},${b.length} @@`;
|
|
4768
|
-
const body = diff.map((d) => `${d.op === " " ? " " : d.op} ${d.line}`).join("\n");
|
|
4769
|
-
return `${hunk}
|
|
4770
|
-
${body}`;
|
|
4771
|
-
}
|
|
4772
|
-
function lineDiff(a, b) {
|
|
4773
|
-
const n = a.length;
|
|
4774
|
-
const m = b.length;
|
|
4775
|
-
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
4776
|
-
for (let i2 = 1; i2 <= n; i2++) {
|
|
4777
|
-
for (let j2 = 1; j2 <= m; j2++) {
|
|
4778
|
-
if (a[i2 - 1] === b[j2 - 1]) dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
4779
|
-
else dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
4780
|
-
}
|
|
4781
|
-
}
|
|
4782
|
-
const out = [];
|
|
4783
|
-
let i = n;
|
|
4784
|
-
let j = m;
|
|
4785
|
-
while (i > 0 && j > 0) {
|
|
4786
|
-
if (a[i - 1] === b[j - 1]) {
|
|
4787
|
-
out.unshift({ op: " ", line: a[i - 1] });
|
|
4788
|
-
i--;
|
|
4789
|
-
j--;
|
|
4790
|
-
} else if ((dp[i - 1][j] ?? 0) > (dp[i][j - 1] ?? 0)) {
|
|
4791
|
-
out.unshift({ op: "-", line: a[i - 1] });
|
|
4792
|
-
i--;
|
|
4793
|
-
} else {
|
|
4794
|
-
out.unshift({ op: "+", line: b[j - 1] });
|
|
4795
|
-
j--;
|
|
4796
|
-
}
|
|
4797
|
-
}
|
|
4798
|
-
while (i > 0) {
|
|
4799
|
-
out.unshift({ op: "-", line: a[i - 1] });
|
|
4800
|
-
i--;
|
|
4801
|
-
}
|
|
4802
|
-
while (j > 0) {
|
|
4803
|
-
out.unshift({ op: "+", line: b[j - 1] });
|
|
4804
|
-
j--;
|
|
4805
|
-
}
|
|
4806
|
-
return out;
|
|
4807
|
-
}
|
|
4808
4943
|
|
|
4809
4944
|
// src/tools/memory.ts
|
|
4810
4945
|
function registerMemoryTools(registry, opts = {}) {
|
|
@@ -5278,6 +5413,50 @@ function registerPlanTool(registry, opts = {}) {
|
|
|
5278
5413
|
return registry;
|
|
5279
5414
|
}
|
|
5280
5415
|
|
|
5416
|
+
// src/tools/subagent-types.ts
|
|
5417
|
+
var EXPLORE_SYSTEM = `You are an exploration subagent. Wide-net read-only investigation; return one distilled answer.
|
|
5418
|
+
|
|
5419
|
+
How to operate:
|
|
5420
|
+
- Read-only tools only (read_file, search_files, search_content, directory_tree, list_directory, get_file_info).
|
|
5421
|
+
- For "find all places that call / reference / use X" \u2014 use search_content (content grep), NOT search_files (which only matches names).
|
|
5422
|
+
- Cast a wide net first to map the territory, then read the 3-10 most relevant files in full. Stop as soon as you can answer.
|
|
5423
|
+
- The parent does not see your tool calls \u2014 over-exploration is pure waste.
|
|
5424
|
+
|
|
5425
|
+
Final answer:
|
|
5426
|
+
- One paragraph or short bullets; lead with the conclusion.
|
|
5427
|
+
- Cite file:line ranges when they back the claim.
|
|
5428
|
+
- No follow-up offers, no "let me know if you need more" \u2014 the parent will ask again.
|
|
5429
|
+
|
|
5430
|
+
${NEGATIVE_CLAIM_RULE}
|
|
5431
|
+
|
|
5432
|
+
${TUI_FORMATTING_RULES}`;
|
|
5433
|
+
var VERIFY_SYSTEM = `You are a verify subagent. Narrow check \u2014 return YES / NO / INCONCLUSIVE with evidence. Do not expand scope.
|
|
5434
|
+
|
|
5435
|
+
How to operate:
|
|
5436
|
+
- Read only what's needed to verify the specific claim. No exploration past the claim.
|
|
5437
|
+
- Use search_content / read_file to confirm the exact behavior, type, or call site in question.
|
|
5438
|
+
- Cap at 6-8 tool calls. If you can't verify in that, return INCONCLUSIVE plus what's missing.
|
|
5439
|
+
|
|
5440
|
+
Final answer:
|
|
5441
|
+
- Lead with VERIFIED / NOT VERIFIED / INCONCLUSIVE.
|
|
5442
|
+
- Cite file:line for the evidence.
|
|
5443
|
+
- One paragraph or a few bullets. No follow-up offers.
|
|
5444
|
+
|
|
5445
|
+
${NEGATIVE_CLAIM_RULE}
|
|
5446
|
+
|
|
5447
|
+
${TUI_FORMATTING_RULES}`;
|
|
5448
|
+
var TYPES = {
|
|
5449
|
+
explore: { system: EXPLORE_SYSTEM, maxToolIters: 20 },
|
|
5450
|
+
verify: { system: VERIFY_SYSTEM, maxToolIters: 8 }
|
|
5451
|
+
};
|
|
5452
|
+
var SUBAGENT_TYPE_NAMES = Object.freeze(
|
|
5453
|
+
Object.keys(TYPES)
|
|
5454
|
+
);
|
|
5455
|
+
function getSubagentType(name) {
|
|
5456
|
+
if (typeof name !== "string") return void 0;
|
|
5457
|
+
return TYPES[name];
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5281
5460
|
// src/tools/subagent.ts
|
|
5282
5461
|
var DEFAULT_SUBAGENT_SYSTEM = `You are a Reasonix subagent. The parent agent spawned you to handle one focused subtask, then return.
|
|
5283
5462
|
|
|
@@ -5294,6 +5473,8 @@ ${ESCALATION_CONTRACT}
|
|
|
5294
5473
|
${TUI_FORMATTING_RULES}`;
|
|
5295
5474
|
var DEFAULT_MAX_RESULT_CHARS2 = 8e3;
|
|
5296
5475
|
var DEFAULT_MAX_ITERS = 16;
|
|
5476
|
+
var MIN_MAX_ITERS = 1;
|
|
5477
|
+
var MAX_MAX_ITERS = 32;
|
|
5297
5478
|
var DEFAULT_SUBAGENT_MODEL = "deepseek-v4-flash";
|
|
5298
5479
|
var DEFAULT_SUBAGENT_EFFORT = "high";
|
|
5299
5480
|
var SUBAGENT_TOOL_NAME = "spawn_subagent";
|
|
@@ -5314,7 +5495,41 @@ async function spawnSubagent(opts) {
|
|
|
5314
5495
|
iter: 0,
|
|
5315
5496
|
elapsedMs: 0
|
|
5316
5497
|
});
|
|
5317
|
-
|
|
5498
|
+
if (opts.allowedTools) {
|
|
5499
|
+
const missing = opts.allowedTools.filter((n) => !opts.parentRegistry.has(n));
|
|
5500
|
+
if (missing.length > 0) {
|
|
5501
|
+
const errorMessage2 = `subagent allow-list names tool(s) not registered in the parent: ${missing.join(", ")}. Fix the skill's \`allowed-tools\` frontmatter or check spelling.`;
|
|
5502
|
+
sink?.current?.({
|
|
5503
|
+
kind: "end",
|
|
5504
|
+
task: taskPreview,
|
|
5505
|
+
skillName,
|
|
5506
|
+
model,
|
|
5507
|
+
iter: 0,
|
|
5508
|
+
elapsedMs: Date.now() - startedAt,
|
|
5509
|
+
error: errorMessage2,
|
|
5510
|
+
turns: 0,
|
|
5511
|
+
costUsd: 0,
|
|
5512
|
+
usage: new Usage()
|
|
5513
|
+
});
|
|
5514
|
+
return {
|
|
5515
|
+
success: false,
|
|
5516
|
+
output: "",
|
|
5517
|
+
error: errorMessage2,
|
|
5518
|
+
turns: 0,
|
|
5519
|
+
toolIters: 0,
|
|
5520
|
+
elapsedMs: Date.now() - startedAt,
|
|
5521
|
+
costUsd: 0,
|
|
5522
|
+
model,
|
|
5523
|
+
skillName,
|
|
5524
|
+
usage: new Usage()
|
|
5525
|
+
};
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
const childTools = opts.allowedTools ? forkRegistryWithAllowList(
|
|
5529
|
+
opts.parentRegistry,
|
|
5530
|
+
new Set(opts.allowedTools),
|
|
5531
|
+
NEVER_INHERITED_TOOLS
|
|
5532
|
+
) : forkRegistryExcluding(opts.parentRegistry, NEVER_INHERITED_TOOLS);
|
|
5318
5533
|
const childPrefix = new ImmutablePrefix({
|
|
5319
5534
|
system: opts.system,
|
|
5320
5535
|
toolSpecs: childTools.specs()
|
|
@@ -5479,6 +5694,17 @@ function registerSubagentTool(parentRegistry, opts) {
|
|
|
5479
5694
|
type: "string",
|
|
5480
5695
|
enum: ["deepseek-v4-flash", "deepseek-v4-pro"],
|
|
5481
5696
|
description: "Which DeepSeek model the subagent runs on. Default is 'deepseek-v4-flash' \u2014 cheap and fast, fine for explore/research-style subtasks. Override to 'deepseek-v4-pro' (~12\xD7 more expensive) when the subtask genuinely needs the stronger model: cross-file architecture, subtle bug hunts, anything where flash has empirically underperformed."
|
|
5697
|
+
},
|
|
5698
|
+
max_iters: {
|
|
5699
|
+
type: "integer",
|
|
5700
|
+
minimum: MIN_MAX_ITERS,
|
|
5701
|
+
maximum: MAX_MAX_ITERS,
|
|
5702
|
+
description: `Cap on the subagent's tool-call iterations. Default 16 (or the type's default when 'type' is set). Hard range: ${MIN_MAX_ITERS}-${MAX_MAX_ITERS}; out-of-range values are clamped to the nearest end.`
|
|
5703
|
+
},
|
|
5704
|
+
type: {
|
|
5705
|
+
type: "string",
|
|
5706
|
+
enum: [...SUBAGENT_TYPE_NAMES],
|
|
5707
|
+
description: "Optional persona shaping the system prompt and default iter budget. 'explore' = wide-net read-only investigation (20-iter budget, returns a distilled answer). 'verify' = narrow yes/no check with evidence (8-iter budget). Omit when supplying your own 'system' prompt or when the default generic persona fits. Caller-supplied 'system' / 'max_iters' override the type's defaults."
|
|
5482
5708
|
}
|
|
5483
5709
|
},
|
|
5484
5710
|
required: ["task"]
|
|
@@ -5490,15 +5716,17 @@ function registerSubagentTool(parentRegistry, opts) {
|
|
|
5490
5716
|
error: "spawn_subagent requires a non-empty 'task' argument."
|
|
5491
5717
|
});
|
|
5492
5718
|
}
|
|
5493
|
-
const
|
|
5719
|
+
const typeSpec = getSubagentType(args.type);
|
|
5720
|
+
const system = typeof args.system === "string" && args.system.trim().length > 0 ? args.system.trim() : typeSpec?.system ?? defaultSystem;
|
|
5494
5721
|
const model = typeof args.model === "string" && args.model.startsWith("deepseek-") ? args.model : defaultModel;
|
|
5722
|
+
const callerIters = clampMaxIters(args.max_iters);
|
|
5495
5723
|
const result = await spawnSubagent({
|
|
5496
5724
|
client: opts.client,
|
|
5497
5725
|
parentRegistry,
|
|
5498
5726
|
system,
|
|
5499
5727
|
task,
|
|
5500
5728
|
model,
|
|
5501
|
-
maxToolIters,
|
|
5729
|
+
maxToolIters: callerIters ?? typeSpec?.maxToolIters ?? maxToolIters,
|
|
5502
5730
|
maxResultChars,
|
|
5503
5731
|
sink,
|
|
5504
5732
|
parentSignal: ctx?.signal
|
|
@@ -5508,6 +5736,13 @@ function registerSubagentTool(parentRegistry, opts) {
|
|
|
5508
5736
|
});
|
|
5509
5737
|
return parentRegistry;
|
|
5510
5738
|
}
|
|
5739
|
+
function clampMaxIters(raw) {
|
|
5740
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return void 0;
|
|
5741
|
+
const n = Math.floor(raw);
|
|
5742
|
+
if (n < MIN_MAX_ITERS) return MIN_MAX_ITERS;
|
|
5743
|
+
if (n > MAX_MAX_ITERS) return MAX_MAX_ITERS;
|
|
5744
|
+
return n;
|
|
5745
|
+
}
|
|
5511
5746
|
function forkRegistryExcluding(parent, exclude) {
|
|
5512
5747
|
const child = new ToolRegistry();
|
|
5513
5748
|
for (const spec of parent.specs()) {
|
|
@@ -5520,18 +5755,29 @@ function forkRegistryExcluding(parent, exclude) {
|
|
|
5520
5755
|
if (parent.planMode) child.setPlanMode(true);
|
|
5521
5756
|
return child;
|
|
5522
5757
|
}
|
|
5758
|
+
function forkRegistryWithAllowList(parent, allow, alsoExclude) {
|
|
5759
|
+
const child = new ToolRegistry();
|
|
5760
|
+
for (const spec of parent.specs()) {
|
|
5761
|
+
const name = spec.function.name;
|
|
5762
|
+
if (!allow.has(name)) continue;
|
|
5763
|
+
if (alsoExclude.has(name)) continue;
|
|
5764
|
+
const def = parent.get(name);
|
|
5765
|
+
if (!def) continue;
|
|
5766
|
+
child.register(def);
|
|
5767
|
+
}
|
|
5768
|
+
if (parent.planMode) child.setPlanMode(true);
|
|
5769
|
+
return child;
|
|
5770
|
+
}
|
|
5523
5771
|
|
|
5524
5772
|
// src/tools/shell.ts
|
|
5525
|
-
import
|
|
5526
|
-
import { existsSync as existsSync8, statSync as statSync4 } from "fs";
|
|
5527
|
-
import * as pathMod4 from "path";
|
|
5773
|
+
import * as pathMod7 from "path";
|
|
5528
5774
|
|
|
5529
5775
|
// src/config.ts
|
|
5530
5776
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
|
|
5531
5777
|
import { homedir as homedir5 } from "os";
|
|
5532
|
-
import { dirname as dirname4, join as
|
|
5778
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
5533
5779
|
function defaultConfigPath() {
|
|
5534
|
-
return
|
|
5780
|
+
return join10(homedir5(), ".reasonix", "config.json");
|
|
5535
5781
|
}
|
|
5536
5782
|
function readConfig(path2 = defaultConfigPath()) {
|
|
5537
5783
|
try {
|
|
@@ -5582,7 +5828,7 @@ function redactKey(key) {
|
|
|
5582
5828
|
|
|
5583
5829
|
// src/tools/jobs.ts
|
|
5584
5830
|
import { spawn as spawn2 } from "child_process";
|
|
5585
|
-
import * as
|
|
5831
|
+
import * as pathMod4 from "path";
|
|
5586
5832
|
function killProcessTree(pid, signal) {
|
|
5587
5833
|
if (process.platform === "win32") {
|
|
5588
5834
|
const args = ["/pid", String(pid), "/T"];
|
|
@@ -5642,7 +5888,7 @@ var JobRegistry = class {
|
|
|
5642
5888
|
const maxBytes = opts.maxBufferBytes ?? DEFAULT_OUTPUT_CAP_BYTES;
|
|
5643
5889
|
const { bin, args, spawnOverrides } = prepareSpawn(argv);
|
|
5644
5890
|
const spawnOpts = {
|
|
5645
|
-
cwd:
|
|
5891
|
+
cwd: pathMod4.resolve(opts.cwd),
|
|
5646
5892
|
shell: false,
|
|
5647
5893
|
windowsHide: true,
|
|
5648
5894
|
env: process.env,
|
|
@@ -5673,6 +5919,9 @@ var JobRegistry = class {
|
|
|
5673
5919
|
child: null,
|
|
5674
5920
|
readyPromise: Promise.resolve(),
|
|
5675
5921
|
signalReady: () => {
|
|
5922
|
+
},
|
|
5923
|
+
closedPromise: Promise.resolve(),
|
|
5924
|
+
signalClosed: () => {
|
|
5676
5925
|
}
|
|
5677
5926
|
};
|
|
5678
5927
|
this.jobs.set(id2, job2);
|
|
@@ -5691,6 +5940,11 @@ var JobRegistry = class {
|
|
|
5691
5940
|
const readyPromise = new Promise((res) => {
|
|
5692
5941
|
readyResolve = res;
|
|
5693
5942
|
});
|
|
5943
|
+
let closedResolve = () => {
|
|
5944
|
+
};
|
|
5945
|
+
const closedPromise = new Promise((res) => {
|
|
5946
|
+
closedResolve = res;
|
|
5947
|
+
});
|
|
5694
5948
|
const job = {
|
|
5695
5949
|
id,
|
|
5696
5950
|
command: trimmed,
|
|
@@ -5702,7 +5956,9 @@ var JobRegistry = class {
|
|
|
5702
5956
|
running: true,
|
|
5703
5957
|
child,
|
|
5704
5958
|
readyPromise,
|
|
5705
|
-
signalReady: readyResolve
|
|
5959
|
+
signalReady: readyResolve,
|
|
5960
|
+
closedPromise,
|
|
5961
|
+
signalClosed: closedResolve
|
|
5706
5962
|
};
|
|
5707
5963
|
this.jobs.set(id, job);
|
|
5708
5964
|
let readyMatched = false;
|
|
@@ -5736,11 +5992,13 @@ ${job.output.slice(start)}`;
|
|
|
5736
5992
|
job.running = false;
|
|
5737
5993
|
job.spawnError = err.message;
|
|
5738
5994
|
job.signalReady();
|
|
5995
|
+
job.signalClosed();
|
|
5739
5996
|
});
|
|
5740
5997
|
child.on("close", (code) => {
|
|
5741
5998
|
job.running = false;
|
|
5742
5999
|
job.exitCode = code;
|
|
5743
6000
|
job.signalReady();
|
|
6001
|
+
job.signalClosed();
|
|
5744
6002
|
});
|
|
5745
6003
|
const onAbort = () => this.stop(id, { graceMs: 100 });
|
|
5746
6004
|
if (opts.signal?.aborted) {
|
|
@@ -5802,7 +6060,7 @@ ${job.output.slice(start)}`;
|
|
|
5802
6060
|
} catch {
|
|
5803
6061
|
}
|
|
5804
6062
|
}
|
|
5805
|
-
await Promise.race([job.
|
|
6063
|
+
await Promise.race([job.closedPromise, new Promise((res) => setTimeout(res, graceMs))]);
|
|
5806
6064
|
if (job.running) {
|
|
5807
6065
|
if (job.pid !== null) {
|
|
5808
6066
|
killProcessTree(job.pid, "SIGKILL");
|
|
@@ -5812,7 +6070,7 @@ ${job.output.slice(start)}`;
|
|
|
5812
6070
|
} catch {
|
|
5813
6071
|
}
|
|
5814
6072
|
}
|
|
5815
|
-
await new Promise((res) => setTimeout(res,
|
|
6073
|
+
await Promise.race([job.closedPromise, new Promise((res) => setTimeout(res, 5e3))]);
|
|
5816
6074
|
}
|
|
5817
6075
|
return snapshot(job);
|
|
5818
6076
|
}
|
|
@@ -5868,10 +6126,15 @@ function snapshot(job) {
|
|
|
5868
6126
|
};
|
|
5869
6127
|
}
|
|
5870
6128
|
|
|
6129
|
+
// src/tools/shell/exec.ts
|
|
6130
|
+
import { spawn as spawn4, spawnSync } from "child_process";
|
|
6131
|
+
import { existsSync as existsSync8, statSync as statSync4 } from "fs";
|
|
6132
|
+
import * as pathMod6 from "path";
|
|
6133
|
+
|
|
5871
6134
|
// src/tools/shell-chain.ts
|
|
5872
6135
|
import { spawn as spawn3 } from "child_process";
|
|
5873
6136
|
import { closeSync, openSync } from "fs";
|
|
5874
|
-
import * as
|
|
6137
|
+
import * as pathMod5 from "path";
|
|
5875
6138
|
var UnsupportedSyntaxError = class extends Error {
|
|
5876
6139
|
constructor(detail) {
|
|
5877
6140
|
super(`run_command: ${detail}`);
|
|
@@ -6138,7 +6401,7 @@ function openRedirects(redirects, cwd) {
|
|
|
6138
6401
|
let bothFd = null;
|
|
6139
6402
|
const toClose = [];
|
|
6140
6403
|
const open = (target, flags) => {
|
|
6141
|
-
const resolved =
|
|
6404
|
+
const resolved = pathMod5.resolve(cwd, target);
|
|
6142
6405
|
const fd = openSync(resolved, flags);
|
|
6143
6406
|
toClose.push(fd);
|
|
6144
6407
|
return fd;
|
|
@@ -6287,31 +6550,7 @@ var OutputBuffer = class {
|
|
|
6287
6550
|
}
|
|
6288
6551
|
};
|
|
6289
6552
|
|
|
6290
|
-
// src/tools/shell.ts
|
|
6291
|
-
function killProcessTree2(child) {
|
|
6292
|
-
if (!child.pid || child.killed) return;
|
|
6293
|
-
if (process.platform === "win32") {
|
|
6294
|
-
try {
|
|
6295
|
-
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
6296
|
-
stdio: "ignore",
|
|
6297
|
-
windowsHide: true
|
|
6298
|
-
});
|
|
6299
|
-
return;
|
|
6300
|
-
} catch {
|
|
6301
|
-
}
|
|
6302
|
-
}
|
|
6303
|
-
try {
|
|
6304
|
-
process.kill(-child.pid, "SIGKILL");
|
|
6305
|
-
return;
|
|
6306
|
-
} catch {
|
|
6307
|
-
}
|
|
6308
|
-
try {
|
|
6309
|
-
child.kill("SIGKILL");
|
|
6310
|
-
} catch {
|
|
6311
|
-
}
|
|
6312
|
-
}
|
|
6313
|
-
var DEFAULT_TIMEOUT_SEC = 60;
|
|
6314
|
-
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
6553
|
+
// src/tools/shell/parse.ts
|
|
6315
6554
|
var BUILTIN_ALLOWLIST = [
|
|
6316
6555
|
// Repo inspection
|
|
6317
6556
|
"git status",
|
|
@@ -6524,6 +6763,32 @@ function isCommandAllowed(cmd, extra = []) {
|
|
|
6524
6763
|
if (chain === null) return isAllowed(cmd, extra);
|
|
6525
6764
|
return chainAllowed(chain, (seg) => isAllowed(seg, extra));
|
|
6526
6765
|
}
|
|
6766
|
+
|
|
6767
|
+
// src/tools/shell/exec.ts
|
|
6768
|
+
var DEFAULT_TIMEOUT_SEC = 60;
|
|
6769
|
+
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
6770
|
+
function killProcessTree2(child) {
|
|
6771
|
+
if (!child.pid || child.killed) return;
|
|
6772
|
+
if (process.platform === "win32") {
|
|
6773
|
+
try {
|
|
6774
|
+
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
6775
|
+
stdio: "ignore",
|
|
6776
|
+
windowsHide: true
|
|
6777
|
+
});
|
|
6778
|
+
return;
|
|
6779
|
+
} catch {
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6782
|
+
try {
|
|
6783
|
+
process.kill(-child.pid, "SIGKILL");
|
|
6784
|
+
return;
|
|
6785
|
+
} catch {
|
|
6786
|
+
}
|
|
6787
|
+
try {
|
|
6788
|
+
child.kill("SIGKILL");
|
|
6789
|
+
} catch {
|
|
6790
|
+
}
|
|
6791
|
+
}
|
|
6527
6792
|
async function runCommand(cmd, opts) {
|
|
6528
6793
|
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
|
|
6529
6794
|
const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
@@ -6632,16 +6897,16 @@ function resolveExecutable(cmd, opts = {}) {
|
|
|
6632
6897
|
const platform = opts.platform ?? process.platform;
|
|
6633
6898
|
if (platform !== "win32") return cmd;
|
|
6634
6899
|
if (!cmd) return cmd;
|
|
6635
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
6636
|
-
if (
|
|
6900
|
+
if (cmd.includes("/") || cmd.includes("\\") || pathMod6.isAbsolute(cmd)) return cmd;
|
|
6901
|
+
if (pathMod6.extname(cmd)) return cmd;
|
|
6637
6902
|
const env = opts.env ?? process.env;
|
|
6638
6903
|
const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
|
|
6639
|
-
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" :
|
|
6904
|
+
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod6.delimiter);
|
|
6640
6905
|
const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
|
|
6641
6906
|
const isFile = opts.isFile ?? defaultIsFile;
|
|
6642
6907
|
for (const dir of pathDirs) {
|
|
6643
6908
|
for (const ext of pathExt) {
|
|
6644
|
-
const full =
|
|
6909
|
+
const full = pathMod6.win32.join(dir, cmd + ext);
|
|
6645
6910
|
if (isFile(full)) return full;
|
|
6646
6911
|
}
|
|
6647
6912
|
}
|
|
@@ -6711,8 +6976,8 @@ function withUtf8Codepage(cmdline) {
|
|
|
6711
6976
|
function isBareWindowsName(s) {
|
|
6712
6977
|
if (!s) return false;
|
|
6713
6978
|
if (s.includes("/") || s.includes("\\")) return false;
|
|
6714
|
-
if (
|
|
6715
|
-
if (
|
|
6979
|
+
if (pathMod6.isAbsolute(s)) return false;
|
|
6980
|
+
if (pathMod6.extname(s)) return false;
|
|
6716
6981
|
return true;
|
|
6717
6982
|
}
|
|
6718
6983
|
function quoteForCmdExe(arg) {
|
|
@@ -6720,6 +6985,8 @@ function quoteForCmdExe(arg) {
|
|
|
6720
6985
|
if (!/[\s"&|<>^%(),;!]/.test(arg)) return arg;
|
|
6721
6986
|
return `"${arg.replace(/"/g, '""')}"`;
|
|
6722
6987
|
}
|
|
6988
|
+
|
|
6989
|
+
// src/tools/shell.ts
|
|
6723
6990
|
var NeedsConfirmationError = class extends Error {
|
|
6724
6991
|
command;
|
|
6725
6992
|
constructor(command) {
|
|
@@ -6731,7 +6998,7 @@ var NeedsConfirmationError = class extends Error {
|
|
|
6731
6998
|
}
|
|
6732
6999
|
};
|
|
6733
7000
|
function registerShellTools(registry, opts) {
|
|
6734
|
-
const rootDir =
|
|
7001
|
+
const rootDir = pathMod7.resolve(opts.rootDir);
|
|
6735
7002
|
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
|
|
6736
7003
|
const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
6737
7004
|
const jobs = opts.jobs ?? new JobRegistry();
|
|
@@ -6930,6 +7197,7 @@ ${r.output}` : header;
|
|
|
6930
7197
|
}
|
|
6931
7198
|
|
|
6932
7199
|
// src/tools/web.ts
|
|
7200
|
+
import { parse as parseHtml } from "node-html-parser";
|
|
6933
7201
|
var DEFAULT_FETCH_MAX_CHARS = 32e3;
|
|
6934
7202
|
var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
|
|
6935
7203
|
var DEFAULT_TOPK = 5;
|
|
@@ -7059,28 +7327,70 @@ async function readBodyCapped(resp, maxBytes) {
|
|
|
7059
7327
|
}
|
|
7060
7328
|
return out;
|
|
7061
7329
|
}
|
|
7330
|
+
var MAX_HTML_INPUT = 5 * 1024 * 1024;
|
|
7331
|
+
var STRIP_BLOCK_TAGS = "script, style, noscript, nav, footer, aside, svg";
|
|
7332
|
+
var BLOCK_BREAK_TAGS = /* @__PURE__ */ new Set([
|
|
7333
|
+
"p",
|
|
7334
|
+
"div",
|
|
7335
|
+
"br",
|
|
7336
|
+
"h1",
|
|
7337
|
+
"h2",
|
|
7338
|
+
"h3",
|
|
7339
|
+
"h4",
|
|
7340
|
+
"h5",
|
|
7341
|
+
"h6",
|
|
7342
|
+
"li",
|
|
7343
|
+
"tr",
|
|
7344
|
+
"section",
|
|
7345
|
+
"article"
|
|
7346
|
+
]);
|
|
7062
7347
|
function htmlToText(html) {
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
s =
|
|
7069
|
-
s = s.replace(/<aside[\s\S]*?<\/aside>/gi, "");
|
|
7070
|
-
s = s.replace(/<svg[\s\S]*?<\/svg>/gi, "");
|
|
7071
|
-
s = s.replace(/<\/?(p|div|br|h[1-6]|li|tr|section|article)\b[^>]*>/gi, "\n");
|
|
7072
|
-
s = s.replace(/<[^>]+>/g, "");
|
|
7348
|
+
const input = html.length > MAX_HTML_INPUT ? html.slice(0, MAX_HTML_INPUT) : html;
|
|
7349
|
+
const root = parseHtml(input);
|
|
7350
|
+
for (const node of root.querySelectorAll(STRIP_BLOCK_TAGS)) node.remove();
|
|
7351
|
+
const out = [];
|
|
7352
|
+
walkExtract(root, out);
|
|
7353
|
+
let s = out.join("");
|
|
7073
7354
|
s = decodeHtmlEntities(s);
|
|
7074
7355
|
s = s.replace(/[ \t]+/g, " ");
|
|
7075
7356
|
s = s.replace(/\n[ \t]+/g, "\n");
|
|
7076
7357
|
s = s.replace(/\n{3,}/g, "\n\n");
|
|
7077
7358
|
return s.trim();
|
|
7078
7359
|
}
|
|
7079
|
-
function
|
|
7080
|
-
|
|
7360
|
+
function walkExtract(node, out) {
|
|
7361
|
+
if (node.nodeType === 3) {
|
|
7362
|
+
out.push(node.rawText ?? node.text ?? "");
|
|
7363
|
+
return;
|
|
7364
|
+
}
|
|
7365
|
+
const tag = node.rawTagName?.toLowerCase();
|
|
7366
|
+
const isBreak = tag !== void 0 && BLOCK_BREAK_TAGS.has(tag);
|
|
7367
|
+
if (isBreak) out.push("\n");
|
|
7368
|
+
for (const child of node.childNodes) walkExtract(child, out);
|
|
7369
|
+
if (isBreak) out.push("\n");
|
|
7081
7370
|
}
|
|
7371
|
+
function stripHtml(s) {
|
|
7372
|
+
return parseHtml(s).text;
|
|
7373
|
+
}
|
|
7374
|
+
var HTML_ENTITIES = {
|
|
7375
|
+
amp: "&",
|
|
7376
|
+
lt: "<",
|
|
7377
|
+
gt: ">",
|
|
7378
|
+
quot: '"',
|
|
7379
|
+
apos: "'",
|
|
7380
|
+
nbsp: " "
|
|
7381
|
+
};
|
|
7082
7382
|
function decodeHtmlEntities(s) {
|
|
7083
|
-
return s.replace(/&
|
|
7383
|
+
return s.replace(/&(#\d+|#x[0-9a-fA-F]+|\w+);/g, (raw, name) => {
|
|
7384
|
+
if (name.startsWith("#x") || name.startsWith("#X")) {
|
|
7385
|
+
const code = Number.parseInt(name.slice(2), 16);
|
|
7386
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : raw;
|
|
7387
|
+
}
|
|
7388
|
+
if (name.startsWith("#")) {
|
|
7389
|
+
const code = Number.parseInt(name.slice(1), 10);
|
|
7390
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : raw;
|
|
7391
|
+
}
|
|
7392
|
+
return HTML_ENTITIES[name.toLowerCase()] ?? raw;
|
|
7393
|
+
});
|
|
7084
7394
|
}
|
|
7085
7395
|
function extractTitle(html) {
|
|
7086
7396
|
const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
@@ -7675,7 +7985,7 @@ function truncate(s, n) {
|
|
|
7675
7985
|
// src/version.ts
|
|
7676
7986
|
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
|
|
7677
7987
|
import { homedir as homedir6 } from "os";
|
|
7678
|
-
import { dirname as dirname5, join as
|
|
7988
|
+
import { dirname as dirname5, join as join11 } from "path";
|
|
7679
7989
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7680
7990
|
var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
|
|
7681
7991
|
var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -7684,7 +7994,7 @@ function readPackageVersion() {
|
|
|
7684
7994
|
try {
|
|
7685
7995
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
7686
7996
|
for (let i = 0; i < 6; i++) {
|
|
7687
|
-
const p =
|
|
7997
|
+
const p = join11(dir, "package.json");
|
|
7688
7998
|
if (existsSync9(p)) {
|
|
7689
7999
|
const pkg = JSON.parse(readFileSync12(p, "utf8"));
|
|
7690
8000
|
if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
|
|
@@ -7701,7 +8011,7 @@ function readPackageVersion() {
|
|
|
7701
8011
|
}
|
|
7702
8012
|
var VERSION = readPackageVersion();
|
|
7703
8013
|
function cachePath(homeDirOverride) {
|
|
7704
|
-
return
|
|
8014
|
+
return join11(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
|
|
7705
8015
|
}
|
|
7706
8016
|
function readCache(homeDirOverride) {
|
|
7707
8017
|
try {
|
|
@@ -8517,7 +8827,19 @@ async function trySection(load) {
|
|
|
8517
8827
|
}
|
|
8518
8828
|
|
|
8519
8829
|
// src/code/edit-blocks.ts
|
|
8520
|
-
import {
|
|
8830
|
+
import {
|
|
8831
|
+
closeSync as closeSync2,
|
|
8832
|
+
existsSync as existsSync10,
|
|
8833
|
+
fstatSync,
|
|
8834
|
+
ftruncateSync,
|
|
8835
|
+
mkdirSync as mkdirSync5,
|
|
8836
|
+
openSync as openSync2,
|
|
8837
|
+
readFileSync as readFileSync13,
|
|
8838
|
+
readSync,
|
|
8839
|
+
unlinkSync as unlinkSync3,
|
|
8840
|
+
writeFileSync as writeFileSync5,
|
|
8841
|
+
writeSync
|
|
8842
|
+
} from "fs";
|
|
8521
8843
|
import { dirname as dirname6, resolve as resolve9 } from "path";
|
|
8522
8844
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
8523
8845
|
function parseEditBlocks(text) {
|
|
@@ -8546,42 +8868,76 @@ function applyEditBlock(block, rootDir) {
|
|
|
8546
8868
|
};
|
|
8547
8869
|
}
|
|
8548
8870
|
const searchEmpty = block.search.length === 0;
|
|
8549
|
-
|
|
8871
|
+
if (searchEmpty) {
|
|
8872
|
+
try {
|
|
8873
|
+
mkdirSync5(dirname6(absTarget), { recursive: true });
|
|
8874
|
+
const fd = openSync2(absTarget, "wx");
|
|
8875
|
+
try {
|
|
8876
|
+
writeSync(fd, block.replace);
|
|
8877
|
+
} finally {
|
|
8878
|
+
closeSync2(fd);
|
|
8879
|
+
}
|
|
8880
|
+
return { path: block.path, status: "created" };
|
|
8881
|
+
} catch (err) {
|
|
8882
|
+
const e = err;
|
|
8883
|
+
if (e.code === "EEXIST") {
|
|
8884
|
+
return {
|
|
8885
|
+
path: block.path,
|
|
8886
|
+
status: "not-found",
|
|
8887
|
+
message: "empty SEARCH only creates new files \u2014 this file already exists"
|
|
8888
|
+
};
|
|
8889
|
+
}
|
|
8890
|
+
return { path: block.path, status: "error", message: e.message };
|
|
8891
|
+
}
|
|
8892
|
+
}
|
|
8550
8893
|
try {
|
|
8551
|
-
|
|
8552
|
-
|
|
8894
|
+
let fd;
|
|
8895
|
+
try {
|
|
8896
|
+
fd = openSync2(absTarget, "r+");
|
|
8897
|
+
} catch (err) {
|
|
8898
|
+
if (err.code === "ENOENT") {
|
|
8553
8899
|
return {
|
|
8554
8900
|
path: block.path,
|
|
8555
8901
|
status: "file-missing",
|
|
8556
8902
|
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
8557
8903
|
};
|
|
8558
8904
|
}
|
|
8559
|
-
|
|
8560
|
-
writeFileSync5(absTarget, block.replace, "utf8");
|
|
8561
|
-
return { path: block.path, status: "created" };
|
|
8562
|
-
}
|
|
8563
|
-
const content = readFileSync13(absTarget, "utf8");
|
|
8564
|
-
if (searchEmpty) {
|
|
8565
|
-
return {
|
|
8566
|
-
path: block.path,
|
|
8567
|
-
status: "not-found",
|
|
8568
|
-
message: "empty SEARCH only creates new files \u2014 this file already exists"
|
|
8569
|
-
};
|
|
8905
|
+
throw err;
|
|
8570
8906
|
}
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8907
|
+
try {
|
|
8908
|
+
const stat2 = fstatSync(fd);
|
|
8909
|
+
const inBuf = Buffer.alloc(stat2.size);
|
|
8910
|
+
let readBytes = 0;
|
|
8911
|
+
while (readBytes < stat2.size) {
|
|
8912
|
+
const n = readSync(fd, inBuf, readBytes, stat2.size - readBytes, readBytes);
|
|
8913
|
+
if (n <= 0) break;
|
|
8914
|
+
readBytes += n;
|
|
8915
|
+
}
|
|
8916
|
+
const content = inBuf.toString("utf8", 0, readBytes);
|
|
8917
|
+
const le = lineEndingOf(content);
|
|
8918
|
+
const adaptedSearch = block.search.replace(/\r?\n/g, le);
|
|
8919
|
+
const adaptedReplace = block.replace.replace(/\r?\n/g, le);
|
|
8920
|
+
const idx = content.indexOf(adaptedSearch);
|
|
8921
|
+
if (idx === -1) {
|
|
8922
|
+
return {
|
|
8923
|
+
path: block.path,
|
|
8924
|
+
status: "not-found",
|
|
8925
|
+
message: "SEARCH text does not match the current file content exactly"
|
|
8926
|
+
};
|
|
8927
|
+
}
|
|
8928
|
+
const replaced = `${content.slice(0, idx)}${adaptedReplace}${content.slice(idx + adaptedSearch.length)}`;
|
|
8929
|
+
const outBuf = Buffer.from(replaced, "utf8");
|
|
8930
|
+
ftruncateSync(fd, outBuf.length);
|
|
8931
|
+
let written = 0;
|
|
8932
|
+
while (written < outBuf.length) {
|
|
8933
|
+
const n = writeSync(fd, outBuf, written, outBuf.length - written, written);
|
|
8934
|
+
if (n <= 0) break;
|
|
8935
|
+
written += n;
|
|
8936
|
+
}
|
|
8937
|
+
return { path: block.path, status: "applied" };
|
|
8938
|
+
} finally {
|
|
8939
|
+
closeSync2(fd);
|
|
8581
8940
|
}
|
|
8582
|
-
const replaced = `${content.slice(0, idx)}${adaptedReplace}${content.slice(idx + adaptedSearch.length)}`;
|
|
8583
|
-
writeFileSync5(absTarget, replaced, "utf8");
|
|
8584
|
-
return { path: block.path, status: "applied" };
|
|
8585
8941
|
} catch (err) {
|
|
8586
8942
|
return { path: block.path, status: "error", message: err.message };
|
|
8587
8943
|
}
|
|
@@ -8649,7 +9005,7 @@ function lineEndingOf(text) {
|
|
|
8649
9005
|
|
|
8650
9006
|
// src/code/prompt.ts
|
|
8651
9007
|
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
8652
|
-
import { join as
|
|
9008
|
+
import { join as join12 } from "path";
|
|
8653
9009
|
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.
|
|
8654
9010
|
|
|
8655
9011
|
# Cite or shut up \u2014 non-negotiable
|
|
@@ -8851,7 +9207,7 @@ If \`semantic_search\` returns nothing useful (low scores, off-topic), THEN fall
|
|
|
8851
9207
|
function codeSystemPrompt(rootDir, opts = {}) {
|
|
8852
9208
|
const base = opts.hasSemanticSearch ? `${CODE_SYSTEM_PROMPT}${SEMANTIC_SEARCH_ROUTING}` : CODE_SYSTEM_PROMPT;
|
|
8853
9209
|
const withMemory = applyMemoryStack(base, rootDir);
|
|
8854
|
-
const gitignorePath =
|
|
9210
|
+
const gitignorePath = join12(rootDir, ".gitignore");
|
|
8855
9211
|
let result = withMemory;
|
|
8856
9212
|
if (existsSync11(gitignorePath)) {
|
|
8857
9213
|
let content;
|
|
@@ -8889,34 +9245,47 @@ ${appendParts.join("\n\n")}`;
|
|
|
8889
9245
|
// src/telemetry/usage.ts
|
|
8890
9246
|
import {
|
|
8891
9247
|
appendFileSync as appendFileSync2,
|
|
9248
|
+
closeSync as closeSync3,
|
|
8892
9249
|
existsSync as existsSync12,
|
|
9250
|
+
fstatSync as fstatSync2,
|
|
8893
9251
|
mkdirSync as mkdirSync6,
|
|
9252
|
+
openSync as openSync3,
|
|
8894
9253
|
readFileSync as readFileSync15,
|
|
9254
|
+
readSync as readSync2,
|
|
9255
|
+
renameSync as renameSync2,
|
|
8895
9256
|
statSync as statSync5,
|
|
9257
|
+
unlinkSync as unlinkSync4,
|
|
8896
9258
|
writeFileSync as writeFileSync6
|
|
8897
9259
|
} from "fs";
|
|
8898
9260
|
import { homedir as homedir7 } from "os";
|
|
8899
|
-
import { dirname as dirname7, join as
|
|
9261
|
+
import { dirname as dirname7, join as join13 } from "path";
|
|
8900
9262
|
function defaultUsageLogPath(homeDirOverride) {
|
|
8901
|
-
return
|
|
9263
|
+
return join13(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
|
|
8902
9264
|
}
|
|
8903
9265
|
var USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;
|
|
8904
9266
|
var USAGE_RETENTION_DAYS = 365;
|
|
8905
9267
|
function compactUsageLogIfLarge(path2, now) {
|
|
8906
|
-
let size;
|
|
8907
|
-
try {
|
|
8908
|
-
size = statSync5(path2).size;
|
|
8909
|
-
} catch {
|
|
8910
|
-
return;
|
|
8911
|
-
}
|
|
8912
|
-
if (size < USAGE_COMPACTION_THRESHOLD_BYTES) return;
|
|
8913
|
-
const cutoff = now - USAGE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
8914
9268
|
let raw;
|
|
8915
9269
|
try {
|
|
8916
|
-
|
|
9270
|
+
const fd = openSync3(path2, "r");
|
|
9271
|
+
try {
|
|
9272
|
+
const stat2 = fstatSync2(fd);
|
|
9273
|
+
if (stat2.size < USAGE_COMPACTION_THRESHOLD_BYTES) return;
|
|
9274
|
+
const buf = Buffer.alloc(stat2.size);
|
|
9275
|
+
let read = 0;
|
|
9276
|
+
while (read < stat2.size) {
|
|
9277
|
+
const n = readSync2(fd, buf, read, stat2.size - read, read);
|
|
9278
|
+
if (n <= 0) break;
|
|
9279
|
+
read += n;
|
|
9280
|
+
}
|
|
9281
|
+
raw = buf.toString("utf8", 0, read);
|
|
9282
|
+
} finally {
|
|
9283
|
+
closeSync3(fd);
|
|
9284
|
+
}
|
|
8917
9285
|
} catch {
|
|
8918
9286
|
return;
|
|
8919
9287
|
}
|
|
9288
|
+
const cutoff = now - USAGE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
8920
9289
|
const lines = raw.split(/\r?\n/);
|
|
8921
9290
|
const kept = [];
|
|
8922
9291
|
for (const line of lines) {
|
|
@@ -8928,10 +9297,16 @@ function compactUsageLogIfLarge(path2, now) {
|
|
|
8928
9297
|
}
|
|
8929
9298
|
}
|
|
8930
9299
|
if (kept.length === lines.filter((l) => l.trim()).length) return;
|
|
9300
|
+
const tmp = `${path2}.compacting`;
|
|
8931
9301
|
try {
|
|
8932
|
-
writeFileSync6(
|
|
9302
|
+
writeFileSync6(tmp, kept.length > 0 ? `${kept.join("\n")}
|
|
8933
9303
|
` : "", "utf8");
|
|
9304
|
+
renameSync2(tmp, path2);
|
|
8934
9305
|
} catch {
|
|
9306
|
+
try {
|
|
9307
|
+
unlinkSync4(tmp);
|
|
9308
|
+
} catch {
|
|
9309
|
+
}
|
|
8935
9310
|
}
|
|
8936
9311
|
}
|
|
8937
9312
|
function appendUsage(input) {
|