@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.js
CHANGED
|
@@ -283,8 +283,11 @@ function lookupContextWindow(modelId) {
|
|
|
283
283
|
return DEFAULT_CONTEXT_WINDOW;
|
|
284
284
|
}
|
|
285
285
|
var KEEP_RECENT_FRACTION = 0.25;
|
|
286
|
-
var
|
|
287
|
-
var
|
|
286
|
+
var SOFT_THRESHOLD_RATIO = 0.75;
|
|
287
|
+
var ELIDE_KEEP_FRACTION = 0.15;
|
|
288
|
+
var SUMMARY_MAX_TOKENS = 3e3;
|
|
289
|
+
var SPAN_TOOL_RESULT_CLIP = 200;
|
|
290
|
+
var SPAN_BUDGET_CHARS = 8e4;
|
|
288
291
|
var USER_MESSAGE_CLIP = 600;
|
|
289
292
|
var USER_MESSAGES_MAX = 40;
|
|
290
293
|
var TRIM_FRACTION = 0.1;
|
|
@@ -307,9 +310,22 @@ function triggerTokens(contextWindow, thresholdPct = DEFAULT_THRESHOLD_PCT, capT
|
|
|
307
310
|
}
|
|
308
311
|
return base;
|
|
309
312
|
}
|
|
313
|
+
function softTriggerTokens(contextWindow, thresholdPct = DEFAULT_THRESHOLD_PCT, capTokens, softThresholdPct) {
|
|
314
|
+
const hard = triggerTokens(contextWindow, thresholdPct, capTokens);
|
|
315
|
+
if (softThresholdPct != null && Number.isFinite(softThresholdPct) && softThresholdPct > 0) {
|
|
316
|
+
return Math.min(
|
|
317
|
+
triggerTokens(contextWindow, softThresholdPct, capTokens),
|
|
318
|
+
Math.floor(hard * 0.95)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return Math.floor(hard * SOFT_THRESHOLD_RATIO);
|
|
322
|
+
}
|
|
310
323
|
function shouldCompact(signal, contextWindow, thresholdPct, capTokens) {
|
|
311
324
|
return signal >= triggerTokens(contextWindow, thresholdPct, capTokens);
|
|
312
325
|
}
|
|
326
|
+
function elisionKeepChars(hardTriggerTokens, fraction = ELIDE_KEEP_FRACTION) {
|
|
327
|
+
return Math.max(4e3, Math.floor(hardTriggerTokens * fraction * 4));
|
|
328
|
+
}
|
|
313
329
|
function turnStarts(messages, start) {
|
|
314
330
|
const users = [];
|
|
315
331
|
const assistants = [];
|
|
@@ -459,10 +475,11 @@ var SUMMARY_SYSTEM_PROMPT = `\u4F60\u6B63\u5728\u538B\u7F29\u4E00\u4E2A AI \u534
|
|
|
459
475
|
|
|
460
476
|
\u89C4\u5219\uFF1A
|
|
461
477
|
- \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
|
|
478
|
+
- \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
|
|
462
479
|
- \u8981\u5177\u4F53\uFF1A\u8DEF\u5F84\u3001\u540D\u79F0\u3001\u547D\u4EE4\u3001id\u2014\u2014\u800C\u4E0D\u662F\u6A21\u7CCA\u7684\u5F15\u7528\u3002
|
|
463
480
|
- \u4EC5\u8F93\u51FA\u6458\u8981\u7AE0\u8282\uFF0C\u4E0D\u9644\u52A0\u524D\u8A00\u3002`;
|
|
464
481
|
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";
|
|
465
|
-
function renderSpan(span,
|
|
482
|
+
function renderSpan(span, budgetChars2 = SPAN_BUDGET_CHARS) {
|
|
466
483
|
const lines = [];
|
|
467
484
|
for (const m of span) {
|
|
468
485
|
const role = m.role;
|
|
@@ -486,8 +503,8 @@ function renderSpan(span, budgetChars = SPAN_BUDGET_CHARS) {
|
|
|
486
503
|
if (text) lines.push(`[${role}] ${text}`);
|
|
487
504
|
}
|
|
488
505
|
let rendered = lines.join("\n");
|
|
489
|
-
if (rendered.length >
|
|
490
|
-
rendered = "(\u2026oldest turns elided\u2026)\n" + rendered.slice(-
|
|
506
|
+
if (rendered.length > budgetChars2) {
|
|
507
|
+
rendered = "(\u2026oldest turns elided\u2026)\n" + rendered.slice(-budgetChars2);
|
|
491
508
|
}
|
|
492
509
|
return rendered;
|
|
493
510
|
}
|
|
@@ -501,9 +518,17 @@ function summarizerMessages(span, priorSummary = "") {
|
|
|
501
518
|
{ role: "user", content: body }
|
|
502
519
|
];
|
|
503
520
|
}
|
|
521
|
+
function summarizerReasoningEffort() {
|
|
522
|
+
const v = process.env.COMPACTION_REASONING_EFFORT?.toLowerCase();
|
|
523
|
+
if (v === "low" || v === "medium" || v === "high") return v;
|
|
524
|
+
return "low";
|
|
525
|
+
}
|
|
504
526
|
async function summarizeSpan(provider, model, span, priorSummary = "", opts) {
|
|
505
527
|
const msgs = summarizerMessages(span, priorSummary);
|
|
506
|
-
const chunks = await provider.stream(model, msgs, [],
|
|
528
|
+
const chunks = await provider.stream(model, msgs, [], {
|
|
529
|
+
...opts,
|
|
530
|
+
maxTokens: SUMMARY_MAX_TOKENS
|
|
531
|
+
});
|
|
507
532
|
let text = "";
|
|
508
533
|
for await (const chunk of chunks) {
|
|
509
534
|
if (chunk.content) text += chunk.content;
|
|
@@ -614,6 +639,166 @@ function applyToOutbound(messages, state) {
|
|
|
614
639
|
head.push({ role: "user", content: compactedBlock(state) });
|
|
615
640
|
return [...head, ...messages.slice(boundary)];
|
|
616
641
|
}
|
|
642
|
+
var ELIDE_EXEMPT_TOOLS = /* @__PURE__ */ new Set([
|
|
643
|
+
"explore",
|
|
644
|
+
"ask_user",
|
|
645
|
+
"propose_plan",
|
|
646
|
+
"request_directory",
|
|
647
|
+
"todo_write"
|
|
648
|
+
]);
|
|
649
|
+
function elidedToolContent(m) {
|
|
650
|
+
const name = m.tool_call_name || "tool";
|
|
651
|
+
const size = m.content.length;
|
|
652
|
+
const saved = /full output saved to ([^\]]+)/.exec(m.content)?.[1]?.trim();
|
|
653
|
+
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]`;
|
|
654
|
+
}
|
|
655
|
+
function isPlaceholder(content) {
|
|
656
|
+
return content.startsWith("[old tool result elided") || content.startsWith("[superseded by a later read_file");
|
|
657
|
+
}
|
|
658
|
+
var ONE_SHOT_TOOLS = /* @__PURE__ */ new Set(["git_diff"]);
|
|
659
|
+
var ONE_SHOT_KEEP_RECENT = 1;
|
|
660
|
+
function applyOneShotElision(messages, keepRecent = ONE_SHOT_KEEP_RECENT, tools = ONE_SHOT_TOOLS) {
|
|
661
|
+
const out = [...messages];
|
|
662
|
+
let seen = 0;
|
|
663
|
+
let elided = 0;
|
|
664
|
+
let freed = 0;
|
|
665
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
666
|
+
const m = messages[i];
|
|
667
|
+
if (m.role !== "tool" || !tools.has(m.tool_call_name ?? "")) continue;
|
|
668
|
+
if (isPlaceholder(m.content)) continue;
|
|
669
|
+
seen++;
|
|
670
|
+
if (seen <= keepRecent) continue;
|
|
671
|
+
const placeholder = elidedToolContent(m);
|
|
672
|
+
freed += m.content.length - placeholder.length;
|
|
673
|
+
out[i] = { ...m, content: placeholder };
|
|
674
|
+
elided++;
|
|
675
|
+
}
|
|
676
|
+
return elided > 0 ? { messages: out, elided, freedChars: freed } : { messages, elided: 0, freedChars: 0 };
|
|
677
|
+
}
|
|
678
|
+
function applyToolResultElision(messages, keepChars, exempt = ELIDE_EXEMPT_TOOLS) {
|
|
679
|
+
let total = 0;
|
|
680
|
+
for (const m of messages) {
|
|
681
|
+
if (m.role === "tool" && !exempt.has(m.tool_call_name ?? "")) {
|
|
682
|
+
total += m.content.length;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (total <= keepChars) return { messages, elided: 0, freedChars: 0 };
|
|
686
|
+
const out = [...messages];
|
|
687
|
+
let budget = 0;
|
|
688
|
+
let elided = 0;
|
|
689
|
+
let freed = 0;
|
|
690
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
691
|
+
const m = messages[i];
|
|
692
|
+
if (m.role !== "tool") continue;
|
|
693
|
+
if (exempt.has(m.tool_call_name ?? "")) continue;
|
|
694
|
+
if (elided === 0 && budget === 0 && m.content.length > keepChars) {
|
|
695
|
+
const kept = m.content.slice(0, keepChars) + "\n\u2026(truncated)";
|
|
696
|
+
freed += m.content.length - kept.length;
|
|
697
|
+
out[i] = { ...m, content: kept };
|
|
698
|
+
budget = keepChars;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (budget + m.content.length <= keepChars) {
|
|
702
|
+
budget += m.content.length;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const placeholder = elidedToolContent(m);
|
|
706
|
+
freed += m.content.length - placeholder.length;
|
|
707
|
+
out[i] = { ...m, content: placeholder };
|
|
708
|
+
elided++;
|
|
709
|
+
}
|
|
710
|
+
return { messages: out, elided, freedChars: freed };
|
|
711
|
+
}
|
|
712
|
+
function normalizeToolPath(p) {
|
|
713
|
+
let s = p.trim().replace(/\\/g, "/");
|
|
714
|
+
while (s.startsWith("./")) s = s.slice(2);
|
|
715
|
+
return s.replace(/\/{2,}/g, "/");
|
|
716
|
+
}
|
|
717
|
+
var MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
718
|
+
"write_file",
|
|
719
|
+
"replace_in_file",
|
|
720
|
+
"apply_patch",
|
|
721
|
+
"apply_unified_diff",
|
|
722
|
+
"run_shell"
|
|
723
|
+
]);
|
|
724
|
+
function parseReadFileSpan(content) {
|
|
725
|
+
const body = content.replace(/^\[tool output truncated:[^\n]*\]\n\n/, "");
|
|
726
|
+
let path10;
|
|
727
|
+
let start;
|
|
728
|
+
let end;
|
|
729
|
+
try {
|
|
730
|
+
const obj = JSON.parse(body);
|
|
731
|
+
path10 = obj.path;
|
|
732
|
+
start = obj.start_line;
|
|
733
|
+
end = obj.end_line;
|
|
734
|
+
} catch {
|
|
735
|
+
const str = (key) => new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`).exec(body)?.[1];
|
|
736
|
+
const num = (key) => {
|
|
737
|
+
const m = new RegExp(`"${key}"\\s*:\\s*(\\d+)`).exec(body);
|
|
738
|
+
return m ? Number(m[1]) : void 0;
|
|
739
|
+
};
|
|
740
|
+
path10 = str("path");
|
|
741
|
+
start = num("start_line");
|
|
742
|
+
end = num("end_line");
|
|
743
|
+
}
|
|
744
|
+
if (typeof path10 !== "string" || !path10) return null;
|
|
745
|
+
if (typeof start !== "number" || typeof end !== "number") return null;
|
|
746
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
747
|
+
return { path: path10, start, end };
|
|
748
|
+
}
|
|
749
|
+
function addSpan(spans, start, end) {
|
|
750
|
+
spans.push([start, end]);
|
|
751
|
+
spans.sort((a, b) => a[0] - b[0]);
|
|
752
|
+
const merged = [];
|
|
753
|
+
for (const cur of spans) {
|
|
754
|
+
const last = merged[merged.length - 1];
|
|
755
|
+
if (last && cur[0] <= last[1] + 1) {
|
|
756
|
+
last[1] = Math.max(last[1], cur[1]);
|
|
757
|
+
} else {
|
|
758
|
+
merged.push([...cur]);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
spans.length = 0;
|
|
762
|
+
spans.push(...merged);
|
|
763
|
+
}
|
|
764
|
+
function coversSpans(spans, start, end) {
|
|
765
|
+
return spans.some(([a, b]) => a <= start && b >= end);
|
|
766
|
+
}
|
|
767
|
+
function applyReadFileDedup(messages) {
|
|
768
|
+
let candidates = 0;
|
|
769
|
+
for (const m of messages) {
|
|
770
|
+
if (m.role === "tool" && m.tool_call_name === "read_file") candidates++;
|
|
771
|
+
}
|
|
772
|
+
if (candidates < 2) return { messages, elided: 0 };
|
|
773
|
+
const out = [...messages];
|
|
774
|
+
const covered = /* @__PURE__ */ new Map();
|
|
775
|
+
let elided = 0;
|
|
776
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
777
|
+
const m = messages[i];
|
|
778
|
+
if (m.role === "assistant") {
|
|
779
|
+
if (m.tool_calls?.some((tc) => MUTATING_TOOLS.has(tc.function.name))) {
|
|
780
|
+
covered.clear();
|
|
781
|
+
}
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (m.role !== "tool" || m.tool_call_name !== "read_file") continue;
|
|
785
|
+
const span = parseReadFileSpan(m.content);
|
|
786
|
+
if (!span) continue;
|
|
787
|
+
const key = normalizeToolPath(span.path);
|
|
788
|
+
const seen = covered.get(key) ?? [];
|
|
789
|
+
if (coversSpans(seen, span.start, span.end)) {
|
|
790
|
+
out[i] = {
|
|
791
|
+
...m,
|
|
792
|
+
content: `[superseded by a later read_file of ${span.path} (lines ${span.start}-${span.end})]`
|
|
793
|
+
};
|
|
794
|
+
elided++;
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
addSpan(seen, span.start, span.end);
|
|
798
|
+
covered.set(key, seen);
|
|
799
|
+
}
|
|
800
|
+
return { messages: elided > 0 ? out : messages, elided };
|
|
801
|
+
}
|
|
617
802
|
var OVERFLOW_MARKERS = [
|
|
618
803
|
"context_length_exceeded",
|
|
619
804
|
"maximum context length",
|
|
@@ -629,9 +814,108 @@ function isContextOverflow(exc) {
|
|
|
629
814
|
return OVERFLOW_MARKERS.some((m) => text.includes(m));
|
|
630
815
|
}
|
|
631
816
|
|
|
817
|
+
// src/engine/toolResultBudget.ts
|
|
818
|
+
import * as fs from "fs";
|
|
819
|
+
import * as path2 from "path";
|
|
820
|
+
|
|
821
|
+
// src/paths.ts
|
|
822
|
+
import * as os from "os";
|
|
823
|
+
import * as path from "path";
|
|
824
|
+
var APP_DIR_NAME = "zqagent";
|
|
825
|
+
var IS_WINDOWS = process.platform === "win32";
|
|
826
|
+
function homeDir() {
|
|
827
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir() || os.tmpdir();
|
|
828
|
+
}
|
|
829
|
+
function stateDir() {
|
|
830
|
+
const base = process.env.XDG_STATE_HOME || (IS_WINDOWS ? process.env.LOCALAPPDATA : void 0) || path.join(homeDir(), ".local", "state");
|
|
831
|
+
return path.join(base, APP_DIR_NAME);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/engine/toolResultBudget.ts
|
|
835
|
+
var DEFAULT_TOOL_RESULT_MAX_CHARS = 24e3;
|
|
836
|
+
var HEAD_RATIO = 0.6;
|
|
837
|
+
function budgetChars() {
|
|
838
|
+
const n = Number(process.env.TOOL_RESULT_MAX_CHARS);
|
|
839
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_TOOL_RESULT_MAX_CHARS;
|
|
840
|
+
}
|
|
841
|
+
function toolResultDir() {
|
|
842
|
+
const env = process.env.TOOL_RESULT_DIR?.trim();
|
|
843
|
+
return env || path2.join(stateDir(), "tool-results");
|
|
844
|
+
}
|
|
845
|
+
function clipText(text, budget) {
|
|
846
|
+
if (text.length <= budget) return text;
|
|
847
|
+
const room = Math.max(
|
|
848
|
+
0,
|
|
849
|
+
budget - `
|
|
850
|
+
|
|
851
|
+
\u2026 [${text.length} characters omitted] \u2026
|
|
852
|
+
|
|
853
|
+
`.length
|
|
854
|
+
);
|
|
855
|
+
const headLen = Math.floor(room * HEAD_RATIO);
|
|
856
|
+
const tailLen = room - headLen;
|
|
857
|
+
const omitted = text.length - headLen - tailLen;
|
|
858
|
+
return text.slice(0, headLen) + `
|
|
859
|
+
|
|
860
|
+
\u2026 [${omitted} characters omitted] \u2026
|
|
861
|
+
|
|
862
|
+
` + text.slice(-tailLen);
|
|
863
|
+
}
|
|
864
|
+
function clipDeep(value, perField) {
|
|
865
|
+
if (typeof value === "string") {
|
|
866
|
+
return value.length <= perField ? value : clipText(value, perField);
|
|
867
|
+
}
|
|
868
|
+
if (Array.isArray(value)) {
|
|
869
|
+
return value.map((v) => clipDeep(v, perField));
|
|
870
|
+
}
|
|
871
|
+
if (value && typeof value === "object") {
|
|
872
|
+
const out = {};
|
|
873
|
+
for (const [k, v] of Object.entries(value)) out[k] = clipDeep(v, perField);
|
|
874
|
+
return out;
|
|
875
|
+
}
|
|
876
|
+
return value;
|
|
877
|
+
}
|
|
878
|
+
function saveRaw(tool, raw, dir) {
|
|
879
|
+
try {
|
|
880
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
881
|
+
const safe = tool.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40) || "tool";
|
|
882
|
+
const name = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}-${safe}.txt`;
|
|
883
|
+
const file = path2.join(dir, name);
|
|
884
|
+
fs.writeFileSync(file, raw, "utf-8");
|
|
885
|
+
return file;
|
|
886
|
+
} catch {
|
|
887
|
+
return void 0;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function applyToolResultBudget(tool, result, opts = {}) {
|
|
891
|
+
const budget = opts.maxChars ?? budgetChars();
|
|
892
|
+
const raw = typeof result === "string" ? result : JSON.stringify(result, null, 2) ?? String(result);
|
|
893
|
+
if (raw.length <= budget) {
|
|
894
|
+
return { content: raw, truncated: false };
|
|
895
|
+
}
|
|
896
|
+
let content = raw;
|
|
897
|
+
if (typeof result === "object" && result !== null) {
|
|
898
|
+
let perField = Math.floor(budget * 0.7);
|
|
899
|
+
let clipped = JSON.stringify(clipDeep(result, perField), null, 2);
|
|
900
|
+
for (let i = 0; i < 6 && clipped && clipped.length > budget && perField > 128; i++) {
|
|
901
|
+
perField = Math.floor(perField / 2);
|
|
902
|
+
clipped = JSON.stringify(clipDeep(result, perField), null, 2);
|
|
903
|
+
}
|
|
904
|
+
if (clipped && clipped.length <= budget) content = clipped;
|
|
905
|
+
}
|
|
906
|
+
if (content.length > budget) content = clipText(content, budget);
|
|
907
|
+
const savedPath = saveRaw(tool, raw, opts.dir ?? toolResultDir());
|
|
908
|
+
if (savedPath) {
|
|
909
|
+
content = `[tool output truncated: ${raw.length} chars, full output saved to ${savedPath}]
|
|
910
|
+
|
|
911
|
+
` + content;
|
|
912
|
+
}
|
|
913
|
+
return { content, truncated: true, savedPath };
|
|
914
|
+
}
|
|
915
|
+
|
|
632
916
|
// src/engine/TurnEngine.ts
|
|
633
917
|
import * as crypto from "crypto";
|
|
634
|
-
import * as
|
|
918
|
+
import * as fs2 from "fs";
|
|
635
919
|
import * as _path from "path";
|
|
636
920
|
|
|
637
921
|
// src/prompts/index.ts
|
|
@@ -676,13 +960,17 @@ var INTERACTIVE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
676
960
|
"propose_plan",
|
|
677
961
|
"request_directory"
|
|
678
962
|
]);
|
|
679
|
-
|
|
680
|
-
|
|
963
|
+
var MAX_COMPACT_ROUNDS = 4;
|
|
964
|
+
var OVERFLOW_TARGET_RATIO = 0.5;
|
|
965
|
+
var OVERFLOW_TRIM_FRACTION = 0.5;
|
|
966
|
+
var COMPACT_STALL_RETRY_DELTA = 20;
|
|
967
|
+
function toolResultMessage(tc, result, taskId, toolResultDir2) {
|
|
968
|
+
const budgeted = applyToolResultBudget(tc.name, result, { dir: toolResultDir2 });
|
|
681
969
|
return {
|
|
682
970
|
role: "tool",
|
|
683
971
|
tool_call_id: tc.id,
|
|
684
972
|
tool_call_name: tc.name,
|
|
685
|
-
content,
|
|
973
|
+
content: budgeted.content,
|
|
686
974
|
taskId
|
|
687
975
|
};
|
|
688
976
|
}
|
|
@@ -793,12 +1081,22 @@ var TurnEngine = class _TurnEngine {
|
|
|
793
1081
|
// 上下文压缩
|
|
794
1082
|
compactionState = null;
|
|
795
1083
|
_lastContextTokens = null;
|
|
1084
|
+
/** 压缩无进展时的消息条数快照;-1 表示无冷却 */
|
|
1085
|
+
_compactStalledAt = -1;
|
|
796
1086
|
/** 显式配置的上下文窗口大小(token 数),未设置时为 undefined(走自动检测) */
|
|
797
1087
|
_contextWindow;
|
|
798
1088
|
/** 压缩触发阈值比例(默认 0.8) */
|
|
799
1089
|
_thresholdPct;
|
|
1090
|
+
/** 软阈值比例(undefined = 取硬阈值的 75%) */
|
|
1091
|
+
_softThresholdPct;
|
|
1092
|
+
/** 是否启用软阈值淘汰(COMPACTION_ELIDE=0 可关闭) */
|
|
1093
|
+
_elisionEnabled;
|
|
1094
|
+
/** 是否启用重复读取去重(COMPACTION_DEDUP=0 可关闭) */
|
|
1095
|
+
_dedupEnabled;
|
|
800
1096
|
/** 压缩触发阈值绝对上限(null = 不限制) */
|
|
801
1097
|
_capTokens;
|
|
1098
|
+
/** 超预算工具结果的落盘目录(undefined = 使用默认状态目录) */
|
|
1099
|
+
_toolResultDir;
|
|
802
1100
|
// 产出文件追踪(本轮 Turn 中成功写入的文件)
|
|
803
1101
|
_producedFiles = [];
|
|
804
1102
|
_turnStartTime = 0;
|
|
@@ -850,7 +1148,15 @@ var TurnEngine = class _TurnEngine {
|
|
|
850
1148
|
this._hook = opts.hook;
|
|
851
1149
|
this._contextWindow = opts.contextWindow;
|
|
852
1150
|
this._thresholdPct = opts.thresholdPct ?? DEFAULT_THRESHOLD_PCT;
|
|
1151
|
+
this._softThresholdPct = opts.softThresholdPct;
|
|
1152
|
+
this._elisionEnabled = !/^(0|false|off|no)$/i.test(
|
|
1153
|
+
(process.env.COMPACTION_ELIDE ?? "").trim()
|
|
1154
|
+
);
|
|
1155
|
+
this._dedupEnabled = !/^(0|false|off|no)$/i.test(
|
|
1156
|
+
(process.env.COMPACTION_DEDUP ?? "").trim()
|
|
1157
|
+
);
|
|
853
1158
|
this._capTokens = opts.capTokens ?? null;
|
|
1159
|
+
this._toolResultDir = opts.toolResultDir;
|
|
854
1160
|
this._reasoningEffort = opts.reasoningEffort;
|
|
855
1161
|
if (this.messages.length === 0 || this.messages[0].role !== "system") {
|
|
856
1162
|
this.messages.unshift({ role: "system", content: this.instructions });
|
|
@@ -906,6 +1212,7 @@ var TurnEngine = class _TurnEngine {
|
|
|
906
1212
|
this._cancel = false;
|
|
907
1213
|
this._producedFiles = [];
|
|
908
1214
|
this._turnStartTime = Date.now();
|
|
1215
|
+
this._compactStalledAt = -1;
|
|
909
1216
|
this._emitHook((h) => h.onTurnStart?.(userInput));
|
|
910
1217
|
yield {
|
|
911
1218
|
type: "turn_start" /* TURN_START */,
|
|
@@ -1622,7 +1929,9 @@ var TurnEngine = class _TurnEngine {
|
|
|
1622
1929
|
for (let i = 0; i < concurrent.length; i++) {
|
|
1623
1930
|
const tc = concurrent[i];
|
|
1624
1931
|
const { result, status } = results[i];
|
|
1625
|
-
this.messages.push(
|
|
1932
|
+
this.messages.push(
|
|
1933
|
+
toolResultMessage(tc, result, this.taskId, this._toolResultDir)
|
|
1934
|
+
);
|
|
1626
1935
|
this._trackProducedFile(
|
|
1627
1936
|
tc,
|
|
1628
1937
|
result,
|
|
@@ -1685,7 +1994,9 @@ var TurnEngine = class _TurnEngine {
|
|
|
1685
1994
|
};
|
|
1686
1995
|
status = "error";
|
|
1687
1996
|
}
|
|
1688
|
-
this.messages.push(
|
|
1997
|
+
this.messages.push(
|
|
1998
|
+
toolResultMessage(tc, result, this.taskId, this._toolResultDir)
|
|
1999
|
+
);
|
|
1689
2000
|
this._trackProducedFile(
|
|
1690
2001
|
tc,
|
|
1691
2002
|
result,
|
|
@@ -1720,13 +2031,41 @@ var TurnEngine = class _TurnEngine {
|
|
|
1720
2031
|
* 过滤 notice 消息;tool 消息只保留必要字段;
|
|
1721
2032
|
* 其余消息剥离 display-only sidecar 字段(source/_display/ts/
|
|
1722
2033
|
* reasoning/usage),避免污染模型上下文。
|
|
2034
|
+
*
|
|
2035
|
+
* 达到软阈值时先淘汰旧工具结果(零 LLM 成本),再产出最终视图。
|
|
1723
2036
|
*/
|
|
1724
2037
|
_outboundMessages() {
|
|
1725
|
-
const
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
2038
|
+
const base = applyToOutbound(this.messages, this.compactionState);
|
|
2039
|
+
const deduped = this._dedupEnabled ? applyReadFileDedup(base).messages : base;
|
|
2040
|
+
let view = this._finalizeOutbound(deduped);
|
|
2041
|
+
if (!this._elisionEnabled) return view;
|
|
2042
|
+
if (estimateTokens(view) < this._softTriggerTokens()) {
|
|
2043
|
+
return view;
|
|
2044
|
+
}
|
|
2045
|
+
const oneShot = applyOneShotElision(deduped);
|
|
2046
|
+
if (oneShot.freedChars > 0) {
|
|
2047
|
+
view = this._finalizeOutbound(oneShot.messages);
|
|
2048
|
+
if (estimateTokens(view) < this._softTriggerTokens()) {
|
|
2049
|
+
return view;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
const keepChars = elisionKeepChars(
|
|
2053
|
+
triggerTokens(
|
|
2054
|
+
this._getContextWindow(),
|
|
2055
|
+
this._thresholdPct,
|
|
2056
|
+
this._capTokens
|
|
2057
|
+
)
|
|
1729
2058
|
);
|
|
2059
|
+
const elided = applyToolResultElision(
|
|
2060
|
+
oneShot.messages,
|
|
2061
|
+
keepChars
|
|
2062
|
+
);
|
|
2063
|
+
if (elided.freedChars === 0) return view;
|
|
2064
|
+
return this._finalizeOutbound(elided.messages);
|
|
2065
|
+
}
|
|
2066
|
+
/** 过滤 notice + 剥离 display-only 字段,得到最终发送视图 */
|
|
2067
|
+
_finalizeOutbound(sourceMessages) {
|
|
2068
|
+
const _STRIP = /* @__PURE__ */ new Set(["source", "_display", "ts", "reasoning", "usage"]);
|
|
1730
2069
|
return sourceMessages.filter((m) => m.role !== "notice").map((m) => {
|
|
1731
2070
|
if (m.role === "tool") {
|
|
1732
2071
|
return {
|
|
@@ -1744,6 +2083,15 @@ var TurnEngine = class _TurnEngine {
|
|
|
1744
2083
|
return stripped;
|
|
1745
2084
|
});
|
|
1746
2085
|
}
|
|
2086
|
+
/** 软阈值(token 数):达到它只淘汰旧工具结果 */
|
|
2087
|
+
_softTriggerTokens() {
|
|
2088
|
+
return softTriggerTokens(
|
|
2089
|
+
this._getContextWindow(),
|
|
2090
|
+
this._thresholdPct,
|
|
2091
|
+
this._capTokens,
|
|
2092
|
+
this._softThresholdPct
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
1747
2095
|
/** 通知消息:仅用于日志/展示,不发给模型 */
|
|
1748
2096
|
_appendNotice(kind, text) {
|
|
1749
2097
|
this.messages.push({
|
|
@@ -1785,18 +2133,36 @@ var TurnEngine = class _TurnEngine {
|
|
|
1785
2133
|
return false;
|
|
1786
2134
|
}
|
|
1787
2135
|
// ===================== 自动压缩 =====================
|
|
1788
|
-
/**
|
|
2136
|
+
/**
|
|
2137
|
+
* 当前上下文 token 用量。
|
|
2138
|
+
*
|
|
2139
|
+
* API 返回的 `usage.input` 是精确值,但反映的是「上一次请求」——工具结果
|
|
2140
|
+
* 与淘汰/压缩都会让它过时。因此同时估算当前 outbound 视图,取较大者:
|
|
2141
|
+
* 宁可早一点压缩,也不要漏判导致溢出。
|
|
2142
|
+
*/
|
|
1789
2143
|
_compactionTokens() {
|
|
1790
|
-
|
|
1791
|
-
|
|
2144
|
+
const estimated = estimateTokens(this._outboundMessages());
|
|
2145
|
+
if (this._lastContextTokens == null) {
|
|
2146
|
+
return { tokens: estimated, estimated: true };
|
|
1792
2147
|
}
|
|
1793
|
-
|
|
1794
|
-
tokens:
|
|
1795
|
-
|
|
1796
|
-
};
|
|
2148
|
+
if (estimated > this._lastContextTokens) {
|
|
2149
|
+
return { tokens: estimated, estimated: true };
|
|
2150
|
+
}
|
|
2151
|
+
return { tokens: this._lastContextTokens, estimated: false };
|
|
2152
|
+
}
|
|
2153
|
+
/** 压缩后重置精确计数:后续按估算值判断 */
|
|
2154
|
+
_resetContextTokens() {
|
|
2155
|
+
this._lastContextTokens = null;
|
|
2156
|
+
}
|
|
2157
|
+
/** 当前 outbound 视图的估算 token 数 */
|
|
2158
|
+
_estimateContextTokens() {
|
|
2159
|
+
return estimateTokens(this._outboundMessages());
|
|
1797
2160
|
}
|
|
1798
2161
|
/** 检查是否需要压缩(每轮 LLM 调用前) */
|
|
1799
2162
|
_compactionDue() {
|
|
2163
|
+
if (this._compactStalledAt >= 0 && this.messages.length < this._compactStalledAt + COMPACT_STALL_RETRY_DELTA) {
|
|
2164
|
+
return false;
|
|
2165
|
+
}
|
|
1800
2166
|
const { tokens } = this._compactionTokens();
|
|
1801
2167
|
return shouldCompact(
|
|
1802
2168
|
tokens,
|
|
@@ -1843,63 +2209,82 @@ var TurnEngine = class _TurnEngine {
|
|
|
1843
2209
|
async _compactNow(reqId, force = false) {
|
|
1844
2210
|
const contextWindow = this._getContextWindow();
|
|
1845
2211
|
const messagesBefore = this._outboundMessages().length;
|
|
1846
|
-
const
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
this._capTokens
|
|
1851
|
-
)
|
|
2212
|
+
const trigger = triggerTokens(
|
|
2213
|
+
contextWindow,
|
|
2214
|
+
this._thresholdPct,
|
|
2215
|
+
this._capTokens
|
|
1852
2216
|
);
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
2217
|
+
const target = Math.floor(
|
|
2218
|
+
trigger * (force ? OVERFLOW_TARGET_RATIO : 0.9)
|
|
2219
|
+
);
|
|
2220
|
+
const model = process.env.COMPACTION_MODEL?.trim() || this.model;
|
|
2221
|
+
const reasoningEffort = summarizerReasoningEffort();
|
|
2222
|
+
let keep = Math.floor(KEEP_RECENT_FRACTION * trigger);
|
|
2223
|
+
let method = null;
|
|
2224
|
+
let lastState = null;
|
|
2225
|
+
let progressed = false;
|
|
2226
|
+
for (let round = 0; round < MAX_COMPACT_ROUNDS; round++) {
|
|
2227
|
+
if (round > 0 && this._estimateContextTokens() <= target) break;
|
|
2228
|
+
let state = null;
|
|
2229
|
+
for (let attempt = 0; attempt < 2 && !state; attempt++) {
|
|
2230
|
+
try {
|
|
2231
|
+
const logTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
2232
|
+
state = await buildState(
|
|
2233
|
+
this.messages,
|
|
2234
|
+
this.provider,
|
|
2235
|
+
model,
|
|
2236
|
+
keep,
|
|
2237
|
+
this.compactionState,
|
|
2238
|
+
{ reqId, logTime, reasoningEffort }
|
|
2239
|
+
);
|
|
2240
|
+
} catch {
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
if (state) {
|
|
2244
|
+
this.compactionState = state;
|
|
2245
|
+
lastState = state;
|
|
2246
|
+
method = "summarized";
|
|
2247
|
+
progressed = true;
|
|
2248
|
+
} else {
|
|
2249
|
+
const trimmed = trimState(
|
|
1860
2250
|
this.messages,
|
|
1861
|
-
this.provider,
|
|
1862
|
-
model,
|
|
1863
|
-
keep,
|
|
1864
2251
|
this.compactionState,
|
|
1865
|
-
|
|
2252
|
+
force ? OVERFLOW_TRIM_FRACTION : 0.25
|
|
1866
2253
|
);
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
2254
|
+
if (!trimmed) break;
|
|
2255
|
+
this.compactionState = trimmed;
|
|
2256
|
+
lastState = trimmed;
|
|
2257
|
+
method = "trimmed";
|
|
2258
|
+
progressed = true;
|
|
1871
2259
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
2260
|
+
this._resetContextTokens();
|
|
2261
|
+
if (this._estimateContextTokens() <= trigger) break;
|
|
2262
|
+
keep = Math.max(Math.floor(trigger * 0.05), Math.floor(keep / 2));
|
|
2263
|
+
}
|
|
2264
|
+
if (!progressed || !lastState) return { notice: null, method: null };
|
|
2265
|
+
this._compactStalledAt = this._estimateContextTokens() >= trigger ? this.messages.length : -1;
|
|
2266
|
+
const messagesAfter = this._outboundMessages().length;
|
|
2267
|
+
const common = {
|
|
2268
|
+
boundaryIndex: lastState.boundaryIndex,
|
|
2269
|
+
messagesBefore,
|
|
2270
|
+
messagesAfter
|
|
2271
|
+
};
|
|
2272
|
+
if (method === "summarized") {
|
|
1876
2273
|
return {
|
|
1877
2274
|
notice: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29 \u2014 \u65E9\u671F\u8F6E\u6B21\u5DF2\u6458\u8981",
|
|
1878
2275
|
method: "summarized",
|
|
1879
|
-
modelUsed:
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
messagesBefore,
|
|
1885
|
-
messagesAfter: this._outboundMessages().length
|
|
2276
|
+
modelUsed: lastState.modelUsed,
|
|
2277
|
+
summaryPreview: lastState.summaryText.slice(0, 200),
|
|
2278
|
+
keptUserMessages: lastState.userMessages.length,
|
|
2279
|
+
droppedUserMessages: lastState.userMessagesDropped,
|
|
2280
|
+
...common
|
|
1886
2281
|
};
|
|
1887
2282
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
return {
|
|
1894
|
-
notice: "\u4E0A\u4E0B\u6587\u5DF2\u88C1\u526A \u2014 \u6700\u65E7\u7684\u8F6E\u6B21\u5DF2\u4E22\u5F03\uFF08\u6458\u8981\u4E0D\u53EF\u7528\uFF09",
|
|
1895
|
-
method: "trimmed",
|
|
1896
|
-
boundaryIndex: trimmed.boundaryIndex,
|
|
1897
|
-
messagesBefore,
|
|
1898
|
-
messagesAfter: this._outboundMessages().length
|
|
1899
|
-
};
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
return { notice: null, method: null };
|
|
2283
|
+
return {
|
|
2284
|
+
notice: "\u4E0A\u4E0B\u6587\u5DF2\u88C1\u526A \u2014 \u6700\u65E7\u7684\u8F6E\u6B21\u5DF2\u4E22\u5F03\uFF08\u6458\u8981\u4E0D\u53EF\u7528\uFF09",
|
|
2285
|
+
method: "trimmed",
|
|
2286
|
+
...common
|
|
2287
|
+
};
|
|
1903
2288
|
}
|
|
1904
2289
|
/** 追踪文件写入工具成功产生的文件 */
|
|
1905
2290
|
_trackProducedFile(tc, result, status) {
|
|
@@ -1909,9 +2294,9 @@ var TurnEngine = class _TurnEngine {
|
|
|
1909
2294
|
return;
|
|
1910
2295
|
}
|
|
1911
2296
|
if (result && typeof result === "object") {
|
|
1912
|
-
const
|
|
1913
|
-
if (
|
|
1914
|
-
this._producedFiles.push(
|
|
2297
|
+
const path10 = result.path;
|
|
2298
|
+
if (path10) {
|
|
2299
|
+
this._producedFiles.push(path10);
|
|
1915
2300
|
return;
|
|
1916
2301
|
}
|
|
1917
2302
|
const changed = result.changed_files;
|
|
@@ -1935,7 +2320,7 @@ var TurnEngine = class _TurnEngine {
|
|
|
1935
2320
|
_fileExists(f, ws) {
|
|
1936
2321
|
if (!ws && !_path.isAbsolute(f)) return true;
|
|
1937
2322
|
try {
|
|
1938
|
-
return
|
|
2323
|
+
return fs2.existsSync(_path.isAbsolute(f) ? f : _path.join(ws, f));
|
|
1939
2324
|
} catch {
|
|
1940
2325
|
return false;
|
|
1941
2326
|
}
|
|
@@ -1958,7 +2343,7 @@ var TurnEngine = class _TurnEngine {
|
|
|
1958
2343
|
if (depth > 4 || results.length >= 50) return;
|
|
1959
2344
|
let entries;
|
|
1960
2345
|
try {
|
|
1961
|
-
entries =
|
|
2346
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
1962
2347
|
} catch {
|
|
1963
2348
|
return;
|
|
1964
2349
|
}
|
|
@@ -1973,7 +2358,7 @@ var TurnEngine = class _TurnEngine {
|
|
|
1973
2358
|
const ext = dotIdx !== -1 ? e.name.slice(dotIdx).toLowerCase() : "";
|
|
1974
2359
|
if (!_TurnEngine.SCAN_SUFFIXES.has(ext)) continue;
|
|
1975
2360
|
try {
|
|
1976
|
-
const st =
|
|
2361
|
+
const st = fs2.statSync(full);
|
|
1977
2362
|
if (st.mtimeMs >= since) {
|
|
1978
2363
|
results.push(
|
|
1979
2364
|
_path.relative(_path.resolve(ws), _path.resolve(full))
|
|
@@ -2004,48 +2389,48 @@ function parseMaxIterations(value, fallback) {
|
|
|
2004
2389
|
}
|
|
2005
2390
|
|
|
2006
2391
|
// src/tools/pathSafety.ts
|
|
2007
|
-
import * as
|
|
2008
|
-
import * as
|
|
2392
|
+
import * as fs3 from "fs";
|
|
2393
|
+
import * as path3 from "path";
|
|
2009
2394
|
function isInsideRelative(rel) {
|
|
2010
|
-
return !
|
|
2395
|
+
return !path3.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path3.sep}`);
|
|
2011
2396
|
}
|
|
2012
2397
|
function isFilesystemRoot(p) {
|
|
2013
|
-
return
|
|
2398
|
+
return path3.parse(p).root === p;
|
|
2014
2399
|
}
|
|
2015
2400
|
function resolveWithinRoot(workspace, target) {
|
|
2016
|
-
const abs =
|
|
2401
|
+
const abs = path3.resolve(workspace, target);
|
|
2017
2402
|
let rootReal;
|
|
2018
2403
|
try {
|
|
2019
|
-
rootReal =
|
|
2404
|
+
rootReal = fs3.realpathSync(workspace);
|
|
2020
2405
|
} catch {
|
|
2021
|
-
const rootAbs =
|
|
2022
|
-
if (!isInsideRelative(
|
|
2406
|
+
const rootAbs = path3.resolve(workspace);
|
|
2407
|
+
if (!isInsideRelative(path3.relative(rootAbs, abs))) return null;
|
|
2023
2408
|
return { root: rootAbs, real: abs };
|
|
2024
2409
|
}
|
|
2025
2410
|
if (isFilesystemRoot(rootReal)) return null;
|
|
2026
2411
|
let real;
|
|
2027
2412
|
try {
|
|
2028
|
-
real =
|
|
2413
|
+
real = fs3.realpathSync(abs);
|
|
2029
2414
|
} catch {
|
|
2030
2415
|
let probe = abs;
|
|
2031
2416
|
const tail = [];
|
|
2032
2417
|
for (; ; ) {
|
|
2033
|
-
const exists =
|
|
2418
|
+
const exists = fs3.existsSync(probe) || fs3.lstatSync(probe, { throwIfNoEntry: false }) !== void 0;
|
|
2034
2419
|
if (exists) {
|
|
2035
2420
|
try {
|
|
2036
|
-
real =
|
|
2421
|
+
real = path3.join(fs3.realpathSync(probe), ...tail);
|
|
2037
2422
|
break;
|
|
2038
2423
|
} catch {
|
|
2039
2424
|
return null;
|
|
2040
2425
|
}
|
|
2041
2426
|
}
|
|
2042
|
-
tail.unshift(
|
|
2043
|
-
const parent =
|
|
2427
|
+
tail.unshift(path3.basename(probe));
|
|
2428
|
+
const parent = path3.dirname(probe);
|
|
2044
2429
|
if (parent === probe) return null;
|
|
2045
2430
|
probe = parent;
|
|
2046
2431
|
}
|
|
2047
2432
|
}
|
|
2048
|
-
if (!isInsideRelative(
|
|
2433
|
+
if (!isInsideRelative(path3.relative(rootReal, real))) return null;
|
|
2049
2434
|
return { root: rootReal, real };
|
|
2050
2435
|
}
|
|
2051
2436
|
function isWithinRoot(workspace, target) {
|
|
@@ -2053,7 +2438,7 @@ function isWithinRoot(workspace, target) {
|
|
|
2053
2438
|
}
|
|
2054
2439
|
|
|
2055
2440
|
// src/permissions/PermissionEngine.ts
|
|
2056
|
-
import * as
|
|
2441
|
+
import * as path4 from "path";
|
|
2057
2442
|
var READ_ONLY_MODES = /* @__PURE__ */ new Set(["discuss" /* DISCUSS */, "plan" /* PLAN */]);
|
|
2058
2443
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
2059
2444
|
"write_file",
|
|
@@ -2103,7 +2488,7 @@ var PermissionEngine = class {
|
|
|
2103
2488
|
/** 会话级「总是允许此命令」集合 */
|
|
2104
2489
|
sessionAllowCommands;
|
|
2105
2490
|
constructor(opts) {
|
|
2106
|
-
this.workspaceRoot =
|
|
2491
|
+
this.workspaceRoot = path4.resolve(opts.workspaceRoot);
|
|
2107
2492
|
this.mode = opts.mode;
|
|
2108
2493
|
this.allowedCommands = Array.isArray(opts.allowedCommands) ? opts.allowedCommands : [];
|
|
2109
2494
|
this.sessionAllowTools = /* @__PURE__ */ new Set();
|
|
@@ -2195,12 +2580,12 @@ var PermissionEngine = class {
|
|
|
2195
2580
|
};
|
|
2196
2581
|
|
|
2197
2582
|
// src/tools/files.ts
|
|
2198
|
-
import * as
|
|
2199
|
-
import * as
|
|
2583
|
+
import * as fs4 from "fs";
|
|
2584
|
+
import * as path5 from "path";
|
|
2200
2585
|
var DEFAULT_MAX_LINES = 2e3;
|
|
2201
2586
|
var MAX_LINE_CHARS = 500;
|
|
2202
2587
|
function makeReadFile(workspace) {
|
|
2203
|
-
const root =
|
|
2588
|
+
const root = path5.resolve(workspace);
|
|
2204
2589
|
return function read_file(args) {
|
|
2205
2590
|
const filePath = String(args.path ?? "");
|
|
2206
2591
|
const startLine = Math.max(1, Number(args.start_line ?? 1));
|
|
@@ -2210,13 +2595,13 @@ function makeReadFile(workspace) {
|
|
|
2210
2595
|
return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
|
|
2211
2596
|
}
|
|
2212
2597
|
const target = resolved.real;
|
|
2213
|
-
if (!
|
|
2598
|
+
if (!fs4.existsSync(target) || !fs4.statSync(target).isFile()) {
|
|
2214
2599
|
return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
|
|
2215
2600
|
}
|
|
2216
2601
|
const selected = [];
|
|
2217
2602
|
let total = 0;
|
|
2218
2603
|
try {
|
|
2219
|
-
const content =
|
|
2604
|
+
const content = fs4.readFileSync(target, "utf-8");
|
|
2220
2605
|
const lines = content.split("\n");
|
|
2221
2606
|
total = lines.length;
|
|
2222
2607
|
for (let i = startLine - 1; i < Math.min(startLine - 1 + maxLines, lines.length); i++) {
|
|
@@ -2231,7 +2616,7 @@ function makeReadFile(workspace) {
|
|
|
2231
2616
|
}
|
|
2232
2617
|
const end = startLine + selected.length - 1;
|
|
2233
2618
|
const result = {
|
|
2234
|
-
path:
|
|
2619
|
+
path: path5.relative(resolved.root, target),
|
|
2235
2620
|
start_line: startLine,
|
|
2236
2621
|
end_line: end,
|
|
2237
2622
|
total_lines: total,
|
|
@@ -2244,18 +2629,18 @@ function makeReadFile(workspace) {
|
|
|
2244
2629
|
};
|
|
2245
2630
|
}
|
|
2246
2631
|
function makeListFiles(workspace) {
|
|
2247
|
-
const root =
|
|
2632
|
+
const root = path5.resolve(workspace);
|
|
2248
2633
|
return function list_files(args) {
|
|
2249
2634
|
const dirPath = String(args.path ?? ".");
|
|
2250
2635
|
const maxResults = Math.min(Number(args.max_results ?? 100), 100);
|
|
2251
2636
|
const resolved = resolveWithinRoot(root, dirPath);
|
|
2252
2637
|
if (!resolved) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
|
|
2253
2638
|
const target = resolved.real;
|
|
2254
|
-
if (!
|
|
2639
|
+
if (!fs4.existsSync(target) || !fs4.statSync(target).isDirectory()) {
|
|
2255
2640
|
return { error: `\u4E0D\u662F\u76EE\u5F55: ${dirPath}` };
|
|
2256
2641
|
}
|
|
2257
2642
|
try {
|
|
2258
|
-
const entries =
|
|
2643
|
+
const entries = fs4.readdirSync(target, { withFileTypes: true });
|
|
2259
2644
|
const results = [];
|
|
2260
2645
|
for (const e of entries) {
|
|
2261
2646
|
if (results.length >= maxResults) break;
|
|
@@ -2264,7 +2649,7 @@ function makeListFiles(workspace) {
|
|
|
2264
2649
|
results.push(e.isDirectory() ? `${e.name}/` : e.name);
|
|
2265
2650
|
}
|
|
2266
2651
|
results.sort();
|
|
2267
|
-
return { path:
|
|
2652
|
+
return { path: path5.relative(resolved.root, target), entries: results.slice(0, maxResults) };
|
|
2268
2653
|
} catch (exc) {
|
|
2269
2654
|
return { error: `\u5217\u51FA\u6587\u4EF6\u5931\u8D25: ${String(exc)}` };
|
|
2270
2655
|
}
|
|
@@ -2473,10 +2858,10 @@ function execAsync(file, argv, opts = {}) {
|
|
|
2473
2858
|
}
|
|
2474
2859
|
|
|
2475
2860
|
// src/tools/shellRuntime.ts
|
|
2476
|
-
import * as
|
|
2861
|
+
import * as path6 from "path";
|
|
2477
2862
|
var WINDOWS_SUFFIXES = [".exe", ".cmd", ".bat", ".com", ""];
|
|
2478
2863
|
function pathApi(platform) {
|
|
2479
|
-
return platform === "win32" ?
|
|
2864
|
+
return platform === "win32" ? path6.win32 : path6.posix;
|
|
2480
2865
|
}
|
|
2481
2866
|
function joinPath(platform, ...parts) {
|
|
2482
2867
|
return pathApi(platform).join(...parts);
|
|
@@ -2642,8 +3027,8 @@ function decodeOutput(buf) {
|
|
|
2642
3027
|
var shellTasks = new ShellTaskManager();
|
|
2643
3028
|
|
|
2644
3029
|
// src/tools/nodeGrep.ts
|
|
2645
|
-
import * as
|
|
2646
|
-
import * as
|
|
3030
|
+
import * as fs5 from "fs";
|
|
3031
|
+
import * as path7 from "path";
|
|
2647
3032
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2648
3033
|
".git",
|
|
2649
3034
|
"node_modules",
|
|
@@ -2668,27 +3053,27 @@ function nodeGrep(opts) {
|
|
|
2668
3053
|
} catch (err) {
|
|
2669
3054
|
throw new Error(`\u65E0\u6548\u7684\u6B63\u5219\u8868\u8FBE\u5F0F: ${opts.pattern}\uFF08${String(err)}\uFF09`);
|
|
2670
3055
|
}
|
|
2671
|
-
const target =
|
|
2672
|
-
if (!
|
|
3056
|
+
const target = path7.resolve(opts.workspace, opts.target);
|
|
3057
|
+
if (!fs5.existsSync(target)) {
|
|
2673
3058
|
throw new Error(`\u641C\u7D22\u8DEF\u5F84\u4E0D\u5B58\u5728: ${opts.target}`);
|
|
2674
3059
|
}
|
|
2675
3060
|
const matches = [];
|
|
2676
3061
|
const scanFile = (file) => {
|
|
2677
3062
|
let stat2;
|
|
2678
3063
|
try {
|
|
2679
|
-
stat2 =
|
|
3064
|
+
stat2 = fs5.statSync(file);
|
|
2680
3065
|
} catch {
|
|
2681
3066
|
return false;
|
|
2682
3067
|
}
|
|
2683
3068
|
if (!stat2.isFile() || stat2.size > maxFileBytes) return false;
|
|
2684
3069
|
let buf;
|
|
2685
3070
|
try {
|
|
2686
|
-
buf =
|
|
3071
|
+
buf = fs5.readFileSync(file);
|
|
2687
3072
|
} catch {
|
|
2688
3073
|
return false;
|
|
2689
3074
|
}
|
|
2690
3075
|
if (looksBinary(buf)) return false;
|
|
2691
|
-
const rel =
|
|
3076
|
+
const rel = path7.relative(opts.workspace, file) || path7.basename(file);
|
|
2692
3077
|
const lines = buf.toString("utf-8").split("\n");
|
|
2693
3078
|
for (let i = 0; i < lines.length; i++) {
|
|
2694
3079
|
const raw = lines[i].replace(/\r$/, "");
|
|
@@ -2699,7 +3084,7 @@ function nodeGrep(opts) {
|
|
|
2699
3084
|
}
|
|
2700
3085
|
return false;
|
|
2701
3086
|
};
|
|
2702
|
-
const stat =
|
|
3087
|
+
const stat = fs5.statSync(target);
|
|
2703
3088
|
if (stat.isFile()) {
|
|
2704
3089
|
scanFile(target);
|
|
2705
3090
|
return matches;
|
|
@@ -2709,12 +3094,12 @@ function nodeGrep(opts) {
|
|
|
2709
3094
|
const dir = stack.pop();
|
|
2710
3095
|
let entries;
|
|
2711
3096
|
try {
|
|
2712
|
-
entries =
|
|
3097
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
2713
3098
|
} catch {
|
|
2714
3099
|
continue;
|
|
2715
3100
|
}
|
|
2716
3101
|
for (const entry of entries) {
|
|
2717
|
-
const full =
|
|
3102
|
+
const full = path7.join(dir, entry.name);
|
|
2718
3103
|
if (entry.isDirectory()) {
|
|
2719
3104
|
if (!SKIP_DIRS.has(entry.name)) stack.push(full);
|
|
2720
3105
|
} else if (entry.isFile()) {
|
|
@@ -2726,22 +3111,22 @@ function nodeGrep(opts) {
|
|
|
2726
3111
|
}
|
|
2727
3112
|
|
|
2728
3113
|
// src/tools/ripgrep.ts
|
|
2729
|
-
import * as
|
|
2730
|
-
import * as
|
|
2731
|
-
import * as
|
|
3114
|
+
import * as fs6 from "fs";
|
|
3115
|
+
import * as os2 from "os";
|
|
3116
|
+
import * as path8 from "path";
|
|
2732
3117
|
import { createRequire } from "module";
|
|
2733
3118
|
var cached;
|
|
2734
3119
|
function extractBundledRipgrep() {
|
|
2735
3120
|
try {
|
|
2736
|
-
const req = typeof __require === "function" ? __require : createRequire(
|
|
3121
|
+
const req = typeof __require === "function" ? __require : createRequire(path8.join(process.cwd(), "__zqagent__.js"));
|
|
2737
3122
|
const sea = req("node:sea");
|
|
2738
3123
|
if (!sea?.isSea?.() || typeof sea.getAsset !== "function") return null;
|
|
2739
3124
|
const asset = sea.getAsset("rg");
|
|
2740
3125
|
if (!asset) return null;
|
|
2741
|
-
const dir =
|
|
2742
|
-
const file =
|
|
2743
|
-
|
|
2744
|
-
|
|
3126
|
+
const dir = fs6.mkdtempSync(path8.join(os2.tmpdir(), "zqagent-rg-"));
|
|
3127
|
+
const file = path8.join(dir, process.platform === "win32" ? "rg.exe" : "rg");
|
|
3128
|
+
fs6.writeFileSync(file, Buffer.from(asset));
|
|
3129
|
+
fs6.chmodSync(file, 493);
|
|
2745
3130
|
return file;
|
|
2746
3131
|
} catch {
|
|
2747
3132
|
return null;
|
|
@@ -2750,7 +3135,7 @@ function extractBundledRipgrep() {
|
|
|
2750
3135
|
function resolveRipgrep() {
|
|
2751
3136
|
if (cached !== void 0) return cached;
|
|
2752
3137
|
const explicit = (process.env.ZQAGENT_RG ?? "").trim();
|
|
2753
|
-
if (explicit &&
|
|
3138
|
+
if (explicit && fs6.existsSync(explicit)) {
|
|
2754
3139
|
cached = explicit;
|
|
2755
3140
|
return cached;
|
|
2756
3141
|
}
|
|
@@ -2764,7 +3149,7 @@ function resolveRipgrep() {
|
|
|
2764
3149
|
env: process.env,
|
|
2765
3150
|
fileExists: (p) => {
|
|
2766
3151
|
try {
|
|
2767
|
-
return
|
|
3152
|
+
return fs6.statSync(p).isFile();
|
|
2768
3153
|
} catch {
|
|
2769
3154
|
return false;
|
|
2770
3155
|
}
|
|
@@ -2849,8 +3234,8 @@ function registerSearchTool(registry, workspace) {
|
|
|
2849
3234
|
}
|
|
2850
3235
|
|
|
2851
3236
|
// src/tools/git.ts
|
|
2852
|
-
import * as
|
|
2853
|
-
import * as
|
|
3237
|
+
import * as fs7 from "fs";
|
|
3238
|
+
import * as path9 from "path";
|
|
2854
3239
|
async function runGit(repoDir, args, timeout = 15e3) {
|
|
2855
3240
|
const res = await execAsync("git", ["-c", "core.quotepath=false", ...args], {
|
|
2856
3241
|
cwd: repoDir,
|
|
@@ -2867,9 +3252,9 @@ async function runGit(repoDir, args, timeout = 15e3) {
|
|
|
2867
3252
|
async function resolveRepoRoot(workspace, pathArg) {
|
|
2868
3253
|
let dir = workspace;
|
|
2869
3254
|
if (pathArg) {
|
|
2870
|
-
const abs =
|
|
3255
|
+
const abs = path9.resolve(workspace, pathArg);
|
|
2871
3256
|
try {
|
|
2872
|
-
dir =
|
|
3257
|
+
dir = fs7.statSync(abs).isDirectory() ? abs : path9.dirname(abs);
|
|
2873
3258
|
} catch {
|
|
2874
3259
|
dir = abs;
|
|
2875
3260
|
}
|
|
@@ -2882,24 +3267,24 @@ function repoDirArg(workspace, raw) {
|
|
|
2882
3267
|
if (typeof raw !== "string") return void 0;
|
|
2883
3268
|
const v = raw.trim();
|
|
2884
3269
|
if (!v) return void 0;
|
|
2885
|
-
const base =
|
|
2886
|
-
const abs =
|
|
2887
|
-
const rel =
|
|
2888
|
-
if (rel.startsWith("..") ||
|
|
3270
|
+
const base = path9.resolve(workspace);
|
|
3271
|
+
const abs = path9.resolve(base, v);
|
|
3272
|
+
const rel = path9.relative(base, abs);
|
|
3273
|
+
if (rel.startsWith("..") || path9.isAbsolute(rel)) return void 0;
|
|
2889
3274
|
return abs;
|
|
2890
3275
|
}
|
|
2891
3276
|
function toPathspec(repoRoot, workspace, pathArg) {
|
|
2892
3277
|
if (!pathArg) return null;
|
|
2893
|
-
const abs =
|
|
2894
|
-
let rel =
|
|
3278
|
+
const abs = path9.resolve(workspace, pathArg);
|
|
3279
|
+
let rel = path9.relative(repoRoot, abs);
|
|
2895
3280
|
if (rel === "") return ".";
|
|
2896
|
-
if (rel.startsWith("..") ||
|
|
3281
|
+
if (rel.startsWith("..") || path9.isAbsolute(rel)) {
|
|
2897
3282
|
try {
|
|
2898
|
-
const realRepo =
|
|
2899
|
-
const realAbs =
|
|
2900
|
-
const rel2 =
|
|
3283
|
+
const realRepo = fs7.realpathSync(repoRoot);
|
|
3284
|
+
const realAbs = fs7.realpathSync(abs);
|
|
3285
|
+
const rel2 = path9.relative(realRepo, realAbs);
|
|
2901
3286
|
if (rel2 === "") return ".";
|
|
2902
|
-
if (!rel2.startsWith("..") && !
|
|
3287
|
+
if (!rel2.startsWith("..") && !path9.isAbsolute(rel2)) return rel2;
|
|
2903
3288
|
} catch {
|
|
2904
3289
|
}
|
|
2905
3290
|
return null;
|
|
@@ -3064,7 +3449,8 @@ function buildExplorerEngine(opts) {
|
|
|
3064
3449
|
model: opts.model,
|
|
3065
3450
|
instructions: EXPLORER_INSTRUCTIONS,
|
|
3066
3451
|
maxIterations: parseMaxIterations(process.env.EXPLORE_MAX_ITERATIONS, 30),
|
|
3067
|
-
hook: opts.hook
|
|
3452
|
+
hook: opts.hook,
|
|
3453
|
+
toolResultDir: opts.toolResultDir
|
|
3068
3454
|
});
|
|
3069
3455
|
}
|
|
3070
3456
|
function makeExploreTool(opts) {
|
|
@@ -3076,7 +3462,8 @@ function makeExploreTool(opts) {
|
|
|
3076
3462
|
provider: opts.provider,
|
|
3077
3463
|
model: opts.model,
|
|
3078
3464
|
readonlyRegistrars: opts.readonlyRegistrars,
|
|
3079
|
-
hook: opts.hookFactory?.()
|
|
3465
|
+
hook: opts.hookFactory?.(),
|
|
3466
|
+
toolResultDir: opts.toolResultDir
|
|
3080
3467
|
});
|
|
3081
3468
|
let report = "";
|
|
3082
3469
|
let status = "unknown";
|