foliko 1.1.2 → 1.1.4

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.
Files changed (205) hide show
  1. package/.agent/agents/code-assistant.json +14 -0
  2. package/.agent/agents/email-assistant.json +14 -0
  3. package/.agent/agents/file-assistant.json +15 -0
  4. package/.agent/agents/system-assistant.json +15 -0
  5. package/.agent/agents/web-assistant.json +12 -0
  6. package/.agent/data/ambient/goals.json +50 -0
  7. package/.agent/data/ambient/memories.json +7 -0
  8. package/.agent/data/default.json +3 -412
  9. package/.agent/data/plugins-state.json +174 -173
  10. package/.agent/data/scheduler/tasks.json +1 -0
  11. package/.agent/memory/core.md +1 -0
  12. package/.agent/memory/project/mnn93ogy-ypjn27.md +9 -0
  13. package/.agent/memory/project/mnn98fqy-5nhc1u.md +25 -0
  14. package/.agent/memory/reference/mnq3oenw-46haj6.md +63 -0
  15. package/.agent/memory/reference/mnq5qxm2-mjoooh.md +116 -0
  16. package/.agent/memory/user/mnm67t9m-x8rekk.md +9 -0
  17. package/.agent/memory/user/mnn5mmqh-w6aktx.md +11 -0
  18. package/.agent/memory/user/mnnbfhhn-dk1bd1.md +22 -0
  19. package/.agent/package.json +8 -0
  20. package/.agent/plugins/__pycache__/file_writer.cpython-312.pyc +0 -0
  21. package/.agent/plugins/daytona/README.md +89 -0
  22. package/.agent/plugins/daytona/index.js +377 -0
  23. package/.agent/plugins/daytona/package.json +12 -0
  24. package/.agent/plugins/marknative/README.md +134 -0
  25. package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
  26. package/.agent/plugins/marknative/index.js +256 -0
  27. package/.agent/plugins/marknative/package.json +12 -0
  28. package/.agent/plugins/marknative/update-readme.js +134 -0
  29. package/.agent/plugins/poster-plugin/emojis/rocket.png +1 -0
  30. package/.agent/plugins/poster-plugin/fonts/SegoeUI Emoji.ttf +0 -0
  31. package/.agent/plugins/poster-plugin/src/elements/text.js +3 -1
  32. package/.agent/plugins/poster-plugin/src/fonts.js +10 -0
  33. package/.agent/plugins/poster-plugin/yarn.lock +1007 -0
  34. package/.agent/plugins/system-info/index.js +387 -0
  35. package/.agent/plugins/system-info/package.json +4 -0
  36. package/.agent/plugins/system-info/test.js +40 -0
  37. package/.agent/plugins.json +11 -5
  38. package/.agent/python-scripts/test_sample.py +24 -0
  39. package/.agent/sessions/cli_default.json +1869 -691
  40. package/.agent/skills/agent-browser/SKILL.md +311 -0
  41. package/.agent/skills/agent-browser/TEST_PLAN.md +200 -0
  42. package/.agent/skills/sysinfo/SKILL.md +38 -0
  43. package/.agent/skills/sysinfo/system-info.sh +130 -0
  44. package/.agent/skills/workflow/SKILL.md +324 -0
  45. package/.agent/weixin.json +6 -0
  46. package/.agent/workflows/email-digest.json +50 -0
  47. package/.agent/workflows/file-backup.json +21 -0
  48. package/.agent/workflows/get-ip-notify.json +32 -0
  49. package/.agent/workflows/news-aggregator.json +93 -0
  50. package/.agent/workflows/news-dashboard-v2.json +94 -0
  51. package/.agent/workflows/notification-batch.json +32 -0
  52. package/.claude/settings.local.json +1 -20
  53. package/.env.example +56 -56
  54. package/README.md +441 -441
  55. package/cli/src/commands/chat.js +22 -13
  56. package/cli/src/ui/chat-ui.js +50 -37
  57. package/output/emoji-segoe-test-v2.png +0 -0
  58. package/output/emoji-segoe-test.png +0 -0
  59. package/output/emoji-test.png +0 -0
  60. package/output/emoji-windows-test.png +0 -0
  61. package/output/foliko-emoji-poster.png +0 -0
  62. package/output/foliko-muji-poster-final.png +0 -0
  63. package/output/foliko-muji-poster-v2.png +0 -0
  64. package/output/foliko-muji-poster.png +0 -0
  65. package/output/foliko-share.png +0 -0
  66. package/output/progress-circle-test.png +0 -0
  67. package/output/vb-agent-poster.png +0 -0
  68. package/package.json +1 -2
  69. package/plugins/default-plugins.js +4 -3
  70. package/plugins/extension-executor-plugin.js +12 -91
  71. package/plugins/file-system-plugin.js +19 -4
  72. package/plugins/memory-plugin.js +33 -4
  73. package/plugins/subagent-plugin.js +14 -37
  74. package/plugins/weixin-plugin.js +40 -168
  75. package/skills/find-skills/AGENTS.md +162 -162
  76. package/skills/find-skills/SKILL.md +133 -133
  77. package/skills/poster-guide/SKILL.md +669 -1426
  78. package/src/core/agent-chat.js +439 -269
  79. package/src/core/agent.js +3 -6
  80. package/.agent/.shared/ui-ux-pro-max/data/charts.csv +0 -26
  81. package/.agent/.shared/ui-ux-pro-max/data/colors.csv +0 -97
  82. package/.agent/.shared/ui-ux-pro-max/data/icons.csv +0 -101
  83. package/.agent/.shared/ui-ux-pro-max/data/landing.csv +0 -31
  84. package/.agent/.shared/ui-ux-pro-max/data/products.csv +0 -97
  85. package/.agent/.shared/ui-ux-pro-max/data/prompts.csv +0 -24
  86. package/.agent/.shared/ui-ux-pro-max/data/react-performance.csv +0 -45
  87. package/.agent/.shared/ui-ux-pro-max/data/stacks/flutter.csv +0 -53
  88. package/.agent/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -56
  89. package/.agent/.shared/ui-ux-pro-max/data/stacks/jetpack-compose.csv +0 -53
  90. package/.agent/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +0 -53
  91. package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -51
  92. package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -59
  93. package/.agent/.shared/ui-ux-pro-max/data/stacks/react-native.csv +0 -52
  94. package/.agent/.shared/ui-ux-pro-max/data/stacks/react.csv +0 -54
  95. package/.agent/.shared/ui-ux-pro-max/data/stacks/shadcn.csv +0 -61
  96. package/.agent/.shared/ui-ux-pro-max/data/stacks/svelte.csv +0 -54
  97. package/.agent/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +0 -51
  98. package/.agent/.shared/ui-ux-pro-max/data/stacks/vue.csv +0 -50
  99. package/.agent/.shared/ui-ux-pro-max/data/styles.csv +0 -59
  100. package/.agent/.shared/ui-ux-pro-max/data/typography.csv +0 -58
  101. package/.agent/.shared/ui-ux-pro-max/data/ui-reasoning.csv +0 -101
  102. package/.agent/.shared/ui-ux-pro-max/data/ux-guidelines.csv +0 -100
  103. package/.agent/.shared/ui-ux-pro-max/data/web-interface.csv +0 -31
  104. package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc +0 -0
  105. package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc +0 -0
  106. package/.agent/.shared/ui-ux-pro-max/scripts/core.py +0 -258
  107. package/.agent/.shared/ui-ux-pro-max/scripts/design_system.py +0 -1067
  108. package/.agent/.shared/ui-ux-pro-max/scripts/search.py +0 -106
  109. package/.agent/ARCHITECTURE.md +0 -288
  110. package/.agent/agents/ambient-agent.md +0 -57
  111. package/.agent/agents/debugger.md +0 -55
  112. package/.agent/agents/email-assistant.md +0 -49
  113. package/.agent/agents/file-manager.md +0 -42
  114. package/.agent/agents/python-developer.md +0 -60
  115. package/.agent/agents/scheduler.md +0 -59
  116. package/.agent/agents/web-developer.md +0 -45
  117. package/.agent/data/puppeteer-sessions/undefined.json +0 -6
  118. package/.agent/data/weixin-media/2026-04-08/img_1775618677512.jpg +0 -0
  119. package/.agent/data/weixin-media/2026-04-08/img_1775619073340.jpg +0 -0
  120. package/.agent/data/weixin-media/2026-04-08/img_1775619097536.jpg +0 -0
  121. package/.agent/data/weixin-media/2026-04-08/img_1775619209388.jpg +0 -0
  122. package/.agent/mcp_config_updated.json +0 -12
  123. package/.agent/plugins/poster-plugin/fonts/NotoColorEmoji-Regular.ttf +0 -0
  124. package/.agent/plugins/puppeteer-plugin/README.md +0 -147
  125. package/.agent/plugins/puppeteer-plugin/index.js +0 -1418
  126. package/.agent/plugins/puppeteer-plugin/package.json +0 -9
  127. package/.agent/rules/GEMINI.md +0 -273
  128. package/.agent/rules/allow-rule.md +0 -77
  129. package/.agent/rules/log-rule.md +0 -83
  130. package/.agent/rules/security-rule.md +0 -93
  131. package/.agent/scripts/auto_preview.py +0 -148
  132. package/.agent/scripts/checklist.py +0 -217
  133. package/.agent/scripts/session_manager.py +0 -120
  134. package/.agent/scripts/verify_all.py +0 -327
  135. package/.agent/sessions/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.json +0 -11097
  136. package/.agent/skills/api-patterns/SKILL.md +0 -81
  137. package/.agent/skills/api-patterns/api-style.md +0 -42
  138. package/.agent/skills/api-patterns/auth.md +0 -24
  139. package/.agent/skills/api-patterns/documentation.md +0 -26
  140. package/.agent/skills/api-patterns/graphql.md +0 -41
  141. package/.agent/skills/api-patterns/rate-limiting.md +0 -31
  142. package/.agent/skills/api-patterns/response.md +0 -37
  143. package/.agent/skills/api-patterns/rest.md +0 -40
  144. package/.agent/skills/api-patterns/scripts/api_validator.py +0 -211
  145. package/.agent/skills/api-patterns/security-testing.md +0 -122
  146. package/.agent/skills/api-patterns/trpc.md +0 -41
  147. package/.agent/skills/api-patterns/versioning.md +0 -22
  148. package/.agent/skills/app-builder/SKILL.md +0 -75
  149. package/.agent/skills/app-builder/agent-coordination.md +0 -71
  150. package/.agent/skills/app-builder/feature-building.md +0 -53
  151. package/.agent/skills/app-builder/project-detection.md +0 -34
  152. package/.agent/skills/app-builder/scaffolding.md +0 -118
  153. package/.agent/skills/app-builder/tech-stack.md +0 -40
  154. package/.agent/skills/app-builder/templates/SKILL.md +0 -39
  155. package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +0 -76
  156. package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +0 -92
  157. package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +0 -88
  158. package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +0 -88
  159. package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +0 -83
  160. package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +0 -90
  161. package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +0 -90
  162. package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +0 -122
  163. package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +0 -122
  164. package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +0 -169
  165. package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +0 -134
  166. package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +0 -83
  167. package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +0 -119
  168. package/.agent/skills/architecture/SKILL.md +0 -55
  169. package/.agent/skills/architecture/context-discovery.md +0 -43
  170. package/.agent/skills/architecture/examples.md +0 -94
  171. package/.agent/skills/architecture/pattern-selection.md +0 -68
  172. package/.agent/skills/architecture/patterns-reference.md +0 -50
  173. package/.agent/skills/architecture/trade-off-analysis.md +0 -77
  174. package/.agent/skills/clean-code/SKILL.md +0 -201
  175. package/.agent/skills/doc.md +0 -177
  176. package/.agent/skills/frontend-design/SKILL.md +0 -418
  177. package/.agent/skills/frontend-design/animation-guide.md +0 -331
  178. package/.agent/skills/frontend-design/color-system.md +0 -311
  179. package/.agent/skills/frontend-design/decision-trees.md +0 -418
  180. package/.agent/skills/frontend-design/motion-graphics.md +0 -306
  181. package/.agent/skills/frontend-design/scripts/accessibility_checker.py +0 -183
  182. package/.agent/skills/frontend-design/scripts/ux_audit.py +0 -722
  183. package/.agent/skills/frontend-design/typography-system.md +0 -345
  184. package/.agent/skills/frontend-design/ux-psychology.md +0 -1116
  185. package/.agent/skills/frontend-design/visual-effects.md +0 -383
  186. package/.agent/skills/i18n-localization/SKILL.md +0 -154
  187. package/.agent/skills/i18n-localization/scripts/i18n_checker.py +0 -241
  188. package/.agent/skills/mcp-builder/SKILL.md +0 -176
  189. package/.agent/skills/web-design-guidelines/SKILL.md +0 -57
  190. package/.agent/workflows/brainstorm.md +0 -113
  191. package/.agent/workflows/create.md +0 -59
  192. package/.agent/workflows/debug.md +0 -103
  193. package/.agent/workflows/deploy.md +0 -176
  194. package/.agent/workflows/enhance.md +0 -63
  195. package/.agent/workflows/orchestrate.md +0 -237
  196. package/.agent/workflows/plan.md +0 -89
  197. package/.agent/workflows/preview.md +0 -81
  198. package/.agent/workflows/simple-test.md +0 -42
  199. package/.agent/workflows/status.md +0 -86
  200. package/.agent/workflows/structured-orchestrate.md +0 -180
  201. package/.agent/workflows/test.md +0 -144
  202. package/.agent/workflows/ui-ux-pro-max.md +0 -296
  203. package/output/beef-love-poster.png +0 -0
  204. package/output/international-news-daily.png +0 -0
  205. package/poster-test-2.png +0 -0
