foliko 1.1.93 → 2.0.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.
Files changed (212) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/CLAUDE.md +56 -30
  3. package/REFACTORING_PLAN.md +645 -0
  4. package/docs/architecture.md +131 -0
  5. package/docs/migration.md +57 -0
  6. package/docs/public-api.md +138 -0
  7. package/docs/usage.md +385 -0
  8. package/examples/ambient-example.js +20 -137
  9. package/examples/basic.js +21 -48
  10. package/examples/bootstrap.js +16 -74
  11. package/examples/mcp-example.js +6 -29
  12. package/examples/skill-example.js +6 -19
  13. package/examples/workflow.js +8 -56
  14. package/package.json +8 -4
  15. package/plugins/README.md +49 -0
  16. package/plugins/{ambient-agent → ambient}/EventWatcher.js +1 -1
  17. package/plugins/{ambient-agent → ambient}/ExplorerLoop.js +3 -3
  18. package/plugins/{ambient-agent → ambient}/GoalManager.js +2 -2
  19. package/plugins/ambient/README.md +14 -0
  20. package/plugins/{ambient-agent → ambient}/Reflector.js +1 -1
  21. package/plugins/{ambient-agent → ambient}/StateStore.js +1 -1
  22. package/plugins/{ambient-agent → ambient}/index.js +2 -2
  23. package/plugins/{ai-plugin.js → core/ai/index.js} +14 -30
  24. package/plugins/{audit-plugin.js → core/audit/index.js} +3 -30
  25. package/plugins/{coordinator-plugin.js → core/coordinator/index.js} +3 -35
  26. package/plugins/core/default/bootstrap.js +202 -0
  27. package/plugins/core/default/config.js +220 -0
  28. package/plugins/core/default/index.js +58 -0
  29. package/plugins/core/mcp/index.js +1 -0
  30. package/plugins/{python-plugin-loader.js → core/python-loader/index.js} +7 -187
  31. package/plugins/{rules-plugin.js → core/rules/index.js} +121 -64
  32. package/plugins/{scheduler-plugin.js → core/scheduler/index.js} +12 -114
  33. package/plugins/{session-plugin.js → core/session/index.js} +9 -73
  34. package/{src/capabilities/skill-manager.js → plugins/core/skill-manager/index.js} +64 -18
  35. package/plugins/{storage-plugin.js → core/storage/index.js} +5 -29
  36. package/plugins/{subagent-plugin.js → core/sub-agent/index.js} +10 -171
  37. package/plugins/{think-plugin.js → core/think/index.js} +24 -91
  38. package/{src/capabilities/workflow-engine.js → plugins/core/workflow/index.js} +87 -85
  39. package/plugins/default-plugins.js +6 -720
  40. package/plugins/{data-splitter-plugin.js → executors/data-splitter/index.js} +9 -83
  41. package/plugins/{extension-executor-plugin.js → executors/extension/index.js} +13 -97
  42. package/plugins/{python-executor-plugin.js → executors/python/index.js} +6 -31
  43. package/plugins/{shell-executor-plugin.js → executors/shell/index.js} +2 -5
  44. package/plugins/install/README.md +9 -0
  45. package/plugins/{install-plugin.js → install/index.js} +3 -3
  46. package/plugins/{file-system-plugin.js → io/file-system/index.js} +34 -236
  47. package/plugins/{web-plugin.js → io/web/index.js} +11 -113
  48. package/plugins/memory/README.md +13 -0
  49. package/plugins/{memory-plugin.js → memory/index.js} +4 -18
  50. package/plugins/messaging/email/README.md +19 -0
  51. package/plugins/{email → messaging/email}/index.js +2 -2
  52. package/plugins/{feishu-plugin.js → messaging/feishu/index.js} +3 -3
  53. package/plugins/{qq-plugin.js → messaging/qq/index.js} +5 -16
  54. package/plugins/{telegram-plugin.js → messaging/telegram/index.js} +3 -3
  55. package/plugins/{weixin-plugin.js → messaging/weixin/index.js} +15 -15
  56. package/plugins/{plugin-manager-plugin.js → plugin-manager/index.js} +36 -180
  57. package/plugins/{tools-plugin.js → tools/index.js} +68 -116
  58. package/plugins/trading/README.md +15 -0
  59. package/plugins/{gate-trading.js → trading/index.js} +8 -8
  60. package/{examples → sandbox}/test-concurrent-chat.js +2 -2
  61. package/{examples → sandbox}/test-long-chat.js +2 -2
  62. package/{examples → sandbox}/test-session-chat.js +2 -2
  63. package/{examples → sandbox}/test-web-plugin.js +1 -1
  64. package/{examples → sandbox}/test-weixin-feishu.js +2 -2
  65. package/src/agent/base.js +56 -0
  66. package/src/{core/agent-chat.js → agent/chat.js} +11 -11
  67. package/src/{core/coordinator-manager.js → agent/coordinator.js} +3 -3
  68. package/src/agent/index.js +111 -0
  69. package/src/agent/main.js +337 -0
  70. package/src/agent/prompt.js +78 -0
  71. package/src/agent/sub.js +198 -0
  72. package/src/agent/worker.js +104 -0
  73. package/{cli/bin/foliko.js → src/cli/bin.js} +1 -1
  74. package/{cli/src → src/cli}/commands/chat.js +25 -21
  75. package/{cli/src → src/cli}/index.js +1 -0
  76. package/{cli/src → src/cli}/ui/chat-ui-old.js +40 -178
  77. package/{cli/src → src/cli}/ui/chat-ui.js +3 -3
  78. package/{cli/src → src/cli}/ui/components/footer-bar.js +1 -1
  79. package/src/common/errors.js +402 -0
  80. package/src/{utils → common}/logger.js +33 -0
  81. package/src/{utils/chat-queue.js → common/queue.js} +2 -2
  82. package/src/config/plugin-config.js +50 -0
  83. package/src/context/agent.js +32 -0
  84. package/src/context/compaction-prompts.js +170 -0
  85. package/src/context/compaction-utils.js +191 -0
  86. package/src/context/compressor.js +413 -0
  87. package/src/context/index.js +9 -0
  88. package/src/{core/context-manager.js → context/manager.js} +1 -1
  89. package/src/context/request.js +50 -0
  90. package/src/context/session.js +33 -0
  91. package/src/context/storage.js +30 -0
  92. package/src/executors/mcp-client.js +153 -0
  93. package/src/executors/mcp-desc.js +236 -0
  94. package/src/executors/mcp-executor.js +91 -956
  95. package/src/{core → framework}/command-registry.js +1 -1
  96. package/src/framework/framework.js +300 -0
  97. package/src/framework/index.js +18 -0
  98. package/src/framework/lifecycle.js +203 -0
  99. package/src/framework/loader.js +78 -0
  100. package/src/framework/registry.js +86 -0
  101. package/src/{core/ui-extension-context.js → framework/ui-extension.js} +1 -1
  102. package/src/index.js +130 -15
  103. package/src/llm/index.js +26 -0
  104. package/src/llm/provider.js +212 -0
  105. package/src/llm/registry.js +11 -0
  106. package/src/{core/token-counter.js → llm/tokens.js} +4 -37
  107. package/src/{core/plugin-base.js → plugin/base.js} +10 -136
  108. package/src/plugin/index.js +14 -0
  109. package/src/plugin/loader.js +101 -0
  110. package/src/plugin/manager.js +261 -0
  111. package/src/{core → session}/branch-summary-auto.js +2 -2
  112. package/src/{core/chat-session.js → session/chat.js} +2 -2
  113. package/src/session/index.js +7 -0
  114. package/src/{core/session-manager.js → session/session.js} +2 -2
  115. package/src/session/ttl.js +92 -0
  116. package/src/{core/jsonl-storage.js → storage/jsonl.js} +1 -1
  117. package/src/tool/executor.js +85 -0
  118. package/src/tool/index.js +15 -0
  119. package/src/tool/registry.js +143 -0
  120. package/src/{core/tool-router.js → tool/router.js} +17 -124
  121. package/src/tool/schema.js +108 -0
  122. package/src/utils/data-splitter.js +1 -1
  123. package/src/utils/download.js +1 -1
  124. package/src/utils/index.js +6 -6
  125. package/src/utils/message-validator.js +1 -1
  126. package/tests/core/context-storage.test.js +46 -0
  127. package/tests/core/llm.test.js +54 -0
  128. package/tests/core/plugin.test.js +42 -0
  129. package/tests/core/tool.test.js +60 -0
  130. package/tests/setup.js +10 -0
  131. package/tests/smoke.test.js +58 -0
  132. package/vitest.config.js +9 -0
  133. package/cli/src/daemon.js +0 -149
  134. package/docs/CONTEXT_DESIGN.md +0 -1596
  135. package/docs/ai-sdk-optimization.md +0 -655
  136. package/docs/features.md +0 -120
  137. package/docs/qq-bot.md +0 -976
  138. package/docs/quick-reference.md +0 -160
  139. package/docs/user-manual.md +0 -1391
  140. package/images/geometric_shapes.jpg +0 -0
  141. package/images/sunset_mountain_lake.jpg +0 -0
  142. package/skills/poster-guide/SKILL.md +0 -792
  143. package/src/capabilities/index.js +0 -11
  144. package/src/core/agent.js +0 -808
  145. package/src/core/context-compressor.js +0 -959
  146. package/src/core/enhanced-context-compressor.js +0 -210
  147. package/src/core/framework.js +0 -1422
  148. package/src/core/index.js +0 -30
  149. package/src/core/plugin-manager.js +0 -961
  150. package/src/core/provider-registry.js +0 -159
  151. package/src/core/provider.js +0 -156
  152. package/src/core/request-context.js +0 -98
  153. package/src/core/subagent.js +0 -442
  154. package/src/core/system-prompt-builder.js +0 -120
  155. package/src/core/tool-executor.js +0 -202
  156. package/src/core/tool-registry.js +0 -517
  157. package/src/core/worker-agent.js +0 -192
  158. package/src/executors/executor-base.js +0 -58
  159. package/src/utils/error-boundary.js +0 -363
  160. package/src/utils/error.js +0 -374
  161. package/system.md +0 -1645
  162. package/website_v2/README.md +0 -57
  163. package/website_v2/SPEC.md +0 -1
  164. package/website_v2/docs/api.html +0 -128
  165. package/website_v2/docs/configuration.html +0 -147
  166. package/website_v2/docs/plugin-development.html +0 -129
  167. package/website_v2/docs/project-structure.html +0 -89
  168. package/website_v2/docs/skill-development.html +0 -85
  169. package/website_v2/index.html +0 -489
  170. package/website_v2/scripts/main.js +0 -93
  171. package/website_v2/styles/animations.css +0 -8
  172. package/website_v2/styles/docs.css +0 -83
  173. package/website_v2/styles/main.css +0 -417
  174. package/xhs_auth.json +0 -268
  175. package//346/265/267/346/212/245/346/217/222/344/273/266.md +0 -621
  176. /package/plugins/{ambient-agent → ambient}/constants.js +0 -0
  177. /package/plugins/{email → messaging/email}/constants.js +0 -0
  178. /package/plugins/{email → messaging/email}/handlers.js +0 -0
  179. /package/plugins/{email → messaging/email}/monitor.js +0 -0
  180. /package/plugins/{email → messaging/email}/parser.js +0 -0
  181. /package/plugins/{email → messaging/email}/reply.js +0 -0
  182. /package/plugins/{email → messaging/email}/utils.js +0 -0
  183. /package/{examples → sandbox}/test-chat.js +0 -0
  184. /package/{examples → sandbox}/test-mcp.js +0 -0
  185. /package/{examples → sandbox}/test-reload.js +0 -0
  186. /package/{examples → sandbox}/test-telegram.js +0 -0
  187. /package/{examples → sandbox}/test-tg-bot.js +0 -0
  188. /package/{examples → sandbox}/test-tg-simple.js +0 -0
  189. /package/{examples → sandbox}/test-tg.js +0 -0
  190. /package/{examples → sandbox}/test-think.js +0 -0
  191. /package/src/{core/sub-agent-config.js → agent/sub-config.js} +0 -0
  192. /package/{cli/src → src/cli}/commands/daemon.js +0 -0
  193. /package/{cli/src → src/cli}/commands/list.js +0 -0
  194. /package/{cli/src → src/cli}/commands/plugin.js +0 -0
  195. /package/{cli/src → src/cli}/ui/components/agent-mention-provider.js +0 -0
  196. /package/{cli/src → src/cli}/ui/components/chained-autocomplete-provider.js +0 -0
  197. /package/{cli/src → src/cli}/ui/components/message-bubble.js +0 -0
  198. /package/{cli/src → src/cli}/ui/components/status-bar.js +0 -0
  199. /package/{cli/src → src/cli}/utils/ansi.js +0 -0
  200. /package/{cli/src → src/cli}/utils/config.js +0 -0
  201. /package/{cli/src → src/cli}/utils/markdown.js +0 -0
  202. /package/{cli/src → src/cli}/utils/plugin-config.js +0 -0
  203. /package/{cli/src → src/cli}/utils/render-diff.js +0 -0
  204. /package/src/{utils/circuit-breaker.js → common/circuit.js} +0 -0
  205. /package/src/{core → common}/constants.js +0 -0
  206. /package/src/{utils/edit-diff.js → common/diff.js} +0 -0
  207. /package/src/{utils/event-emitter.js → common/events.js} +0 -0
  208. /package/src/{utils → common}/id.js +0 -0
  209. /package/src/{utils → common}/retry.js +0 -0
  210. /package/src/{core/notification-manager.js → notification/manager.js} +0 -0
  211. /package/src/{core/session-entry.js → session/entry.js} +0 -0
  212. /package/src/{core/storage-manager.js → storage/manager.js} +0 -0
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Compaction utility functions
5
+ * 提取自 compressor.js 的纯函数工具集
6
+ */
7
+
8
+ const { estimateTokens } = require('../llm/tokens');
9
+ const { safeJsonStringify } = require('../llm/tokens');
10
+ const { EntryTypes, createCustomMessage, createBranchSummaryMessage, createCompactionSummaryMessage } = require('../session/entry');
11
+
12
+ const TOOL_RESULT_MAX_CHARS = 2000;
13
+
14
+ function createFileOps() {
15
+ return { read: new Set(), written: new Set(), edited: new Set() };
16
+ }
17
+
18
+ function extractFileOpsFromMessage(message, fileOps) {
19
+ if (message.role !== 'assistant') return;
20
+ if (!message.content || !Array.isArray(message.content)) return;
21
+
22
+ for (const block of message.content) {
23
+ if (!block || block.type !== 'toolCall') continue;
24
+ const args = block.arguments;
25
+ if (!args) continue;
26
+ const filePath = typeof args.path === 'string' ? args.path : undefined;
27
+ if (!filePath) continue;
28
+ switch (block.name) {
29
+ case 'read': fileOps.read.add(filePath); break;
30
+ case 'write': fileOps.written.add(filePath); break;
31
+ case 'edit': fileOps.edited.add(filePath); break;
32
+ }
33
+ }
34
+ }
35
+
36
+ function computeFileLists(fileOps) {
37
+ const modified = new Set([...fileOps.edited, ...fileOps.written]);
38
+ const readOnly = [...fileOps.read].filter(f => !modified.has(f)).sort();
39
+ const modifiedFiles = [...modified].sort();
40
+ return { readFiles: readOnly, modifiedFiles };
41
+ }
42
+
43
+ function formatFileOperations(readFiles, modifiedFiles) {
44
+ const sections = [];
45
+ if (readFiles.length > 0) sections.push(`<read-files>\n${readFiles.join('\n')}\n</read-files>`);
46
+ if (modifiedFiles.length > 0) sections.push(`<modified-files>\n${modifiedFiles.join('\n')}\n</modified-files>`);
47
+ if (sections.length === 0) return '';
48
+ return `\n\n${sections.join('\n\n')}`;
49
+ }
50
+
51
+ function truncateForSummary(text, maxChars) {
52
+ if (text.length <= maxChars) return text;
53
+ const truncatedChars = text.length - maxChars;
54
+ return `${text.slice(0, maxChars)}\n\n[... ${truncatedChars} more characters truncated]`;
55
+ }
56
+
57
+ function serializeConversation(messages) {
58
+ const parts = [];
59
+ for (const msg of messages) {
60
+ if (msg.role === 'user') {
61
+ const content = Array.isArray(msg.content)
62
+ ? msg.content.filter(c => c.type === 'text').map(c => c.text).join('')
63
+ : msg.content;
64
+ if (content) parts.push(`[User]: ${content}`);
65
+ } else if (msg.role === 'assistant') {
66
+ const textParts = []; const thinkingParts = []; const toolCalls = [];
67
+ for (const block of msg.content || []) {
68
+ if (block.type === 'text') textParts.push(block.text);
69
+ else if (block.type === 'thinking') thinkingParts.push(block.thinking);
70
+ else if (block.type === 'toolCall') {
71
+ const argsStr = Object.entries(block.arguments || {})
72
+ .map(([k, v]) => `${k}=${safeJsonStringify(v)}`).join(', ');
73
+ toolCalls.push(`${block.name}(${argsStr})`);
74
+ }
75
+ }
76
+ if (thinkingParts.length > 0) parts.push(`[Assistant thinking]: ${thinkingParts.join('\n')}`);
77
+ if (textParts.length > 0) parts.push(`[Assistant]: ${textParts.join('\n')}`);
78
+ if (toolCalls.length > 0) parts.push(`[Assistant tool calls]: ${toolCalls.join('; ')}`);
79
+ } else if (msg.role === 'toolResult') {
80
+ const content = Array.isArray(msg.content)
81
+ ? msg.content.filter(c => c.type === 'text').map(c => c.text).join('')
82
+ : msg.content;
83
+ if (content) parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
84
+ }
85
+ }
86
+ return parts.join('\n\n');
87
+ }
88
+
89
+ function getMessageFromEntry(entry) {
90
+ switch (entry.type) {
91
+ case EntryTypes.MESSAGE:
92
+ if (entry.message.role === 'toolResult') return undefined;
93
+ return entry.message;
94
+ case EntryTypes.CUSTOM_MESSAGE:
95
+ return createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);
96
+ case EntryTypes.BRANCH_SUMMARY:
97
+ return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
98
+ case EntryTypes.COMPACTION:
99
+ return createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);
100
+ default:
101
+ return undefined;
102
+ }
103
+ }
104
+
105
+ function getMessageFromEntryForCompaction(entry) {
106
+ if (entry.type === EntryTypes.COMPACTION) return undefined;
107
+ return getMessageFromEntry(entry);
108
+ }
109
+
110
+ function findValidCutPoints(entries, startIndex, endIndex) {
111
+ const cutPoints = [];
112
+ for (let i = startIndex; i < endIndex; i++) {
113
+ const entry = entries[i];
114
+ if (entry.type === EntryTypes.MESSAGE) {
115
+ const role = entry.message.role;
116
+ switch (role) {
117
+ case 'bashExecution': case 'custom': case 'branchSummary':
118
+ case 'compactionSummary': case 'user': case 'assistant':
119
+ cutPoints.push(i); break;
120
+ case 'toolResult': break;
121
+ }
122
+ }
123
+ if (entry.type === EntryTypes.BRANCH_SUMMARY || entry.type === EntryTypes.CUSTOM_MESSAGE) {
124
+ cutPoints.push(i);
125
+ }
126
+ }
127
+ return cutPoints;
128
+ }
129
+
130
+ function findTurnStartIndex(entries, entryIndex, startIndex) {
131
+ for (let i = entryIndex; i >= startIndex; i--) {
132
+ const entry = entries[i];
133
+ if (entry.type === EntryTypes.BRANCH_SUMMARY || entry.type === EntryTypes.CUSTOM_MESSAGE) return i;
134
+ if (entry.type === EntryTypes.MESSAGE && (entry.message.role === 'user' || entry.message.role === 'bashExecution')) return i;
135
+ }
136
+ return -1;
137
+ }
138
+
139
+ function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
140
+ const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
141
+ if (cutPoints.length === 0) {
142
+ return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };
143
+ }
144
+ let accumulatedTokens = 0;
145
+ let cutIndex = cutPoints[0];
146
+
147
+ for (let i = endIndex - 1; i >= startIndex; i--) {
148
+ const entry = entries[i];
149
+ if (entry.type !== EntryTypes.MESSAGE) continue;
150
+ accumulatedTokens += estimateTokens(entry.message);
151
+ if (accumulatedTokens >= keepRecentTokens) {
152
+ for (let c = 0; c < cutPoints.length; c++) {
153
+ if (cutPoints[c] >= i) { cutIndex = cutPoints[c]; break; }
154
+ }
155
+ break;
156
+ }
157
+ }
158
+ while (cutIndex > startIndex) {
159
+ const prevEntry = entries[cutIndex - 1];
160
+ if (prevEntry.type === EntryTypes.COMPACTION) break;
161
+ if (prevEntry.type === EntryTypes.MESSAGE) break;
162
+ cutIndex--;
163
+ }
164
+
165
+ const cutEntry = entries[cutIndex];
166
+ const isUserMessage = cutEntry.type === EntryTypes.MESSAGE && cutEntry.message.role === 'user';
167
+ const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
168
+
169
+ return { firstKeptEntryIndex: cutIndex, turnStartIndex, isSplitTurn: !isUserMessage && turnStartIndex !== -1 };
170
+ }
171
+
172
+ function extractFileOperations(messages, entries, prevCompactionIndex) {
173
+ const fileOps = createFileOps();
174
+ if (prevCompactionIndex >= 0) {
175
+ const prevCompaction = entries[prevCompactionIndex];
176
+ if (!prevCompaction.fromHook && prevCompaction.details) {
177
+ const { details } = prevCompaction;
178
+ if (Array.isArray(details.readFiles)) { for (const f of details.readFiles) fileOps.read.add(f); }
179
+ if (Array.isArray(details.modifiedFiles)) { for (const f of details.modifiedFiles) fileOps.edited.add(f); }
180
+ }
181
+ }
182
+ for (const msg of messages) extractFileOpsFromMessage(msg, fileOps);
183
+ return fileOps;
184
+ }
185
+
186
+ module.exports = {
187
+ TOOL_RESULT_MAX_CHARS, createFileOps, extractFileOpsFromMessage,
188
+ computeFileLists, formatFileOperations, truncateForSummary,
189
+ serializeConversation, getMessageFromEntry, getMessageFromEntryForCompaction,
190
+ findValidCutPoints, findTurnStartIndex, findCutPoint, extractFileOperations,
191
+ };
@@ -0,0 +1,413 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ContextCompressor - 上下文压缩器
5
+ * Ported from pi's compaction/compaction.ts with enhanced functionality
6
+ */
7
+
8
+ const { logger } = require('../common/logger');
9
+ const { estimateTokens, estimateContextTokens, buildSessionContext, TokenCounter } = require('../llm/tokens');
10
+ const { validateMessagesPairing } = require('../utils/message-validator');
11
+ const { EntryTypes } = require('../session/entry');
12
+ const {
13
+ createFileOps, extractFileOpsFromMessage, computeFileLists, formatFileOperations,
14
+ serializeConversation, getMessageFromEntry, getMessageFromEntryForCompaction,
15
+ findCutPoint, findTurnStartIndex, extractFileOperations,
16
+ } = require('./compaction-utils');
17
+ const {
18
+ SUMMARIZATION_SYSTEM_PROMPT, SUMMARIZATION_PROMPT, UPDATE_SUMMARIZATION_PROMPT,
19
+ TURN_PREFIX_SUMMARIZATION_PROMPT, BRANCH_SUMMARY_PREAMBLE, BRANCH_SUMMARY_PROMPT,
20
+ DEFAULT_COMPACTION_SETTINGS, MODEL_CONTEXT_LIMITS, COMPRESSION_TIMEOUT_MS,
21
+ } = require('./compaction-prompts');
22
+
23
+ // ─── Helper classes ───────────────────────────────────────────────────
24
+
25
+ class CompactionDetails {
26
+ constructor(readFiles = [], modifiedFiles = []) {
27
+ this.readFiles = readFiles;
28
+ this.modifiedFiles = modifiedFiles;
29
+ }
30
+ }
31
+
32
+ class Result {
33
+ constructor(ok, value, error) {
34
+ this.ok = ok; this.value = value; this.error = error;
35
+ }
36
+ static success(value) { return new Result(true, value, null); }
37
+ static failure(error) { return new Result(false, null, error); }
38
+ }
39
+
40
+ class CompactionError extends Error {
41
+ constructor(code, message, cause) {
42
+ super(message, cause === undefined ? undefined : { cause });
43
+ this.name = 'CompactionError'; this.code = code;
44
+ }
45
+ }
46
+
47
+ class BranchSummaryError extends Error {
48
+ constructor(code, message, cause) {
49
+ super(message, cause === undefined ? undefined : { cause });
50
+ this.name = 'BranchSummaryError'; this.code = code;
51
+ }
52
+ }
53
+
54
+ // ─── Compaction preparation ──────────────────────────────────────────
55
+
56
+ class CompactionResult {
57
+ constructor(summary, firstKeptEntryId, tokensBefore, details) {
58
+ this.summary = summary;
59
+ this.firstKeptEntryId = firstKeptEntryId;
60
+ this.tokensBefore = tokensBefore;
61
+ this.details = details;
62
+ }
63
+ }
64
+
65
+ function prepareCompaction(pathEntries, settings) {
66
+ if (pathEntries.length === 0 || pathEntries[pathEntries.length - 1].type === EntryTypes.COMPACTION) {
67
+ return Result.success(undefined);
68
+ }
69
+
70
+ let prevCompactionIndex = -1;
71
+ for (let i = pathEntries.length - 1; i >= 0; i--) {
72
+ if (pathEntries[i].type === EntryTypes.COMPACTION) { prevCompactionIndex = i; break; }
73
+ }
74
+
75
+ let previousSummary;
76
+ let boundaryStart = 0;
77
+ if (prevCompactionIndex >= 0) {
78
+ const prevCompaction = pathEntries[prevCompactionIndex];
79
+ previousSummary = prevCompaction.summary;
80
+ const firstKeptEntryIndex = pathEntries.findIndex(entry => entry.id === prevCompaction.firstKeptEntryId);
81
+ boundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;
82
+ }
83
+ const boundaryEnd = pathEntries.length;
84
+ const tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;
85
+
86
+ const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
87
+ const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];
88
+ if (!firstKeptEntry?.id) {
89
+ return Result.failure(new CompactionError('invalid_session', 'First kept entry has no UUID'));
90
+ }
91
+ const firstKeptEntryId = firstKeptEntry.id;
92
+
93
+ const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
94
+ const messagesToSummarize = [];
95
+ for (let i = boundaryStart; i < historyEnd; i++) {
96
+ const msg = getMessageFromEntryForCompaction(pathEntries[i]);
97
+ if (msg) messagesToSummarize.push(msg);
98
+ }
99
+ const turnPrefixMessages = [];
100
+ if (cutPoint.isSplitTurn) {
101
+ for (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {
102
+ const msg = getMessageFromEntryForCompaction(pathEntries[i]);
103
+ if (msg) turnPrefixMessages.push(msg);
104
+ }
105
+ }
106
+ const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
107
+ if (cutPoint.isSplitTurn) {
108
+ for (const msg of turnPrefixMessages) extractFileOpsFromMessage(msg, fileOps);
109
+ }
110
+
111
+ return Result.success({
112
+ firstKeptEntryId, messagesToSummarize, turnPrefixMessages,
113
+ isSplitTurn: cutPoint.isSplitTurn, tokensBefore, previousSummary, fileOps, settings,
114
+ });
115
+ }
116
+
117
+ class BranchPreparation {
118
+ constructor(messages, fileOps, totalTokens) {
119
+ this.messages = messages; this.fileOps = fileOps; this.totalTokens = totalTokens;
120
+ }
121
+ }
122
+
123
+ function prepareBranchEntries(entries, tokenBudget = 0) {
124
+ const messages = [];
125
+ const fileOps = createFileOps();
126
+ let totalTokens = 0;
127
+
128
+ for (const entry of entries) {
129
+ if (entry.type === EntryTypes.BRANCH_SUMMARY && !entry.fromHook && entry.details) {
130
+ const { details } = entry;
131
+ if (Array.isArray(details.readFiles)) { for (const f of details.readFiles) fileOps.read.add(f); }
132
+ if (Array.isArray(details.modifiedFiles)) { for (const f of details.modifiedFiles) fileOps.edited.add(f); }
133
+ }
134
+ }
135
+
136
+ for (let i = entries.length - 1; i >= 0; i--) {
137
+ const entry = entries[i];
138
+ const message = getMessageFromEntry(entry);
139
+ if (!message) continue;
140
+ extractFileOpsFromMessage(message, fileOps);
141
+
142
+ const tokens = estimateTokens(message);
143
+ if (tokenBudget > 0 && totalTokens + tokens > tokenBudget) {
144
+ if (entry.type === EntryTypes.COMPACTION || entry.type === EntryTypes.BRANCH_SUMMARY) {
145
+ if (totalTokens < tokenBudget * 0.9) { messages.unshift(message); totalTokens += tokens; }
146
+ }
147
+ break;
148
+ }
149
+ messages.unshift(message);
150
+ totalTokens += tokens;
151
+ }
152
+ return new BranchPreparation(messages, fileOps, totalTokens);
153
+ }
154
+
155
+ function collectEntriesForBranchSummary(session, oldLeafId, targetId) {
156
+ if (!oldLeafId) return { entries: [], commonAncestorId: null };
157
+
158
+ const oldPath = new Set(session.getBranch(oldLeafId).map(e => e.id));
159
+ const targetPath = session.getBranch(targetId);
160
+ let commonAncestorId = null;
161
+ for (let i = targetPath.length - 1; i >= 0; i--) {
162
+ if (oldPath.has(targetPath[i].id)) { commonAncestorId = targetPath[i].id; break; }
163
+ }
164
+
165
+ const entries = [];
166
+ let current = oldLeafId;
167
+ while (current && current !== commonAncestorId) {
168
+ const entry = session.getEntry(current);
169
+ if (!entry) break;
170
+ entries.push(entry);
171
+ current = entry.parentId;
172
+ }
173
+ entries.reverse();
174
+ return { entries, commonAncestorId };
175
+ }
176
+
177
+ // ─── ContextCompressor ───────────────────────────────────────────────
178
+
179
+ class ContextCompressor {
180
+ constructor(config = {}) {
181
+ this.config = config;
182
+ this.agent = config.agent;
183
+ this.framework = config.framework;
184
+
185
+ this.model = config.model || 'deepseek-chat';
186
+ this._maxContextTokens = config.maxContextTokens || this._getDefaultContextLimit();
187
+ this._keepRecentMessages = config.keepRecentMessages || 20;
188
+ this._compressionMessageThreshold = config.compressionMessageThreshold || 200;
189
+ this._enableSmartCompress = config.enableSmartCompress !== false;
190
+ this._compactionSettings = config.compactionSettings || DEFAULT_COMPACTION_SETTINGS;
191
+
192
+ this._compressionCount = 0;
193
+ this._compressionInProgress = false;
194
+ this._compressionPromise = null;
195
+ this._compressionTimeoutId = null;
196
+ this._maxToolResultSize = config.maxToolResultSize || 4000;
197
+ this._tokenCounter = new TokenCounter();
198
+ }
199
+
200
+ _getDefaultContextLimit() {
201
+ const modelKey = Object.keys(MODEL_CONTEXT_LIMITS).find(k =>
202
+ this.model.toLowerCase().includes(k.toLowerCase())
203
+ );
204
+ return modelKey ? MODEL_CONTEXT_LIMITS[modelKey] : 40000;
205
+ }
206
+
207
+ validateMessagesPairing(messages) {
208
+ return validateMessagesPairing(messages);
209
+ }
210
+
211
+ async compress(sessionId, messages, messageStore) {
212
+ if (this._compressionInProgress && this._compressionPromise) {
213
+ logger.debug('Compression already in progress, waiting...');
214
+ return this._compressionPromise;
215
+ }
216
+
217
+ if (messages.length <= this._keepRecentMessages) return;
218
+
219
+ this._compressionInProgress = true;
220
+ this._compressionPromise = this._executeWithTimeout(sessionId, messages, messageStore).finally(() => {
221
+ this._compressionInProgress = false;
222
+ this._compressionPromise = null;
223
+ if (this._compressionTimeoutId) { clearTimeout(this._compressionTimeoutId); this._compressionTimeoutId = null; }
224
+ });
225
+ return this._compressionPromise;
226
+ }
227
+
228
+ async _executeWithTimeout(sessionId, messages, messageStore) {
229
+ try {
230
+ return await Promise.race([
231
+ this._doCompress(sessionId, messages, messageStore),
232
+ new Promise((_, reject) => {
233
+ this._compressionTimeoutId = setTimeout(() => {
234
+ this._compressionTimeoutId = null;
235
+ reject(new Error(`Compression timeout (${COMPRESSION_TIMEOUT_MS}ms)`));
236
+ }, COMPRESSION_TIMEOUT_MS);
237
+ }),
238
+ ]);
239
+ } catch (err) {
240
+ logger.warn('Compression failed:', err.message);
241
+ this._simpleCompress(sessionId, messages, messageStore);
242
+ }
243
+ }
244
+
245
+ cancelCompression() {
246
+ if (this._compressionTimeoutId) { clearTimeout(this._compressionTimeoutId); this._compressionTimeoutId = null; }
247
+ this._compressionInProgress = false;
248
+ this._compressionPromise = null;
249
+ }
250
+
251
+ async _doCompress(sessionId, messages, messageStore) {
252
+ const systemMessages = messages.filter(m => m.role === 'system');
253
+ const otherMessages = messages.filter(m => m.role !== 'system');
254
+ const recentMessages = otherMessages.slice(-this._keepRecentMessages);
255
+ const messagesToSummarize = otherMessages.slice(0, -this._keepRecentMessages);
256
+ const compressedCount = messagesToSummarize.length;
257
+
258
+ let summaryContent = '';
259
+ if (this._enableSmartCompress && this.agent?._chatHandler?._aiClient) {
260
+ try {
261
+ const summaryText = await this._summarizeMessages(messagesToSummarize);
262
+ summaryContent = `[Early conversation summary]: ${summaryText || '(no content)'}`;
263
+ } catch (err) {
264
+ logger.warn('AI summary failed, using simple compression:', err.message);
265
+ summaryContent = `[Context compressed: ${compressedCount} early messages omitted. Kept recent ${this._keepRecentMessages} messages.]`;
266
+ }
267
+ } else {
268
+ summaryContent = `[Context compressed: ${compressedCount} early messages omitted. Kept recent ${this._keepRecentMessages} messages.]`;
269
+ }
270
+
271
+ const summary = { role: 'assistant', content: summaryContent };
272
+ const combined = [...systemMessages, summary, ...recentMessages];
273
+ this._cleanupOrphanedToolResults(combined);
274
+
275
+ messages.length = 0;
276
+ messages.push(...combined);
277
+
278
+ this._compressionCount++;
279
+ const tokenCount = this._tokenCounter.countMessages(messages);
280
+ if (messageStore.compressionState) {
281
+ messageStore.compressionState.count++;
282
+ messageStore.compressionState.lastCompressedAt = Date.now();
283
+ messageStore.compressionState.lastTokenCount = tokenCount;
284
+ }
285
+ if (typeof messageStore.recordCompression === 'function') messageStore.recordCompression(tokenCount);
286
+
287
+ logger.info(`Context compressed (${this._compressionCount} times). Messages: ${messages.length}`);
288
+ }
289
+
290
+ _simpleCompress(sessionId, messages, messageStore) {
291
+ const systemMessages = messages.filter(m => m.role === 'system');
292
+ const otherMessages = messages.filter(m => m.role !== 'system');
293
+ const recentMessages = otherMessages.slice(-this._keepRecentMessages);
294
+ const compressedCount = otherMessages.length - this._keepRecentMessages;
295
+
296
+ const summary = { role: 'assistant', content: `[Context compressed: ${compressedCount} early messages omitted. Kept recent ${this._keepRecentMessages} messages.]` };
297
+ const combined = [...systemMessages, summary, ...recentMessages];
298
+ this._cleanupOrphanedToolResults(combined);
299
+
300
+ messages.length = 0;
301
+ messages.push(...combined);
302
+
303
+ this._compressionCount++;
304
+ if (messageStore.compressionState) {
305
+ messageStore.compressionState.count++;
306
+ messageStore.compressionState.lastCompressedAt = Date.now();
307
+ messageStore.compressionState.lastTokenCount = this._tokenCounter.countMessages(combined);
308
+ }
309
+ logger.info(`Context simple compressed. Messages: ${messages.length}`);
310
+ }
311
+
312
+ _cleanupOrphanedToolResults(messages) {
313
+ const validToolCallIds = new Set();
314
+ for (const msg of messages) {
315
+ if (msg.role !== 'assistant') continue;
316
+ if (Array.isArray(msg.content)) {
317
+ for (const item of msg.content) {
318
+ if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) validToolCallIds.add(item.toolCallId);
319
+ }
320
+ }
321
+ if (Array.isArray(msg.tool_calls)) {
322
+ for (const tc of msg.tool_calls) { if (tc.id) validToolCallIds.add(tc.id); }
323
+ }
324
+ }
325
+
326
+ let removedItems = 0;
327
+ let removedMsgs = 0;
328
+ for (const msg of messages) {
329
+ if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
330
+ const originalLength = msg.content.length;
331
+ msg.content = msg.content.filter((item) => {
332
+ if (item && (item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId && !validToolCallIds.has(item.toolCallId)) {
333
+ removedItems++; return false;
334
+ }
335
+ return true;
336
+ });
337
+ if (msg.content.length === 0 && originalLength > 0) msg._orphaned = true;
338
+ }
339
+
340
+ for (let i = messages.length - 1; i >= 0; i--) {
341
+ if (messages[i]._orphaned) { messages.splice(i, 1); removedMsgs++; }
342
+ }
343
+
344
+ if (removedItems > 0 || removedMsgs > 0) {
345
+ logger.debug(`[ContextCompressor] cleanup: removed ${removedItems} orphaned tool-result items, ${removedMsgs} orphaned tool messages`);
346
+ }
347
+ }
348
+
349
+ async _summarizeMessages(messages) {
350
+ if (!this.framework) throw new Error('Framework not available');
351
+
352
+ const msg_str = messages.map(m => {
353
+ let text = '';
354
+ if (typeof m.content === 'string') text = m.content;
355
+ else if (Array.isArray(m.content)) text = m.content.filter(c => c.type === 'text').map(c => c.text).join(' ');
356
+ if (m.role === 'tool') return '';
357
+ return `${m.role}: ${text}`;
358
+ }).filter(line => line.length > 0).join('\n');
359
+
360
+ const task = `Summarize the following conversation, keeping only meaningful information. Describe in 1000 characters or less:\n\n${msg_str}`;
361
+
362
+ try {
363
+ const subagent = this.framework.createSubAgent({
364
+ name: 'context-compressor',
365
+ role: 'Information extraction expert',
366
+ systemPrompt: 'You are a conversation summarization assistant. Extract and preserve only meaningful information. Keep output concise.',
367
+ maxRetries: 0,
368
+ disableTools: true,
369
+ });
370
+
371
+ const result = await subagent.chat(task);
372
+ if (result.success) return result.message;
373
+ throw new Error(result.error || 'Summary generation failed');
374
+ } catch (err) {
375
+ logger.warn('Summarize failed:', err.message);
376
+ throw err;
377
+ }
378
+ }
379
+
380
+ getStats() {
381
+ return { compressionCount: this._compressionCount, inProgress: this._compressionInProgress };
382
+ }
383
+ }
384
+
385
+ module.exports = {
386
+ ContextCompressor,
387
+ CompactionDetails,
388
+ CompactionResult,
389
+ CompactionError,
390
+ BranchSummaryError,
391
+ Result,
392
+ DEFAULT_COMPACTION_SETTINGS,
393
+ createFileOps,
394
+ extractFileOpsFromMessage,
395
+ computeFileLists,
396
+ formatFileOperations,
397
+ serializeConversation,
398
+ findCutPoint,
399
+ findTurnStartIndex,
400
+ SUMMARIZATION_SYSTEM_PROMPT,
401
+ SUMMARIZATION_PROMPT,
402
+ UPDATE_SUMMARIZATION_PROMPT,
403
+ TURN_PREFIX_SUMMARIZATION_PROMPT,
404
+ BRANCH_SUMMARY_PREAMBLE,
405
+ BRANCH_SUMMARY_PROMPT,
406
+ extractFileOperations,
407
+ prepareCompaction,
408
+ prepareBranchEntries,
409
+ collectEntriesForBranchSummary,
410
+ getMessageFromEntry,
411
+ getMessageFromEntryForCompaction,
412
+ EntryTypes,
413
+ };
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ ContextStorage: require('./storage'),
5
+ RequestContext: require('./request').RequestContext,
6
+ SessionContext: require('./session').SessionContext,
7
+ AgentContext: require('./agent').AgentContext,
8
+ ContextCompressor: require('./compressor').ContextCompressor,
9
+ };
@@ -7,7 +7,7 @@
7
7
  * 3. 处理上下文清理和资源释放
