kimiflare 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -653,7 +653,8 @@ async function logTurnDebug(ctx) {
653
653
  toolTotalReducedBytes: toolTotalReduced,
654
654
  toolSavingsPct: toolTotalRaw > 0 ? Math.round((toolTotalRaw - toolTotalReduced) / toolTotalRaw * 100) : 0,
655
655
  cacheDiagnostics,
656
- compaction: ctx.compaction
656
+ compaction: ctx.compaction,
657
+ shadowStrip: ctx.shadowStrip
657
658
  });
658
659
  }
659
660
  var LOG_VERSION;
@@ -665,6 +666,53 @@ var init_cost_debug = __esm({
665
666
  }
666
667
  });
667
668
 
669
+ // src/agent/strip-reasoning.ts
670
+ function stripHistoricalReasoning(messages, opts2 = {}) {
671
+ const keepLast = opts2.keepLast ?? DEFAULT_KEEP_LAST;
672
+ const assistantIndices = [];
673
+ for (let i = 0; i < messages.length; i++) {
674
+ if (messages[i].role === "assistant") {
675
+ assistantIndices.push(i);
676
+ }
677
+ }
678
+ const preservedSet = keepLast === 0 ? /* @__PURE__ */ new Set() : new Set(assistantIndices.slice(-keepLast));
679
+ return messages.map((m, idx) => {
680
+ if (m.role !== "assistant") return m;
681
+ if (preservedSet.has(idx)) return m;
682
+ const next = { ...m };
683
+ delete next.reasoning_content;
684
+ if (next.tool_calls && next.tool_calls.length > 0) {
685
+ if (typeof next.content === "string") {
686
+ next.content = "";
687
+ } else if (Array.isArray(next.content)) {
688
+ next.content = next.content.map(
689
+ (part) => part.type === "text" ? { ...part, text: "" } : part
690
+ );
691
+ }
692
+ return next;
693
+ }
694
+ const textLen = typeof next.content === "string" ? next.content.length : Array.isArray(next.content) ? next.content.filter((p) => p.type === "text").reduce((sum, p) => sum + p.text.length, 0) : 0;
695
+ if (textLen <= SUBSTANTIVE_TEXT_THRESHOLD) {
696
+ if (typeof next.content === "string") {
697
+ next.content = "";
698
+ } else if (Array.isArray(next.content)) {
699
+ next.content = next.content.map(
700
+ (part) => part.type === "text" ? { ...part, text: "" } : part
701
+ );
702
+ }
703
+ }
704
+ return next;
705
+ });
706
+ }
707
+ var DEFAULT_KEEP_LAST, SUBSTANTIVE_TEXT_THRESHOLD;
708
+ var init_strip_reasoning = __esm({
709
+ "src/agent/strip-reasoning.ts"() {
710
+ "use strict";
711
+ DEFAULT_KEEP_LAST = 1;
712
+ SUBSTANTIVE_TEXT_THRESHOLD = 200;
713
+ }
714
+ });
715
+
668
716
  // src/agent/loop.ts
669
717
  async function runAgentTurn(opts2) {
670
718
  const max = opts2.maxToolIterations ?? 50;
@@ -679,11 +727,44 @@ async function runAgentTurn(opts2) {
679
727
  let content = "";
680
728
  let reasoning = "";
681
729
  opts2.callbacks.onAssistantStart?.();
730
+ const stripReasoning = process.env.KIMIFLARE_STRIP_REASONING === "1";
731
+ const shadowStrip = process.env.KIMIFLARE_SHADOW_STRIP === "1";
732
+ const keepLastRaw = process.env.KIMIFLARE_REASONING_KEEP_LAST;
733
+ const keepLast = keepLastRaw ? parseInt(keepLastRaw, 10) : 1;
734
+ let apiMessages = opts2.messages;
735
+ let shadowStripMetrics;
736
+ if (stripReasoning || shadowStrip) {
737
+ const stripped = stripHistoricalReasoning(opts2.messages, {
738
+ keepLast: Number.isNaN(keepLast) ? 1 : keepLast
739
+ });
740
+ if (shadowStrip) {
741
+ const originalSections = analyzePrompt(opts2.messages);
742
+ const strippedSections = analyzePrompt(stripped);
743
+ const originalApproxTokens = originalSections.reduce(
744
+ (sum, s) => sum + s.approxTokens,
745
+ 0
746
+ );
747
+ const strippedApproxTokens = strippedSections.reduce(
748
+ (sum, s) => sum + s.approxTokens,
749
+ 0
750
+ );
751
+ shadowStripMetrics = {
752
+ originalApproxTokens,
753
+ strippedApproxTokens,
754
+ savingsPct: originalApproxTokens > 0 ? Math.round(
755
+ (originalApproxTokens - strippedApproxTokens) / originalApproxTokens * 100
756
+ ) : 0
757
+ };
758
+ }
759
+ if (stripReasoning) {
760
+ apiMessages = stripped;
761
+ }
762
+ }
682
763
  const events = runKimi({
683
764
  accountId: opts2.accountId,
684
765
  apiToken: opts2.apiToken,
685
766
  model: opts2.model,
686
- messages: opts2.messages,
767
+ messages: apiMessages,
687
768
  tools: toolDefs,
688
769
  signal: opts2.signal,
689
770
  temperature: opts2.temperature,
@@ -750,7 +831,8 @@ async function runAgentTurn(opts2) {
750
831
  messages: opts2.messages,
751
832
  previousMessages,
752
833
  toolResults,
753
- usage: lastUsage
834
+ usage: lastUsage,
835
+ shadowStrip: shadowStripMetrics
754
836
  });
755
837
  }
756
838
  return;
@@ -778,7 +860,8 @@ async function runAgentTurn(opts2) {
778
860
  messages: opts2.messages,
779
861
  previousMessages,
780
862
  toolResults,
781
- usage: lastUsage
863
+ usage: lastUsage,
864
+ shadowStrip: shadowStripMetrics
782
865
  });
783
866
  }
