foliko 1.1.93 → 2.0.1
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 +2 -1
- package/CLAUDE.md +56 -30
- package/REFACTORING_PLAN.md +645 -0
- package/docs/architecture.md +131 -0
- package/docs/migration.md +57 -0
- package/docs/public-api.md +138 -0
- package/docs/usage.md +385 -0
- package/examples/ambient-example.js +20 -137
- package/examples/basic.js +21 -48
- package/examples/bootstrap.js +16 -74
- package/examples/mcp-example.js +6 -29
- package/examples/skill-example.js +6 -19
- package/examples/workflow.js +8 -56
- package/package.json +8 -4
- package/plugins/README.md +49 -0
- package/plugins/{ambient-agent → ambient}/EventWatcher.js +1 -1
- package/plugins/{ambient-agent → ambient}/ExplorerLoop.js +3 -3
- package/plugins/{ambient-agent → ambient}/GoalManager.js +2 -2
- package/plugins/ambient/README.md +14 -0
- package/plugins/{ambient-agent → ambient}/Reflector.js +1 -1
- package/plugins/{ambient-agent → ambient}/StateStore.js +1 -1
- package/plugins/{ambient-agent → ambient}/index.js +2 -2
- package/plugins/{ai-plugin.js → core/ai/index.js} +14 -30
- package/plugins/{audit-plugin.js → core/audit/index.js} +3 -30
- package/plugins/{coordinator-plugin.js → core/coordinator/index.js} +3 -35
- package/plugins/core/default/bootstrap.js +224 -0
- package/plugins/core/default/config.js +222 -0
- package/plugins/core/default/index.js +58 -0
- package/plugins/core/mcp/index.js +1 -0
- package/plugins/{python-plugin-loader.js → core/python-loader/index.js} +7 -187
- package/plugins/{rules-plugin.js → core/rules/index.js} +121 -64
- package/plugins/{scheduler-plugin.js → core/scheduler/index.js} +12 -114
- package/plugins/{session-plugin.js → core/session/index.js} +9 -73
- package/{src/capabilities/skill-manager.js → plugins/core/skill-manager/index.js} +64 -18
- package/plugins/{storage-plugin.js → core/storage/index.js} +5 -29
- package/plugins/{subagent-plugin.js → core/sub-agent/index.js} +10 -171
- package/plugins/{think-plugin.js → core/think/index.js} +24 -91
- package/{src/capabilities/workflow-engine.js → plugins/core/workflow/index.js} +87 -85
- package/plugins/default-plugins.js +6 -720
- package/plugins/{data-splitter-plugin.js → executors/data-splitter/index.js} +9 -83
- package/plugins/{extension-executor-plugin.js → executors/extension/index.js} +13 -97
- package/plugins/{python-executor-plugin.js → executors/python/index.js} +6 -31
- package/plugins/{shell-executor-plugin.js → executors/shell/index.js} +2 -5
- package/plugins/install/README.md +9 -0
- package/plugins/{install-plugin.js → install/index.js} +3 -3
- package/plugins/{file-system-plugin.js → io/file-system/index.js} +34 -236
- package/plugins/{web-plugin.js → io/web/index.js} +11 -113
- package/plugins/memory/README.md +13 -0
- package/plugins/{memory-plugin.js → memory/index.js} +4 -18
- package/plugins/messaging/email/README.md +19 -0
- package/plugins/{email → messaging/email}/index.js +3 -3
- package/plugins/{feishu-plugin.js → messaging/feishu/index.js} +4 -4
- package/plugins/{qq-plugin.js → messaging/qq/index.js} +6 -17
- package/plugins/{telegram-plugin.js → messaging/telegram/index.js} +4 -4
- package/plugins/{weixin-plugin.js → messaging/weixin/index.js} +16 -16
- package/plugins/{plugin-manager-plugin.js → plugin-manager/index.js} +36 -180
- package/plugins/{tools-plugin.js → tools/index.js} +68 -116
- package/plugins/trading/README.md +15 -0
- package/plugins/{gate-trading.js → trading/index.js} +8 -8
- package/{examples → sandbox}/test-concurrent-chat.js +2 -2
- package/{examples → sandbox}/test-long-chat.js +2 -2
- package/{examples → sandbox}/test-session-chat.js +2 -2
- package/{examples → sandbox}/test-web-plugin.js +1 -1
- package/{examples → sandbox}/test-weixin-feishu.js +2 -2
- package/src/agent/base.js +56 -0
- package/src/{core/agent-chat.js → agent/chat.js} +11 -11
- package/src/{core/coordinator-manager.js → agent/coordinator.js} +3 -3
- package/src/agent/index.js +111 -0
- package/src/agent/main.js +337 -0
- package/src/agent/prompt.js +78 -0
- package/src/agent/sub.js +198 -0
- package/src/agent/worker.js +104 -0
- package/{cli/bin/foliko.js → src/cli/bin.js} +1 -1
- package/{cli/src → src/cli}/commands/chat.js +25 -21
- package/{cli/src → src/cli}/index.js +1 -0
- package/{cli/src → src/cli}/ui/chat-ui-old.js +40 -178
- package/{cli/src → src/cli}/ui/chat-ui.js +3 -3
- package/{cli/src → src/cli}/ui/components/footer-bar.js +1 -1
- package/src/common/errors.js +402 -0
- package/src/{utils → common}/logger.js +33 -0
- package/src/{utils/chat-queue.js → common/queue.js} +2 -2
- package/src/config/plugin-config.js +50 -0
- package/src/context/agent.js +32 -0
- package/src/context/compaction-prompts.js +170 -0
- package/src/context/compaction-utils.js +191 -0
- package/src/context/compressor.js +413 -0
- package/src/context/index.js +9 -0
- package/src/{core/context-manager.js → context/manager.js} +1 -1
- package/src/context/request.js +50 -0
- package/src/context/session.js +33 -0
- package/src/context/storage.js +30 -0
- package/src/executors/mcp-client.js +153 -0
- package/src/executors/mcp-desc.js +236 -0
- package/src/executors/mcp-executor.js +91 -956
- package/src/{core → framework}/command-registry.js +1 -1
- package/src/framework/framework.js +300 -0
- package/src/framework/index.js +18 -0
- package/src/framework/lifecycle.js +203 -0
- package/src/framework/loader.js +78 -0
- package/src/framework/registry.js +86 -0
- package/src/{core/ui-extension-context.js → framework/ui-extension.js} +1 -1
- package/src/index.js +130 -15
- package/src/llm/index.js +26 -0
- package/src/llm/provider.js +212 -0
- package/src/llm/registry.js +11 -0
- package/src/{core/token-counter.js → llm/tokens.js} +4 -37
- package/src/{core/plugin-base.js → plugin/base.js} +10 -136
- package/src/plugin/index.js +14 -0
- package/src/plugin/loader.js +101 -0
- package/src/plugin/manager.js +484 -0
- package/src/{core → session}/branch-summary-auto.js +2 -2
- package/src/{core/chat-session.js → session/chat.js} +2 -2
- package/src/session/index.js +7 -0
- package/src/{core/session-manager.js → session/session.js} +2 -2
- package/src/session/ttl.js +92 -0
- package/src/{core/jsonl-storage.js → storage/jsonl.js} +1 -1
- package/src/tool/executor.js +85 -0
- package/src/tool/index.js +15 -0
- package/src/tool/registry.js +143 -0
- package/src/{core/tool-router.js → tool/router.js} +17 -124
- package/src/tool/schema.js +108 -0
- package/src/utils/data-splitter.js +1 -1
- package/src/utils/download.js +1 -1
- package/src/utils/index.js +6 -6
- package/src/utils/message-validator.js +1 -1
- package/tests/core/context-storage.test.js +46 -0
- package/tests/core/llm.test.js +54 -0
- package/tests/core/plugin.test.js +42 -0
- package/tests/core/tool.test.js +60 -0
- package/tests/setup.js +10 -0
- package/tests/smoke.test.js +58 -0
- package/vitest.config.js +9 -0
- package/cli/src/daemon.js +0 -149
- package/docs/CONTEXT_DESIGN.md +0 -1596
- package/docs/ai-sdk-optimization.md +0 -655
- package/docs/features.md +0 -120
- package/docs/qq-bot.md +0 -976
- package/docs/quick-reference.md +0 -160
- package/docs/user-manual.md +0 -1391
- package/images/geometric_shapes.jpg +0 -0
- package/images/sunset_mountain_lake.jpg +0 -0
- package/skills/poster-guide/SKILL.md +0 -792
- package/src/capabilities/index.js +0 -11
- package/src/core/agent.js +0 -808
- package/src/core/context-compressor.js +0 -959
- package/src/core/enhanced-context-compressor.js +0 -210
- package/src/core/framework.js +0 -1422
- package/src/core/index.js +0 -30
- package/src/core/plugin-manager.js +0 -961
- package/src/core/provider-registry.js +0 -159
- package/src/core/provider.js +0 -156
- package/src/core/request-context.js +0 -98
- package/src/core/subagent.js +0 -442
- package/src/core/system-prompt-builder.js +0 -120
- package/src/core/tool-executor.js +0 -202
- package/src/core/tool-registry.js +0 -517
- package/src/core/worker-agent.js +0 -192
- package/src/executors/executor-base.js +0 -58
- package/src/utils/error-boundary.js +0 -363
- package/src/utils/error.js +0 -374
- package/system.md +0 -1645
- package/website_v2/README.md +0 -57
- package/website_v2/SPEC.md +0 -1
- package/website_v2/docs/api.html +0 -128
- package/website_v2/docs/configuration.html +0 -147
- package/website_v2/docs/plugin-development.html +0 -129
- package/website_v2/docs/project-structure.html +0 -89
- package/website_v2/docs/skill-development.html +0 -85
- package/website_v2/index.html +0 -489
- package/website_v2/scripts/main.js +0 -93
- package/website_v2/styles/animations.css +0 -8
- package/website_v2/styles/docs.css +0 -83
- package/website_v2/styles/main.css +0 -417
- package/xhs_auth.json +0 -268
- package//346/265/267/346/212/245/346/217/222/344/273/266.md +0 -621
- /package/plugins/{ambient-agent → ambient}/constants.js +0 -0
- /package/plugins/{email → messaging/email}/constants.js +0 -0
- /package/plugins/{email → messaging/email}/handlers.js +0 -0
- /package/plugins/{email → messaging/email}/monitor.js +0 -0
- /package/plugins/{email → messaging/email}/parser.js +0 -0
- /package/plugins/{email → messaging/email}/reply.js +0 -0
- /package/plugins/{email → messaging/email}/utils.js +0 -0
- /package/{examples → sandbox}/test-chat.js +0 -0
- /package/{examples → sandbox}/test-mcp.js +0 -0
- /package/{examples → sandbox}/test-reload.js +0 -0
- /package/{examples → sandbox}/test-telegram.js +0 -0
- /package/{examples → sandbox}/test-tg-bot.js +0 -0
- /package/{examples → sandbox}/test-tg-simple.js +0 -0
- /package/{examples → sandbox}/test-tg.js +0 -0
- /package/{examples → sandbox}/test-think.js +0 -0
- /package/src/{core/sub-agent-config.js → agent/sub-config.js} +0 -0
- /package/{cli/src → src/cli}/commands/daemon.js +0 -0
- /package/{cli/src → src/cli}/commands/list.js +0 -0
- /package/{cli/src → src/cli}/commands/plugin.js +0 -0
- /package/{cli/src → src/cli}/ui/components/agent-mention-provider.js +0 -0
- /package/{cli/src → src/cli}/ui/components/chained-autocomplete-provider.js +0 -0
- /package/{cli/src → src/cli}/ui/components/message-bubble.js +0 -0
- /package/{cli/src → src/cli}/ui/components/status-bar.js +0 -0
- /package/{cli/src → src/cli}/utils/ansi.js +0 -0
- /package/{cli/src → src/cli}/utils/config.js +0 -0
- /package/{cli/src → src/cli}/utils/markdown.js +0 -0
- /package/{cli/src → src/cli}/utils/plugin-config.js +0 -0
- /package/{cli/src → src/cli}/utils/render-diff.js +0 -0
- /package/src/{utils/circuit-breaker.js → common/circuit.js} +0 -0
- /package/src/{core → common}/constants.js +0 -0
- /package/src/{utils/edit-diff.js → common/diff.js} +0 -0
- /package/src/{utils/event-emitter.js → common/events.js} +0 -0
- /package/src/{utils → common}/id.js +0 -0
- /package/src/{utils → common}/retry.js +0 -0
- /package/src/{core/notification-manager.js → notification/manager.js} +0 -0
- /package/src/{core/session-entry.js → session/entry.js} +0 -0
- /package/src/{core/storage-manager.js → storage/manager.js} +0 -0
|
@@ -1,959 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ContextCompressor - 上下文压缩器
|
|
3
|
-
* Ported from pi's compaction/compaction.ts with enhanced functionality
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const { logger } = require('../utils/logger');
|
|
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
|
-
};
|
|
36
|
-
|
|
37
|
-
// Model context limits
|
|
38
|
-
const MODEL_CONTEXT_LIMITS = {
|
|
39
|
-
'deepseek-chat': 100000,
|
|
40
|
-
'deepseek-coder': 100000,
|
|
41
|
-
'deepseek-v4-pro': 800000,
|
|
42
|
-
'deepseek-v4-flash': 800000,
|
|
43
|
-
'deepseek-reasoner': 100000,
|
|
44
|
-
'MiniMax-M2.7': 100000,
|
|
45
|
-
'gpt-4': 100000,
|
|
46
|
-
'gpt-4o': 100000,
|
|
47
|
-
'gpt-4o-mini': 100000,
|
|
48
|
-
'gpt-4-turbo': 100000,
|
|
49
|
-
'claude-3-5-sonnet': 110000,
|
|
50
|
-
'claude-3-opus': 110000,
|
|
51
|
-
'claude-3-sonnet': 110000,
|
|
52
|
-
'claude-3-haiku': 110000,
|
|
53
|
-
'claude-4-sonnet': 110000,
|
|
54
|
-
'claude-4-opus': 110000,
|
|
55
|
-
'glm-5.1': 110000
|
|
56
|
-
};
|
|
57
|
-
|
|
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;
|
|
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
|
-
*/
|
|
653
|
-
class ContextCompressor {
|
|
654
|
-
constructor(config = {}) {
|
|
655
|
-
this.config = config;
|
|
656
|
-
this.agent = config.agent;
|
|
657
|
-
this.framework = config.framework;
|
|
658
|
-
|
|
659
|
-
this.model = config.model || 'deepseek-chat';
|
|
660
|
-
this._maxContextTokens = config.maxContextTokens || this._getDefaultContextLimit();
|
|
661
|
-
this._keepRecentMessages = config.keepRecentMessages || 20;
|
|
662
|
-
// 消息数量阈值:超过该数量即触发压缩(与 token 阈值并联触发)
|
|
663
|
-
this._compressionMessageThreshold = config.compressionMessageThreshold || 200;
|
|
664
|
-
this._enableSmartCompress = config.enableSmartCompress !== false;
|
|
665
|
-
this._compactionSettings = config.compactionSettings || DEFAULT_COMPACTION_SETTINGS;
|
|
666
|
-
|
|
667
|
-
this._compressionCount = 0;
|
|
668
|
-
this._compressionInProgress = false;
|
|
669
|
-
this._compressionPromise = null;
|
|
670
|
-
this._compressionTimeoutId = null;
|
|
671
|
-
this._maxToolResultSize = config.maxToolResultSize || 4000;
|
|
672
|
-
this._tokenCounter = new TokenCounter();
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
_getDefaultContextLimit() {
|
|
676
|
-
const modelKey = Object.keys(MODEL_CONTEXT_LIMITS).find(k =>
|
|
677
|
-
this.model.toLowerCase().includes(k.toLowerCase())
|
|
678
|
-
);
|
|
679
|
-
return modelKey ? MODEL_CONTEXT_LIMITS[modelKey] : 40000;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Validate message pairing (delegate to message-validator)
|
|
684
|
-
*/
|
|
685
|
-
validateMessagesPairing(messages) {
|
|
686
|
-
return validateMessagesPairing(messages);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
async compress(sessionId, messages, messageStore) {
|
|
690
|
-
if (this._compressionInProgress && this._compressionPromise) {
|
|
691
|
-
logger.debug('Compression already in progress, waiting...');
|
|
692
|
-
return this._compressionPromise;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (messages.length <= this._keepRecentMessages) {
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
this._compressionInProgress = true;
|
|
700
|
-
|
|
701
|
-
this._compressionPromise = this._executeWithTimeout(sessionId, messages, messageStore).finally(() => {
|
|
702
|
-
this._compressionInProgress = false;
|
|
703
|
-
this._compressionPromise = null;
|
|
704
|
-
if (this._compressionTimeoutId) {
|
|
705
|
-
clearTimeout(this._compressionTimeoutId);
|
|
706
|
-
this._compressionTimeoutId = null;
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
return this._compressionPromise;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
async _executeWithTimeout(sessionId, messages, messageStore) {
|
|
714
|
-
try {
|
|
715
|
-
return await Promise.race([
|
|
716
|
-
this._doCompress(sessionId, messages, messageStore),
|
|
717
|
-
this._createTimeoutPromise()
|
|
718
|
-
]);
|
|
719
|
-
} catch (err) {
|
|
720
|
-
logger.warn('Compression failed:', err.message);
|
|
721
|
-
this._simpleCompress(sessionId, messages, messageStore);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
_createTimeoutPromise() {
|
|
726
|
-
return new Promise((_, reject) => {
|
|
727
|
-
this._compressionTimeoutId = setTimeout(() => {
|
|
728
|
-
this._compressionTimeoutId = null;
|
|
729
|
-
reject(new Error(`Compression timeout (${COMPRESSION_TIMEOUT_MS}ms)`));
|
|
730
|
-
}, COMPRESSION_TIMEOUT_MS);
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
cancelCompression() {
|
|
735
|
-
if (this._compressionTimeoutId) {
|
|
736
|
-
clearTimeout(this._compressionTimeoutId);
|
|
737
|
-
this._compressionTimeoutId = null;
|
|
738
|
-
}
|
|
739
|
-
this._compressionInProgress = false;
|
|
740
|
-
this._compressionPromise = null;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
async _doCompress(sessionId, messages, messageStore) {
|
|
744
|
-
const systemMessages = messages.filter(m => m.role === 'system');
|
|
745
|
-
const otherMessages = messages.filter(m => m.role !== 'system');
|
|
746
|
-
const recentMessages = otherMessages.slice(-this._keepRecentMessages);
|
|
747
|
-
const messagesToSummarize = otherMessages.slice(0, -this._keepRecentMessages);
|
|
748
|
-
|
|
749
|
-
const compressedCount = messagesToSummarize.length;
|
|
750
|
-
let summaryContent = '';
|
|
751
|
-
|
|
752
|
-
if (this._enableSmartCompress && this.agent?._chatHandler?._aiClient) {
|
|
753
|
-
try {
|
|
754
|
-
const summaryText = await this._summarizeMessages(messagesToSummarize);
|
|
755
|
-
summaryContent = `[Early conversation summary]: ${summaryText || '(no content)'}`;
|
|
756
|
-
} catch (err) {
|
|
757
|
-
logger.warn('AI summary failed, using simple compression:', err.message);
|
|
758
|
-
summaryContent = `[Context compressed: ${compressedCount} early messages omitted. Kept recent ${this._keepRecentMessages} messages.]`;
|
|
759
|
-
}
|
|
760
|
-
} else {
|
|
761
|
-
summaryContent = `[Context compressed: ${compressedCount} early messages omitted. Kept recent ${this._keepRecentMessages} messages.]`;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
const summary = {
|
|
765
|
-
role: 'assistant',
|
|
766
|
-
content: summaryContent
|
|
767
|
-
};
|
|
768
|
-
|
|
769
|
-
const combined = [...systemMessages, summary, ...recentMessages];
|
|
770
|
-
this._cleanupOrphanedToolResults(combined);
|
|
771
|
-
|
|
772
|
-
messages.length = 0;
|
|
773
|
-
messages.push(...combined);
|
|
774
|
-
|
|
775
|
-
this._compressionCount++;
|
|
776
|
-
const tokenCount = this._tokenCounter.countMessages(messages);
|
|
777
|
-
if (messageStore.compressionState) {
|
|
778
|
-
messageStore.compressionState.count++;
|
|
779
|
-
messageStore.compressionState.lastCompressedAt = Date.now();
|
|
780
|
-
messageStore.compressionState.lastTokenCount = tokenCount;
|
|
781
|
-
}
|
|
782
|
-
if (typeof messageStore.recordCompression === 'function') {
|
|
783
|
-
messageStore.recordCompression(tokenCount);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
logger.info(`Context compressed (${this._compressionCount} times). Messages: ${messages.length}`);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
_simpleCompress(sessionId, messages, messageStore) {
|
|
790
|
-
const systemMessages = messages.filter(m => m.role === 'system');
|
|
791
|
-
const otherMessages = messages.filter(m => m.role !== 'system');
|
|
792
|
-
const recentMessages = otherMessages.slice(-this._keepRecentMessages);
|
|
793
|
-
const compressedCount = otherMessages.length - this._keepRecentMessages;
|
|
794
|
-
|
|
795
|
-
const summaryContent = `[Context compressed: ${compressedCount} early messages omitted. Kept recent ${this._keepRecentMessages} messages.]`;
|
|
796
|
-
|
|
797
|
-
const summary = {
|
|
798
|
-
role: 'assistant',
|
|
799
|
-
content: summaryContent
|
|
800
|
-
};
|
|
801
|
-
|
|
802
|
-
const combined = [...systemMessages, summary, ...recentMessages];
|
|
803
|
-
this._cleanupOrphanedToolResults(combined);
|
|
804
|
-
|
|
805
|
-
messages.length = 0;
|
|
806
|
-
messages.push(...combined);
|
|
807
|
-
|
|
808
|
-
this._compressionCount++;
|
|
809
|
-
if (messageStore.compressionState) {
|
|
810
|
-
messageStore.compressionState.count++;
|
|
811
|
-
messageStore.compressionState.lastCompressedAt = Date.now();
|
|
812
|
-
messageStore.compressionState.lastTokenCount = this._tokenCounter.countMessages(combined);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
logger.info(`Context simple compressed. Messages: ${messages.length}`);
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
/**
|
|
819
|
-
* 清理孤立的 tool-result 消息(没有对应 tool-call 的)
|
|
820
|
-
* 压缩/裁剪后 recentMessages 可能切碎 tool-call/tool-result 配对,
|
|
821
|
-
* 必须就地清理,否则下游 API 会报 "tool result's tool id not found"
|
|
822
|
-
*/
|
|
823
|
-
_cleanupOrphanedToolResults(messages) {
|
|
824
|
-
const validToolCallIds = new Set();
|
|
825
|
-
for (const msg of messages) {
|
|
826
|
-
if (msg.role !== 'assistant') continue;
|
|
827
|
-
if (Array.isArray(msg.content)) {
|
|
828
|
-
for (const item of msg.content) {
|
|
829
|
-
if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) {
|
|
830
|
-
validToolCallIds.add(item.toolCallId);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
if (Array.isArray(msg.tool_calls)) {
|
|
835
|
-
for (const tc of msg.tool_calls) {
|
|
836
|
-
if (tc.id) validToolCallIds.add(tc.id);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
let removedItems = 0;
|
|
842
|
-
let removedMsgs = 0;
|
|
843
|
-
for (const msg of messages) {
|
|
844
|
-
if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
|
|
845
|
-
const originalLength = msg.content.length;
|
|
846
|
-
msg.content = msg.content.filter((item) => {
|
|
847
|
-
if (
|
|
848
|
-
item &&
|
|
849
|
-
(item.type === 'tool-result' || item.type === 'tool_result') &&
|
|
850
|
-
item.toolCallId &&
|
|
851
|
-
!validToolCallIds.has(item.toolCallId)
|
|
852
|
-
) {
|
|
853
|
-
removedItems++;
|
|
854
|
-
return false;
|
|
855
|
-
}
|
|
856
|
-
return true;
|
|
857
|
-
});
|
|
858
|
-
if (msg.content.length === 0 && originalLength > 0) {
|
|
859
|
-
msg._orphaned = true;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
864
|
-
if (messages[i]._orphaned) {
|
|
865
|
-
messages.splice(i, 1);
|
|
866
|
-
removedMsgs++;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
if (removedItems > 0 || removedMsgs > 0) {
|
|
871
|
-
logger.debug(
|
|
872
|
-
`[ContextCompressor] cleanup: removed ${removedItems} orphaned tool-result items, ${removedMsgs} orphaned tool messages`
|
|
873
|
-
);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
async _summarizeMessages(messages) {
|
|
878
|
-
if (!this.framework) {
|
|
879
|
-
throw new Error('Framework not available');
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
const msg_str = messages
|
|
883
|
-
.map(m => {
|
|
884
|
-
let text = '';
|
|
885
|
-
if (typeof m.content === 'string') {
|
|
886
|
-
text = m.content;
|
|
887
|
-
} else if (Array.isArray(m.content)) {
|
|
888
|
-
text = m.content
|
|
889
|
-
.filter(c => c.type === 'text')
|
|
890
|
-
.map(c => c.text)
|
|
891
|
-
.join(' ');
|
|
892
|
-
}
|
|
893
|
-
if (m.role === 'tool') {
|
|
894
|
-
return '';
|
|
895
|
-
}
|
|
896
|
-
return `${m.role}: ${text}`;
|
|
897
|
-
})
|
|
898
|
-
.filter(line => line.length > 0)
|
|
899
|
-
.join('\n');
|
|
900
|
-
|
|
901
|
-
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}`;
|
|
902
|
-
|
|
903
|
-
try {
|
|
904
|
-
const subagent = this.framework.createSubAgent({
|
|
905
|
-
name: 'context-compressor',
|
|
906
|
-
role: 'Information extraction expert',
|
|
907
|
-
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.',
|
|
908
|
-
maxRetries: 0,
|
|
909
|
-
disableTools: true
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
const result = await subagent.chat(task);
|
|
913
|
-
if (result.success) {
|
|
914
|
-
return result.message;
|
|
915
|
-
}
|
|
916
|
-
throw new Error(result.error || 'Summary generation failed');
|
|
917
|
-
} catch (err) {
|
|
918
|
-
logger.warn('Summarize failed:', err.message);
|
|
919
|
-
throw err;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
getStats() {
|
|
924
|
-
return {
|
|
925
|
-
compressionCount: this._compressionCount,
|
|
926
|
-
inProgress: this._compressionInProgress
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
module.exports = {
|
|
932
|
-
ContextCompressor,
|
|
933
|
-
CompactionDetails,
|
|
934
|
-
CompactionResult,
|
|
935
|
-
CompactionError,
|
|
936
|
-
BranchSummaryError,
|
|
937
|
-
Result,
|
|
938
|
-
DEFAULT_COMPACTION_SETTINGS,
|
|
939
|
-
createFileOps,
|
|
940
|
-
extractFileOpsFromMessage,
|
|
941
|
-
computeFileLists,
|
|
942
|
-
formatFileOperations,
|
|
943
|
-
serializeConversation,
|
|
944
|
-
findCutPoint,
|
|
945
|
-
findTurnStartIndex,
|
|
946
|
-
SUMMARIZATION_SYSTEM_PROMPT,
|
|
947
|
-
SUMMARIZATION_PROMPT,
|
|
948
|
-
UPDATE_SUMMARIZATION_PROMPT,
|
|
949
|
-
TURN_PREFIX_SUMMARIZATION_PROMPT,
|
|
950
|
-
BRANCH_SUMMARY_PREAMBLE,
|
|
951
|
-
BRANCH_SUMMARY_PROMPT,
|
|
952
|
-
extractFileOperations,
|
|
953
|
-
prepareCompaction,
|
|
954
|
-
prepareBranchEntries,
|
|
955
|
-
collectEntriesForBranchSummary,
|
|
956
|
-
getMessageFromEntry,
|
|
957
|
-
getMessageFromEntryForCompaction,
|
|
958
|
-
EntryTypes
|
|
959
|
-
};
|