@amical/aiagent-sdk 0.1.10 → 0.1.12

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.
@@ -167,9 +167,11 @@ return (
167
167
  },
168
168
  {
169
169
  name: "agent_read_file" /* FILE_READ */,
170
- description: "Read the contents of a specific file.",
170
+ description: "Read the contents of a specific file. The returned content includes line numbers (e.g. '10|const x = 1;') so you can reference exact lines when editing.",
171
171
  params: {
172
- path: "(required) The absolute or relative file path to read."
172
+ path: "(required) The absolute or relative file path to read.",
173
+ start_line: "(optional) The 1-based line number to start reading from (inclusive). Defaults to 1.",
174
+ end_line: "(optional) The 1-based line number to stop reading at (inclusive). Defaults to the last line."
173
175
  }
174
176
  },
175
177
  {
@@ -516,6 +518,44 @@ var toolParamNames = [
516
518
  "start_line",
517
519
  "end_line"
518
520
  ];
521
+
522
+ // src/shared/custom-tools.ts
523
+ function isCustomToolGroup(tool) {
524
+ return Array.isArray(tool.children) && tool.children.length > 0;
525
+ }
526
+ function collectCustomToolNames(tools) {
527
+ const names = [];
528
+ const visit = (list) => {
529
+ for (const tool of list) {
530
+ names.push(tool.name);
531
+ if (Array.isArray(tool.children) && tool.children.length > 0) {
532
+ visit(tool.children);
533
+ }
534
+ }
535
+ };
536
+ visit(tools);
537
+ return names;
538
+ }
539
+ function renderCustomToolPrompt(tool, cwd = "") {
540
+ const params = tool.params ? Object.entries(tool.params) : [];
541
+ const replaceCwd = (text) => String(text).replaceAll("${context.cwd}", cwd);
542
+ const paramLines = params.map(([name, description]) => `- ${name}: ${replaceCwd(description)}`).join("\n");
543
+ const usageLines = params.map(([name, description]) => `<${name}>${replaceCwd(description)}</${name}>`).join("\n");
544
+ return `### ${tool.name}
545
+ Description: ${tool.description}
546
+ Parameters:
547
+ ${paramLines}
548
+ - task_progress: (optional) A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)
549
+ Usage:
550
+ <${tool.name}>
551
+ ${usageLines}
552
+ <task_progress>Checklist here (optional)</task_progress>
553
+ </${tool.name}>
554
+ `;
555
+ }
556
+ function renderCustomToolsPrompt(tools, cwd = "") {
557
+ return tools.map((tool) => renderCustomToolPrompt(tool, cwd)).join("\n");
558
+ }
519
559
  var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
520
560
  "txt",
521
561
  "md",
@@ -574,7 +614,17 @@ var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
574
614
  "dockerfile",
575
615
  "makefile",
576
616
  "properties",
577
- "lock"
617
+ "lock",
618
+ "node-version",
619
+ "nvmrc",
620
+ "editorconfig",
621
+ "prettierrc",
622
+ "eslintrc",
623
+ "babelrc",
624
+ "stylelintrc",
625
+ "huskyrc",
626
+ "commitlintrc",
627
+ "lintstagedrc"
578
628
  ]);
