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.
- package/.agent/data/default.json +3 -4310
- 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/marknative/index.js +2 -7
- 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 +176 -171
- package/CLAUDE.md +144 -108
- package/docs/CONTEXT_DESIGN.md +1596 -0
- package/examples/test-concurrent-chat.js +60 -0
- package/examples/test-long-chat.js +77 -0
- package/examples/test-session-chat.js +93 -0
- 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 +78 -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 +627 -180
- package/src/core/agent-context.js +188 -0
- package/src/core/agent.js +9 -63
- 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/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
|
|
|
@@ -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
|
-
*
|
|
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
|
|
139
|
-
if (!
|
|
208
|
+
const sessionCtx = this.agent.framework.getOrCreateSessionContext(sessionId);
|
|
209
|
+
if (!sessionCtx) return [];
|
|
140
210
|
|
|
141
|
-
const messages =
|
|
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
|
-
* 保存聊天历史到
|
|
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
|
|
226
|
+
if (!sessionId || !this.agent?.framework || !messages) return;
|
|
157
227
|
|
|
158
228
|
try {
|
|
159
|
-
const
|
|
160
|
-
if (!
|
|
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
|
|
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 (
|
|
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(
|
|
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([
|
|
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 =
|
|
365
|
-
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');
|
|
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
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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: ${
|
|
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 =
|
|
409
|
-
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');
|
|
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
|
-
|
|
421
|
-
|
|
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: ${
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
685
|
-
this._messages.push(userMessage);
|
|
993
|
+
messages.push(userMessage);
|
|
686
994
|
|
|
687
995
|
// 检查是否需要压缩上下文(包括工具定义)
|
|
688
|
-
const messagesTokens = this._countMessagesTokens(
|
|
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
|
-
//
|
|
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
|
-
|
|
716
|
-
prepareStep: async ({ stepNumber, messages }) =>
|
|
717
|
-
this._prepareStepForPruning(stepNumber, messages),
|
|
1045
|
+
prepareStep: prepareStepChainStream,
|
|
718
1046
|
});
|
|
719
1047
|
|
|
720
|
-
// 使用
|
|
721
|
-
const result = await framework.
|
|
722
|
-
return agent.generate({ messages
|
|
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
|
-
|
|
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(
|
|
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
|
-
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
779
|
-
this._messages.push(userMessage);
|
|
1100
|
+
messages.push(userMessage);
|
|
780
1101
|
|
|
781
1102
|
// 检查是否需要压缩上下文(包括工具定义)
|
|
782
|
-
const messagesTokens = this._countMessagesTokens(
|
|
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
|
-
|
|
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
|
-
|
|
810
|
-
prepareStep: async ({ stepNumber, messages }) =>
|
|
811
|
-
this._prepareStepForPruning(stepNumber, messages),
|
|
1150
|
+
prepareStep: prepareStepChainStream,
|
|
812
1151
|
});
|
|
813
1152
|
|
|
814
|
-
// 使用
|
|
815
|
-
const result = await framework.
|
|
1153
|
+
// 使用 runInSession 让工具执行时能获取 sessionId(Per-Session 隔离)
|
|
1154
|
+
const result = await framework.runInSession(sessionId, {}, async () => {
|
|
816
1155
|
return agent.stream({
|
|
817
|
-
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
|
-
|
|
1184
|
+
messages.push(...finishMessages);
|
|
846
1185
|
|
|
847
|
-
//
|
|
848
|
-
|
|
849
|
-
|
|
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.
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
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.
|
|
1486
|
+
this._sessionMessageStores.clear();
|
|
1487
|
+
this._sessionMessageStores = new Map();
|
|
1041
1488
|
this._tools.clear();
|
|
1042
1489
|
this._encoder = null;
|
|
1043
1490
|
this.removeAllListeners();
|