@zqagent/core 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -43
- package/README.oss.en.md +5 -0
- package/README.oss.md +5 -0
- package/README.oss.zh-TW.md +5 -0
- package/dist/cli/index.cjs +1135 -599
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1141 -605
- package/dist/cli/index.js.map +1 -1
- package/dist/{explore-D4lCKXbA.d.ts → explore-B_bIPzmB.d.ts} +49 -1
- package/dist/{explore-Dj1j_GyN.d.cts → explore-COqHKDoK.d.cts} +49 -1
- package/dist/index.cjs +654 -258
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -2
- package/dist/index.d.ts +12 -2
- package/dist/index.js +654 -258
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +974 -442
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.js +980 -448
- package/dist/server/index.js.map +1 -1
- package/dist/tools/explore.cjs +528 -141
- package/dist/tools/explore.cjs.map +1 -1
- package/dist/tools/explore.d.cts +1 -1
- package/dist/tools/explore.d.ts +1 -1
- package/dist/tools/explore.js +528 -141
- package/dist/tools/explore.js.map +1 -1
- package/package.json +1 -1
package/dist/tools/explore.cjs
CHANGED
|
@@ -314,8 +314,11 @@ function lookupContextWindow(modelId) {
|
|
|
314
314
|
return DEFAULT_CONTEXT_WINDOW;
|
|
315
315
|
}
|
|
316
316
|
var KEEP_RECENT_FRACTION = 0.25;
|
|
317
|
-
var
|
|
318
|
-
var
|
|
317
|
+
var SOFT_THRESHOLD_RATIO = 0.75;
|
|
318
|
+
var ELIDE_KEEP_FRACTION = 0.15;
|
|
319
|
+
var SUMMARY_MAX_TOKENS = 3e3;
|
|
320
|
+
var SPAN_TOOL_RESULT_CLIP = 200;
|
|
321
|
+
var SPAN_BUDGET_CHARS = 8e4;
|
|
319
322
|
var USER_MESSAGE_CLIP = 600;
|
|
320
323
|
var USER_MESSAGES_MAX = 40;
|
|
321
324
|
var TRIM_FRACTION = 0.1;
|
|
@@ -338,9 +341,22 @@ function triggerTokens(contextWindow, thresholdPct = DEFAULT_THRESHOLD_PCT, capT
|
|
|
338
341
|
}
|
|
339
342
|
return base;
|
|
340
343
|
}
|
|
344
|
+
function softTriggerTokens(contextWindow, thresholdPct = DEFAULT_THRESHOLD_PCT, capTokens, softThresholdPct) {
|
|
345
|
+
const hard = triggerTokens(contextWindow, thresholdPct, capTokens);
|
|
346
|
+
if (softThresholdPct != null && Number.isFinite(softThresholdPct) && softThresholdPct > 0) {
|
|
347
|
+
return Math.min(
|
|
348
|
+
triggerTokens(contextWindow, softThresholdPct, capTokens),
|
|
349
|
+
Math.floor(hard * 0.95)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return Math.floor(hard * SOFT_THRESHOLD_RATIO);
|
|
353
|
+
}
|
|
341
354
|
function shouldCompact(signal, contextWindow, thresholdPct, capTokens) {
|
|
342
355
|
return signal >= triggerTokens(contextWindow, thresholdPct, capTokens);
|
|
343
356
|
}
|
|
357
|
+
function elisionKeepChars(hardTriggerTokens, fraction = ELIDE_KEEP_FRACTION) {
|
|
358
|
+
return Math.max(4e3, Math.floor(hardTriggerTokens * fraction * 4));
|
|
359
|
+
}
|
|
344
360
|
function turnStarts(messages, start) {
|
|
345
361
|
const users = [];
|
|
346
362
|
const assistants = [];
|
|
@@ -490,10 +506,11 @@ var SUMMARY_SYSTEM_PROMPT = `\u4F60\u6B63\u5728\u538B\u7F29\u4E00\u4E2A AI \u534
|
|
|
490
506
|
|
|
491
507
|
\u89C4\u5219\uFF1A
|
|
492
508
|
- \u4E0D\u8981\u5C06\u5B8C\u6574\u7684\u6587\u4EF6\u5185\u5BB9\u5F53\u4F5C\u771F\u7406\u3002\u53EA\u8BB0\u5F55\u67D0\u4E2A\u6587\u4EF6\u88AB\u8BFB\u53D6/\u7F16\u8F91\u8FC7\u8FD9\u4E00\u4E8B\u5B9E\uFF1B\u534F\u4F5C\u8005\u5982\u679C\u9700\u8981\u518D\u6B21\u83B7\u53D6\u5185\u5BB9\u4F1A\u91CD\u65B0\u8BFB\u53D6\u3002\u9648\u65E7\u7684\u6587\u4EF6\u8BB0\u5FC6\u6BD4\u6CA1\u6709\u8BB0\u5FC6\u66F4\u7CDF\u7CD5\u3002
|
|
509
|
+
- \u82E5\u63D0\u4F9B\u4E86\u4E0A\u4E00\u7248\u6458\u8981\uFF1A\u53EA\u628A\u5B83\u4ECD\u7136\u6709\u6548\u7684\u7ED3\u8BBA\u5E76\u5165\u5BF9\u5E94\u7AE0\u8282\uFF0C\u4E0D\u8981\u6574\u6BB5\u590D\u8FF0\u6216\u9010\u6761\u8F6C\u6284\u3002
|
|
493
510
|
- \u8981\u5177\u4F53\uFF1A\u8DEF\u5F84\u3001\u540D\u79F0\u3001\u547D\u4EE4\u3001id\u2014\u2014\u800C\u4E0D\u662F\u6A21\u7CCA\u7684\u5F15\u7528\u3002
|
|
494
511
|
- \u4EC5\u8F93\u51FA\u6458\u8981\u7AE0\u8282\uFF0C\u4E0D\u9644\u52A0\u524D\u8A00\u3002`;
|
|
495
512
|
var CONTINUATION_CONTRACT = "\u4ECE\u4F60\u4E4B\u524D\u4E2D\u65AD\u7684\u5730\u65B9\u7EE7\u7EED\uFF1A\u5B8C\u5168\u6309\u7167\u63CF\u8FF0\u63A5\u7EED\u5F53\u524D\u5DE5\u4F5C\u548C\u4E0B\u4E00\u6B65\u3002\u4E0D\u8981\u91CD\u65B0\u8BE2\u95EE\u5DF2\u56DE\u7B54\u7684\u95EE\u9898\uFF0C\u4E0D\u8981\u56DE\u987E\uFF0C\u4E0D\u8981\u63D0\u53CA\u4E0A\u4E0B\u6587\u5DF2\u88AB\u538B\u7F29\u3002\u5982\u679C\u4F60\u9700\u8981\u4E0A\u8FF0\u63D0\u5230\u7684\u6587\u4EF6\u7684\u5185\u5BB9\uFF0C\u8BF7\u91CD\u65B0\u8BFB\u53D6\u5B83\u3002";
|
|
496
|
-
function renderSpan(span,
|
|
513
|
+
function renderSpan(span, budgetChars2 = SPAN_BUDGET_CHARS) {
|
|
497
514
|
const lines = [];
|
|
498
515
|
for (const m of span) {
|
|
499
516
|
const role = m.role;
|
|
@@ -517,8 +534,8 @@ function renderSpan(span, budgetChars = SPAN_BUDGET_CHARS) {
|
|
|
517
534
|
if (text) lines.push(`[${role}] ${text}`);
|
|
518
535
|
}
|
|
519
536
|
let rendered = lines.join("\n");
|
|
520
|
-
if (rendered.length >
|
|
521
|
-
rendered = "(\u2026oldest turns elided\u2026)\n" + rendered.slice(-
|
|
537
|
+
if (rendered.length > budgetChars2) {
|
|
538
|
+
rendered = "(\u2026oldest turns elided\u2026)\n" + rendered.slice(-budgetChars2);
|
|
522
539
|
}
|
|
523
540
|
return rendered;
|
|
524
541
|
}
|
|
@@ -532,9 +549,17 @@ function summarizerMessages(span, priorSummary = "") {
|
|
|
532
549
|
{ role: "user", content: body }
|
|
533
550
|
];
|
|
534
551
|
}
|
|
552
|
+
function summarizerReasoningEffort() {
|
|
553
|
+
const v = process.env.COMPACTION_REASONING_EFFORT?.toLowerCase();
|
|
554
|
+
if (v === "low" || v === "medium" || v === "high") return v;
|
|
555
|
+
return "low";
|
|
556
|
+
}
|
|
535
557
|
async function summarizeSpan(provider, model, span, priorSummary = "", opts) {
|
|
536
558
|
const msgs = summarizerMessages(span, priorSummary);
|
|
537
|
-
const chunks = await provider.stream(model, msgs, [],
|
|
559
|
+
const chunks = await provider.stream(model, msgs, [], {
|
|
560
|
+
...opts,
|
|
561
|
+
maxTokens: SUMMARY_MAX_TOKENS
|
|
562
|
+
});
|
|
538
563
|
let text = "";
|
|
539
564
|
for await (const chunk of chunks) {
|
|
540
565
|
if (chunk.content) text += chunk.content;
|
|
@@ -645,6 +670,166 @@ function applyToOutbound(messages, state) {
|
|
|
645
670
|
head.push({ role: "user", content: compactedBlock(state) });
|
|
646
671
|
return [...head, ...messages.slice(boundary)];
|
|
647
672
|
}
|
|
673
|
+
var ELIDE_EXEMPT_TOOLS = /* @__PURE__ */ new Set([
|
|
674
|
+
"explore",
|
|
675
|
+
"ask_user",
|
|
676
|
+
"propose_plan",
|
|
677
|
+
"request_directory",
|
|
678
|
+
"todo_write"
|
|
679
|
+
]);
|
|
680
|
+
function elidedToolContent(m) {
|
|
681
|
+
const name = m.tool_call_name || "tool";
|
|
682
|
+
const size = m.content.length;
|
|
683
|
+
const saved = /full output saved to ([^\]]+)/.exec(m.content)?.[1]?.trim();
|
|
684
|
+
return saved ? `[old tool result elided: ${name}, ${size} chars; full output saved to ${saved}]` : `[old tool result elided: ${name}, ${size} chars; re-run the tool if its content is still needed]`;
|
|
685
|
+
}
|
|
686
|
+
function isPlaceholder(content) {
|
|
687
|
+
return content.startsWith("[old tool result elided") || content.startsWith("[superseded by a later read_file");
|
|
688
|
+
}
|
|
689
|
+
var ONE_SHOT_TOOLS = /* @__PURE__ */ new Set(["git_diff"]);
|
|
690
|
+
var ONE_SHOT_KEEP_RECENT = 1;
|
|
691
|
+
function applyOneShotElision(messages, keepRecent = ONE_SHOT_KEEP_RECENT, tools = ONE_SHOT_TOOLS) {
|
|
692
|
+
const out = [...messages];
|
|
693
|
+
let seen = 0;
|
|
694
|
+
let elided = 0;
|
|
695
|
+
let freed = 0;
|
|
696
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
697
|
+
const m = messages[i];
|
|
698
|
+
if (m.role !== "tool" || !tools.has(m.tool_call_name ?? "")) continue;
|
|
699
|
+
if (isPlaceholder(m.content)) continue;
|
|
700
|
+
seen++;
|
|
701
|
+
if (seen <= keepRecent) continue;
|
|
702
|
+
const placeholder = elidedToolContent(m);
|
|
703
|
+
freed += m.content.length - placeholder.length;
|
|
704
|
+
out[i] = { ...m, content: placeholder };
|
|
705
|
+
elided++;
|
|
706
|
+
}
|
|
707
|
+
return elided > 0 ? { messages: out, elided, freedChars: freed } : { messages, elided: 0, freedChars: 0 };
|
|
708
|
+
}
|
|
709
|
+
function applyToolResultElision(messages, keepChars, exempt = ELIDE_EXEMPT_TOOLS) {
|
|
710
|
+
let total = 0;
|
|
711
|
+
for (const m of messages) {
|
|
712
|
+
if (m.role === "tool" && !exempt.has(m.tool_call_name ?? "")) {
|
|
713
|
+
total += m.content.length;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (total <= keepChars) return { messages, elided: 0, freedChars: 0 };
|
|
717
|
+
const out = [...messages];
|
|
718
|
+
let budget = 0;
|
|
719
|
+
let elided = 0;
|
|
720
|
+
let freed = 0;
|
|
721
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
722
|
+
const m = messages[i];
|
|
723
|
+
if (m.role !== "tool") continue;
|
|
724
|
+
if (exempt.has(m.tool_call_name ?? "")) continue;
|
|
725
|
+
if (elided === 0 && budget === 0 && m.content.length > keepChars) {
|
|
726
|
+
const kept = m.content.slice(0, keepChars) + "\n\u2026(truncated)";
|
|
727
|
+
freed += m.content.length - kept.length;
|
|
728
|
+
out[i] = { ...m, content: kept };
|
|
729
|
+
budget = keepChars;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (budget + m.content.length <= keepChars) {
|
|
733
|
+
budget += m.content.length;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const placeholder = elidedToolContent(m);
|
|
737
|
+
freed += m.content.length - placeholder.length;
|
|
738
|
+
out[i] = { ...m, content: placeholder };
|
|
739
|
+
elided++;
|
|
740
|
+
}
|
|
741
|
+
return { messages: out, elided, freedChars: freed };
|
|
742
|
+
}
|
|
743
|
+
function normalizeToolPath(p) {
|
|
744
|
+
let s = p.trim().replace(/\\/g, "/");
|
|
745
|
+
while (s.startsWith("./")) s = s.slice(2);
|
|
746
|
+
return s.replace(/\/{2,}/g, "/");
|
|
747
|
+
}
|
|
748
|
+
var MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
749
|
+
"write_file",
|
|
750
|
+
"replace_in_file",
|
|
751
|
+
"apply_patch",
|
|
752
|
+
"apply_unified_diff",
|
|
753
|
+
"run_shell"
|
|
754
|
+
]);
|
|
755
|
+
function parseReadFileSpan(content) {
|
|
756
|
+
const body = content.replace(/^\[tool output truncated:[^\n]*\]\n\n/, "");
|
|
757
|
+
let path10;
|
|
758
|
+
let start;
|
|
759
|
+
let end;
|
|
760
|
+
try {
|
|
761
|
+
const obj = JSON.parse(body);
|
|
762
|
+
path10 = obj.path;
|
|
763
|
+
start = obj.start_line;
|
|
764
|
+
end = obj.end_line;
|
|
765
|
+
} catch {
|
|
766
|
+
const str = (key) => new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`).exec(body)?.[1];
|
|
767
|
+
const num = (key) => {
|
|
768
|
+
const m = new RegExp(`"${key}"\\s*:\\s*(\\d+)`).exec(body);
|
|
769
|
+
return m ? Number(m[1]) : void 0;
|
|
770
|
+
};
|
|
771
|
+
path10 = str("path");
|
|
772
|
+
start = num("start_line");
|
|
773
|
+
end = num("end_line");
|
|
774
|
+
}
|
|
775
|
+
if (typeof path10 !== "string" || !path10) return null;
|
|
776
|
+
if (typeof start !== "number" || typeof end !== "number") return null;
|
|
777
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
778
|
+
return { path: path10, start, end };
|
|
779
|
+
}
|
|
780
|
+
function addSpan(spans, start, end) {
|
|
781
|
+
spans.push([start, end]);
|
|
782
|
+
spans.sort((a, b) => a[0] - b[0]);
|
|
783
|
+
const merged = [];
|
|
784
|
+
for (const cur of spans) {
|
|
785
|
+
const last = merged[merged.length - 1];
|
|
786
|
+
if (last && cur[0] <= last[1] + 1) {
|
|
787
|
+
last[1] = Math.max(last[1], cur[1]);
|
|
788
|
+
} else {
|
|
789
|
+
merged.push([...cur]);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
spans.length = 0;
|
|
793
|
+
spans.push(...merged);
|
|
794
|
+
}
|
|
795
|
+
function coversSpans(spans, start, end) {
|
|
796
|
+
return spans.some(([a, b]) => a <= start && b >= end);
|
|
797
|
+
}
|
|
798
|
+
function applyReadFileDedup(messages) {
|
|
799
|
+
let candidates = 0;
|
|
800
|
+
for (const m of messages) {
|
|
801
|
+
if (m.role === "tool" && m.tool_call_name === "read_file") candidates++;
|
|
802
|
+
}
|
|
803
|
+
if (candidates < 2) return { messages, elided: 0 };
|
|
804
|
+
const out = [...messages];
|
|
805
|
+
const covered = /* @__PURE__ */ new Map();
|
|
806
|
+
let elided = 0;
|
|
807
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
808
|
+
const m = messages[i];
|
|
809
|
+
if (m.role === "assistant") {
|
|
810
|
+
if (m.tool_calls?.some((tc) => MUTATING_TOOLS.has(tc.function.name))) {
|
|
811
|
+
covered.clear();
|
|
812
|
+
}
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
if (m.role !== "tool" || m.tool_call_name !== "read_file") continue;
|
|
816
|
+
const span = parseReadFileSpan(m.content);
|
|
817
|
+
if (!span) continue;
|
|
818
|
+
const key = normalizeToolPath(span.path);
|
|
819
|
+
const seen = covered.get(key) ?? [];
|
|
820
|
+
if (coversSpans(seen, span.start, span.end)) {
|
|
821
|
+
out[i] = {
|
|
822
|
+
...m,
|
|
823
|
+
content: `[superseded by a later read_file of ${span.path} (lines ${span.start}-${span.end})]`
|
|
824
|
+
};
|
|
825
|
+
elided++;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
addSpan(seen, span.start, span.end);
|
|
829
|
+
covered.set(key, seen);
|
|
830
|
+
}
|
|
831
|
+
return { messages: elided > 0 ? out : messages, elided };
|
|
832
|
+
}
|
|
648
833
|
var OVERFLOW_MARKERS = [
|
|
649
834
|
"context_length_exceeded",
|
|
650
835
|
"maximum context length",
|
|
@@ -660,9 +845,108 @@ function isContextOverflow(exc) {
|
|
|
660
845
|
return OVERFLOW_MARKERS.some((m) => text.includes(m));
|
|
661
846
|
}
|
|
662
847
|
|
|
848
|
+
// src/engine/toolResultBudget.ts
|
|
849
|
+
var fs = __toESM(require("fs"), 1);
|
|
850
|
+
var path2 = __toESM(require("path"), 1);
|
|
851
|
+
|
|
852
|
+
// src/paths.ts
|
|
853
|
+
var os = __toESM(require("os"), 1);
|
|
854
|
+
var path = __toESM(require("path"), 1);
|
|
855
|
+
var APP_DIR_NAME = "zqagent";
|
|
856
|
+
var IS_WINDOWS = process.platform === "win32";
|
|
857
|
+
function homeDir() {
|
|
858
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir() || os.tmpdir();
|
|
859
|
+
}
|
|
860
|
+
function stateDir() {
|
|
861
|
+
const base = process.env.XDG_STATE_HOME || (IS_WINDOWS ? process.env.LOCALAPPDATA : void 0) || path.join(homeDir(), ".local", "state");
|
|
862
|
+
return path.join(base, APP_DIR_NAME);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/engine/toolResultBudget.ts
|
|
866
|
+
var DEFAULT_TOOL_RESULT_MAX_CHARS = 24e3;
|
|
867
|
+
var HEAD_RATIO = 0.6;
|
|
868
|
+
function budgetChars() {
|
|
869
|
+
const n = Number(process.env.TOOL_RESULT_MAX_CHARS);
|
|
870
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_TOOL_RESULT_MAX_CHARS;
|
|
871
|
+
}
|
|
872
|
+
function toolResultDir() {
|
|
873
|
+
const env = process.env.TOOL_RESULT_DIR?.trim();
|
|
874
|
+
return env || path2.join(stateDir(), "tool-results");
|
|
875
|
+
}
|
|
876
|
+
function clipText(text, budget) {
|
|
877
|
+
if (text.length <= budget) return text;
|
|
878
|
+
const room = Math.max(
|
|
879
|
+
0,
|
|
880
|
+
budget - `
|
|
881
|
+
|
|
882
|
+
\u2026 [${text.length} characters omitted] \u2026
|
|
883
|
+
|
|
884
|
+
`.length
|
|
885
|
+
);
|
|
886
|
+
const headLen = Math.floor(room * HEAD_RATIO);
|
|
887
|
+
const tailLen = room - headLen;
|
|
888
|
+
const omitted = text.length - headLen - tailLen;
|
|
889
|
+
return text.slice(0, headLen) + `
|
|
890
|
+
|
|
891
|
+
\u2026 [${omitted} characters omitted] \u2026
|
|
892
|
+
|
|
893
|
+
` + text.slice(-tailLen);
|
|
894
|
+
}
|
|
895
|
+
function clipDeep(value, perField) {
|
|
896
|
+
if (typeof value === "string") {
|
|
897
|
+
return value.length <= perField ? value : clipText(value, perField);
|
|
898
|
+
}
|
|
899
|
+
if (Array.isArray(value)) {
|
|
900
|
+
return value.map((v) => clipDeep(v, perField));
|
|
901
|
+
}
|
|
902
|
+
if (value && typeof value === "object") {
|
|
903
|
+
const out = {};
|
|
904
|
+
for (const [k, v] of Object.entries(value)) out[k] = clipDeep(v, perField);
|
|
905
|
+
return out;
|
|
906
|
+
}
|
|
907
|
+
return value;
|
|
908
|
+
}
|
|
909
|
+
function saveRaw(tool, raw, dir) {
|
|
910
|
+
try {
|
|
911
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
912
|
+
const safe = tool.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40) || "tool";
|
|
913
|
+
const name = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}-${safe}.txt`;
|
|
914
|
+
const file = path2.join(dir, name);
|
|
915
|
+
fs.writeFileSync(file, raw, "utf-8");
|
|
916
|
+
return file;
|
|
917
|
+
} catch {
|
|
918
|
+
return void 0;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function applyToolResultBudget(tool, result, opts = {}) {
|
|
922
|
+
const budget = opts.maxChars ?? budgetChars();
|
|
923
|
+
const raw = typeof result === "string" ? result : JSON.stringify(result, null, 2) ?? String(result);
|
|
924
|
+
if (raw.length <= budget) {
|
|
925
|
+
return { content: raw, truncated: false };
|
|
926
|
+
}
|
|
927
|
+
let content = raw;
|
|
928
|
+
if (typeof result === "object" && result !== null) {
|
|
929
|
+
let perField = Math.floor(budget * 0.7);
|
|
930
|
+
let clipped = JSON.stringify(clipDeep(result, perField), null, 2);
|
|
931
|
+
for (let i = 0; i < 6 && clipped && clipped.length > budget && perField > 128; i++) {
|
|
932
|
+
perField = Math.floor(perField / 2);
|
|
933
|
+
clipped = JSON.stringify(clipDeep(result, perField), null, 2);
|
|
934
|
+
}
|
|
935
|
+
if (clipped && clipped.length <= budget) content = clipped;
|
|
936
|
+
}
|
|
937
|
+
if (content.length > budget) content = clipText(content, budget);
|
|
938
|
+
const savedPath = saveRaw(tool, raw, opts.dir ?? toolResultDir());
|
|
939
|
+
if (savedPath) {
|
|
940
|
+
content = `[tool output truncated: ${raw.length} chars, full output saved to ${savedPath}]
|
|
941
|
+
|
|
942
|
+
` + content;
|
|
943
|
+
}
|
|
944
|
+
return { content, truncated: true, savedPath };
|
|
945
|
+
}
|
|
946
|
+
|
|
663
947
|
// src/engine/TurnEngine.ts
|
|
664
948
|
var crypto = __toESM(require("crypto"), 1);
|
|
665
|
-
var
|
|
949
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
666
950
|
var _path = __toESM(require("path"), 1);
|
|
667
951
|
|
|
668
952
|
// src/prompts/index.ts
|
|
@@ -707,13 +991,17 @@ var INTERACTIVE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
707
991
|
"propose_plan",
|
|
708
992
|
"request_directory"
|
|
709
993
|
]);
|
|
710
|
-
|
|
711
|
-
|
|
994
|
+
var MAX_COMPACT_ROUNDS = 4;
|
|
995
|
+
var OVERFLOW_TARGET_RATIO = 0.5;
|
|
996
|
+
var OVERFLOW_TRIM_FRACTION = 0.5;
|
|
997
|
+
var COMPACT_STALL_RETRY_DELTA = 20;
|
|
998
|
+
function toolResultMessage(tc, result, taskId, toolResultDir2) {
|
|
999
|
+
const budgeted = applyToolResultBudget(tc.name, result, { dir: toolResultDir2 });
|
|
712
1000
|
return {
|
|
713
1001
|
role: "tool",
|
|
714
1002
|
tool_call_id: tc.id,
|
|
715
1003
|
tool_call_name: tc.name,
|
|
716
|
-
content,
|
|
1004
|
+
content: budgeted.content,
|
|
717
1005
|
taskId
|
|
718
1006
|
};
|
|
719
1007
|
}
|
|
@@ -824,12 +1112,22 @@ var TurnEngine = class _TurnEngine {
|
|
|
824
1112
|
// 上下文压缩
|
|
825
1113
|
compactionState = null;
|
|
826
1114
|
_lastContextTokens = null;
|
|
1115
|
+
/** 压缩无进展时的消息条数快照;-1 表示无冷却 */
|
|
1116
|
+
_compactStalledAt = -1;
|
|
827
1117
|
/** 显式配置的上下文窗口大小(token 数),未设置时为 undefined(走自动检测) */
|
|
828
1118
|
_contextWindow;
|
|
829
1119
|
/** 压缩触发阈值比例(默认 0.8) */
|
|
830
1120
|
_thresholdPct;
|
|
1121
|
+
/** 软阈值比例(undefined = 取硬阈值的 75%) */
|
|
1122
|
+
_softThresholdPct;
|
|
1123
|
+
/** 是否启用软阈值淘汰(COMPACTION_ELIDE=0 可关闭) */
|
|
1124
|
+
_elisionEnabled;
|
|
1125
|
+
/** 是否启用重复读取去重(COMPACTION_DEDUP=0 可关闭) */
|
|
1126
|
+
_dedupEnabled;
|
|
831
1127
|
/** 压缩触发阈值绝对上限(null = 不限制) */
|
|
832
1128
|
_capTokens;
|
|
1129
|
+
/** 超预算工具结果的落盘目录(undefined = 使用默认状态目录) */
|
|
1130
|
+
_toolResultDir;
|
|
833
1131
|
// 产出文件追踪(本轮 Turn 中成功写入的文件)
|
|
834
1132
|
_producedFiles = [];
|
|
835
1133
|
_turnStartTime = 0;
|
|
@@ -881,7 +1179,15 @@ var TurnEngine = class _TurnEngine {
|
|
|
881
1179
|
this._hook = opts.hook;
|
|
882
1180
|
this._contextWindow = opts.contextWindow;
|
|
883
1181
|
this._thresholdPct = opts.thresholdPct ?? DEFAULT_THRESHOLD_PCT;
|
|
1182
|
+
this._softThresholdPct = opts.softThresholdPct;
|
|
1183
|
+
this._elisionEnabled = !/^(0|false|off|no)$/i.test(
|
|
1184
|
+
(process.env.COMPACTION_ELIDE ?? "").trim()
|
|
1185
|
+
);
|
|
1186
|
+
this._dedupEnabled = !/^(0|false|off|no)$/i.test(
|
|
1187
|
+
(process.env.COMPACTION_DEDUP ?? "").trim()
|
|
1188
|
+
);
|
|
884
1189
|
this._capTokens = opts.capTokens ?? null;
|
|
1190
|
+
this._toolResultDir = opts.toolResultDir;
|
|
885
1191
|
this._reasoningEffort = opts.reasoningEffort;
|
|
886
1192
|
if (this.messages.length === 0 || this.messages[0].role !== "system") {
|
|
887
1193
|
this.messages.unshift({ role: "system", content: this.instructions });
|
|
@@ -937,6 +1243,7 @@ var TurnEngine = class _TurnEngine {
|
|
|
937
1243
|
this._cancel = false;
|
|
938
1244
|
this._producedFiles = [];
|
|
939
1245
|
this._turnStartTime = Date.now();
|
|
1246
|
+
this._compactStalledAt = -1;
|
|
940
1247
|
this._emitHook((h) => h.onTurnStart?.(userInput));
|
|
941
1248
|
yield {
|
|
942
1249
|
type: "turn_start" /* TURN_START */,
|
|
@@ -1653,7 +1960,9 @@ var TurnEngine = class _TurnEngine {
|
|
|
1653
1960
|
for (let i = 0; i < concurrent.length; i++) {
|
|
1654
1961
|
const tc = concurrent[i];
|
|
1655
1962
|
const { result, status } = results[i];
|
|
1656
|
-
this.messages.push(
|
|
1963
|
+
this.messages.push(
|
|
1964
|
+
toolResultMessage(tc, result, this.taskId, this._toolResultDir)
|
|
1965
|
+
);
|
|
1657
1966
|
this._trackProducedFile(
|
|
1658
1967
|
tc,
|
|
1659
1968
|
result,
|
|
@@ -1716,7 +2025,9 @@ var TurnEngine = class _TurnEngine {
|
|
|
1716
2025
|
};
|
|
1717
2026
|
status = "error";
|
|
1718
2027
|
}
|
|
1719
|
-
this.messages.push(
|
|
2028
|
+
this.messages.push(
|
|
2029
|
+
toolResultMessage(tc, result, this.taskId, this._toolResultDir)
|
|
2030
|
+
);
|
|
1720
2031
|
this._trackProducedFile(
|
|
1721
2032
|
tc,
|
|
1722
2033
|
result,
|
|
@@ -1751,13 +2062,41 @@ var TurnEngine = class _TurnEngine {
|
|
|
1751
2062
|
* 过滤 notice 消息;tool 消息只保留必要字段;
|
|
1752
2063
|
* 其余消息剥离 display-only sidecar 字段(source/_display/ts/
|
|
1753
2064
|
* reasoning/usage),避免污染模型上下文。
|
|
2065
|
+
*
|
|
2066
|
+
* 达到软阈值时先淘汰旧工具结果(零 LLM 成本),再产出最终视图。
|
|
1754
2067
|
*/
|
|
1755
2068
|
_outboundMessages() {
|
|
1756
|
-
const
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
|
|
2069
|
+
const base = applyToOutbound(this.messages, this.compactionState);
|
|
2070
|
+
const deduped = this._dedupEnabled ? applyReadFileDedup(base).messages : base;
|
|
2071
|
+
let view = this._finalizeOutbound(deduped);
|
|
2072
|
+
if (!this._elisionEnabled) return view;
|
|
2073
|
+
if (estimateTokens(view) < this._softTriggerTokens()) {
|
|
2074
|
+
return view;
|
|
2075
|
+
}
|
|
2076
|
+
const oneShot = applyOneShotElision(deduped);
|
|
2077
|
+
if (oneShot.freedChars > 0) {
|
|
2078
|
+
view = this._finalizeOutbound(oneShot.messages);
|
|
2079
|
+
if (estimateTokens(view) < this._softTriggerTokens()) {
|
|
2080
|
+
return view;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
const keepChars = elisionKeepChars(
|
|
2084
|
+
triggerTokens(
|
|
2085
|
+
this._getContextWindow(),
|
|
2086
|
+
this._thresholdPct,
|
|
2087
|
+
this._capTokens
|
|
2088
|
+
)
|
|
1760
2089
|
);
|
|
2090
|
+
const elided = applyToolResultElision(
|
|
2091
|
+
oneShot.messages,
|
|
2092
|
+
keepChars
|
|
2093
|
+
);
|
|
2094
|
+
if (elided.freedChars === 0) return view;
|
|
2095
|
+
return this._finalizeOutbound(elided.messages);
|
|
2096
|
+
}
|
|
2097
|
+
/** 过滤 notice + 剥离 display-only 字段,得到最终发送视图 */
|
|
2098
|
+
_finalizeOutbound(sourceMessages) {
|
|
2099
|
+
const _STRIP = /* @__PURE__ */ new Set(["source", "_display", "ts", "reasoning", "usage"]);
|
|
1761
2100
|
return sourceMessages.filter((m) => m.role !== "notice").map((m) => {
|
|
1762
2101
|
if (m.role === "tool") {
|
|
1763
2102
|
return {
|
|
@@ -1775,6 +2114,15 @@ var TurnEngine = class _TurnEngine {
|
|
|
1775
2114
|
return stripped;
|
|
1776
2115
|
});
|
|
1777
2116
|
}
|
|
2117
|
+
/** 软阈值(token 数):达到它只淘汰旧工具结果 */
|
|
2118
|
+
_softTriggerTokens() {
|
|
2119
|
+
return softTriggerTokens(
|
|
2120
|
+
this._getContextWindow(),
|
|
2121
|
+
this._thresholdPct,
|
|
2122
|
+
this._capTokens,
|
|
2123
|
+
this._softThresholdPct
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
1778
2126
|
/** 通知消息:仅用于日志/展示,不发给模型 */
|
|
1779
2127
|
_appendNotice(kind, text) {
|
|
1780
2128
|
this.messages.push({
|
|
@@ -1816,18 +2164,36 @@ var TurnEngine = class _TurnEngine {
|
|
|
1816
2164
|
return false;
|
|
1817
2165
|
}
|
|
1818
2166
|
// ===================== 自动压缩 =====================
|
|
1819
|
-
/**
|
|
2167
|
+
/**
|
|
2168
|
+
* 当前上下文 token 用量。
|
|
2169
|
+
*
|
|
2170
|
+
* API 返回的 `usage.input` 是精确值,但反映的是「上一次请求」——工具结果
|
|
2171
|
+
* 与淘汰/压缩都会让它过时。因此同时估算当前 outbound 视图,取较大者:
|
|
2172
|
+
* 宁可早一点压缩,也不要漏判导致溢出。
|
|
2173
|
+
*/
|
|
1820
2174
|
_compactionTokens() {
|
|
1821
|
-
|
|
1822
|
-
|
|
2175
|
+
const estimated = estimateTokens(this._outboundMessages());
|
|
2176
|
+
if (this._lastContextTokens == null) {
|
|
2177
|
+
return { tokens: estimated, estimated: true };
|
|
1823
2178
|
}
|
|
1824
|
-
|
|
1825
|
-
tokens:
|
|
1826
|
-
|
|
1827
|
-
};
|
|
2179
|
+
if (estimated > this._lastContextTokens) {
|
|
2180
|
+
return { tokens: estimated, estimated: true };
|
|
2181
|
+
}
|
|
2182
|
+
return { tokens: this._lastContextTokens, estimated: false };
|
|
2183
|
+
}
|
|
2184
|
+
/** 压缩后重置精确计数:后续按估算值判断 */
|
|
2185
|
+
_resetContextTokens() {
|
|
2186
|
+
this._lastContextTokens = null;
|
|
2187
|
+
}
|
|
2188
|
+
/** 当前 outbound 视图的估算 token 数 */
|
|
2189
|
+
_estimateContextTokens() {
|
|
2190
|
+
return estimateTokens(this._outboundMessages());
|
|
1828
2191
|
}
|
|
1829
2192
|
/** 检查是否需要压缩(每轮 LLM 调用前) */
|
|
1830
2193
|
_compactionDue() {
|
|
2194
|
+
if (this._compactStalledAt >= 0 && this.messages.length < this._compactStalledAt + COMPACT_STALL_RETRY_DELTA) {
|
|
2195
|
+
return false;
|
|
2196
|
+
}
|
|
1831
2197
|
const { tokens } = this._compactionTokens();
|
|
1832
2198
|
return shouldCompact(
|
|
1833
2199
|
tokens,
|
|
@@ -1874,63 +2240,82 @@ var TurnEngine = class _TurnEngine {
|
|
|
1874
2240
|
async _compactNow(reqId, force = false) {
|
|
1875
2241
|
const contextWindow = this._getContextWindow();
|
|
1876
2242
|
const messagesBefore = this._outboundMessages().length;
|
|
1877
|
-
const
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
this._capTokens
|
|
1882
|
-
)
|
|
2243
|
+
const trigger = triggerTokens(
|
|
2244
|
+
contextWindow,
|
|
2245
|
+
this._thresholdPct,
|
|
2246
|
+
this._capTokens
|
|
1883
2247
|
);
|
|
1884
|
-
const
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2248
|
+
const target = Math.floor(
|
|
2249
|
+
trigger * (force ? OVERFLOW_TARGET_RATIO : 0.9)
|
|
2250
|
+
);
|
|
2251
|
+
const model = process.env.COMPACTION_MODEL?.trim() || this.model;
|
|
2252
|
+
const reasoningEffort = summarizerReasoningEffort();
|
|
2253
|
+
let keep = Math.floor(KEEP_RECENT_FRACTION * trigger);
|
|
2254
|
+
let method = null;
|
|
2255
|
+
let lastState = null;
|
|
2256
|
+
let progressed = false;
|
|
2257
|
+
for (let round = 0; round < MAX_COMPACT_ROUNDS; round++) {
|
|
2258
|
+
if (round > 0 && this._estimateContextTokens() <= target) break;
|
|
2259
|
+
let state = null;
|
|
2260
|
+
for (let attempt = 0; attempt < 2 && !state; attempt++) {
|
|
2261
|
+
try {
|
|
2262
|
+
const logTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
2263
|
+
state = await buildState(
|
|
2264
|
+
this.messages,
|
|
2265
|
+
this.provider,
|
|
2266
|
+
model,
|
|
2267
|
+
keep,
|
|
2268
|
+
this.compactionState,
|
|
2269
|
+
{ reqId, logTime, reasoningEffort }
|
|
2270
|
+
);
|
|
2271
|
+
} catch {
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
if (state) {
|
|
2275
|
+
this.compactionState = state;
|
|
2276
|
+
lastState = state;
|
|
2277
|
+
method = "summarized";
|
|
2278
|
+
progressed = true;
|
|
2279
|
+
} else {
|
|
2280
|
+
const trimmed = trimState(
|
|
1891
2281
|
this.messages,
|
|
1892
|
-
this.provider,
|
|
1893
|
-
model,
|
|
1894
|
-
keep,
|
|
1895
2282
|
this.compactionState,
|
|
1896
|
-
|
|
2283
|
+
force ? OVERFLOW_TRIM_FRACTION : 0.25
|
|
1897
2284
|
);
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2285
|
+
if (!trimmed) break;
|
|
2286
|
+
this.compactionState = trimmed;
|
|
2287
|
+
lastState = trimmed;
|
|
2288
|
+
method = "trimmed";
|
|
2289
|
+
progressed = true;
|
|
1902
2290
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
2291
|
+
this._resetContextTokens();
|
|
2292
|
+
if (this._estimateContextTokens() <= trigger) break;
|
|
2293
|
+
keep = Math.max(Math.floor(trigger * 0.05), Math.floor(keep / 2));
|
|
2294
|
+
}
|
|
2295
|
+
if (!progressed || !lastState) return { notice: null, method: null };
|
|
2296
|
+
this._compactStalledAt = this._estimateContextTokens() >= trigger ? this.messages.length : -1;
|
|
2297
|
+
const messagesAfter = this._outboundMessages().length;
|
|
2298
|
+
const common = {
|
|
2299
|
+
boundaryIndex: lastState.boundaryIndex,
|
|
2300
|
+
messagesBefore,
|
|
2301
|
+
messagesAfter
|
|
2302
|
+
};
|
|
2303
|
+
if (method === "summarized") {
|
|
1907
2304
|
return {
|
|
1908
2305
|
notice: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29 \u2014 \u65E9\u671F\u8F6E\u6B21\u5DF2\u6458\u8981",
|
|
1909
2306
|
method: "summarized",
|
|
1910
|
-
modelUsed:
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
messagesBefore,
|
|
1916
|
-
messagesAfter: this._outboundMessages().length
|
|
2307
|
+
modelUsed: lastState.modelUsed,
|
|
2308
|
+
summaryPreview: lastState.summaryText.slice(0, 200),
|
|
2309
|
+
keptUserMessages: lastState.userMessages.length,
|
|
2310
|
+
droppedUserMessages: lastState.userMessagesDropped,
|
|
2311
|
+
...common
|
|
1917
2312
|
};
|
|
1918
2313
|
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
return {
|
|
1925
|
-
notice: "\u4E0A\u4E0B\u6587\u5DF2\u88C1\u526A \u2014 \u6700\u65E7\u7684\u8F6E\u6B21\u5DF2\u4E22\u5F03\uFF08\u6458\u8981\u4E0D\u53EF\u7528\uFF09",
|
|
1926
|
-
method: "trimmed",
|
|
1927
|
-
boundaryIndex: trimmed.boundaryIndex,
|
|
1928
|
-
messagesBefore,
|
|
1929
|
-
messagesAfter: this._outboundMessages().length
|
|
1930
|
-
};
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
return { notice: null, method: null };
|
|
2314
|
+
return {
|
|
2315
|
+
notice: "\u4E0A\u4E0B\u6587\u5DF2\u88C1\u526A \u2014 \u6700\u65E7\u7684\u8F6E\u6B21\u5DF2\u4E22\u5F03\uFF08\u6458\u8981\u4E0D\u53EF\u7528\uFF09",
|
|
2316
|
+
method: "trimmed",
|
|
2317
|
+
...common
|
|
2318
|
+
};
|
|
1934
2319
|
}
|
|
1935
2320
|
/** 追踪文件写入工具成功产生的文件 */
|
|
1936
2321
|
_trackProducedFile(tc, result, status) {
|
|
@@ -1940,9 +2325,9 @@ var TurnEngine = class _TurnEngine {
|
|
|
1940
2325
|
return;
|
|
1941
2326
|
}
|
|
1942
2327
|
if (result && typeof result === "object") {
|
|
1943
|
-
const
|
|
1944
|
-
if (
|
|
1945
|
-
this._producedFiles.push(
|
|
2328
|
+
const path10 = result.path;
|
|
2329
|
+
if (path10) {
|
|
2330
|
+
this._producedFiles.push(path10);
|
|
1946
2331
|
return;
|
|
1947
2332
|
}
|
|
1948
2333
|
const changed = result.changed_files;
|
|
@@ -1966,7 +2351,7 @@ var TurnEngine = class _TurnEngine {
|
|
|
1966
2351
|
_fileExists(f, ws) {
|
|
1967
2352
|
if (!ws && !_path.isAbsolute(f)) return true;
|
|
1968
2353
|
try {
|
|
1969
|
-
return
|
|
2354
|
+
return fs2.existsSync(_path.isAbsolute(f) ? f : _path.join(ws, f));
|
|
1970
2355
|
} catch {
|
|
1971
2356
|
return false;
|
|
1972
2357
|
}
|
|
@@ -1989,7 +2374,7 @@ var TurnEngine = class _TurnEngine {
|
|
|
1989
2374
|
if (depth > 4 || results.length >= 50) return;
|
|
1990
2375
|
let entries;
|
|
1991
2376
|
try {
|
|
1992
|
-
entries =
|
|
2377
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
1993
2378
|
} catch {
|
|
1994
2379
|
return;
|
|
1995
2380
|
}
|
|
@@ -2004,7 +2389,7 @@ var TurnEngine = class _TurnEngine {
|
|
|
2004
2389
|
const ext = dotIdx !== -1 ? e.name.slice(dotIdx).toLowerCase() : "";
|
|
2005
2390
|
if (!_TurnEngine.SCAN_SUFFIXES.has(ext)) continue;
|
|
2006
2391
|
try {
|
|
2007
|
-
const st =
|
|
2392
|
+
const st = fs2.statSync(full);
|
|
2008
2393
|
if (st.mtimeMs >= since) {
|
|
2009
2394
|
results.push(
|
|
2010
2395
|
_path.relative(_path.resolve(ws), _path.resolve(full))
|
|
@@ -2035,48 +2420,48 @@ function parseMaxIterations(value, fallback) {
|
|
|
2035
2420
|
}
|
|
2036
2421
|
|
|
2037
2422
|
// src/tools/pathSafety.ts
|
|
2038
|
-
var
|
|
2039
|
-
var
|
|
2423
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
2424
|
+
var path3 = __toESM(require("path"), 1);
|
|
2040
2425
|
function isInsideRelative(rel) {
|
|
2041
|
-
return !
|
|
2426
|
+
return !path3.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path3.sep}`);
|
|
2042
2427
|
}
|
|
2043
2428
|
function isFilesystemRoot(p) {
|
|
2044
|
-
return
|
|
2429
|
+
return path3.parse(p).root === p;
|
|
2045
2430
|
}
|
|
2046
2431
|
function resolveWithinRoot(workspace, target) {
|
|
2047
|
-
const abs =
|
|
2432
|
+
const abs = path3.resolve(workspace, target);
|
|
2048
2433
|
let rootReal;
|
|
2049
2434
|
try {
|
|
2050
|
-
rootReal =
|
|
2435
|
+
rootReal = fs3.realpathSync(workspace);
|
|
2051
2436
|
} catch {
|
|
2052
|
-
const rootAbs =
|
|
2053
|
-
if (!isInsideRelative(
|
|
2437
|
+
const rootAbs = path3.resolve(workspace);
|
|
2438
|
+
if (!isInsideRelative(path3.relative(rootAbs, abs))) return null;
|
|
2054
2439
|
return { root: rootAbs, real: abs };
|
|
2055
2440
|
}
|
|
2056
2441
|
if (isFilesystemRoot(rootReal)) return null;
|
|
2057
2442
|
let real;
|
|
2058
2443
|
try {
|
|
2059
|
-
real =
|
|
2444
|
+
real = fs3.realpathSync(abs);
|
|
2060
2445
|
} catch {
|
|
2061
2446
|
let probe = abs;
|
|
2062
2447
|
const tail = [];
|
|
2063
2448
|
for (; ; ) {
|
|
2064
|
-
const exists =
|
|
2449
|
+
const exists = fs3.existsSync(probe) || fs3.lstatSync(probe, { throwIfNoEntry: false }) !== void 0;
|
|
2065
2450
|
if (exists) {
|
|
2066
2451
|
try {
|
|
2067
|
-
real =
|
|
2452
|
+
real = path3.join(fs3.realpathSync(probe), ...tail);
|
|
2068
2453
|
break;
|
|
2069
2454
|
} catch {
|
|
2070
2455
|
return null;
|
|
2071
2456
|
}
|
|
2072
2457
|
}
|
|
2073
|
-
tail.unshift(
|
|
2074
|
-
const parent =
|
|
2458
|
+
tail.unshift(path3.basename(probe));
|
|
2459
|
+
const parent = path3.dirname(probe);
|
|
2075
2460
|
if (parent === probe) return null;
|
|
2076
2461
|
probe = parent;
|
|
2077
2462
|
}
|
|
2078
2463
|
}
|
|
2079
|
-
if (!isInsideRelative(
|
|
2464
|
+
if (!isInsideRelative(path3.relative(rootReal, real))) return null;
|
|
2080
2465
|
return { root: rootReal, real };
|
|
2081
2466
|
}
|
|
2082
2467
|
function isWithinRoot(workspace, target) {
|
|
@@ -2084,7 +2469,7 @@ function isWithinRoot(workspace, target) {
|
|
|
2084
2469
|
}
|
|
2085
2470
|
|
|
2086
2471
|
// src/permissions/PermissionEngine.ts
|
|
2087
|
-
var
|
|
2472
|
+
var path4 = __toESM(require("path"), 1);
|
|
2088
2473
|
var READ_ONLY_MODES = /* @__PURE__ */ new Set(["discuss" /* DISCUSS */, "plan" /* PLAN */]);
|
|
2089
2474
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
2090
2475
|
"write_file",
|
|
@@ -2134,7 +2519,7 @@ var PermissionEngine = class {
|
|
|
2134
2519
|
/** 会话级「总是允许此命令」集合 */
|
|
2135
2520
|
sessionAllowCommands;
|
|
2136
2521
|
constructor(opts) {
|
|
2137
|
-
this.workspaceRoot =
|
|
2522
|
+
this.workspaceRoot = path4.resolve(opts.workspaceRoot);
|
|
2138
2523
|
this.mode = opts.mode;
|
|
2139
2524
|
this.allowedCommands = Array.isArray(opts.allowedCommands) ? opts.allowedCommands : [];
|
|
2140
2525
|
this.sessionAllowTools = /* @__PURE__ */ new Set();
|
|
@@ -2226,12 +2611,12 @@ var PermissionEngine = class {
|
|
|
2226
2611
|
};
|
|
2227
2612
|
|
|
2228
2613
|
// src/tools/files.ts
|
|
2229
|
-
var
|
|
2230
|
-
var
|
|
2614
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
2615
|
+
var path5 = __toESM(require("path"), 1);
|
|
2231
2616
|
var DEFAULT_MAX_LINES = 2e3;
|
|
2232
2617
|
var MAX_LINE_CHARS = 500;
|
|
2233
2618
|
function makeReadFile(workspace) {
|
|
2234
|
-
const root =
|
|
2619
|
+
const root = path5.resolve(workspace);
|
|
2235
2620
|
return function read_file(args) {
|
|
2236
2621
|
const filePath = String(args.path ?? "");
|
|
2237
2622
|
const startLine = Math.max(1, Number(args.start_line ?? 1));
|
|
@@ -2241,13 +2626,13 @@ function makeReadFile(workspace) {
|
|
|
2241
2626
|
return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
|
|
2242
2627
|
}
|
|
2243
2628
|
const target = resolved.real;
|
|
2244
|
-
if (!
|
|
2629
|
+
if (!fs4.existsSync(target) || !fs4.statSync(target).isFile()) {
|
|
2245
2630
|
return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
|
|
2246
2631
|
}
|
|
2247
2632
|
const selected = [];
|
|
2248
2633
|
let total = 0;
|
|
2249
2634
|
try {
|
|
2250
|
-
const content =
|
|
2635
|
+
const content = fs4.readFileSync(target, "utf-8");
|
|
2251
2636
|
const lines = content.split("\n");
|
|
2252
2637
|
total = lines.length;
|
|
2253
2638
|
for (let i = startLine - 1; i < Math.min(startLine - 1 + maxLines, lines.length); i++) {
|
|
@@ -2262,7 +2647,7 @@ function makeReadFile(workspace) {
|
|
|
2262
2647
|
}
|
|
2263
2648
|
const end = startLine + selected.length - 1;
|
|
2264
2649
|
const result = {
|
|
2265
|
-
path:
|
|
2650
|
+
path: path5.relative(resolved.root, target),
|
|
2266
2651
|
start_line: startLine,
|
|
2267
2652
|
end_line: end,
|
|
2268
2653
|
total_lines: total,
|
|
@@ -2275,18 +2660,18 @@ function makeReadFile(workspace) {
|
|
|
2275
2660
|
};
|
|
2276
2661
|
}
|
|
2277
2662
|
function makeListFiles(workspace) {
|
|
2278
|
-
const root =
|
|
2663
|
+
const root = path5.resolve(workspace);
|
|
2279
2664
|
return function list_files(args) {
|
|
2280
2665
|
const dirPath = String(args.path ?? ".");
|
|
2281
2666
|
const maxResults = Math.min(Number(args.max_results ?? 100), 100);
|
|
2282
2667
|
const resolved = resolveWithinRoot(root, dirPath);
|
|
2283
2668
|
if (!resolved) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
|
|
2284
2669
|
const target = resolved.real;
|
|
2285
|
-
if (!
|
|
2670
|
+
if (!fs4.existsSync(target) || !fs4.statSync(target).isDirectory()) {
|
|
2286
2671
|
return { error: `\u4E0D\u662F\u76EE\u5F55: ${dirPath}` };
|
|
2287
2672
|
}
|
|
2288
2673
|
try {
|
|
2289
|
-
const entries =
|
|
2674
|
+
const entries = fs4.readdirSync(target, { withFileTypes: true });
|
|
2290
2675
|
const results = [];
|
|
2291
2676
|
for (const e of entries) {
|
|
2292
2677
|
if (results.length >= maxResults) break;
|
|
@@ -2295,7 +2680,7 @@ function makeListFiles(workspace) {
|
|
|
2295
2680
|
results.push(e.isDirectory() ? `${e.name}/` : e.name);
|
|
2296
2681
|
}
|
|
2297
2682
|
results.sort();
|
|
2298
|
-
return { path:
|
|
2683
|
+
return { path: path5.relative(resolved.root, target), entries: results.slice(0, maxResults) };
|
|
2299
2684
|
} catch (exc) {
|
|
2300
2685
|
return { error: `\u5217\u51FA\u6587\u4EF6\u5931\u8D25: ${String(exc)}` };
|
|
2301
2686
|
}
|
|
@@ -2504,10 +2889,10 @@ function execAsync(file, argv, opts = {}) {
|
|
|
2504
2889
|
}
|
|
2505
2890
|
|
|
2506
2891
|
// src/tools/shellRuntime.ts
|
|
2507
|
-
var
|
|
2892
|
+
var path6 = __toESM(require("path"), 1);
|
|
2508
2893
|
var WINDOWS_SUFFIXES = [".exe", ".cmd", ".bat", ".com", ""];
|
|
2509
2894
|
function pathApi(platform) {
|
|
2510
|
-
return platform === "win32" ?
|
|
2895
|
+
return platform === "win32" ? path6.win32 : path6.posix;
|
|
2511
2896
|
}
|
|
2512
2897
|
function joinPath(platform, ...parts) {
|
|
2513
2898
|
return pathApi(platform).join(...parts);
|
|
@@ -2673,8 +3058,8 @@ function decodeOutput(buf) {
|
|
|
2673
3058
|
var shellTasks = new ShellTaskManager();
|
|
2674
3059
|
|
|
2675
3060
|
// src/tools/nodeGrep.ts
|
|
2676
|
-
var
|
|
2677
|
-
var
|
|
3061
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
3062
|
+
var path7 = __toESM(require("path"), 1);
|
|
2678
3063
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2679
3064
|
".git",
|
|
2680
3065
|
"node_modules",
|
|
@@ -2699,27 +3084,27 @@ function nodeGrep(opts) {
|
|
|
2699
3084
|
} catch (err) {
|
|
2700
3085
|
throw new Error(`\u65E0\u6548\u7684\u6B63\u5219\u8868\u8FBE\u5F0F: ${opts.pattern}\uFF08${String(err)}\uFF09`);
|
|
2701
3086
|
}
|
|
2702
|
-
const target =
|
|
2703
|
-
if (!
|
|
3087
|
+
const target = path7.resolve(opts.workspace, opts.target);
|
|
3088
|
+
if (!fs5.existsSync(target)) {
|
|
2704
3089
|
throw new Error(`\u641C\u7D22\u8DEF\u5F84\u4E0D\u5B58\u5728: ${opts.target}`);
|
|
2705
3090
|
}
|
|
2706
3091
|
const matches = [];
|
|
2707
3092
|
const scanFile = (file) => {
|
|
2708
3093
|
let stat2;
|
|
2709
3094
|
try {
|
|
2710
|
-
stat2 =
|
|
3095
|
+
stat2 = fs5.statSync(file);
|
|
2711
3096
|
} catch {
|
|
2712
3097
|
return false;
|
|
2713
3098
|
}
|
|
2714
3099
|
if (!stat2.isFile() || stat2.size > maxFileBytes) return false;
|
|
2715
3100
|
let buf;
|
|
2716
3101
|
try {
|
|
2717
|
-
buf =
|
|
3102
|
+
buf = fs5.readFileSync(file);
|
|
2718
3103
|
} catch {
|
|
2719
3104
|
return false;
|
|
2720
3105
|
}
|
|
2721
3106
|
if (looksBinary(buf)) return false;
|
|
2722
|
-
const rel =
|
|
3107
|
+
const rel = path7.relative(opts.workspace, file) || path7.basename(file);
|
|
2723
3108
|
const lines = buf.toString("utf-8").split("\n");
|
|
2724
3109
|
for (let i = 0; i < lines.length; i++) {
|
|
2725
3110
|
const raw = lines[i].replace(/\r$/, "");
|
|
@@ -2730,7 +3115,7 @@ function nodeGrep(opts) {
|
|
|
2730
3115
|
}
|
|
2731
3116
|
return false;
|
|
2732
3117
|
};
|
|
2733
|
-
const stat =
|
|
3118
|
+
const stat = fs5.statSync(target);
|
|
2734
3119
|
if (stat.isFile()) {
|
|
2735
3120
|
scanFile(target);
|
|
2736
3121
|
return matches;
|
|
@@ -2740,12 +3125,12 @@ function nodeGrep(opts) {
|
|
|
2740
3125
|
const dir = stack.pop();
|
|
2741
3126
|
let entries;
|
|
2742
3127
|
try {
|
|
2743
|
-
entries =
|
|
3128
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
2744
3129
|
} catch {
|
|
2745
3130
|
continue;
|
|
2746
3131
|
}
|
|
2747
3132
|
for (const entry of entries) {
|
|
2748
|
-
const full =
|
|
3133
|
+
const full = path7.join(dir, entry.name);
|
|
2749
3134
|
if (entry.isDirectory()) {
|
|
2750
3135
|
if (!SKIP_DIRS.has(entry.name)) stack.push(full);
|
|
2751
3136
|
} else if (entry.isFile()) {
|
|
@@ -2757,22 +3142,22 @@ function nodeGrep(opts) {
|
|
|
2757
3142
|
}
|
|
2758
3143
|
|
|
2759
3144
|
// src/tools/ripgrep.ts
|
|
2760
|
-
var
|
|
2761
|
-
var
|
|
2762
|
-
var
|
|
3145
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
3146
|
+
var os2 = __toESM(require("os"), 1);
|
|
3147
|
+
var path8 = __toESM(require("path"), 1);
|
|
2763
3148
|
var import_node_module = require("module");
|
|
2764
3149
|
var cached;
|
|
2765
3150
|
function extractBundledRipgrep() {
|
|
2766
3151
|
try {
|
|
2767
|
-
const req = typeof require === "function" ? require : (0, import_node_module.createRequire)(
|
|
3152
|
+
const req = typeof require === "function" ? require : (0, import_node_module.createRequire)(path8.join(process.cwd(), "__zqagent__.js"));
|
|
2768
3153
|
const sea = req("node:sea");
|
|
2769
3154
|
if (!sea?.isSea?.() || typeof sea.getAsset !== "function") return null;
|
|
2770
3155
|
const asset = sea.getAsset("rg");
|
|
2771
3156
|
if (!asset) return null;
|
|
2772
|
-
const dir =
|
|
2773
|
-
const file =
|
|
2774
|
-
|
|
2775
|
-
|
|
3157
|
+
const dir = fs6.mkdtempSync(path8.join(os2.tmpdir(), "zqagent-rg-"));
|
|
3158
|
+
const file = path8.join(dir, process.platform === "win32" ? "rg.exe" : "rg");
|
|
3159
|
+
fs6.writeFileSync(file, Buffer.from(asset));
|
|
3160
|
+
fs6.chmodSync(file, 493);
|
|
2776
3161
|
return file;
|
|
2777
3162
|
} catch {
|
|
2778
3163
|
return null;
|
|
@@ -2781,7 +3166,7 @@ function extractBundledRipgrep() {
|
|
|
2781
3166
|
function resolveRipgrep() {
|
|
2782
3167
|
if (cached !== void 0) return cached;
|
|
2783
3168
|
const explicit = (process.env.ZQAGENT_RG ?? "").trim();
|
|
2784
|
-
if (explicit &&
|
|
3169
|
+
if (explicit && fs6.existsSync(explicit)) {
|
|
2785
3170
|
cached = explicit;
|
|
2786
3171
|
return cached;
|
|
2787
3172
|
}
|
|
@@ -2795,7 +3180,7 @@ function resolveRipgrep() {
|
|
|
2795
3180
|
env: process.env,
|
|
2796
3181
|
fileExists: (p) => {
|
|
2797
3182
|
try {
|
|
2798
|
-
return
|
|
3183
|
+
return fs6.statSync(p).isFile();
|
|
2799
3184
|
} catch {
|
|
2800
3185
|
return false;
|
|
2801
3186
|
}
|
|
@@ -2880,8 +3265,8 @@ function registerSearchTool(registry, workspace) {
|
|
|
2880
3265
|
}
|
|
2881
3266
|
|
|
2882
3267
|
// src/tools/git.ts
|
|
2883
|
-
var
|
|
2884
|
-
var
|
|
3268
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
3269
|
+
var path9 = __toESM(require("path"), 1);
|
|
2885
3270
|
async function runGit(repoDir, args, timeout = 15e3) {
|
|
2886
3271
|
const res = await execAsync("git", ["-c", "core.quotepath=false", ...args], {
|
|
2887
3272
|
cwd: repoDir,
|
|
@@ -2898,9 +3283,9 @@ async function runGit(repoDir, args, timeout = 15e3) {
|
|
|
2898
3283
|
async function resolveRepoRoot(workspace, pathArg) {
|
|
2899
3284
|
let dir = workspace;
|
|
2900
3285
|
if (pathArg) {
|
|
2901
|
-
const abs =
|
|
3286
|
+
const abs = path9.resolve(workspace, pathArg);
|
|
2902
3287
|
try {
|
|
2903
|
-
dir =
|
|
3288
|
+
dir = fs7.statSync(abs).isDirectory() ? abs : path9.dirname(abs);
|
|
2904
3289
|
} catch {
|
|
2905
3290
|
dir = abs;
|
|
2906
3291
|
}
|
|
@@ -2913,24 +3298,24 @@ function repoDirArg(workspace, raw) {
|
|
|
2913
3298
|
if (typeof raw !== "string") return void 0;
|
|
2914
3299
|
const v = raw.trim();
|
|
2915
3300
|
if (!v) return void 0;
|
|
2916
|
-
const base =
|
|
2917
|
-
const abs =
|
|
2918
|
-
const rel =
|
|
2919
|
-
if (rel.startsWith("..") ||
|
|
3301
|
+
const base = path9.resolve(workspace);
|
|
3302
|
+
const abs = path9.resolve(base, v);
|
|
3303
|
+
const rel = path9.relative(base, abs);
|
|
3304
|
+
if (rel.startsWith("..") || path9.isAbsolute(rel)) return void 0;
|
|
2920
3305
|
return abs;
|
|
2921
3306
|
}
|
|
2922
3307
|
function toPathspec(repoRoot, workspace, pathArg) {
|
|
2923
3308
|
if (!pathArg) return null;
|
|
2924
|
-
const abs =
|
|
2925
|
-
let rel =
|
|
3309
|
+
const abs = path9.resolve(workspace, pathArg);
|
|
3310
|
+
let rel = path9.relative(repoRoot, abs);
|
|
2926
3311
|
if (rel === "") return ".";
|
|
2927
|
-
if (rel.startsWith("..") ||
|
|
3312
|
+
if (rel.startsWith("..") || path9.isAbsolute(rel)) {
|
|
2928
3313
|
try {
|
|
2929
|
-
const realRepo =
|
|
2930
|
-
const realAbs =
|
|
2931
|
-
const rel2 =
|
|
3314
|
+
const realRepo = fs7.realpathSync(repoRoot);
|
|
3315
|
+
const realAbs = fs7.realpathSync(abs);
|
|
3316
|
+
const rel2 = path9.relative(realRepo, realAbs);
|
|
2932
3317
|
if (rel2 === "") return ".";
|
|
2933
|
-
if (!rel2.startsWith("..") && !
|
|
3318
|
+
if (!rel2.startsWith("..") && !path9.isAbsolute(rel2)) return rel2;
|
|
2934
3319
|
} catch {
|
|
2935
3320
|
}
|
|
2936
3321
|
return null;
|
|
@@ -3095,7 +3480,8 @@ function buildExplorerEngine(opts) {
|
|
|
3095
3480
|
model: opts.model,
|
|
3096
3481
|
instructions: EXPLORER_INSTRUCTIONS,
|
|
3097
3482
|
maxIterations: parseMaxIterations(process.env.EXPLORE_MAX_ITERATIONS, 30),
|
|
3098
|
-
hook: opts.hook
|
|
3483
|
+
hook: opts.hook,
|
|
3484
|
+
toolResultDir: opts.toolResultDir
|
|
3099
3485
|
});
|
|
3100
3486
|
}
|
|
3101
3487
|
function makeExploreTool(opts) {
|
|
@@ -3107,7 +3493,8 @@ function makeExploreTool(opts) {
|
|
|
3107
3493
|
provider: opts.provider,
|
|
3108
3494
|
model: opts.model,
|
|
3109
3495
|
readonlyRegistrars: opts.readonlyRegistrars,
|
|
3110
|
-
hook: opts.hookFactory?.()
|
|
3496
|
+
hook: opts.hookFactory?.(),
|
|
3497
|
+
toolResultDir: opts.toolResultDir
|
|
3111
3498
|
});
|
|
3112
3499
|
let report = "";
|
|
3113
3500
|
let status = "unknown";
|