579
629
  var AgentState = /* @__PURE__ */ ((AgentState2) => {
580
630
  AgentState2["IDLE"] = "idle";
@@ -599,9 +649,19 @@ var ClineAgent = class {
599
649
  skillDir;
600
650
  rulesDir;
601
651
  customTools;
652
+ /** 当前可用(已展开集合)的顶层工具列表,传给 PromptManager 渲染 */
653
+ activeCustomTools;
654
+ /** 所有已知工具名(含未激活 children),便于查找定义 */
655
+ customToolsByName;
602
656
  customToolNames;
603
657
  allowDefaultTools;
604
658
  customInstructions;
659
+ /** 已激活的工具集入口名 */
660
+ activatedGroups = /* @__PURE__ */ new Set();
661
+ /** activeCustomTools 变更版本号,用于 Prompt 缓存失效 */
662
+ activeToolsVersion = 0;
663
+ /** 缓存 sysPrompt 时记录的版本号 */
664
+ promptActiveToolsVersion = -1;
605
665
  sysPrompt;
606
666
  noUseTollSysPrompt;
607
667
  // 简单的事件回调系统,供宿主环境注册以监听 Agent 内部事件
@@ -621,33 +681,47 @@ var ClineAgent = class {
621
681
  this.allowDefaultTools.push("agent_attempt_completion" /* ATTEMPT */);
622
682
  }
623
683
  this.customInstructions = options.customInstructions;
684
+ this.customToolsByName = /* @__PURE__ */ new Map();
685
+ this.activeCustomTools = [...this.customTools];
686
+ this.customToolNames = new Set(collectCustomToolNames(this.customTools));
624
687
  if (this.customTools.length > 0) {
625
- const dynamicToolNames = /* @__PURE__ */ new Set();
626
688
  const customToolParams = /* @__PURE__ */ new Set();
627
- for (const customTool of this.customTools) {
628
- dynamicToolNames.add(customTool.name);
629
- if (customTool.params) {
630
- for (const key in customTool.params) {
631
- if (!Object.hasOwn(customTool.params, key)) continue;
632
- customToolParams.add(key);
689
+ const visit = (tools) => {
690
+ for (const customTool of tools) {
691
+ this.customToolsByName.set(customTool.name, customTool);
692
+ if (customTool.params) {
693
+ for (const key in customTool.params) {
694
+ if (!Object.hasOwn(customTool.params, key)) continue;
695
+ customToolParams.add(key);
696
+ }
697
+ }
698
+ if (Array.isArray(customTool.children) && customTool.children.length > 0) {
699
+ visit(customTool.children);
633
700
  }
634
701
  }
635
- }
702
+ };
703
+ visit(this.customTools);
636
704
  const defaultToolNames = Object.values(AgentDefaultTool);
637
- const overlappingTools = Array.from(dynamicToolNames).filter((name) => defaultToolNames.includes(name));
705
+ const overlappingTools = Array.from(this.customToolNames).filter((name) => defaultToolNames.includes(name));
638
706
  if (overlappingTools.length > 0) {
639
707
  throw new Error(`Custom tools and default tools have overlapping names: ${overlappingTools.join(", ")}`);
640
708
  }
641
- if (dynamicToolNames.size > 0) {
642
- setDynamicToolUseNames("customUser", Array.from(dynamicToolNames));
709
+ if (this.customToolNames.size > 0) {
710
+ setDynamicToolUseNames("customUser", Array.from(this.customToolNames));
643
711
  }
644
712
  if (customToolParams.size > 0) {
645
713
  this.allToolParamNames = Array.from(/* @__PURE__ */ new Set([...this.allToolParamNames, ...Array.from(customToolParams)]));
646
714
  }
647
715
  }
648
- this.customToolNames = new Set(this.customTools.map((t) => t.name));
649
716
  this.skillDir = this.runtime.path.join(this.runtime.env.cwd(), ".agents/skills");
650
717
  this.rulesDir = this.runtime.path.join(this.runtime.env.cwd(), ".agents/rules");
718
+ const originalOnCondense = this.memory.onCondense;
719
+ this.memory.onCondense = async () => {
720
+ this.api.clearConversationId();
721
+ if (originalOnCondense) {
722
+ await originalOnCondense();
723
+ }
724
+ };
651
725
  }
652
726
  setState(newState) {
653
727
  this.state = newState;
@@ -669,10 +743,11 @@ var ClineAgent = class {
669
743
  shell: this.runtime.env.shell(),
670
744
  skills: await this.getSkills(),
671
745
  agentRules: await this.getAgentRules(),
672
- customTools: this.customTools,
746
+ customTools: this.activeCustomTools,
673
747
  allowDefaultTools: this.allowDefaultTools,
674
748
  customInstructions: this.customInstructions
675
749
  };
750
+ this.promptActiveToolsVersion = this.activeToolsVersion;
676
751
  this.sysPrompt = await this.promptManager.getSystemPrompt(context);
677
752
  this.noUseTollSysPrompt = this.promptManager.getNoUseToolInstructions(context);
678
753
  return { sysPrompt: this.sysPrompt, noUseTollSysPrompt: this.noUseTollSysPrompt };
@@ -704,16 +779,6 @@ var ClineAgent = class {
704
779
  async getAgentRules() {
705
780
  const cwd = this.runtime.env.cwd();
706
781
  const rules = [];
707
- const agentsMdPath = this.runtime.path.join(cwd, "Agents.md");
708
- try {
709
- if (await this.runtime.fs.fileExists(agentsMdPath)) {
710
- const content = await this.runtime.fs.readFile(agentsMdPath);
711
- if (content && content.trim()) {
712
- rules.push(content.trim());
713
- }
714
- }
715
- } catch (e) {
716
- }
717
782
  try {
718
783
  if (await this.runtime.fs.fileExists(this.rulesDir)) {
719
784
  const files = await this.runtime.fs.readDir(this.rulesDir);
@@ -739,6 +804,28 @@ ${content.trim()}\`\`\``);
739
804
  }
740
805
  } catch (e) {
741
806
  }
807
+ try {
808
+ const getAgentsMdPath = async () => {
809
+ const agentsMdPathList = [
810
+ this.runtime.path.join(cwd, "AGENTS.md"),
811
+ this.runtime.path.join(cwd, "Agents.md"),
812
+ this.runtime.path.join(cwd, "agents.md")
813
+ ];
814
+ for (const item of agentsMdPathList) {
815
+ if (await this.runtime.fs.fileExists(item)) {
816
+ return item;
817
+ }
818
+ }
819
+ };
820
+ const agentsMdPath = await getAgentsMdPath();
821
+ if (agentsMdPath) {
822
+ const content = await this.runtime.fs.readFile(agentsMdPath);
823
+ if (content && content.trim()) {
824
+ rules.push(content.trim());
825
+ }
826
+ }
827
+ } catch (e) {
828
+ }
742
829
  return rules.join("\n---\n");
743
830
  }
744
831
  async getSkills() {
@@ -781,9 +868,9 @@ ${content.trim()}\`\`\``);
781
868
  * 核心任务循环
782
869
  */
783
870
  async loop() {
784
- const { sysPrompt, noUseTollSysPrompt } = await this.getPrompt(true);
785
- const { customTools, customToolNames } = this;
871
+ const { customToolsByName, customToolNames } = this;
786
872
  while (this.state === "thinking" /* THINKING */) {
873
+ const { sysPrompt, noUseTollSysPrompt } = await this.getCurrentPrompt();
787
874
  this.loopCount++;
788
875
  if (this.loopCount > this.maxLoopCount) {
789
876
  await this.memory.appendMessage({
@@ -828,7 +915,33 @@ ${content.trim()}\`\`\``);
828
915
  }
829
916
  }
830
917
  if (customToolNames.has(tool.name)) {
831
- const customTool = customTools.find((t) => t.name === tool.name);
918
+ const customTool = customToolsByName.get(tool.name);
919
+ if (!customTool) {
920
+ toolResults.push({ tool, result: `Error: Custom tool "${tool.name}" not found.` });
921
+ continue;
922
+ }
923
+ if (isCustomToolGroup(customTool)) {
924
+ let groupResult;
925
+ if (customTool.onCall) {
926
+ try {
927
+ groupResult = await customTool.onCall(tool.params);
928
+ } catch (error) {
929
+ toolResults.push({ tool, result: `Error: ${error.message}` });
930
+ continue;
931
+ }
932
+ }
933
+ const childrenPrompt = this.activateCustomToolGroup(customTool);
934
+ const finalPrompt = `# New Available Tools
935
+
936
+ The following are instructions for using the new available tools.
937
+
938
+ ${childrenPrompt}`;
939
+ const promptBlock = groupResult !== void 0 ? `${typeof groupResult === "string" ? groupResult : JSON.stringify(groupResult)}
940
+
941
+ ${finalPrompt}` : finalPrompt;
942
+ toolResults.push({ tool, result: promptBlock });
943
+ continue;
944
+ }
832
945
  if (customTool.onCall) {
833
946
  try {
834
947
  const res = await customTool.onCall(tool.params);
@@ -850,7 +963,8 @@ ${content.trim()}\`\`\``);
850
963
  if (path3) {
851
964
  try {
852
965
  const content = await this.runtime.fs.readFile(path3);
853
- toolResults.push({ tool, result: content });
966
+ const result = this.formatFileContentWithLineNumbers(content, tool.params.start_line, tool.params.end_line);
967
+ toolResults.push({ tool, result });
854
968
  } catch (error) {
855
969
  toolResults.push({ tool, result: `Error: ${error.message}` });
856
970
  }
@@ -863,6 +977,8 @@ ${content.trim()}\`\`\``);
863
977
  const result = await this.runtime.terminal.execCommand(cmd);
864
978
  toolResults.push({ tool, result: `STDOUT: ${result.stdout}
865
979
  STDERR: ${result.stderr}` });
980
+ } else {
981
+ toolResults.push({ tool, result: "Error: No command provided." });
866
982
  }
867
983
  } else if (tool.name === "agent_write_to_file" /* FILE_NEW */) {
868
984
  const path3 = tool.params.path;
@@ -874,12 +990,18 @@ STDERR: ${result.stderr}` });
874
990
  } catch (error) {
875
991
  toolResults.push({ tool, result: `Error: ${error.message}` });
876
992
  }
993
+ } else {
994
+ toolResults.push({ tool, result: "Error: Missing path or content." });
877
995
  }
878
996
  } else if (tool.name === "agent_list_files" /* LIST_FILES */) {
879
997
  const path3 = tool.params.path || this.runtime.env.cwd();
998
+ const recursive = tool.params.recursive === "true";
880
999
  try {
881
- const files = await this.runtime.fs.readDir(path3);
882
- toolResults.push({ tool, result: files.map((f) => f.name + (f.isDirectory() ? "/" : "")).join("\n") });
1000
+ const files = await this.runtime.fs.readDir(path3, { recursive });
1001
+ toolResults.push({
1002
+ tool,
1003
+ result: files.map((f) => `${f.isDirectory() ? "[DIR] " : " "}${f.name}${f.isDirectory() ? "/" : ""}`).join("\n")
1004
+ });
883
1005
  } catch (e) {
884
1006
  toolResults.push({ tool, result: `Error: ${e.message}` });
885
1007
  }
@@ -1019,10 +1141,15 @@ STDERR: ${result.stderr}` });
1019
1141
  try {
1020
1142
  const res = await fetch(url);
1021
1143
  const text = await res.text();
1022
- toolResults.push({ tool, result: text.substring(0, 3e3) });
1144
+ const MAX_LEN = 8e3;
1145
+ const truncated = text.length > MAX_LEN ? `${text.substring(0, MAX_LEN)}
1146
+ ...[truncated ${text.length - MAX_LEN} chars]` : text;
1147
+ toolResults.push({ tool, result: truncated });
1023
1148
  } catch (e) {
1024
1149
  toolResults.push({ tool, result: `Error: ${e.message}` });
1025
1150
  }
1151
+ } else {
1152
+ toolResults.push({ tool, result: "Error: No url provided." });
1026
1153
  }
1027
1154
  } else if (tool.name === "agent_focus_chain" /* TODO */) {
1028
1155
  toolResults.push({ tool, result: `Focus chain updated successfully.` });
@@ -1089,6 +1216,65 @@ ${this.promptManager.getUsedToolResultInstructions()}`;
1089
1216
  }
1090
1217
  }
1091
1218
  }
1219
+ /**
1220
+ * 激活自定义工具集:将入口的 children 注册到活跃工具列表并返回渲染后的 prompt。
1221
+ */
1222
+ activateCustomToolGroup(group) {
1223
+ if (this.activatedGroups.has(group.name)) {
1224
+ return renderCustomToolsPrompt(group.children || [], this.runtime.env.cwd());
1225
+ }
1226
+ this.activatedGroups.add(group.name);
1227
+ this.activeCustomTools = [...this.activeCustomTools, ...group.children || []];
1228
+ this.activeToolsVersion++;
1229
+ return renderCustomToolsPrompt(group.children || [], this.runtime.env.cwd());
1230
+ }
1231
+ /**
1232
+ * 检查自上次获取 prompt 后是否有工具集被激活,若有则刷新缓存。
1233
+ */
1234
+ async getCurrentPrompt() {
1235
+ if (this.promptActiveToolsVersion !== this.activeToolsVersion) {
1236
+ return this.getPrompt(true);
1237
+ }
1238
+ return this.getPrompt();
1239
+ }
1240
+ /**
1241
+ * 将文件内容格式化为带行号文本(模拟编辑器行号显示),
1242
+ * 支持通过 start_line / end_line 截取读取范围。
1243
+ */
1244
+ formatFileContentWithLineNumbers(content, startLineParam, endLineParam) {
1245
+ const lines = content.split(/\r?\n/);
1246
+ const normalized = lines.length > 0 && lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines;
1247
+ const total = normalized.length;
1248
+ if (total === 0) {
1249
+ return "1|(empty file)";
1250
+ }
1251
+ const parseLine = (val, fallback) => {
1252
+ if (val === void 0 || val === null || val.trim() === "") {
1253
+ return fallback;
1254
+ }
1255
+ const num = Number.parseInt(val, 10);
1256
+ if (!Number.isFinite(num) || num <= 0) {
1257
+ return fallback;
1258
+ }
1259
+ return num;
1260
+ };
1261
+ const start = parseLine(startLineParam, 1);
1262
+ const end = parseLine(endLineParam, total);
1263
+ if (start > end) {
1264
+ return `Error: start_line (${start}) cannot be greater than end_line (${end}).`;
1265
+ }
1266
+ if (start > total) {
1267
+ return `Error: start_line (${start}) is out of range. The file has ${total} lines.`;
1268
+ }
1269
+ const from = Math.max(1, start);
1270
+ const to = Math.min(total, end);
1271
+ const width = String(total).length;
1272
+ const selected = normalized.slice(from - 1, to);
1273
+ const numbered = selected.map((line, idx) => `${String(from + idx).padStart(width)}|${line}`).join("\n");
1274
+ const header = from === 1 && to === total ? `1-${total}/${total}` : `${from}-${to}/${total}`;
1275
+ return `${header}
1276
+ ${numbered}`;
1277
+ }
1092
1278
  /**
1093
1279
  * 简单的 Diff 解析与替换辅助方法
1094
1280
  */
@@ -1338,6 +1524,9 @@ var NodeApiHandler = class {
1338
1524
  };
1339
1525
  }
1340
1526
  }
1527
+ clearConversationId() {
1528
+ this.conversationId = void 0;
1529
+ }
1341
1530
  };
1342
1531
 
1343
1532
  // src/prompt/NodePromptManager.ts
@@ -1450,10 +1639,11 @@ ${context.customInstructions}
1450
1639
  if (allowDefaultTools) {
1451
1640
  useDefaultTools = defaultTools.filter((tool) => allowDefaultTools.includes(tool.name));
1452
1641
  }
1642
+ const renderedCustomTools = renderCustomToolsPrompt(customTools, context.cwd);
1453
1643
  return `# Available Tools
1454
1644
  You can use the following tools by formatting your output as XML blocks. Only use ONE tool per message, and ALWAYS wait for the user's response with the execution result before proceeding.
1455
1645
 
1456
- ${[...useDefaultTools, ...customTools].map((tool) => {
1646
+ ${[...useDefaultTools].map((tool) => {
1457
1647
  return `### ${tool.name}
1458
1648
  Description: ${tool.description}
1459
1649
  Parameters:
@@ -1466,6 +1656,8 @@ ${Object.entries(tool.params).map(([name, description]) => `<${name}>${descripti
1466
1656
  </${tool.name}>
1467
1657
  `;
1468
1658
  }).join("\n")}
1659
+ ${renderedCustomTools ? `
1660
+ ${renderedCustomTools}` : ""}
1469
1661
 
1470
1662
  # Tool Use Examples
1471
1663
  ${useDefaultTools.filter((item) => Boolean(item.example)).map((item) => item.example).join("\n")}}
@@ -1639,14 +1831,125 @@ ${this?.getEnvironmentInfoPrompt?.(context)}`;
1639
1831
  4. Ensure the list accurately reflects the current state`;
1640
1832
  }
1641
1833
  };
1834
+
1835
+ // src/memory/condense.ts
1836
+ var TOOL_RESULT_CONTENT_REGEX = /<tool_call_result[^>]*>[\s\S]*?<\/tool_call_result>/;
1837
+ var EXPIRED_MARKER_TEXT = "[The result has expired and has been removed from the context]";
1838
+ var ATTEMPT_TOOL_NAME = "agent_attempt_completion" /* ATTEMPT */;
1839
+ function getContentString(content) {
1840
+ return typeof content === "string" ? content : "";
1841
+ }
1842
+ function isToolResultMessage(content) {
1843
+ if (typeof content !== "string") {
1844
+ return false;
1845
+ }
1846
+ return TOOL_RESULT_CONTENT_REGEX.test(content);
1847
+ }
1848
+ function isAttemptCallMessage(message) {
1849
+ if (message.role !== "assistant") {
1850
+ return false;
1851
+ }
1852
+ const content = getContentString(message.content);
1853
+ return new RegExp(`<${ATTEMPT_TOOL_NAME}[\\s>]`).test(content) || new RegExp(`<name>\\s*${ATTEMPT_TOOL_NAME}\\s*</name>`).test(content);
1854
+ }
1855
+ function markExpiredToolResult(content) {
1856
+ if (typeof content !== "string") {
1857
+ return content;
1858
+ }
1859
+ return content.replace(
1860
+ /(<tool_call_result[^>]*tool_name="([^"]*)"[^>]*>)[\s\S]*?(<\/tool_call_result>)/g,
1861
+ (_match, openTag, _toolName, closeTag) => {
1862
+ return `${openTag}
1863
+ ${EXPIRED_MARKER_TEXT}
1864
+ ${closeTag}`;
1865
+ }
1866
+ );
1867
+ }
1868
+ function splitConversations(messages) {
1869
+ const conversations = [];
1870
+ let current = [];
1871
+ for (const message of messages) {
1872
+ current.push(message);
1873
+ if (isAttemptCallMessage(message)) {
1874
+ conversations.push(current);
1875
+ current = [];
1876
+ }
1877
+ }
1878
+ if (current.length > 0) {
1879
+ conversations.push(current);
1880
+ }
1881
+ return conversations;
1882
+ }
1883
+ function isCompleteConversation(conversation) {
1884
+ if (conversation.length === 0) {
1885
+ return false;
1886
+ }
1887
+ return isAttemptCallMessage(conversation[conversation.length - 1]);
1888
+ }
1889
+ function compressCompleteConversation(conversation) {
1890
+ if (conversation.length <= 2) {
1891
+ return conversation;
1892
+ }
1893
+ const head = conversation[0];
1894
+ const tail = conversation[conversation.length - 1];
1895
+ const mergedMarker = {
1896
+ role: "user",
1897
+ content: "[History Condensed] The intermediate tool calls and tool call results of this conversation have been merged. Please refer to the `agent_attempt_completion` summary below for the final conclusion."
1898
+ };
1899
+ return [head, mergedMarker, tail];
1900
+ }
1901
+ function markExpiredFirstHalf(messages) {
1902
+ const half = Math.floor(messages.length / 2);
1903
+ return messages.map((message, index) => {
1904
+ if (index < half && isToolResultMessage(message.content)) {
1905
+ return { ...message, content: markExpiredToolResult(message.content) };
1906
+ }
1907
+ return message;
1908
+ });
1909
+ }
1910
+ function condenseMessages(messages, maxMessages) {
1911
+ if (messages.length <= maxMessages) {
1912
+ return messages;
1913
+ }
1914
+ const systemMessages = messages.filter((message) => message.role === "system");
1915
+ const restMessages = messages.filter((message) => message.role !== "system");
1916
+ const conversations = splitConversations(restMessages);
1917
+ const completeIndices = [];
1918
+ for (let i = 0; i < conversations.length; i++) {
1919
+ if (isCompleteConversation(conversations[i])) {
1920
+ completeIndices.push(i);
1921
+ }
1922
+ }
1923
+ const currentLength = () => systemMessages.length + conversations.reduce((acc, c) => acc + c.length, 0);
1924
+ if (completeIndices.length >= 2) {
1925
+ const lastCompleteIndex = completeIndices[completeIndices.length - 1];
1926
+ for (const idx of completeIndices) {
1927
+ if (idx === lastCompleteIndex) {
1928
+ break;
1929
+ }
1930
+ conversations[idx] = compressCompleteConversation(conversations[idx]);
1931
+ if (currentLength() <= maxMessages) {
1932
+ break;
1933
+ }
1934
+ }
1935
+ }
1936
+ let condensed = conversations.flat();
1937
+ condensed = markExpiredFirstHalf(condensed);
1938
+ if (systemMessages.length + condensed.length > maxMessages) {
1939
+ const nonSystemBudget = Math.max(0, maxMessages - systemMessages.length);
1940
+ condensed = condensed.slice(-nonSystemBudget);
1941
+ }
1942
+ return [...systemMessages, ...condensed];
1943
+ }
1642
1944
  var NodeMemoryManager = class {
1643
1945
  filePath;
1644
1946
  maxMessages;
1947
+ onCondense;
1645
1948
  /**
1646
1949
  * @param dbPath 存储历史记录的本地文件路径(例如:'./.cline_memory.json')
1647
- * @param maxMessages 保留的最大消息数量,用于简单的历史截断(默认 150)
1950
+ * @param maxMessages 保留的最大消息数量,用于简单的历史截断(默认 200)
1648
1951
  */
1649
- constructor(dbPath, maxMessages = 150) {
1952
+ constructor(dbPath, maxMessages = 200) {
1650
1953
  this.filePath = path__namespace.resolve(dbPath);
1651
1954
  this.maxMessages = maxMessages;
1652
1955
  }
@@ -1681,19 +1984,22 @@ var NodeMemoryManager = class {
1681
1984
  await this.writeMessages(messages);
1682
1985
  await this.condenseHistory();
1683
1986
  }
1987
+ /**
1988
+ * 压缩历史:
1989
+ * - system 消息保持不变
1990
+ * - 以 agent_attempt_completion 为边界切分完整对话,仅合并“后面还有其他完整对话”的旧完整对话
1991
+ * - 其余需要压缩的场景下,对前半段工具调用结果标记为“结果已过期”
1992
+ * - 同时触发 onCondense 回调(宿主可据此清空 dify conversationId 新开窗口)
1993
+ */
1684
1994
  async condenseHistory() {
1685
1995
  const messages = await this.readMessages();
1686
- if (messages.length > this.maxMessages) {
1687
- const firstMessage = messages[0];
1688
- const isFirstSystem = firstMessage && firstMessage.role === "system";
1689
- const sliceCount = messages.length - this.maxMessages;
1690
- let condensed;
1691
- if (isFirstSystem) {
1692
- condensed = [firstMessage, ...messages.slice(sliceCount + 1)];
1693
- } else {
1694
- condensed = messages.slice(sliceCount);
1695
- }
1696
- await this.writeMessages(condensed);
1996
+ const condensed = condenseMessages(messages, this.maxMessages);
1997
+ if (condensed === messages) {
1998
+ return;
1999
+ }
2000
+ await this.writeMessages(condensed);
2001
+ if (this.onCondense) {
2002
+ await this.onCondense();
1697
2003
  }
1698
2004
  }
1699
2005
  async clear() {
@@ -1913,10 +2219,14 @@ exports.NodePromptManager = NodePromptManager;
1913
2219
  exports.READ_ONLY_TOOLS = READ_ONLY_TOOLS;
1914
2220
  exports.TOOL_RESULT_BLOCK = TOOL_RESULT_BLOCK;
1915
2221
  exports.VERSION = VERSION;
2222
+ exports.collectCustomToolNames = collectCustomToolNames;
1916
2223
  exports.createNodeAgent = createNodeAgent;
1917
2224
  exports.defaultTools = defaultTools;
1918
2225
  exports.getToolUseNames = getToolUseNames;
2226
+ exports.isCustomToolGroup = isCustomToolGroup;
1919
2227
  exports.parseAssistantMessageV2 = parseAssistantMessageV2;
2228
+ exports.renderCustomToolPrompt = renderCustomToolPrompt;
2229
+ exports.renderCustomToolsPrompt = renderCustomToolsPrompt;
1920
2230
  exports.setDynamicToolUseNames = setDynamicToolUseNames;
1921
2231
  exports.toolParamNames = toolParamNames;
1922
2232
  exports.toolUseNames = toolUseNames;