aws-runtime-bridge 1.9.32 → 1.9.40
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/dist/adapter/AcodeSdkAdapter.d.ts +35 -1
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/AcodeSdkAdapter.js +417 -40
- package/dist/adapter/ClaudeSdkAdapter.d.ts +6 -0
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +23 -1
- package/dist/adapter/idle-poll-loop.d.ts +2 -1
- package/dist/adapter/idle-poll-loop.d.ts.map +1 -1
- package/dist/adapter/types.d.ts +65 -3
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/adapter/types.js +5 -4
- package/dist/index.js +4 -0
- package/dist/routes/acode-todos.d.ts +11 -0
- package/dist/routes/acode-todos.d.ts.map +1 -0
- package/dist/routes/acode-todos.js +75 -0
- package/dist/routes/memory.d.ts +1 -1
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/memory.js +3 -3
- package/dist/routes/pty.d.ts.map +1 -1
- package/dist/routes/pty.js +9 -0
- package/dist/routes/sub-agents.d.ts +14 -0
- package/dist/routes/sub-agents.d.ts.map +1 -0
- package/dist/routes/sub-agents.js +253 -0
- package/dist/routes/terminal.d.ts +9 -1
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +207 -22
- package/dist/services/agent-event-store.d.ts +1 -0
- package/dist/services/agent-event-store.d.ts.map +1 -1
- package/dist/services/event-delta-pusher.js +4 -4
- package/dist/services/session-output.d.ts +3 -0
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +38 -2
- package/dist/services/sub-agent-manager-instance.d.ts +4 -0
- package/dist/services/sub-agent-manager-instance.d.ts.map +1 -0
- package/dist/services/sub-agent-manager-instance.js +12 -0
- package/dist/services/workspace-files.d.ts +1 -0
- package/dist/services/workspace-files.d.ts.map +1 -1
- package/dist/services/workspace-files.js +57 -1
- package/package/acode/dist/ask-user-question-tool.d.ts +47 -0
- package/package/acode/dist/ask-user-question-tool.d.ts.map +1 -0
- package/package/acode/dist/ask-user-question-tool.js +233 -0
- package/package/acode/dist/built-in-file-tools.d.ts +2 -0
- package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
- package/package/acode/dist/compaction.d.ts +206 -0
- package/package/acode/dist/compaction.d.ts.map +1 -0
- package/package/acode/dist/compaction.js +664 -0
- package/package/acode/dist/config.d.ts +6 -1
- package/package/acode/dist/config.d.ts.map +1 -1
- package/package/acode/dist/config.js +27 -2
- package/package/acode/dist/index.d.ts +4 -0
- package/package/acode/dist/index.d.ts.map +1 -1
- package/package/acode/dist/index.js +4 -0
- package/package/acode/dist/runtime.d.ts +137 -0
- package/package/acode/dist/runtime.d.ts.map +1 -1
- package/package/acode/dist/runtime.js +974 -172
- package/package/acode/dist/skill-tool.d.ts +30 -0
- package/package/acode/dist/skill-tool.d.ts.map +1 -0
- package/package/acode/dist/skill-tool.js +174 -0
- package/package/acode/dist/sub-agent-manager.d.ts +169 -0
- package/package/acode/dist/sub-agent-manager.d.ts.map +1 -0
- package/package/acode/dist/sub-agent-manager.js +535 -0
- package/package/acode/dist/sub-agent-store.d.ts +33 -0
- package/package/acode/dist/sub-agent-store.d.ts.map +1 -0
- package/package/acode/dist/sub-agent-store.js +109 -0
- package/package/acode/dist/sub-agent-tools.d.ts +72 -0
- package/package/acode/dist/sub-agent-tools.d.ts.map +1 -0
- package/package/acode/dist/sub-agent-tools.js +429 -0
- package/package/acode/dist/sub-agent-types.d.ts +81 -0
- package/package/acode/dist/sub-agent-types.d.ts.map +1 -0
- package/package/acode/dist/sub-agent-types.js +8 -0
- package/package/acode/dist/todo-store.d.ts +42 -0
- package/package/acode/dist/todo-store.d.ts.map +1 -0
- package/package/acode/dist/todo-store.js +68 -0
- package/package/acode/dist/todo-tool.d.ts +6 -0
- package/package/acode/dist/todo-tool.d.ts.map +1 -0
- package/package/acode/dist/todo-tool.js +138 -0
- package/package/acode/dist/types.d.ts +22 -1
- package/package/acode/dist/types.d.ts.map +1 -1
- package/package/acode/package.json +13 -2
- package/package/aws-client-agent-mcp/dist/context-manager.test.js +0 -2
- package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +83 -45
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +451 -325
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +259 -241
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +1 -19
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.js +2 -69
- package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts +17 -19
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.js +62 -99
- package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.test.js +207 -125
- package/package/aws-client-agent-mcp/dist/memory-store.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +3 -4
- package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.js +28 -33
- package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +27 -9
- package/package/aws-client-agent-mcp/dist/message-buffer.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.js +100 -23
- package/package/aws-client-agent-mcp/dist/message-buffer.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +1 -3
- package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/server-memory-store.js +7 -11
- package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.js +8 -5
- package/package/aws-client-agent-mcp/dist/status-reporter.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.test.js +93 -0
- package/package/aws-client-agent-mcp/dist/status-reporter.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +3 -25
- package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
- package/package.json +17 -7
- package/dist/adapter/AcodeSdkAdapter.test.d.ts +0 -2
- package/dist/adapter/AcodeSdkAdapter.test.d.ts.map +0 -1
- package/dist/adapter/AcodeSdkAdapter.test.js +0 -2000
- package/dist/adapter/AdapterRegistry.test.d.ts +0 -5
- package/dist/adapter/AdapterRegistry.test.d.ts.map +0 -1
- package/dist/adapter/AdapterRegistry.test.js +0 -118
- package/dist/adapter/ClaudeSdkAdapter.test.d.ts +0 -2
- package/dist/adapter/ClaudeSdkAdapter.test.d.ts.map +0 -1
- package/dist/adapter/ClaudeSdkAdapter.test.js +0 -311
- package/dist/adapter/CodexSdkAdapter.test.d.ts +0 -2
- package/dist/adapter/CodexSdkAdapter.test.d.ts.map +0 -1
- package/dist/adapter/CodexSdkAdapter.test.js +0 -270
- package/dist/adapter/OpencodeSdkAdapter.test.d.ts +0 -2
- package/dist/adapter/OpencodeSdkAdapter.test.d.ts.map +0 -1
- package/dist/adapter/OpencodeSdkAdapter.test.js +0 -158
- package/dist/adapter/acode-tool-names.test.d.ts +0 -2
- package/dist/adapter/acode-tool-names.test.d.ts.map +0 -1
- package/dist/adapter/acode-tool-names.test.js +0 -28
- package/dist/adapter/adapter.test.d.ts +0 -5
- package/dist/adapter/adapter.test.d.ts.map +0 -1
- package/dist/adapter/adapter.test.js +0 -180
- package/dist/adapter/idle-poll-loop.test.d.ts +0 -2
- package/dist/adapter/idle-poll-loop.test.d.ts.map +0 -1
- package/dist/adapter/idle-poll-loop.test.js +0 -85
- package/dist/adapter/types.test.d.ts +0 -2
- package/dist/adapter/types.test.d.ts.map +0 -1
- package/dist/adapter/types.test.js +0 -50
- package/dist/adapters/cc-switch/mcp-acode.test.d.ts +0 -2
- package/dist/adapters/cc-switch/mcp-acode.test.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-acode.test.js +0 -80
- package/dist/adapters/cc-switch/mcp-claudecode.test.d.ts +0 -2
- package/dist/adapters/cc-switch/mcp-claudecode.test.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-claudecode.test.js +0 -75
- package/dist/adapters/cc-switch/mcp-codex.test.d.ts +0 -2
- package/dist/adapters/cc-switch/mcp-codex.test.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-codex.test.js +0 -126
- package/dist/adapters/cc-switch/mcp-opencode.test.d.ts +0 -2
- package/dist/adapters/cc-switch/mcp-opencode.test.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-opencode.test.js +0 -62
- package/dist/adapters/cc-switch/skill-directory.test.d.ts +0 -2
- package/dist/adapters/cc-switch/skill-directory.test.d.ts.map +0 -1
- package/dist/adapters/cc-switch/skill-directory.test.js +0 -74
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -107
- package/dist/routes/ai-sources.test.d.ts +0 -2
- package/dist/routes/ai-sources.test.d.ts.map +0 -1
- package/dist/routes/ai-sources.test.js +0 -169
- package/dist/routes/file-browser.test.d.ts +0 -5
- package/dist/routes/file-browser.test.d.ts.map +0 -1
- package/dist/routes/file-browser.test.js +0 -155
- package/dist/routes/git.test.d.ts +0 -2
- package/dist/routes/git.test.d.ts.map +0 -1
- package/dist/routes/git.test.js +0 -330
- package/dist/routes/instance.test.d.ts +0 -5
- package/dist/routes/instance.test.d.ts.map +0 -1
- package/dist/routes/instance.test.js +0 -389
- package/dist/routes/mcp.test.d.ts +0 -5
- package/dist/routes/mcp.test.d.ts.map +0 -1
- package/dist/routes/mcp.test.js +0 -92
- package/dist/routes/memory-route-wiring.test.d.ts +0 -2
- package/dist/routes/memory-route-wiring.test.d.ts.map +0 -1
- package/dist/routes/memory-route-wiring.test.js +0 -15
- package/dist/routes/memory.test.d.ts +0 -2
- package/dist/routes/memory.test.d.ts.map +0 -1
- package/dist/routes/memory.test.js +0 -177
- package/dist/routes/properties.test.d.ts +0 -5
- package/dist/routes/properties.test.d.ts.map +0 -1
- package/dist/routes/properties.test.js +0 -72
- package/dist/routes/pty.test.d.ts +0 -2
- package/dist/routes/pty.test.d.ts.map +0 -1
- package/dist/routes/pty.test.js +0 -82
- package/dist/routes/runtime-binding.test.d.ts +0 -2
- package/dist/routes/runtime-binding.test.d.ts.map +0 -1
- package/dist/routes/runtime-binding.test.js +0 -32
- package/dist/routes/sessions.test.d.ts +0 -5
- package/dist/routes/sessions.test.d.ts.map +0 -1
- package/dist/routes/sessions.test.js +0 -86
- package/dist/routes/skills.test.d.ts +0 -5
- package/dist/routes/skills.test.d.ts.map +0 -1
- package/dist/routes/skills.test.js +0 -50
- package/dist/routes/system-metrics.test.d.ts +0 -2
- package/dist/routes/system-metrics.test.d.ts.map +0 -1
- package/dist/routes/system-metrics.test.js +0 -16
- package/dist/routes/terminal.stop-options.test.d.ts +0 -2
- package/dist/routes/terminal.stop-options.test.d.ts.map +0 -1
- package/dist/routes/terminal.stop-options.test.js +0 -39
- package/dist/routes/terminal.test.d.ts +0 -2
- package/dist/routes/terminal.test.d.ts.map +0 -1
- package/dist/routes/terminal.test.js +0 -746
- package/dist/routes/yml.test.d.ts +0 -5
- package/dist/routes/yml.test.d.ts.map +0 -1
- package/dist/routes/yml.test.js +0 -58
- package/dist/services/ai-source-apply.test.d.ts +0 -2
- package/dist/services/ai-source-apply.test.d.ts.map +0 -1
- package/dist/services/ai-source-apply.test.js +0 -138
- package/dist/services/auto-register.test.d.ts +0 -2
- package/dist/services/auto-register.test.d.ts.map +0 -1
- package/dist/services/auto-register.test.js +0 -184
- package/dist/services/aws-client-agent-mcp.test.d.ts +0 -2
- package/dist/services/aws-client-agent-mcp.test.d.ts.map +0 -1
- package/dist/services/aws-client-agent-mcp.test.js +0 -242
- package/dist/services/bridge-package-publish-hooks.test.d.ts +0 -2
- package/dist/services/bridge-package-publish-hooks.test.d.ts.map +0 -1
- package/dist/services/bridge-package-publish-hooks.test.js +0 -136
- package/dist/services/cc-switch-discovery.test.d.ts +0 -2
- package/dist/services/cc-switch-discovery.test.d.ts.map +0 -1
- package/dist/services/cc-switch-discovery.test.js +0 -56
- package/dist/services/cli-commands.test.d.ts +0 -2
- package/dist/services/cli-commands.test.d.ts.map +0 -1
- package/dist/services/cli-commands.test.js +0 -230
- package/dist/services/instance-init-service.test.d.ts +0 -2
- package/dist/services/instance-init-service.test.d.ts.map +0 -1
- package/dist/services/instance-init-service.test.js +0 -140
- package/dist/services/instance-state.test.d.ts +0 -2
- package/dist/services/instance-state.test.d.ts.map +0 -1
- package/dist/services/instance-state.test.js +0 -245
- package/dist/services/mcp-launch-binding-queue.test.d.ts +0 -2
- package/dist/services/mcp-launch-binding-queue.test.d.ts.map +0 -1
- package/dist/services/mcp-launch-binding-queue.test.js +0 -181
- package/dist/services/runtime-binding.test.d.ts +0 -2
- package/dist/services/runtime-binding.test.d.ts.map +0 -1
- package/dist/services/runtime-binding.test.js +0 -78
- package/dist/services/runtime-lifecycle-policy.test.d.ts +0 -2
- package/dist/services/runtime-lifecycle-policy.test.d.ts.map +0 -1
- package/dist/services/runtime-lifecycle-policy.test.js +0 -41
- package/dist/services/session-output.test.d.ts +0 -5
- package/dist/services/session-output.test.d.ts.map +0 -1
- package/dist/services/session-output.test.js +0 -177
- package/dist/services/startup-config-wizard.test.d.ts +0 -2
- package/dist/services/startup-config-wizard.test.d.ts.map +0 -1
- package/dist/services/startup-config-wizard.test.js +0 -184
- package/dist/services/terminal-persistence.test.d.ts +0 -5
- package/dist/services/terminal-persistence.test.d.ts.map +0 -1
- package/dist/services/terminal-persistence.test.js +0 -159
- package/dist/services/tool-installer.test.d.ts +0 -2
- package/dist/services/tool-installer.test.d.ts.map +0 -1
- package/dist/services/tool-installer.test.js +0 -225
- package/dist/services/workspace-files.test.d.ts +0 -2
- package/dist/services/workspace-files.test.d.ts.map +0 -1
- package/dist/services/workspace-files.test.js +0 -648
- package/dist/services/workspace-watch.test.d.ts +0 -2
- package/dist/services/workspace-watch.test.d.ts.map +0 -1
- package/dist/services/workspace-watch.test.js +0 -38
- package/dist/utils/file-utils.test.d.ts +0 -2
- package/dist/utils/file-utils.test.d.ts.map +0 -1
- package/dist/utils/file-utils.test.js +0 -244
- package/dist/utils/logger.test.d.ts +0 -2
- package/dist/utils/logger.test.d.ts.map +0 -1
- package/dist/utils/logger.test.js +0 -93
- package/dist/utils/sdk-package-loader.test.d.ts +0 -2
- package/dist/utils/sdk-package-loader.test.d.ts.map +0 -1
- package/dist/utils/sdk-package-loader.test.js +0 -84
- package/dist/utils/validation.test.d.ts +0 -2
- package/dist/utils/validation.test.d.ts.map +0 -1
- package/dist/utils/validation.test.js +0 -88
- package/dist/utils/yaml-utils.test.d.ts +0 -2
- package/dist/utils/yaml-utils.test.d.ts.map +0 -1
- package/dist/utils/yaml-utils.test.js +0 -363
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上下文压缩模块 — 参考 OpenCode compaction.ts 的设计
|
|
3
|
+
*
|
|
4
|
+
* 核心策略:
|
|
5
|
+
* 1. Prune(裁剪):从旧到新遍历工具输出,超过保护线后用占位符替换内容
|
|
6
|
+
* 2. Summarize(摘要):将早期对话结构化摘要,保留目标/指令/发现/已完成/相关文件
|
|
7
|
+
*
|
|
8
|
+
* 触发时机:在每次 AI 请求前检测 token 是否溢出,溢出则执行压缩
|
|
9
|
+
*
|
|
10
|
+
* 与 OpenCode 的对应关系:
|
|
11
|
+
* - isOverflow ↔ SessionCompaction.isOverflow
|
|
12
|
+
* - prune ↔ SessionCompaction.prune
|
|
13
|
+
* - summarize ↔ SessionCompaction.process (LLM 摘要的简化版)
|
|
14
|
+
*/
|
|
15
|
+
// ============ 常量(与 OpenCode compaction.ts 对齐) ============
|
|
16
|
+
/** 压缩缓冲区:保留这么多 token 的空闲空间,避免压缩过程中溢出 */
|
|
17
|
+
export const COMPACTION_BUFFER = 20_000;
|
|
18
|
+
/** 基于实际累积 token 的触发比例:当累积量达到上下文窗口的此比例时触发压缩 */
|
|
19
|
+
export const COMPACTION_TRIGGER_RATIO = 0.55;
|
|
20
|
+
/** Prune 保护线默认值:保留最近 PRUNE_PROTECT token 的工具输出完整。
|
|
21
|
+
* 注意:实际使用时会按 contextLimit 比例调整(见 pruneToolOutputs),
|
|
22
|
+
* 避免在小上下文窗口(如 60k)下保护线过高导致 prune 永不触发。
|
|
23
|
+
*/
|
|
24
|
+
export const PRUNE_PROTECT = 40_000;
|
|
25
|
+
/** Prune 最小收益默认值:累计可释放 token 不足此数时跳过修剪。
|
|
26
|
+
* 注意:实际使用时会按 contextLimit 比例调整。
|
|
27
|
+
*/
|
|
28
|
+
export const PRUNE_MINIMUM = 20_000;
|
|
29
|
+
/** Prune 保护线占 contextLimit 的比例(取较小者:固定值或比例值) */
|
|
30
|
+
const PRUNE_PROTECT_RATIO = 0.5;
|
|
31
|
+
/** Prune 最小收益占 contextLimit 的比例 */
|
|
32
|
+
const PRUNE_MINIMUM_RATIO = 0.1;
|
|
33
|
+
/** Prune 白名单:这些工具的输出永远不会被裁剪 */
|
|
34
|
+
const PRUNE_PROTECTED_TOOLS = new Set(["skill", "todo_write", "todo_read"]);
|
|
35
|
+
/** 默认上下文窗口上限(查找不到模型时使用) */
|
|
36
|
+
const DEFAULT_CONTEXT_LIMIT = 128_000;
|
|
37
|
+
/** 默认最大输出 token */
|
|
38
|
+
const DEFAULT_MAX_OUTPUT = 8_192;
|
|
39
|
+
/** 压缩后保留的最近消息条数。
|
|
40
|
+
* 注意:此值针对 compactable 消息(user/assistant/tool,不含 system)。
|
|
41
|
+
* 设为 8 而非 12,避免在常见场景(4 条 system + 12 条非 system = 16 条总消息)下
|
|
42
|
+
* removableCount = max(0, 12 - 12) = 0 导致 summarize 完全不生效。
|
|
43
|
+
*/
|
|
44
|
+
const COMPACTION_RECENT_MESSAGE_COUNT = 8;
|
|
45
|
+
/** 摘要最大字符数 */
|
|
46
|
+
const COMPACTION_MAX_SUMMARY_CHARS = 6000;
|
|
47
|
+
/** 单条消息预览最大字符数 */
|
|
48
|
+
const COMPACTION_MAX_PREVIEW_CHARS = 500;
|
|
49
|
+
/** 摘要前缀标识 */
|
|
50
|
+
const COMPACTION_SUMMARY_PREFIX = "ACode context compaction summary:";
|
|
51
|
+
/**
|
|
52
|
+
* LLM 摘要 prompt 模板(OpenCode 风格结构化摘要)。
|
|
53
|
+
*
|
|
54
|
+
* 用于 summarizeConversationWithLLM:追加到 user 消息末尾,指导 LLM 按结构化
|
|
55
|
+
* 段落(Goal/Instructions/Discoveries/Accomplished/Files)输出摘要,便于后续
|
|
56
|
+
* 轮次快速恢复上下文。LLM 摘要失败时降级到 rule-based summarizeConversation。
|
|
57
|
+
*/
|
|
58
|
+
export const COMPACTION_LLM_PROMPT_TEMPLATE = [
|
|
59
|
+
"Please summarize the conversation above using the following structured format.",
|
|
60
|
+
"Keep each section concise but preserve all actionable details.",
|
|
61
|
+
"",
|
|
62
|
+
"## Goal",
|
|
63
|
+
"The user's primary objective in this conversation.",
|
|
64
|
+
"",
|
|
65
|
+
"## Instructions",
|
|
66
|
+
"Standing instructions, constraints, or preferences the user has stated.",
|
|
67
|
+
"",
|
|
68
|
+
"## Discoveries",
|
|
69
|
+
"Key findings, decisions, tool results, file paths, and errors encountered.",
|
|
70
|
+
"",
|
|
71
|
+
"## Accomplished",
|
|
72
|
+
"Tasks that have been completed (do not redo these).",
|
|
73
|
+
"",
|
|
74
|
+
"## Files",
|
|
75
|
+
"Files read, modified, or created with brief context.",
|
|
76
|
+
"",
|
|
77
|
+
"Output only the summary, no preamble.",
|
|
78
|
+
].join("\n");
|
|
79
|
+
/** token 估算系数:约 3 字符/token(混合中英文) */
|
|
80
|
+
const ESTIMATED_CHARS_PER_TOKEN = 3;
|
|
81
|
+
/**
|
|
82
|
+
* 计算压缩触发阈值:取 min(contextLimit × 55%, hardLimit - reserved)。
|
|
83
|
+
*
|
|
84
|
+
* - ratioBased = contextLimit × COMPACTION_TRIGGER_RATIO(基于窗口比例的软阈值)
|
|
85
|
+
* - hardLimit = inputLimit ?? contextLimit(provider 显式 inputLimit 优先)
|
|
86
|
+
* - reserved = min(COMPACTION_BUFFER, maxOutput)(buffer 不超过输出上限)
|
|
87
|
+
*
|
|
88
|
+
* 取两者较小者,既避免小窗口下过早触发,又避免接近硬上限时来不及压缩。
|
|
89
|
+
*/
|
|
90
|
+
export function calculateCompactionThreshold(contextLimit, maxOutput, inputLimit) {
|
|
91
|
+
const ratioBased = Math.floor(contextLimit * COMPACTION_TRIGGER_RATIO);
|
|
92
|
+
const hardLimit = inputLimit ?? contextLimit;
|
|
93
|
+
const reserved = Math.min(COMPACTION_BUFFER, maxOutput);
|
|
94
|
+
const hardLimitBased = hardLimit - reserved;
|
|
95
|
+
return Math.min(ratioBased, hardLimitBased);
|
|
96
|
+
}
|
|
97
|
+
// ============ 模型上下文窗口查找 ============
|
|
98
|
+
/**
|
|
99
|
+
* 知名模型的上下文窗口大小查找表。
|
|
100
|
+
* 数据来源:各模型官方文档。
|
|
101
|
+
*/
|
|
102
|
+
const MODEL_CONTEXT_LIMITS = {
|
|
103
|
+
// Anthropic Claude
|
|
104
|
+
"claude-4": { contextLimit: 200_000, maxOutput: 8_192 },
|
|
105
|
+
"claude-sonnet-4": { contextLimit: 200_000, maxOutput: 8_192 },
|
|
106
|
+
"claude-3-5-sonnet": { contextLimit: 200_000, maxOutput: 8_192 },
|
|
107
|
+
"claude-3-opus": { contextLimit: 200_000, maxOutput: 8_192 },
|
|
108
|
+
"claude-3-haiku": { contextLimit: 200_000, maxOutput: 8_192 },
|
|
109
|
+
"claude-2": { contextLimit: 100_000, maxOutput: 4_096 },
|
|
110
|
+
// OpenAI GPT
|
|
111
|
+
"gpt-4o": { contextLimit: 128_000, maxOutput: 16_384 },
|
|
112
|
+
"gpt-4-turbo": { contextLimit: 128_000, maxOutput: 4_096 },
|
|
113
|
+
"gpt-4": { contextLimit: 8_192, maxOutput: 4_096 },
|
|
114
|
+
"gpt-3.5-turbo": { contextLimit: 16_384, maxOutput: 4_096 },
|
|
115
|
+
"o1": { contextLimit: 200_000, maxOutput: 100_000 },
|
|
116
|
+
"o3-mini": { contextLimit: 200_000, maxOutput: 100_000 },
|
|
117
|
+
// Google Gemini
|
|
118
|
+
"gemini-2.0-flash": { contextLimit: 1_048_576, maxOutput: 8_192 },
|
|
119
|
+
"gemini-2.5-pro": { contextLimit: 1_048_576, maxOutput: 8_192 },
|
|
120
|
+
"gemini-1.5-pro": { contextLimit: 2_097_152, maxOutput: 8_192 },
|
|
121
|
+
"gemini-1.5-flash": { contextLimit: 1_048_576, maxOutput: 8_192 },
|
|
122
|
+
// DeepSeek
|
|
123
|
+
"deepseek": { contextLimit: 64_000, maxOutput: 8_192 },
|
|
124
|
+
"deepseek-chat": { contextLimit: 64_000, maxOutput: 8_192 },
|
|
125
|
+
"deepseek-reasoner": { contextLimit: 64_000, maxOutput: 8_192 },
|
|
126
|
+
// Qwen
|
|
127
|
+
"qwen-max": { contextLimit: 32_000, maxOutput: 8_192 },
|
|
128
|
+
"qwen-plus": { contextLimit: 128_000, maxOutput: 8_192 },
|
|
129
|
+
"qwen-turbo": { contextLimit: 1_000_000, maxOutput: 8_192 },
|
|
130
|
+
// GLM
|
|
131
|
+
"glm-4": { contextLimit: 128_000, maxOutput: 4_096 },
|
|
132
|
+
// Yi
|
|
133
|
+
"yi-": { contextLimit: 200_000, maxOutput: 4_096 },
|
|
134
|
+
// Meta Llama
|
|
135
|
+
"llama-3": { contextLimit: 8_192, maxOutput: 4_096 },
|
|
136
|
+
"llama-2": { contextLimit: 4_096, maxOutput: 4_096 },
|
|
137
|
+
// Mistral
|
|
138
|
+
"mistral": { contextLimit: 32_000, maxOutput: 4_096 },
|
|
139
|
+
"mixtral": { contextLimit: 32_000, maxOutput: 4_096 },
|
|
140
|
+
"codestral": { contextLimit: 256_000, maxOutput: 8_192 },
|
|
141
|
+
// Grok
|
|
142
|
+
"grok": { contextLimit: 131_072, maxOutput: 4_096 },
|
|
143
|
+
// Amazon Nova
|
|
144
|
+
"nova-": { contextLimit: 200_000, maxOutput: 8_192 },
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* 根据模型名查找上下文窗口大小。
|
|
148
|
+
* 主流程:优先使用传入的 configOverrides → 精确匹配 → 前缀匹配(按长度降序)→ 默认值。
|
|
149
|
+
*
|
|
150
|
+
* @param modelName 模型名
|
|
151
|
+
* @param configOverrides 可选的配置覆盖值(来自 acode.json 的 contextLimit/maxOutput)
|
|
152
|
+
*/
|
|
153
|
+
export function lookupModelLimits(modelName, configOverrides) {
|
|
154
|
+
// 配置覆盖优先(来自 acode.json 等模板配置)
|
|
155
|
+
if (configOverrides?.contextLimit && configOverrides.contextLimit > 0) {
|
|
156
|
+
return {
|
|
157
|
+
contextLimit: configOverrides.contextLimit,
|
|
158
|
+
maxOutput: configOverrides.maxOutput ?? DEFAULT_MAX_OUTPUT,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (!modelName)
|
|
162
|
+
return { contextLimit: DEFAULT_CONTEXT_LIMIT, maxOutput: DEFAULT_MAX_OUTPUT };
|
|
163
|
+
const key = modelName.toLowerCase().trim();
|
|
164
|
+
if (MODEL_CONTEXT_LIMITS[key])
|
|
165
|
+
return MODEL_CONTEXT_LIMITS[key];
|
|
166
|
+
const matches = Object.keys(MODEL_CONTEXT_LIMITS)
|
|
167
|
+
.filter((prefix) => key.startsWith(prefix))
|
|
168
|
+
.sort((a, b) => b.length - a.length);
|
|
169
|
+
if (matches.length > 0)
|
|
170
|
+
return MODEL_CONTEXT_LIMITS[matches[0]];
|
|
171
|
+
return { contextLimit: DEFAULT_CONTEXT_LIMIT, maxOutput: DEFAULT_MAX_OUTPUT };
|
|
172
|
+
}
|
|
173
|
+
// ============ Token 估算 ============
|
|
174
|
+
/**
|
|
175
|
+
* 粗略估算文本的 token 数。
|
|
176
|
+
* 不使用真实 tokenizer(太慢),基于字符数乘经验系数。
|
|
177
|
+
* 对纯 ASCII 约 4 字符/token,对中文约 1.5-2 字符/token,混合取 3 字符/token。
|
|
178
|
+
*/
|
|
179
|
+
export function estimateTokens(text) {
|
|
180
|
+
if (!text)
|
|
181
|
+
return 0;
|
|
182
|
+
return Math.ceil(text.length / ESTIMATED_CHARS_PER_TOKEN);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* 估算单条消息的字节大小。
|
|
186
|
+
*/
|
|
187
|
+
function estimateMessageBytes(message) {
|
|
188
|
+
let size = 16;
|
|
189
|
+
if (typeof message.content === "string") {
|
|
190
|
+
size += message.content.length * 2;
|
|
191
|
+
}
|
|
192
|
+
else if (Array.isArray(message.content)) {
|
|
193
|
+
for (const part of message.content) {
|
|
194
|
+
if (typeof part === "object" && part !== null) {
|
|
195
|
+
size += JSON.stringify(part).length;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (message.tool_calls) {
|
|
200
|
+
size += JSON.stringify(message.tool_calls).length;
|
|
201
|
+
}
|
|
202
|
+
if (message.tool_call_id) {
|
|
203
|
+
size += message.tool_call_id.length;
|
|
204
|
+
}
|
|
205
|
+
return size;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* 估算整个对话的 token 数。
|
|
209
|
+
*/
|
|
210
|
+
export function estimateConversationTokens(messages) {
|
|
211
|
+
let totalChars = 0;
|
|
212
|
+
for (const msg of messages) {
|
|
213
|
+
if (typeof msg.content === "string") {
|
|
214
|
+
totalChars += msg.content.length;
|
|
215
|
+
}
|
|
216
|
+
else if (Array.isArray(msg.content)) {
|
|
217
|
+
for (const part of msg.content) {
|
|
218
|
+
if (typeof part === "object" && part !== null) {
|
|
219
|
+
totalChars += JSON.stringify(part).length;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (msg.tool_calls) {
|
|
224
|
+
totalChars += JSON.stringify(msg.tool_calls).length;
|
|
225
|
+
}
|
|
226
|
+
if (msg.tool_call_id) {
|
|
227
|
+
totalChars += msg.tool_call_id.length;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return Math.ceil(totalChars / ESTIMATED_CHARS_PER_TOKEN);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* 计算对话的 KB 大小。
|
|
234
|
+
*/
|
|
235
|
+
export function conversationSizeKB(messages) {
|
|
236
|
+
let totalBytes = 0;
|
|
237
|
+
for (const msg of messages) {
|
|
238
|
+
totalBytes += estimateMessageBytes(msg);
|
|
239
|
+
}
|
|
240
|
+
return Math.round((totalBytes / 1024) * 10) / 10;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* 判断对话是否上下文溢出(基于文本估算)。
|
|
244
|
+
* 使用显式传入的 contextLimit 和比例触发,不依赖模型名查找表。
|
|
245
|
+
*
|
|
246
|
+
* @param messages 对话消息数组
|
|
247
|
+
* @param contextLimit 上下文窗口上限
|
|
248
|
+
* @param maxOutput 模型最大输出 token(未使用,保留参数兼容)
|
|
249
|
+
*/
|
|
250
|
+
export function isOverflow(messages, contextLimit, _maxOutput) {
|
|
251
|
+
if (contextLimit <= 0)
|
|
252
|
+
return false;
|
|
253
|
+
const totalUsed = estimateConversationTokens(messages);
|
|
254
|
+
const ratioThreshold = Math.floor(contextLimit * COMPACTION_TRIGGER_RATIO);
|
|
255
|
+
return totalUsed >= ratioThreshold;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* 基于 provider 实际返回的累积 token 使用量判断是否应触发压缩。
|
|
259
|
+
*
|
|
260
|
+
* provider 每次请求都会返回实际 token 计数(input + output),
|
|
261
|
+
* 这个计数比基于文本的估算(estimateConversationTokens)要准确得多。
|
|
262
|
+
*
|
|
263
|
+
* 触发条件:累积 tokens >= contextLimit * COMPACTION_TRIGGER_RATIO
|
|
264
|
+
* 使用显式传入的 contextLimit,不依赖模型名查找表。
|
|
265
|
+
*
|
|
266
|
+
* @param accumulatedInput provider 实际累积的 input tokens
|
|
267
|
+
* @param accumulatedOutput provider 实际累积的 output tokens
|
|
268
|
+
* @param contextLimit 上下文窗口上限(来自模板配置,未配置时使用默认值)
|
|
269
|
+
*/
|
|
270
|
+
export function isOverflowByActualUsage(accumulatedInput, accumulatedOutput, contextLimit) {
|
|
271
|
+
if (contextLimit <= 0)
|
|
272
|
+
return false;
|
|
273
|
+
const totalUsed = accumulatedInput + accumulatedOutput;
|
|
274
|
+
const ratioThreshold = Math.floor(contextLimit * COMPACTION_TRIGGER_RATIO);
|
|
275
|
+
return totalUsed >= ratioThreshold;
|
|
276
|
+
}
|
|
277
|
+
// ============ Prune 策略 ============
|
|
278
|
+
/**
|
|
279
|
+
* 判断消息是否为工具消息(可裁剪)。
|
|
280
|
+
*/
|
|
281
|
+
function isToolMessage(message) {
|
|
282
|
+
return message.role === "tool";
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* 从工具消息中提取工具名(用于白名单检查)。
|
|
286
|
+
* 通过 tool_call_id 在对话历史中查找对应的 assistant 消息的 tool_calls,
|
|
287
|
+
* 匹配后返回 function name。
|
|
288
|
+
*/
|
|
289
|
+
function getToolNameFromMessage(message, allMessages, messageIndex) {
|
|
290
|
+
if (message.role !== "tool" || !message.tool_call_id) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
for (let i = messageIndex - 1; i >= 0; i--) {
|
|
294
|
+
const m = allMessages[i];
|
|
295
|
+
if (!m)
|
|
296
|
+
continue;
|
|
297
|
+
if (m.role === "assistant" && Array.isArray(m.tool_calls)) {
|
|
298
|
+
for (const tc of m.tool_calls) {
|
|
299
|
+
if (typeof tc === "object" &&
|
|
300
|
+
tc !== null &&
|
|
301
|
+
"id" in tc &&
|
|
302
|
+
tc.id === message.tool_call_id) {
|
|
303
|
+
const fn = tc.function;
|
|
304
|
+
return fn ? String(fn.name ?? "") : undefined;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Prune:从消息列表中裁剪旧工具消息的输出内容。
|
|
313
|
+
*
|
|
314
|
+
* 算法(参考 OpenCode compaction.ts:58-99):
|
|
315
|
+
* 1. 从最新消息往旧遍历
|
|
316
|
+
* 2. 跳过前 2 轮对话(保护最近上下文)
|
|
317
|
+
* 3. 累计工具消息的 token
|
|
318
|
+
* 4. 超过保护线后,将工具输出替换为占位符
|
|
319
|
+
* 5. 只有收益超过最小阈值才真正执行
|
|
320
|
+
*
|
|
321
|
+
* 保护线和最小阈值会按 contextLimit 比例调整:
|
|
322
|
+
* - protectLine = min(PRUNE_PROTECT, contextLimit * PRUNE_PROTECT_RATIO)
|
|
323
|
+
* - minimumGain = min(PRUNE_MINIMUM, contextLimit * PRUNE_MINIMUM_RATIO)
|
|
324
|
+
* 这样在小上下文窗口(如 60k)下也能正常触发 prune,
|
|
325
|
+
* 而不是固定 40k/20k 导致 60k 上下文永远无法 prune。
|
|
326
|
+
*
|
|
327
|
+
* @param messages 对话消息数组(会被原地修改)
|
|
328
|
+
* @param contextLimit 上下文窗口大小,用于按比例调整阈值(默认使用 DEFAULT_CONTEXT_LIMIT)
|
|
329
|
+
* @returns 裁剪结果
|
|
330
|
+
*/
|
|
331
|
+
export function pruneToolOutputs(messages, contextLimit = DEFAULT_CONTEXT_LIMIT) {
|
|
332
|
+
if (messages.length === 0) {
|
|
333
|
+
return { prunedCount: 0, freedTokens: 0 };
|
|
334
|
+
}
|
|
335
|
+
// 按比例调整保护线和最小收益阈值
|
|
336
|
+
const protectLine = Math.min(PRUNE_PROTECT, Math.floor(contextLimit * PRUNE_PROTECT_RATIO));
|
|
337
|
+
const minimumGain = Math.min(PRUNE_MINIMUM, Math.floor(contextLimit * PRUNE_MINIMUM_RATIO));
|
|
338
|
+
let total = 0;
|
|
339
|
+
let freedTokens = 0;
|
|
340
|
+
const toPrune = [];
|
|
341
|
+
let turns = 0;
|
|
342
|
+
// 从最新往旧遍历
|
|
343
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
344
|
+
const msg = messages[i];
|
|
345
|
+
if (msg.role === "user")
|
|
346
|
+
turns++;
|
|
347
|
+
if (turns < 2)
|
|
348
|
+
continue;
|
|
349
|
+
if (!isToolMessage(msg))
|
|
350
|
+
continue;
|
|
351
|
+
const toolName = getToolNameFromMessage(msg, messages, i);
|
|
352
|
+
if (toolName && PRUNE_PROTECTED_TOOLS.has(toolName))
|
|
353
|
+
continue;
|
|
354
|
+
const contentStr = typeof msg.content === "string" ? msg.content : "";
|
|
355
|
+
const estimate = estimateTokens(contentStr);
|
|
356
|
+
total += estimate;
|
|
357
|
+
if (total > protectLine) {
|
|
358
|
+
freedTokens += estimate;
|
|
359
|
+
toPrune.push(msg);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (freedTokens < minimumGain) {
|
|
363
|
+
return { prunedCount: 0, freedTokens: 0 };
|
|
364
|
+
}
|
|
365
|
+
// 执行裁剪:将工具输出替换为占位符
|
|
366
|
+
for (const msg of toPrune) {
|
|
367
|
+
msg.content = "[Old tool result content cleared]";
|
|
368
|
+
}
|
|
369
|
+
return { prunedCount: toPrune.length, freedTokens };
|
|
370
|
+
}
|
|
371
|
+
// ============ Summarize 策略 ============
|
|
372
|
+
/**
|
|
373
|
+
* 判断消息是否为压缩摘要消息。
|
|
374
|
+
*/
|
|
375
|
+
function isCompactionSummaryMessage(message) {
|
|
376
|
+
return (message.role === "system" &&
|
|
377
|
+
typeof message.content === "string" &&
|
|
378
|
+
message.content.startsWith(COMPACTION_SUMMARY_PREFIX));
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* 生成单条消息的摘要预览。
|
|
382
|
+
*/
|
|
383
|
+
function summarizeMessage(message, index) {
|
|
384
|
+
const contentStr = typeof message.content === "string"
|
|
385
|
+
? message.content
|
|
386
|
+
: Array.isArray(message.content)
|
|
387
|
+
? message.content.map((p) => (typeof p === "object" && p !== null ? JSON.stringify(p) : String(p))).join(" ")
|
|
388
|
+
: "";
|
|
389
|
+
const preview = contentStr.length > COMPACTION_MAX_PREVIEW_CHARS
|
|
390
|
+
? `${contentStr.slice(0, COMPACTION_MAX_PREVIEW_CHARS)}…`
|
|
391
|
+
: contentStr;
|
|
392
|
+
const parts = [`${index + 1}. ${message.role}`];
|
|
393
|
+
if (message.tool_calls && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
|
|
394
|
+
const toolCallSummary = message.tool_calls
|
|
395
|
+
.map((tc) => {
|
|
396
|
+
if (typeof tc === "object" && tc !== null) {
|
|
397
|
+
const fn = tc.function;
|
|
398
|
+
return fn ? String(fn.name ?? "") : "";
|
|
399
|
+
}
|
|
400
|
+
return "";
|
|
401
|
+
})
|
|
402
|
+
.filter(Boolean)
|
|
403
|
+
.join(", ");
|
|
404
|
+
if (toolCallSummary)
|
|
405
|
+
parts.push(`tool_calls=${toolCallSummary}`);
|
|
406
|
+
}
|
|
407
|
+
if (message.tool_call_id)
|
|
408
|
+
parts.push(`tool_call_id=${message.tool_call_id}`);
|
|
409
|
+
if (preview)
|
|
410
|
+
parts.push(`content=${preview}`);
|
|
411
|
+
return parts.join(" | ");
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Summarize:将早期对话消息替换为结构化摘要。
|
|
415
|
+
*
|
|
416
|
+
* 算法(参考 OpenCode compaction.ts:101-229 的 process 函数):
|
|
417
|
+
* 1. 分离 base system 消息和可压缩消息
|
|
418
|
+
* 2. 保留最近 COMPACTION_RECENT_MESSAGE_COUNT 条消息
|
|
419
|
+
* 3. 将早期消息生成结构化摘要
|
|
420
|
+
* 4. 用摘要消息替换早期消息
|
|
421
|
+
*
|
|
422
|
+
* @param messages 对话消息数组(会被原地修改)
|
|
423
|
+
* @returns 摘要化的消息数
|
|
424
|
+
*/
|
|
425
|
+
export async function summarizeConversation(messages, options) {
|
|
426
|
+
const baseSystemMessages = messages.filter((m) => m.role === "system" && !isCompactionSummaryMessage(m));
|
|
427
|
+
const compactableMessages = messages.filter((m) => m.role !== "system" || isCompactionSummaryMessage(m));
|
|
428
|
+
const removableCount = Math.max(0, compactableMessages.length - COMPACTION_RECENT_MESSAGE_COUNT);
|
|
429
|
+
if (removableCount === 0)
|
|
430
|
+
return 0;
|
|
431
|
+
// ★ 切片 + 后清理(取代之前的边界推进):
|
|
432
|
+
// 边界推进在找不到"干净边界"时会一路推进到末尾,返回 0,导致 summarize 完全不生效。
|
|
433
|
+
// 新策略:先按 removableCount 切片,再清理 kept 部分中的 orphan tool 消息和
|
|
434
|
+
// orphan assistant(tool_calls),保证消息序列对 OpenAI 兼容 API 有效。
|
|
435
|
+
const removedMessages = compactableMessages.slice(0, removableCount);
|
|
436
|
+
let keptMessages = compactableMessages.slice(removableCount);
|
|
437
|
+
// 收集 kept 部分所有 assistant(tool_calls) 的 tool_call_id
|
|
438
|
+
const keptToolCallIds = new Set();
|
|
439
|
+
for (const m of keptMessages) {
|
|
440
|
+
if (m.role === "assistant" && m.tool_calls?.length) {
|
|
441
|
+
for (const tc of m.tool_calls) {
|
|
442
|
+
if (typeof tc === "object" &&
|
|
443
|
+
tc !== null &&
|
|
444
|
+
typeof tc.id === "string") {
|
|
445
|
+
keptToolCallIds.add(tc.id);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// 第一轮过滤:移除 orphan tool 消息(其父 assistant 已被摘要化)
|
|
451
|
+
keptMessages = keptMessages.filter((m) => {
|
|
452
|
+
if (m.role === "tool" && m.tool_call_id) {
|
|
453
|
+
return keptToolCallIds.has(m.tool_call_id);
|
|
454
|
+
}
|
|
455
|
+
return true;
|
|
456
|
+
});
|
|
457
|
+
// 第二轮过滤:移除 orphan assistant(tool_calls)(其部分 tool 结果已在第一轮被移除)
|
|
458
|
+
// 反复迭代直到稳定(一次过滤可能产生新的 orphan)
|
|
459
|
+
let changed = true;
|
|
460
|
+
while (changed) {
|
|
461
|
+
changed = false;
|
|
462
|
+
// 重新收集 kept 中的 tool_call_id
|
|
463
|
+
const currentToolCallIds = new Set();
|
|
464
|
+
for (const m of keptMessages) {
|
|
465
|
+
if (m.role === "assistant" && m.tool_calls?.length) {
|
|
466
|
+
for (const tc of m.tool_calls) {
|
|
467
|
+
if (typeof tc === "object" &&
|
|
468
|
+
tc !== null &&
|
|
469
|
+
typeof tc.id === "string") {
|
|
470
|
+
currentToolCallIds.add(tc.id);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// 重新收集 kept 中的 tool_result id
|
|
476
|
+
const currentToolResultIds = new Set();
|
|
477
|
+
for (const m of keptMessages) {
|
|
478
|
+
if (m.role === "tool" && m.tool_call_id) {
|
|
479
|
+
currentToolResultIds.add(m.tool_call_id);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const nextKept = keptMessages.filter((m) => {
|
|
483
|
+
if (m.role === "tool" && m.tool_call_id) {
|
|
484
|
+
return currentToolCallIds.has(m.tool_call_id);
|
|
485
|
+
}
|
|
486
|
+
if (m.role === "assistant" && m.tool_calls?.length) {
|
|
487
|
+
const expectedIds = new Set();
|
|
488
|
+
for (const tc of m.tool_calls) {
|
|
489
|
+
if (typeof tc === "object" &&
|
|
490
|
+
tc !== null &&
|
|
491
|
+
typeof tc.id === "string") {
|
|
492
|
+
expectedIds.add(tc.id);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (expectedIds.size === 0)
|
|
496
|
+
return true;
|
|
497
|
+
let allFound = true;
|
|
498
|
+
for (const id of expectedIds) {
|
|
499
|
+
if (!currentToolResultIds.has(id)) {
|
|
500
|
+
allFound = false;
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return allFound;
|
|
505
|
+
}
|
|
506
|
+
return true;
|
|
507
|
+
});
|
|
508
|
+
if (nextKept.length !== keptMessages.length) {
|
|
509
|
+
changed = true;
|
|
510
|
+
keptMessages = nextKept;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// 如果清理后 kept 为空,放弃本次摘要(避免清空整个对话)
|
|
514
|
+
if (keptMessages.length === 0) {
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
// ★ 尝试 LLM 摘要(若注入了 llmSummarizer),失败则降级到规则摘要。
|
|
518
|
+
// LLM 摘要失败时静默降级,不抛异常,保证压缩流程继续。
|
|
519
|
+
let llmSummary = null;
|
|
520
|
+
if (options?.llmSummarizer) {
|
|
521
|
+
try {
|
|
522
|
+
const conversationText = removedMessages
|
|
523
|
+
.map((message, index) => summarizeMessage(message, index))
|
|
524
|
+
.join("\n");
|
|
525
|
+
llmSummary = await options.llmSummarizer({
|
|
526
|
+
conversationText,
|
|
527
|
+
messageCount: removableCount,
|
|
528
|
+
contextLimit: options.contextLimit ?? DEFAULT_CONTEXT_LIMIT,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// 降级到规则摘要
|
|
533
|
+
llmSummary = null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// 原地修改 messages 数组
|
|
537
|
+
messages.length = 0;
|
|
538
|
+
messages.push(...baseSystemMessages);
|
|
539
|
+
if (llmSummary !== null) {
|
|
540
|
+
// LLM 摘要路径:标记 "summarized by LLM" 以便 performCompaction 识别
|
|
541
|
+
messages.push({
|
|
542
|
+
role: "system",
|
|
543
|
+
content: [
|
|
544
|
+
COMPACTION_SUMMARY_PREFIX,
|
|
545
|
+
`${removableCount} earlier conversation messages were summarized by LLM to free context space.`,
|
|
546
|
+
"Preserve the user's goal, decisions, tool results, file paths, errors, and unresolved tasks from this summary.",
|
|
547
|
+
"Recent messages below remain authoritative.",
|
|
548
|
+
"",
|
|
549
|
+
llmSummary,
|
|
550
|
+
].join("\n"),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
// 规则摘要路径(降级或未注入 llmSummarizer)
|
|
555
|
+
const summaryLines = removedMessages.map((message, index) => summarizeMessage(message, index));
|
|
556
|
+
const summary = summaryLines.join("\n").slice(0, COMPACTION_MAX_SUMMARY_CHARS);
|
|
557
|
+
messages.push({
|
|
558
|
+
role: "system",
|
|
559
|
+
content: [
|
|
560
|
+
COMPACTION_SUMMARY_PREFIX,
|
|
561
|
+
`${removableCount} earlier conversation messages were summarized to free context space.`,
|
|
562
|
+
"Preserve the user's goal, decisions, tool results, file paths, errors, and unresolved tasks from this summary.",
|
|
563
|
+
"Recent messages below remain authoritative.",
|
|
564
|
+
"",
|
|
565
|
+
summary,
|
|
566
|
+
].join("\n"),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
messages.push(...keptMessages);
|
|
570
|
+
return removableCount;
|
|
571
|
+
}
|
|
572
|
+
// ============ 完整压缩流程 ============
|
|
573
|
+
/**
|
|
574
|
+
* 执行完整压缩流程:Prune → 若仍溢出则 Summarize。
|
|
575
|
+
*
|
|
576
|
+
* 参考 OpenCode 的两阶段策略:
|
|
577
|
+
* 1. 先尝试 prune(低成本,只清除旧工具输出)
|
|
578
|
+
* 2. 若 prune 后仍溢出,执行 summarize(高成本,摘要化早期对话)
|
|
579
|
+
*
|
|
580
|
+
* @param messages 对话消息数组(会被原地修改)
|
|
581
|
+
* @param contextLimit 上下文窗口上限(来自模板配置,未配置时使用默认值)
|
|
582
|
+
* @param maxOutput 模型最大输出 token
|
|
583
|
+
* @param options 可选:llmSummarizer(LLM 摘要器,失败降级到规则摘要)、onPhase(阶段事件回调)
|
|
584
|
+
* @returns 压缩结果
|
|
585
|
+
*/
|
|
586
|
+
export async function performCompaction(messages, contextLimit, maxOutput = DEFAULT_MAX_OUTPUT, options) {
|
|
587
|
+
const sizeBeforeKB = conversationSizeKB(messages);
|
|
588
|
+
const messageCountBefore = messages.length;
|
|
589
|
+
const tokensBefore = estimateConversationTokens(messages);
|
|
590
|
+
let strategy = "none";
|
|
591
|
+
let prunedToolMessages = 0;
|
|
592
|
+
let summarizedMessages = 0;
|
|
593
|
+
let freedTokens = 0;
|
|
594
|
+
let usedLLM = false;
|
|
595
|
+
// 阶段 1: Prune(传入 contextLimit 以按比例调整保护线和最小收益阈值)
|
|
596
|
+
if (options?.onPhase) {
|
|
597
|
+
options.onPhase({ phase: "prune_start", messageCount: messages.length });
|
|
598
|
+
}
|
|
599
|
+
const pruneResult = pruneToolOutputs(messages, contextLimit);
|
|
600
|
+
if (pruneResult.prunedCount > 0) {
|
|
601
|
+
strategy = "prune";
|
|
602
|
+
prunedToolMessages = pruneResult.prunedCount;
|
|
603
|
+
freedTokens += pruneResult.freedTokens;
|
|
604
|
+
}
|
|
605
|
+
if (options?.onPhase) {
|
|
606
|
+
options.onPhase({
|
|
607
|
+
phase: "prune_end",
|
|
608
|
+
prunedCount: pruneResult.prunedCount,
|
|
609
|
+
freedTokens: pruneResult.freedTokens,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
// 阶段 2: 若仍溢出,执行 Summarize
|
|
613
|
+
if (isOverflow(messages, contextLimit, maxOutput)) {
|
|
614
|
+
if (options?.onPhase) {
|
|
615
|
+
options.onPhase({ phase: "summarize_start", messageCount: messages.length });
|
|
616
|
+
}
|
|
617
|
+
const summarized = await summarizeConversation(messages, {
|
|
618
|
+
llmSummarizer: options?.llmSummarizer,
|
|
619
|
+
contextLimit,
|
|
620
|
+
});
|
|
621
|
+
if (summarized > 0) {
|
|
622
|
+
strategy = strategy === "prune" ? "prune+summarize" : "summarize";
|
|
623
|
+
summarizedMessages = summarized;
|
|
624
|
+
const tokensAfter = estimateConversationTokens(messages);
|
|
625
|
+
freedTokens = Math.max(freedTokens, tokensBefore - tokensAfter);
|
|
626
|
+
// 检测是否使用了 LLM 摘要(通过摘要消息内容识别)
|
|
627
|
+
const summaryMsg = messages.find((m) => m.role === "system" &&
|
|
628
|
+
typeof m.content === "string" &&
|
|
629
|
+
m.content.includes("summarized by LLM"));
|
|
630
|
+
usedLLM = Boolean(summaryMsg);
|
|
631
|
+
}
|
|
632
|
+
if (options?.onPhase) {
|
|
633
|
+
options.onPhase({
|
|
634
|
+
phase: "summarize_end",
|
|
635
|
+
summarizedCount: summarizedMessages,
|
|
636
|
+
usedLLM,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const sizeAfterKB = conversationSizeKB(messages);
|
|
641
|
+
const messageCountAfter = messages.length;
|
|
642
|
+
if (strategy === "none") {
|
|
643
|
+
return {
|
|
644
|
+
strategy: "none",
|
|
645
|
+
freedTokens: 0,
|
|
646
|
+
sizeBeforeKB,
|
|
647
|
+
sizeAfterKB: sizeBeforeKB,
|
|
648
|
+
messageCountBefore,
|
|
649
|
+
messageCountAfter,
|
|
650
|
+
prunedToolMessages: 0,
|
|
651
|
+
summarizedMessages: 0,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
strategy,
|
|
656
|
+
freedTokens,
|
|
657
|
+
sizeBeforeKB,
|
|
658
|
+
sizeAfterKB,
|
|
659
|
+
messageCountBefore,
|
|
660
|
+
messageCountAfter,
|
|
661
|
+
prunedToolMessages,
|
|
662
|
+
summarizedMessages,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type { ACodeConfigFile, ACodeSkillMetadata } from './types.js';
|
|
1
|
+
import type { ACodeConfigFile, ACodeSkillGroup, ACodeSkillMetadata } from './types.js';
|
|
2
2
|
export declare function loadAcodeConfig(configPath: string | undefined): Promise<ACodeConfigFile>;
|
|
3
3
|
export declare function loadAcodeSkills(skillsDir: string | undefined): Promise<ACodeSkillMetadata[]>;
|
|
4
|
+
/**
|
|
5
|
+
* 加载单个 skills 目录:返回目录路径、是否存在、以及该目录下的 skills 列表。
|
|
6
|
+
* 目录不存在时返回 exists=false + 空 skills,供前端展示空节点。
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadAcodeSkillGroup(skillsDir: string): Promise<ACodeSkillGroup>;
|
|
4
9
|
//# sourceMappingURL=config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAWvF,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,eAAe,CAAC,CAU9F;AA4DD,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAKlG;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAgBrF"}
|