kode-sdk 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +74 -0
- package/dist/core/agent/breakpoint-manager.d.ts +16 -0
- package/dist/core/agent/breakpoint-manager.js +36 -0
- package/dist/core/agent/message-queue.d.ts +26 -0
- package/dist/core/agent/message-queue.js +47 -0
- package/dist/core/agent/permission-manager.d.ts +9 -0
- package/dist/core/agent/permission-manager.js +32 -0
- package/dist/core/agent/todo-manager.d.ts +26 -0
- package/dist/core/agent/todo-manager.js +91 -0
- package/dist/core/agent/tool-runner.d.ts +9 -0
- package/dist/core/agent/tool-runner.js +45 -0
- package/dist/core/agent.d.ts +271 -0
- package/dist/core/agent.js +2334 -0
- package/dist/core/checkpointer.d.ts +96 -0
- package/dist/core/checkpointer.js +57 -0
- package/dist/core/checkpointers/file.d.ts +20 -0
- package/dist/core/checkpointers/file.js +153 -0
- package/dist/core/checkpointers/index.d.ts +3 -0
- package/dist/core/checkpointers/index.js +9 -0
- package/dist/core/checkpointers/redis.d.ts +35 -0
- package/dist/core/checkpointers/redis.js +113 -0
- package/dist/core/compression/ai-strategy.d.ts +53 -0
- package/dist/core/compression/ai-strategy.js +298 -0
- package/dist/core/compression/index.d.ts +12 -0
- package/dist/core/compression/index.js +27 -0
- package/dist/core/compression/prompts.d.ts +35 -0
- package/dist/core/compression/prompts.js +114 -0
- package/dist/core/compression/simple-strategy.d.ts +44 -0
- package/dist/core/compression/simple-strategy.js +240 -0
- package/dist/core/compression/token-estimator.d.ts +42 -0
- package/dist/core/compression/token-estimator.js +121 -0
- package/dist/core/compression/types.d.ts +140 -0
- package/dist/core/compression/types.js +9 -0
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.js +2 -0
- package/dist/core/context-manager.d.ts +115 -0
- package/dist/core/context-manager.js +107 -0
- package/dist/core/errors.d.ts +6 -0
- package/dist/core/errors.js +17 -0
- package/dist/core/events.d.ts +49 -0
- package/dist/core/events.js +312 -0
- package/dist/core/file-pool.d.ts +43 -0
- package/dist/core/file-pool.js +120 -0
- package/dist/core/hooks.d.ts +23 -0
- package/dist/core/hooks.js +71 -0
- package/dist/core/permission-modes.d.ts +31 -0
- package/dist/core/permission-modes.js +61 -0
- package/dist/core/pool.d.ts +31 -0
- package/dist/core/pool.js +87 -0
- package/dist/core/room.d.ts +15 -0
- package/dist/core/room.js +57 -0
- package/dist/core/scheduler.d.ts +33 -0
- package/dist/core/scheduler.js +58 -0
- package/dist/core/template.d.ts +69 -0
- package/dist/core/template.js +35 -0
- package/dist/core/time-bridge.d.ts +18 -0
- package/dist/core/time-bridge.js +100 -0
- package/dist/core/todo.d.ts +34 -0
- package/dist/core/todo.js +89 -0
- package/dist/core/types.d.ts +380 -0
- package/dist/core/types.js +3 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +147 -0
- package/dist/infra/provider.d.ts +144 -0
- package/dist/infra/provider.js +294 -0
- package/dist/infra/sandbox-factory.d.ts +10 -0
- package/dist/infra/sandbox-factory.js +21 -0
- package/dist/infra/sandbox.d.ts +87 -0
- package/dist/infra/sandbox.js +255 -0
- package/dist/infra/store.d.ts +154 -0
- package/dist/infra/store.js +584 -0
- package/dist/skills/index.d.ts +12 -0
- package/dist/skills/index.js +36 -0
- package/dist/skills/injector.d.ts +29 -0
- package/dist/skills/injector.js +96 -0
- package/dist/skills/loader.d.ts +59 -0
- package/dist/skills/loader.js +215 -0
- package/dist/skills/manager.d.ts +85 -0
- package/dist/skills/manager.js +221 -0
- package/dist/skills/parser.d.ts +40 -0
- package/dist/skills/parser.js +107 -0
- package/dist/skills/types.d.ts +107 -0
- package/dist/skills/types.js +7 -0
- package/dist/skills/validator.d.ts +30 -0
- package/dist/skills/validator.js +121 -0
- package/dist/store.d.ts +1 -0
- package/dist/store.js +5 -0
- package/dist/tools/bash_kill/index.d.ts +1 -0
- package/dist/tools/bash_kill/index.js +35 -0
- package/dist/tools/bash_kill/prompt.d.ts +2 -0
- package/dist/tools/bash_kill/prompt.js +14 -0
- package/dist/tools/bash_logs/index.d.ts +1 -0
- package/dist/tools/bash_logs/index.js +40 -0
- package/dist/tools/bash_logs/prompt.d.ts +2 -0
- package/dist/tools/bash_logs/prompt.js +14 -0
- package/dist/tools/bash_run/index.d.ts +16 -0
- package/dist/tools/bash_run/index.js +61 -0
- package/dist/tools/bash_run/prompt.d.ts +2 -0
- package/dist/tools/bash_run/prompt.js +18 -0
- package/dist/tools/builtin.d.ts +9 -0
- package/dist/tools/builtin.js +27 -0
- package/dist/tools/define.d.ts +101 -0
- package/dist/tools/define.js +214 -0
- package/dist/tools/fs_edit/index.d.ts +1 -0
- package/dist/tools/fs_edit/index.js +62 -0
- package/dist/tools/fs_edit/prompt.d.ts +2 -0
- package/dist/tools/fs_edit/prompt.js +15 -0
- package/dist/tools/fs_glob/index.d.ts +1 -0
- package/dist/tools/fs_glob/index.js +60 -0
- package/dist/tools/fs_glob/prompt.d.ts +2 -0
- package/dist/tools/fs_glob/prompt.js +18 -0
- package/dist/tools/fs_grep/index.d.ts +1 -0
- package/dist/tools/fs_grep/index.js +66 -0
- package/dist/tools/fs_grep/prompt.d.ts +2 -0
- package/dist/tools/fs_grep/prompt.js +16 -0
- package/dist/tools/fs_multi_edit/index.d.ts +1 -0
- package/dist/tools/fs_multi_edit/index.js +106 -0
- package/dist/tools/fs_multi_edit/prompt.d.ts +2 -0
- package/dist/tools/fs_multi_edit/prompt.js +16 -0
- package/dist/tools/fs_read/index.d.ts +1 -0
- package/dist/tools/fs_read/index.js +40 -0
- package/dist/tools/fs_read/prompt.d.ts +2 -0
- package/dist/tools/fs_read/prompt.js +16 -0
- package/dist/tools/fs_rm/index.d.ts +1 -0
- package/dist/tools/fs_rm/index.js +41 -0
- package/dist/tools/fs_rm/prompt.d.ts +2 -0
- package/dist/tools/fs_rm/prompt.js +14 -0
- package/dist/tools/fs_write/index.d.ts +1 -0
- package/dist/tools/fs_write/index.js +40 -0
- package/dist/tools/fs_write/prompt.d.ts +2 -0
- package/dist/tools/fs_write/prompt.js +15 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +56 -0
- package/dist/tools/mcp.d.ts +73 -0
- package/dist/tools/mcp.js +198 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +26 -0
- package/dist/tools/skill_activate/index.d.ts +5 -0
- package/dist/tools/skill_activate/index.js +63 -0
- package/dist/tools/skill_list/index.d.ts +5 -0
- package/dist/tools/skill_list/index.js +48 -0
- package/dist/tools/skill_resource/index.d.ts +5 -0
- package/dist/tools/skill_resource/index.js +82 -0
- package/dist/tools/task_run/index.d.ts +7 -0
- package/dist/tools/task_run/index.js +60 -0
- package/dist/tools/task_run/prompt.d.ts +5 -0
- package/dist/tools/task_run/prompt.js +29 -0
- package/dist/tools/todo_read/index.d.ts +1 -0
- package/dist/tools/todo_read/index.js +29 -0
- package/dist/tools/todo_read/prompt.d.ts +2 -0
- package/dist/tools/todo_read/prompt.js +18 -0
- package/dist/tools/todo_write/index.d.ts +1 -0
- package/dist/tools/todo_write/index.js +42 -0
- package/dist/tools/todo_write/prompt.d.ts +2 -0
- package/dist/tools/todo_write/prompt.js +23 -0
- package/dist/tools/tool.d.ts +43 -0
- package/dist/tools/tool.js +104 -0
- package/dist/tools/toolkit.d.ts +69 -0
- package/dist/tools/toolkit.js +98 -0
- package/dist/tools/type-inference.d.ts +127 -0
- package/dist/tools/type-inference.js +207 -0
- package/dist/utils/agent-id.d.ts +1 -0
- package/dist/utils/agent-id.js +28 -0
- package/dist/utils/session-id.d.ts +21 -0
- package/dist/utils/session-id.js +64 -0
- package/dist/utils/unicode.d.ts +17 -0
- package/dist/utils/unicode.js +62 -0
- package/package.json +117 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 简单压缩策略
|
|
4
|
+
*
|
|
5
|
+
* 从现有 ContextManager 提取的压缩逻辑:
|
|
6
|
+
* - 按消息数保留 60% 或 compressToTokens/totalTokens 比例
|
|
7
|
+
* - 简单截断生成摘要
|
|
8
|
+
* - 处理孤立 tool_result
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.SimpleCompressionStrategy = void 0;
|
|
12
|
+
const token_estimator_1 = require("./token-estimator");
|
|
13
|
+
class SimpleCompressionStrategy {
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
this.store = opts.store;
|
|
16
|
+
this.agentId = opts.agentId;
|
|
17
|
+
this.maxTokens = opts.maxTokens;
|
|
18
|
+
this.compressToTokens = opts.compressToTokens;
|
|
19
|
+
this.compressionModel = opts.compressionModel ?? 'simple';
|
|
20
|
+
this.compressionPrompt = opts.compressionPrompt ?? 'Simple truncation summary';
|
|
21
|
+
this.eventEmitter = opts.eventEmitter;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 分析上下文使用情况(粗略的 token 估算)
|
|
25
|
+
*/
|
|
26
|
+
analyze(messages) {
|
|
27
|
+
const totalTokens = messages.reduce((sum, message) => {
|
|
28
|
+
return (sum +
|
|
29
|
+
message.content.reduce((inner, block) => {
|
|
30
|
+
if (block.type === 'text')
|
|
31
|
+
return inner + Math.ceil(block.text.length / 4);
|
|
32
|
+
return inner + Math.ceil(JSON.stringify(block).length / 4);
|
|
33
|
+
}, 0));
|
|
34
|
+
}, 0);
|
|
35
|
+
return {
|
|
36
|
+
totalTokens,
|
|
37
|
+
messageCount: messages.length,
|
|
38
|
+
shouldCompress: totalTokens > this.maxTokens,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 执行压缩
|
|
43
|
+
*/
|
|
44
|
+
async compress(messages, events, filePool, sandbox) {
|
|
45
|
+
const usage = this.analyze(messages);
|
|
46
|
+
if (!usage.shouldCompress)
|
|
47
|
+
return undefined;
|
|
48
|
+
// 发送开始事件
|
|
49
|
+
this.eventEmitter?.emit({
|
|
50
|
+
phase: 'start',
|
|
51
|
+
algorithm: 'simple',
|
|
52
|
+
});
|
|
53
|
+
const timestamp = Date.now();
|
|
54
|
+
const windowId = `window-${timestamp}`;
|
|
55
|
+
const compressionId = `comp-${timestamp}`;
|
|
56
|
+
// 1. 保存历史窗口
|
|
57
|
+
const window = {
|
|
58
|
+
id: windowId,
|
|
59
|
+
messages,
|
|
60
|
+
events,
|
|
61
|
+
stats: {
|
|
62
|
+
messageCount: messages.length,
|
|
63
|
+
tokenCount: usage.totalTokens,
|
|
64
|
+
eventCount: events.length,
|
|
65
|
+
},
|
|
66
|
+
timestamp,
|
|
67
|
+
};
|
|
68
|
+
await this.store.saveHistoryWindow(this.agentId, window);
|
|
69
|
+
// 2. 执行压缩(简化版:保留 60% 消息或按比例)
|
|
70
|
+
const targetRatio = this.compressToTokens / usage.totalTokens;
|
|
71
|
+
const keepCount = Math.ceil(messages.length * Math.max(targetRatio, 0.6));
|
|
72
|
+
const retainedMessages = messages.slice(-keepCount);
|
|
73
|
+
const removedMessages = messages.slice(0, messages.length - keepCount);
|
|
74
|
+
// 处理孤立的 tool_result
|
|
75
|
+
const toolSanitized = this.sanitizeOrphanToolResults(retainedMessages);
|
|
76
|
+
const retainedSanitized = toolSanitized.messages;
|
|
77
|
+
// 生成摘要
|
|
78
|
+
const summaryText = this.generateSummary(removedMessages);
|
|
79
|
+
const summary = {
|
|
80
|
+
role: 'system',
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: `<context-summary timestamp="${new Date().toISOString()}" window="${windowId}">\n${summaryText}\n</context-summary>`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
// 3. 保存文件快照
|
|
89
|
+
const recoveredPaths = [];
|
|
90
|
+
if (filePool && sandbox) {
|
|
91
|
+
const accessed = filePool.getAccessedFiles().slice(0, 5);
|
|
92
|
+
for (const { path, mtime } of accessed) {
|
|
93
|
+
recoveredPaths.push(path);
|
|
94
|
+
try {
|
|
95
|
+
const content = await sandbox.fs.read(path);
|
|
96
|
+
const file = {
|
|
97
|
+
path,
|
|
98
|
+
content,
|
|
99
|
+
mtime,
|
|
100
|
+
timestamp,
|
|
101
|
+
};
|
|
102
|
+
await this.store.saveRecoveredFile(this.agentId, file);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const file = {
|
|
106
|
+
path,
|
|
107
|
+
content: `// Failed to read file: ${err instanceof Error ? err.message : String(err)}`,
|
|
108
|
+
mtime,
|
|
109
|
+
timestamp,
|
|
110
|
+
};
|
|
111
|
+
await this.store.saveRecoveredFile(this.agentId, file);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// 4. 保存压缩记录
|
|
116
|
+
const ratio = retainedMessages.length / messages.length;
|
|
117
|
+
const record = {
|
|
118
|
+
id: compressionId,
|
|
119
|
+
windowId,
|
|
120
|
+
config: {
|
|
121
|
+
model: this.compressionModel,
|
|
122
|
+
prompt: this.compressionPrompt,
|
|
123
|
+
threshold: this.maxTokens,
|
|
124
|
+
},
|
|
125
|
+
summary: summaryText.slice(0, 500),
|
|
126
|
+
ratio,
|
|
127
|
+
recoveredFiles: recoveredPaths,
|
|
128
|
+
timestamp,
|
|
129
|
+
};
|
|
130
|
+
await this.store.saveCompressionRecord(this.agentId, record);
|
|
131
|
+
// 计算 token 统计
|
|
132
|
+
const tokensRetained = (0, token_estimator_1.estimateTotalTokens)(retainedSanitized);
|
|
133
|
+
const tokensRemoved = usage.totalTokens - tokensRetained;
|
|
134
|
+
// 发送完成事件
|
|
135
|
+
this.eventEmitter?.emit({
|
|
136
|
+
phase: 'end',
|
|
137
|
+
algorithm: 'simple',
|
|
138
|
+
ratio,
|
|
139
|
+
tokensRemoved,
|
|
140
|
+
tokensRetained,
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
summary,
|
|
144
|
+
removedMessages,
|
|
145
|
+
retainedMessages: retainedSanitized,
|
|
146
|
+
windowId,
|
|
147
|
+
compressionId,
|
|
148
|
+
ratio,
|
|
149
|
+
algorithm: 'simple',
|
|
150
|
+
aiGenerated: false,
|
|
151
|
+
tokensRemoved,
|
|
152
|
+
tokensRetained,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 预览文本(截断长文本)
|
|
157
|
+
*/
|
|
158
|
+
preview(value, limit = 1000) {
|
|
159
|
+
try {
|
|
160
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value);
|
|
161
|
+
return text.length > limit ? `${text.slice(0, limit)}…` : text;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
const text = String(value ?? '');
|
|
165
|
+
return text.length > limit ? `${text.slice(0, limit)}…` : text;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 处理孤立的 tool_result
|
|
170
|
+
*
|
|
171
|
+
* 简单的 slice 可能保留 tool_result 但丢弃配对的 tool_use,
|
|
172
|
+
* 导致 Anthropic API 报错 (2013)。将孤立的 tool_result 转换为文本。
|
|
173
|
+
*/
|
|
174
|
+
sanitizeOrphanToolResults(messages) {
|
|
175
|
+
const toolUseIds = new Set();
|
|
176
|
+
for (const msg of messages || []) {
|
|
177
|
+
for (const block of msg?.content || []) {
|
|
178
|
+
if (block && block.type === 'tool_use') {
|
|
179
|
+
const id = String(block.id ?? '').trim();
|
|
180
|
+
if (id)
|
|
181
|
+
toolUseIds.add(id);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
let converted = 0;
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const msg of messages || []) {
|
|
188
|
+
if (!msg || !Array.isArray(msg.content)) {
|
|
189
|
+
out.push(msg);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
let changed = false;
|
|
193
|
+
const nextBlocks = [];
|
|
194
|
+
for (const block of msg.content) {
|
|
195
|
+
if (block && block.type === 'tool_result') {
|
|
196
|
+
const toolUseId = String(block.tool_use_id ?? '').trim();
|
|
197
|
+
if (!toolUseId || !toolUseIds.has(toolUseId)) {
|
|
198
|
+
changed = true;
|
|
199
|
+
converted += 1;
|
|
200
|
+
const isErr = Boolean(block.is_error);
|
|
201
|
+
const payload = this.preview(block.content, 1400);
|
|
202
|
+
nextBlocks.push({
|
|
203
|
+
type: 'text',
|
|
204
|
+
text: `[tool_result orphaned by compression] tool_use_id=${toolUseId || 'unknown'}${isErr ? ' (error)' : ''}\n${payload}`,
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
nextBlocks.push(block);
|
|
210
|
+
}
|
|
211
|
+
out.push(changed ? { ...msg, content: nextBlocks } : msg);
|
|
212
|
+
}
|
|
213
|
+
return { messages: out, converted };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* 生成简单摘要(截断文本)
|
|
217
|
+
*/
|
|
218
|
+
generateSummary(messages) {
|
|
219
|
+
return messages
|
|
220
|
+
.map((msg, idx) => {
|
|
221
|
+
const header = `${idx + 1}. [${msg.role}]`;
|
|
222
|
+
const content = msg.content
|
|
223
|
+
.map((block) => {
|
|
224
|
+
if (block.type === 'text')
|
|
225
|
+
return block.text.slice(0, 200);
|
|
226
|
+
if (block.type === 'tool_use')
|
|
227
|
+
return `🔧 ${block.name}(...)`;
|
|
228
|
+
if (block.type === 'tool_result') {
|
|
229
|
+
const preview = JSON.stringify(block.content).slice(0, 100);
|
|
230
|
+
return `✅ result: ${preview}`;
|
|
231
|
+
}
|
|
232
|
+
return '';
|
|
233
|
+
})
|
|
234
|
+
.join('\n');
|
|
235
|
+
return `${header}\n${content}`;
|
|
236
|
+
})
|
|
237
|
+
.join('\n\n');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
exports.SimpleCompressionStrategy = SimpleCompressionStrategy;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token 估算工具
|
|
3
|
+
*
|
|
4
|
+
* 提供更精确的 token 数量估算,考虑:
|
|
5
|
+
* - 中英文字符差异
|
|
6
|
+
* - 不同消息块类型的开销
|
|
7
|
+
*/
|
|
8
|
+
import { Message, ContentBlock } from '../types';
|
|
9
|
+
/**
|
|
10
|
+
* 估算文本的 token 数量
|
|
11
|
+
*
|
|
12
|
+
* 规则:
|
|
13
|
+
* - 英文/数字/符号:约 4 字符 = 1 token
|
|
14
|
+
* - 中文/日文/韩文:约 1.5 字符 = 1 token
|
|
15
|
+
*/
|
|
16
|
+
export declare function estimateTextTokens(text: string): number;
|
|
17
|
+
/**
|
|
18
|
+
* 估算单个内容块的 token 数量
|
|
19
|
+
*/
|
|
20
|
+
export declare function estimateBlockTokens(block: ContentBlock): number;
|
|
21
|
+
/**
|
|
22
|
+
* 估算单条消息的 token 数量
|
|
23
|
+
*/
|
|
24
|
+
export declare function estimateMessageTokens(message: Message): number;
|
|
25
|
+
/**
|
|
26
|
+
* 估算消息列表的总 token 数量
|
|
27
|
+
*/
|
|
28
|
+
export declare function estimateTotalTokens(messages: Message[]): number;
|
|
29
|
+
/**
|
|
30
|
+
* 根据目标 token 数选择要保留的消息
|
|
31
|
+
*
|
|
32
|
+
* 从后往前累加,直到达到目标 token 数
|
|
33
|
+
*
|
|
34
|
+
* @param messages - 消息列表
|
|
35
|
+
* @param targetTokens - 目标 token 数
|
|
36
|
+
* @param minMessages - 最少保留消息数(默认 2)
|
|
37
|
+
* @returns 保留的消息和移除的消息
|
|
38
|
+
*/
|
|
39
|
+
export declare function selectMessagesByTokens(messages: Message[], targetTokens: number, minMessages?: number): {
|
|
40
|
+
retained: Message[];
|
|
41
|
+
removed: Message[];
|
|
42
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Token 估算工具
|
|
4
|
+
*
|
|
5
|
+
* 提供更精确的 token 数量估算,考虑:
|
|
6
|
+
* - 中英文字符差异
|
|
7
|
+
* - 不同消息块类型的开销
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.estimateTextTokens = estimateTextTokens;
|
|
11
|
+
exports.estimateBlockTokens = estimateBlockTokens;
|
|
12
|
+
exports.estimateMessageTokens = estimateMessageTokens;
|
|
13
|
+
exports.estimateTotalTokens = estimateTotalTokens;
|
|
14
|
+
exports.selectMessagesByTokens = selectMessagesByTokens;
|
|
15
|
+
/**
|
|
16
|
+
* 估算文本的 token 数量
|
|
17
|
+
*
|
|
18
|
+
* 规则:
|
|
19
|
+
* - 英文/数字/符号:约 4 字符 = 1 token
|
|
20
|
+
* - 中文/日文/韩文:约 1.5 字符 = 1 token
|
|
21
|
+
*/
|
|
22
|
+
function estimateTextTokens(text) {
|
|
23
|
+
if (!text)
|
|
24
|
+
return 0;
|
|
25
|
+
// 匹配 CJK(中日韩)字符
|
|
26
|
+
const cjkChars = (text.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length;
|
|
27
|
+
const otherChars = text.length - cjkChars;
|
|
28
|
+
// CJK 字符约 1.5 字符 = 1 token,其他约 4 字符 = 1 token
|
|
29
|
+
return Math.ceil(cjkChars / 1.5) + Math.ceil(otherChars / 4);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 估算单个内容块的 token 数量
|
|
33
|
+
*/
|
|
34
|
+
function estimateBlockTokens(block) {
|
|
35
|
+
if (!block)
|
|
36
|
+
return 0;
|
|
37
|
+
switch (block.type) {
|
|
38
|
+
case 'text':
|
|
39
|
+
return estimateTextTokens(block.text);
|
|
40
|
+
case 'tool_use':
|
|
41
|
+
// 工具调用包含:名称、ID、输入参数
|
|
42
|
+
// 基础开销约 50 tokens(JSON 结构、字段名等)
|
|
43
|
+
const toolUseContent = JSON.stringify(block.input || {});
|
|
44
|
+
return estimateTextTokens(block.name) + estimateTextTokens(toolUseContent) + 50;
|
|
45
|
+
case 'tool_result':
|
|
46
|
+
// 工具结果包含:ID、内容
|
|
47
|
+
// 基础开销约 20 tokens
|
|
48
|
+
const toolResultContent = typeof block.content === 'string' ? block.content : JSON.stringify(block.content || '');
|
|
49
|
+
return estimateTextTokens(toolResultContent) + 20;
|
|
50
|
+
default:
|
|
51
|
+
// 未知类型,使用 JSON 序列化估算
|
|
52
|
+
try {
|
|
53
|
+
return estimateTextTokens(JSON.stringify(block));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return 100; // 默认估算
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 估算单条消息的 token 数量
|
|
62
|
+
*/
|
|
63
|
+
function estimateMessageTokens(message) {
|
|
64
|
+
if (!message || !Array.isArray(message.content))
|
|
65
|
+
return 0;
|
|
66
|
+
// 消息元数据开销(role 等)约 5 tokens
|
|
67
|
+
const baseOverhead = 5;
|
|
68
|
+
const contentTokens = message.content.reduce((sum, block) => {
|
|
69
|
+
return sum + estimateBlockTokens(block);
|
|
70
|
+
}, 0);
|
|
71
|
+
return baseOverhead + contentTokens;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 估算消息列表的总 token 数量
|
|
75
|
+
*/
|
|
76
|
+
function estimateTotalTokens(messages) {
|
|
77
|
+
if (!messages || !Array.isArray(messages))
|
|
78
|
+
return 0;
|
|
79
|
+
return messages.reduce((sum, message) => {
|
|
80
|
+
return sum + estimateMessageTokens(message);
|
|
81
|
+
}, 0);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 根据目标 token 数选择要保留的消息
|
|
85
|
+
*
|
|
86
|
+
* 从后往前累加,直到达到目标 token 数
|
|
87
|
+
*
|
|
88
|
+
* @param messages - 消息列表
|
|
89
|
+
* @param targetTokens - 目标 token 数
|
|
90
|
+
* @param minMessages - 最少保留消息数(默认 2)
|
|
91
|
+
* @returns 保留的消息和移除的消息
|
|
92
|
+
*/
|
|
93
|
+
function selectMessagesByTokens(messages, targetTokens, minMessages = 2) {
|
|
94
|
+
if (!messages || messages.length === 0) {
|
|
95
|
+
return { retained: [], removed: [] };
|
|
96
|
+
}
|
|
97
|
+
let currentTokens = 0;
|
|
98
|
+
const retained = [];
|
|
99
|
+
// 从后往前累加,保留最近的消息
|
|
100
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
101
|
+
const msgTokens = estimateMessageTokens(messages[i]);
|
|
102
|
+
// 如果已经超过目标但还没达到最小消息数,继续保留
|
|
103
|
+
if (currentTokens + msgTokens > targetTokens && retained.length >= minMessages) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
retained.unshift(messages[i]);
|
|
107
|
+
currentTokens += msgTokens;
|
|
108
|
+
}
|
|
109
|
+
// 确保至少保留 minMessages 条消息
|
|
110
|
+
while (retained.length < minMessages && retained.length < messages.length) {
|
|
111
|
+
const idx = messages.length - retained.length - 1;
|
|
112
|
+
if (idx >= 0) {
|
|
113
|
+
retained.unshift(messages[idx]);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const removed = messages.slice(0, messages.length - retained.length);
|
|
120
|
+
return { retained, removed };
|
|
121
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上下文压缩策略模块 - 类型定义
|
|
3
|
+
*
|
|
4
|
+
* 支持多种压缩算法:
|
|
5
|
+
* - simple: 简单截断(保留最近消息 + 文本摘要)
|
|
6
|
+
* - ai: AI 智能压缩(使用 LLM 生成结构化摘要)
|
|
7
|
+
*/
|
|
8
|
+
import { Message, Timeline, Bookmark } from '../types';
|
|
9
|
+
import { Store } from '../../infra/store';
|
|
10
|
+
import { Sandbox } from '../../infra/sandbox';
|
|
11
|
+
import { ModelProvider, ModelConfig } from '../../infra/provider';
|
|
12
|
+
/**
|
|
13
|
+
* 上下文使用情况分析结果
|
|
14
|
+
*/
|
|
15
|
+
export interface ContextUsage {
|
|
16
|
+
totalTokens: number;
|
|
17
|
+
messageCount: number;
|
|
18
|
+
shouldCompress: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 压缩结果
|
|
22
|
+
*/
|
|
23
|
+
export interface CompressionResult {
|
|
24
|
+
/** 生成的摘要消息 */
|
|
25
|
+
summary: Message;
|
|
26
|
+
/** 被移除的消息 */
|
|
27
|
+
removedMessages: Message[];
|
|
28
|
+
/** 保留的消息 */
|
|
29
|
+
retainedMessages: Message[];
|
|
30
|
+
/** 历史窗口 ID */
|
|
31
|
+
windowId: string;
|
|
32
|
+
/** 压缩记录 ID */
|
|
33
|
+
compressionId: string;
|
|
34
|
+
/** 压缩比例(保留消息数 / 总消息数) */
|
|
35
|
+
ratio: number;
|
|
36
|
+
/** 使用的压缩算法 */
|
|
37
|
+
algorithm?: 'simple' | 'ai';
|
|
38
|
+
/** 摘要是否由 AI 生成(降级时为 false) */
|
|
39
|
+
aiGenerated?: boolean;
|
|
40
|
+
/** 压缩掉的 token 数(估算) */
|
|
41
|
+
tokensRemoved?: number;
|
|
42
|
+
/** 保留的 token 数(估算) */
|
|
43
|
+
tokensRetained?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 文件池状态接口(用于获取访问过的文件)
|
|
47
|
+
*/
|
|
48
|
+
export interface FilePoolState {
|
|
49
|
+
getAccessedFiles(): Array<{
|
|
50
|
+
path: string;
|
|
51
|
+
mtime: number;
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 压缩事件发送器
|
|
56
|
+
*/
|
|
57
|
+
export interface CompressionEventEmitter {
|
|
58
|
+
emit(event: CompressionEventPayload): void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 压缩事件负载
|
|
62
|
+
*/
|
|
63
|
+
export interface CompressionEventPayload {
|
|
64
|
+
phase: 'start' | 'summarizing' | 'end' | 'fallback';
|
|
65
|
+
algorithm?: 'simple' | 'ai';
|
|
66
|
+
summary?: string;
|
|
67
|
+
ratio?: number;
|
|
68
|
+
tokensRemoved?: number;
|
|
69
|
+
tokensRetained?: number;
|
|
70
|
+
error?: string;
|
|
71
|
+
bookmark?: Bookmark;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 压缩策略接口
|
|
75
|
+
*
|
|
76
|
+
* 所有压缩算法(simple、ai)都需要实现此接口
|
|
77
|
+
*/
|
|
78
|
+
export interface ICompressionStrategy {
|
|
79
|
+
/**
|
|
80
|
+
* 分析上下文使用情况
|
|
81
|
+
*/
|
|
82
|
+
analyze(messages: Message[]): ContextUsage;
|
|
83
|
+
/**
|
|
84
|
+
* 执行压缩
|
|
85
|
+
*
|
|
86
|
+
* @param messages - 当前消息列表
|
|
87
|
+
* @param events - 事件时间线
|
|
88
|
+
* @param filePool - 文件池状态(可选)
|
|
89
|
+
* @param sandbox - 沙箱实例(可选,用于读取文件内容)
|
|
90
|
+
* @returns 压缩结果,如果不需要压缩则返回 undefined
|
|
91
|
+
*/
|
|
92
|
+
compress(messages: Message[], events: Timeline[], filePool?: FilePoolState, sandbox?: Sandbox): Promise<CompressionResult | undefined>;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 模型工厂函数类型
|
|
96
|
+
*/
|
|
97
|
+
export type ModelFactory = (config: ModelConfig) => ModelProvider;
|
|
98
|
+
/**
|
|
99
|
+
* 压缩策略基础配置
|
|
100
|
+
*/
|
|
101
|
+
export interface CompressionStrategyBaseOptions {
|
|
102
|
+
/** Store 实例(用于保存历史窗口和压缩记录) */
|
|
103
|
+
store: Store;
|
|
104
|
+
/** Agent ID */
|
|
105
|
+
agentId: string;
|
|
106
|
+
/** 压缩触发阈值(tokens) */
|
|
107
|
+
maxTokens: number;
|
|
108
|
+
/** 压缩后目标大小(tokens) */
|
|
109
|
+
compressToTokens: number;
|
|
110
|
+
/** 事件发送器 */
|
|
111
|
+
eventEmitter?: CompressionEventEmitter;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 简单压缩策略配置
|
|
115
|
+
*/
|
|
116
|
+
export interface SimpleCompressionStrategyOptions extends CompressionStrategyBaseOptions {
|
|
117
|
+
/** 压缩模型名称(仅用于记录) */
|
|
118
|
+
compressionModel?: string;
|
|
119
|
+
/** 压缩提示词(仅用于记录) */
|
|
120
|
+
compressionPrompt?: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* AI 压缩策略配置
|
|
124
|
+
*/
|
|
125
|
+
export interface AICompressionStrategyOptions extends CompressionStrategyBaseOptions {
|
|
126
|
+
/** 主模型配置(用于复用 apiKey/baseUrl) */
|
|
127
|
+
modelConfig: ModelConfig;
|
|
128
|
+
/** 模型工厂(用于创建摘要模型) */
|
|
129
|
+
modelFactory: ModelFactory;
|
|
130
|
+
/** 摘要模型配置(可选,覆盖主模型配置) */
|
|
131
|
+
summaryModelConfig?: {
|
|
132
|
+
model?: string;
|
|
133
|
+
apiKey?: string;
|
|
134
|
+
baseUrl?: string;
|
|
135
|
+
};
|
|
136
|
+
/** 自定义摘要提示词 */
|
|
137
|
+
summaryPrompt?: string;
|
|
138
|
+
/** 始终保留最近 N 轮对话(默认 3) */
|
|
139
|
+
preserveRecentTurns?: number;
|
|
140
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface Configurable<TConfig> {
|
|
2
|
+
toConfig(): TConfig;
|
|
3
|
+
}
|
|
4
|
+
export interface SerializedComponent<TKind extends string, TConfig = Record<string, any>> {
|
|
5
|
+
kind: TKind;
|
|
6
|
+
config: TConfig;
|
|
7
|
+
}
|
|
8
|
+
export interface WithMetadata<TMeta = Record<string, any>> {
|
|
9
|
+
metadata?: TMeta;
|
|
10
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Message, Timeline } from './types';
|
|
2
|
+
import { Store, HistoryWindow, CompressionRecord, RecoveredFile } from '../infra/store';
|
|
3
|
+
import { Sandbox } from '../infra/sandbox';
|
|
4
|
+
import { ModelConfig } from '../infra/provider';
|
|
5
|
+
import { CompressionEventEmitter, ModelFactory } from './compression';
|
|
6
|
+
export interface ContextUsage {
|
|
7
|
+
totalTokens: number;
|
|
8
|
+
messageCount: number;
|
|
9
|
+
shouldCompress: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface CompressionResult {
|
|
12
|
+
summary: Message;
|
|
13
|
+
removedMessages: Message[];
|
|
14
|
+
retainedMessages: Message[];
|
|
15
|
+
windowId: string;
|
|
16
|
+
compressionId: string;
|
|
17
|
+
ratio: number;
|
|
18
|
+
/** 使用的压缩算法 */
|
|
19
|
+
algorithm?: 'simple' | 'ai';
|
|
20
|
+
/** 摘要是否由 AI 生成 */
|
|
21
|
+
aiGenerated?: boolean;
|
|
22
|
+
/** 压缩掉的 token 数 */
|
|
23
|
+
tokensRemoved?: number;
|
|
24
|
+
/** 保留的 token 数 */
|
|
25
|
+
tokensRetained?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ContextManagerOptions {
|
|
28
|
+
maxTokens?: number;
|
|
29
|
+
compressToTokens?: number;
|
|
30
|
+
compressionModel?: string;
|
|
31
|
+
compressionPrompt?: string;
|
|
32
|
+
/** 压缩算法:'simple' (默认) 或 'ai' */
|
|
33
|
+
algorithm?: 'simple' | 'ai';
|
|
34
|
+
/** 主模型配置(用于复用 apiKey/baseUrl) */
|
|
35
|
+
modelConfig?: ModelConfig;
|
|
36
|
+
/** 模型工厂(用于创建摘要模型) */
|
|
37
|
+
modelFactory?: ModelFactory;
|
|
38
|
+
/** 摘要模型配置(可选,覆盖主模型配置) */
|
|
39
|
+
summaryModelConfig?: {
|
|
40
|
+
model?: string;
|
|
41
|
+
apiKey?: string;
|
|
42
|
+
baseUrl?: string;
|
|
43
|
+
};
|
|
44
|
+
/** 自定义摘要提示词 */
|
|
45
|
+
summaryPrompt?: string;
|
|
46
|
+
/** 始终保留最近 N 轮对话(默认 3) */
|
|
47
|
+
preserveRecentTurns?: number;
|
|
48
|
+
/** 事件发送器 */
|
|
49
|
+
eventEmitter?: CompressionEventEmitter;
|
|
50
|
+
}
|
|
51
|
+
export interface FilePoolState {
|
|
52
|
+
getAccessedFiles(): Array<{
|
|
53
|
+
path: string;
|
|
54
|
+
mtime: number;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* ContextManager v3 - 支持多种压缩策略的上下文管理器
|
|
59
|
+
*
|
|
60
|
+
* 职责:
|
|
61
|
+
* 1. 分析上下文使用情况(token 估算)
|
|
62
|
+
* 2. 根据配置选择压缩策略(simple 或 ai)
|
|
63
|
+
* 3. 委托策略执行压缩
|
|
64
|
+
* 4. 提供历史记录查询接口
|
|
65
|
+
*
|
|
66
|
+
* 策略模式:
|
|
67
|
+
* - simple: 简单截断(保留最近消息 + 文本摘要)
|
|
68
|
+
* - ai: AI 智能压缩(使用 LLM 生成结构化摘要)
|
|
69
|
+
*/
|
|
70
|
+
export declare class ContextManager {
|
|
71
|
+
private readonly store;
|
|
72
|
+
private readonly agentId;
|
|
73
|
+
private readonly strategy;
|
|
74
|
+
constructor(store: Store, agentId: string, opts?: ContextManagerOptions);
|
|
75
|
+
/**
|
|
76
|
+
* 创建压缩策略
|
|
77
|
+
*
|
|
78
|
+
* 默认值基于 Claude 官方最佳实践:
|
|
79
|
+
* - maxTokens: 64K = 50% 的 128K 上下文窗口(对标 Claude 默认 100K/200K)
|
|
80
|
+
* - compressToTokens: 40K = 约 62.5% 的 maxTokens
|
|
81
|
+
* @see https://platform.claude.com/cookbook/tool-use-automatic-context-compaction
|
|
82
|
+
*/
|
|
83
|
+
private createStrategy;
|
|
84
|
+
/**
|
|
85
|
+
* 分析上下文使用情况(粗略的 token 估算)
|
|
86
|
+
*
|
|
87
|
+
* 方法签名保持不变,委托给策略执行
|
|
88
|
+
*/
|
|
89
|
+
analyze(messages: Message[]): ContextUsage;
|
|
90
|
+
/**
|
|
91
|
+
* 压缩上下文并保存历史
|
|
92
|
+
*
|
|
93
|
+
* 方法签名保持不变,委托给策略执行
|
|
94
|
+
*
|
|
95
|
+
* 流程:
|
|
96
|
+
* 1. 保存 HistoryWindow(压缩前的完整快照)
|
|
97
|
+
* 2. 执行压缩(根据策略不同,方式不同)
|
|
98
|
+
* 3. 保存 CompressionRecord(压缩元信息)
|
|
99
|
+
* 4. 保存重要文件快照(如果有 FilePool)
|
|
100
|
+
* 5. 返回压缩结果
|
|
101
|
+
*/
|
|
102
|
+
compress(messages: Message[], events: Timeline[], filePool?: FilePoolState, sandbox?: Sandbox): Promise<CompressionResult | undefined>;
|
|
103
|
+
/**
|
|
104
|
+
* 恢复历史窗口(用于审计或调试)
|
|
105
|
+
*/
|
|
106
|
+
loadHistory(): Promise<HistoryWindow[]>;
|
|
107
|
+
/**
|
|
108
|
+
* 加载压缩记录
|
|
109
|
+
*/
|
|
110
|
+
loadCompressions(): Promise<CompressionRecord[]>;
|
|
111
|
+
/**
|
|
112
|
+
* 加载恢复的文件
|
|
113
|
+
*/
|
|
114
|
+
loadRecoveredFiles(): Promise<RecoveredFile[]>;
|
|
115
|
+
}
|