784
867
  }
@@ -800,6 +883,7 @@ var init_loop = __esm({
800
883
  init_registry();
801
884
  init_messages();
802
885
  init_cost_debug();
886
+ init_strip_reasoning();
803
887
  }
804
888
  });
805
889
 
@@ -872,22 +956,18 @@ function isReadOnlyBash(command) {
872
956
  return false;
873
957
  }
874
958
  }
875
- const argCheck = COMMANDS_NEEDING_ARG_CHECK[cmd];
876
- if (argCheck) {
877
- return argCheck(args);
878
- }
879
959
  return READONLY_COMMANDS.has(cmd);
880
960
  }
881
961
  function systemPromptForMode(m) {
882
962
  if (m === "plan") {
883
- return "\n\nPLAN MODE is active. The user wants you to investigate and produce a plan WITHOUT making any changes. Do not call write, edit, or mutating bash commands. You may use read-only bash commands (e.g., git log, git diff, ls, cat) along with read/glob/grep/web-fetch. At the end, present a concise plan (bullets, files to change, approach). The user will review and then exit plan mode to execute.";
963
+ return "\n\nPLAN MODE is active. The user wants you to investigate and produce a plan WITHOUT making any changes. Do not call write, edit, or mutating bash commands. You may use read-only bash commands (e.g., git log, git diff, ls, cat, grep) along with read/glob/grep/web-fetch. Scripting interpreters (node, python3, ruby, perl, awk) and build/package tools (npm, cargo, go, tsc, jest, etc.) are blocked in plan mode. At the end, present a concise plan (bullets, files to change, approach). The user will review and then exit plan mode to execute.";
884
964
  }
885
965
  if (m === "auto") {
886
966
  return "\n\nAUTO MODE is active. The user has opted into autonomous execution \u2014 every tool call will be auto-approved. Work efficiently, but do not take irreversible destructive actions (rm -rf, git push --force, dropping tables, etc.) without pausing to describe them in chat first. Prefer smaller reversible steps.";
887
967
  }
888
968
  return "";
889
969
  }
