foliko 1.0.87 → 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.
- package/.agent/data/default.json +3 -108
- package/.agent/data/plugins-state.json +34 -1
- package/.agent/mcp_config.json +0 -1
- package/.agent/memory/core.md +1 -0
- package/.agent/memory/project/mnn93ogy-ypjn27.md +9 -0
- package/.agent/memory/project/mnn98fqy-5nhc1u.md +25 -0
- package/.agent/memory/user/mnm67t9m-x8rekk.md +9 -0
- package/.agent/memory/user/mnn5mmqh-w6aktx.md +11 -0
- package/.agent/memory/user/mnnbfhhn-dk1bd1.md +22 -0
- package/.agent/plugins/__pycache__/file_writer.cpython-312.pyc +0 -0
- package/.agent/plugins/poster-plugin/README.md +304 -0
- package/.agent/plugins/poster-plugin/fonts/PatuaOne-Regular.ttf +0 -0
- package/.agent/plugins/poster-plugin/fonts//345/276/256/350/275/257/351/233/205/351/273/221.ttf +0 -0
- 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
- package/.agent/plugins/poster-plugin/index.js +13 -0
- package/.agent/plugins/poster-plugin/package.json +28 -0
- package/.agent/plugins/poster-plugin/src/canvas.js +161 -0
- package/.agent/plugins/poster-plugin/src/components/arrow.js +84 -0
- package/.agent/plugins/poster-plugin/src/components/avatar.js +71 -0
- package/.agent/plugins/poster-plugin/src/components/badge.js +85 -0
- package/.agent/plugins/poster-plugin/src/components/card.js +88 -0
- package/.agent/plugins/poster-plugin/src/components/chart.js +127 -0
- package/.agent/plugins/poster-plugin/src/components/chip.js +88 -0
- package/.agent/plugins/poster-plugin/src/components/columns.js +107 -0
- package/.agent/plugins/poster-plugin/src/components/cta.js +85 -0
- package/.agent/plugins/poster-plugin/src/components/divider.js +55 -0
- package/.agent/plugins/poster-plugin/src/components/feature.js +85 -0
- package/.agent/plugins/poster-plugin/src/components/featureGrid.js +112 -0
- package/.agent/plugins/poster-plugin/src/components/grid.js +118 -0
- package/.agent/plugins/poster-plugin/src/components/imageFrame.js +155 -0
- package/.agent/plugins/poster-plugin/src/components/index.js +62 -0
- package/.agent/plugins/poster-plugin/src/components/listItem.js +146 -0
- package/.agent/plugins/poster-plugin/src/components/notification.js +123 -0
- package/.agent/plugins/poster-plugin/src/components/progress.js +79 -0
- package/.agent/plugins/poster-plugin/src/components/progressCircle.js +117 -0
- package/.agent/plugins/poster-plugin/src/components/quote.js +97 -0
- package/.agent/plugins/poster-plugin/src/components/rating.js +85 -0
- package/.agent/plugins/poster-plugin/src/components/star.js +70 -0
- package/.agent/plugins/poster-plugin/src/components/statCard.js +105 -0
- package/.agent/plugins/poster-plugin/src/components/stepper.js +118 -0
- package/.agent/plugins/poster-plugin/src/components/table.js +159 -0
- package/.agent/plugins/poster-plugin/src/components/tagCloud.js +78 -0
- package/.agent/plugins/poster-plugin/src/components/timeline.js +105 -0
- package/.agent/plugins/poster-plugin/src/components/watermark.js +52 -0
- package/.agent/plugins/poster-plugin/src/composer.js +1904 -0
- package/.agent/plugins/poster-plugin/src/elements/artText.js +60 -0
- package/.agent/plugins/poster-plugin/src/elements/background.js +52 -0
- package/.agent/plugins/poster-plugin/src/elements/circle.js +31 -0
- package/.agent/plugins/poster-plugin/src/elements/image.js +71 -0
- package/.agent/plugins/poster-plugin/src/elements/index.js +26 -0
- package/.agent/plugins/poster-plugin/src/elements/line.js +23 -0
- package/.agent/plugins/poster-plugin/src/elements/polygon.js +32 -0
- package/.agent/plugins/poster-plugin/src/elements/rectangle.js +32 -0
- package/.agent/plugins/poster-plugin/src/elements/svg.js +92 -0
- package/.agent/plugins/poster-plugin/src/elements/text.js +38 -0
- package/.agent/plugins/poster-plugin/src/fonts.js +118 -0
- package/.agent/plugins/poster-plugin/src/index.js +1659 -0
- package/.agent/plugins/poster-plugin/src/presets.js +36 -0
- package/.agent/plugins/poster-plugin/src/templates/business.js +60 -0
- package/.agent/plugins/poster-plugin/src/templates/gradient.js +64 -0
- package/.agent/plugins/poster-plugin/src/templates/index.js +43 -0
- package/.agent/plugins/poster-plugin/src/templates/modern.js +69 -0
- package/.agent/plugins/poster-plugin/src/templates/simple.js +58 -0
- package/.agent/plugins/poster-plugin/src/templates/social.js +62 -0
- package/.agent/plugins/poster-plugin/src/templates/tech.js +84 -0
- package/.agent/sessions/cli_default.json +24265 -0
- package/.agent/weixin.json +6 -0
- package/.claude/settings.local.json +5 -8
- package/CLAUDE.md +144 -108
- package/docs/CONTEXT_DESIGN.md +1596 -0
- package/examples/test-concurrent-chat.js +60 -60
- package/output/beef-love-poster.png +0 -0
- package/package.json +2 -2
- package/plugins/default-plugins.js +2 -1
- package/plugins/extension-executor-plugin.js +11 -0
- package/plugins/memory-plugin.js +984 -0
- package/plugins/session-plugin.js +57 -1
- package/plugins/weixin-plugin.js +24 -22
- package/skills/poster-guide/SKILL.md +743 -0
- package/skills/python-plugin-dev/SKILL.md +238 -238
- package/skills/skill-guide/SKILL.md +130 -108
- package/src/capabilities/skill-manager.js +99 -0
- package/src/core/agent-chat.js +538 -138
- package/src/core/agent-context.js +188 -0
- package/src/core/agent.js +6 -2
- package/src/core/context-manager.js +283 -0
- package/src/core/framework.js +264 -3
- package/src/core/plugin-manager.js +79 -2
- package/src/core/request-context.js +98 -0
- package/src/core/session-context.js +341 -0
- package/src/core/session-storage.js +274 -0
- package/src/executors/mcp-executor.js +2 -2
- package/src/utils/index.js +239 -67
- package/src/utils/plugin-helpers.js +17 -0
- package//346/265/267/346/212/245/346/217/222/344/273/266.md +621 -0
- package/.agent/plugins/__pycache__/test_plugin.cpython-312.pyc +0 -0
- package/.agent/plugins/temp-repo/LICENSE +0 -201
- package/.agent/plugins/temp-repo/puppeteer-plugin/README.md +0 -147
- package/.agent/plugins/temp-repo/puppeteer-plugin/index.js +0 -1418
- package/.agent/plugins/temp-repo/puppeteer-plugin/package.json +0 -9
- package/.agent/plugins/test_plugin.py +0 -304
- package/examples/test-chat-debug.js +0 -102
- package/examples/test-chat-result.js +0 -76
- package/examples/test-chat-stream-diff.js +0 -63
package/src/core/agent-chat.js
CHANGED
|
@@ -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
|
|
|
@@ -127,10 +145,58 @@ class AgentChatHandler extends EventEmitter {
|
|
|
127
145
|
// Per-Session 队列管理
|
|
128
146
|
this._sessionQueues = new Map(); // sessionId -> Queue of {message, options, resolve, reject}
|
|
129
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
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
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);
|
|
130
196
|
}
|
|
131
197
|
|
|
132
198
|
/**
|
|
133
|
-
* 从
|
|
199
|
+
* 从 SessionContext 加载聊天历史
|
|
134
200
|
* @param {string} sessionId - 会话 ID
|
|
135
201
|
* @returns {Array} 消息数组
|
|
136
202
|
* @private
|
|
@@ -139,11 +205,10 @@ class AgentChatHandler extends EventEmitter {
|
|
|
139
205
|
if (!sessionId || !this.agent?.framework) return [];
|
|
140
206
|
|
|
141
207
|
try {
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
208
|
+
const sessionCtx = this.agent.framework.getOrCreateSessionContext(sessionId);
|
|
209
|
+
if (!sessionCtx) return [];
|
|
144
210
|
|
|
145
|
-
const messages =
|
|
146
|
-
// 直接返回,保存时已清理,数据应该是干净的
|
|
211
|
+
const messages = sessionCtx.getMessages();
|
|
147
212
|
return messages || [];
|
|
148
213
|
} catch (err) {
|
|
149
214
|
// 忽略加载错误
|
|
@@ -152,7 +217,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
152
217
|
}
|
|
153
218
|
|
|
154
219
|
/**
|
|
155
|
-
* 保存聊天历史到
|
|
220
|
+
* 保存聊天历史到 SessionContext
|
|
156
221
|
* @param {string} sessionId - 会话 ID
|
|
157
222
|
* @param {Array} messages - 消息数组
|
|
158
223
|
* @private
|
|
@@ -161,12 +226,12 @@ class AgentChatHandler extends EventEmitter {
|
|
|
161
226
|
if (!sessionId || !this.agent?.framework || !messages) return;
|
|
162
227
|
|
|
163
228
|
try {
|
|
164
|
-
const
|
|
165
|
-
if (!
|
|
229
|
+
const sessionCtx = this.agent.framework.getOrCreateSessionContext(sessionId);
|
|
230
|
+
if (!sessionCtx) return;
|
|
166
231
|
|
|
167
232
|
// 清理消息格式后整体替换
|
|
168
233
|
const cleanedMessages = prepareMessagesForAPI(messages);
|
|
169
|
-
|
|
234
|
+
sessionCtx.replaceMessages(cleanedMessages);
|
|
170
235
|
} catch (err) {
|
|
171
236
|
// 忽略保存错误
|
|
172
237
|
}
|
|
@@ -287,16 +352,6 @@ class AgentChatHandler extends EventEmitter {
|
|
|
287
352
|
return total;
|
|
288
353
|
}
|
|
289
354
|
|
|
290
|
-
/**
|
|
291
|
-
* 检查是否需要压缩上下文
|
|
292
|
-
* @returns {boolean}
|
|
293
|
-
* @private
|
|
294
|
-
*/
|
|
295
|
-
_shouldCompress() {
|
|
296
|
-
const totalTokens = this._countMessagesTokens(this._messages);
|
|
297
|
-
return totalTokens > this._maxContextTokens * this._compressionThreshold;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
355
|
/**
|
|
301
356
|
* 计算工具定义的 token 数(估算)
|
|
302
357
|
* @returns {number}
|
|
@@ -321,21 +376,6 @@ class AgentChatHandler extends EventEmitter {
|
|
|
321
376
|
return total;
|
|
322
377
|
}
|
|
323
378
|
|
|
324
|
-
/**
|
|
325
|
-
* 检查是否需要压缩(包括工具定义)
|
|
326
|
-
* @returns {boolean}
|
|
327
|
-
* @private
|
|
328
|
-
*/
|
|
329
|
-
_shouldCompressWithTools() {
|
|
330
|
-
const messagesTokens = this._countMessagesTokens(this._messages);
|
|
331
|
-
const toolsTokens = this._countToolsTokens();
|
|
332
|
-
const systemPromptTokens = this._countTokens(this._systemPrompt);
|
|
333
|
-
const total = messagesTokens + toolsTokens + systemPromptTokens;
|
|
334
|
-
|
|
335
|
-
// 如果总token数超过上下文限制的 85%,就压缩
|
|
336
|
-
return total > this._maxContextTokens * 0.85;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
379
|
/**
|
|
340
380
|
* 压缩上下文消息(智能摘要模式,支持超时控制)
|
|
341
381
|
* 策略:
|
|
@@ -343,23 +383,29 @@ class AgentChatHandler extends EventEmitter {
|
|
|
343
383
|
* 2. 否则使用简单裁剪 + 标记
|
|
344
384
|
* 3. 支持超时控制,防止压缩阻塞太久
|
|
345
385
|
* @param {string} sessionId - 会话 ID(用于压缩后同步)
|
|
386
|
+
* @param {Array} messages - 消息数组引用
|
|
387
|
+
* @param {Object} messageStore - 消息存储对象
|
|
346
388
|
* @private
|
|
347
389
|
*/
|
|
348
|
-
async _compressContext(sessionId) {
|
|
390
|
+
async _compressContext(sessionId, messages, messageStore) {
|
|
349
391
|
// 如果已经有压缩在进行,返回现有的 Promise
|
|
350
392
|
if (this._compressionInProgress && this._compressionPromise) {
|
|
351
393
|
logger.debug('Compression already in progress, waiting for it...');
|
|
352
394
|
return this._compressionPromise;
|
|
353
395
|
}
|
|
354
396
|
|
|
355
|
-
if (
|
|
397
|
+
if (messages.length <= this._keepRecentMessages) {
|
|
356
398
|
return;
|
|
357
399
|
}
|
|
358
400
|
|
|
359
401
|
this._compressionInProgress = true;
|
|
360
402
|
|
|
361
403
|
// 创建压缩 Promise,带超时控制
|
|
362
|
-
this._compressionPromise = this._executeCompressionWithTimeout(
|
|
404
|
+
this._compressionPromise = this._executeCompressionWithTimeout(
|
|
405
|
+
sessionId,
|
|
406
|
+
messages,
|
|
407
|
+
messageStore
|
|
408
|
+
).finally(() => {
|
|
363
409
|
this._compressionInProgress = false;
|
|
364
410
|
this._compressionPromise = null;
|
|
365
411
|
});
|
|
@@ -370,15 +416,20 @@ class AgentChatHandler extends EventEmitter {
|
|
|
370
416
|
/**
|
|
371
417
|
* 执行压缩(带超时)
|
|
372
418
|
* @param {string} sessionId - 会话 ID
|
|
419
|
+
* @param {Array} messages - 消息数组引用
|
|
420
|
+
* @param {Object} messageStore - 消息存储对象
|
|
373
421
|
* @private
|
|
374
422
|
*/
|
|
375
|
-
async _executeCompressionWithTimeout(sessionId) {
|
|
423
|
+
async _executeCompressionWithTimeout(sessionId, messages, messageStore) {
|
|
376
424
|
try {
|
|
377
|
-
return await Promise.race([
|
|
425
|
+
return await Promise.race([
|
|
426
|
+
this._doCompress(sessionId, messages, messageStore),
|
|
427
|
+
this._createTimeoutPromise(),
|
|
428
|
+
]);
|
|
378
429
|
} catch (err) {
|
|
379
430
|
logger.warn('Compression failed:', err.message);
|
|
380
431
|
// 压缩失败时使用简单的截断策略
|
|
381
|
-
this._simpleCompress(sessionId);
|
|
432
|
+
this._simpleCompress(sessionId, messages, messageStore);
|
|
382
433
|
}
|
|
383
434
|
}
|
|
384
435
|
|
|
@@ -416,11 +467,13 @@ class AgentChatHandler extends EventEmitter {
|
|
|
416
467
|
/**
|
|
417
468
|
* 执行实际压缩逻辑
|
|
418
469
|
* @param {string} sessionId - 会话 ID
|
|
470
|
+
* @param {Array} messages - 消息数组引用
|
|
471
|
+
* @param {Object} messageStore - 消息存储对象
|
|
419
472
|
* @private
|
|
420
473
|
*/
|
|
421
|
-
async _doCompress(sessionId) {
|
|
422
|
-
const systemMessages =
|
|
423
|
-
const otherMessages =
|
|
474
|
+
async _doCompress(sessionId, messages, messageStore) {
|
|
475
|
+
const systemMessages = messages.filter((m) => m.role === 'system');
|
|
476
|
+
const otherMessages = messages.filter((m) => m.role !== 'system');
|
|
424
477
|
|
|
425
478
|
// 保留最近的 N 条非系统消息
|
|
426
479
|
const recentMessages = otherMessages.slice(-this._keepRecentMessages);
|
|
@@ -448,29 +501,117 @@ class AgentChatHandler extends EventEmitter {
|
|
|
448
501
|
content: summaryContent,
|
|
449
502
|
};
|
|
450
503
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
);
|
|
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
|
+
// 更新压缩状态
|
|
454
587
|
this._compressionCount++;
|
|
455
|
-
|
|
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);
|
|
456
595
|
logger.info(
|
|
457
|
-
`Context compressed (${this._compressionCount} times). Messages: ${
|
|
596
|
+
`Context compressed (${this._compressionCount} times). Messages: ${messages.length}, Est. tokens: ${totalTokens}`
|
|
458
597
|
);
|
|
459
598
|
|
|
460
599
|
// 压缩后立即同步到 session
|
|
461
600
|
if (sessionId) {
|
|
462
|
-
this._saveHistoryToSession(sessionId,
|
|
601
|
+
this._saveHistoryToSession(sessionId, messages);
|
|
463
602
|
}
|
|
464
603
|
}
|
|
465
604
|
|
|
466
605
|
/**
|
|
467
606
|
* 简单压缩(当 AI 压缩失败时使用)
|
|
468
607
|
* @param {string} sessionId - 会话 ID
|
|
608
|
+
* @param {Array} messages - 消息数组引用
|
|
609
|
+
* @param {Object} messageStore - 消息存储对象
|
|
469
610
|
* @private
|
|
470
611
|
*/
|
|
471
|
-
_simpleCompress(sessionId) {
|
|
472
|
-
const systemMessages =
|
|
473
|
-
const otherMessages =
|
|
612
|
+
_simpleCompress(sessionId, messages, messageStore) {
|
|
613
|
+
const systemMessages = messages.filter((m) => m.role === 'system');
|
|
614
|
+
const otherMessages = messages.filter((m) => m.role !== 'system');
|
|
474
615
|
const recentMessages = otherMessages.slice(-this._keepRecentMessages);
|
|
475
616
|
const compressedCount = otherMessages.length - this._keepRecentMessages;
|
|
476
617
|
|
|
@@ -481,18 +622,102 @@ class AgentChatHandler extends EventEmitter {
|
|
|
481
622
|
content: summaryContent,
|
|
482
623
|
};
|
|
483
624
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
);
|
|
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
|
+
// 更新压缩状态
|
|
487
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
|
+
}
|
|
488
713
|
|
|
489
714
|
logger.info(
|
|
490
|
-
`Context simple compressed (${this._compressionCount} times). Messages: ${
|
|
715
|
+
`Context simple compressed (${this._compressionCount} times). Messages: ${messages.length}`
|
|
491
716
|
);
|
|
492
717
|
|
|
493
718
|
// 压缩后立即同步到 session
|
|
494
719
|
if (sessionId) {
|
|
495
|
-
this._saveHistoryToSession(sessionId,
|
|
720
|
+
this._saveHistoryToSession(sessionId, messages);
|
|
496
721
|
}
|
|
497
722
|
}
|
|
498
723
|
|
|
@@ -677,23 +902,34 @@ ${truncatedContent}${truncatedNote}
|
|
|
677
902
|
* @param {string} sessionId - 可选,指定 session 则同时清除 session 中的历史
|
|
678
903
|
*/
|
|
679
904
|
clearHistory(sessionId) {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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();
|
|
691
923
|
}
|
|
924
|
+
} catch (err) {
|
|
925
|
+
// 忽略错误
|
|
692
926
|
}
|
|
693
|
-
} catch (err) {
|
|
694
|
-
// 忽略错误
|
|
695
927
|
}
|
|
928
|
+
} else {
|
|
929
|
+
// 清除所有 Per-Session 消息存储
|
|
930
|
+
this._sessionMessageStores.clear();
|
|
696
931
|
}
|
|
932
|
+
this._compressionCount = 0;
|
|
697
933
|
return this;
|
|
698
934
|
}
|
|
699
935
|
|
|
@@ -739,19 +975,13 @@ ${truncatedContent}${truncatedNote}
|
|
|
739
975
|
*/
|
|
740
976
|
async _doChat(message, options = {}) {
|
|
741
977
|
const sessionId = options.sessionId || null;
|
|
742
|
-
const context = { sessionId, isStream: false };
|
|
743
978
|
const framework = this.agent.framework;
|
|
744
979
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const savedMessages = this._loadHistoryFromSession(sessionId);
|
|
749
|
-
if (savedMessages.length > 0) {
|
|
750
|
-
this._messages = savedMessages;
|
|
751
|
-
logger.info(`Loaded ${savedMessages.length} messages from session ${sessionId}`);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
980
|
+
// 获取 Per-Session 的消息存储(自动从 session 加载)
|
|
981
|
+
const messageStore = this._getSessionMessageStore(sessionId);
|
|
982
|
+
const messages = messageStore.messages;
|
|
754
983
|
|
|
984
|
+
try {
|
|
755
985
|
// 关键:每次 chat 调用时刷新系统提示词,确保包含最新的工具/技能描述
|
|
756
986
|
// 解决上下文过长时 LLM 不调用工具的问题
|
|
757
987
|
this._systemPrompt = this.agent._buildSystemPrompt();
|
|
@@ -760,11 +990,10 @@ ${truncatedContent}${truncatedNote}
|
|
|
760
990
|
|
|
761
991
|
const userMessage =
|
|
762
992
|
typeof message === 'string' ? { role: 'user', content: message } : message;
|
|
763
|
-
|
|
764
|
-
this._messages.push(userMessage);
|
|
993
|
+
messages.push(userMessage);
|
|
765
994
|
|
|
766
995
|
// 检查是否需要压缩上下文(包括工具定义)
|
|
767
|
-
const messagesTokens = this._countMessagesTokens(
|
|
996
|
+
const messagesTokens = this._countMessagesTokens(messages);
|
|
768
997
|
const toolsTokens = this._countToolsTokens();
|
|
769
998
|
const systemPromptTokens = this._countTokens(this._systemPrompt);
|
|
770
999
|
const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
|
|
@@ -774,55 +1003,75 @@ ${truncatedContent}${truncatedNote}
|
|
|
774
1003
|
logger.info(
|
|
775
1004
|
`Context large (${totalTokens}/${this._maxContextTokens} tokens = msgs:${messagesTokens} + tools:${toolsTokens} + sys:${systemPromptTokens}), compressing...`
|
|
776
1005
|
);
|
|
777
|
-
//
|
|
778
|
-
await this._compressContext(sessionId);
|
|
1006
|
+
// 使用带超时控制的压缩(传入 messages 引用)
|
|
1007
|
+
await this._compressContext(sessionId, messages, messageStore);
|
|
779
1008
|
}
|
|
780
1009
|
|
|
781
1010
|
const maxSteps = options.maxSteps || this._maxSteps;
|
|
782
1011
|
const tools = this._getAITools(tool);
|
|
783
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
|
+
|
|
784
1036
|
if (!this._aiClient) {
|
|
785
1037
|
throw new Error('AI client not configured.');
|
|
786
1038
|
}
|
|
787
1039
|
|
|
788
|
-
//
|
|
789
|
-
// ToolLoopAgent 会自动处理消息格式,不需要手动 prune
|
|
1040
|
+
// 不使用 prepareStep,消息格式已在 _saveHistoryToSession 中处理
|
|
790
1041
|
const agent = new ToolLoopAgent({
|
|
791
1042
|
model: this._aiClient,
|
|
792
1043
|
instructions: this._systemPrompt,
|
|
793
1044
|
tools: tools,
|
|
794
|
-
|
|
795
|
-
prepareStep: async ({ stepNumber, messages }) =>
|
|
796
|
-
this._prepareStepForPruning(stepNumber, messages),
|
|
1045
|
+
prepareStep: prepareStepChainStream,
|
|
797
1046
|
});
|
|
798
1047
|
|
|
799
|
-
// 使用
|
|
800
|
-
const result = await framework.
|
|
801
|
-
return agent.generate({ messages
|
|
1048
|
+
// 使用 runInSession 让工具执行时能获取 sessionId(Per-Session 隔离)
|
|
1049
|
+
const result = await framework.runInSession(sessionId, {}, async () => {
|
|
1050
|
+
return agent.generate({ messages, ...this.providerOptions });
|
|
802
1051
|
});
|
|
803
1052
|
|
|
804
|
-
|
|
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 });
|
|
805
1058
|
|
|
806
1059
|
// 生成后检查:如果消息太长,下次需要压缩
|
|
807
|
-
const afterTokens = this._countMessagesTokens(
|
|
1060
|
+
const afterTokens = this._countMessagesTokens(messages);
|
|
808
1061
|
if (afterTokens > this._maxContextTokens * 0.8) {
|
|
809
1062
|
logger.info(`After generation: ${afterTokens} tokens, will compress on next turn`);
|
|
810
1063
|
}
|
|
811
1064
|
|
|
812
|
-
// 保存聊天历史到 session
|
|
813
|
-
if (sessionId) {
|
|
814
|
-
this._saveHistoryToSession(sessionId, this._messages);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
1065
|
return {
|
|
818
1066
|
success: true,
|
|
819
1067
|
message: result.text || '',
|
|
820
1068
|
stepCount: result.stepCount || 1,
|
|
821
1069
|
};
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1070
|
+
} finally {
|
|
1071
|
+
// 校验并修复消息中的不完整工具调用
|
|
1072
|
+
this._validateToolCalls(messages);
|
|
1073
|
+
// 确保保存聊天历史到 session(无论成功还是失败)
|
|
1074
|
+
messageStore.save();
|
|
826
1075
|
}
|
|
827
1076
|
}
|
|
828
1077
|
|
|
@@ -833,19 +1082,13 @@ ${truncatedContent}${truncatedNote}
|
|
|
833
1082
|
*/
|
|
834
1083
|
async *chatStream(message, options = {}) {
|
|
835
1084
|
const sessionId = options.sessionId || null;
|
|
836
|
-
const context = { sessionId, isStream: true };
|
|
837
1085
|
const framework = this.agent.framework;
|
|
838
1086
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const savedMessages = this._loadHistoryFromSession(sessionId);
|
|
843
|
-
if (savedMessages.length > 0) {
|
|
844
|
-
this._messages = savedMessages;
|
|
845
|
-
logger.info(`Loaded ${savedMessages.length} messages from session ${sessionId}`);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
1087
|
+
// 获取 Per-Session 的消息存储
|
|
1088
|
+
const messageStore = this._getSessionMessageStore(sessionId);
|
|
1089
|
+
const messages = messageStore.messages;
|
|
848
1090
|
|
|
1091
|
+
try {
|
|
849
1092
|
// 关键:每次 chat 调用时刷新系统提示词,确保包含最新的工具/技能描述
|
|
850
1093
|
// 解决上下文过长时 LLM 不调用工具的问题
|
|
851
1094
|
this._systemPrompt = this.agent._buildSystemPrompt();
|
|
@@ -854,23 +1097,21 @@ ${truncatedContent}${truncatedNote}
|
|
|
854
1097
|
|
|
855
1098
|
const userMessage =
|
|
856
1099
|
typeof message === 'string' ? { role: 'user', content: message } : message;
|
|
857
|
-
|
|
858
|
-
this._messages.push(userMessage);
|
|
1100
|
+
messages.push(userMessage);
|
|
859
1101
|
|
|
860
1102
|
// 检查是否需要压缩上下文(包括工具定义)
|
|
861
|
-
const messagesTokens = this._countMessagesTokens(
|
|
1103
|
+
const messagesTokens = this._countMessagesTokens(messages);
|
|
862
1104
|
const toolsTokens = this._countToolsTokens();
|
|
863
1105
|
const systemPromptTokens = this._countTokens(this._systemPrompt);
|
|
864
1106
|
const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
|
|
865
1107
|
const limit = this._maxContextTokens * 0.7; // 降低到 70%
|
|
866
|
-
//console.log(messagesTokens,toolsTokens,systemPromptTokens,totalTokens,this._maxContextTokens)
|
|
867
1108
|
// 对于流式调用,如果上下文太大,先压缩再开始
|
|
868
1109
|
if (totalTokens > limit) {
|
|
869
1110
|
logger.info(
|
|
870
1111
|
`Context large (${totalTokens}/${this._maxContextTokens} tokens), compressing...`
|
|
871
1112
|
);
|
|
872
1113
|
// 流式调用时等待压缩完成(使用带超时控制的压缩)
|
|
873
|
-
await this._compressContext(sessionId);
|
|
1114
|
+
await this._compressContext(sessionId, messages, messageStore);
|
|
874
1115
|
}
|
|
875
1116
|
|
|
876
1117
|
const maxSteps = options.maxSteps || this._maxSteps;
|
|
@@ -880,20 +1121,39 @@ ${truncatedContent}${truncatedNote}
|
|
|
880
1121
|
}
|
|
881
1122
|
|
|
882
1123
|
// 准备传给 agent 的消息
|
|
883
|
-
|
|
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
|
+
|
|
884
1146
|
const agent = new ToolLoopAgent({
|
|
885
1147
|
model: this._aiClient,
|
|
886
1148
|
instructions: this._systemPrompt,
|
|
887
1149
|
tools: tools,
|
|
888
|
-
|
|
889
|
-
prepareStep: async ({ stepNumber, messages }) =>
|
|
890
|
-
this._prepareStepForPruning(stepNumber, messages),
|
|
1150
|
+
prepareStep: prepareStepChainStream,
|
|
891
1151
|
});
|
|
892
1152
|
|
|
893
|
-
// 使用
|
|
894
|
-
const result = await framework.
|
|
1153
|
+
// 使用 runInSession 让工具执行时能获取 sessionId(Per-Session 隔离)
|
|
1154
|
+
const result = await framework.runInSession(sessionId, {}, async () => {
|
|
895
1155
|
return agent.stream({
|
|
896
|
-
messages
|
|
1156
|
+
messages,
|
|
897
1157
|
...this.providerOptions,
|
|
898
1158
|
});
|
|
899
1159
|
});
|
|
@@ -921,18 +1181,106 @@ ${truncatedContent}${truncatedNote}
|
|
|
921
1181
|
}
|
|
922
1182
|
|
|
923
1183
|
const finishMessages = (await result.response).messages;
|
|
924
|
-
|
|
1184
|
+
messages.push(...finishMessages);
|
|
925
1185
|
|
|
926
|
-
//
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
1186
|
+
// 触发 agent:message 事件,让 memory 插件可以自动提取记忆
|
|
1187
|
+
const userMsg = messages[messages.length - finishMessages.length - 1];
|
|
1188
|
+
this.emit('message', { userMessage: userMsg, assistantResponse: fullText });
|
|
930
1189
|
} catch (err) {
|
|
931
1190
|
this.emit('error', { error: err.message });
|
|
932
1191
|
yield { type: 'error', error: err.message };
|
|
1192
|
+
} finally {
|
|
1193
|
+
// 校验并修复消息中的不完整工具调用
|
|
1194
|
+
this._validateToolCalls(messages);
|
|
1195
|
+
// 确保保存聊天历史到 session(无论成功还是失败)
|
|
1196
|
+
messageStore.save();
|
|
933
1197
|
}
|
|
934
1198
|
}
|
|
935
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
|
+
|
|
936
1284
|
/**
|
|
937
1285
|
* 修剪消息确保配对的 tool call/result 不被拆分
|
|
938
1286
|
* @param {number} stepNumber - 当前步骤号
|
|
@@ -959,11 +1307,16 @@ ${truncatedContent}${truncatedNote}
|
|
|
959
1307
|
}
|
|
960
1308
|
}
|
|
961
1309
|
}
|
|
962
|
-
if (msg.role === 'tool' && msg.
|
|
963
|
-
|
|
964
|
-
|
|
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
|
+
}
|
|
965
1319
|
}
|
|
966
|
-
toolResultIndices.get(msg.toolCallId).push(i);
|
|
967
1320
|
}
|
|
968
1321
|
}
|
|
969
1322
|
|
|
@@ -996,6 +1349,49 @@ ${truncatedContent}${truncatedNote}
|
|
|
996
1349
|
return { messages: pruned };
|
|
997
1350
|
}
|
|
998
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
|
+
|
|
999
1395
|
/**
|
|
1000
1396
|
* 获取 AI SDK 格式的工具
|
|
1001
1397
|
* AI SDK 6.x 需要对象形式 { toolName: tool }
|
|
@@ -1022,7 +1418,10 @@ ${truncatedContent}${truncatedNote}
|
|
|
1022
1418
|
try {
|
|
1023
1419
|
const result = await toolDef.execute(cleanedArgs, this.agent.framework);
|
|
1024
1420
|
this.emit('tool-result', { name: toolName, args: cleanedArgs, result });
|
|
1025
|
-
|
|
1421
|
+
if (result !== null && typeof result === 'object') {
|
|
1422
|
+
return JSON.stringify(result);
|
|
1423
|
+
}
|
|
1424
|
+
return String(result);
|
|
1026
1425
|
} catch (err) {
|
|
1027
1426
|
this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });
|
|
1028
1427
|
return { error: err.message };
|
|
@@ -1084,7 +1483,8 @@ ${truncatedContent}${truncatedNote}
|
|
|
1084
1483
|
* 销毁
|
|
1085
1484
|
*/
|
|
1086
1485
|
destroy() {
|
|
1087
|
-
this.
|
|
1486
|
+
this._sessionMessageStores.clear();
|
|
1487
|
+
this._sessionMessageStores = new Map();
|
|
1088
1488
|
this._tools.clear();
|
|
1089
1489
|
this._encoder = null;
|
|
1090
1490
|
this.removeAllListeners();
|