foliko 1.0.86 → 1.1.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.
Files changed (104) hide show
  1. package/.agent/data/default.json +3 -4310
  2. package/.agent/data/plugins-state.json +34 -1
  3. package/.agent/mcp_config.json +0 -1
  4. package/.agent/memory/core.md +1 -0
  5. package/.agent/memory/project/mnn93ogy-ypjn27.md +9 -0
  6. package/.agent/memory/project/mnn98fqy-5nhc1u.md +25 -0
  7. package/.agent/memory/user/mnm67t9m-x8rekk.md +9 -0
  8. package/.agent/memory/user/mnn5mmqh-w6aktx.md +11 -0
  9. package/.agent/memory/user/mnnbfhhn-dk1bd1.md +22 -0
  10. package/.agent/plugins/__pycache__/file_writer.cpython-312.pyc +0 -0
  11. package/.agent/plugins/marknative/index.js +2 -7
  12. package/.agent/plugins/poster-plugin/README.md +304 -0
  13. package/.agent/plugins/poster-plugin/fonts/PatuaOne-Regular.ttf +0 -0
  14. package/.agent/plugins/poster-plugin/fonts//345/276/256/350/275/257/351/233/205/351/273/221.ttf +0 -0
  15. package/.agent/plugins/poster-plugin/fonts//345/276/256/350/275/257/351/233/205/351/273/221/347/262/227/344/275/223.ttf +0 -0
  16. package/.agent/plugins/poster-plugin/index.js +13 -0
  17. package/.agent/plugins/poster-plugin/package.json +28 -0
  18. package/.agent/plugins/poster-plugin/src/canvas.js +161 -0
  19. package/.agent/plugins/poster-plugin/src/components/arrow.js +84 -0
  20. package/.agent/plugins/poster-plugin/src/components/avatar.js +71 -0
  21. package/.agent/plugins/poster-plugin/src/components/badge.js +85 -0
  22. package/.agent/plugins/poster-plugin/src/components/card.js +88 -0
  23. package/.agent/plugins/poster-plugin/src/components/chart.js +127 -0
  24. package/.agent/plugins/poster-plugin/src/components/chip.js +88 -0
  25. package/.agent/plugins/poster-plugin/src/components/columns.js +107 -0
  26. package/.agent/plugins/poster-plugin/src/components/cta.js +85 -0
  27. package/.agent/plugins/poster-plugin/src/components/divider.js +55 -0
  28. package/.agent/plugins/poster-plugin/src/components/feature.js +85 -0
  29. package/.agent/plugins/poster-plugin/src/components/featureGrid.js +112 -0
  30. package/.agent/plugins/poster-plugin/src/components/grid.js +118 -0
  31. package/.agent/plugins/poster-plugin/src/components/imageFrame.js +155 -0
  32. package/.agent/plugins/poster-plugin/src/components/index.js +62 -0
  33. package/.agent/plugins/poster-plugin/src/components/listItem.js +146 -0
  34. package/.agent/plugins/poster-plugin/src/components/notification.js +123 -0
  35. package/.agent/plugins/poster-plugin/src/components/progress.js +79 -0
  36. package/.agent/plugins/poster-plugin/src/components/progressCircle.js +117 -0
  37. package/.agent/plugins/poster-plugin/src/components/quote.js +97 -0
  38. package/.agent/plugins/poster-plugin/src/components/rating.js +85 -0
  39. package/.agent/plugins/poster-plugin/src/components/star.js +70 -0
  40. package/.agent/plugins/poster-plugin/src/components/statCard.js +105 -0
  41. package/.agent/plugins/poster-plugin/src/components/stepper.js +118 -0
  42. package/.agent/plugins/poster-plugin/src/components/table.js +159 -0
  43. package/.agent/plugins/poster-plugin/src/components/tagCloud.js +78 -0
  44. package/.agent/plugins/poster-plugin/src/components/timeline.js +105 -0
  45. package/.agent/plugins/poster-plugin/src/components/watermark.js +52 -0
  46. package/.agent/plugins/poster-plugin/src/composer.js +1904 -0
  47. package/.agent/plugins/poster-plugin/src/elements/artText.js +60 -0
  48. package/.agent/plugins/poster-plugin/src/elements/background.js +52 -0
  49. package/.agent/plugins/poster-plugin/src/elements/circle.js +31 -0
  50. package/.agent/plugins/poster-plugin/src/elements/image.js +71 -0
  51. package/.agent/plugins/poster-plugin/src/elements/index.js +26 -0
  52. package/.agent/plugins/poster-plugin/src/elements/line.js +23 -0
  53. package/.agent/plugins/poster-plugin/src/elements/polygon.js +32 -0
  54. package/.agent/plugins/poster-plugin/src/elements/rectangle.js +32 -0
  55. package/.agent/plugins/poster-plugin/src/elements/svg.js +92 -0
  56. package/.agent/plugins/poster-plugin/src/elements/text.js +38 -0
  57. package/.agent/plugins/poster-plugin/src/fonts.js +118 -0
  58. package/.agent/plugins/poster-plugin/src/index.js +1659 -0
  59. package/.agent/plugins/poster-plugin/src/presets.js +36 -0
  60. package/.agent/plugins/poster-plugin/src/templates/business.js +60 -0
  61. package/.agent/plugins/poster-plugin/src/templates/gradient.js +64 -0
  62. package/.agent/plugins/poster-plugin/src/templates/index.js +43 -0
  63. package/.agent/plugins/poster-plugin/src/templates/modern.js +69 -0
  64. package/.agent/plugins/poster-plugin/src/templates/simple.js +58 -0
  65. package/.agent/plugins/poster-plugin/src/templates/social.js +62 -0
  66. package/.agent/plugins/poster-plugin/src/templates/tech.js +84 -0
  67. package/.agent/sessions/cli_default.json +24265 -0
  68. package/.agent/weixin.json +6 -0
  69. package/.claude/settings.local.json +176 -171
  70. package/CLAUDE.md +144 -108
  71. package/docs/CONTEXT_DESIGN.md +1596 -0
  72. package/examples/test-concurrent-chat.js +60 -0
  73. package/examples/test-long-chat.js +77 -0
  74. package/examples/test-session-chat.js +93 -0
  75. package/output/beef-love-poster.png +0 -0
  76. package/package.json +2 -2
  77. package/plugins/default-plugins.js +2 -1
  78. package/plugins/extension-executor-plugin.js +11 -0
  79. package/plugins/memory-plugin.js +984 -0
  80. package/plugins/session-plugin.js +78 -1
  81. package/plugins/weixin-plugin.js +24 -22
  82. package/skills/poster-guide/SKILL.md +743 -0
  83. package/skills/python-plugin-dev/SKILL.md +238 -238
  84. package/skills/skill-guide/SKILL.md +130 -108
  85. package/src/capabilities/skill-manager.js +99 -0
  86. package/src/core/agent-chat.js +627 -180
  87. package/src/core/agent-context.js +188 -0
  88. package/src/core/agent.js +9 -63
  89. package/src/core/context-manager.js +283 -0
  90. package/src/core/framework.js +264 -3
  91. package/src/core/plugin-manager.js +79 -2
  92. package/src/core/request-context.js +98 -0
  93. package/src/core/session-context.js +341 -0
  94. package/src/core/session-storage.js +274 -0
  95. package/src/executors/mcp-executor.js +2 -2
  96. package/src/utils/index.js +239 -67
  97. package/src/utils/plugin-helpers.js +17 -0
  98. package//346/265/267/346/212/245/346/217/222/344/273/266.md +621 -0
  99. package/.agent/plugins/__pycache__/test_plugin.cpython-312.pyc +0 -0
  100. package/.agent/plugins/temp-repo/LICENSE +0 -201
  101. package/.agent/plugins/temp-repo/puppeteer-plugin/README.md +0 -147
  102. package/.agent/plugins/temp-repo/puppeteer-plugin/index.js +0 -1418
  103. package/.agent/plugins/temp-repo/puppeteer-plugin/package.json +0 -9
  104. package/.agent/plugins/test_plugin.py +0 -304