890
- var MODES, MUTATING_TOOLS, DANGEROUS_PATTERNS, GIT_READONLY_SUBCOMMANDS, READONLY_COMMANDS, COMMANDS_NEEDING_ARG_CHECK;
970
+ var MODES, MUTATING_TOOLS, DANGEROUS_PATTERNS, GIT_READONLY_SUBCOMMANDS, READONLY_COMMANDS;
891
971
  var init_mode = __esm({
892
972
  "src/mode.ts"() {
893
973
  "use strict";
@@ -958,16 +1038,8 @@ var init_mode = __esm({
958
1038
  "id",
959
1039
  "whoami",
960
1040
  "groups",
961
- // Dev tools (version/info only)
962
- "node",
963
- "npx",
964
- "python3",
965
- "ruby",
966
- "perl",
967
1041
  // Utilities
968
1042
  "jq",
969
- "yq",
970
- "awk",
971
1043
  "cut",
972
1044
  "tr",
973
1045
  "base64",
@@ -990,32 +1062,6 @@ var init_mode = __esm({
990
1062
  "ss",
991
1063
  "lsof"
992
1064
  ]);
993
- COMMANDS_NEEDING_ARG_CHECK = {
994
- find: (args) => !args.some((a) => a === "-delete" || a === "-exec"),
995
- sed: (args) => !args.some((a) => a === "-i" || a.startsWith("-i")),
996
- tar: (args) => args[0] === "-tf" || args[0] === "--list",
997
- unzip: (args) => args[0] === "-l",
998
- curl: (args) => !args.some((a) => a === "-o" || a === "-O" || a === "-d" || a === "--data" || a.startsWith("-X")),
999
- wget: (args) => !args.some((a) => a === "-O" || a === "--output-document" || a.startsWith("--post")),
1000
- npm: (args) => ["list", "view", "config"].includes(args[0] ?? "") && !(args[0] === "config" && args[1] && !args[1].startsWith("get") && args[1] !== "list"),
1001
- tsc: (args) => args.every(
1002
- (a) => ["--noEmit", "--version", "--showConfig", "--help", "-h", "--init"].includes(a)
1003
- ),
1004
- eslint: (args) => args.every(
1005
- (a) => ["--version", "--print-config", "--help", "-h"].includes(a) || !a.startsWith("-")
1006
- ),
1007
- prettier: (args) => args.every(
1008
- (a) => ["--version", "--check", "--help", "-h"].includes(a) || !a.startsWith("-")
1009
- ),
1010
- jest: (args) => args.every(
1011
- (a) => ["--version", "--listTests", "--showConfig", "--help", "-h"].includes(a) || !a.startsWith("-")
1012
- ),
1013
- vitest: (args) => args.every(
1014
- (a) => ["--version", "--help", "-h"].includes(a) || !a.startsWith("-")
1015
- ),
1016
- go: (args) => ["version", "env", "list", "mod"].includes(args[0] ?? "") && !(args[0] === "mod" && args[1] && !["graph", "download", "why", "verify"].includes(args[1])),
1017
- cargo: (args) => ["--version", "-V", "check", "test", "metadata"].includes(args[0] ?? "") && !(args[0] === "test" && args.includes("--no-run") === false)
1018
- };
1019
1065
  }
1020
1066
  });
1021
1067
 
@@ -1049,7 +1095,12 @@ How to work:
1049
1095
  - You have a 262k-token context window. Read as much of a file as needed rather than guessing.
1050
1096
  - If a request is ambiguous, ask one focused question instead of making large assumptions.
1051
1097
  - When you finish a task, stop. Do not add a closing summary.
1052
- - When creating git commits, you must include \`Co-authored-by: kimiflare <kimiflare@proton.me>\` in the commit message so kimiflare is credited as a contributor. The bash tool will also auto-append this trailer when it detects git commit-creating commands.`;
1098
+ - When creating git commits, you must include \`Co-authored-by: kimiflare <kimiflare@proton.me>\` in the commit message so kimiflare is credited as a contributor. The bash tool will also auto-append this trailer when it detects git commit-creating commands.
1099
+
1100
+ Tool output reduction:
1101
+ - Large tool outputs (grep, read, bash, web_fetch) are reduced to compact summaries by default to preserve context window.
1102
+ - When you see "[output reduced]" with an artifact ID, you can call \`expand_artifact\` with that ID to retrieve the full raw output if you need more detail.
1103
+ - You can also re-run the original tool with more targeted parameters (e.g. read with offset/limit, grep with output_mode="files") instead of expanding.`;
1053
1104
  }
1054
1105
  function buildSessionPrefix(opts2) {
1055
1106
  const now2 = opts2.now ?? /* @__PURE__ */ new Date();
@@ -1103,11 +1154,6 @@ function resolvePath(cwd, input) {
1103
1154
  }
1104
1155
  return isAbsolute(input) ? input : resolve(cwd, input);
1105
1156
  }
1106
- function truncate(s, n) {
1107
- if (s.length <= n) return s;
1108
- return s.slice(0, n) + `
1109
- ... [truncated, ${s.length - n} chars omitted]`;
1110
- }
1111
1157
  function collapsePath(input, cwd, maxLen = 40) {
1112
1158
  if (!input) return input;
1113
1159
  let abs;
@@ -1144,7 +1190,7 @@ var init_read = __esm({
1144
1190
  MAX_BYTES = 2 * 1024 * 1024;
1145
1191
  readTool = {
1146
1192
  name: "read",
1147
- description: "Read a text file from the local filesystem. Supports optional line offset/limit. Refuses files larger than 2MB. Returns contents with 1-indexed line numbers prefixed, cat -n style.",
1193
+ description: "Read a text file from the local filesystem. Supports optional line offset/limit. Refuses files larger than 2MB. Returns contents with 1-indexed line numbers prefixed, cat -n style. When reading a full file without offset/limit, the output is reduced to a compact outline (imports, exports, signatures, preview) by default; use expand_artifact to retrieve the full content or specify offset/limit for a targeted slice.",
1148
1194
  parameters: {
1149
1195
  type: "object",
1150
1196
  properties: {
@@ -1340,26 +1386,23 @@ ${stdout.trimEnd()}`);
1340
1386
  ${stderr.trimEnd()}`);
1341
1387
  if (!stdout && !stderr) parts.push("(no output)");
1342
1388
  const raw = parts.join("\n");
1343
- const reduced = truncate(raw, OUTPUT_CAP);
1344
1389
  resolve2({
1345
- content: reduced,
1390
+ content: raw,
1346
1391
  rawBytes: Buffer.byteLength(raw, "utf8"),
1347
- reducedBytes: Buffer.byteLength(reduced, "utf8")
1392
+ reducedBytes: Buffer.byteLength(raw, "utf8")
1348
1393
  });
1349
1394
  });
1350
1395
  });
1351
1396
  }
1352
- var DEFAULT_TIMEOUT, MAX_TIMEOUT, OUTPUT_CAP, bashTool;
1397
+ var DEFAULT_TIMEOUT, MAX_TIMEOUT, bashTool;
1353
1398
  var init_bash = __esm({
1354
1399
  "src/tools/bash.ts"() {
1355
1400
  "use strict";
1356
- init_paths();
1357
1401
  DEFAULT_TIMEOUT = 12e4;
1358
1402
  MAX_TIMEOUT = 6e5;
1359
- OUTPUT_CAP = 3e4;
1360
1403
  bashTool = {
1361
1404
  name: "bash",
1362
- description: "Run a shell command via `bash -lc`. Prompts the user for permission before executing. stdout and stderr are captured, combined, and capped at 30KB.",
1405
+ description: "Run a shell command via `bash -lc`. Prompts the user for permission before executing. stdout and stderr are captured and combined. Large outputs are reduced to a compact summary by default; use expand_artifact to retrieve the full log.",
1363
1406
  parameters: {
1364
1407
  type: "object",
1365
1408
  properties: {
@@ -1444,11 +1487,10 @@ async function runRipgrep(args, root, mode) {
1444
1487
  const { stdout } = await pExecFile("rg", rgArgs, { maxBuffer: 10 * 1024 * 1024 });
1445
1488
  const trimmed = stdout.trim();
1446
1489
  if (!trimmed) return { content: "(no matches)", rawBytes: 0, reducedBytes: 0 };
1447
- const reduced = truncate(trimmed, 3e4);
1448
1490
  return {
1449
- content: reduced,
1491
+ content: trimmed,
1450
1492
  rawBytes: Buffer.byteLength(trimmed, "utf8"),
1451
- reducedBytes: Buffer.byteLength(reduced, "utf8")
1493
+ reducedBytes: Buffer.byteLength(trimmed, "utf8")
1452
1494
  };
1453
1495
  } catch (e) {
1454
1496
  const err = e;
@@ -1487,11 +1529,10 @@ async function runJsFallback(args, root, mode) {
1487
1529
  }
1488
1530
  if (!out.length) return { content: "(no matches)", rawBytes: 0, reducedBytes: 0 };
1489
1531
  const raw = out.join("\n");
1490
- const reduced = truncate(raw, 3e4);
1491
1532
  return {
1492
- content: reduced,
1533
+ content: raw,
1493
1534
  rawBytes: Buffer.byteLength(raw, "utf8"),
1494
- reducedBytes: Buffer.byteLength(reduced, "utf8")
1535
+ reducedBytes: Buffer.byteLength(raw, "utf8")
1495
1536
  };
1496
1537
  }
1497
1538
  var pExecFile, cachedHasRg, grepTool;
@@ -1534,17 +1575,15 @@ var init_grep = __esm({
1534
1575
 
1535
1576
  // src/tools/web-fetch.ts
1536
1577
  import TurndownService from "turndown";
1537
- var MAX_BYTES2, MAX_OUTPUT, TIMEOUT_MS, webFetchTool;
1578
+ var MAX_BYTES2, TIMEOUT_MS, webFetchTool;
1538
1579
  var init_web_fetch = __esm({
1539
1580
  "src/tools/web-fetch.ts"() {
1540
1581
  "use strict";
1541
- init_paths();
1542
1582
  MAX_BYTES2 = 1 * 1024 * 1024;
1543
- MAX_OUTPUT = 1e5;
1544
1583
  TIMEOUT_MS = 2e4;
1545
1584
  webFetchTool = {
1546
1585
  name: "web_fetch",
1547
- description: "Fetch a URL over HTTPS and return its content. HTML pages are converted to markdown. Output is capped at ~100KB.",
1586
+ description: "Fetch a URL over HTTPS and return its content. HTML pages are converted to markdown. Large pages are reduced to a summary by default; use expand_artifact to retrieve the full content.",
1548
1587
  parameters: {
1549
1588
  type: "object",
1550
1589
  properties: {
@@ -1578,11 +1617,10 @@ ${td.turndown(bounded)}`;
1578
1617
 
1579
1618
  ${bounded}`;
1580
1619
  }
1581
- const reduced = truncate(raw, MAX_OUTPUT);
1582
1620
  return {
1583
- content: reduced,
1621
+ content: raw,
1584
1622
  rawBytes: Buffer.byteLength(raw, "utf8"),
1585
- reducedBytes: Buffer.byteLength(reduced, "utf8")
1623
+ reducedBytes: Buffer.byteLength(raw, "utf8")
1586
1624
  };
1587
1625
  } finally {
1588
1626
  clearTimeout(timer);
@@ -1674,6 +1712,423 @@ var init_tasks = __esm({
1674
1712
  }
1675
1713
  });
1676
1714
 
1715
+ // src/tools/artifact-store.ts
1716
+ var ToolArtifactStore;
1717
+ var init_artifact_store = __esm({
1718
+ "src/tools/artifact-store.ts"() {
1719
+ "use strict";
1720
+ ToolArtifactStore = class {
1721
+ artifacts = /* @__PURE__ */ new Map();
1722
+ nextId = 0;
1723
+ maxArtifacts;
1724
+ maxTotalChars;
1725
+ constructor(opts2) {
1726
+ this.maxArtifacts = opts2?.maxArtifacts ?? 500;
1727
+ this.maxTotalChars = opts2?.maxTotalChars ?? 2e6;
1728
+ }
1729
+ /** Store raw content and return a stable artifact ID. */
1730
+ store(raw) {
1731
+ const id = `art_${++this.nextId}`;
1732
+ while (this.totalChars() + raw.length > this.maxTotalChars && this.artifacts.size > 0) {
1733
+ this.evictOldest();
1734
+ }
1735
+ while (this.artifacts.size >= this.maxArtifacts && this.artifacts.size > 0) {
1736
+ this.evictOldest();
1737
+ }
1738
+ this.artifacts.set(id, raw);
1739
+ return id;
1740
+ }
1741
+ retrieve(id) {
1742
+ return this.artifacts.get(id);
1743
+ }
1744
+ has(id) {
1745
+ return this.artifacts.has(id);
1746
+ }
1747
+ clear() {
1748
+ this.artifacts.clear();
1749
+ this.nextId = 0;
1750
+ }
1751
+ size() {
1752
+ return this.artifacts.size;
1753
+ }
1754
+ totalChars() {
1755
+ let sum = 0;
1756
+ for (const raw of this.artifacts.values()) {
1757
+ sum += raw.length;
1758
+ }
1759
+ return sum;
1760
+ }
1761
+ evictOldest() {
1762
+ const first = this.artifacts.keys().next().value;
1763
+ if (first !== void 0) {
1764
+ this.artifacts.delete(first);
1765
+ }
1766
+ }
1767
+ };
1768
+ }
1769
+ });
1770
+
1771
+ // src/tools/reducer.ts
1772
+ function reduceToolOutput(toolName, raw, args, store, config = DEFAULT_REDUCER_CONFIG) {
1773
+ const rawBytes = Buffer.byteLength(raw, "utf8");
1774
+ const artifactId = store.store(raw);
1775
+ if (!config.enabled) {
1776
+ return { content: raw, rawBytes, reducedBytes: rawBytes, artifactId };
1777
+ }
1778
+ let reduced;
1779
+ let wasReduced = false;
1780
+ let hint;
1781
+ switch (toolName) {
1782
+ case "grep": {
1783
+ const r = reduceGrep(raw, args, config.grep);
1784
+ reduced = r.body;
1785
+ wasReduced = r.wasReduced;
1786
+ hint = r.hint;
1787
+ break;
1788
+ }
1789
+ case "read": {
1790
+ const r = reduceRead(raw, args, config.read);
1791
+ reduced = r.body;
1792
+ wasReduced = r.wasReduced;
1793
+ hint = r.hint;
1794
+ break;
1795
+ }
1796
+ case "bash": {
1797
+ const r = reduceBash(raw, args, config.bash);
1798
+ reduced = r.body;
1799
+ wasReduced = r.wasReduced;
1800
+ hint = r.hint;
1801
+ break;
1802
+ }
1803
+ case "web_fetch": {
1804
+ const r = reduceWebFetch(raw, args, config.webFetch);
1805
+ reduced = r.body;
1806
+ wasReduced = r.wasReduced;
1807
+ hint = r.hint;
1808
+ break;
1809
+ }
1810
+ default:
1811
+ reduced = raw;
1812
+ break;
1813
+ }
1814
+ if (!wasReduced) {
1815
+ return { content: reduced, rawBytes, reducedBytes: rawBytes, artifactId };
1816
+ }
1817
+ const footer = `[output reduced \u2014 full raw stored as artifact ${artifactId}]`;
1818
+ const content = hint ? `${reduced}
1819
+ ${footer}
1820
+ ${hint}` : `${reduced}
1821
+ ${footer}`;
1822
+ const reducedBytes = Buffer.byteLength(content, "utf8");
1823
+ return { content, rawBytes, reducedBytes, artifactId };
1824
+ }
1825
+ function parseGrepLines(raw) {
1826
+ const matches = [];
1827
+ for (const line of raw.split("\n")) {
1828
+ const trimmed = line.trim();
1829
+ if (!trimmed) continue;
1830
+ const m = trimmed.match(/^(.+?):(\d+)?:(.*)$/);
1831
+ if (m) {
1832
+ matches.push({ file: m[1], line: m[2] ? parseInt(m[2], 10) : 0, text: m[3] });
1833
+ } else {
1834
+ matches.push({ file: trimmed, line: 0, text: "" });
1835
+ }
1836
+ }
1837
+ return matches;
1838
+ }
1839
+ function reduceGrep(raw, args, cfg) {
1840
+ const isFilesMode = args.output_mode === "files";
1841
+ const matches = parseGrepLines(raw);
1842
+ if (matches.length === 0) {
1843
+ return { body: raw, wasReduced: false };
1844
+ }
1845
+ if (isFilesMode) {
1846
+ const files = [...new Set(matches.map((m) => m.file))];
1847
+ const lines2 = [`${files.length} file(s) matched:`, ...files];
1848
+ return {
1849
+ body: lines2.join("\n"),
1850
+ wasReduced: true,
1851
+ hint: 'Re-run with output_mode="content" for match details.'
1852
+ };
1853
+ }
1854
+ const byFile = /* @__PURE__ */ new Map();
1855
+ for (const m of matches) {
1856
+ const list = byFile.get(m.file) ?? [];
1857
+ list.push(m);
1858
+ byFile.set(m.file, list);
1859
+ }
1860
+ const lines = [];
1861
+ let totalShown = 0;
1862
+ const totalHits = matches.length;
1863
+ const fileCount = byFile.size;
1864
+ lines.push(`Matched ${fileCount} file(s) (${totalHits} total hits):`);
1865
+ for (const [file, hits] of byFile) {
1866
+ if (totalShown >= cfg.maxTotalLines) break;
1867
+ lines.push(` ${file}: ${hits.length} hit(s)`);
1868
+ const toShow = Math.min(hits.length, cfg.maxMatchesPerFile);
1869
+ for (let i = 0; i < toShow; i++) {
1870
+ const h = hits[i];
1871
+ const text = h.text.length > cfg.maxLineLength ? h.text.slice(0, cfg.maxLineLength) + "\u2026" : h.text;
1872
+ const prefix = h.line > 0 ? ` ${h.line}:` : " ";
1873
+ lines.push(`${prefix}${text}`);
1874
+ totalShown++;
1875
+ if (totalShown >= cfg.maxTotalLines) break;
1876
+ }
1877
+ }
1878
+ if (totalShown < totalHits) {
1879
+ lines.push(` \u2026 (${totalHits - totalShown} more hits omitted)`);
1880
+ }
1881
+ return {
1882
+ body: lines.join("\n"),
1883
+ wasReduced: totalHits > totalShown || fileCount > 1,
1884
+ hint: 'Use expand_artifact for full matches, or re-run with output_mode="files" for paths only.'
1885
+ };
1886
+ }
1887
+ function reduceRead(raw, args, cfg) {
1888
+ const hasSlice = typeof args.offset === "number" || typeof args.limit === "number";
1889
+ if (hasSlice) {
1890
+ const lines = raw.split("\n");
1891
+ if (lines.length > cfg.maxSliceLines) {
1892
+ const kept = lines.slice(0, cfg.maxSliceLines).join("\n");
1893
+ return {
1894
+ body: kept,
1895
+ wasReduced: true,
1896
+ hint: `\u2026 (${lines.length - cfg.maxSliceLines} more lines omitted)`
1897
+ };
1898
+ }
1899
+ return { body: raw, wasReduced: false };
1900
+ }
1901
+ const allLines = raw.split("\n");
1902
+ const totalLines = allLines.length;
1903
+ const cleanLines = allLines.map((l) => l.replace(/^\s*\d+\t/, ""));
1904
+ const imports = [];
1905
+ const exports = [];
1906
+ const functions = [];
1907
+ const classes = [];
1908
+ for (let i = 0; i < cleanLines.length; i++) {
1909
+ const line = cleanLines[i];
1910
+ const lineNum = i + 1;
1911
+ if (/^import\s+/.test(line)) {
1912
+ imports.push(`${lineNum}: ${line.trim()}`);
1913
+ } else if (/^(?:export\s+)?class\s+\w+/.test(line)) {
1914
+ classes.push(`${lineNum}: ${line.trim()}`);
1915
+ } else if (/^export\s+/.test(line)) {
1916
+ exports.push(`${lineNum}: ${line.trim()}`);
1917
+ } else if (/^(?:async\s+)?function\s+\w+/.test(line)) {
1918
+ functions.push(`${lineNum}: ${line.trim()}`);
1919
+ }
1920
+ }
1921
+ const parts = [];
1922
+ parts.push(`File: ${totalLines} lines total`);
1923
+ if (imports.length > 0) {
1924
+ parts.push(`
1925
+ Imports (${imports.length}):`);
1926
+ parts.push(...imports.slice(0, Math.floor(cfg.maxOutlineLines / 4)));
1927
+ }
1928
+ if (exports.length > 0) {
1929
+ parts.push(`
1930
+ Exports (${exports.length}):`);
1931
+ parts.push(...exports.slice(0, Math.floor(cfg.maxOutlineLines / 4)));
1932
+ }
1933
+ if (functions.length > 0) {
1934
+ parts.push(`
1935
+ Functions (${functions.length}):`);
1936
+ parts.push(...functions.slice(0, Math.floor(cfg.maxOutlineLines / 4)));
1937
+ }
1938
+ if (classes.length > 0) {
1939
+ parts.push(`
1940
+ Classes (${classes.length}):`);
1941
+ parts.push(...classes.slice(0, Math.floor(cfg.maxOutlineLines / 4)));
1942
+ }
1943
+ const previewCount = Math.min(cfg.maxPreviewLines, totalLines);
1944
+ parts.push(`
1945
+ Preview (lines 1\u2013${previewCount}):`);
1946
+ parts.push(...allLines.slice(0, previewCount));
1947
+ return {
1948
+ body: parts.join("\n"),
1949
+ wasReduced: true,
1950
+ hint: "Use expand_artifact for full file, or read with offset/limit for a specific slice."
1951
+ };
1952
+ }
1953
+ function reduceBash(raw, _args, cfg) {
1954
+ const lines = raw.split("\n");
1955
+ if (lines.length <= cfg.maxTotalLines) {
1956
+ return { body: raw, wasReduced: false };
1957
+ }
1958
+ let header = "";
1959
+ let bodyStart = 0;
1960
+ if (lines[0]?.startsWith("exit=") || lines[0]?.startsWith("(timed out")) {
1961
+ header = lines[0];
1962
+ bodyStart = 1;
1963
+ }
1964
+ const body = lines.slice(bodyStart);
1965
+ const isFailure = header.includes("exit=1") || raw.includes("Error:") || raw.includes("error:") || raw.includes("FAIL") || raw.includes("failed");
1966
+ const out = [header];
1967
+ if (isFailure) {
1968
+ const errorIndices = [];
1969
+ for (let i = 0; i < body.length; i++) {
1970
+ const line = body[i];
1971
+ if (/\bError\b/i.test(line) || /\berror\b/i.test(line) || /\bFAIL\b/i.test(line) || /\bfailed\b/i.test(line) || /^\s+at\s+/.test(line) || /\s+Error:\s+/.test(line)) {
1972
+ for (let j = Math.max(0, i - 2); j <= Math.min(body.length - 1, i + 2); j++) {
1973
+ if (!errorIndices.includes(j)) errorIndices.push(j);
1974
+ }
1975
+ }
1976
+ }
1977
+ errorIndices.sort((a, b) => a - b);
1978
+ const cappedError = errorIndices.slice(0, cfg.maxErrorBlockLines);
1979
+ if (cappedError.length > 0) {
1980
+ out.push("--- error block ---");
1981
+ for (const idx of cappedError) {
1982
+ out.push(body[idx]);
1983
+ }
1984
+ }
1985
+ const testNames = [];
1986
+ for (const line of body) {
1987
+ const m = line.match(/(?:✗|✕|×|FAIL)\s+(.+)/) || line.match(/failing\s*\d*\s*:?\s*(.+)/i) || line.match(/Test\s+\w+\s+failed/i);
1988
+ if (m && m[1]) {
1989
+ const name = m[1].trim().slice(0, 120);
1990
+ if (!testNames.includes(name)) testNames.push(name);
1991
+ }
1992
+ }
1993
+ if (testNames.length > 0) {
1994
+ out.push("--- failing tests ---");
1995
+ out.push(...testNames.slice(0, 10));
1996
+ }
1997
+ }
1998
+ const trailing = body.slice(-cfg.maxTrailingLines);
1999
+ out.push("--- last lines ---");
2000
+ out.push(...trailing);
2001
+ let result = out.join("\n");
2002
+ if (cfg.dedupeConsecutiveLines) {
2003
+ result = dedupeConsecutive(result);
2004
+ }
2005
+ return {
2006
+ body: result,
2007
+ wasReduced: true,
2008
+ hint: "Use expand_artifact for full output."
2009
+ };
2010
+ }
2011
+ function dedupeConsecutive(text) {
2012
+ const lines = text.split("\n");
2013
+ const out = [];
2014
+ let repeatCount = 1;
2015
+ for (let i = 0; i < lines.length; i++) {
2016
+ const line = lines[i];
2017
+ const next = lines[i + 1];
2018
+ if (next !== void 0 && next === line) {
2019
+ repeatCount++;
2020
+ continue;
2021
+ }
2022
+ if (repeatCount > 2) {
2023
+ out.push(line);
2024
+ out.push(`\u2026 (${repeatCount - 1} identical lines omitted)`);
2025
+ } else {
2026
+ for (let j = 0; j < repeatCount; j++) {
2027
+ out.push(line);
2028
+ }
2029
+ }
2030
+ repeatCount = 1;
2031
+ }
2032
+ return out.join("\n");
2033
+ }
2034
+ function reduceWebFetch(raw, args, cfg) {
2035
+ const url = typeof args.url === "string" ? args.url : "(unknown URL)";
2036
+ const titleMatch = raw.match(/^#\s+(.+)$/m);
2037
+ const title = titleMatch ? titleMatch[1].trim() : "(no title)";
2038
+ const parts = [];
2039
+ parts.push(`Title: ${title}`);
2040
+ parts.push(`URL: ${url}`);
2041
+ const headings = raw.match(/^#{1,3}\s+.+$/gm) ?? [];
2042
+ if (headings.length > 0) {
2043
+ parts.push("\nSections:");
2044
+ for (const h of headings.slice(0, 10)) {
2045
+ parts.push(` ${h}`);
2046
+ }
2047
+ }
2048
+ const bodyStart = raw.indexOf("\n\n");
2049
+ const body = bodyStart > 0 ? raw.slice(bodyStart + 2) : raw;
2050
+ const excerpt = body.slice(0, cfg.maxChars).trim();
2051
+ if (excerpt) {
2052
+ parts.push(`
2053
+ Excerpt (${excerpt.length} chars):`);
2054
+ parts.push(excerpt);
2055
+ }
2056
+ if (body.length > cfg.maxChars) {
2057
+ parts.push(`
2058
+ \u2026 (${body.length - cfg.maxChars} more chars omitted)`);
2059
+ }
2060
+ return {
2061
+ body: parts.join("\n"),
2062
+ wasReduced: true,
2063
+ hint: "Use expand_artifact for full page content."
2064
+ };
2065
+ }
2066
+ var DEFAULT_REDUCER_CONFIG;
2067
+ var init_reducer = __esm({
2068
+ "src/tools/reducer.ts"() {
2069
+ "use strict";
2070
+ DEFAULT_REDUCER_CONFIG = {
2071
+ enabled: true,
2072
+ grep: {
2073
+ maxTotalLines: 50,
2074
+ maxMatchesPerFile: 3,
2075
+ maxLineLength: 200,
2076
+ maxOutputChars: 3e3
2077
+ },
2078
+ read: {
2079
+ maxOutlineLines: 60,
2080
+ maxSliceLines: 200,
2081
+ maxPreviewLines: 30,
2082
+ maxOutputChars: 4e3
2083
+ },
2084
+ bash: {
2085
+ maxTotalLines: 40,
2086
+ maxErrorBlockLines: 20,
2087
+ maxTrailingLines: 20,
2088
+ maxOutputChars: 4e3,
2089
+ dedupeConsecutiveLines: true
2090
+ },
2091
+ webFetch: {
2092
+ maxChars: 2e3,
2093
+ maxHeadingChars: 500
2094
+ }
2095
+ };
2096
+ }
2097
+ });
2098
+
2099
+ // src/tools/expand-artifact.ts
2100
+ function makeExpandArtifactTool(store) {
2101
+ return {
2102
+ name: "expand_artifact",
2103
+ description: "Retrieve the full raw content of a previously reduced tool output by its artifact ID. Use this when the compact summary is insufficient and you need the complete original output.",
2104
+ parameters: {
2105
+ type: "object",
2106
+ properties: {
2107
+ artifact_id: {
2108
+ type: "string",
2109
+ description: "The artifact ID from a reduced tool output footer, e.g. art_42."
2110
+ }
2111
+ },
2112
+ required: ["artifact_id"],
2113
+ additionalProperties: false
2114
+ },
2115
+ needsPermission: false,
2116
+ render: (args) => ({ title: `expand ${args.artifact_id}` }),
2117
+ run: async (args) => {
2118
+ const raw = store.retrieve(args.artifact_id);
2119
+ if (!raw) {
2120
+ return `Artifact "${args.artifact_id}" not found. It may have been evicted from memory. Re-run the original tool to regenerate the output.`;
2121
+ }
2122
+ return raw;
2123
+ }
2124
+ };
2125
+ }
2126
+ var init_expand_artifact = __esm({
2127
+ "src/tools/expand-artifact.ts"() {
2128
+ "use strict";
2129
+ }
2130
+ });
2131
+
1677
2132
  // src/tools/executor.ts
1678
2133
  function normalizeToolOutput(result) {
1679
2134
  if (typeof result === "string") {
@@ -1697,6 +2152,9 @@ var init_executor = __esm({
1697
2152
  init_grep();
1698
2153
  init_web_fetch();
1699
2154
  init_tasks();
2155
+ init_artifact_store();
2156
+ init_reducer();
2157
+ init_expand_artifact();
1700
2158
  ALL_TOOLS = [
1701
2159
  readTool,
1702
2160
  writeTool,
@@ -1710,8 +2168,11 @@ var init_executor = __esm({
1710
2168
  ToolExecutor = class {
1711
2169
  sessionAllowed = /* @__PURE__ */ new Set();
1712
2170
  tools;
2171
+ artifactStore;
1713
2172
  constructor(tools = ALL_TOOLS) {
1714
2173
  this.tools = new Map(tools.map((t) => [t.name, t]));
2174
+ this.artifactStore = new ToolArtifactStore();
2175
+ this.tools.set("expand_artifact", makeExpandArtifactTool(this.artifactStore));
1715
2176
  }
1716
2177
  list() {
1717
2178
  return [...this.tools.values()];
@@ -1725,6 +2186,9 @@ var init_executor = __esm({
1725
2186
  clearSessionPermissions() {
1726
2187
  this.sessionAllowed.clear();
1727
2188
  }
2189
+ clearArtifacts() {
2190
+ this.artifactStore.clear();
2191
+ }
1728
2192
  async run(call, askPermission, ctx) {
1729
2193
  const tool = this.tools.get(call.name);
1730
2194
  if (!tool) {
@@ -1764,13 +2228,21 @@ var init_executor = __esm({
1764
2228
  try {
1765
2229
  const result = await tool.run(args, ctx);
1766
2230
  const normalized = normalizeToolOutput(result);
2231
+ const reduced = reduceToolOutput(
2232
+ call.name,
2233
+ normalized.content,
2234
+ args,
2235
+ this.artifactStore,
2236
+ DEFAULT_REDUCER_CONFIG
2237
+ );
1767
2238
  return {
1768
2239
  tool_call_id: call.id,
1769
2240
  name: call.name,
1770
- content: normalized.content,
2241
+ content: reduced.content,
1771
2242
  ok: true,
1772
- rawBytes: normalized.rawBytes,
1773
- reducedBytes: normalized.reducedBytes
2243
+ rawBytes: reduced.rawBytes,
2244
+ reducedBytes: reduced.reducedBytes,
2245
+ artifactId: reduced.artifactId
1774
2246
  };
1775
2247
  } catch (e) {
1776
2248
  const msg = `Error running ${call.name}: ${e.message ?? String(e)}`;
@@ -2921,7 +3393,7 @@ function buildRightParts(usage, contextLimit) {
2921
3393
  `in ${usage.prompt_tokens}${cached ? ` (${cached} cached)` : ""}`,
2922
3394
  `out ${usage.completion_tokens}`,
2923
3395
  `ctx ${pct}%`,
2924
- `${cost.total.toFixed(5)}`
3396
+ `$${cost.total.toFixed(5)}`
2925
3397
  ];
2926
3398
  }
2927
3399
  function shortModel(m) {
@@ -5282,6 +5754,7 @@ function App({ initialCfg, initialUpdateResult }) {
5282
5754
  sessionIdRef.current = null;
5283
5755
  sessionStateRef.current = emptySessionState();
5284
5756
  artifactStoreRef.current = new ArtifactStore();
5757
+ executorRef.current.clearArtifacts();
5285
5758
  setEvents([]);
5286
5759
  setUsage(null);
5287
5760
  setTasks([]);