8
8
  */
9
9
 
10
- const { RequestContext } = require('./request-context');
10
+ const { RequestContext } = require('../context/request');
11
11
 
12
12
  class ContextManager {
13
13
  /**
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const { uuid } = require('../common/id');
4
+
5
+ class RequestContext {
6
+ constructor(options = {}) {
7
+ this.requestId = options.requestId || `req_${uuid()}`;
8
+ this.traceId = options.traceId || `trace_${uuid()}`;
9
+ this.parentSpanId = options.parentSpanId || null;
10
+ this.timeout = options.timeout || 120000;
11
+ this.startTime = options.startTime || Date.now();
12
+ this.isStream = options.isStream || false;
13
+ this.userId = options.userId || null;
14
+ this.cancelled = false;
15
+ this.abortController = null;
16
+ this.metadata = options.metadata || {};
17
+ }
18
+
19
+ getAbortController() {
20
+ if (!this.abortController) {
21
+ this.abortController = new AbortController();
22
+ }
23
+ return this.abortController;
24
+ }
25
+
26
+ isTimeout() { return Date.now() - this.startTime > this.timeout; }
27
+
28
+ cancel() {
29
+ this.cancelled = true;
30
+ if (this.abortController) this.abortController.abort();
31
+ }
32
+
33
+ getElapsed() { return Date.now() - this.startTime; }
34
+
35
+ toJSON() {
36
+ return {
37
+ requestId: this.requestId,
38
+ traceId: this.traceId,
39
+ parentSpanId: this.parentSpanId,
40
+ timeout: this.timeout,
41
+ startTime: this.startTime,
42
+ elapsed: this.getElapsed(),
43
+ isStream: this.isStream,
44
+ userId: this.userId,
45
+ cancelled: this.cancelled,
46
+ };
47
+ }
48
+ }
49
+
50
+ module.exports = { RequestContext };