@@ -7,6 +7,23 @@ const { EventEmitter } = require('../utils/event-emitter');
7
7
  const { logger } = require('../utils/logger');
8
8
  const { generateText, stepCountIs } = require('ai');
9
9
  const { prepareMessagesForAPI } = require('../utils');
10
+ const fs = require('fs/promises');
11
+
12
+ /**
13
+ * 从消息中提取 toolCallId(兼容两种格式)
14
+ * 格式1: msg.toolCallId (旧格式)
15
+ * 格式2: msg.content[0].toolCallId (AI SDK CoreMessage 格式)
16
+ * @param {Object} msg - 消息对象
17
+ * @returns {string|null}
18
+ */
19
+ function extractToolCallId(msg) {
20
+ if (msg.toolCallId) return msg.toolCallId;
21
+ if (Array.isArray(msg.content) && msg.content.length > 0) {
22
+ const firstContent = msg.content[0];
23
+ if (firstContent && firstContent.toolCallId) return firstContent.toolCallId;
24
+ }
25
+ return null;
26
+ }
10
27
 
11
28
  // 模型上下文限制表(留 15-20% 余量给 system prompt 和输出)
12
29
  const MODEL_CONTEXT_LIMITS = {
@@ -96,7 +113,8 @@ class AgentChatHandler extends EventEmitter {
96
113
  this.providerOptions = config.providerOptions || {};
97
114
 
98
115
  this._systemPrompt = config.systemPrompt || 'You are a helpful assistant.';
99
- this._messages = [];
116
+ // this._messages = []; // 移除共享消息存储!改用 Per-Session
117
+ this._sessionMessageStores = new Map(); // sessionId → { messages: [], historyLoaded: false, compressionState: {} }
100
118
  this._tools = new Map();
101
119
  this._maxSteps = 20; // 降低默认步骤数,减少上下文消耗
102
120
 
@@ -123,10 +141,62 @@ class AgentChatHandler extends EventEmitter {
123
141
 
124
142
  // Session 历史存储配置
125
143
  this._sessionHistoryKey = config.sessionHistoryKey || 'chat_history';
144
+
145
+ // Per-Session 队列管理
146
+ this._sessionQueues = new Map(); // sessionId -> Queue of {message, options, resolve, reject}
147
+ this._processingSessions = new Set(); // sessionId -> processing flag
148
+
149
+ // Session Memory prepareStep(从 memory 插件获取)
150
+ this._sessionMemoryPrepareStep = null;
151
+ if (agent?.framework) {
152
+ const memoryPlugin = agent.framework.pluginManager.get('memory');
153
+ if (memoryPlugin?.instance?.getPrepareStep) {
154
+ this._sessionMemoryPrepareStep = memoryPlugin.instance.getPrepareStep();
155
+ logger.debug('Session memory prepareStep loaded');
156
+ }
157
+ }
126
158
  }
127
159
 
128
160
  /**
129
- * session 存储加载聊天历史
161
+ * 获取 Per-Session 的消息存储
162
+ * @param {string} sessionId - 会话 ID
163
+ * @returns {Object} 消息存储对象
164
+ * @private
165
+ */
166
+ _getSessionMessageStore(sessionId) {
167
+ if (!sessionId) {
168
+ // 无 session 的单次请求,使用临时存储
169
+ return { messages: [], historyLoaded: false, compressionState: {}, save: () => {} };
170
+ }
171
+
172
+ if (!this._sessionMessageStores.has(sessionId)) {
173
+ // 自动从 session 加载消息
174
+ const savedMessages = this._loadHistoryFromSession(sessionId);
175
+ this._sessionMessageStores.set(sessionId, {
176
+ sessionId, // 保存 sessionId 引用
177
+ messages: savedMessages,
178
+ historyLoaded: true, // 已加载
179
+ compressionState: {
180
+ lastCompressedAt: null,
181
+ lastTokenCount: 0,
182
+ count: 0,
183
+ },
184
+ save: () => {
185
+ // 简洁的保存方法
186
+ if (this.agent?.framework) {
187
+ const sessionCtx = this.agent.framework.getOrCreateSessionContext(sessionId);
188
+ if (sessionCtx) {
189
+ sessionCtx.replaceMessages(this._sessionMessageStores.get(sessionId).messages);
190
+ }
191
+ }
192
+ },
193
+ });
194
+ }
195
+ return this._sessionMessageStores.get(sessionId);
196
+ }
197
+
198
+ /**
199
+ * 从 SessionContext 加载聊天历史
130
200
  * @param {string} sessionId - 会话 ID
131
201
  * @returns {Array} 消息数组
132
202
  * @private
@@ -135,10 +205,10 @@ class AgentChatHandler extends EventEmitter {
135
205
  if (!sessionId || !this.agent?.framework) return [];
136
206
 
137
207
  try {
138
- const sessionPlugin = this.agent.framework.pluginManager.get('session');
139
- if (!sessionPlugin) return [];
208
+ const sessionCtx = this.agent.framework.getOrCreateSessionContext(sessionId);
209
+ if (!sessionCtx) return [];
140
210
 
141
- const messages = sessionPlugin.getHistory(sessionId);
211
+ const messages = sessionCtx.getMessages();
142
212
  return messages || [];
143
213
  } catch (err) {
144
214
  // 忽略加载错误
@@ -147,34 +217,84 @@ class AgentChatHandler extends EventEmitter {
147
217
  }
148
218
 
149
219
  /**
150
- * 保存聊天历史到 session 存储
220
+ * 保存聊天历史到 SessionContext
151
221
  * @param {string} sessionId - 会话 ID
152
222
  * @param {Array} messages - 消息数组
153
223
  * @private
154
224
  */
155
225
  _saveHistoryToSession(sessionId, messages) {
156
- if (!sessionId || !this.agent?.framework || !messages || messages.length === 0) return;
226
+ if (!sessionId || !this.agent?.framework || !messages) return;
157
227
 
158
228
  try {
159
- const sessionPlugin = this.agent.framework.pluginManager.get('session');
160
- if (!sessionPlugin) return;
161
-
162
- // 获取 session 中已有消息数量,避免重复添加
163
- const existingHistory = sessionPlugin.getHistory(sessionId);
164
- const existingCount = existingHistory.length;
229
+ const sessionCtx = this.agent.framework.getOrCreateSessionContext(sessionId);
230
+ if (!sessionCtx) return;
165
231
 
166
- // 只添加新消息(避免重复添加已加载的历史)
167
- const simpleMessages = messages;
168
-
169
- const newMessages = simpleMessages.slice(existingCount);
170
- for (const msg of newMessages) {
171
- sessionPlugin.addMessage(sessionId, msg);
172
- }
232
+ // 清理消息格式后整体替换
233
+ const cleanedMessages = prepareMessagesForAPI(messages);
234
+ sessionCtx.replaceMessages(cleanedMessages);
173
235
  } catch (err) {
174
236
  // 忽略保存错误
175
237
  }
176
238
  }
177
239
 
240
+ /**
241
+ * 将消息加入队列(Per-Session)
242
+ * @param {string} sessionId - 会话 ID
243
+ * @param {string|Object} message - 消息
244
+ * @param {Object} options - 选项
245
+ * @returns {Promise}
246
+ */
247
+ enqueue(sessionId, message, options = {}) {
248
+ if (!this._sessionQueues.has(sessionId)) {
249
+ this._sessionQueues.set(sessionId, []);
250
+ }
251
+ const queue = this._sessionQueues.get(sessionId);
252
+
253
+ if (this._processingSessions.has(sessionId)) {
254
+ // 已在处理中,加入队列
255
+ return new Promise((resolve, reject) => {
256
+ queue.push({ message, options, resolve, reject });
257
+ });
258
+ }
259
+
260
+ // 直接处理
261
+ return this._processWithSession(sessionId, message, options);
262
+ }
263
+
264
+ /**
265
+ * 处理单个 session 的消息
266
+ * @param {string} sessionId - 会话 ID
267
+ * @param {string|Object} message - 消息
268
+ * @param {Object} options - 选项
269
+ * @returns {Promise}
270
+ * @private
271
+ */
272
+ async _processWithSession(sessionId, message, options) {
273
+ this._processingSessions.add(sessionId);
274
+ try {
275
+ return await this._doChat(message, options);
276
+ } finally {
277
+ this._processingSessions.delete(sessionId);
278
+ // 处理队列中的下一条
279
+ setImmediate(() => this._drainQueue(sessionId));
280
+ }
281
+ }
282
+
283
+ /**
284
+ * 消费队列中的下一条消息
285
+ * @param {string} sessionId - 会话 ID
286
+ * @private
287
+ */
288
+ _drainQueue(sessionId) {
289
+ const queue = this._sessionQueues.get(sessionId);
290
+ if (!queue || queue.length === 0) return;
291
+
292
+ const item = queue.shift();
293
+ this._processWithSession(sessionId, item.message, item.options)
294
+ .then(item.resolve)
295
+ .catch(item.reject);
296
+ }
297
+
178
298
  /**
179
299
  * 静态方法:保留接口兼容性(无实际作用)
180
300
  */
@@ -232,16 +352,6 @@ class AgentChatHandler extends EventEmitter {
232
352
  return total;
233
353
  }
234
354
 
235
- /**
236
- * 检查是否需要压缩上下文
237
- * @returns {boolean}
238
- * @private
239
- */
240
- _shouldCompress() {
241
- const totalTokens = this._countMessagesTokens(this._messages);
242
- return totalTokens > this._maxContextTokens * this._compressionThreshold;
243
- }
244
-
245
355
  /**
246
356
  * 计算工具定义的 token 数(估算)
247
357
  * @returns {number}
@@ -266,44 +376,36 @@ class AgentChatHandler extends EventEmitter {
266
376
  return total;
267
377
  }
268
378
 
269
- /**
270
- * 检查是否需要压缩(包括工具定义)
271
- * @returns {boolean}
272
- * @private
273
- */
274
- _shouldCompressWithTools() {
275
- const messagesTokens = this._countMessagesTokens(this._messages);
276
- const toolsTokens = this._countToolsTokens();
277
- const systemPromptTokens = this._countTokens(this._systemPrompt);
278
- const total = messagesTokens + toolsTokens + systemPromptTokens;
279
-
280
- // 如果总token数超过上下文限制的 85%,就压缩
281
- return total > this._maxContextTokens * 0.85;
282
- }
283
-
284
379
  /**
285
380
  * 压缩上下文消息(智能摘要模式,支持超时控制)
286
381
  * 策略:
287
382
  * 1. 如果启用了智能摘要且有 AI 客户端,对早期消息进行 AI 总结
288
383
  * 2. 否则使用简单裁剪 + 标记
289
384
  * 3. 支持超时控制,防止压缩阻塞太久
385
+ * @param {string} sessionId - 会话 ID(用于压缩后同步)
386
+ * @param {Array} messages - 消息数组引用
387
+ * @param {Object} messageStore - 消息存储对象
290
388
  * @private
291
389
  */
292
- async _compressContext() {
390
+ async _compressContext(sessionId, messages, messageStore) {
293
391
  // 如果已经有压缩在进行,返回现有的 Promise
294
392
  if (this._compressionInProgress && this._compressionPromise) {
295
393
  logger.debug('Compression already in progress, waiting for it...');
296
394
  return this._compressionPromise;
297
395
  }
298
396
 
299
- if (this._messages.length <= this._keepRecentMessages) {
397
+ if (messages.length <= this._keepRecentMessages) {
300
398
  return;
301
399
  }
302
400
 
303
401
  this._compressionInProgress = true;
304
402
 
305
403
  // 创建压缩 Promise,带超时控制
306
- this._compressionPromise = this._executeCompressionWithTimeout().finally(() => {
404
+ this._compressionPromise = this._executeCompressionWithTimeout(
405
+ sessionId,
406
+ messages,
407
+ messageStore
408
+ ).finally(() => {
307
409
  this._compressionInProgress = false;
308
410
  this._compressionPromise = null;
309
411
  });
@@ -313,15 +415,21 @@ class AgentChatHandler extends EventEmitter {
313
415
 
314
416
  /**
315
417
  * 执行压缩(带超时)
418
+ * @param {string} sessionId - 会话 ID
419
+ * @param {Array} messages - 消息数组引用
420
+ * @param {Object} messageStore - 消息存储对象
316
421
  * @private
317
422
  */
318
- async _executeCompressionWithTimeout() {
423
+ async _executeCompressionWithTimeout(sessionId, messages, messageStore) {
319
424
  try {
320
- return await Promise.race([this._doCompress(), this._createTimeoutPromise()]);
425
+ return await Promise.race([
426
+ this._doCompress(sessionId, messages, messageStore),
427
+ this._createTimeoutPromise(),
428
+ ]);
321
429
  } catch (err) {
322
430
  logger.warn('Compression failed:', err.message);
323
431
  // 压缩失败时使用简单的截断策略
324
- this._simpleCompress();
432
+ this._simpleCompress(sessionId, messages, messageStore);
325
433
  }
326
434
  }
327
435
 
@@ -358,11 +466,14 @@ class AgentChatHandler extends EventEmitter {
358
466
 
359
467
  /**
360
468
  * 执行实际压缩逻辑
469
+ * @param {string} sessionId - 会话 ID
470
+ * @param {Array} messages - 消息数组引用
471
+ * @param {Object} messageStore - 消息存储对象
361
472
  * @private
362
473
  */
363
- async _doCompress() {
364
- const systemMessages = this._messages.filter((m) => m.role === 'system');
365
- const otherMessages = this._messages.filter((m) => m.role !== 'system');
474
+ async _doCompress(sessionId, messages, messageStore) {
475
+ const systemMessages = messages.filter((m) => m.role === 'system');
476
+ const otherMessages = messages.filter((m) => m.role !== 'system');
366
477
 
367
478
  // 保留最近的 N 条非系统消息
368
479
  const recentMessages = otherMessages.slice(-this._keepRecentMessages);
@@ -390,23 +501,117 @@ class AgentChatHandler extends EventEmitter {
390
501
  content: summaryContent,
391
502
  };
392
503
 
393
- this._messages = [...systemMessages, summary, ...recentMessages].filter(
394
- (item) => item.role !== 'tool'
395
- );
504
+ // 构建保留的消息,确保 tool call 和 tool result 配对不被分离
505
+ const assistantToolCalls = new Map(); // toolCallId -> assistant message index
506
+ const toolResults = new Map(); // toolCallId -> tool result indices
507
+
508
+ // 第一遍:找出所有 tool call 和它们的 results
509
+ for (let i = 0; i < recentMessages.length; i++) {
510
+ const msg = recentMessages[i];
511
+ if (msg.role === 'assistant' && msg.content) {
512
+ const content = Array.isArray(msg.content) ? msg.content : [msg.content];
513
+ for (const item of content) {
514
+ if (item.type === 'tool-call' && item.toolCallId) {
515
+ assistantToolCalls.set(item.toolCallId, i);
516
+ }
517
+ }
518
+ }
519
+ if (msg.role === 'tool' && Array.isArray(msg.content)) {
520
+ // 遍历 tool message 中的所有 tool-results
521
+ for (const item of msg.content) {
522
+ if (item && item.type === 'tool-result' && item.toolCallId) {
523
+ if (!toolResults.has(item.toolCallId)) {
524
+ toolResults.set(item.toolCallId, []);
525
+ }
526
+ toolResults.get(item.toolCallId).push(i);
527
+ }
528
+ }
529
+ }
530
+ }
531
+
532
+ // 第二遍:找出哪些 tool call 没有对应的 result(需要保留)
533
+ const orphanToolCalls = new Set();
534
+ for (const [toolCallId, assistantIdx] of assistantToolCalls) {
535
+ if (!toolResults.has(toolCallId)) {
536
+ orphanToolCalls.add(toolCallId);
537
+ }
538
+ }
539
+
540
+ // 过滤时保留:有 result 的 tool call,以及没有 result 的 tool call(但要附带其 result)
541
+ const indicesToKeep = new Set();
542
+ for (let i = 0; i < recentMessages.length; i++) {
543
+ const msg = recentMessages[i];
544
+ if (msg.role === 'tool') {
545
+ // 保留所有 tool result(它们是必要的)
546
+ indicesToKeep.add(i);
547
+ } else if (msg.role === 'assistant') {
548
+ // 检查这个 assistant 消息是否有 tool call
549
+ let hasToolCall = false;
550
+ if (msg.content) {
551
+ const content = Array.isArray(msg.content) ? msg.content : [msg.content];
552
+ for (const item of content) {
553
+ if (item.type === 'tool-call' && item.toolCallId) {
554
+ hasToolCall = true;
555
+ break;
556
+ }
557
+ }
558
+ }
559
+ if (hasToolCall) {
560
+ indicesToKeep.add(i);
561
+ } else if (i >= recentMessages.length - 3) {
562
+ // 保留最近几条 assistant 消息(它们可能包含重要上下文)
563
+ indicesToKeep.add(i);
564
+ }
565
+ } else {
566
+ // user, system 等角色默认保留
567
+ indicesToKeep.add(i);
568
+ }
569
+ }
570
+
571
+ // 如果有孤儿的 tool call(没有 result),保留该 assistant 消息
572
+ for (const [toolCallId, assistantIdx] of assistantToolCalls) {
573
+ if (!toolResults.has(toolCallId)) {
574
+ indicesToKeep.add(assistantIdx);
575
+ }
576
+ }
577
+
578
+ // 按索引排序并重建消息数组
579
+ const sortedIndices = Array.from(indicesToKeep).sort((a, b) => a - b);
580
+ const filteredRecentMessages = sortedIndices.map((i) => recentMessages[i]);
581
+
582
+ // 直接修改传入的 messages 数组
583
+ messages.length = 0;
584
+ messages.push(...systemMessages, summary, ...filteredRecentMessages);
585
+
586
+ // 更新压缩状态
396
587
  this._compressionCount++;
397
- const totalTokens = this._countMessagesTokens(this._messages);
588
+ if (messageStore.compressionState) {
589
+ messageStore.compressionState.count++;
590
+ messageStore.compressionState.lastCompressedAt = Date.now();
591
+ messageStore.compressionState.lastTokenCount = this._countMessagesTokens(messages);
592
+ }
593
+
594
+ const totalTokens = this._countMessagesTokens(messages);
398
595
  logger.info(
399
- `Context compressed (${this._compressionCount} times). Messages: ${this._messages.length}, Est. tokens: ${totalTokens}`
596
+ `Context compressed (${this._compressionCount} times). Messages: ${messages.length}, Est. tokens: ${totalTokens}`
400
597
  );
598
+
599
+ // 压缩后立即同步到 session
600
+ if (sessionId) {
601
+ this._saveHistoryToSession(sessionId, messages);
602
+ }
401
603
  }
402
604
 
403
605
  /**
404
606
  * 简单压缩(当 AI 压缩失败时使用)
607
+ * @param {string} sessionId - 会话 ID
608
+ * @param {Array} messages - 消息数组引用
609
+ * @param {Object} messageStore - 消息存储对象
405
610
  * @private
406
611
  */
407
- _simpleCompress() {
408
- const systemMessages = this._messages.filter((m) => m.role === 'system');
409
- const otherMessages = this._messages.filter((m) => m.role !== 'system');
612
+ _simpleCompress(sessionId, messages, messageStore) {
613
+ const systemMessages = messages.filter((m) => m.role === 'system');
614
+ const otherMessages = messages.filter((m) => m.role !== 'system');
410
615
  const recentMessages = otherMessages.slice(-this._keepRecentMessages);
411
616
  const compressedCount = otherMessages.length - this._keepRecentMessages;
412
617
 
@@ -417,14 +622,103 @@ class AgentChatHandler extends EventEmitter {
417
622
  content: summaryContent,
418
623
  };
419
624
 
420
- this._messages = [...systemMessages, summary, ...recentMessages].filter(
421
- (item) => item.role !== 'tool'
422
- );
625
+ // 构建保留的消息,确保 tool call 和 tool result 配对不被分离
626
+ const assistantToolCalls = new Map(); // toolCallId -> assistant message index
627
+ const toolResults = new Map(); // toolCallId -> tool result indices
628
+
629
+ // 第一遍:找出所有 tool call 和它们的 results
630
+ for (let i = 0; i < recentMessages.length; i++) {
631
+ const msg = recentMessages[i];
632
+ if (msg.role === 'assistant' && msg.content) {
633
+ const content = Array.isArray(msg.content) ? msg.content : [msg.content];
634
+ for (const item of content) {
635
+ if (item.type === 'tool-call' && item.toolCallId) {
636
+ assistantToolCalls.set(item.toolCallId, i);
637
+ }
638
+ }
639
+ }
640
+ if (msg.role === 'tool' && Array.isArray(msg.content)) {
641
+ // 遍历 tool message 中的所有 tool-results
642
+ for (const item of msg.content) {
643
+ if (item && item.type === 'tool-result' && item.toolCallId) {
644
+ if (!toolResults.has(item.toolCallId)) {
645
+ toolResults.set(item.toolCallId, []);
646
+ }
647
+ toolResults.get(item.toolCallId).push(i);
648
+ }
649
+ }
650
+ }
651
+ }
652
+
653
+ // 第二遍:找出哪些 tool call 没有对应的 result(需要保留)
654
+ const orphanToolCalls = new Set();
655
+ for (const [toolCallId, assistantIdx] of assistantToolCalls) {
656
+ if (!toolResults.has(toolCallId)) {
657
+ orphanToolCalls.add(toolCallId);
658
+ }
659
+ }
660
+
661
+ // 过滤时保留:有 result 的 tool call,以及没有 result 的 tool call
662
+ const indicesToKeep = new Set();
663
+ for (let i = 0; i < recentMessages.length; i++) {
664
+ const msg = recentMessages[i];
665
+ if (msg.role === 'tool') {
666
+ // 保留所有 tool result
667
+ indicesToKeep.add(i);
668
+ } else if (msg.role === 'assistant') {
669
+ let hasToolCall = false;
670
+ if (msg.content) {
671
+ const content = Array.isArray(msg.content) ? msg.content : [msg.content];
672
+ for (const item of content) {
673
+ if (item.type === 'tool-call' && item.toolCallId) {
674
+ hasToolCall = true;
675
+ break;
676
+ }
677
+ }
678
+ }
679
+ if (hasToolCall) {
680
+ indicesToKeep.add(i);
681
+ } else if (i >= recentMessages.length - 3) {
682
+ // 保留最近几条 assistant 消息
683
+ indicesToKeep.add(i);
684
+ }
685
+ } else {
686
+ // user, system 等角色默认保留
687
+ indicesToKeep.add(i);
688
+ }
689
+ }
690
+
691
+ // 如果有孤儿的 tool call(没有 result),保留该 assistant 消息
692
+ for (const [toolCallId, assistantIdx] of assistantToolCalls) {
693
+ if (!toolResults.has(toolCallId)) {
694
+ indicesToKeep.add(assistantIdx);
695
+ }
696
+ }
697
+
698
+ // 按索引排序并重建消息数组
699
+ const sortedIndices = Array.from(indicesToKeep).sort((a, b) => a - b);
700
+ const filteredRecentMessages = sortedIndices.map((i) => recentMessages[i]);
701
+
702
+ // 直接修改传入的 messages 数组
703
+ messages.length = 0;
704
+ messages.push(...systemMessages, summary, ...filteredRecentMessages);
705
+
706
+ // 更新压缩状态
423
707
  this._compressionCount++;
708
+ if (messageStore.compressionState) {
709
+ messageStore.compressionState.count++;
710
+ messageStore.compressionState.lastCompressedAt = Date.now();
711
+ messageStore.compressionState.lastTokenCount = this._countMessagesTokens(messages);
712
+ }
424
713
 
425
714
  logger.info(
426
- `Context simple compressed (${this._compressionCount} times). Messages: ${this._messages.length}`
715
+ `Context simple compressed (${this._compressionCount} times). Messages: ${messages.length}`
427
716
  );
717
+
718
+ // 压缩后立即同步到 session
719
+ if (sessionId) {
720
+ this._saveHistoryToSession(sessionId, messages);
721
+ }
428
722
  }
429
723
 
430
724
  /**
@@ -608,23 +902,34 @@ ${truncatedContent}${truncatedNote}
608
902
  * @param {string} sessionId - 可选,指定 session 则同时清除 session 中的历史
609
903
  */
610
904
  clearHistory(sessionId) {
611
- this._messages = [];
612
- this._compressionCount = 0;
613
-
614
- // 同时清除 session 中的历史
615
- if (sessionId && this.agent?.framework) {
616
- try {
617
- const sessionPlugin = this.agent.framework.pluginManager.get('session');
618
- if (sessionPlugin) {
619
- const session = sessionPlugin.getSession(sessionId);
620
- if (session) {
621
- session.messages = [];
905
+ if (sessionId) {
906
+ // 清除 Per-Session 消息存储
907
+ const messageStore = this._sessionMessageStores.get(sessionId);
908
+ if (messageStore) {
909
+ messageStore.messages = [];
910
+ messageStore.historyLoaded = false;
911
+ messageStore.compressionState = {
912
+ lastCompressedAt: null,
913
+ lastTokenCount: 0,
914
+ count: 0,
915
+ };
916
+ }
917
+ // 清除 SessionContext 中的历史
918
+ if (this.agent?.framework) {
919
+ try {
920
+ const sessionCtx = this.agent.framework.getSessionContext(sessionId);
921
+ if (sessionCtx) {
922
+ sessionCtx.clearMessages();
622
923
  }
924
+ } catch (err) {
925
+ // 忽略错误
623
926
  }
624
- } catch (err) {
625
- // 忽略错误
626
927
  }
928
+ } else {
929
+ // 清除所有 Per-Session 消息存储
930
+ this._sessionMessageStores.clear();
627
931
  }
932
+ this._compressionCount = 0;
628
933
  return this;
629
934
  }
630
935
 
@@ -653,26 +958,30 @@ ${truncatedContent}${truncatedNote}
653
958
  }
654
959
 
655
960
  /**
656
- * 发送消息(非流式)
961
+ * 发送消息(非流式)- 使用队列机制
657
962
  * @param {string|Object} message - 消息
658
963
  * @param {Object} options - 选项
659
964
  */
660
965
  async chat(message, options = {}) {
661
966
  const sessionId = options.sessionId || null;
662
- const context = { sessionId, isStream: false };
967
+ return this.enqueue(sessionId, message, options);
968
+ }
969
+
970
+ /**
971
+ * 执行实际聊天逻辑
972
+ * @param {string|Object} message - 消息
973
+ * @param {Object} options - 选项
974
+ * @private
975
+ */
976
+ async _doChat(message, options = {}) {
977
+ const sessionId = options.sessionId || null;
663
978
  const framework = this.agent.framework;
664
- const self = this; // 保存引用用于回调
665
979
 
666
- try {
667
- // session 加载聊天历史
668
- if (sessionId) {
669
- const savedMessages = this._loadHistoryFromSession(sessionId);
670
- if (savedMessages.length > 0) {
671
- this._messages = savedMessages;
672
- logger.info(`Loaded ${savedMessages.length} messages from session ${sessionId}`);
673
- }
674
- }
980
+ // 获取 Per-Session 的消息存储(自动从 session 加载)
981
+ const messageStore = this._getSessionMessageStore(sessionId);
982
+ const messages = messageStore.messages;
675
983
 
984
+ try {
676
985
  // 关键:每次 chat 调用时刷新系统提示词,确保包含最新的工具/技能描述
677
986
  // 解决上下文过长时 LLM 不调用工具的问题
678
987
  this._systemPrompt = this.agent._buildSystemPrompt();
@@ -681,11 +990,10 @@ ${truncatedContent}${truncatedNote}
681
990
 
682
991
  const userMessage =
683
992
  typeof message === 'string' ? { role: 'user', content: message } : message;
684
- //console.log('System Prompt:', this._systemPrompt);
685
- this._messages.push(userMessage);
993
+ messages.push(userMessage);
686
994
 
687
995
  // 检查是否需要压缩上下文(包括工具定义)
688
- const messagesTokens = this._countMessagesTokens(this._messages);
996
+ const messagesTokens = this._countMessagesTokens(messages);
689
997
  const toolsTokens = this._countToolsTokens();
690
998
  const systemPromptTokens = this._countTokens(this._systemPrompt);
691
999
  const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
@@ -695,55 +1003,75 @@ ${truncatedContent}${truncatedNote}
695
1003
  logger.info(
696
1004
  `Context large (${totalTokens}/${this._maxContextTokens} tokens = msgs:${messagesTokens} + tools:${toolsTokens} + sys:${systemPromptTokens}), compressing...`
697
1005
  );
698
- // 使用带超时控制的压缩
699
- await this._compressContext();
1006
+ // 使用带超时控制的压缩(传入 messages 引用)
1007
+ await this._compressContext(sessionId, messages, messageStore);
700
1008
  }
701
1009
 
702
1010
  const maxSteps = options.maxSteps || this._maxSteps;
703
1011
  const tools = this._getAITools(tool);
704
1012
 
1013
+ // 准备传给 agent 的消息
1014
+ const prepareStepChainStream = async ({ stepNumber, messages }) => {
1015
+ try {
1016
+ // 1. 验证消息格式
1017
+ if (!Array.isArray(messages)) {
1018
+ logger.warn('prepareStep received non-array messages');
1019
+ return messages;
1020
+ }
1021
+
1022
+ // 2. 消息数量超过阈值才修剪
1023
+ if (messages.length <= 50) {
1024
+ return messages;
1025
+ }
1026
+
1027
+ // 3. 保留配对完整的消息
1028
+ const pruned = this._prepareMessagesForAI(messages);
1029
+ return pruned;
1030
+ } catch (err) {
1031
+ logger.error('prepareStep error:', err.message);
1032
+ return messages; // 出错时返回原消息
1033
+ }
1034
+ };
1035
+
705
1036
  if (!this._aiClient) {
706
1037
  throw new Error('AI client not configured.');
707
1038
  }
708
1039
 
709
- // 准备传给 agent 的消息
710
- // ToolLoopAgent 会自动处理消息格式,不需要手动 prune
1040
+ // 不使用 prepareStep,消息格式已在 _saveHistoryToSession 中处理
711
1041
  const agent = new ToolLoopAgent({
712
1042
  model: this._aiClient,
713
1043
  instructions: this._systemPrompt,
714
1044
  tools: tools,
715
- //stopWhen: stepCountIs(maxSteps),
716
- prepareStep: async ({ stepNumber, messages }) =>
717
- this._prepareStepForPruning(stepNumber, messages),
1045
+ prepareStep: prepareStepChainStream,
718
1046
  });
719
1047
 
720
- // 使用 runWithContext 让工具执行时能获取 sessionId
721
- const result = await framework.runWithContext(context, async () => {
722
- return agent.generate({ messages: this._messages, ...this.providerOptions });
1048
+ // 使用 runInSession 让工具执行时能获取 sessionId(Per-Session 隔离)
1049
+ const result = await framework.runInSession(sessionId, {}, async () => {
1050
+ return agent.generate({ messages, ...this.providerOptions });
723
1051
  });
724
1052
 
725
- this._messages.push(...result.response.messages);
1053
+ messages.push(...result.response.messages);
1054
+
1055
+ // 触发 agent:message 事件,让 memory 插件可以自动提取记忆
1056
+ const userMsg = messages[messages.length - result.response.messages.length - 1];
1057
+ this.emit('message', { userMessage: userMsg, assistantResponse: result.text });
726
1058
 
727
1059
  // 生成后检查:如果消息太长,下次需要压缩
728
- const afterTokens = this._countMessagesTokens(this._messages);
1060
+ const afterTokens = this._countMessagesTokens(messages);
729
1061
  if (afterTokens > this._maxContextTokens * 0.8) {
730
1062
  logger.info(`After generation: ${afterTokens} tokens, will compress on next turn`);
731
1063
  }
732
1064
 
733
- // 保存聊天历史到 session
734
- if (sessionId) {
735
- this._saveHistoryToSession(sessionId, this._messages);
736
- }
737
-
738
1065
  return {
739
1066
  success: true,
740
1067
  message: result.text || '',
741
1068
  stepCount: result.stepCount || 1,
742
1069
  };
743
- } catch (err) {
744
- this.emit('error', { error: err.message });
745
- // 抛出错误而不是返回错误响应,让 Agent 能捕获
746
- throw err;
1070
+ } finally {
1071
+ // 校验并修复消息中的不完整工具调用
1072
+ this._validateToolCalls(messages);
1073
+ // 确保保存聊天历史到 session(无论成功还是失败)
1074
+ messageStore.save();
747
1075
  }
748
1076
  }
749
1077
 
@@ -754,19 +1082,13 @@ ${truncatedContent}${truncatedNote}
754
1082
  */
755
1083
  async *chatStream(message, options = {}) {
756
1084
  const sessionId = options.sessionId || null;
757
- const context = { sessionId, isStream: true };
758
1085
  const framework = this.agent.framework;
759
1086
 
760
- try {
761
- //从 session 加载聊天历史
762
- if (sessionId) {
763
- const savedMessages = this._loadHistoryFromSession(sessionId);
764
- if (savedMessages.length > 0) {
765
- this._messages = savedMessages;
766
- logger.info(`Loaded ${savedMessages.length} messages from session ${sessionId}`);
767
- }
768
- }
1087
+ // 获取 Per-Session 的消息存储
1088
+ const messageStore = this._getSessionMessageStore(sessionId);
1089
+ const messages = messageStore.messages;
769
1090
 
1091
+ try {
770
1092
  // 关键:每次 chat 调用时刷新系统提示词,确保包含最新的工具/技能描述
771
1093
  // 解决上下文过长时 LLM 不调用工具的问题
772
1094
  this._systemPrompt = this.agent._buildSystemPrompt();
@@ -775,23 +1097,21 @@ ${truncatedContent}${truncatedNote}
775
1097
 
776
1098
  const userMessage =
777
1099
  typeof message === 'string' ? { role: 'user', content: message } : message;
778
- //console.log('System Prompt:', this._systemPrompt);
779
- this._messages.push(userMessage);
1100
+ messages.push(userMessage);
780
1101
 
781
1102
  // 检查是否需要压缩上下文(包括工具定义)
782
- const messagesTokens = this._countMessagesTokens(this._messages);
1103
+ const messagesTokens = this._countMessagesTokens(messages);
783
1104
  const toolsTokens = this._countToolsTokens();
784
1105
  const systemPromptTokens = this._countTokens(this._systemPrompt);
785
1106
  const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
786
1107
  const limit = this._maxContextTokens * 0.7; // 降低到 70%
787
- //console.log(messagesTokens,toolsTokens,systemPromptTokens,totalTokens,this._maxContextTokens)
788
1108
  // 对于流式调用,如果上下文太大,先压缩再开始
789
1109
  if (totalTokens > limit) {
790
1110
  logger.info(
791
1111
  `Context large (${totalTokens}/${this._maxContextTokens} tokens), compressing...`
792
1112
  );
793
1113
  // 流式调用时等待压缩完成(使用带超时控制的压缩)
794
- await this._compressContext();
1114
+ await this._compressContext(sessionId, messages, messageStore);
795
1115
  }
796
1116
 
797
1117
  const maxSteps = options.maxSteps || this._maxSteps;
@@ -801,20 +1121,39 @@ ${truncatedContent}${truncatedNote}
801
1121
  }
802
1122
 
803
1123
  // 准备传给 agent 的消息
804
- // ToolLoopAgent 会自动处理消息格式,不需要手动 prune
1124
+ const prepareStepChainStream = async ({ stepNumber, messages }) => {
1125
+ try {
1126
+ // 1. 验证消息格式
1127
+ if (!Array.isArray(messages)) {
1128
+ logger.warn('prepareStep received non-array messages');
1129
+ return messages;
1130
+ }
1131
+
1132
+ // 2. 消息数量超过阈值才修剪
1133
+ if (messages.length <= 50) {
1134
+ return messages;
1135
+ }
1136
+
1137
+ // 3. 保留配对完整的消息
1138
+ const pruned = this._prepareMessagesForAI(messages);
1139
+ return pruned;
1140
+ } catch (err) {
1141
+ logger.error('prepareStep error:', err.message);
1142
+ return messages; // 出错时返回原消息
1143
+ }
1144
+ };
1145
+
805
1146
  const agent = new ToolLoopAgent({
806
1147
  model: this._aiClient,
807
1148
  instructions: this._systemPrompt,
808
1149
  tools: tools,
809
- //stopWhen: stepCountIs(maxSteps),
810
- prepareStep: async ({ stepNumber, messages }) =>
811
- this._prepareStepForPruning(stepNumber, messages),
1150
+ prepareStep: prepareStepChainStream,
812
1151
  });
813
1152
 
814
- // 使用 runWithContext 让工具执行时能获取 sessionId(支持并行)
815
- const result = await framework.runWithContext(context, async () => {
1153
+ // 使用 runInSession 让工具执行时能获取 sessionId(Per-Session 隔离)
1154
+ const result = await framework.runInSession(sessionId, {}, async () => {
816
1155
  return agent.stream({
817
- messages: this._messages,
1156
+ messages,
818
1157
  ...this.providerOptions,
819
1158
  });
820
1159
  });
@@ -842,18 +1181,106 @@ ${truncatedContent}${truncatedNote}
842
1181
  }
843
1182
 
844
1183
  const finishMessages = (await result.response).messages;
845
- this._messages.push(...finishMessages);
1184
+ messages.push(...finishMessages);
846
1185
 
847
- // 保存聊天历史到 session(使用实际传给 agent 的消息一致)
848
- if (sessionId) {
849
- this._saveHistoryToSession(sessionId, this._messages);
850
- }
1186
+ // 触发 agent:message 事件,让 memory 插件可以自动提取记忆
1187
+ const userMsg = messages[messages.length - finishMessages.length - 1];
1188
+ this.emit('message', { userMessage: userMsg, assistantResponse: fullText });
851
1189
  } catch (err) {
852
1190
  this.emit('error', { error: err.message });
853
1191
  yield { type: 'error', error: err.message };
1192
+ } finally {
1193
+ // 校验并修复消息中的不完整工具调用
1194
+ this._validateToolCalls(messages);
1195
+ // 确保保存聊天历史到 session(无论成功还是失败)
1196
+ messageStore.save();
854
1197
  }
855
1198
  }
856
1199
 
1200
+ /**
1201
+ * 为 AI SDK prepareStep 准备消息
1202
+ * 确保 tool call 和 tool result 正确配对
1203
+ * @param {Array} messages - 消息列表
1204
+ * @returns {Array} 过滤后的消息数组
1205
+ * @private
1206
+ */
1207
+ _prepareMessagesForAI(messages) {
1208
+ if (messages.length <= 10) {
1209
+ return messages;
1210
+ }
1211
+
1212
+ // 收集所有 assistant 的 tool-call
1213
+ const assistantToolCalls = new Map(); // toolCallId -> message index
1214
+ for (let i = 0; i < messages.length; i++) {
1215
+ const msg = messages[i];
1216
+ if (msg.role === 'assistant' && Array.isArray(msg.content)) {
1217
+ for (const item of msg.content) {
1218
+ if (item.type === 'tool-call' && item.toolCallId) {
1219
+ assistantToolCalls.set(item.toolCallId, i);
1220
+ }
1221
+ }
1222
+ }
1223
+ }
1224
+
1225
+ // 收集配对的 tool results
1226
+ const pairedToolResults = new Set(); // 保留的 tool result indices
1227
+ for (let i = 0; i < messages.length; i++) {
1228
+ const msg = messages[i];
1229
+ if (msg.role === 'tool' && Array.isArray(msg.content)) {
1230
+ for (const item of msg.content) {
1231
+ if (item.type === 'tool-result' && item.toolCallId) {
1232
+ if (assistantToolCalls.has(item.toolCallId)) {
1233
+ pairedToolResults.add(i);
1234
+ }
1235
+ }
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ // 过滤:保留最近的消息,但确保 tool call/result 配对完整
1241
+ const recentCount = Math.max(20, messages.length - 10);
1242
+ const minIndexToKeep = messages.length - recentCount;
1243
+
1244
+ const result = [];
1245
+ for (let i = minIndexToKeep; i < messages.length; i++) {
1246
+ const msg = messages[i];
1247
+
1248
+ if (msg.role === 'tool') {
1249
+ // 只保留有配对的 tool messages
1250
+ if (pairedToolResults.has(i)) {
1251
+ result.push(msg);
1252
+ }
1253
+ } else if (msg.role === 'assistant') {
1254
+ // 检查是否有未配对的 tool-call
1255
+ if (Array.isArray(msg.content)) {
1256
+ const hasUnpairedToolCall = msg.content.some(
1257
+ (item) => item.type === 'tool-call' && !assistantToolCalls.has(item.toolCallId)
1258
+ );
1259
+ if (hasUnpairedToolCall) {
1260
+ // 过滤掉未配对的 tool-call
1261
+ const filteredContent = msg.content.filter((item) => {
1262
+ if (item.type === 'tool-call') {
1263
+ return assistantToolCalls.has(item.toolCallId);
1264
+ }
1265
+ return true;
1266
+ });
1267
+ if (filteredContent.length > 0) {
1268
+ result.push({ ...msg, content: filteredContent });
1269
+ }
1270
+ } else {
1271
+ result.push(msg);
1272
+ }
1273
+ } else {
1274
+ result.push(msg);
1275
+ }
1276
+ } else {
1277
+ result.push(msg);
1278
+ }
1279
+ }
1280
+
1281
+ return result;
1282
+ }
1283
+
857
1284
  /**
858
1285
  * 修剪消息确保配对的 tool call/result 不被拆分
859
1286
  * @param {number} stepNumber - 当前步骤号
@@ -880,11 +1307,16 @@ ${truncatedContent}${truncatedNote}
880
1307
  }
881
1308
  }
882
1309
  }
883
- if (msg.role === 'tool' && msg.toolCallId) {
884
- if (!toolResultIndices.has(msg.toolCallId)) {
885
- toolResultIndices.set(msg.toolCallId, []);
1310
+ if (msg.role === 'tool' && Array.isArray(msg.content)) {
1311
+ // 遍历 tool message 中的所有 tool-results
1312
+ for (const item of msg.content) {
1313
+ if (item && item.type === 'tool-result' && item.toolCallId) {
1314
+ if (!toolResultIndices.has(item.toolCallId)) {
1315
+ toolResultIndices.set(item.toolCallId, []);
1316
+ }
1317
+ toolResultIndices.get(item.toolCallId).push(i);
1318
+ }
886
1319
  }
887
- toolResultIndices.get(msg.toolCallId).push(i);
888
1320
  }
889
1321
  }
890
1322
 
@@ -917,6 +1349,49 @@ ${truncatedContent}${truncatedNote}
917
1349
  return { messages: pruned };
918
1350
  }
919
1351
 
1352
+ /**
1353
+ * 校验并修复消息中的工具调用参数
1354
+ * 移除不完整的 JSON(如只有 "{" )的工具调用
1355
+ * @param {Array} messages - 消息列表
1356
+ * @private
1357
+ */
1358
+ _validateToolCalls(messages) {
1359
+ let fixedCount = 0;
1360
+
1361
+ for (const msg of messages) {
1362
+ if (msg.role !== 'assistant' || !Array.isArray(msg.content)) {
1363
+ continue;
1364
+ }
1365
+
1366
+ for (const item of msg.content) {
1367
+ if (item.type !== 'tool-call') {
1368
+ continue;
1369
+ }
1370
+
1371
+ const input = item.input;
1372
+ if (typeof input !== 'string') {
1373
+ continue;
1374
+ }
1375
+
1376
+ // 检查 input 是否是有效的 JSON(不是不完整的)
1377
+ const trimmed = input.trim();
1378
+ if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
1379
+ // 不完整的 JSON,移除这个 tool-call
1380
+ item.type = 'text';
1381
+ item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
1382
+ delete item.toolCallId;
1383
+ delete item.toolName;
1384
+ delete item.input;
1385
+ fixedCount++;
1386
+ }
1387
+ }
1388
+ }
1389
+
1390
+ if (fixedCount > 0) {
1391
+ logger.info(`Fixed ${fixedCount} incomplete tool calls`);
1392
+ }
1393
+ }
1394
+
920
1395
  /**
921
1396
  * 获取 AI SDK 格式的工具
922
1397
  * AI SDK 6.x 需要对象形式 { toolName: tool }
@@ -943,7 +1418,10 @@ ${truncatedContent}${truncatedNote}
943
1418
  try {
944
1419
  const result = await toolDef.execute(cleanedArgs, this.agent.framework);
945
1420
  this.emit('tool-result', { name: toolName, args: cleanedArgs, result });
946
- return result;
1421
+ if (result !== null && typeof result === 'object') {
1422
+ return JSON.stringify(result);
1423
+ }
1424
+ return String(result);
947
1425
  } catch (err) {
948
1426
  this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });
949
1427
  return { error: err.message };
@@ -1001,43 +1479,12 @@ ${truncatedContent}${truncatedNote}
1001
1479
  return cleaned;
1002
1480
  }
1003
1481
 
1004
- /**
1005
- * 清理消息格式
1006
- * @private
1007
- */
1008
- _cleanMessages(messages) {
1009
- return messages.map((msg) => {
1010
- if (!msg || typeof msg !== 'object') {
1011
- return { role: 'user', content: '' };
1012
- }
1013
-
1014
- const cleaned = {
1015
- role: msg.role || 'user',
1016
- };
1017
-
1018
- if (Array.isArray(msg.content)) {
1019
- cleaned.content = msg.content;
1020
- } else if (typeof msg.content === 'string') {
1021
- cleaned.content = msg.content;
1022
- } else if (typeof msg.content === 'object' && msg.content !== null) {
1023
- // 对象类型的 content(如 tool result),转为字符串
1024
- cleaned.content =
1025
- typeof msg.content === 'object' && msg.content.content !== undefined
1026
- ? String(msg.content.content)
1027
- : JSON.stringify(msg.content);
1028
- } else {
1029
- cleaned.content = msg.text || msg.input || '';
1030
- }
1031
-
1032
- return cleaned;
1033
- });
1034
- }
1035
-
1036
1482
  /**
1037
1483
  * 销毁
1038
1484
  */
1039
1485
  destroy() {
1040
- this._messages = [];
1486
+ this._sessionMessageStores.clear();
1487
+ this._sessionMessageStores = new Map();
1041
1488
  this._tools.clear();
1042
1489
  this._encoder = null;
1043
1490
  this.removeAllListeners();