@zqagent/core 0.2.5 → 0.2.7

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.
@@ -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 SPAN_TOOL_RESULT_CLIP = 400;
287
- var SPAN_BUDGET_CHARS = 4e5;
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, budgetChars = SPAN_BUDGET_CHARS) {
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 > budgetChars) {
490
- rendered = "(\u2026oldest turns elided\u2026)\n" + rendered.slice(-budgetChars);
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, [], opts);
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 fs from "fs";
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
- function toolResultMessage(tc, result, taskId) {
680
- const content = typeof result === "string" ? result : JSON.stringify(result, null, 2);
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(toolResultMessage(tc, result, this.taskId));
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(toolResultMessage(tc, result, this.taskId));
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 _STRIP = /* @__PURE__ */ new Set(["source", "_display", "ts", "reasoning", "usage"]);
1726
- const sourceMessages = applyToOutbound(
1727
- this.messages,
1728
- this.compactionState
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
- /** 当前上下文 token 用量:优先采信 API 返回的精确数量,否则用 chars/4 估算 */
2136
+ /**
2137
+ * 当前上下文 token 用量。
2138
+ *
2139
+ * API 返回的 `usage.input` 是精确值,但反映的是「上一次请求」——工具结果
2140
+ * 与淘汰/压缩都会让它过时。因此同时估算当前 outbound 视图,取较大者:
2141
+ * 宁可早一点压缩,也不要漏判导致溢出。
2142
+ */
1789
2143
  _compactionTokens() {
1790
- if (this._lastContextTokens != null) {
1791
- return { tokens: this._lastContextTokens, estimated: false };
2144
+ const estimated = estimateTokens(this._outboundMessages());
2145
+ if (this._lastContextTokens == null) {
2146
+ return { tokens: estimated, estimated: true };
1792
2147
  }
1793
- return {
1794
- tokens: estimateTokens(this._outboundMessages()),
1795
- estimated: true
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 keep = Math.floor(
1847
- KEEP_RECENT_FRACTION * triggerTokens(
1848
- contextWindow,
1849
- this._thresholdPct,
1850
- this._capTokens
1851
- )
2212
+ const trigger = triggerTokens(
2213
+ contextWindow,
2214
+ this._thresholdPct,
2215
+ this._capTokens
1852
2216
  );
1853
- const model = this.model;
1854
- let state = null;
1855
- let failed = false;
1856
- for (let attempt = 0; attempt < 2; attempt++) {
1857
- try {
1858
- const logTime = (/* @__PURE__ */ new Date()).toISOString();
1859
- state = await buildState(
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
- { reqId, logTime, reasoningEffort: this._reasoningEffort }
2252
+ force ? OVERFLOW_TRIM_FRACTION : 0.25
1866
2253
  );
1867
- failed = false;
1868
- break;
1869
- } catch {
1870
- failed = true;
2254
+ if (!trimmed) break;
2255
+ this.compactionState = trimmed;
2256
+ lastState = trimmed;
2257
+ method = "trimmed";
2258
+ progressed = true;
1871
2259
  }
1872
- }
1873
- if (state) {
1874
- this.compactionState = state;
1875
- this._lastContextTokens = null;
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: state.modelUsed,
1880
- boundaryIndex: state.boundaryIndex,
1881
- summaryPreview: state.summaryText.slice(0, 200),
1882
- keptUserMessages: state.userMessages.length,
1883
- droppedUserMessages: state.userMessagesDropped,
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
- if (failed || force) {
1889
- const trimmed = trimState(this.messages, this.compactionState);
1890
- if (trimmed) {
1891
- this.compactionState = trimmed;
1892
- this._lastContextTokens = null;
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 path8 = result.path;
1913
- if (path8) {
1914
- this._producedFiles.push(path8);
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 fs.existsSync(_path.isAbsolute(f) ? f : _path.join(ws, f));
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 = fs.readdirSync(dir, { withFileTypes: true });
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 = fs.statSync(full);
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 fs2 from "fs";
2008
- import * as path from "path";
2392
+ import * as fs3 from "fs";
2393
+ import * as path3 from "path";
2009
2394
  function isInsideRelative(rel) {
2010
- return !path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path.sep}`);
2395
+ return !path3.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path3.sep}`);
2011
2396
  }
2012
2397
  function isFilesystemRoot(p) {
2013
- return path.parse(p).root === p;
2398
+ return path3.parse(p).root === p;
2014
2399
  }
2015
2400
  function resolveWithinRoot(workspace, target) {
2016
- const abs = path.resolve(workspace, target);
2401
+ const abs = path3.resolve(workspace, target);
2017
2402
  let rootReal;
2018
2403
  try {
2019
- rootReal = fs2.realpathSync(workspace);
2404
+ rootReal = fs3.realpathSync(workspace);
2020
2405
  } catch {
2021
- const rootAbs = path.resolve(workspace);
2022
- if (!isInsideRelative(path.relative(rootAbs, abs))) return null;
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 = fs2.realpathSync(abs);
2413
+ real = fs3.realpathSync(abs);
2029
2414
  } catch {
2030
2415
  let probe = abs;
2031
2416
  const tail = [];
2032
2417
  for (; ; ) {
2033
- const exists = fs2.existsSync(probe) || fs2.lstatSync(probe, { throwIfNoEntry: false }) !== void 0;
2418
+ const exists = fs3.existsSync(probe) || fs3.lstatSync(probe, { throwIfNoEntry: false }) !== void 0;
2034
2419
  if (exists) {
2035
2420
  try {
2036
- real = path.join(fs2.realpathSync(probe), ...tail);
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(path.basename(probe));
2043
- const parent = path.dirname(probe);
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(path.relative(rootReal, real))) return null;
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 path2 from "path";
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 = path2.resolve(opts.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 fs3 from "fs";
2199
- import * as path3 from "path";
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 = path3.resolve(workspace);
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 (!fs3.existsSync(target) || !fs3.statSync(target).isFile()) {
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 = fs3.readFileSync(target, "utf-8");
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: path3.relative(resolved.root, target),
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 = path3.resolve(workspace);
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 (!fs3.existsSync(target) || !fs3.statSync(target).isDirectory()) {
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 = fs3.readdirSync(target, { withFileTypes: true });
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: path3.relative(resolved.root, target), entries: results.slice(0, maxResults) };
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 path4 from "path";
2861
+ import * as path6 from "path";
2477
2862
  var WINDOWS_SUFFIXES = [".exe", ".cmd", ".bat", ".com", ""];
2478
2863
  function pathApi(platform) {
2479
- return platform === "win32" ? path4.win32 : path4.posix;
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 fs4 from "fs";
2646
- import * as path5 from "path";
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 = path5.resolve(opts.workspace, opts.target);
2672
- if (!fs4.existsSync(target)) {
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 = fs4.statSync(file);
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 = fs4.readFileSync(file);
3071
+ buf = fs5.readFileSync(file);
2687
3072
  } catch {
2688
3073
  return false;
2689
3074
  }
2690
3075
  if (looksBinary(buf)) return false;
2691
- const rel = path5.relative(opts.workspace, file) || path5.basename(file);
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 = fs4.statSync(target);
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 = fs4.readdirSync(dir, { withFileTypes: true });
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 = path5.join(dir, entry.name);
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 fs5 from "fs";
2730
- import * as os from "os";
2731
- import * as path6 from "path";
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(path6.join(process.cwd(), "__zqagent__.js"));
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 = fs5.mkdtempSync(path6.join(os.tmpdir(), "zqagent-rg-"));
2742
- const file = path6.join(dir, process.platform === "win32" ? "rg.exe" : "rg");
2743
- fs5.writeFileSync(file, Buffer.from(asset));
2744
- fs5.chmodSync(file, 493);
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 && fs5.existsSync(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 fs5.statSync(p).isFile();
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 fs6 from "fs";
2853
- import * as path7 from "path";
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 = path7.resolve(workspace, pathArg);
3255
+ const abs = path9.resolve(workspace, pathArg);
2871
3256
  try {
2872
- dir = fs6.statSync(abs).isDirectory() ? abs : path7.dirname(abs);
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 = path7.resolve(workspace);
2886
- const abs = path7.resolve(base, v);
2887
- const rel = path7.relative(base, abs);
2888
- if (rel.startsWith("..") || path7.isAbsolute(rel)) return void 0;
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 = path7.resolve(workspace, pathArg);
2894
- let rel = path7.relative(repoRoot, abs);
3278
+ const abs = path9.resolve(workspace, pathArg);
3279
+ let rel = path9.relative(repoRoot, abs);
2895
3280
  if (rel === "") return ".";
2896
- if (rel.startsWith("..") || path7.isAbsolute(rel)) {
3281
+ if (rel.startsWith("..") || path9.isAbsolute(rel)) {
2897
3282
  try {
2898
- const realRepo = fs6.realpathSync(repoRoot);
2899
- const realAbs = fs6.realpathSync(abs);
2900
- const rel2 = path7.relative(realRepo, realAbs);
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("..") && !path7.isAbsolute(rel2)) return rel2;
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";