foliko 1.1.13 → 1.1.15
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/plugins-state.json +1 -1
- package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
- package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
- package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
- package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
- package/.agent/mcp_config.json +7 -0
- package/.agent/memory/feedback/mnxe0cxc-14l6q5.md +17 -0
- package/.agent/memory/feedback/mnxe11pa-nxf577.md +9 -0
- package/.agent/memory/feedback/mnxe1an2-84faff.md +9 -0
- package/.agent/memory/feedback/mnxgcfj0-qg3wjc.md +9 -0
- package/.agent/memory/feedback/mnxgcn3y-40mqss.md +9 -0
- package/.agent/memory/feedback/mnxgcxq9-jm7ydl.md +9 -0
- package/.agent/memory/feedback/mnxgdyfj-pzjvkb.md +9 -0
- package/.agent/memory/feedback/mnxge3z1-7vyit1.md +9 -0
- package/.agent/memory/feedback/mnxhrg28-41hhjr.md +9 -0
- package/.agent/memory/feedback/mnxhrx0e-yth94k.md +9 -0
- package/.agent/memory/feedback/mnxhs3jd-rvx8aq.md +9 -0
- package/.agent/memory/feedback/mnxhs7p7-g5rtn9.md +9 -0
- package/.agent/memory/feedback/mnxhslx5-oqwuhr.md +9 -0
- package/.agent/memory/feedback/mnxhsvd6-nuyvvc.md +9 -0
- package/.agent/memory/project/mnxegq6z-5fc64w.md +22 -0
- package/.agent/memory/project/mnxh2w4r-le9hur.md +17 -0
- package/.agent/memory/project/mnxhq2yv-9qa8ay.md +31 -0
- package/.agent/memory/project/mnxhql11-iaun2o.md +34 -0
- package/.agent/memory/project/mnxhr78p-jpg7eq.md +23 -0
- package/.agent/memory/reference/mnxe0oa9-p6wzk6.md +27 -0
- package/.agent/memory/reference/mnxehcll-kcrmpf.md +29 -0
- package/.agent/memory/reference/mnxei0ts-jw091y.md +18 -0
- package/.agent/memory/reference/mnxfnrr4-rski36.md +40 -0
- package/.agent/memory/reference/mnxfo6n5-af9zls.md +18 -0
- package/.agent/memory/reference/mnxh2ady-u6cmvk.md +61 -0
- package/.agent/memory/reference/mnxhqdqh-ucsbsk.md +31 -0
- package/.agent/memory/reference/mnxiixyp-rz2gvw.md +34 -0
- package/.agent/memory/user/mnxhqxk3-vjjhlf.md +23 -0
- package/.agent/sessions/cli_default.json +11 -639
- package/.agent/sessions/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.json +25 -0
- package/.claude/settings.local.json +23 -1
- package/cli/src/commands/chat.js +9 -15
- package/cli/src/ui/chat-ui.js +40 -71
- package/package.json +4 -2
- package/plugins/default-plugins.js +5 -5
- package/plugins/file-system-plugin.js +1 -1
- package/plugins/memory-plugin.js +12 -12
- package/plugins/plugin-manager-plugin.js +1 -0
- package/plugins/subagent-plugin.js +55 -1
- package/plugins/telegram-plugin.js +9 -6
- package/plugins/weixin-plugin.js +75 -78
- package/src/core/agent-chat.js +468 -1612
- package/src/core/agent.js +53 -134
- package/src/core/chat-session.js +423 -0
- package/src/core/context-compressor.js +473 -0
- package/src/core/context-manager.js +0 -48
- package/src/core/framework.js +95 -68
- package/src/core/index.js +11 -0
- package/src/core/notification-manager.js +125 -0
- package/src/core/subagent.js +295 -0
- package/src/core/token-counter.js +190 -0
- package/src/core/tool-executor.js +270 -0
- package/src/executors/mcp-executor.js +14 -1
- package/src/utils/download.js +596 -0
- package/system.md +312 -2373
- package/.agent/agents/code-assistant.json +0 -17
- package/.agent/agents/email-assistant.json +0 -14
- package/.agent/agents/file-assistant.json +0 -18
- package/.agent/agents/orchestrator-demo.md +0 -53
- package/.agent/agents/orchestrator.json +0 -7
- package/.agent/agents/poster-expert.md +0 -228
- package/.agent/agents/system-assistant.json +0 -15
- package/.agent/agents/web-assistant.json +0 -12
- package/.agent/memory/feedback/mnv3nu27-3o15pf.md +0 -9
- package/.agent/memory/feedback/mnv3o078-b959yj.md +0 -9
- package/.agent/memory/feedback/mnv3o6ej-u0fif5.md +0 -9
- package/.agent/memory/feedback/mnv3obgl-bkkjoj.md +0 -9
- package/.agent/memory/feedback/mnv4a3js-dv6onx.md +0 -9
- package/.agent/memory/feedback/mnv4aacm-sxxowp.md +0 -9
- package/.agent/memory/feedback/mnv4ahto-w40ffm.md +0 -9
- package/.agent/memory/feedback/mnv4anvp-3cs06y.md +0 -9
- package/.agent/memory/feedback/mnvzgvtd-0o2900.md +0 -9
- package/.agent/memory/feedback/mnvzhajn-swbx61.md +0 -15
- package/.agent/memory/feedback/mnvzhgsp-p5vog3.md +0 -9
- package/.agent/memory/feedback/mnvzho0c-fgql7q.md +0 -14
- package/.agent/memory/feedback/mnvzhtzq-ufr5at.md +0 -9
- package/.agent/memory/feedback/mnvzhyb3-9byq2z.md +0 -9
- package/.agent/memory/feedback/mnvzi7hp-hyeafp.md +0 -9
- package/.agent/memory/feedback/mnvzibph-z7rwp5.md +0 -9
- package/.agent/memory/feedback/mnvzilys-7h176w.md +0 -14
- package/.agent/memory/feedback/mnvziuh5-zjshci.md +0 -9
- package/.agent/memory/feedback/mnw07wde-6zqsc8.md +0 -9
- package/.agent/memory/feedback/mnw084bp-j0ba2a.md +0 -9
- package/.agent/memory/user/mnv3n62r-y0h79j.md +0 -21
- package/.agent/memory/user/mnv3n9yf-ead4g8.md +0 -13
- package/.agent/memory/user/mnv3ne3j-82tq1k.md +0 -19
- package/.agent/memory/user/mnv3nhgm-g2s2us.md +0 -11
- package/.agent/memory/user/mnv3nl9u-ejd998.md +0 -16
- package/.agent/memory/user/mnv3nofp-ya5szl.md +0 -10
- package/.agent/memory/user/mnv49qne-bhk0ki.md +0 -9
- package/.agent/memory/user/mnv49w3y-rzr8ju.md +0 -13
- package/.agent/sessions/test.json +0 -16
- package/plugins/python-plugin-loader.js.bak +0 -856
- package/src/core/agent-context.js +0 -188
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextCompressor - 上下文压缩器
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. AI 智能压缩(生成摘要)
|
|
6
|
+
* 2. 简单压缩(截断保留最近消息)
|
|
7
|
+
* 3. 消息配对验证(防止 orphaned tool result)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { logger } = require('../utils/logger');
|
|
11
|
+
const { zodSchemaToMarkdown, zodSchemaToTable } = require('@chnak/zod-to-markdown');
|
|
12
|
+
|
|
13
|
+
// 模型上下文限制表
|
|
14
|
+
const MODEL_CONTEXT_LIMITS = {
|
|
15
|
+
'deepseek-chat': 128000,
|
|
16
|
+
'deepseek-coder': 128000,
|
|
17
|
+
'deepseek-reasoner': 128000,
|
|
18
|
+
'MiniMax-M2.7': 110000,
|
|
19
|
+
'gpt-4': 100000,
|
|
20
|
+
'gpt-4o': 100000,
|
|
21
|
+
'gpt-4o-mini': 100000,
|
|
22
|
+
'gpt-4-turbo': 100000,
|
|
23
|
+
'claude-3-5-sonnet': 150000,
|
|
24
|
+
'claude-3-opus': 150000,
|
|
25
|
+
'claude-3-sonnet': 150000,
|
|
26
|
+
'glm-5.1': 200000,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// 压缩超时
|
|
30
|
+
const COMPRESSION_TIMEOUT = 120000;
|
|
31
|
+
|
|
32
|
+
class ContextCompressor {
|
|
33
|
+
/**
|
|
34
|
+
* @param {Object} config - 配置
|
|
35
|
+
*/
|
|
36
|
+
constructor(config = {}) {
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.agent = config.agent;
|
|
39
|
+
this.framework = config.framework;
|
|
40
|
+
|
|
41
|
+
// 模型相关配置
|
|
42
|
+
this.model = config.model || 'deepseek-chat';
|
|
43
|
+
this._maxContextTokens = config.maxContextTokens || this._getDefaultContextLimit();
|
|
44
|
+
this._keepRecentMessages = config.keepRecentMessages || 20;
|
|
45
|
+
this._enableSmartCompress = config.enableSmartCompress !== false;
|
|
46
|
+
|
|
47
|
+
// 压缩状态
|
|
48
|
+
this._compressionCount = 0;
|
|
49
|
+
this._compressionInProgress = false;
|
|
50
|
+
this._compressionPromise = null;
|
|
51
|
+
|
|
52
|
+
// 工具结果压缩配置
|
|
53
|
+
this._maxToolResultSize = config.maxToolResultSize || 4000;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 获取默认上下文限制
|
|
58
|
+
* @returns {number}
|
|
59
|
+
* @private
|
|
60
|
+
*/
|
|
61
|
+
_getDefaultContextLimit() {
|
|
62
|
+
const modelKey = Object.keys(MODEL_CONTEXT_LIMITS).find((k) =>
|
|
63
|
+
this.model.toLowerCase().includes(k.toLowerCase())
|
|
64
|
+
);
|
|
65
|
+
return modelKey ? MODEL_CONTEXT_LIMITS[modelKey] : 40000;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 压缩上下文
|
|
70
|
+
* @param {string} sessionId - Session ID
|
|
71
|
+
* @param {Array} messages - 消息数组引用
|
|
72
|
+
* @param {Object} messageStore - 消息存储
|
|
73
|
+
* @returns {Promise}
|
|
74
|
+
*/
|
|
75
|
+
async compress(sessionId, messages, messageStore) {
|
|
76
|
+
if (this._compressionInProgress && this._compressionPromise) {
|
|
77
|
+
logger.debug('Compression already in progress, waiting...');
|
|
78
|
+
return this._compressionPromise;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (messages.length <= this._keepRecentMessages) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this._compressionInProgress = true;
|
|
86
|
+
|
|
87
|
+
this._compressionPromise = this._executeWithTimeout(sessionId, messages, messageStore).finally(
|
|
88
|
+
() => {
|
|
89
|
+
this._compressionInProgress = false;
|
|
90
|
+
this._compressionPromise = null;
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return this._compressionPromise;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 带超时的压缩执行
|
|
99
|
+
* @private
|
|
100
|
+
*/
|
|
101
|
+
async _executeWithTimeout(sessionId, messages, messageStore) {
|
|
102
|
+
try {
|
|
103
|
+
return await Promise.race([
|
|
104
|
+
this._doCompress(sessionId, messages, messageStore),
|
|
105
|
+
this._createTimeoutPromise(),
|
|
106
|
+
]);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.warn('Compression failed:', err.message);
|
|
109
|
+
this._simpleCompress(sessionId, messages, messageStore);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 创建超时 Promise
|
|
115
|
+
* @returns {Promise}
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
_createTimeoutPromise() {
|
|
119
|
+
return new Promise((_, reject) => {
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
reject(new Error(`Compression timeout (${COMPRESSION_TIMEOUT}ms)`));
|
|
122
|
+
}, COMPRESSION_TIMEOUT);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 执行智能压缩(使用 AI 生成摘要)
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
async _doCompress(sessionId, messages, messageStore) {
|
|
131
|
+
const systemMessages = messages.filter((m) => m.role === 'system');
|
|
132
|
+
const otherMessages = messages.filter((m) => m.role !== 'system');
|
|
133
|
+
|
|
134
|
+
// 保留最近的 N 条非系统消息
|
|
135
|
+
const recentMessages = otherMessages.slice(-this._keepRecentMessages);
|
|
136
|
+
const messagesToSummarize = otherMessages.slice(0, -this._keepRecentMessages);
|
|
137
|
+
|
|
138
|
+
const compressedCount = messagesToSummarize.length;
|
|
139
|
+
let summaryContent = '';
|
|
140
|
+
|
|
141
|
+
// 使用 AI 生成摘要
|
|
142
|
+
if (this._enableSmartCompress && this.agent?._chatHandler?._aiClient) {
|
|
143
|
+
try {
|
|
144
|
+
const summaryText = await this._summarizeMessages(messagesToSummarize);
|
|
145
|
+
summaryContent = `[早期对话摘要]: ${summaryText}`;
|
|
146
|
+
logger.info(`AI summary generated (${summaryText.length} chars)`);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
logger.warn('AI summary failed, using simple compression:', err.message);
|
|
149
|
+
summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const summary = {
|
|
156
|
+
role: 'assistant',
|
|
157
|
+
content: summaryContent,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// 构建保留的消息(确保 tool call 和 tool result 配对)
|
|
161
|
+
const filteredRecentMessages = this._filterPairedMessages(recentMessages);
|
|
162
|
+
|
|
163
|
+
// 替换消息数组
|
|
164
|
+
messages.length = 0;
|
|
165
|
+
messages.push(...systemMessages, summary, ...filteredRecentMessages);
|
|
166
|
+
|
|
167
|
+
// 更新压缩状态
|
|
168
|
+
this._compressionCount++;
|
|
169
|
+
if (messageStore.compressionState) {
|
|
170
|
+
messageStore.compressionState.count++;
|
|
171
|
+
messageStore.compressionState.lastCompressedAt = Date.now();
|
|
172
|
+
messageStore.compressionState.lastTokenCount = this._countMessagesTokens(messages);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
logger.info(
|
|
176
|
+
`Context compressed (${this._compressionCount} times). Messages: ${messages.length}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 简单压缩(截断保留最近消息)
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_simpleCompress(sessionId, messages, messageStore) {
|
|
185
|
+
const systemMessages = messages.filter((m) => m.role === 'system');
|
|
186
|
+
const otherMessages = messages.filter((m) => m.role !== 'system');
|
|
187
|
+
const recentMessages = otherMessages.slice(-this._keepRecentMessages);
|
|
188
|
+
const compressedCount = otherMessages.length - this._keepRecentMessages;
|
|
189
|
+
|
|
190
|
+
const summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;
|
|
191
|
+
|
|
192
|
+
const summary = {
|
|
193
|
+
role: 'assistant',
|
|
194
|
+
content: summaryContent,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// 确保 tool call 和 tool result 配对
|
|
198
|
+
const filteredRecentMessages = this._filterPairedMessages(recentMessages);
|
|
199
|
+
|
|
200
|
+
messages.length = 0;
|
|
201
|
+
messages.push(...systemMessages, summary, ...filteredRecentMessages);
|
|
202
|
+
|
|
203
|
+
this._compressionCount++;
|
|
204
|
+
if (messageStore.compressionState) {
|
|
205
|
+
messageStore.compressionState.count++;
|
|
206
|
+
messageStore.compressionState.lastCompressedAt = Date.now();
|
|
207
|
+
messageStore.compressionState.lastTokenCount = this._countMessagesTokens(messages);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
logger.info(`Context simple compressed. Messages: ${messages.length}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 过滤消息,保留配对的 tool call 和 tool result
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
_filterPairedMessages(messages) {
|
|
218
|
+
// 工具类型检查
|
|
219
|
+
const isToolCall = (item) =>
|
|
220
|
+
item && (item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId;
|
|
221
|
+
const isToolResult = (item) =>
|
|
222
|
+
item && (item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId;
|
|
223
|
+
|
|
224
|
+
// 第一遍:找出所有 tool call 和它们的 results
|
|
225
|
+
const assistantToolCalls = new Map(); // toolCallId -> assistant message index
|
|
226
|
+
const toolResults = new Map(); // toolCallId -> tool result indices
|
|
227
|
+
|
|
228
|
+
for (let i = 0; i < messages.length; i++) {
|
|
229
|
+
const msg = messages[i];
|
|
230
|
+
if (msg.role === 'assistant' && msg.content) {
|
|
231
|
+
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
232
|
+
for (const item of content) {
|
|
233
|
+
if (isToolCall(item)) {
|
|
234
|
+
assistantToolCalls.set(item.toolCallId, i);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
239
|
+
for (const item of msg.content) {
|
|
240
|
+
if (isToolResult(item)) {
|
|
241
|
+
if (!toolResults.has(item.toolCallId)) {
|
|
242
|
+
toolResults.set(item.toolCallId, []);
|
|
243
|
+
}
|
|
244
|
+
toolResults.get(item.toolCallId).push(i);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 第二遍:找出哪些 assistant 消息需要保留
|
|
251
|
+
const assistantIndicesToKeep = new Set();
|
|
252
|
+
for (let i = 0; i < messages.length; i++) {
|
|
253
|
+
const msg = messages[i];
|
|
254
|
+
if (msg.role === 'assistant') {
|
|
255
|
+
let hasToolCall = false;
|
|
256
|
+
if (msg.content) {
|
|
257
|
+
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
258
|
+
for (const item of content) {
|
|
259
|
+
if (isToolCall(item)) {
|
|
260
|
+
hasToolCall = true;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (hasToolCall) {
|
|
266
|
+
assistantIndicesToKeep.add(i);
|
|
267
|
+
} else if (i >= messages.length - 3) {
|
|
268
|
+
// 保留最近几条 assistant 消息
|
|
269
|
+
assistantIndicesToKeep.add(i);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 如果有孤儿的 tool call(没有 result),保留该 assistant 消息
|
|
275
|
+
for (const [toolCallId, assistantIdx] of assistantToolCalls) {
|
|
276
|
+
if (!toolResults.has(toolCallId)) {
|
|
277
|
+
assistantIndicesToKeep.add(assistantIdx);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 第三遍:确定哪些 tool result 要保留
|
|
282
|
+
const indicesToKeep = new Set();
|
|
283
|
+
for (let i = 0; i < messages.length; i++) {
|
|
284
|
+
const msg = messages[i];
|
|
285
|
+
if (msg.role === 'tool') {
|
|
286
|
+
let hasPairedAssistant = false;
|
|
287
|
+
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
288
|
+
for (const item of content) {
|
|
289
|
+
if (isToolResult(item)) {
|
|
290
|
+
const assistantIdx = assistantToolCalls.get(item.toolCallId);
|
|
291
|
+
if (assistantIdx !== undefined && assistantIndicesToKeep.has(assistantIdx)) {
|
|
292
|
+
hasPairedAssistant = true;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (hasPairedAssistant) {
|
|
298
|
+
indicesToKeep.add(i);
|
|
299
|
+
}
|
|
300
|
+
} else if (msg.role === 'assistant') {
|
|
301
|
+
if (assistantIndicesToKeep.has(i)) {
|
|
302
|
+
indicesToKeep.add(i);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
// user, system 等角色默认保留
|
|
306
|
+
indicesToKeep.add(i);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 按索引排序并返回
|
|
311
|
+
const sortedIndices = Array.from(indicesToKeep).sort((a, b) => a - b);
|
|
312
|
+
return sortedIndices.map((i) => messages[i]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 验证消息配对(防止 orphaned tool result)
|
|
317
|
+
* @param {Array} messages - 消息数组
|
|
318
|
+
* @returns {Array} 验证后的消息
|
|
319
|
+
*/
|
|
320
|
+
validateMessagesPairing(messages) {
|
|
321
|
+
// 收集所有 assistant 的 tool-call IDs(兼容多种类型名)
|
|
322
|
+
const assistantToolCallIds = new Set();
|
|
323
|
+
for (const msg of messages) {
|
|
324
|
+
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
325
|
+
for (const item of msg.content) {
|
|
326
|
+
// 兼容 tool-call 和 tool-use 两种类型
|
|
327
|
+
if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) {
|
|
328
|
+
assistantToolCallIds.add(item.toolCallId);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 检查并删除没有配对的 tool result
|
|
335
|
+
let removedCount = 0;
|
|
336
|
+
for (const msg of messages) {
|
|
337
|
+
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
338
|
+
const originalLength = msg.content.length;
|
|
339
|
+
msg.content = msg.content.filter((item) => {
|
|
340
|
+
// 兼容 tool-result 和 tool_result 两种类型
|
|
341
|
+
if (
|
|
342
|
+
item &&
|
|
343
|
+
(item.type === 'tool-result' || item.type === 'tool_result') &&
|
|
344
|
+
item.toolCallId
|
|
345
|
+
) {
|
|
346
|
+
if (!assistantToolCallIds.has(item.toolCallId)) {
|
|
347
|
+
removedCount++;
|
|
348
|
+
return false; // 删除没有配对的 tool result
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return true;
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// 如果所有 content 都被删除了,标记整个消息待删除
|
|
355
|
+
if (msg.content.length === 0 && originalLength > 0) {
|
|
356
|
+
msg._orphaned = true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 移除被标记为 orphaned 的 tool 消息
|
|
362
|
+
const originalLength = messages.length;
|
|
363
|
+
const filtered = messages.filter((msg) => !(msg.role === 'tool' && msg._orphaned));
|
|
364
|
+
|
|
365
|
+
if (removedCount > 0 || filtered.length !== originalLength) {
|
|
366
|
+
logger.debug(
|
|
367
|
+
`Removed ${removedCount} orphaned tool-results, ${originalLength - filtered.length} orphaned tool messages`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return filtered;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 使用 AI 生成消息摘要
|
|
376
|
+
* @param {Array} messages - 消息数组
|
|
377
|
+
* @returns {Promise<string>}
|
|
378
|
+
* @private
|
|
379
|
+
*/
|
|
380
|
+
async _summarizeMessages(messages) {
|
|
381
|
+
if (!this.agent?._chatHandler?._aiClient) {
|
|
382
|
+
throw new Error('AI client not available');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const { generateText } = await import('ai');
|
|
386
|
+
|
|
387
|
+
const prompt = `请简要总结以下对话的主要内容,用 100 字以内描述:
|
|
388
|
+
|
|
389
|
+
${messages.map((m) => `${m.role}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`).join('\n')}
|
|
390
|
+
|
|
391
|
+
摘要:`;
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const result = await generateText({
|
|
395
|
+
model: this.agent._chatHandler._aiClient,
|
|
396
|
+
prompt: prompt,
|
|
397
|
+
maxTokens: 200,
|
|
398
|
+
});
|
|
399
|
+
return result.text || '';
|
|
400
|
+
} catch (err) {
|
|
401
|
+
logger.warn('Summarize failed:', err.message);
|
|
402
|
+
throw err;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* 计算消息数组的 token 数
|
|
408
|
+
* @param {Array} messages - 消息数组
|
|
409
|
+
* @returns {number}
|
|
410
|
+
*/
|
|
411
|
+
_countMessagesTokens(messages) {
|
|
412
|
+
if (!messages || !Array.isArray(messages)) return 0;
|
|
413
|
+
|
|
414
|
+
let total = 0;
|
|
415
|
+
for (const msg of messages) {
|
|
416
|
+
total += this._countMessageTokens(msg);
|
|
417
|
+
}
|
|
418
|
+
return total;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 计算单条消息的 token 数
|
|
423
|
+
* @private
|
|
424
|
+
*/
|
|
425
|
+
_countMessageTokens(msg) {
|
|
426
|
+
if (!msg) return 0;
|
|
427
|
+
|
|
428
|
+
let total = 4; // 角色开销
|
|
429
|
+
|
|
430
|
+
if (typeof msg.content === 'string') {
|
|
431
|
+
total += Math.ceil(Buffer.byteLength(msg.content, 'utf8') / 4);
|
|
432
|
+
} else if (Array.isArray(msg.content)) {
|
|
433
|
+
for (const block of msg.content) {
|
|
434
|
+
if (block.type === 'text') {
|
|
435
|
+
total += Math.ceil(Buffer.byteLength(block.text || '', 'utf8') / 4);
|
|
436
|
+
} else if (block.type === 'tool-call' || block.type === 'tool-use') {
|
|
437
|
+
if (block.input) {
|
|
438
|
+
total += Math.ceil(Buffer.byteLength(JSON.stringify(block.input), 'utf8') / 4);
|
|
439
|
+
}
|
|
440
|
+
} else if (block.type === 'tool-result' || block.type === 'tool_result') {
|
|
441
|
+
const content =
|
|
442
|
+
typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
443
|
+
total += Math.ceil(Buffer.byteLength(content, 'utf8') / 4);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (msg.tool_calls) {
|
|
449
|
+
total += 15;
|
|
450
|
+
for (const tc of msg.tool_calls) {
|
|
451
|
+
if (tc.function) {
|
|
452
|
+
total += Math.ceil(Buffer.byteLength(tc.function.name || '', 'utf8') / 4);
|
|
453
|
+
total += Math.ceil(Buffer.byteLength(tc.function.arguments || '{}', 'utf8') / 4);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return total;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* 获取压缩统计
|
|
463
|
+
* @returns {Object}
|
|
464
|
+
*/
|
|
465
|
+
getStats() {
|
|
466
|
+
return {
|
|
467
|
+
compressionCount: this._compressionCount,
|
|
468
|
+
inProgress: this._compressionInProgress,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
module.exports = { ContextCompressor };
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
const { RequestContext } = require('./request-context');
|
|
11
11
|
const { SessionContext } = require('./session-context');
|
|
12
|
-
const { AgentContext } = require('./agent-context');
|
|
13
12
|
|
|
14
13
|
class ContextManager {
|
|
15
14
|
/**
|
|
@@ -21,7 +20,6 @@ class ContextManager {
|
|
|
21
20
|
// Context 存储
|
|
22
21
|
this._requestContexts = new Map(); // requestId → RequestContext
|
|
23
22
|
this._sessionContexts = new Map(); // sessionId → SessionContext
|
|
24
|
-
this._agentContexts = new Map(); // agentId → AgentContext
|
|
25
23
|
|
|
26
24
|
// 引用计数(用于自动清理)
|
|
27
25
|
this._refCounts = new Map();
|
|
@@ -161,45 +159,6 @@ class ContextManager {
|
|
|
161
159
|
return this._sessionContexts.has(sessionId);
|
|
162
160
|
}
|
|
163
161
|
|
|
164
|
-
// ==================== Agent Context ====================
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* 获取或创建 Agent Context
|
|
168
|
-
* @param {string} agentId - Agent ID
|
|
169
|
-
* @param {Agent} agent - Agent 实例
|
|
170
|
-
* @param {Object} options - 配置选项
|
|
171
|
-
* @returns {AgentContext}
|
|
172
|
-
*/
|
|
173
|
-
getOrCreateAgentContext(agentId, agent, options = {}) {
|
|
174
|
-
let ctx = this._agentContexts.get(agentId);
|
|
175
|
-
if (!ctx) {
|
|
176
|
-
ctx = new AgentContext(agentId, agent, options);
|
|
177
|
-
this._agentContexts.set(agentId, ctx);
|
|
178
|
-
}
|
|
179
|
-
return ctx;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* 获取 Agent Context
|
|
184
|
-
* @param {string} agentId - Agent ID
|
|
185
|
-
* @returns {AgentContext|null}
|
|
186
|
-
*/
|
|
187
|
-
getAgentContext(agentId) {
|
|
188
|
-
return this._agentContexts.get(agentId) || null;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* 删除 Agent Context
|
|
193
|
-
* @param {string} agentId - Agent ID
|
|
194
|
-
*/
|
|
195
|
-
removeAgentContext(agentId) {
|
|
196
|
-
const ctx = this._agentContexts.get(agentId);
|
|
197
|
-
if (ctx) {
|
|
198
|
-
ctx.destroy();
|
|
199
|
-
this._agentContexts.delete(agentId);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
162
|
// ==================== 批量操作 ====================
|
|
204
163
|
|
|
205
164
|
/**
|
|
@@ -235,11 +194,6 @@ class ContextManager {
|
|
|
235
194
|
|
|
236
195
|
// 清理 Session Contexts
|
|
237
196
|
this.destroyAllSessionContexts();
|
|
238
|
-
|
|
239
|
-
// 清理 Agent Contexts
|
|
240
|
-
for (const [agentId] of this._agentContexts) {
|
|
241
|
-
this.removeAgentContext(agentId);
|
|
242
|
-
}
|
|
243
197
|
}
|
|
244
198
|
|
|
245
199
|
// ==================== 统计 ====================
|
|
@@ -252,7 +206,6 @@ class ContextManager {
|
|
|
252
206
|
return {
|
|
253
207
|
activeRequests: this._requestContexts.size,
|
|
254
208
|
activeSessions: this._sessionContexts.size,
|
|
255
|
-
activeAgents: this._agentContexts.size,
|
|
256
209
|
refCounts: Object.fromEntries(this._refCounts),
|
|
257
210
|
};
|
|
258
211
|
}
|
|
@@ -273,7 +226,6 @@ class ContextManager {
|
|
|
273
226
|
return {
|
|
274
227
|
requestContexts: this._requestContexts.size,
|
|
275
228
|
sessionContexts: this._sessionContexts.size,
|
|
276
|
-
agentContexts: this._agentContexts.size,
|
|
277
229
|
totalSessionMessages: sessionMessagesSize,
|
|
278
230
|
totalSessionVariables: sessionVariablesSize,
|
|
279
231
|
};
|