foliko 1.1.67 → 1.1.69
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/.claude/settings.local.json +19 -10
- package/.dockerignore +45 -45
- package/.env.example +56 -56
- package/CLAUDE.md +2 -2
- package/README.md +13 -13
- package/SPEC.md +3 -3
- package/cli/src/commands/chat.js +2 -20
- package/cli/src/commands/list.js +7 -6
- package/cli/src/commands/plugin.js +3 -2
- package/cli/src/daemon.js +2 -2
- package/cli/src/ui/chat-ui-old.js +15 -4
- package/cli/src/ui/chat-ui.js +236 -203
- package/cli/src/ui/footer-bar.js +20 -46
- package/cli/src/ui/message-bubble.js +24 -2
- package/cli/src/ui/status-bar.js +177 -0
- package/cli/src/utils/config.js +29 -0
- package/cli/src/utils/plugin-config.js +1 -1
- package/docker-compose.yml +33 -33
- package/docs/features.md +120 -120
- package/docs/quick-reference.md +160 -160
- package/docs/user-manual.md +1391 -1391
- package/examples/ambient-example.js +2 -2
- package/examples/bootstrap.js +3 -3
- package/examples/test-chat.js +1 -1
- package/examples/test-reload.js +1 -1
- package/examples/test-telegram.js +1 -1
- package/examples/test-tg-bot.js +1 -1
- package/examples/test-tg-simple.js +2 -2
- package/examples/test-tg.js +1 -1
- package/examples/test-think.js +1 -1
- package/examples/test-weixin-feishu.js +3 -3
- package/package.json +3 -1
- package/plugins/ambient-agent/index.js +1 -1
- package/plugins/audit-plugin.js +84 -29
- package/plugins/coordinator-plugin.js +14 -12
- package/plugins/data-splitter-plugin.js +323 -0
- package/plugins/default-plugins.js +23 -12
- package/plugins/email/index.js +1 -1
- package/plugins/extension-executor-plugin.js +87 -9
- package/plugins/feishu-plugin.js +118 -16
- package/plugins/file-system-plugin.js +68 -50
- package/plugins/gate-trading.js +10 -10
- package/plugins/install-plugin.js +7 -7
- package/plugins/memory-plugin.js +9 -12
- package/plugins/plugin-manager-plugin.js +12 -14
- package/plugins/python-executor-plugin.js +1 -1
- package/plugins/python-plugin-loader.js +1 -1
- package/plugins/qq-plugin.js +151 -24
- package/plugins/rules-plugin.js +8 -8
- package/plugins/scheduler-plugin.js +24 -20
- package/plugins/session-plugin.js +313 -397
- package/plugins/storage-plugin.js +235 -175
- package/plugins/subagent-plugin.js +17 -13
- package/plugins/telegram-plugin.js +116 -17
- package/plugins/think-plugin.js +64 -60
- package/plugins/tools-plugin.js +8 -8
- package/plugins/web-plugin.js +2 -2
- package/plugins/weixin-plugin.js +107 -24
- package/skills/find-skills/AGENTS.md +2 -2
- package/skills/find-skills/SKILL.md +133 -133
- package/skills/foliko-dev/AGENTS.md +236 -236
- package/skills/foliko-dev/SKILL.md +19 -19
- package/skills/mcp-usage/SKILL.md +200 -200
- package/skills/plugin-guide/SKILL.md +4 -4
- package/skills/python-plugin-dev/SKILL.md +5 -5
- package/skills/skill-guide/SKILL.md +104 -6
- package/skills/subagent-guide/SKILL.md +237 -237
- package/skills/workflow-guide/SKILL.md +646 -646
- package/src/capabilities/skill-manager.js +124 -17
- package/src/capabilities/workflow-engine.js +3 -3
- package/src/core/agent-chat.js +72 -26
- package/src/core/agent.js +17 -27
- package/src/core/branch-summary-auto.js +206 -0
- package/src/core/chat-session.js +45 -169
- package/src/core/command-registry.js +200 -0
- package/src/core/constants.js +198 -0
- package/src/core/context-compressor.js +702 -326
- package/src/core/context-manager.js +0 -1
- package/src/core/enhanced-context-compressor.js +210 -0
- package/src/core/framework.js +260 -84
- package/src/core/jsonl-storage.js +253 -0
- package/src/core/plugin-base.js +7 -5
- package/src/core/plugin-manager.js +15 -10
- package/src/core/provider-registry.js +159 -0
- package/src/core/provider.js +2 -0
- package/src/core/session-entry.js +225 -0
- package/src/core/session-manager.js +701 -0
- package/src/core/storage-manager.js +494 -0
- package/src/core/sub-agent-config.js +1 -1
- package/src/core/subagent.js +16 -135
- package/src/core/token-counter.js +177 -58
- package/src/core/tool-executor.js +2 -70
- package/src/core/ui-extension-context.js +174 -0
- package/src/executors/mcp-executor.js +27 -16
- package/src/utils/chat-queue.js +11 -22
- package/src/utils/data-splitter.js +345 -0
- package/src/utils/logger.js +152 -180
- package/src/utils/message-validator.js +283 -0
- package/src/utils/plugin-helpers.js +2 -2
- package/src/utils/retry.js +168 -22
- package/website_v2/docs/api.html +1 -1
- package/website_v2/docs/configuration.html +2 -2
- package/website_v2/docs/plugin-development.html +4 -4
- package/website_v2/docs/project-structure.html +2 -2
- package/website_v2/docs/skill-development.html +2 -2
- package/website_v2/index.html +1 -1
- package/website_v2/styles/animations.css +7 -7
- package/.agent/agents/backend-dev.md +0 -102
- package/.agent/agents/data-analyst.md +0 -117
- package/.agent/agents/devops.md +0 -115
- package/.agent/agents/frontend-dev.md +0 -94
- package/.agent/agents/network-requester.md +0 -44
- package/.agent/agents/poster-designer.md +0 -52
- package/.agent/agents/product-manager.md +0 -85
- package/.agent/agents/qa-engineer.md +0 -100
- package/.agent/agents/security-engineer.md +0 -99
- package/.agent/agents/team-lead.md +0 -137
- package/.agent/agents/ui-designer.md +0 -116
- package/.agent/data/default.json +0 -58
- package/.agent/data/email/processed-emails.json +0 -1
- package/.agent/data/plugins-state.json +0 -199
- package/.agent/data/scheduler/tasks.json +0 -1
- package/.agent/data/web/web-config.json +0 -5
- 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/data/weixin.json +0 -6
- package/.agent/mcp_config.json +0 -14
- package/.agent/memory/user/mof6gk94-kneeuh.md +0 -9
- package/.agent/package.json +0 -8
- package/.agent/plugins/marknative/README.md +0 -134
- package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
- package/.agent/plugins/marknative/fonts.zip +0 -0
- package/.agent/plugins/marknative/index.js +0 -256
- package/.agent/plugins/marknative/package.json +0 -12
- package/.agent/plugins/test-plugin.py +0 -99
- package/.agent/plugins.json +0 -14
- package/.agent/python-scripts/test_sample.py +0 -24
- package/.agent/sessions/cli_default.json +0 -247
- package/.agent/skills/agent-browser/SKILL.md +0 -311
- package/.agent/skills/agent-browser/TEST_PLAN.md +0 -200
- package/.agent/skills/sysinfo/SKILL.md +0 -38
- package/.agent/skills/sysinfo/system-info.sh +0 -130
- package/.agent/skills/workflow/SKILL.md +0 -324
- package/.agent/test-agent.js +0 -35
- package/.agent/weixin.json +0 -6
- package/.agent/workflows/email-digest.json +0 -50
- package/.agent/workflows/file-backup.json +0 -21
- package/.agent/workflows/get-ip-notify.json +0 -32
- package/.agent/workflows/news-aggregator.json +0 -93
- package/.agent/workflows/news-dashboard-v2.json +0 -94
- package/.agent/workflows/notification-batch.json +0 -32
- package/src/core/session-context.js +0 -346
- package/src/core/session-storage.js +0 -295
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ContextCompressor - 上下文压缩器
|
|
3
|
-
*
|
|
4
|
-
* 职责:
|
|
5
|
-
* 1. AI 智能压缩(生成摘要)
|
|
6
|
-
* 2. 简单压缩(截断保留最近消息)
|
|
7
|
-
* 3. 消息配对验证(防止 orphaned tool result)
|
|
3
|
+
* Ported from pi's compaction/compaction.ts with enhanced functionality
|
|
8
4
|
*/
|
|
9
5
|
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
10
8
|
const { logger } = require('../utils/logger');
|
|
11
|
-
const {
|
|
12
|
-
|
|
9
|
+
const {
|
|
10
|
+
estimateTokens,
|
|
11
|
+
estimateContextTokens,
|
|
12
|
+
shouldCompact,
|
|
13
|
+
getLastAssistantUsage,
|
|
14
|
+
calculateContextTokens,
|
|
15
|
+
safeJsonStringify,
|
|
16
|
+
TokenCounter
|
|
17
|
+
} = require('./token-counter');
|
|
18
|
+
const {
|
|
19
|
+
validateMessagesPairing,
|
|
20
|
+
} = require('../utils/message-validator');
|
|
21
|
+
const {
|
|
22
|
+
EntryTypes,
|
|
23
|
+
createBranchSummaryMessage,
|
|
24
|
+
createCompactionSummaryMessage,
|
|
25
|
+
createCustomMessage,
|
|
26
|
+
convertToLlm,
|
|
27
|
+
buildSessionContext
|
|
28
|
+
} = require('./session-entry');
|
|
29
|
+
|
|
30
|
+
// Default compaction settings
|
|
31
|
+
const DEFAULT_COMPACTION_SETTINGS = {
|
|
32
|
+
enabled: true,
|
|
33
|
+
reserveTokens: 16384,
|
|
34
|
+
keepRecentTokens: 20000
|
|
35
|
+
};
|
|
13
36
|
|
|
14
|
-
//
|
|
37
|
+
// Model context limits
|
|
15
38
|
const MODEL_CONTEXT_LIMITS = {
|
|
16
39
|
'deepseek-chat': 100000,
|
|
17
40
|
'deepseek-coder': 100000,
|
|
@@ -26,56 +49,641 @@ const MODEL_CONTEXT_LIMITS = {
|
|
|
26
49
|
'claude-3-5-sonnet': 110000,
|
|
27
50
|
'claude-3-opus': 110000,
|
|
28
51
|
'claude-3-sonnet': 110000,
|
|
29
|
-
'
|
|
52
|
+
'claude-3-haiku': 110000,
|
|
53
|
+
'claude-4-sonnet': 110000,
|
|
54
|
+
'claude-4-opus': 110000,
|
|
55
|
+
'glm-5.1': 110000
|
|
30
56
|
};
|
|
31
57
|
|
|
32
|
-
//
|
|
33
|
-
const
|
|
58
|
+
// Compression timeout
|
|
59
|
+
const COMPRESSION_TIMEOUT_MS = 120000;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* CompactionDetails - File operation details stored on generated compaction entries
|
|
63
|
+
*/
|
|
64
|
+
class CompactionDetails {
|
|
65
|
+
constructor(readFiles = [], modifiedFiles = []) {
|
|
66
|
+
this.readFiles = readFiles;
|
|
67
|
+
this.modifiedFiles = modifiedFiles;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Result - Result of a fallible operation
|
|
73
|
+
*/
|
|
74
|
+
class Result {
|
|
75
|
+
constructor(ok, value, error) {
|
|
76
|
+
this.ok = ok;
|
|
77
|
+
this.value = value;
|
|
78
|
+
this.error = error;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static success(value) {
|
|
82
|
+
return new Result(true, value, null);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static failure(error) {
|
|
86
|
+
return new Result(false, null, error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* CompactionError
|
|
92
|
+
*/
|
|
93
|
+
class CompactionError extends Error {
|
|
94
|
+
constructor(code, message, cause) {
|
|
95
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
96
|
+
this.name = 'CompactionError';
|
|
97
|
+
this.code = code;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* BranchSummaryError
|
|
103
|
+
*/
|
|
104
|
+
class BranchSummaryError extends Error {
|
|
105
|
+
constructor(code, message, cause) {
|
|
106
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
107
|
+
this.name = 'BranchSummaryError';
|
|
108
|
+
this.code = code;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// File operations utilities
|
|
113
|
+
function createFileOps() {
|
|
114
|
+
return {
|
|
115
|
+
read: new Set(),
|
|
116
|
+
written: new Set(),
|
|
117
|
+
edited: new Set()
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractFileOpsFromMessage(message, fileOps) {
|
|
122
|
+
if (message.role !== 'assistant') return;
|
|
123
|
+
if (!message.content || !Array.isArray(message.content)) return;
|
|
124
|
+
|
|
125
|
+
for (const block of message.content) {
|
|
126
|
+
if (!block || block.type !== 'toolCall') continue;
|
|
127
|
+
|
|
128
|
+
const args = block.arguments;
|
|
129
|
+
if (!args) continue;
|
|
130
|
+
|
|
131
|
+
const filePath = typeof args.path === 'string' ? args.path : undefined;
|
|
132
|
+
if (!filePath) continue;
|
|
133
|
+
|
|
134
|
+
switch (block.name) {
|
|
135
|
+
case 'read':
|
|
136
|
+
fileOps.read.add(filePath);
|
|
137
|
+
break;
|
|
138
|
+
case 'write':
|
|
139
|
+
fileOps.written.add(filePath);
|
|
140
|
+
break;
|
|
141
|
+
case 'edit':
|
|
142
|
+
fileOps.edited.add(filePath);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function computeFileLists(fileOps) {
|
|
149
|
+
const modified = new Set([...fileOps.edited, ...fileOps.written]);
|
|
150
|
+
const readOnly = [...fileOps.read].filter(f => !modified.has(f)).sort();
|
|
151
|
+
const modifiedFiles = [...modified].sort();
|
|
152
|
+
return { readFiles: readOnly, modifiedFiles };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatFileOperations(readFiles, modifiedFiles) {
|
|
156
|
+
const sections = [];
|
|
157
|
+
if (readFiles.length > 0) {
|
|
158
|
+
sections.push(`<read-files>\n${readFiles.join('\n')}\n</read-files>`);
|
|
159
|
+
}
|
|
160
|
+
if (modifiedFiles.length > 0) {
|
|
161
|
+
sections.push(`<modified-files>\n${modifiedFiles.join('\n')}\n</modified-files>`);
|
|
162
|
+
}
|
|
163
|
+
if (sections.length === 0) return '';
|
|
164
|
+
return `\n\n${sections.join('\n\n')}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const TOOL_RESULT_MAX_CHARS = 2000;
|
|
168
|
+
|
|
169
|
+
function truncateForSummary(text, maxChars) {
|
|
170
|
+
if (text.length <= maxChars) return text;
|
|
171
|
+
const truncatedChars = text.length - maxChars;
|
|
172
|
+
return `${text.slice(0, maxChars)}\n\n[... ${truncatedChars} more characters truncated]`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function serializeConversation(messages) {
|
|
176
|
+
const parts = [];
|
|
177
|
+
|
|
178
|
+
for (const msg of messages) {
|
|
179
|
+
if (msg.role === 'user') {
|
|
180
|
+
const content = Array.isArray(msg.content)
|
|
181
|
+
? msg.content.filter(c => c.type === 'text').map(c => c.text).join('')
|
|
182
|
+
: msg.content;
|
|
183
|
+
if (content) parts.push(`[User]: ${content}`);
|
|
184
|
+
} else if (msg.role === 'assistant') {
|
|
185
|
+
const textParts = [];
|
|
186
|
+
const thinkingParts = [];
|
|
187
|
+
const toolCalls = [];
|
|
188
|
+
|
|
189
|
+
for (const block of msg.content || []) {
|
|
190
|
+
if (block.type === 'text') {
|
|
191
|
+
textParts.push(block.text);
|
|
192
|
+
} else if (block.type === 'thinking') {
|
|
193
|
+
thinkingParts.push(block.thinking);
|
|
194
|
+
} else if (block.type === 'toolCall') {
|
|
195
|
+
const argsStr = Object.entries(block.arguments || {})
|
|
196
|
+
.map(([k, v]) => `${k}=${safeJsonStringify(v)}`)
|
|
197
|
+
.join(', ');
|
|
198
|
+
toolCalls.push(`${block.name}(${argsStr})`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (thinkingParts.length > 0) {
|
|
203
|
+
parts.push(`[Assistant thinking]: ${thinkingParts.join('\n')}`);
|
|
204
|
+
}
|
|
205
|
+
if (textParts.length > 0) {
|
|
206
|
+
parts.push(`[Assistant]: ${textParts.join('\n')}`);
|
|
207
|
+
}
|
|
208
|
+
if (toolCalls.length > 0) {
|
|
209
|
+
parts.push(`[Assistant tool calls]: ${toolCalls.join('; ')}`);
|
|
210
|
+
}
|
|
211
|
+
} else if (msg.role === 'toolResult') {
|
|
212
|
+
const content = Array.isArray(msg.content)
|
|
213
|
+
? msg.content.filter(c => c.type === 'text').map(c => c.text).join('')
|
|
214
|
+
: msg.content;
|
|
215
|
+
if (content) {
|
|
216
|
+
parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return parts.join('\n\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getMessageFromEntry(entry) {
|
|
225
|
+
switch (entry.type) {
|
|
226
|
+
case EntryTypes.MESSAGE:
|
|
227
|
+
if (entry.message.role === 'toolResult') return undefined;
|
|
228
|
+
return entry.message;
|
|
229
|
+
case EntryTypes.CUSTOM_MESSAGE:
|
|
230
|
+
return createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);
|
|
231
|
+
case EntryTypes.BRANCH_SUMMARY:
|
|
232
|
+
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
|
|
233
|
+
case EntryTypes.COMPACTION:
|
|
234
|
+
return createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);
|
|
235
|
+
default:
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getMessageFromEntryForCompaction(entry) {
|
|
241
|
+
if (entry.type === EntryTypes.COMPACTION) {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
return getMessageFromEntry(entry);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function findValidCutPoints(entries, startIndex, endIndex) {
|
|
248
|
+
const cutPoints = [];
|
|
249
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
250
|
+
const entry = entries[i];
|
|
251
|
+
if (entry.type === EntryTypes.MESSAGE) {
|
|
252
|
+
const role = entry.message.role;
|
|
253
|
+
switch (role) {
|
|
254
|
+
case 'bashExecution':
|
|
255
|
+
case 'custom':
|
|
256
|
+
case 'branchSummary':
|
|
257
|
+
case 'compactionSummary':
|
|
258
|
+
case 'user':
|
|
259
|
+
case 'assistant':
|
|
260
|
+
cutPoints.push(i);
|
|
261
|
+
break;
|
|
262
|
+
case 'toolResult':
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (entry.type === EntryTypes.BRANCH_SUMMARY || entry.type === EntryTypes.CUSTOM_MESSAGE) {
|
|
267
|
+
cutPoints.push(i);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return cutPoints;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function findTurnStartIndex(entries, entryIndex, startIndex) {
|
|
274
|
+
for (let i = entryIndex; i >= startIndex; i--) {
|
|
275
|
+
const entry = entries[i];
|
|
276
|
+
if (entry.type === EntryTypes.BRANCH_SUMMARY || entry.type === EntryTypes.CUSTOM_MESSAGE) {
|
|
277
|
+
return i;
|
|
278
|
+
}
|
|
279
|
+
if (entry.type === EntryTypes.MESSAGE) {
|
|
280
|
+
const role = entry.message.role;
|
|
281
|
+
if (role === 'user' || role === 'bashExecution') {
|
|
282
|
+
return i;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return -1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Find the compaction cut point that keeps approximately the requested recent-token budget
|
|
291
|
+
*/
|
|
292
|
+
function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
|
|
293
|
+
const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
|
|
294
|
+
|
|
295
|
+
if (cutPoints.length === 0) {
|
|
296
|
+
return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };
|
|
297
|
+
}
|
|
298
|
+
let accumulatedTokens = 0;
|
|
299
|
+
let cutIndex = cutPoints[0];
|
|
300
|
+
|
|
301
|
+
for (let i = endIndex - 1; i >= startIndex; i--) {
|
|
302
|
+
const entry = entries[i];
|
|
303
|
+
if (entry.type !== EntryTypes.MESSAGE) continue;
|
|
304
|
+
const messageTokens = estimateTokens(entry.message);
|
|
305
|
+
accumulatedTokens += messageTokens;
|
|
306
|
+
if (accumulatedTokens >= keepRecentTokens) {
|
|
307
|
+
for (let c = 0; c < cutPoints.length; c++) {
|
|
308
|
+
if (cutPoints[c] >= i) {
|
|
309
|
+
cutIndex = cutPoints[c];
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
while (cutIndex > startIndex) {
|
|
317
|
+
const prevEntry = entries[cutIndex - 1];
|
|
318
|
+
if (prevEntry.type === EntryTypes.COMPACTION) break;
|
|
319
|
+
if (prevEntry.type === EntryTypes.MESSAGE) break;
|
|
320
|
+
cutIndex--;
|
|
321
|
+
}
|
|
322
|
+
const cutEntry = entries[cutIndex];
|
|
323
|
+
const isUserMessage = cutEntry.type === EntryTypes.MESSAGE && cutEntry.message.role === 'user';
|
|
324
|
+
const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
firstKeptEntryIndex: cutIndex,
|
|
328
|
+
turnStartIndex,
|
|
329
|
+
isSplitTurn: !isUserMessage && turnStartIndex !== -1
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.
|
|
334
|
+
|
|
335
|
+
Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`;
|
|
336
|
+
|
|
337
|
+
const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
|
|
338
|
+
|
|
339
|
+
Use this EXACT format:
|
|
340
|
+
|
|
341
|
+
## Goal
|
|
342
|
+
[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.]
|
|
343
|
+
|
|
344
|
+
## Constraints & Preferences
|
|
345
|
+
- [Any constraints, preferences, or requirements mentioned by user]
|
|
346
|
+
- [Or "(none)" if none were mentioned]
|
|
347
|
+
|
|
348
|
+
## Progress
|
|
349
|
+
### Done
|
|
350
|
+
- [x] [Completed tasks/changes]
|
|
351
|
+
|
|
352
|
+
### In Progress
|
|
353
|
+
- [ ] [Current work]
|
|
354
|
+
|
|
355
|
+
### Blocked
|
|
356
|
+
- [Issues preventing progress, if any]
|
|
357
|
+
|
|
358
|
+
## Key Decisions
|
|
359
|
+
- **[Decision]**: [Brief rationale]
|
|
360
|
+
|
|
361
|
+
## Next Steps
|
|
362
|
+
1. [Ordered list of what should happen next]
|
|
363
|
+
|
|
364
|
+
## Critical Context
|
|
365
|
+
- [Any data, examples, or references needed to continue]
|
|
366
|
+
- [Or "(none)" if not applicable]
|
|
367
|
+
|
|
368
|
+
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
|
369
|
+
|
|
370
|
+
const UPDATE_SUMMARIZATION_PROMPT = `The messages above are NEW conversation messages to incorporate into the existing summary provided in <previous-summary> tags.
|
|
371
|
+
|
|
372
|
+
Update the existing structured summary with new information. RULES:
|
|
373
|
+
- PRESERVE all existing information from the previous summary
|
|
374
|
+
- ADD new progress, decisions, and context from the new messages
|
|
375
|
+
- UPDATE the Progress section: move items from "In Progress" to "Done" when completed
|
|
376
|
+
- UPDATE "Next Steps" based on what was accomplished
|
|
377
|
+
- PRESERVE exact file paths, function names, and error messages
|
|
378
|
+
- If something is no longer relevant, you may remove it
|
|
379
|
+
|
|
380
|
+
Use this EXACT format:
|
|
381
|
+
|
|
382
|
+
## Goal
|
|
383
|
+
[Preserve existing goals, add new ones if the task expanded]
|
|
384
|
+
|
|
385
|
+
## Constraints & Preferences
|
|
386
|
+
- [Preserve existing, add new ones discovered]
|
|
387
|
+
|
|
388
|
+
## Progress
|
|
389
|
+
### Done
|
|
390
|
+
- [x] [Include previously done items AND newly completed items]
|
|
391
|
+
|
|
392
|
+
### In Progress
|
|
393
|
+
- [ ] [Current work - update based on progress]
|
|
394
|
+
|
|
395
|
+
### Blocked
|
|
396
|
+
- [Current blockers - remove if resolved]
|
|
397
|
+
|
|
398
|
+
## Key Decisions
|
|
399
|
+
- **[Decision]**: [Brief rationale] (preserve all previous, add new)
|
|
400
|
+
|
|
401
|
+
## Next Steps
|
|
402
|
+
1. [Update based on current state]
|
|
403
|
+
|
|
404
|
+
## Critical Context
|
|
405
|
+
- [Preserve important context, add new if needed]
|
|
406
|
+
|
|
407
|
+
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
|
408
|
+
|
|
409
|
+
const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
|
|
410
|
+
|
|
411
|
+
Summarize the prefix to provide context for the retained suffix:
|
|
412
|
+
|
|
413
|
+
## Original Request
|
|
414
|
+
[What did the user ask for in this turn?]
|
|
415
|
+
|
|
416
|
+
## Early Progress
|
|
417
|
+
- [Key decisions and work done in the prefix]
|
|
418
|
+
|
|
419
|
+
## Context for Suffix
|
|
420
|
+
- [Information needed to understand the retained recent work]
|
|
421
|
+
|
|
422
|
+
Be concise. Focus on what's needed to understand the kept suffix.`;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Extract file operations from messages and previous compaction
|
|
426
|
+
*/
|
|
427
|
+
function extractFileOperations(messages, entries, prevCompactionIndex) {
|
|
428
|
+
const fileOps = createFileOps();
|
|
429
|
+
if (prevCompactionIndex >= 0) {
|
|
430
|
+
const prevCompaction = entries[prevCompactionIndex];
|
|
431
|
+
if (!prevCompaction.fromHook && prevCompaction.details) {
|
|
432
|
+
const details = prevCompaction.details;
|
|
433
|
+
if (Array.isArray(details.readFiles)) {
|
|
434
|
+
for (const f of details.readFiles) fileOps.read.add(f);
|
|
435
|
+
}
|
|
436
|
+
if (Array.isArray(details.modifiedFiles)) {
|
|
437
|
+
for (const f of details.modifiedFiles) {
|
|
438
|
+
fileOps.edited.add(f);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const msg of messages) {
|
|
444
|
+
extractFileOpsFromMessage(msg, fileOps);
|
|
445
|
+
}
|
|
446
|
+
return fileOps;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* CompactionResult - Prepared compaction data ready to be persisted
|
|
451
|
+
*/
|
|
452
|
+
class CompactionResult {
|
|
453
|
+
constructor(summary, firstKeptEntryId, tokensBefore, details) {
|
|
454
|
+
this.summary = summary;
|
|
455
|
+
this.firstKeptEntryId = firstKeptEntryId;
|
|
456
|
+
this.tokensBefore = tokensBefore;
|
|
457
|
+
this.details = details;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Prepare session entries for compaction
|
|
463
|
+
*/
|
|
464
|
+
function prepareCompaction(pathEntries, settings) {
|
|
465
|
+
if (pathEntries.length === 0 || pathEntries[pathEntries.length - 1].type === EntryTypes.COMPACTION) {
|
|
466
|
+
return Result.success(undefined);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
let prevCompactionIndex = -1;
|
|
470
|
+
for (let i = pathEntries.length - 1; i >= 0; i--) {
|
|
471
|
+
if (pathEntries[i].type === EntryTypes.COMPACTION) {
|
|
472
|
+
prevCompactionIndex = i;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
let previousSummary;
|
|
478
|
+
let boundaryStart = 0;
|
|
479
|
+
if (prevCompactionIndex >= 0) {
|
|
480
|
+
const prevCompaction = pathEntries[prevCompactionIndex];
|
|
481
|
+
previousSummary = prevCompaction.summary;
|
|
482
|
+
const firstKeptEntryIndex = pathEntries.findIndex(entry => entry.id === prevCompaction.firstKeptEntryId);
|
|
483
|
+
boundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;
|
|
484
|
+
}
|
|
485
|
+
const boundaryEnd = pathEntries.length;
|
|
34
486
|
|
|
487
|
+
const tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;
|
|
488
|
+
|
|
489
|
+
const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
|
|
490
|
+
const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];
|
|
491
|
+
if (!firstKeptEntry?.id) {
|
|
492
|
+
return Result.failure(new CompactionError('invalid_session', 'First kept entry has no UUID'));
|
|
493
|
+
}
|
|
494
|
+
const firstKeptEntryId = firstKeptEntry.id;
|
|
495
|
+
|
|
496
|
+
const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
|
|
497
|
+
const messagesToSummarize = [];
|
|
498
|
+
for (let i = boundaryStart; i < historyEnd; i++) {
|
|
499
|
+
const msg = getMessageFromEntryForCompaction(pathEntries[i]);
|
|
500
|
+
if (msg) messagesToSummarize.push(msg);
|
|
501
|
+
}
|
|
502
|
+
const turnPrefixMessages = [];
|
|
503
|
+
if (cutPoint.isSplitTurn) {
|
|
504
|
+
for (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {
|
|
505
|
+
const msg = getMessageFromEntryForCompaction(pathEntries[i]);
|
|
506
|
+
if (msg) turnPrefixMessages.push(msg);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
|
|
510
|
+
if (cutPoint.isSplitTurn) {
|
|
511
|
+
for (const msg of turnPrefixMessages) {
|
|
512
|
+
extractFileOpsFromMessage(msg, fileOps);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return Result.success({
|
|
517
|
+
firstKeptEntryId,
|
|
518
|
+
messagesToSummarize,
|
|
519
|
+
turnPrefixMessages,
|
|
520
|
+
isSplitTurn: cutPoint.isSplitTurn,
|
|
521
|
+
tokensBefore,
|
|
522
|
+
previousSummary,
|
|
523
|
+
fileOps,
|
|
524
|
+
settings
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Branch summarization utilities
|
|
529
|
+
const BRANCH_SUMMARY_PREAMBLE = `The user explored a different conversation branch before returning here.
|
|
530
|
+
Summary of that exploration:
|
|
531
|
+
|
|
532
|
+
`;
|
|
533
|
+
|
|
534
|
+
const BRANCH_SUMMARY_PROMPT = `Create a structured summary of this conversation branch for context when returning later.
|
|
535
|
+
|
|
536
|
+
Use this EXACT format:
|
|
537
|
+
|
|
538
|
+
## Goal
|
|
539
|
+
[What was the user trying to accomplish in this branch?]
|
|
540
|
+
|
|
541
|
+
## Constraints & Preferences
|
|
542
|
+
- [Any constraints, preferences, or requirements mentioned]
|
|
543
|
+
- [Or "(none)" if none were mentioned]
|
|
544
|
+
|
|
545
|
+
## Progress
|
|
546
|
+
### Done
|
|
547
|
+
- [x] [Completed tasks/changes]
|
|
548
|
+
|
|
549
|
+
### In Progress
|
|
550
|
+
- [ ] [Work that was started but not finished]
|
|
551
|
+
|
|
552
|
+
### Blocked
|
|
553
|
+
- [Issues preventing progress, if any]
|
|
554
|
+
|
|
555
|
+
## Key Decisions
|
|
556
|
+
- **[Decision]**: [Brief rationale]
|
|
557
|
+
|
|
558
|
+
## Next Steps
|
|
559
|
+
1. [What should happen next to continue this work]
|
|
560
|
+
|
|
561
|
+
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* BranchPreparation - Prepared branch content for summarization
|
|
565
|
+
*/
|
|
566
|
+
class BranchPreparation {
|
|
567
|
+
constructor(messages, fileOps, totalTokens) {
|
|
568
|
+
this.messages = messages;
|
|
569
|
+
this.fileOps = fileOps;
|
|
570
|
+
this.totalTokens = totalTokens;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Prepare branch entries for summarization within an optional token budget
|
|
576
|
+
*/
|
|
577
|
+
function prepareBranchEntries(entries, tokenBudget = 0) {
|
|
578
|
+
const messages = [];
|
|
579
|
+
const fileOps = createFileOps();
|
|
580
|
+
let totalTokens = 0;
|
|
581
|
+
|
|
582
|
+
for (const entry of entries) {
|
|
583
|
+
if (entry.type === EntryTypes.BRANCH_SUMMARY && !entry.fromHook && entry.details) {
|
|
584
|
+
const details = entry.details;
|
|
585
|
+
if (Array.isArray(details.readFiles)) {
|
|
586
|
+
for (const f of details.readFiles) fileOps.read.add(f);
|
|
587
|
+
}
|
|
588
|
+
if (Array.isArray(details.modifiedFiles)) {
|
|
589
|
+
for (const f of details.modifiedFiles) {
|
|
590
|
+
fileOps.edited.add(f);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
597
|
+
const entry = entries[i];
|
|
598
|
+
const message = getMessageFromEntry(entry);
|
|
599
|
+
if (!message) continue;
|
|
600
|
+
extractFileOpsFromMessage(message, fileOps);
|
|
601
|
+
|
|
602
|
+
const tokens = estimateTokens(message);
|
|
603
|
+
if (tokenBudget > 0 && totalTokens + tokens > tokenBudget) {
|
|
604
|
+
if (entry.type === EntryTypes.COMPACTION || entry.type === EntryTypes.BRANCH_SUMMARY) {
|
|
605
|
+
if (totalTokens < tokenBudget * 0.9) {
|
|
606
|
+
messages.unshift(message);
|
|
607
|
+
totalTokens += tokens;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
messages.unshift(message);
|
|
614
|
+
totalTokens += tokens;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return new BranchPreparation(messages, fileOps, totalTokens);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Collect entries that should be summarized before navigating to a different session tree entry
|
|
622
|
+
*/
|
|
623
|
+
function collectEntriesForBranchSummary(session, oldLeafId, targetId) {
|
|
624
|
+
if (!oldLeafId) {
|
|
625
|
+
return { entries: [], commonAncestorId: null };
|
|
626
|
+
}
|
|
627
|
+
const oldPath = new Set(session.getBranch(oldLeafId).map(e => e.id));
|
|
628
|
+
const targetPath = session.getBranch(targetId);
|
|
629
|
+
let commonAncestorId = null;
|
|
630
|
+
for (let i = targetPath.length - 1; i >= 0; i--) {
|
|
631
|
+
if (oldPath.has(targetPath[i].id)) {
|
|
632
|
+
commonAncestorId = targetPath[i].id;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const entries = [];
|
|
637
|
+
let current = oldLeafId;
|
|
638
|
+
|
|
639
|
+
while (current && current !== commonAncestorId) {
|
|
640
|
+
const entry = session.getEntry(current);
|
|
641
|
+
if (!entry) break;
|
|
642
|
+
entries.push(entry);
|
|
643
|
+
current = entry.parentId;
|
|
644
|
+
}
|
|
645
|
+
entries.reverse();
|
|
646
|
+
|
|
647
|
+
return { entries, commonAncestorId };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* ContextCompressor - Main compression class
|
|
652
|
+
*/
|
|
35
653
|
class ContextCompressor {
|
|
36
|
-
/**
|
|
37
|
-
* @param {Object} config - 配置
|
|
38
|
-
*/
|
|
39
654
|
constructor(config = {}) {
|
|
40
655
|
this.config = config;
|
|
41
656
|
this.agent = config.agent;
|
|
42
657
|
this.framework = config.framework;
|
|
43
658
|
|
|
44
|
-
// 模型相关配置
|
|
45
659
|
this.model = config.model || 'deepseek-chat';
|
|
46
660
|
this._maxContextTokens = config.maxContextTokens || this._getDefaultContextLimit();
|
|
47
661
|
this._keepRecentMessages = config.keepRecentMessages || 20;
|
|
48
662
|
this._enableSmartCompress = config.enableSmartCompress !== false;
|
|
663
|
+
this._compactionSettings = config.compactionSettings || DEFAULT_COMPACTION_SETTINGS;
|
|
49
664
|
|
|
50
|
-
// 压缩状态
|
|
51
665
|
this._compressionCount = 0;
|
|
52
666
|
this._compressionInProgress = false;
|
|
53
667
|
this._compressionPromise = null;
|
|
54
|
-
this._compressionTimeoutId = null;
|
|
55
|
-
|
|
56
|
-
// 工具结果压缩配置
|
|
668
|
+
this._compressionTimeoutId = null;
|
|
57
669
|
this._maxToolResultSize = config.maxToolResultSize || 4000;
|
|
670
|
+
this._tokenCounter = new TokenCounter();
|
|
58
671
|
}
|
|
59
672
|
|
|
60
|
-
/**
|
|
61
|
-
* 获取默认上下文限制
|
|
62
|
-
* @returns {number}
|
|
63
|
-
* @private
|
|
64
|
-
*/
|
|
65
673
|
_getDefaultContextLimit() {
|
|
66
|
-
const modelKey = Object.keys(MODEL_CONTEXT_LIMITS).find(
|
|
674
|
+
const modelKey = Object.keys(MODEL_CONTEXT_LIMITS).find(k =>
|
|
67
675
|
this.model.toLowerCase().includes(k.toLowerCase())
|
|
68
676
|
);
|
|
69
677
|
return modelKey ? MODEL_CONTEXT_LIMITS[modelKey] : 40000;
|
|
70
678
|
}
|
|
71
679
|
|
|
72
680
|
/**
|
|
73
|
-
*
|
|
74
|
-
* @param {string} sessionId - Session ID
|
|
75
|
-
* @param {Array} messages - 消息数组引用
|
|
76
|
-
* @param {Object} messageStore - 消息存储
|
|
77
|
-
* @returns {Promise}
|
|
681
|
+
* Validate message pairing (delegate to message-validator)
|
|
78
682
|
*/
|
|
683
|
+
validateMessagesPairing(messages) {
|
|
684
|
+
return validateMessagesPairing(messages);
|
|
685
|
+
}
|
|
686
|
+
|
|
79
687
|
async compress(sessionId, messages, messageStore) {
|
|
80
688
|
if (this._compressionInProgress && this._compressionPromise) {
|
|
81
689
|
logger.debug('Compression already in progress, waiting...');
|
|
@@ -88,30 +696,23 @@ class ContextCompressor {
|
|
|
88
696
|
|
|
89
697
|
this._compressionInProgress = true;
|
|
90
698
|
|
|
91
|
-
this._compressionPromise = this._executeWithTimeout(sessionId, messages, messageStore).finally(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
clearTimeout(this._compressionTimeoutId);
|
|
98
|
-
this._compressionTimeoutId = null;
|
|
99
|
-
}
|
|
699
|
+
this._compressionPromise = this._executeWithTimeout(sessionId, messages, messageStore).finally(() => {
|
|
700
|
+
this._compressionInProgress = false;
|
|
701
|
+
this._compressionPromise = null;
|
|
702
|
+
if (this._compressionTimeoutId) {
|
|
703
|
+
clearTimeout(this._compressionTimeoutId);
|
|
704
|
+
this._compressionTimeoutId = null;
|
|
100
705
|
}
|
|
101
|
-
);
|
|
706
|
+
});
|
|
102
707
|
|
|
103
708
|
return this._compressionPromise;
|
|
104
709
|
}
|
|
105
710
|
|
|
106
|
-
/**
|
|
107
|
-
* 带超时的压缩执行
|
|
108
|
-
* @private
|
|
109
|
-
*/
|
|
110
711
|
async _executeWithTimeout(sessionId, messages, messageStore) {
|
|
111
712
|
try {
|
|
112
713
|
return await Promise.race([
|
|
113
714
|
this._doCompress(sessionId, messages, messageStore),
|
|
114
|
-
this._createTimeoutPromise()
|
|
715
|
+
this._createTimeoutPromise()
|
|
115
716
|
]);
|
|
116
717
|
} catch (err) {
|
|
117
718
|
logger.warn('Compression failed:', err.message);
|
|
@@ -119,23 +720,15 @@ class ContextCompressor {
|
|
|
119
720
|
}
|
|
120
721
|
}
|
|
121
722
|
|
|
122
|
-
/**
|
|
123
|
-
* 创建超时 Promise
|
|
124
|
-
* @returns {Promise}
|
|
125
|
-
* @private
|
|
126
|
-
*/
|
|
127
723
|
_createTimeoutPromise() {
|
|
128
724
|
return new Promise((_, reject) => {
|
|
129
725
|
this._compressionTimeoutId = setTimeout(() => {
|
|
130
726
|
this._compressionTimeoutId = null;
|
|
131
|
-
reject(new Error(`Compression timeout (${
|
|
132
|
-
},
|
|
727
|
+
reject(new Error(`Compression timeout (${COMPRESSION_TIMEOUT_MS}ms)`));
|
|
728
|
+
}, COMPRESSION_TIMEOUT_MS);
|
|
133
729
|
});
|
|
134
730
|
}
|
|
135
731
|
|
|
136
|
-
/**
|
|
137
|
-
* 取消正在进行的压缩
|
|
138
|
-
*/
|
|
139
732
|
cancelCompression() {
|
|
140
733
|
if (this._compressionTimeoutId) {
|
|
141
734
|
clearTimeout(this._compressionTimeoutId);
|
|
@@ -145,372 +738,155 @@ class ContextCompressor {
|
|
|
145
738
|
this._compressionPromise = null;
|
|
146
739
|
}
|
|
147
740
|
|
|
148
|
-
/**
|
|
149
|
-
* 执行智能压缩(使用 AI 生成摘要)
|
|
150
|
-
* @private
|
|
151
|
-
*/
|
|
152
741
|
async _doCompress(sessionId, messages, messageStore) {
|
|
153
|
-
const systemMessages = messages.filter(
|
|
154
|
-
const otherMessages = messages.filter(
|
|
155
|
-
|
|
156
|
-
// 保留最近的 N 条非系统消息
|
|
742
|
+
const systemMessages = messages.filter(m => m.role === 'system');
|
|
743
|
+
const otherMessages = messages.filter(m => m.role !== 'system');
|
|
157
744
|
const recentMessages = otherMessages.slice(-this._keepRecentMessages);
|
|
158
745
|
const messagesToSummarize = otherMessages.slice(0, -this._keepRecentMessages);
|
|
159
746
|
|
|
160
747
|
const compressedCount = messagesToSummarize.length;
|
|
161
|
-
|
|
162
748
|
let summaryContent = '';
|
|
163
749
|
|
|
164
|
-
// 使用 AI 生成摘要
|
|
165
750
|
if (this._enableSmartCompress && this.agent?._chatHandler?._aiClient) {
|
|
166
751
|
try {
|
|
167
752
|
const summaryText = await this._summarizeMessages(messagesToSummarize);
|
|
168
|
-
summaryContent = `[
|
|
753
|
+
summaryContent = `[Early conversation summary]: ${summaryText || '(no content)'}`;
|
|
169
754
|
} catch (err) {
|
|
170
755
|
logger.warn('AI summary failed, using simple compression:', err.message);
|
|
171
|
-
summaryContent = `[
|
|
756
|
+
summaryContent = `[Context compressed: ${compressedCount} early messages omitted. Kept recent ${this._keepRecentMessages} messages.]`;
|
|
172
757
|
}
|
|
173
758
|
} else {
|
|
174
|
-
summaryContent = `[
|
|
759
|
+
summaryContent = `[Context compressed: ${compressedCount} early messages omitted. Kept recent ${this._keepRecentMessages} messages.]`;
|
|
175
760
|
}
|
|
176
761
|
|
|
177
762
|
const summary = {
|
|
178
763
|
role: 'assistant',
|
|
179
|
-
content: summaryContent
|
|
764
|
+
content: summaryContent
|
|
180
765
|
};
|
|
181
766
|
|
|
182
|
-
// 构建保留的消息(确保 tool call 和 tool result 配对)
|
|
183
|
-
const filteredRecentMessages = this._filterPairedMessages(recentMessages);
|
|
184
|
-
|
|
185
|
-
// 替换消息数组
|
|
186
767
|
messages.length = 0;
|
|
187
|
-
messages.push(...systemMessages, summary, ...
|
|
768
|
+
messages.push(...systemMessages, summary, ...recentMessages);
|
|
188
769
|
|
|
189
|
-
// 更新压缩状态
|
|
190
770
|
this._compressionCount++;
|
|
191
|
-
const tokenCount = this.
|
|
771
|
+
const tokenCount = this._tokenCounter.countMessages(messages);
|
|
192
772
|
if (messageStore.compressionState) {
|
|
193
773
|
messageStore.compressionState.count++;
|
|
194
774
|
messageStore.compressionState.lastCompressedAt = Date.now();
|
|
195
775
|
messageStore.compressionState.lastTokenCount = tokenCount;
|
|
196
776
|
}
|
|
197
|
-
// 同步更新 SessionContext.metadata.compressionCount(如果可用)
|
|
198
777
|
if (typeof messageStore.recordCompression === 'function') {
|
|
199
778
|
messageStore.recordCompression(tokenCount);
|
|
200
779
|
}
|
|
201
780
|
|
|
202
|
-
logger.info(
|
|
203
|
-
`Context compressed (${this._compressionCount} times). Messages: ${messages.length}`
|
|
204
|
-
);
|
|
781
|
+
logger.info(`Context compressed (${this._compressionCount} times). Messages: ${messages.length}`);
|
|
205
782
|
}
|
|
206
783
|
|
|
207
|
-
/**
|
|
208
|
-
* 简单压缩(截断保留最近消息)
|
|
209
|
-
* @private
|
|
210
|
-
*/
|
|
211
784
|
_simpleCompress(sessionId, messages, messageStore) {
|
|
212
|
-
const systemMessages = messages.filter(
|
|
213
|
-
const otherMessages = messages.filter(
|
|
785
|
+
const systemMessages = messages.filter(m => m.role === 'system');
|
|
786
|
+
const otherMessages = messages.filter(m => m.role !== 'system');
|
|
214
787
|
const recentMessages = otherMessages.slice(-this._keepRecentMessages);
|
|
215
788
|
const compressedCount = otherMessages.length - this._keepRecentMessages;
|
|
216
789
|
|
|
217
|
-
const summaryContent = `[
|
|
790
|
+
const summaryContent = `[Context compressed: ${compressedCount} early messages omitted. Kept recent ${this._keepRecentMessages} messages.]`;
|
|
218
791
|
|
|
219
792
|
const summary = {
|
|
220
793
|
role: 'assistant',
|
|
221
|
-
content: summaryContent
|
|
794
|
+
content: summaryContent
|
|
222
795
|
};
|
|
223
796
|
|
|
224
|
-
// 确保 tool call 和 tool result 配对
|
|
225
|
-
const filteredRecentMessages = this._filterPairedMessages(recentMessages);
|
|
226
|
-
|
|
227
797
|
messages.length = 0;
|
|
228
|
-
messages.push(...systemMessages, summary, ...
|
|
798
|
+
messages.push(...systemMessages, summary, ...recentMessages);
|
|
229
799
|
|
|
230
800
|
this._compressionCount++;
|
|
231
801
|
if (messageStore.compressionState) {
|
|
232
802
|
messageStore.compressionState.count++;
|
|
233
803
|
messageStore.compressionState.lastCompressedAt = Date.now();
|
|
234
|
-
messageStore.compressionState.lastTokenCount = this.
|
|
804
|
+
messageStore.compressionState.lastTokenCount = this._tokenCounter.countMessages(messages);
|
|
235
805
|
}
|
|
236
806
|
|
|
237
807
|
logger.info(`Context simple compressed. Messages: ${messages.length}`);
|
|
238
808
|
}
|
|
239
809
|
|
|
240
|
-
/**
|
|
241
|
-
* 过滤消息,保留配对的 tool call 和 tool result
|
|
242
|
-
* @private
|
|
243
|
-
*/
|
|
244
|
-
_filterPairedMessages(messages) {
|
|
245
|
-
// 工具类型检查
|
|
246
|
-
const isToolCall = (item) =>
|
|
247
|
-
item && (item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId;
|
|
248
|
-
const isToolResult = (item) =>
|
|
249
|
-
item && (item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId;
|
|
250
|
-
|
|
251
|
-
// 第一遍:找出所有 tool call 和它们的 results
|
|
252
|
-
const assistantToolCalls = new Map(); // toolCallId -> assistant message index
|
|
253
|
-
const toolResults = new Map(); // toolCallId -> tool result indices
|
|
254
|
-
|
|
255
|
-
for (let i = 0; i < messages.length; i++) {
|
|
256
|
-
const msg = messages[i];
|
|
257
|
-
if (msg.role === 'assistant') {
|
|
258
|
-
// 格式1: msg.content 中的 tool-call 块
|
|
259
|
-
if (msg.content) {
|
|
260
|
-
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
261
|
-
for (const item of content) {
|
|
262
|
-
if (isToolCall(item)) {
|
|
263
|
-
assistantToolCalls.set(item.toolCallId, i);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
// 格式2: msg.tool_calls 数组 (OpenAI 格式)
|
|
268
|
-
if (Array.isArray(msg.tool_calls)) {
|
|
269
|
-
for (const tc of msg.tool_calls) {
|
|
270
|
-
if (tc.id) {
|
|
271
|
-
assistantToolCalls.set(tc.id, i);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
277
|
-
for (const item of msg.content) {
|
|
278
|
-
if (isToolResult(item)) {
|
|
279
|
-
if (!toolResults.has(item.toolCallId)) {
|
|
280
|
-
toolResults.set(item.toolCallId, []);
|
|
281
|
-
}
|
|
282
|
-
toolResults.get(item.toolCallId).push(i);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// 第二遍:找出哪些 assistant 消息需要保留
|
|
289
|
-
const assistantIndicesToKeep = new Set();
|
|
290
|
-
for (let i = 0; i < messages.length; i++) {
|
|
291
|
-
const msg = messages[i];
|
|
292
|
-
if (msg.role === 'assistant') {
|
|
293
|
-
let hasToolCall = false;
|
|
294
|
-
// 检查 msg.content 格式
|
|
295
|
-
if (msg.content) {
|
|
296
|
-
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
297
|
-
for (const item of content) {
|
|
298
|
-
if (isToolCall(item)) {
|
|
299
|
-
hasToolCall = true;
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
// 检查 msg.tool_calls 格式 (OpenAI)
|
|
305
|
-
if (!hasToolCall && Array.isArray(msg.tool_calls)) {
|
|
306
|
-
for (const tc of msg.tool_calls) {
|
|
307
|
-
if (tc.id) {
|
|
308
|
-
hasToolCall = true;
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
if (hasToolCall) {
|
|
314
|
-
assistantIndicesToKeep.add(i);
|
|
315
|
-
} else if (i >= messages.length - 3) {
|
|
316
|
-
// 保留最近几条 assistant 消息
|
|
317
|
-
assistantIndicesToKeep.add(i);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// 如果有孤儿的 tool call(没有 result),保留该 assistant 消息
|
|
323
|
-
for (const [toolCallId, assistantIdx] of assistantToolCalls) {
|
|
324
|
-
if (!toolResults.has(toolCallId)) {
|
|
325
|
-
assistantIndicesToKeep.add(assistantIdx);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 第三遍:确定哪些 tool result 要保留
|
|
330
|
-
const indicesToKeep = new Set();
|
|
331
|
-
for (let i = 0; i < messages.length; i++) {
|
|
332
|
-
const msg = messages[i];
|
|
333
|
-
if (msg.role === 'tool') {
|
|
334
|
-
let hasPairedAssistant = false;
|
|
335
|
-
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
336
|
-
for (const item of content) {
|
|
337
|
-
if (isToolResult(item)) {
|
|
338
|
-
const assistantIdx = assistantToolCalls.get(item.toolCallId);
|
|
339
|
-
if (assistantIdx !== undefined && assistantIndicesToKeep.has(assistantIdx)) {
|
|
340
|
-
hasPairedAssistant = true;
|
|
341
|
-
break;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
if (hasPairedAssistant) {
|
|
346
|
-
indicesToKeep.add(i);
|
|
347
|
-
}
|
|
348
|
-
} else if (msg.role === 'assistant') {
|
|
349
|
-
if (assistantIndicesToKeep.has(i)) {
|
|
350
|
-
indicesToKeep.add(i);
|
|
351
|
-
}
|
|
352
|
-
} else {
|
|
353
|
-
// user, system 等角色默认保留
|
|
354
|
-
indicesToKeep.add(i);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// 按索引排序并返回
|
|
359
|
-
const sortedIndices = Array.from(indicesToKeep).sort((a, b) => a - b);
|
|
360
|
-
return sortedIndices.map((i) => messages[i]);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* 验证消息配对(防止 orphaned tool result)
|
|
365
|
-
* @param {Array} messages - 消息数组
|
|
366
|
-
* @returns {Array} 验证后的消息
|
|
367
|
-
*/
|
|
368
|
-
validateMessagesPairing(messages) {
|
|
369
|
-
// 收集所有 assistant 的 tool-call IDs(兼容多种类型名)
|
|
370
|
-
const assistantToolCallIds = new Set();
|
|
371
|
-
for (const msg of messages) {
|
|
372
|
-
if (msg.role === 'assistant') {
|
|
373
|
-
// 格式1: msg.content 中的 tool-call 块
|
|
374
|
-
if (Array.isArray(msg.content)) {
|
|
375
|
-
for (const item of msg.content) {
|
|
376
|
-
if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) {
|
|
377
|
-
assistantToolCallIds.add(item.toolCallId);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// 格式2: msg.tool_calls 数组 (OpenAI 格式)
|
|
382
|
-
if (Array.isArray(msg.tool_calls)) {
|
|
383
|
-
for (const tc of msg.tool_calls) {
|
|
384
|
-
if (tc.id) {
|
|
385
|
-
assistantToolCallIds.add(tc.id);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// 检查并删除没有配对的 tool result
|
|
393
|
-
let removedCount = 0;
|
|
394
|
-
for (const msg of messages) {
|
|
395
|
-
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
396
|
-
const originalLength = msg.content.length;
|
|
397
|
-
msg.content = msg.content.filter((item) => {
|
|
398
|
-
// 兼容 tool-result 和 tool_result 两种类型
|
|
399
|
-
if (
|
|
400
|
-
item &&
|
|
401
|
-
(item.type === 'tool-result' || item.type === 'tool_result') &&
|
|
402
|
-
item.toolCallId
|
|
403
|
-
) {
|
|
404
|
-
if (!assistantToolCallIds.has(item.toolCallId)) {
|
|
405
|
-
removedCount++;
|
|
406
|
-
return false; // 删除没有配对的 tool result
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
return true;
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
// 如果所有 content 都被删除了,标记整个消息待删除
|
|
413
|
-
if (msg.content.length === 0 && originalLength > 0) {
|
|
414
|
-
msg._orphaned = true;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// 移除被标记为 orphaned 的 tool 消息
|
|
420
|
-
const originalLength = messages.length;
|
|
421
|
-
const filtered = messages.filter((msg) => !(msg.role === 'tool' && msg._orphaned));
|
|
422
|
-
|
|
423
|
-
if (removedCount > 0 || filtered.length !== originalLength) {
|
|
424
|
-
logger.debug(
|
|
425
|
-
`Removed ${removedCount} orphaned tool-results, ${originalLength - filtered.length} orphaned tool messages`
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return filtered;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* 使用 AI 生成消息摘要
|
|
434
|
-
* @param {Array} messages - 消息数组
|
|
435
|
-
* @returns {Promise<string>}
|
|
436
|
-
* @private
|
|
437
|
-
*/
|
|
438
810
|
async _summarizeMessages(messages) {
|
|
439
811
|
if (!this.framework) {
|
|
440
812
|
throw new Error('Framework not available');
|
|
441
813
|
}
|
|
442
814
|
|
|
443
|
-
// 提取消息文本内容,忽略工具调用等
|
|
444
815
|
const msg_str = messages
|
|
445
|
-
.map(
|
|
816
|
+
.map(m => {
|
|
446
817
|
let text = '';
|
|
447
818
|
if (typeof m.content === 'string') {
|
|
448
819
|
text = m.content;
|
|
449
820
|
} else if (Array.isArray(m.content)) {
|
|
450
|
-
// 只提取 text 类型,忽略 tool-call、tool-result 等
|
|
451
821
|
text = m.content
|
|
452
|
-
.filter(
|
|
453
|
-
.map(
|
|
822
|
+
.filter(c => c.type === 'text')
|
|
823
|
+
.map(c => c.text)
|
|
454
824
|
.join(' ');
|
|
455
825
|
}
|
|
456
|
-
// 跳过 tool 角色的结果(太长且无意义)
|
|
457
826
|
if (m.role === 'tool') {
|
|
458
827
|
return '';
|
|
459
828
|
}
|
|
460
829
|
return `${m.role}: ${text}`;
|
|
461
830
|
})
|
|
462
|
-
.filter(
|
|
831
|
+
.filter(line => line.length > 0)
|
|
463
832
|
.join('\n');
|
|
464
833
|
|
|
465
|
-
const task =
|
|
466
|
-
|
|
467
|
-
${msg_str}`;
|
|
834
|
+
const task = `Summarize the following conversation, keeping only meaningful information (task requirements, decisions, important context). Ignore meaningless chatter and repetitive content. Describe in 1000 characters or less:\n\n${msg_str}`;
|
|
468
835
|
|
|
469
836
|
try {
|
|
470
|
-
// 使用 framework.createSubAgent 创建 Subagent 进行摘要
|
|
471
|
-
// maxRetries: 0 禁用重试,避免长时间等待
|
|
472
837
|
const subagent = this.framework.createSubAgent({
|
|
473
838
|
name: 'context-compressor',
|
|
474
|
-
role: '
|
|
475
|
-
systemPrompt:
|
|
476
|
-
'你是一个对话摘要助手。只提取和保留有意义的信息(如任务需求、决策结论、重要上下文),忽略无意义的闲聊、重复内容和中间过程。输出要简洁。',
|
|
839
|
+
role: 'Information extraction expert',
|
|
840
|
+
systemPrompt: 'You are a conversation summarization assistant. Extract and preserve only meaningful information (task requirements, decisions, important context). Ignore meaningless chatter, repetitive content, and intermediate processes. Keep output concise.',
|
|
477
841
|
maxRetries: 0,
|
|
478
|
-
disableTools: true
|
|
842
|
+
disableTools: true
|
|
479
843
|
});
|
|
480
844
|
|
|
481
845
|
const result = await subagent.chat(task);
|
|
482
846
|
if (result.success) {
|
|
483
847
|
return result.message;
|
|
484
848
|
}
|
|
485
|
-
throw new Error(result.error || '
|
|
849
|
+
throw new Error(result.error || 'Summary generation failed');
|
|
486
850
|
} catch (err) {
|
|
487
851
|
logger.warn('Summarize failed:', err.message);
|
|
488
852
|
throw err;
|
|
489
853
|
}
|
|
490
854
|
}
|
|
491
855
|
|
|
492
|
-
/** TokenCounter 实例 */
|
|
493
|
-
_tokenCounter = new TokenCounter();
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* 计算消息数组的 token 数(委托给 TokenCounter)
|
|
497
|
-
* @param {Array} messages - 消息数组
|
|
498
|
-
* @returns {number}
|
|
499
|
-
*/
|
|
500
|
-
_countMessagesTokens(messages) {
|
|
501
|
-
return this._tokenCounter.countMessages(messages);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* 获取压缩统计
|
|
506
|
-
* @returns {Object}
|
|
507
|
-
*/
|
|
508
856
|
getStats() {
|
|
509
857
|
return {
|
|
510
858
|
compressionCount: this._compressionCount,
|
|
511
|
-
inProgress: this._compressionInProgress
|
|
859
|
+
inProgress: this._compressionInProgress
|
|
512
860
|
};
|
|
513
861
|
}
|
|
514
862
|
}
|
|
515
863
|
|
|
516
|
-
module.exports = {
|
|
864
|
+
module.exports = {
|
|
865
|
+
ContextCompressor,
|
|
866
|
+
CompactionDetails,
|
|
867
|
+
CompactionResult,
|
|
868
|
+
CompactionError,
|
|
869
|
+
BranchSummaryError,
|
|
870
|
+
Result,
|
|
871
|
+
DEFAULT_COMPACTION_SETTINGS,
|
|
872
|
+
createFileOps,
|
|
873
|
+
extractFileOpsFromMessage,
|
|
874
|
+
computeFileLists,
|
|
875
|
+
formatFileOperations,
|
|
876
|
+
serializeConversation,
|
|
877
|
+
findCutPoint,
|
|
878
|
+
findTurnStartIndex,
|
|
879
|
+
SUMMARIZATION_SYSTEM_PROMPT,
|
|
880
|
+
SUMMARIZATION_PROMPT,
|
|
881
|
+
UPDATE_SUMMARIZATION_PROMPT,
|
|
882
|
+
TURN_PREFIX_SUMMARIZATION_PROMPT,
|
|
883
|
+
BRANCH_SUMMARY_PREAMBLE,
|
|
884
|
+
BRANCH_SUMMARY_PROMPT,
|
|
885
|
+
extractFileOperations,
|
|
886
|
+
prepareCompaction,
|
|
887
|
+
prepareBranchEntries,
|
|
888
|
+
collectEntriesForBranchSummary,
|
|
889
|
+
getMessageFromEntry,
|
|
890
|
+
getMessageFromEntryForCompaction,
|
|
891
|
+
EntryTypes
|
|
892
|
+
};
|