@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.
@@ -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 SPAN_TOOL_RESULT_CLIP = 400;
318
- var SPAN_BUDGET_CHARS = 4e5;
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, budgetChars = SPAN_BUDGET_CHARS) {
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 > budgetChars) {
521
- rendered = "(\u2026oldest turns elided\u2026)\n" + rendered.slice(-budgetChars);
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, [], opts);
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 fs = __toESM(require("fs"), 1);
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
- function toolResultMessage(tc, result, taskId) {
711
- const content = typeof result === "string" ? result : JSON.stringify(result, null, 2);
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(toolResultMessage(tc, result, this.taskId));
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(toolResultMessage(tc, result, this.taskId));
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 _STRIP = /* @__PURE__ */ new Set(["source", "_display", "ts", "reasoning", "usage"]);
1757
- const sourceMessages = applyToOutbound(
1758
- this.messages,
1759
- this.compactionState
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
- /** 当前上下文 token 用量:优先采信 API 返回的精确数量,否则用 chars/4 估算 */
2167
+ /**
2168
+ * 当前上下文 token 用量。
2169
+ *
2170
+ * API 返回的 `usage.input` 是精确值,但反映的是「上一次请求」——工具结果
2171
+ * 与淘汰/压缩都会让它过时。因此同时估算当前 outbound 视图,取较大者:
2172
+ * 宁可早一点压缩,也不要漏判导致溢出。
2173
+ */
1820
2174
  _compactionTokens() {
1821
- if (this._lastContextTokens != null) {
1822
- return { tokens: this._lastContextTokens, estimated: false };
2175
+ const estimated = estimateTokens(this._outboundMessages());
2176
+ if (this._lastContextTokens == null) {
2177
+ return { tokens: estimated, estimated: true };
1823
2178
  }
1824
- return {
1825
- tokens: estimateTokens(this._outboundMessages()),
1826
- estimated: true
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 keep = Math.floor(
1878
- KEEP_RECENT_FRACTION * triggerTokens(
1879
- contextWindow,
1880
- this._thresholdPct,
1881
- this._capTokens
1882
- )
2243
+ const trigger = triggerTokens(
2244
+ contextWindow,
2245
+ this._thresholdPct,
2246
+ this._capTokens
1883
2247
  );
1884
- const model = this.model;
1885
- let state = null;
1886
- let failed = false;
1887
- for (let attempt = 0; attempt < 2; attempt++) {
1888
- try {
1889
- const logTime = (/* @__PURE__ */ new Date()).toISOString();
1890
- state = await buildState(
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
- { reqId, logTime, reasoningEffort: this._reasoningEffort }
2283
+ force ? OVERFLOW_TRIM_FRACTION : 0.25
1897
2284
  );
1898
- failed = false;
1899
- break;
1900
- } catch {
1901
- failed = true;
2285
+ if (!trimmed) break;
2286
+ this.compactionState = trimmed;
2287
+ lastState = trimmed;
2288
+ method = "trimmed";
2289
+ progressed = true;
1902
2290
  }
1903
- }
1904
- if (state) {
1905
- this.compactionState = state;
1906
- this._lastContextTokens = null;
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: state.modelUsed,
1911
- boundaryIndex: state.boundaryIndex,
1912
- summaryPreview: state.summaryText.slice(0, 200),
1913
- keptUserMessages: state.userMessages.length,
1914
- droppedUserMessages: state.userMessagesDropped,
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
- if (failed || force) {
1920
- const trimmed = trimState(this.messages, this.compactionState);
1921
- if (trimmed) {
1922
- this.compactionState = trimmed;
1923
- this._lastContextTokens = null;
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 path8 = result.path;
1944
- if (path8) {
1945
- this._producedFiles.push(path8);
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 fs.existsSync(_path.isAbsolute(f) ? f : _path.join(ws, f));
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 = fs.readdirSync(dir, { withFileTypes: true });
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 = fs.statSync(full);
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 fs2 = __toESM(require("fs"), 1);
2039
- var path = __toESM(require("path"), 1);
2423
+ var fs3 = __toESM(require("fs"), 1);
2424
+ var path3 = __toESM(require("path"), 1);
2040
2425
  function isInsideRelative(rel) {
2041
- return !path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path.sep}`);
2426
+ return !path3.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path3.sep}`);
2042
2427
  }
2043
2428
  function isFilesystemRoot(p) {
2044
- return path.parse(p).root === p;
2429
+ return path3.parse(p).root === p;
2045
2430
  }
2046
2431
  function resolveWithinRoot(workspace, target) {
2047
- const abs = path.resolve(workspace, target);
2432
+ const abs = path3.resolve(workspace, target);
2048
2433
  let rootReal;
2049
2434
  try {
2050
- rootReal = fs2.realpathSync(workspace);
2435
+ rootReal = fs3.realpathSync(workspace);
2051
2436
  } catch {
2052
- const rootAbs = path.resolve(workspace);
2053
- if (!isInsideRelative(path.relative(rootAbs, abs))) return null;
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 = fs2.realpathSync(abs);
2444
+ real = fs3.realpathSync(abs);
2060
2445
  } catch {
2061
2446
  let probe = abs;
2062
2447
  const tail = [];
2063
2448
  for (; ; ) {
2064
- const exists = fs2.existsSync(probe) || fs2.lstatSync(probe, { throwIfNoEntry: false }) !== void 0;
2449
+ const exists = fs3.existsSync(probe) || fs3.lstatSync(probe, { throwIfNoEntry: false }) !== void 0;
2065
2450
  if (exists) {
2066
2451
  try {
2067
- real = path.join(fs2.realpathSync(probe), ...tail);
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(path.basename(probe));
2074
- const parent = path.dirname(probe);
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(path.relative(rootReal, real))) return null;
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 path2 = __toESM(require("path"), 1);
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 = path2.resolve(opts.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 fs3 = __toESM(require("fs"), 1);
2230
- var path3 = __toESM(require("path"), 1);
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 = path3.resolve(workspace);
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 (!fs3.existsSync(target) || !fs3.statSync(target).isFile()) {
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 = fs3.readFileSync(target, "utf-8");
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: path3.relative(resolved.root, target),
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 = path3.resolve(workspace);
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 (!fs3.existsSync(target) || !fs3.statSync(target).isDirectory()) {
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 = fs3.readdirSync(target, { withFileTypes: true });
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: path3.relative(resolved.root, target), entries: results.slice(0, maxResults) };
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 path4 = __toESM(require("path"), 1);
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" ? path4.win32 : path4.posix;
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 fs4 = __toESM(require("fs"), 1);
2677
- var path5 = __toESM(require("path"), 1);
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 = path5.resolve(opts.workspace, opts.target);
2703
- if (!fs4.existsSync(target)) {
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 = fs4.statSync(file);
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 = fs4.readFileSync(file);
3102
+ buf = fs5.readFileSync(file);
2718
3103
  } catch {
2719
3104
  return false;
2720
3105
  }
2721
3106
  if (looksBinary(buf)) return false;
2722
- const rel = path5.relative(opts.workspace, file) || path5.basename(file);
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 = fs4.statSync(target);
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 = fs4.readdirSync(dir, { withFileTypes: true });
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 = path5.join(dir, entry.name);
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 fs5 = __toESM(require("fs"), 1);
2761
- var os = __toESM(require("os"), 1);
2762
- var path6 = __toESM(require("path"), 1);
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)(path6.join(process.cwd(), "__zqagent__.js"));
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 = fs5.mkdtempSync(path6.join(os.tmpdir(), "zqagent-rg-"));
2773
- const file = path6.join(dir, process.platform === "win32" ? "rg.exe" : "rg");
2774
- fs5.writeFileSync(file, Buffer.from(asset));
2775
- fs5.chmodSync(file, 493);
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 && fs5.existsSync(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 fs5.statSync(p).isFile();
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 fs6 = __toESM(require("fs"), 1);
2884
- var path7 = __toESM(require("path"), 1);
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 = path7.resolve(workspace, pathArg);
3286
+ const abs = path9.resolve(workspace, pathArg);
2902
3287
  try {
2903
- dir = fs6.statSync(abs).isDirectory() ? abs : path7.dirname(abs);
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 = path7.resolve(workspace);
2917
- const abs = path7.resolve(base, v);
2918
- const rel = path7.relative(base, abs);
2919
- if (rel.startsWith("..") || path7.isAbsolute(rel)) return void 0;
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 = path7.resolve(workspace, pathArg);
2925
- let rel = path7.relative(repoRoot, abs);
3309
+ const abs = path9.resolve(workspace, pathArg);
3310
+ let rel = path9.relative(repoRoot, abs);
2926
3311
  if (rel === "") return ".";
2927
- if (rel.startsWith("..") || path7.isAbsolute(rel)) {
3312
+ if (rel.startsWith("..") || path9.isAbsolute(rel)) {
2928
3313
  try {
2929
- const realRepo = fs6.realpathSync(repoRoot);
2930
- const realAbs = fs6.realpathSync(abs);
2931
- const rel2 = path7.relative(realRepo, realAbs);
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("..") && !path7.isAbsolute(rel2)) return rel2;
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";