@@ -31,7 +31,7 @@ const MODEL_CONTEXT_LIMITS = {
31
31
  'deepseek-chat': 28000,
32
32
  'deepseek-coder': 28000,
33
33
  // MiniMax
34
- 'MiniMax-M2.7': 130800,
34
+ 'MiniMax-M2.7': 90000,
35
35
  // OpenAI
36
36
  'gpt-4': 100000,
37
37
  'gpt-4o': 100000,
@@ -45,11 +45,9 @@ const MODEL_CONTEXT_LIMITS = {
45
45
 
46
46
  /**
47
47
  * 纯 JavaScript token 计数器
48
- * 基于字符数估算,兼容中英文
49
- * 估算规则:
50
- * - 英文:约 4 字符 = 1 token
51
- * - 中文:约 2 字符 = 1 token
52
- * - 混合文本取加权平均
48
+ * 参考 Claude Code 的实现,使用 bytes/token 比率估算
49
+ * - 普通文本:4 bytes ≈ 1 token
50
+ * - JSON 等密集文本:2 bytes 1 token
53
51
  */
54
52
  class SimpleTokenizer {
55
53
  constructor() {
@@ -59,9 +57,10 @@ class SimpleTokenizer {
59
57
  /**
60
58
  * 估算文本的 token 数
61
59
  * @param {string} text
60
+ * @param {number} [bytesPerToken=4] - bytes per token 比率
62
61
  * @returns {number}
63
62
  */
64
- encode(text) {
63
+ encode(text, bytesPerToken = 4) {
65
64
  if (!text || typeof text !== 'string') {
66
65
  return 0;
67
66
  }
@@ -76,16 +75,16 @@ class SimpleTokenizer {
76
75
  return 0;
77
76
  }
78
77
 
79
- // 分离中英文分别计数
80
- const chineseChars = (cleanText.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
81
- const englishChars = cleanText.length - chineseChars;
82
-
83
- // 中英文分开估算后相加
84
- // 中文约 2 字符/token,英文约 4 字符/token
85
- const chineseTokens = chineseChars / 2;
86
- const englishTokens = englishChars / 4;
78
+ return Math.ceil(cleanText.length / bytesPerToken);
79
+ }
87
80
 
88
- return Math.ceil(chineseTokens + englishTokens);
81
+ /**
82
+ * 估算 JSON 文本的 token 数(更密集)
83
+ * @param {string} text
84
+ * @returns {number}
85
+ */
86
+ encodeForJSON(text) {
87
+ return this.encode(text, 2);
89
88
  }
90
89
  }
91
90
 
@@ -93,7 +92,7 @@ class SimpleTokenizer {
93
92
  const _globalTokenizer = new SimpleTokenizer();
94
93
 
95
94
  // 压缩超时时间(毫秒)
96
- const COMPRESSION_TIMEOUT = 120000; // 2分钟
95
+ const COMPRESSION_TIMEOUT = 30000;
97
96
 
98
97
  class AgentChatHandler extends EventEmitter {
99
98
  /**
@@ -166,7 +165,13 @@ class AgentChatHandler extends EventEmitter {
166
165
  _getSessionMessageStore(sessionId) {
167
166
  if (!sessionId) {
168
167
  // 无 session 的单次请求,使用临时存储
169
- return { messages: [], historyLoaded: false, compressionState: {}, save: () => {} };
168
+ return {
169
+ messages: [],
170
+ historyLoaded: false,
171
+ compressionState: {},
172
+ lastUsage: null, // API 返回的真实 usage
173
+ save: () => {},
174
+ };
170
175
  }
171
176
 
172
177
  if (!this._sessionMessageStores.has(sessionId)) {
@@ -181,6 +186,7 @@ class AgentChatHandler extends EventEmitter {
181
186
  lastTokenCount: 0,
182
187
  count: 0,
183
188
  },
189
+ lastUsage: null, // API 返回的真实 usage
184
190
  save: () => {
185
191
  // 简洁的保存方法
186
192
  if (this.agent?.framework) {
@@ -305,22 +311,24 @@ class AgentChatHandler extends EventEmitter {
305
311
  /**
306
312
  * 计算文本的 token 数
307
313
  * @param {string} text
314
+ * @param {number} [bytesPerToken=4] - bytes per token 比率
308
315
  * @returns {number}
309
316
  * @private
310
317
  */
311
- _countTokens(text) {
318
+ _countTokens(text, bytesPerToken = 4) {
312
319
  if (!text || typeof text !== 'string') return 0;
313
320
  // SimpleTokenizer.encode 已经处理了清理逻辑
314
321
  try {
315
- return this._encoder.encode(text);
322
+ return this._encoder.encode(text, bytesPerToken);
316
323
  } catch (err) {
317
324
  // 估算失败时使用字符计数
318
- return Math.ceil(text.length / 4);
325
+ return Math.ceil(text.length / bytesPerToken);
319
326
  }
320
327
  }
321
328
 
322
329
  /**
323
330
  * 计算消息列表的总 token 数
331
+ * 参考 Claude Code 的分块计数逻辑
324
332
  * @param {Array} messages
325
333
  * @returns {number}
326
334
  * @private
@@ -329,68 +337,184 @@ class AgentChatHandler extends EventEmitter {
329
337
  let total = 0;
330
338
  for (const msg of messages) {
331
339
  if (!msg) continue;
332
- total += 4; // role 标记
333
- if (typeof msg.content === 'string') {
334
- total += this._countTokens(msg.content);
335
- } else if (Array.isArray(msg.content)) {
336
- for (const part of msg.content) {
337
- if (!part || typeof part !== 'object') continue;
340
+ total += this._countMessageTokens(msg);
341
+ }
342
+ return total;
343
+ }
338
344
 
339
- // 处理普通文本
340
- if (part.text) {
341
- total += this._countTokens(String(part.text));
342
- }
345
+ /**
346
+ * 计算单条消息的 token 数
347
+ * @param {Object} msg - 消息对象
348
+ * @returns {number}
349
+ * @private
350
+ */
351
+ _countMessageTokens(msg) {
352
+ let total = 0;
353
+ total += 4; // role 标记
343
354
 
344
- // 处理 tool-result output 字段
345
- if (part.type === 'tool-result' && part.output) {
346
- if (typeof part.output === 'string') {
347
- total += this._countTokens(part.output);
348
- } else if (part.output && typeof part.output === 'object') {
349
- // 处理 { type: 'text', value: '...' } 或 { type: 'text', html: '...' } 等
350
- if (part.output.value) {
351
- total += this._countTokens(String(part.output.value));
352
- }
353
- if (part.output.html) {
354
- total += this._countTokens(part.output.html);
355
- }
356
- // 其他字段也序列化计算
357
- try {
358
- const others = {};
359
- for (const [k, v] of Object.entries(part.output)) {
360
- if (k !== 'value' && k !== 'html') others[k] = v;
361
- }
362
- if (Object.keys(others).length > 0) {
363
- total += this._countTokens(JSON.stringify(others));
364
- }
365
- } catch (e) {
366
- // 忽略
367
- }
368
- }
369
- }
355
+ if (typeof msg.content === 'string') {
356
+ total += this._countTokens(msg.content);
357
+ } else if (Array.isArray(msg.content)) {
358
+ for (const part of msg.content) {
359
+ total += this._countContentBlockTokens(part);
360
+ }
361
+ } else if (msg.content && typeof msg.content === 'object') {
362
+ // 处理工具结果等对象类型
363
+ try {
364
+ const str = JSON.stringify(msg.content);
365
+ total += this._countTokens(str, 2); // JSON 用更密集的比率
366
+ } catch (e) {
367
+ // 无法序列化的内容,忽略
368
+ }
369
+ }
370
+ total += 4; // 结尾标记
371
+ return total;
372
+ }
370
373
 
371
- // 处理 tool-use 的 input 字段
372
- if (part.type === 'tool_use' && part.input) {
373
- try {
374
- total += this._countTokens(JSON.stringify(part.input));
375
- } catch (e) {
376
- // 忽略
374
+ /**
375
+ * 计算单个内容块的 token
376
+ * 参考 Claude Code 的实现
377
+ * @param {Object} block - 内容块
378
+ * @returns {number}
379
+ * @private
380
+ */
381
+ _countContentBlockTokens(block) {
382
+ if (!block || typeof block !== 'object') {
383
+ return 0;
384
+ }
385
+
386
+ const tokenizer = this._encoder;
387
+
388
+ // 文本块
389
+ if (block.type === 'text') {
390
+ return tokenizer.encode(block.text || '');
391
+ }
392
+
393
+ // 工具调用块 - 工具名 + 输入参数(JSON 更密集)
394
+ if (block.type === 'tool-use' || block.type === 'tool_call') {
395
+ let count = 0;
396
+ if (block.name || block.toolName) {
397
+ count += tokenizer.encode(String(block.name || block.toolName || ''));
398
+ }
399
+ if (block.input) {
400
+ try {
401
+ count += tokenizer.encode(JSON.stringify(block.input), 2);
402
+ } catch (e) {}
403
+ }
404
+ return count;
405
+ }
406
+
407
+ // 工具结果块
408
+ if (block.type === 'tool-result' || block.type === 'tool_result') {
409
+ let count = 0;
410
+ if (block.content) {
411
+ if (typeof block.content === 'string') {
412
+ count += tokenizer.encode(block.content);
413
+ } else if (Array.isArray(block.content)) {
414
+ for (const c of block.content) {
415
+ if (c && c.type === 'text') {
416
+ count += tokenizer.encode(c.text || '');
377
417
  }
378
418
  }
379
419
  }
380
- } else if (msg.content && typeof msg.content === 'object') {
381
- // 处理工具结果等对象类型
382
- try {
383
- const str = JSON.stringify(msg.content);
384
- total += this._countTokens(str);
385
- } catch (e) {
386
- // 无法序列化的内容,忽略
420
+ }
421
+ if (block.toolUseId || block.tool_call_id) {
422
+ count += 10; // 工具结果标识
423
+ }
424
+ return count;
425
+ }
426
+
427
+ // 输入块 (tool_input)
428
+ if (block.type === 'input') {
429
+ try {
430
+ return tokenizer.encode(JSON.stringify(block), 2);
431
+ } catch (e) {
432
+ return 0;
433
+ }
434
+ }
435
+
436
+ // 其他类型,尝试 JSON 序列化
437
+ try {
438
+ return tokenizer.encode(JSON.stringify(block), 2);
439
+ } catch (e) {
440
+ return 0;
441
+ }
442
+ }
443
+
444
+ /**
445
+ * 估算消息列表的 token 数(不带消息结构的简单估算)
446
+ * 用于快速检查
447
+ * @param {Array} messages
448
+ * @returns {number}
449
+ * @private
450
+ */
451
+ _roughEstimateMessagesTokens(messages) {
452
+ let total = 0;
453
+ for (const msg of messages) {
454
+ if (!msg) continue;
455
+ if (typeof msg.content === 'string') {
456
+ total += this._countTokens(msg.content);
457
+ } else if (Array.isArray(msg.content)) {
458
+ for (const part of msg.content) {
459
+ if (part && part.text) {
460
+ total += this._countTokens(String(part.text));
461
+ }
387
462
  }
388
463
  }
389
464
  }
390
- total += 4; // 结尾标记
391
465
  return total;
392
466
  }
393
467
 
468
+ /**
469
+ * 基于 API usage 和新消息估算计算当前上下文 token 数
470
+ * 参考 Claude Code 的 tokenCountWithEstimation 实现
471
+ * @param {Array} messages - 消息数组
472
+ * @param {Object} lastUsage - 上一次 API 返回的 usage
473
+ * @param {number} baseMessageCount - 基准消息数量(usage 对应时)
474
+ * @returns {number} 估算的总 token 数
475
+ * @private
476
+ */
477
+ _tokenCountWithEstimation(messages, lastUsage, baseMessageCount = 0) {
478
+ // 如果有真实的 usage 数据,使用它作为基准
479
+ if (lastUsage && typeof lastUsage === 'object') {
480
+ const inputTokens = lastUsage.inputTokens || 0;
481
+ const outputTokens = lastUsage.outputTokens || 0;
482
+ const cacheReadTokens = lastUsage.inputTokenDetails?.cacheReadTokens || 0;
483
+ const cacheWriteTokens = lastUsage.inputTokenDetails?.cacheWriteTokens || 0;
484
+
485
+ // 计算已有消息的真实 token 数
486
+ const baseTokens = inputTokens + cacheReadTokens + outputTokens;
487
+
488
+ // 估算新增消息的 token 数
489
+ if (messages.length > baseMessageCount) {
490
+ const newMessages = messages.slice(baseMessageCount);
491
+ const newMessagesTokens = this._countMessagesTokens(newMessages);
492
+ return baseTokens + newMessagesTokens;
493
+ }
494
+
495
+ return baseTokens;
496
+ }
497
+
498
+ // 没有 usage 数据,使用纯估算
499
+ return this._countMessagesTokens(messages);
500
+ }
501
+
502
+ /**
503
+ * 更新 messageStore 的 lastUsage
504
+ * @param {Object} messageStore
505
+ * @param {Object} usage - API 返回的 usage
506
+ * @private
507
+ */
508
+ _updateMessageStoreUsage(messageStore, usage) {
509
+ if (messageStore && usage) {
510
+ messageStore.lastUsage = {
511
+ inputTokens: usage.inputTokens,
512
+ outputTokens: usage.outputTokens,
513
+ inputTokenDetails: usage.inputTokenDetails || {},
514
+ };
515
+ }
516
+ }
517
+
394
518
  /**
395
519
  * 计算工具定义的 token 数(估算)
396
520
  * @returns {number}
@@ -577,13 +701,11 @@ class AgentChatHandler extends EventEmitter {
577
701
  }
578
702
 
579
703
  // 过滤时保留:有 result 的 tool call,以及没有 result 的 tool call(但要附带其 result)
580
- const indicesToKeep = new Set();
704
+ // 第一遍:先确定哪些 assistant 消息要保留
705
+ const assistantIndicesToKeep = new Set();
581
706
  for (let i = 0; i < recentMessages.length; i++) {
582
707
  const msg = recentMessages[i];
583
- if (msg.role === 'tool') {
584
- // 保留所有 tool result(它们是必要的)
585
- indicesToKeep.add(i);
586
- } else if (msg.role === 'assistant') {
708
+ if (msg.role === 'assistant') {
587
709
  // 检查这个 assistant 消息是否有 tool call
588
710
  let hasToolCall = false;
589
711
  if (msg.content) {
@@ -596,21 +718,53 @@ class AgentChatHandler extends EventEmitter {
596
718
  }
597
719
  }
598
720
  if (hasToolCall) {
599
- indicesToKeep.add(i);
721
+ assistantIndicesToKeep.add(i);
600
722
  } else if (i >= recentMessages.length - 3) {
601
723
  // 保留最近几条 assistant 消息(它们可能包含重要上下文)
602
- indicesToKeep.add(i);
724
+ assistantIndicesToKeep.add(i);
603
725
  }
604
- } else {
605
- // user, system 等角色默认保留
606
- indicesToKeep.add(i);
607
726
  }
608
727
  }
609
728
 
610
729
  // 如果有孤儿的 tool call(没有 result),保留该 assistant 消息
611
730
  for (const [toolCallId, assistantIdx] of assistantToolCalls) {
612
731
  if (!toolResults.has(toolCallId)) {
613
- indicesToKeep.add(assistantIdx);
732
+ assistantIndicesToKeep.add(assistantIdx);
733
+ }
734
+ }
735
+
736
+ // 第二遍:基于 assistant 的保留情况,确定哪些 tool result 要保留
737
+ const indicesToKeep = new Set();
738
+ for (let i = 0; i < recentMessages.length; i++) {
739
+ const msg = recentMessages[i];
740
+ if (msg.role === 'tool') {
741
+ // 检查这个 tool result 是否对应一个被保留的 assistant tool_call
742
+ // 如果对应的 assistant 消息被删除了,这个 tool result 也应该被删除(否则会报 orphaned error)
743
+ let hasPairedAssistant = false;
744
+ const content = Array.isArray(msg.content) ? msg.content : [msg.content];
745
+ for (const item of content) {
746
+ if (item && item.type === 'tool-result' && item.toolCallId) {
747
+ // 检查这个 toolCallId 对应的 assistant 消息是否被保留
748
+ const assistantIdx = assistantToolCalls.get(item.toolCallId);
749
+ if (assistantIdx !== undefined && assistantIndicesToKeep.has(assistantIdx)) {
750
+ hasPairedAssistant = true;
751
+ break;
752
+ }
753
+ }
754
+ }
755
+ // 只有有配对的 assistant 消息被保留时,才保留这个 tool result
756
+ if (hasPairedAssistant) {
757
+ indicesToKeep.add(i);
758
+ } else {
759
+ logger.debug(`Dropping orphaned tool result at index ${i}`);
760
+ }
761
+ } else if (msg.role === 'assistant') {
762
+ if (assistantIndicesToKeep.has(i)) {
763
+ indicesToKeep.add(i);
764
+ }
765
+ } else {
766
+ // user, system 等角色默认保留
767
+ indicesToKeep.add(i);
614
768
  }
615
769
  }
616
770
 
@@ -618,9 +772,6 @@ class AgentChatHandler extends EventEmitter {
618
772
  const sortedIndices = Array.from(indicesToKeep).sort((a, b) => a - b);
619
773
  const filteredRecentMessages = sortedIndices.map((i) => recentMessages[i]);
620
774
 
621
- // 清理孤立 tool results(tool call 已被总结,但 result 还在)
622
- this._cleanOrphanedToolResults(filteredRecentMessages);
623
-
624
775
  // 直接修改传入的 messages 数组
625
776
  messages.length = 0;
626
777
  messages.push(...systemMessages, summary, ...filteredRecentMessages);
@@ -741,9 +892,6 @@ class AgentChatHandler extends EventEmitter {
741
892
  const sortedIndices = Array.from(indicesToKeep).sort((a, b) => a - b);
742
893
  const filteredRecentMessages = sortedIndices.map((i) => recentMessages[i]);
743
894
 
744
- // 清理孤立 tool results(tool call 已被总结,但 result 还在)
745
- this._cleanOrphanedToolResults(filteredRecentMessages);
746
-
747
895
  // 直接修改传入的 messages 数组
748
896
  messages.length = 0;
749
897
  messages.push(...systemMessages, summary, ...filteredRecentMessages);
@@ -766,38 +914,6 @@ class AgentChatHandler extends EventEmitter {
766
914
  }
767
915
  }
768
916
 
769
- /**
770
- * 清理孤立的 tool results(对应的 tool call 已被压缩删除)
771
- * @param {Array} messages - 消息数组
772
- * @private
773
- */
774
- _cleanOrphanedToolResults(messages) {
775
- // 构建当前消息中存在的 toolCallId 集合
776
- const validToolCallIds = new Set();
777
- for (const msg of messages) {
778
- if (msg.role === 'assistant' && msg.content) {
779
- const content = Array.isArray(msg.content) ? msg.content : [msg.content];
780
- for (const item of content) {
781
- if (item.type === 'tool-call' && item.toolCallId) {
782
- validToolCallIds.add(item.toolCallId);
783
- }
784
- }
785
- }
786
- }
787
-
788
- // 过滤掉孤立 tool results
789
- for (const msg of messages) {
790
- if (msg.role === 'tool' && Array.isArray(msg.content)) {
791
- msg.content = msg.content.filter((item) => {
792
- if (item && item.type === 'tool-result' && item.toolCallId) {
793
- return validToolCallIds.has(item.toolCallId);
794
- }
795
- return true;
796
- });
797
- }
798
- }
799
- }
800
-
801
917
  /**
802
918
  * 使用 AI 对消息进行总结
803
919
  * @param {Array} messages - 要总结的消息
@@ -1074,7 +1190,7 @@ ${truncatedContent}${truncatedNote}
1074
1190
  const toolsTokens = this._countToolsTokens();
1075
1191
  const systemPromptTokens = this._countTokens(this._systemPrompt);
1076
1192
  const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
1077
- const limit = this._maxContextTokens * 0.7; // 降低到 70%,更早压缩
1193
+ const limit = this._maxContextTokens * 0.5; // 降低到 50%,更早压缩,防止 context window 超限
1078
1194
 
1079
1195
  if (totalTokens > limit) {
1080
1196
  logger.info(
@@ -1082,11 +1198,48 @@ ${truncatedContent}${truncatedNote}
1082
1198
  );
1083
1199
  // 使用带超时控制的压缩(传入 messages 引用)
1084
1200
  await this._compressContext(sessionId, messages, messageStore);
1201
+ // 压缩后验证消息配对(防止 orphaned tool result)
1202
+ const validatedMessages = this._validateMessagesPairing(messages);
1203
+ if (validatedMessages.length !== messages.length) {
1204
+ messages.length = 0;
1205
+ messages.push(...validatedMessages);
1206
+ }
1085
1207
  }
1086
1208
 
1087
1209
  const maxSteps = options.maxSteps || this._maxSteps;
1088
1210
  const tools = this._getAITools(tool);
1089
1211
 
1212
+ // 验证消息配对(防止 API 调用时报 orphaned tool result 错误)
1213
+ const validatedMessages = this._validateMessagesPairing(messages);
1214
+ if (validatedMessages.length !== messages.length) {
1215
+ messages.length = 0;
1216
+ messages.push(...validatedMessages);
1217
+ }
1218
+
1219
+ // 最终检查:在 API 调用前再次验证 token 数
1220
+ const finalMessagesTokens = this._countMessagesTokens(messages);
1221
+ const finalToolsTokens = this._countToolsTokens();
1222
+ const finalSystemPromptTokens = this._countTokens(this._systemPrompt);
1223
+ const finalTotalTokens = finalMessagesTokens + finalToolsTokens + finalSystemPromptTokens;
1224
+ const finalLimit = this._maxContextTokens * 0.5;
1225
+
1226
+ logger.info(
1227
+ `[API Call Check] messages=${messages.length}, tokens=(${finalMessagesTokens}+${finalToolsTokens}+${finalSystemPromptTokens}=${finalTotalTokens}) vs limit=${finalLimit}`
1228
+ );
1229
+
1230
+ // 如果仍然超过限制,强制压缩
1231
+ if (finalTotalTokens > finalLimit) {
1232
+ logger.warn(`[API Call Check] Still over limit after validation, forcing compression`);
1233
+ await this._compressContext(sessionId, messages, messageStore);
1234
+ const compressedMessages = this._validateMessagesPairing(messages);
1235
+ messages.length = 0;
1236
+ messages.push(...compressedMessages);
1237
+ const afterTokens = this._countMessagesTokens(messages);
1238
+ logger.info(
1239
+ `[After Forced Compression] messages=${messages.length}, tokens=${afterTokens}`
1240
+ );
1241
+ }
1242
+
1090
1243
  // 准备传给 agent 的消息
1091
1244
  const prepareStepChainStream = async ({ stepNumber, messages }) => {
1092
1245
  try {
@@ -1096,18 +1249,12 @@ ${truncatedContent}${truncatedNote}
1096
1249
  return messages;
1097
1250
  }
1098
1251
 
1099
- // 2. 检查并移除不完整的 tool-call 和对应的 tool result
1100
- const validated = this._validateToolCallsForPrepare(messages);
1101
- if (validated !== messages) {
1102
- return validated;
1103
- }
1104
-
1105
- // 3. 消息数量超过阈值才修剪
1252
+ // 2. 消息数量超过阈值才修剪
1106
1253
  if (messages.length <= 50) {
1107
1254
  return messages;
1108
1255
  }
1109
1256
 
1110
- // 4. 保留配对完整的消息
1257
+ // 3. 保留配对完整的消息
1111
1258
  const pruned = this._prepareMessagesForAI(messages);
1112
1259
  return pruned;
1113
1260
  } catch (err) {
@@ -1125,7 +1272,6 @@ ${truncatedContent}${truncatedNote}
1125
1272
  model: this._aiClient,
1126
1273
  instructions: this._systemPrompt,
1127
1274
  tools: tools,
1128
- stopWhen: stepCountIs(30),
1129
1275
  prepareStep: prepareStepChainStream,
1130
1276
  });
1131
1277
 
@@ -1134,6 +1280,14 @@ ${truncatedContent}${truncatedNote}
1134
1280
  return agent.generate({ messages, ...this.providerOptions });
1135
1281
  });
1136
1282
 
1283
+ // 捕获 API 返回的 usage 用于更准确的 token 估算
1284
+ if (result.usage) {
1285
+ this._updateMessageStoreUsage(messageStore, result.usage);
1286
+ logger.debug(
1287
+ `API usage: input=${result.usage.inputTokens}, output=${result.usage.outputTokens}`
1288
+ );
1289
+ }
1290
+
1137
1291
  messages.push(...result.response.messages);
1138
1292
 
1139
1293
  // 触发 agent:message 事件,让 memory 插件可以自动提取记忆
@@ -1142,7 +1296,7 @@ ${truncatedContent}${truncatedNote}
1142
1296
 
1143
1297
  // 生成后检查:如果消息太长,下次需要压缩
1144
1298
  const afterTokens = this._countMessagesTokens(messages);
1145
- if (afterTokens > this._maxContextTokens * 0.8) {
1299
+ if (afterTokens > this._maxContextTokens * 0.5) {
1146
1300
  logger.info(`After generation: ${afterTokens} tokens, will compress on next turn`);
1147
1301
  }
1148
1302
 
@@ -1152,6 +1306,8 @@ ${truncatedContent}${truncatedNote}
1152
1306
  stepCount: result.stepCount || 1,
1153
1307
  };
1154
1308
  } finally {
1309
+ // 校验并修复消息中的不完整工具调用
1310
+ this._validateToolCalls(messages);
1155
1311
  // 确保保存聊天历史到 session(无论成功还是失败)
1156
1312
  messageStore.save();
1157
1313
  }
@@ -1176,7 +1332,7 @@ ${truncatedContent}${truncatedNote}
1176
1332
  this._systemPrompt = this.agent._buildSystemPrompt();
1177
1333
  // 动态导入 AI SDK
1178
1334
  const { tool, ToolLoopAgent } = await this._importAI();
1179
-
1335
+ //await fs.writeFile('system.md',this._systemPrompt)
1180
1336
  const userMessage =
1181
1337
  typeof message === 'string' ? { role: 'user', content: message } : message;
1182
1338
  messages.push(userMessage);
@@ -1186,7 +1342,7 @@ ${truncatedContent}${truncatedNote}
1186
1342
  const toolsTokens = this._countToolsTokens();
1187
1343
  const systemPromptTokens = this._countTokens(this._systemPrompt);
1188
1344
  const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
1189
- const limit = this._maxContextTokens * 0.7; // 降低到 70%
1345
+ const limit = this._maxContextTokens * 0.5; // 降低到 50%,更早压缩,防止 context window 超限
1190
1346
  // 对于流式调用,如果上下文太大,先压缩再开始
1191
1347
  if (totalTokens > limit) {
1192
1348
  logger.info(
@@ -1194,6 +1350,12 @@ ${truncatedContent}${truncatedNote}
1194
1350
  );
1195
1351
  // 流式调用时等待压缩完成(使用带超时控制的压缩)
1196
1352
  await this._compressContext(sessionId, messages, messageStore);
1353
+ // 压缩后验证消息配对(防止 orphaned tool result)
1354
+ const validatedMessages = this._validateMessagesPairing(messages);
1355
+ if (validatedMessages.length !== messages.length) {
1356
+ messages.length = 0;
1357
+ messages.push(...validatedMessages);
1358
+ }
1197
1359
  }
1198
1360
 
1199
1361
  const maxSteps = options.maxSteps || this._maxSteps;
@@ -1202,6 +1364,37 @@ ${truncatedContent}${truncatedNote}
1202
1364
  throw new Error('AI client not configured.');
1203
1365
  }
1204
1366
 
1367
+ // 验证消息配对(防止 API 调用时报 orphaned tool result 错误)
1368
+ const validatedMessages = this._validateMessagesPairing(messages);
1369
+ if (validatedMessages.length !== messages.length) {
1370
+ messages.length = 0;
1371
+ messages.push(...validatedMessages);
1372
+ }
1373
+
1374
+ // 最终检查:在 API 调用前再次验证 token 数
1375
+ const finalMessagesTokens = this._countMessagesTokens(messages);
1376
+ const finalToolsTokens = this._countToolsTokens();
1377
+ const finalSystemPromptTokens = this._countTokens(this._systemPrompt);
1378
+ const finalTotalTokens = finalMessagesTokens + finalToolsTokens + finalSystemPromptTokens;
1379
+ const finalLimit = this._maxContextTokens * 0.5;
1380
+
1381
+ logger.info(
1382
+ `[API Call Check (stream)] messages=${messages.length}, tokens=(${finalMessagesTokens}+${finalToolsTokens}+${finalSystemPromptTokens}=${finalTotalTokens}) vs limit=${finalLimit}`
1383
+ );
1384
+
1385
+ // 如果仍然超过限制,强制压缩
1386
+ if (finalTotalTokens > finalLimit) {
1387
+ logger.warn(`[API Call Check (stream)] Still over limit, forcing compression`);
1388
+ await this._compressContext(sessionId, messages, messageStore);
1389
+ const compressedMessages = this._validateMessagesPairing(messages);
1390
+ messages.length = 0;
1391
+ messages.push(...compressedMessages);
1392
+ const afterTokens = this._countMessagesTokens(messages);
1393
+ logger.info(
1394
+ `[After Forced Compression (stream)] messages=${messages.length}, tokens=${afterTokens}`
1395
+ );
1396
+ }
1397
+
1205
1398
  // 准备传给 agent 的消息
1206
1399
  const prepareStepChainStream = async ({ stepNumber, messages }) => {
1207
1400
  try {
@@ -1211,18 +1404,12 @@ ${truncatedContent}${truncatedNote}
1211
1404
  return messages;
1212
1405
  }
1213
1406
 
1214
- // 2. 检查并移除不完整的 tool-call 和对应的 tool result
1215
- const validated = this._validateToolCallsForPrepare(messages);
1216
- if (validated !== messages) {
1217
- return validated;
1218
- }
1219
-
1220
- // 3. 消息数量超过阈值才修剪
1407
+ // 2. 消息数量超过阈值才修剪
1221
1408
  if (messages.length <= 50) {
1222
1409
  return messages;
1223
1410
  }
1224
1411
 
1225
- // 4. 保留配对完整的消息
1412
+ // 3. 保留配对完整的消息
1226
1413
  const pruned = this._prepareMessagesForAI(messages);
1227
1414
  return pruned;
1228
1415
  } catch (err) {
@@ -1235,7 +1422,6 @@ ${truncatedContent}${truncatedNote}
1235
1422
  model: this._aiClient,
1236
1423
  instructions: this._systemPrompt,
1237
1424
  tools: tools,
1238
- stopWhen: stepCountIs(30),
1239
1425
  prepareStep: prepareStepChainStream,
1240
1426
  });
1241
1427
 
@@ -1271,6 +1457,18 @@ ${truncatedContent}${truncatedNote}
1271
1457
 
1272
1458
  const finishMessages = (await result.response).messages;
1273
1459
  messages.push(...finishMessages);
1460
+ // 捕获 API 返回的 usage 用于更准确的 token 估算
1461
+ const usage = await result.totalUsage;
1462
+ if (usage) {
1463
+ try {
1464
+ this._updateMessageStoreUsage(messageStore, usage);
1465
+ logger.debug(
1466
+ `API usage (stream): input=${usage.inputTokens}, output=${usage.outputTokens}`
1467
+ );
1468
+ } catch (e) {
1469
+ logger.debug('Failed to capture stream usage:', e.message);
1470
+ }
1471
+ }
1274
1472
 
1275
1473
  // 触发 agent:message 事件,让 memory 插件可以自动提取记忆
1276
1474
  const userMsg = messages[messages.length - finishMessages.length - 1];
@@ -1279,6 +1477,8 @@ ${truncatedContent}${truncatedNote}
1279
1477
  this.emit('error', { error: err.message });
1280
1478
  yield { type: 'error', error: err.message };
1281
1479
  } finally {
1480
+ // 校验并修复消息中的不完整工具调用
1481
+ this._validateToolCalls(messages);
1282
1482
  // 确保保存聊天历史到 session(无论成功还是失败)
1283
1483
  messageStore.save();
1284
1484
  }
@@ -1437,171 +1637,141 @@ ${truncatedContent}${truncatedNote}
1437
1637
  }
1438
1638
 
1439
1639
  /**
1440
- * 校验并修复消息中的工具调用参数
1441
- * 移除不完整的 JSON(如只有 "{" )的工具调用
1640
+ * 验证并修复消息中的 tool call/result 配对
1641
+ * 删除没有对应 assistant tool_call 的 tool result(会导致 API 报错)
1442
1642
  * @param {Array} messages - 消息列表
1443
1643
  * @private
1444
1644
  */
1445
- _validateToolCalls(messages) {
1446
- let fixedCount = 0;
1447
-
1645
+ _validateMessagesPairing(messages) {
1646
+ // 收集所有 assistant 的 tool-call IDs
1647
+ const assistantToolCallIds = new Set();
1448
1648
  for (const msg of messages) {
1449
- if (msg.role !== 'assistant' || !Array.isArray(msg.content)) {
1450
- continue;
1451
- }
1452
-
1453
- for (const item of msg.content) {
1454
- if (item.type !== 'tool-call') {
1455
- continue;
1456
- }
1457
-
1458
- const input = item.input;
1459
- if (typeof input !== 'string') {
1460
- continue;
1649
+ if (msg.role === 'assistant' && Array.isArray(msg.content)) {
1650
+ for (const item of msg.content) {
1651
+ if (item.type === 'tool-call' && item.toolCallId) {
1652
+ assistantToolCallIds.add(item.toolCallId);
1653
+ }
1461
1654
  }
1655
+ }
1656
+ }
1462
1657
 
1463
- // 检查 input 是否是有效的 JSON(不是不完整的)
1464
- const trimmed = input.trim();
1465
- if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
1466
- // 不完整的 JSON,移除这个 tool-call
1467
- item.type = 'text';
1468
- item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
1469
- delete item.toolCallId;
1470
- delete item.toolName;
1471
- delete item.input;
1472
- fixedCount++;
1473
- } else {
1474
- // 检查 JSON 是否能完整解析
1475
- try {
1476
- JSON.parse(trimmed);
1477
- } catch (e) {
1478
- // JSON 解析失败,说明是不完整的 JSON
1479
- item.type = 'text';
1480
- item.text = `(工具调用 ${item.toolName} 参数不完整(${e.message}),已跳过)`;
1481
- delete item.toolCallId;
1482
- delete item.toolName;
1483
- delete item.input;
1484
- fixedCount++;
1658
+ // 检查并删除没有配对的 tool result
1659
+ let removedCount = 0;
1660
+ for (const msg of messages) {
1661
+ if (msg.role === 'tool' && Array.isArray(msg.content)) {
1662
+ const originalLength = msg.content.length;
1663
+ msg.content = msg.content.filter((item) => {
1664
+ if (item && item.type === 'tool-result' && item.toolCallId) {
1665
+ if (!assistantToolCallIds.has(item.toolCallId)) {
1666
+ removedCount++;
1667
+ return false; // 删除没有配对的 tool result
1668
+ }
1485
1669
  }
1670
+ return true;
1671
+ });
1672
+
1673
+ // 如果所有 content 都被删除了,标记整个消息待删除
1674
+ if (msg.content.length === 0 && originalLength > 0) {
1675
+ msg._orphaned = true;
1486
1676
  }
1487
1677
  }
1488
1678
  }
1489
1679
 
1490
- if (fixedCount > 0) {
1491
- logger.info(`Fixed ${fixedCount} incomplete tool calls`);
1680
+ // 移除被标记为 orphaned tool 消息
1681
+ const originalLength = messages.length;
1682
+ const filtered = messages.filter((msg) => !(msg.role === 'tool' && msg._orphaned));
1683
+
1684
+ if (removedCount > 0 || filtered.length !== originalLength) {
1685
+ logger.debug(
1686
+ `Removed ${removedCount} orphaned tool-results, ${originalLength - filtered.length} orphaned tool messages`
1687
+ );
1492
1688
  }
1689
+
1690
+ return filtered;
1493
1691
  }
1494
1692
 
1495
1693
  /**
1496
- * 在 prepareStep 中检查并移除不完整的 tool-call 和对应的 tool result
1497
- * 返回修改后的消息数组(如果没修改则返回原数组)
1694
+ * 校验并修复消息中的工具调用参数
1695
+ * 移除不完整的 JSON(如只有 "{" )的工具调用
1696
+ * 同时清理 tool result 中的错误信息
1498
1697
  * @param {Array} messages - 消息列表
1499
- * @returns {Array} 修改后的消息数组或原数组
1500
1698
  * @private
1501
1699
  */
1502
- _validateToolCallsForPrepare(messages) {
1503
- // 收集不完整的 toolCallId
1504
- const invalidToolCallIds = new Set();
1700
+ _validateToolCalls(messages) {
1701
+ let fixedCount = 0;
1505
1702
 
1506
1703
  for (const msg of messages) {
1507
- if (msg.role !== 'assistant' || !Array.isArray(msg.content)) {
1508
- continue;
1509
- }
1510
-
1511
- for (const item of msg.content) {
1512
- if (item.type !== 'tool-call' || !item.toolCallId) {
1513
- continue;
1514
- }
1515
-
1516
- const input = item.input;
1517
- if (typeof input !== 'string') {
1518
- continue;
1519
- }
1520
-
1521
- // 检查 input 是否是有效的 JSON
1522
- const trimmed = input.trim();
1523
- let isInvalid = false;
1704
+ // 清理 assistant 消息中的不完整 tool-call
1705
+ if (msg.role === 'assistant' && Array.isArray(msg.content)) {
1706
+ for (const item of msg.content) {
1707
+ if (item.type !== 'tool-call') {
1708
+ continue;
1709
+ }
1524
1710
 
1525
- if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
1526
- isInvalid = true;
1527
- } else {
1528
- try {
1529
- JSON.parse(trimmed);
1530
- } catch (e) {
1531
- isInvalid = true;
1711
+ const input = item.input;
1712
+ if (typeof input !== 'string') {
1713
+ continue;
1532
1714
  }
1533
- }
1534
1715
 
1535
- if (isInvalid) {
1536
- invalidToolCallIds.add(item.toolCallId);
1537
- logger.warn(`Found incomplete tool call: ${item.toolName} (${item.toolCallId})`);
1716
+ // 检查 input 是否是有效的 JSON(不是不完整的)
1717
+ const trimmed = input.trim();
1718
+ if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
1719
+ // 不完整的 JSON,移除这个 tool-call
1720
+ item.type = 'text';
1721
+ item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
1722
+ delete item.toolCallId;
1723
+ delete item.toolName;
1724
+ delete item.input;
1725
+ fixedCount++;
1726
+ }
1538
1727
  }
1539
1728
  }
1540
- }
1541
1729
 
1542
- // 如果没有不完整的 tool-call,返回原数组
1543
- if (invalidToolCallIds.size === 0) {
1544
- return messages;
1545
- }
1730
+ // 清理 tool result 中的无效 JSON 错误信息
1731
+ if (msg.role === 'tool' && Array.isArray(msg.content)) {
1732
+ for (const item of msg.content) {
1733
+ if (item.type !== 'tool-result') {
1734
+ continue;
1735
+ }
1546
1736
 
1547
- // 构建新的消息数组,移除不完整的 tool-call 和对应的 tool result
1548
- const result = [];
1737
+ const output = item.output;
1738
+ let errorText = null;
1549
1739
 
1550
- for (const msg of messages) {
1551
- if (msg.role === 'tool') {
1552
- // 检查 tool result 是否对应不完整的 tool-call
1553
- if (Array.isArray(msg.content)) {
1554
- const newContent = [];
1555
- for (const item of msg.content) {
1556
- if (
1557
- item.type === 'tool-result' &&
1558
- item.toolCallId &&
1559
- invalidToolCallIds.has(item.toolCallId)
1560
- ) {
1561
- // 跳过这个 tool result
1562
- continue;
1740
+ // 处理字符串类型 output
1741
+ if (typeof output === 'string') {
1742
+ errorText = output;
1743
+ } else if (typeof output === 'object' && output !== null) {
1744
+ // 处理 { type: 'error-text', value: '...' } 结构
1745
+ if (output.value && typeof output.value === 'string') {
1746
+ errorText = output.value;
1563
1747
  }
1564
- newContent.push(item);
1565
1748
  }
1566
- // 如果所有 content 都被移除了,跳过这条消息
1567
- if (newContent.length === 0) {
1749
+
1750
+ if (!errorText) {
1568
1751
  continue;
1569
1752
  }
1570
- // 浅拷贝消息对象
1571
- result.push({ ...msg, content: newContent });
1572
- } else {
1573
- result.push(msg);
1574
- }
1575
- } else if (msg.role === 'assistant') {
1576
- // 检查并移除 assistant 消息中的不完整 tool-call
1577
- if (Array.isArray(msg.content)) {
1578
- const newContent = [];
1579
- for (const item of msg.content) {
1580
- if (
1581
- item.type === 'tool-call' &&
1582
- item.toolCallId &&
1583
- invalidToolCallIds.has(item.toolCallId)
1584
- ) {
1585
- // 跳过不完整的 tool-call
1586
- continue;
1753
+
1754
+ // 检查是否包含 "JSON parsing failed" 或类似的不完整 JSON 错误
1755
+ if (errorText.includes('JSON parsing failed') || errorText.includes('Invalid input')) {
1756
+ const toolName = item.toolName || 'unknown';
1757
+ // 替换为更简洁的错误信息
1758
+ if (typeof output === 'string') {
1759
+ item.output = `(工具 ${toolName} 参数不完整,已跳过)`;
1760
+ } else {
1761
+ item.output = {
1762
+ type: 'error-text',
1763
+ value: `(工具 ${toolName} 参数不完整,已跳过)`,
1764
+ };
1587
1765
  }
1588
- newContent.push(item);
1589
- }
1590
- // 如果所有 content 都被移除了,跳过这条消息
1591
- if (newContent.length === 0) {
1592
- continue;
1766
+ fixedCount++;
1593
1767
  }
1594
- result.push({ ...msg, content: newContent });
1595
- } else {
1596
- result.push(msg);
1597
1768
  }
1598
- } else {
1599
- result.push(msg);
1600
1769
  }
1601
1770
  }
1602
1771
 
1603
- logger.info(`Removed ${invalidToolCallIds.size} incomplete tool calls and their results`);
1604
- return result;
1772
+ if (fixedCount > 0) {
1773
+ logger.info(`Fixed ${fixedCount} incomplete tool calls/results`);
1774
+ }
1605
1775
  }
1606
1776
 
1607
1777
  /**