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
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
6
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
4
7
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
8
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
9
|
import { createBuiltInFileTools, } from "./built-in-file-tools.js";
|
|
10
|
+
import { ASK_USER_QUESTION_TOOL_NAME, createAskUserQuestionTool, } from "./ask-user-question-tool.js";
|
|
11
|
+
import { createSkillTool } from "./skill-tool.js";
|
|
12
|
+
import { createTodoWriteTool, createTodoReadTool } from "./todo-tool.js";
|
|
13
|
+
import { createSubAgentTools } from "./sub-agent-tools.js";
|
|
7
14
|
import { cleanupAcodeCmdDist } from "./builtins/commandArtifacts.js";
|
|
8
15
|
import { loadAcodeCommands } from "./commands/commandLoader.js";
|
|
9
16
|
import { ACodeCommandRegistry } from "./commands/commandRegistry.js";
|
|
10
|
-
import {
|
|
17
|
+
import { calculateCompactionThreshold, COMPACTION_LLM_PROMPT_TEMPLATE, conversationSizeKB, estimateConversationTokens, performCompaction, pruneToolOutputs, summarizeConversation, } from "./compaction.js";
|
|
18
|
+
import { loadAcodeConfig, loadAcodeSkillGroup } from "./config.js";
|
|
11
19
|
import { ACodeHookBus, findDeniedHookResult } from "./hooks/hookBus.js";
|
|
12
20
|
import { isMcpPollingToolName } from "./mcpToolNames.js";
|
|
13
21
|
function cloneChatContentPart(part) {
|
|
@@ -67,10 +75,26 @@ const DEFAULT_BASH_BUILT_IN_TOOL_TIMEOUT_MS = 10 * 60_000 + 2000;
|
|
|
67
75
|
const DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
68
76
|
const MAX_INLINE_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
69
77
|
const MAX_REQUEST_BODY_BYTES = 800 * 1024;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
/**
|
|
79
|
+
* 上下文压缩调试日志开关:仅当设置环境变量 ACODE_COMPACTION_DEBUG=1 时启用。
|
|
80
|
+
* 默认关闭,避免生产环境同步写盘阻塞事件循环。
|
|
81
|
+
*/
|
|
82
|
+
const COMPACTION_DEBUG_ENABLED = process.env.ACODE_COMPACTION_DEBUG === "1";
|
|
83
|
+
const COMPACTION_DEBUG_LOG = COMPACTION_DEBUG_ENABLED
|
|
84
|
+
? path.join(os.tmpdir(), "acode-compaction-debug.log")
|
|
85
|
+
: "";
|
|
86
|
+
/**
|
|
87
|
+
* Runtime 调试日志:受 ACODE_COMPACTION_DEBUG=1 开关控制。
|
|
88
|
+
* 默认关闭以避免生产环境同步写盘阻塞事件循环、泄漏用户消息内容。
|
|
89
|
+
* 文件中所有原 `console.log` 调用均改为走此 helper。
|
|
90
|
+
*/
|
|
91
|
+
function runtimeDebugLog(...args) {
|
|
92
|
+
if (!COMPACTION_DEBUG_ENABLED)
|
|
93
|
+
return;
|
|
94
|
+
console.log(...args);
|
|
95
|
+
}
|
|
96
|
+
/** 当 provider 未配置 contextLimit 时使用的默认值 */
|
|
97
|
+
const FALLBACK_CONTEXT_LIMIT = 64_000;
|
|
74
98
|
async function withTimeout(operation, timeoutMs, label, onTimeout) {
|
|
75
99
|
if (timeoutMs <= 0) {
|
|
76
100
|
return await operation;
|
|
@@ -393,38 +417,6 @@ function contentPreview(content) {
|
|
|
393
417
|
.join(" ")
|
|
394
418
|
.trim();
|
|
395
419
|
}
|
|
396
|
-
function truncateText(value, maxChars) {
|
|
397
|
-
if (value.length <= maxChars) {
|
|
398
|
-
return value;
|
|
399
|
-
}
|
|
400
|
-
return `${value.slice(0, maxChars)}…`;
|
|
401
|
-
}
|
|
402
|
-
function summarizeToolCalls(toolCalls) {
|
|
403
|
-
if (!toolCalls || toolCalls.length === 0) {
|
|
404
|
-
return "";
|
|
405
|
-
}
|
|
406
|
-
return toolCalls
|
|
407
|
-
.map((toolCall) => {
|
|
408
|
-
const args = truncateText(toolCall.function.arguments.replace(/\s+/g, " ").trim(), 160);
|
|
409
|
-
return `${toolCall.function.name}(${args})`;
|
|
410
|
-
})
|
|
411
|
-
.join(", ");
|
|
412
|
-
}
|
|
413
|
-
function summarizeConversationMessage(message, index) {
|
|
414
|
-
const preview = truncateText(contentPreview(message.content).replace(/\s+/g, " ").trim(), CONTEXT_COMPACTION_MAX_PREVIEW_CHARS);
|
|
415
|
-
const toolCalls = summarizeToolCalls(message.tool_calls);
|
|
416
|
-
const parts = [`${index + 1}. ${message.role}`];
|
|
417
|
-
if (toolCalls) {
|
|
418
|
-
parts.push(`tool_calls=${toolCalls}`);
|
|
419
|
-
}
|
|
420
|
-
if (message.tool_call_id) {
|
|
421
|
-
parts.push(`tool_call_id=${message.tool_call_id}`);
|
|
422
|
-
}
|
|
423
|
-
if (preview) {
|
|
424
|
-
parts.push(`content=${preview}`);
|
|
425
|
-
}
|
|
426
|
-
return parts.join(" | ");
|
|
427
|
-
}
|
|
428
420
|
/**
|
|
429
421
|
* 解析 ACode 工具权限配置。
|
|
430
422
|
* 主流程:运行时配置优先 -> `.acode/config.json` 次之 -> 兼容旧式 allowedTools/deniedTools 字段。
|
|
@@ -530,6 +522,10 @@ function coerceArguments(rawArguments) {
|
|
|
530
522
|
}
|
|
531
523
|
}
|
|
532
524
|
function getBuiltInToolTimeoutMs(tool, args, fallbackTimeoutMs) {
|
|
525
|
+
// ask_user_question 工具阻塞等待用户回答,不受超时限制
|
|
526
|
+
if (tool.toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
527
|
+
return 0;
|
|
528
|
+
}
|
|
533
529
|
if (tool.toolName !== "Bash") {
|
|
534
530
|
return fallbackTimeoutMs;
|
|
535
531
|
}
|
|
@@ -594,12 +590,16 @@ function estimateMessageSizeBytes(message) {
|
|
|
594
590
|
}
|
|
595
591
|
return size;
|
|
596
592
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
593
|
+
/**
|
|
594
|
+
* 按字节大小截断对话,防止 HTTP 请求体过大。
|
|
595
|
+
* 这是与 token 压缩不同的关注点:即使 token 在上下文窗口内,
|
|
596
|
+
* HTTP 请求体也可能因 base64 图片等内容超过网关限制。
|
|
597
|
+
* 使用 compaction 模块的 performCompaction 进行压缩。
|
|
598
|
+
*
|
|
599
|
+
* ★ 支持 LLM 摘要器(与 performCompaction 一致):
|
|
600
|
+
* 若提供 llmSummarizer,则 summarize 阶段使用 LLM 生成语义化摘要。
|
|
601
|
+
*/
|
|
602
|
+
async function truncateConversationIfNeeded(conversation, maxBytes, contextLimit, maxOutput, llmSummarizer) {
|
|
603
603
|
let totalBytes = 0;
|
|
604
604
|
for (const message of conversation) {
|
|
605
605
|
totalBytes += estimateMessageSizeBytes(message);
|
|
@@ -607,52 +607,14 @@ function truncateConversationIfNeeded(conversation, maxBytes) {
|
|
|
607
607
|
if (totalBytes <= maxBytes) {
|
|
608
608
|
return 0;
|
|
609
609
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
function compactConversationForRequestLimit(conversation, maxBytes) {
|
|
613
|
-
const baseSystemMessages = conversation.filter((m) => m.role === "system" && !isContextCompactionSummaryMessage(m));
|
|
614
|
-
const compactableMessages = conversation.filter((m) => m.role !== "system" || isContextCompactionSummaryMessage(m));
|
|
615
|
-
const keptMessages = [];
|
|
616
|
-
let keptBytes = baseSystemMessages.reduce((sum, m) => sum + estimateMessageSizeBytes(m), 0);
|
|
617
|
-
for (let i = compactableMessages.length - 1; i >= 0; i--) {
|
|
618
|
-
const messageSize = estimateMessageSizeBytes(compactableMessages[i]);
|
|
619
|
-
if (keptBytes + messageSize > maxBytes && keptMessages.length > 0) {
|
|
620
|
-
break;
|
|
621
|
-
}
|
|
622
|
-
keptBytes += messageSize;
|
|
623
|
-
keptMessages.unshift(compactableMessages[i]);
|
|
624
|
-
}
|
|
625
|
-
return compactConversationWithSummary(conversation, compactableMessages.length - keptMessages.length);
|
|
626
|
-
}
|
|
627
|
-
function compactConversationForContextOverflow(conversation) {
|
|
628
|
-
const compactableMessages = conversation.filter((m) => m.role !== "system" || isContextCompactionSummaryMessage(m));
|
|
629
|
-
const removableCount = Math.max(0, compactableMessages.length - CONTEXT_COMPACTION_RECENT_MESSAGE_COUNT);
|
|
630
|
-
return compactConversationWithSummary(conversation, removableCount);
|
|
631
|
-
}
|
|
632
|
-
function compactConversationWithSummary(conversation, requestedRemovedCount) {
|
|
633
|
-
const baseSystemMessages = conversation.filter((m) => m.role === "system" && !isContextCompactionSummaryMessage(m));
|
|
634
|
-
const compactableMessages = conversation.filter((m) => m.role !== "system" || isContextCompactionSummaryMessage(m));
|
|
635
|
-
const removedCount = Math.max(0, Math.min(requestedRemovedCount, compactableMessages.length));
|
|
636
|
-
if (removedCount === 0) {
|
|
637
|
-
return 0;
|
|
638
|
-
}
|
|
639
|
-
const removedMessages = compactableMessages.slice(0, removedCount);
|
|
640
|
-
const keptMessages = compactableMessages.slice(removedCount);
|
|
641
|
-
const summaryLines = removedMessages.map((message, index) => summarizeConversationMessage(message, index));
|
|
642
|
-
const summary = truncateText(summaryLines.join("\n"), CONTEXT_COMPACTION_MAX_SUMMARY_CHARS);
|
|
643
|
-
conversation.length = 0;
|
|
644
|
-
conversation.push(...baseSystemMessages);
|
|
645
|
-
conversation.push({
|
|
646
|
-
role: "system",
|
|
647
|
-
content: [
|
|
648
|
-
CONTEXT_COMPACTION_SUMMARY_PREFIX,
|
|
649
|
-
`${removedCount} earlier compactable conversation messages were summarized to keep the provider request within context limits.`,
|
|
650
|
-
"Preserve the user's goal, decisions, tool results, file paths, errors, and unresolved tasks from this summary. Recent messages below remain authoritative.",
|
|
651
|
-
summary,
|
|
652
|
-
].join("\n"),
|
|
610
|
+
const result = await performCompaction(conversation, contextLimit, maxOutput, {
|
|
611
|
+
llmSummarizer,
|
|
653
612
|
});
|
|
654
|
-
|
|
655
|
-
|
|
613
|
+
if (result.strategy !== "none") {
|
|
614
|
+
return result.prunedToolMessages + result.summarizedMessages;
|
|
615
|
+
}
|
|
616
|
+
// performCompaction 未生效时,强制 summarize
|
|
617
|
+
return summarizeConversation(conversation, { llmSummarizer, contextLimit });
|
|
656
618
|
}
|
|
657
619
|
function isContextOverflowProviderRequestError(error) {
|
|
658
620
|
const message = getErrorMessage(error).toLowerCase();
|
|
@@ -742,10 +704,10 @@ function buildSkillPrompt(skills) {
|
|
|
742
704
|
return "";
|
|
743
705
|
}
|
|
744
706
|
return [
|
|
745
|
-
"
|
|
746
|
-
|
|
747
|
-
"
|
|
748
|
-
].join("
|
|
707
|
+
"Skills are available for this session.",
|
|
708
|
+
"Use the `skill` tool to load specialized skill instructions when a user task matches one of the available skills listed in the tool description.",
|
|
709
|
+
"Do not attempt to follow skill instructions from memory — always invoke the `skill` tool first to load the full content.",
|
|
710
|
+
].join(" ");
|
|
749
711
|
}
|
|
750
712
|
function buildCommandPrompt(commands) {
|
|
751
713
|
if (commands.length === 0) {
|
|
@@ -757,6 +719,49 @@ function buildCommandPrompt(commands) {
|
|
|
757
719
|
"When the user invokes a slash command, ACode expands it before model execution.",
|
|
758
720
|
].join("\n");
|
|
759
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* 格式化 profile 信息为文本,供 rolePrompt 拼接使用。
|
|
724
|
+
* 主流程:提取关键字段并格式化为可读文本,返回空字符串当 profile 为空。
|
|
725
|
+
*/
|
|
726
|
+
function buildProfilePrompt(profile) {
|
|
727
|
+
if (!profile || typeof profile !== "object") {
|
|
728
|
+
return "";
|
|
729
|
+
}
|
|
730
|
+
const p = profile;
|
|
731
|
+
const parts = [];
|
|
732
|
+
const displayName = p.displayName;
|
|
733
|
+
const roleName = p.roleName;
|
|
734
|
+
const instanceName = p.instanceName;
|
|
735
|
+
const projectName = p.projectName;
|
|
736
|
+
const workspacePath = p.workspacePath;
|
|
737
|
+
const runtimeMode = p.runtimeMode;
|
|
738
|
+
const describe = p.describe;
|
|
739
|
+
if (typeof displayName === "string" && displayName.trim()) {
|
|
740
|
+
parts.push(`Display Name: ${displayName.trim()}`);
|
|
741
|
+
}
|
|
742
|
+
if (typeof roleName === "string" && roleName.trim()) {
|
|
743
|
+
parts.push(`Role: ${roleName.trim()}`);
|
|
744
|
+
}
|
|
745
|
+
if (typeof instanceName === "string" && instanceName.trim()) {
|
|
746
|
+
parts.push(`Instance: ${instanceName.trim()}`);
|
|
747
|
+
}
|
|
748
|
+
if (typeof projectName === "string" && projectName.trim()) {
|
|
749
|
+
parts.push(`Project: ${projectName.trim()}`);
|
|
750
|
+
}
|
|
751
|
+
if (typeof workspacePath === "string" && workspacePath.trim()) {
|
|
752
|
+
parts.push(`Workspace: ${workspacePath.trim()}`);
|
|
753
|
+
}
|
|
754
|
+
if (typeof runtimeMode === "string" && runtimeMode.trim()) {
|
|
755
|
+
parts.push(`Runtime Mode: ${runtimeMode.trim()}`);
|
|
756
|
+
}
|
|
757
|
+
if (typeof describe === "string" && describe.trim()) {
|
|
758
|
+
parts.push(`Description: ${describe.trim()}`);
|
|
759
|
+
}
|
|
760
|
+
if (parts.length === 0) {
|
|
761
|
+
return "";
|
|
762
|
+
}
|
|
763
|
+
return ["[User Profile]", ...parts].join("\n");
|
|
764
|
+
}
|
|
760
765
|
function toOpenAiTools(tools) {
|
|
761
766
|
return tools.map((tool) => ({
|
|
762
767
|
type: "function",
|
|
@@ -854,6 +859,8 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
854
859
|
commands: [],
|
|
855
860
|
commandRegistry: new ACodeCommandRegistry([]),
|
|
856
861
|
conversation: [],
|
|
862
|
+
accumulatedInputTokens: 0,
|
|
863
|
+
accumulatedOutputTokens: 0,
|
|
857
864
|
};
|
|
858
865
|
this.abortController = null;
|
|
859
866
|
this.submitQueue = Promise.resolve();
|
|
@@ -862,6 +869,20 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
862
869
|
this.sessionCompleteEmitted = false;
|
|
863
870
|
this.contextVersion = 0;
|
|
864
871
|
this.stopWaiters = new Set();
|
|
872
|
+
/** 上下文压缩调试定时器(每 5 秒输出一次) */
|
|
873
|
+
this.compactionDebugTimer = null;
|
|
874
|
+
/** 可动态刷新的 SkillTool 引用(含 refresh 方法),运行中变更时调用其 refresh */
|
|
875
|
+
this.skillTool = null;
|
|
876
|
+
/** 监听 skills 目录变更的 FSWatcher 列表 */
|
|
877
|
+
this.skillWatchers = [];
|
|
878
|
+
/** skill 目录变更后的防抖定时器,避免短时间多次触发重扫 */
|
|
879
|
+
this.skillRefreshTimer = null;
|
|
880
|
+
/**
|
|
881
|
+
* ask_user_question 工具的挂起等待队列。
|
|
882
|
+
* key: toolCallId;value: resolve/reject 回调 + 创建时的 contextVersion。
|
|
883
|
+
* submitQuestionAnswer() 调用时 resolve;abort/stop/supersede 时 reject。
|
|
884
|
+
*/
|
|
885
|
+
this.questionWaiters = new Map();
|
|
865
886
|
this.hookBus = new ACodeHookBus({
|
|
866
887
|
sessionId: config.sessionId,
|
|
867
888
|
onError: (event, error) => {
|
|
@@ -883,6 +904,44 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
883
904
|
};
|
|
884
905
|
this.emit("event", event);
|
|
885
906
|
}
|
|
907
|
+
/**
|
|
908
|
+
* 发射压缩子事件(phase 事件)到面板时间线。
|
|
909
|
+
* 子事件通过 parentId 关联到父压缩事件,在前端以嵌套子元素形式展示。
|
|
910
|
+
*
|
|
911
|
+
* @param compactionId 父压缩事件 ID
|
|
912
|
+
* @param phase 阶段标识(detect/prune/prune_done/summarize/summarize_done)
|
|
913
|
+
* @param phaseLabel 阶段显示标签(如"阶段1: 裁剪工具输出")
|
|
914
|
+
* @param phaseDetail 阶段详情(如"裁剪了 5 条工具输出")
|
|
915
|
+
*/
|
|
916
|
+
emitCompactionPhase(compactionId, phase, phaseLabel, phaseDetail) {
|
|
917
|
+
this.emitAcodeEvent("context_compaction", {
|
|
918
|
+
action: "phase",
|
|
919
|
+
compactionId,
|
|
920
|
+
parentId: compactionId,
|
|
921
|
+
phase,
|
|
922
|
+
phaseLabel,
|
|
923
|
+
phaseDetail,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* performCompaction 的 onPhase 回调处理:将 CompactionPhaseEvent 转换为面板子事件。
|
|
928
|
+
*/
|
|
929
|
+
handleCompactionPhase(compactionId, event) {
|
|
930
|
+
switch (event.phase) {
|
|
931
|
+
case "prune_start":
|
|
932
|
+
this.emitCompactionPhase(compactionId, "prune", "阶段1: 裁剪工具输出", `正在裁剪 ${event.messageCount ?? 0} 条消息中的工具输出...`);
|
|
933
|
+
break;
|
|
934
|
+
case "prune_end":
|
|
935
|
+
this.emitCompactionPhase(compactionId, "prune_done", "阶段1: 裁剪工具输出", `裁剪了 ${event.prunedCount ?? 0} 条工具输出, 释放 ${event.freedTokens ?? 0} tokens`);
|
|
936
|
+
break;
|
|
937
|
+
case "summarize_start":
|
|
938
|
+
this.emitCompactionPhase(compactionId, "summarize", "阶段2: 摘要化对话", `正在摘要 ${event.messageCount ?? 0} 条早期对话...`);
|
|
939
|
+
break;
|
|
940
|
+
case "summarize_end":
|
|
941
|
+
this.emitCompactionPhase(compactionId, "summarize_done", "阶段2: 摘要化对话", `摘要化了 ${event.summarizedCount ?? 0} 条消息${event.usedLLM ? " (LLM 摘要)" : " (规则摘要)"}`);
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
886
945
|
/**
|
|
887
946
|
* 启动 ACode headless 会话。
|
|
888
947
|
* 主流程:加载 `.acode/config.json` 与 skills 目录 -> 发出 MCP/Skill 状态 ->
|
|
@@ -890,20 +949,42 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
890
949
|
*/
|
|
891
950
|
async start() {
|
|
892
951
|
const acodeConfig = await loadAcodeConfig(this.config.configPath);
|
|
893
|
-
|
|
952
|
+
// Skills 来源(按优先级,后加载的覆盖先加载的同名 skill):
|
|
953
|
+
// 1. 全局目录(~/.acode/skills,由 config.skillsDir 指定)
|
|
954
|
+
// 2. 项目目录(<workingDirectory>/.acode/skills)
|
|
955
|
+
// 3. 项目目录(<workingDirectory>/.agentswork/skills)
|
|
956
|
+
// 各目录的 skills 合并后供 SkillTool / prompt 使用;分组信息供前端按目录展示。
|
|
957
|
+
// 即使目录不存在也会作为空节点返回,前端按目录展示。
|
|
958
|
+
const { skills, skillGroups, watchedDirs } = await this.loadAllSkills();
|
|
894
959
|
const provider = resolveProviderConfig(this.config, acodeConfig);
|
|
895
960
|
// [DEBUG] ACode Runtime 最终解析的 provider 配置(写入 stdout,bridge 会捕获)
|
|
896
|
-
|
|
961
|
+
runtimeDebugLog(`[ACodeRuntime] ★★★ DEBUG ★★★ resolveProviderConfig 结果: config.model=${this.config.model ?? "(undefined)"}, ` +
|
|
897
962
|
`resolvedModel=${provider.model}, baseURL=${provider.baseURL ?? "(none)"}, apiKey=${provider.apiKey ? "[set]" : "(none)"}, ` +
|
|
898
963
|
`configPath=${this.config.configPath ?? "(none)"}`);
|
|
899
964
|
const permissions = resolvePermissionConfig(this.config, acodeConfig);
|
|
900
965
|
const mcpConnections = await this.connectMcpServers(acodeConfig.mcpServers ?? {}, provider.mcpToolTimeoutMs);
|
|
966
|
+
// 获取用户 profile 信息并拼接在 rolePrompt 下方
|
|
967
|
+
const profilePrompt = await this.fetchProfilePrompt(mcpConnections);
|
|
901
968
|
const agentId = this.config.agentId || "unknown";
|
|
902
969
|
// 重启时清理旧 spillover 输出
|
|
903
970
|
void cleanupAcodeCmdDist(agentId);
|
|
904
971
|
const builtInTools = createBuiltInFileTools(this.config.workingDirectory, agentId);
|
|
972
|
+
const askUserQuestionTool = createAskUserQuestionTool(this.buildAskUserQuestionDeps(), () => this.contextVersion);
|
|
973
|
+
const skillTool = createSkillTool(skills);
|
|
974
|
+
this.skillTool = skillTool;
|
|
975
|
+
const todoWriteTool = createTodoWriteTool(agentId);
|
|
976
|
+
const todoReadTool = createTodoReadTool(agentId);
|
|
977
|
+
// 注册 sub-agent 工具集(仅当注入了 subAgentManager)
|
|
978
|
+
const subAgentTools = this.config.subAgentManager
|
|
979
|
+
? createSubAgentTools(this.buildSubAgentToolDeps(this.config.subAgentManager))
|
|
980
|
+
: [];
|
|
905
981
|
const exposedTools = [
|
|
906
982
|
...builtInTools,
|
|
983
|
+
askUserQuestionTool,
|
|
984
|
+
skillTool,
|
|
985
|
+
todoWriteTool,
|
|
986
|
+
todoReadTool,
|
|
987
|
+
...subAgentTools,
|
|
907
988
|
...mcpConnections.flatMap((connection) => connection.tools),
|
|
908
989
|
];
|
|
909
990
|
const commandDirs = this.resolveCommandDirs(acodeConfig);
|
|
@@ -922,9 +1003,14 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
922
1003
|
.join("\n\n");
|
|
923
1004
|
// 辅助函数:将非空文本构造为 system role 消息,消除重复的 spread+ternary 模式
|
|
924
1005
|
const makeSystem = (content) => content?.trim() ? [{ role: 'system', content: content.trim() }] : [];
|
|
1006
|
+
// 将 rolePrompt 与 profilePrompt 合并为第二条 system 消息
|
|
1007
|
+
const roleWithProfile = [this.config.rolePrompt, profilePrompt]
|
|
1008
|
+
.map((item) => item?.trim())
|
|
1009
|
+
.filter((item) => Boolean(item))
|
|
1010
|
+
.join("\n\n");
|
|
925
1011
|
const initialConversation = [
|
|
926
1012
|
...makeSystem(this.config.globalPrompt),
|
|
927
|
-
...makeSystem(
|
|
1013
|
+
...makeSystem(roleWithProfile),
|
|
928
1014
|
...makeSystem(this.config.memory),
|
|
929
1015
|
...(runtimePrompt ? [{ role: 'system', content: runtimePrompt }] : []),
|
|
930
1016
|
];
|
|
@@ -945,6 +1031,8 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
945
1031
|
commands,
|
|
946
1032
|
commandRegistry,
|
|
947
1033
|
conversation: cloneChatConversation(initialConversation),
|
|
1034
|
+
accumulatedInputTokens: 0,
|
|
1035
|
+
accumulatedOutputTokens: 0,
|
|
948
1036
|
};
|
|
949
1037
|
await this.hookBus.dispatch("SessionStart", {
|
|
950
1038
|
workingDirectory: this.config.workingDirectory,
|
|
@@ -972,6 +1060,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
972
1060
|
source: tool.kind === "mcp"
|
|
973
1061
|
? `${tool.serverName}.${tool.toolName}`
|
|
974
1062
|
: `builtin.${tool.toolName}`,
|
|
1063
|
+
description: tool.description || "",
|
|
975
1064
|
})),
|
|
976
1065
|
});
|
|
977
1066
|
this.emitAcodeEvent("skill_status", {
|
|
@@ -980,6 +1069,16 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
980
1069
|
skills: skills.map((skill) => ({
|
|
981
1070
|
name: skill.name,
|
|
982
1071
|
description: skill.description,
|
|
1072
|
+
directory: skill.directory,
|
|
1073
|
+
})),
|
|
1074
|
+
skillGroups: skillGroups.map((group) => ({
|
|
1075
|
+
dir: group.dir,
|
|
1076
|
+
exists: group.exists,
|
|
1077
|
+
skills: group.skills.map((skill) => ({
|
|
1078
|
+
name: skill.name,
|
|
1079
|
+
description: skill.description,
|
|
1080
|
+
directory: skill.directory,
|
|
1081
|
+
})),
|
|
983
1082
|
})),
|
|
984
1083
|
});
|
|
985
1084
|
this.emitAcodeEvent("command_status", {
|
|
@@ -994,6 +1093,234 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
994
1093
|
if (this.config.initialPrompt) {
|
|
995
1094
|
await this.submit(this.config.initialPrompt);
|
|
996
1095
|
}
|
|
1096
|
+
// 启动上下文压缩调试日志(仅当 ACODE_COMPACTION_DEBUG=1 时启用)
|
|
1097
|
+
this.startCompactionDebugLog();
|
|
1098
|
+
// 启动 skills 目录监听,运行中变更会触发重扫并刷新 SkillTool + 重发 skill_status
|
|
1099
|
+
this.startSkillWatchers(watchedDirs);
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* 加载所有 skills 来源目录并合并。
|
|
1103
|
+
*
|
|
1104
|
+
* 三个来源(按优先级,后加载的覆盖先加载的同名 skill):
|
|
1105
|
+
* 1. 全局目录(~/.acode/skills,由 config.skillsDir 指定)
|
|
1106
|
+
* 2. 项目目录(<workingDirectory>/.acode/skills)
|
|
1107
|
+
* 3. 项目目录(<workingDirectory>/.agentswork/skills)
|
|
1108
|
+
*
|
|
1109
|
+
* 返回合并后的 skills 列表、按目录分组的 skillGroups,以及需要监听的目录列表
|
|
1110
|
+
* (已去重、规范化)。watchedDirs 用于启动 fs.watch 监听运行时变更。
|
|
1111
|
+
*/
|
|
1112
|
+
async loadAllSkills() {
|
|
1113
|
+
const globalSkillsDir = this.config.skillsDir ?? "";
|
|
1114
|
+
const projectAcodeSkillsDir = path.join(this.config.workingDirectory, ".acode", "skills");
|
|
1115
|
+
const projectAgentsworkSkillsDir = path.join(this.config.workingDirectory, ".agentswork", "skills");
|
|
1116
|
+
// 按规范化路径去重,避免全局目录与项目目录路径相同时重复加载
|
|
1117
|
+
const seenDirs = new Set();
|
|
1118
|
+
const skillGroups = [];
|
|
1119
|
+
const watchedDirs = [];
|
|
1120
|
+
for (const dir of [globalSkillsDir, projectAcodeSkillsDir, projectAgentsworkSkillsDir]) {
|
|
1121
|
+
if (!dir)
|
|
1122
|
+
continue;
|
|
1123
|
+
const normalized = path.resolve(dir);
|
|
1124
|
+
if (seenDirs.has(normalized))
|
|
1125
|
+
continue;
|
|
1126
|
+
seenDirs.add(normalized);
|
|
1127
|
+
skillGroups.push(await loadAcodeSkillGroup(dir));
|
|
1128
|
+
// 即使目录不存在也加入监听列表:fs.watch 会失败但被静默忽略,
|
|
1129
|
+
// 这样用户后续创建该目录时(如果父目录存在)能通过其他途径感知。
|
|
1130
|
+
// 实际上 fs.watch 不存在的路径会抛错,所以这里只加入存在的目录。
|
|
1131
|
+
try {
|
|
1132
|
+
await fs.promises.access(normalized);
|
|
1133
|
+
watchedDirs.push(normalized);
|
|
1134
|
+
}
|
|
1135
|
+
catch {
|
|
1136
|
+
// 目录不存在,跳过监听
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
// 合并各目录 skills,按 name 去重(项目优先覆盖全局)
|
|
1140
|
+
const skillsByName = new Map();
|
|
1141
|
+
for (const group of skillGroups) {
|
|
1142
|
+
for (const skill of group.skills) {
|
|
1143
|
+
skillsByName.set(skill.name, skill);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
const skills = [...skillsByName.values()];
|
|
1147
|
+
return { skills, skillGroups, watchedDirs };
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* 启动 fs.watch 监听 skills 目录变更。
|
|
1151
|
+
*
|
|
1152
|
+
* 使用 recursive: true(Windows/macOS 原生支持,Linux 在 Node 19+ 支持)。
|
|
1153
|
+
* Linux 旧版本不支持时会抛 ERR_FEATURE_UNAVAILABLE,fallback 到非 recursive
|
|
1154
|
+
* 监听顶层目录(只能感知 skill 子目录的新增/删除,无法感知 SKILL.md 内容修改)。
|
|
1155
|
+
*
|
|
1156
|
+
* 变更事件经防抖后触发 refreshSkills(),重新扫描目录、刷新 SkillTool、重发 skill_status。
|
|
1157
|
+
*/
|
|
1158
|
+
startSkillWatchers(dirs) {
|
|
1159
|
+
for (const dir of dirs) {
|
|
1160
|
+
let watcher = null;
|
|
1161
|
+
try {
|
|
1162
|
+
watcher = fs.watch(dir, { recursive: true }, () => {
|
|
1163
|
+
this.scheduleSkillRefresh();
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
catch {
|
|
1167
|
+
// recursive 在当前平台不支持(如 Linux 旧版本),fallback 到非 recursive
|
|
1168
|
+
try {
|
|
1169
|
+
watcher = fs.watch(dir, () => {
|
|
1170
|
+
this.scheduleSkillRefresh();
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
catch {
|
|
1174
|
+
// 监听失败(如权限问题),跳过该目录
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (watcher) {
|
|
1179
|
+
watcher.on("error", () => {
|
|
1180
|
+
// 静默处理 watcher 错误(如目录被删除),避免未捕获异常
|
|
1181
|
+
});
|
|
1182
|
+
this.skillWatchers.push(watcher);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* 防抖调度 skill 重扫。短时间内多次变更事件只触发一次重扫。
|
|
1188
|
+
*/
|
|
1189
|
+
scheduleSkillRefresh() {
|
|
1190
|
+
if (this.skillRefreshTimer) {
|
|
1191
|
+
clearTimeout(this.skillRefreshTimer);
|
|
1192
|
+
}
|
|
1193
|
+
this.skillRefreshTimer = setTimeout(() => {
|
|
1194
|
+
this.skillRefreshTimer = null;
|
|
1195
|
+
void this.refreshSkills();
|
|
1196
|
+
}, ACodeRuntime.SKILL_REFRESH_DEBOUNCE_MS);
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* 重新扫描 skills 目录并刷新 SkillTool 与 state。
|
|
1200
|
+
*
|
|
1201
|
+
* 调用时机:fs.watch 监听到目录变更后(经防抖)。
|
|
1202
|
+
* 副作用:
|
|
1203
|
+
* - 更新 this.skillTool 的内部 Map 与工具描述(让下一轮 turn 的 LLM 感知新 skill)
|
|
1204
|
+
* - 更新 this.state.skills(影响 fallback 响应等)
|
|
1205
|
+
* - 重发 skill_status 事件(让 DevTools 面板刷新"能力"标签页)
|
|
1206
|
+
*/
|
|
1207
|
+
async refreshSkills() {
|
|
1208
|
+
if (!this.state.started || this.state.terminated)
|
|
1209
|
+
return;
|
|
1210
|
+
try {
|
|
1211
|
+
const { skills, skillGroups } = await this.loadAllSkills();
|
|
1212
|
+
if (this.skillTool) {
|
|
1213
|
+
this.skillTool.refresh(skills);
|
|
1214
|
+
}
|
|
1215
|
+
this.state.skills = skills;
|
|
1216
|
+
this.emitAcodeEvent("skill_status", {
|
|
1217
|
+
skillsDir: this.config.skillsDir,
|
|
1218
|
+
installedCount: skills.length,
|
|
1219
|
+
skills: skills.map((skill) => ({
|
|
1220
|
+
name: skill.name,
|
|
1221
|
+
description: skill.description,
|
|
1222
|
+
directory: skill.directory,
|
|
1223
|
+
})),
|
|
1224
|
+
skillGroups: skillGroups.map((group) => ({
|
|
1225
|
+
dir: group.dir,
|
|
1226
|
+
exists: group.exists,
|
|
1227
|
+
skills: group.skills.map((skill) => ({
|
|
1228
|
+
name: skill.name,
|
|
1229
|
+
description: skill.description,
|
|
1230
|
+
directory: skill.directory,
|
|
1231
|
+
})),
|
|
1232
|
+
})),
|
|
1233
|
+
});
|
|
1234
|
+
runtimeDebugLog(`[skill-watch] Skills refreshed: ${skills.length} skills ` +
|
|
1235
|
+
`(${skills.map((s) => s.name).join(", ") || "none"})`, { sessionId: this.config.sessionId });
|
|
1236
|
+
}
|
|
1237
|
+
catch (error) {
|
|
1238
|
+
console.error(`[skill-watch] Failed to refresh skills:`, error instanceof Error ? error.message : error);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* 关闭所有 skills 目录监听器与防抖定时器。在 stop() 中调用。
|
|
1243
|
+
*/
|
|
1244
|
+
stopSkillWatchers() {
|
|
1245
|
+
if (this.skillRefreshTimer) {
|
|
1246
|
+
clearTimeout(this.skillRefreshTimer);
|
|
1247
|
+
this.skillRefreshTimer = null;
|
|
1248
|
+
}
|
|
1249
|
+
for (const watcher of this.skillWatchers) {
|
|
1250
|
+
try {
|
|
1251
|
+
watcher.close();
|
|
1252
|
+
}
|
|
1253
|
+
catch {
|
|
1254
|
+
// 忽略关闭错误
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
this.skillWatchers = [];
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* 启动上下文压缩调试日志,每 5 秒写入系统临时目录下的 acode-compaction-debug.log。
|
|
1261
|
+
* 仅当环境变量 ACODE_COMPACTION_DEBUG=1 时启用,使用异步写入避免阻塞事件循环。
|
|
1262
|
+
*/
|
|
1263
|
+
startCompactionDebugLog() {
|
|
1264
|
+
if (!COMPACTION_DEBUG_ENABLED)
|
|
1265
|
+
return;
|
|
1266
|
+
if (this.compactionDebugTimer)
|
|
1267
|
+
return;
|
|
1268
|
+
const provider = this.state.provider;
|
|
1269
|
+
const contextLimit = (provider.contextLimit && provider.contextLimit > 0)
|
|
1270
|
+
? provider.contextLimit
|
|
1271
|
+
: FALLBACK_CONTEXT_LIMIT;
|
|
1272
|
+
const maxOutput = (provider.maxOutput && provider.maxOutput > 0)
|
|
1273
|
+
? provider.maxOutput
|
|
1274
|
+
: 8_192;
|
|
1275
|
+
const inputLimit = (provider.inputLimit && provider.inputLimit > 0)
|
|
1276
|
+
? provider.inputLimit
|
|
1277
|
+
: undefined;
|
|
1278
|
+
const triggerThreshold = calculateCompactionThreshold(contextLimit, maxOutput, inputLimit);
|
|
1279
|
+
this.compactionDebugTimer = setInterval(() => {
|
|
1280
|
+
try {
|
|
1281
|
+
const actualUsed = this.state.accumulatedInputTokens + this.state.accumulatedOutputTokens;
|
|
1282
|
+
const estimatedUsed = estimateConversationTokens(this.state.conversation);
|
|
1283
|
+
const effectiveUsed = Math.max(actualUsed, estimatedUsed);
|
|
1284
|
+
const sizeKB = conversationSizeKB(this.state.conversation);
|
|
1285
|
+
const isRunning = this.state.started && !this.state.terminated;
|
|
1286
|
+
const lines = [
|
|
1287
|
+
`[${new Date().toISOString()}]`,
|
|
1288
|
+
`started=${this.state.started}`,
|
|
1289
|
+
`terminated=${this.state.terminated}`,
|
|
1290
|
+
`activeTurn=${this.activeTurn}`,
|
|
1291
|
+
`conversationSize=${this.state.conversation.length}msgs`,
|
|
1292
|
+
`sizeKB=${sizeKB}KB`,
|
|
1293
|
+
`estimation=${estimatedUsed} tokens`,
|
|
1294
|
+
`actualInput=${this.state.accumulatedInputTokens}`,
|
|
1295
|
+
`actualOutput=${this.state.accumulatedOutputTokens}`,
|
|
1296
|
+
`actualUsed=${actualUsed}`,
|
|
1297
|
+
`contextLimit=${contextLimit}`,
|
|
1298
|
+
`inputLimit=${inputLimit ?? "n/a"}`,
|
|
1299
|
+
`maxOutput=${maxOutput}`,
|
|
1300
|
+
`triggerThreshold=${triggerThreshold}`,
|
|
1301
|
+
`remainingToTrigger=${Math.max(0, triggerThreshold - actualUsed)}`,
|
|
1302
|
+
`isRunning=${isRunning}`,
|
|
1303
|
+
`shouldCompact=${isRunning ? effectiveUsed >= triggerThreshold : false}`,
|
|
1304
|
+
`---`,
|
|
1305
|
+
].join(" | ");
|
|
1306
|
+
// 异步写入,避免阻塞事件循环
|
|
1307
|
+
fs.promises.appendFile(COMPACTION_DEBUG_LOG, lines + "\n", "utf-8").catch(() => {
|
|
1308
|
+
// 忽略日志写入错误
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
catch {
|
|
1312
|
+
// 忽略日志写入错误
|
|
1313
|
+
}
|
|
1314
|
+
}, 5000);
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* 停止上下文压缩调试日志。
|
|
1318
|
+
*/
|
|
1319
|
+
stopCompactionDebugLog() {
|
|
1320
|
+
if (this.compactionDebugTimer) {
|
|
1321
|
+
clearInterval(this.compactionDebugTimer);
|
|
1322
|
+
this.compactionDebugTimer = null;
|
|
1323
|
+
}
|
|
997
1324
|
}
|
|
998
1325
|
/**
|
|
999
1326
|
* 处理一轮用户输入。
|
|
@@ -1019,17 +1346,19 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1019
1346
|
this.abortController?.abort();
|
|
1020
1347
|
this.abortController = null;
|
|
1021
1348
|
this.state.conversation = cloneChatConversation(this.initialConversation);
|
|
1349
|
+
this.state.accumulatedInputTokens = 0;
|
|
1350
|
+
this.state.accumulatedOutputTokens = 0;
|
|
1022
1351
|
}
|
|
1023
1352
|
isTurnSuperseded(contextVersion) {
|
|
1024
1353
|
return contextVersion !== this.contextVersion;
|
|
1025
1354
|
}
|
|
1026
1355
|
async executeSubmit(message, contextVersion) {
|
|
1027
1356
|
const turnStartTime = Date.now();
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1357
|
+
runtimeDebugLog(`[ACodeRuntime] ══════════════════════════════════════════`);
|
|
1358
|
+
runtimeDebugLog(`[ACodeRuntime] ▶️ executeSubmit STARTED`);
|
|
1359
|
+
runtimeDebugLog(`[ACodeRuntime] Message: <redacted, length=${message.length}>`);
|
|
1360
|
+
runtimeDebugLog(`[ACodeRuntime] 🔢 Context version: ${contextVersion}`);
|
|
1361
|
+
runtimeDebugLog(`[ACodeRuntime] ═══════════════════════════════════════════`);
|
|
1033
1362
|
if (!this.state.started || this.state.terminated) {
|
|
1034
1363
|
console.error(`[ACodeRuntime] ❌ Runtime not active (started: ${this.state.started}, terminated: ${this.state.terminated})`);
|
|
1035
1364
|
throw new Error("ACode runtime is not active");
|
|
@@ -1039,14 +1368,14 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1039
1368
|
const originalPrompt = message.trim();
|
|
1040
1369
|
let turnCompleted = false;
|
|
1041
1370
|
try {
|
|
1042
|
-
|
|
1371
|
+
runtimeDebugLog(`[ACodeRuntime] 🔍 Dispatching UserPromptSubmit hook...`);
|
|
1043
1372
|
const promptHookStartTime = Date.now();
|
|
1044
1373
|
const promptHookResults = await this.hookBus.dispatch("UserPromptSubmit", { prompt: originalPrompt });
|
|
1045
1374
|
const promptHookDuration = Date.now() - promptHookStartTime;
|
|
1046
|
-
|
|
1375
|
+
runtimeDebugLog(`[ACodeRuntime] ✅ UserPromptSubmit hook completed in ${promptHookDuration}ms, ` +
|
|
1047
1376
|
`results: ${promptHookResults.length}`);
|
|
1048
1377
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1049
|
-
|
|
1378
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Turn superseded after hook, aborting`);
|
|
1050
1379
|
return;
|
|
1051
1380
|
}
|
|
1052
1381
|
const promptDeny = findDeniedHookResult(promptHookResults);
|
|
@@ -1057,15 +1386,15 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1057
1386
|
});
|
|
1058
1387
|
this.emitAcodeEvent("turn_complete", { failed: true });
|
|
1059
1388
|
turnCompleted = true;
|
|
1060
|
-
|
|
1389
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitted turn_complete (failed=true) due to hook deny`);
|
|
1061
1390
|
return;
|
|
1062
1391
|
}
|
|
1063
|
-
|
|
1392
|
+
runtimeDebugLog(`[ACodeRuntime] 🔍 Expanding command if any...`);
|
|
1064
1393
|
const expandedCommand = this.state.commandRegistry.expand(originalPrompt);
|
|
1065
1394
|
const trimmed = expandedCommand?.prompt.trim() || originalPrompt;
|
|
1066
1395
|
const commandAllowedTools = expandedCommand?.command.allowedTools;
|
|
1067
1396
|
if (expandedCommand) {
|
|
1068
|
-
|
|
1397
|
+
runtimeDebugLog(`[ACodeRuntime] 📋 Slash command detected: ${expandedCommand.command.name}`);
|
|
1069
1398
|
this.emitAcodeEvent("command_status", {
|
|
1070
1399
|
action: "slash_command",
|
|
1071
1400
|
command: expandedCommand.command.name,
|
|
@@ -1074,76 +1403,85 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1074
1403
|
});
|
|
1075
1404
|
}
|
|
1076
1405
|
if (!this.state.provider.baseURL || !this.state.provider.apiKey) {
|
|
1077
|
-
|
|
1406
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ No provider configured, using fallback response`);
|
|
1078
1407
|
const text = createFallbackResponse(trimmed, this.state);
|
|
1079
1408
|
this.emitAcodeEvent("assistant_delta", { text });
|
|
1080
1409
|
this.emitAcodeEvent("turn_complete", {
|
|
1081
1410
|
usage: { inputTokens: trimmed.length, outputTokens: text.length },
|
|
1082
1411
|
});
|
|
1083
1412
|
turnCompleted = true;
|
|
1084
|
-
|
|
1413
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitted turn_complete (fallback mode)`);
|
|
1085
1414
|
return;
|
|
1086
1415
|
}
|
|
1087
|
-
|
|
1416
|
+
runtimeDebugLog(`[ACodeRuntime] 📎 Parsing attachment message...`);
|
|
1088
1417
|
const parsedContent = parseAttachmentMessage(trimmed);
|
|
1089
1418
|
if (parsedContent.hasImages && !this.state.provider.supportsImageInput) {
|
|
1090
1419
|
console.error(`[ACodeRuntime] ❌ Image input not supported by current provider`);
|
|
1091
1420
|
throw new Error("ACode provider/model is not configured for image input. Enable provider.default.models[model].modalities.input including 'image' or options.supportsImageInput=true.");
|
|
1092
1421
|
}
|
|
1093
|
-
|
|
1422
|
+
runtimeDebugLog(`[ACodeRuntime] 🖼️ Materializing image content if any...`);
|
|
1094
1423
|
const userContent = await materializeImageContent(parsedContent.content);
|
|
1095
1424
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1096
|
-
|
|
1425
|
+
runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded after materializing content, aborting`);
|
|
1097
1426
|
return;
|
|
1098
1427
|
}
|
|
1099
|
-
|
|
1428
|
+
runtimeDebugLog(`[ACodeRuntime] 💬 Adding user message to conversation`);
|
|
1100
1429
|
this.state.conversation.push({ role: "user", content: userContent });
|
|
1101
|
-
|
|
1102
|
-
|
|
1430
|
+
runtimeDebugLog(`[ACodeRuntime] Conversation size: ${this.state.conversation.length} messages`);
|
|
1431
|
+
runtimeDebugLog(`[ACodeRuntime] Starting runModelTurn...`);
|
|
1103
1432
|
const modelTurnStartTime = Date.now();
|
|
1104
1433
|
const result = await this.runModelTurn(commandAllowedTools, contextVersion);
|
|
1105
1434
|
const modelTurnDuration = Date.now() - modelTurnStartTime;
|
|
1106
|
-
|
|
1107
|
-
|
|
1435
|
+
runtimeDebugLog(`[ACodeRuntime] ✅ runModelTurn completed in ${modelTurnDuration}ms`);
|
|
1436
|
+
runtimeDebugLog(`[ACodeRuntime] 📝 Result: text=${result.text ? result.text.slice(0, 80) + (result.text.length > 80 ? '...' : '') : '(empty)'}, ` +
|
|
1108
1437
|
`finishReason=${result.finishReason || '(none)'}`);
|
|
1109
1438
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1110
|
-
|
|
1439
|
+
runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded after runModelTurn, aborting`);
|
|
1111
1440
|
return;
|
|
1112
1441
|
}
|
|
1113
1442
|
if (result.text && !this.state.provider.stream) {
|
|
1114
|
-
|
|
1443
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitting assistant_delta (non-streaming mode)`);
|
|
1115
1444
|
this.emitAcodeEvent("assistant_delta", { text: result.text });
|
|
1116
1445
|
}
|
|
1117
|
-
|
|
1446
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitting turn_complete...`);
|
|
1118
1447
|
this.emitAcodeEvent("turn_complete", {
|
|
1448
|
+
// ★ 使用 result.totalOutputTextLength 替代 result.text.length,
|
|
1449
|
+
// result.text 只保留最后一次迭代的文本,导致多轮迭代时 outputTokens 被严重低估
|
|
1119
1450
|
usage: result.usage ?? {
|
|
1120
1451
|
inputTokens: trimmed.length,
|
|
1121
|
-
outputTokens: result.text.length,
|
|
1452
|
+
outputTokens: result.totalOutputTextLength ?? result.text.length,
|
|
1122
1453
|
},
|
|
1123
1454
|
...(result.responseBody ? { responseBody: result.responseBody } : {}),
|
|
1124
1455
|
...(result.finishReason ? { finishReason: result.finishReason } : {}),
|
|
1125
1456
|
});
|
|
1457
|
+
// ★ token 用量已在 runModelTurn 中按迭代更新到 state(非累加),
|
|
1458
|
+
// 此处不再累加,避免 actualUsed 跨轮只增不减导致 shouldCompact 永远为 true。
|
|
1459
|
+
// finalUsage 仍用于 turn_complete 事件的 usage 上报(多轮迭代的总和)。
|
|
1460
|
+
runtimeDebugLog(`[ACodeRuntime] 💰 Current context usage: ` +
|
|
1461
|
+
`input=${this.state.accumulatedInputTokens}, ` +
|
|
1462
|
+
`output=${this.state.accumulatedOutputTokens}` +
|
|
1463
|
+
(result.usage ? '' : ' (estimated, no provider usage)'));
|
|
1126
1464
|
turnCompleted = true;
|
|
1127
1465
|
const totalDuration = Date.now() - turnStartTime;
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1466
|
+
runtimeDebugLog(`[ACodeRuntime] ═══════════════════════════════════════════`);
|
|
1467
|
+
runtimeDebugLog(`[ACodeRuntime] ✅ executeSubmit COMPLETED in ${totalDuration}ms`);
|
|
1468
|
+
runtimeDebugLog(`[ACodeRuntime] ══════════════════════════════════════════`);
|
|
1131
1469
|
}
|
|
1132
1470
|
catch (error) {
|
|
1133
1471
|
const errorDuration = Date.now() - turnStartTime;
|
|
1134
1472
|
console.error(`[ACodeRuntime] ❌ executeSubmit FAILED after ${errorDuration}ms:`, error instanceof Error ? error.message : String(error));
|
|
1135
1473
|
console.error(`[ACodeRuntime] 🔍 Error stack:`, error instanceof Error ? error.stack : '(no stack)');
|
|
1136
1474
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1137
|
-
|
|
1475
|
+
runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded in error handler, skipping recovery`);
|
|
1138
1476
|
return;
|
|
1139
1477
|
}
|
|
1140
1478
|
if (this.abortRequested) {
|
|
1141
|
-
|
|
1479
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Abort requested, emitting aborted turn_complete`);
|
|
1142
1480
|
this.emitAcodeEvent("turn_complete", { aborted: true });
|
|
1143
1481
|
turnCompleted = true;
|
|
1144
1482
|
return;
|
|
1145
1483
|
}
|
|
1146
|
-
|
|
1484
|
+
runtimeDebugLog(`[ACodeRuntime] Emitting error and failed turn_complete...`);
|
|
1147
1485
|
this.emitAcodeEvent("error", {
|
|
1148
1486
|
message: error instanceof Error ? error.message : String(error),
|
|
1149
1487
|
});
|
|
@@ -1152,8 +1490,8 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1152
1490
|
}
|
|
1153
1491
|
finally {
|
|
1154
1492
|
const finallyDuration = Date.now() - turnStartTime;
|
|
1155
|
-
|
|
1156
|
-
|
|
1493
|
+
runtimeDebugLog(`[ACodeRuntime] 🔚 executeSubmit finally block entered after ${finallyDuration}ms`);
|
|
1494
|
+
runtimeDebugLog(`[ACodeRuntime] 🔍 turnCompleted=${turnCompleted}, activeTurn=${this.activeTurn}, ` +
|
|
1157
1495
|
`abortRequested=${this.abortRequested}`);
|
|
1158
1496
|
this.activeTurn = false;
|
|
1159
1497
|
this.abortRequested = false;
|
|
@@ -1167,7 +1505,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1167
1505
|
message: "ACode turn completed without proper completion event (internal error)",
|
|
1168
1506
|
});
|
|
1169
1507
|
this.emitAcodeEvent("turn_complete", { failed: true });
|
|
1170
|
-
|
|
1508
|
+
runtimeDebugLog(`[ACodeRuntime] Emergency turn_complete (failed=true) emitted`);
|
|
1171
1509
|
}
|
|
1172
1510
|
}
|
|
1173
1511
|
}
|
|
@@ -1176,6 +1514,144 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1176
1514
|
this.abortRequested = this.activeTurn;
|
|
1177
1515
|
this.abortController?.abort();
|
|
1178
1516
|
this.abortController = null;
|
|
1517
|
+
// ask_user_question 等待用户回答期间被 abort,需 reject 所有 waiter
|
|
1518
|
+
this.rejectAllQuestionWaiters("aborted");
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* 构造 ask_user_question 工具的依赖注入。
|
|
1522
|
+
* onQuestion: 发出 question_request 事件,让 adapter 转发到前端。
|
|
1523
|
+
* waitForAnswer: 阻塞等待 submitQuestionAnswer() 调用 resolve,或 abort/stop/supersede 时 reject。
|
|
1524
|
+
*/
|
|
1525
|
+
buildAskUserQuestionDeps() {
|
|
1526
|
+
return {
|
|
1527
|
+
onQuestion: (toolCallId, questions) => {
|
|
1528
|
+
this.emitAcodeEvent("question_request", {
|
|
1529
|
+
toolCallId,
|
|
1530
|
+
questions: questions,
|
|
1531
|
+
});
|
|
1532
|
+
},
|
|
1533
|
+
waitForAnswer: (toolCallId, contextVersion) => {
|
|
1534
|
+
return new Promise((resolve, reject) => {
|
|
1535
|
+
this.questionWaiters.set(toolCallId, {
|
|
1536
|
+
resolve,
|
|
1537
|
+
reject,
|
|
1538
|
+
contextVersion,
|
|
1539
|
+
});
|
|
1540
|
+
});
|
|
1541
|
+
},
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* 构造 sub-agent 工具集依赖(注入 SubAgentManager 句柄)。
|
|
1546
|
+
*
|
|
1547
|
+
* 5 个工具的依赖:
|
|
1548
|
+
* - spawnSubAgent: 创建子 agent(不阻塞,不自动 start)
|
|
1549
|
+
* - listSubAgents: 列出当前 agent 的所有子 agent
|
|
1550
|
+
* - getSubAgentStatus: 查询某子 agent 实时状态
|
|
1551
|
+
* - awaitSubAgents: 阻塞等待指定子 agent 数组完成
|
|
1552
|
+
* - abortSubAgent: 终止某子 agent
|
|
1553
|
+
*/
|
|
1554
|
+
buildSubAgentToolDeps(manager) {
|
|
1555
|
+
const parentAgentId = this.config.agentId ?? "unknown";
|
|
1556
|
+
const parentSessionId = this.config.sessionId;
|
|
1557
|
+
const workingDirectory = this.config.workingDirectory;
|
|
1558
|
+
const parentConfig = this.config;
|
|
1559
|
+
return {
|
|
1560
|
+
// 非阻塞 spawn:创建后不等待,立即返回 subId
|
|
1561
|
+
spawnSubAgent: async (opts) => {
|
|
1562
|
+
const subId = await manager.spawn({
|
|
1563
|
+
parentAgentId,
|
|
1564
|
+
parentSessionId,
|
|
1565
|
+
subagentType: opts.subagentType,
|
|
1566
|
+
description: opts.description,
|
|
1567
|
+
prompt: opts.prompt,
|
|
1568
|
+
workingDirectory,
|
|
1569
|
+
parentConfig,
|
|
1570
|
+
});
|
|
1571
|
+
// 自动 start(非阻塞,立即返回)
|
|
1572
|
+
await manager.start(subId);
|
|
1573
|
+
return { subId };
|
|
1574
|
+
},
|
|
1575
|
+
// 列出当前 agent 的所有子 agent(运行中 + 历史)
|
|
1576
|
+
listSubAgents: async () => {
|
|
1577
|
+
const entries = manager.listByParent(parentAgentId);
|
|
1578
|
+
const running = [];
|
|
1579
|
+
const history = [];
|
|
1580
|
+
for (const entry of entries) {
|
|
1581
|
+
const info = {
|
|
1582
|
+
subId: entry.subId,
|
|
1583
|
+
subagentType: entry.subagentType,
|
|
1584
|
+
description: entry.description,
|
|
1585
|
+
status: entry.status,
|
|
1586
|
+
startedAt: entry.startedAt,
|
|
1587
|
+
endedAt: entry.endedAt,
|
|
1588
|
+
durationMs: entry.durationMs,
|
|
1589
|
+
};
|
|
1590
|
+
if (entry.status === "pending" || entry.status === "running") {
|
|
1591
|
+
running.push(info);
|
|
1592
|
+
}
|
|
1593
|
+
else {
|
|
1594
|
+
history.push(info);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
// history 按 endedAt 倒序
|
|
1598
|
+
history.sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0));
|
|
1599
|
+
return { running, history };
|
|
1600
|
+
},
|
|
1601
|
+
// 查询某子 agent 实时状态
|
|
1602
|
+
getSubAgentStatus: async (subId) => {
|
|
1603
|
+
const entry = manager.getSubAgent(subId);
|
|
1604
|
+
if (!entry)
|
|
1605
|
+
return null;
|
|
1606
|
+
return {
|
|
1607
|
+
subId: entry.subId,
|
|
1608
|
+
subagentType: entry.subagentType,
|
|
1609
|
+
description: entry.description,
|
|
1610
|
+
status: entry.status,
|
|
1611
|
+
startedAt: entry.startedAt,
|
|
1612
|
+
endedAt: entry.endedAt,
|
|
1613
|
+
durationMs: entry.durationMs,
|
|
1614
|
+
prompt: entry.prompt,
|
|
1615
|
+
result: entry.result,
|
|
1616
|
+
errorMessage: entry.errorMessage,
|
|
1617
|
+
contextSnapshot: entry.contextSnapshot,
|
|
1618
|
+
};
|
|
1619
|
+
},
|
|
1620
|
+
// 阻塞等待指定子 agent 数组完成
|
|
1621
|
+
awaitSubAgents: (subIds, timeoutMs) => manager.awaitCompletion(subIds, timeoutMs),
|
|
1622
|
+
// 终止某子 agent
|
|
1623
|
+
abortSubAgent: (subId) => manager.abort(subId),
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* 接收用户对 ask_user_question 的回答(由 AcodeSdkAdapter.sendQuestionAnswer 调用)。
|
|
1628
|
+
* 若当前 turn 已被 supersede(contextVersion 变更),返回 false 让 adapter 知晓。
|
|
1629
|
+
*/
|
|
1630
|
+
submitQuestionAnswer(toolCallId, answers) {
|
|
1631
|
+
const waiter = this.questionWaiters.get(toolCallId);
|
|
1632
|
+
if (!waiter) {
|
|
1633
|
+
return false;
|
|
1634
|
+
}
|
|
1635
|
+
this.questionWaiters.delete(toolCallId);
|
|
1636
|
+
// 若 turn 已被 supersede,不 resolve(避免过期答案污染新轮次)
|
|
1637
|
+
if (this.isTurnSuperseded(waiter.contextVersion)) {
|
|
1638
|
+
waiter.reject(new Error("turn superseded before user answered"));
|
|
1639
|
+
return false;
|
|
1640
|
+
}
|
|
1641
|
+
waiter.resolve(answers);
|
|
1642
|
+
return true;
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* 中止所有 ask_user_question 等待,让工具 execute() 抛错走 appendToolFailureResult。
|
|
1646
|
+
* 在 abort()/stop() 中调用,确保用户点停止或会话终止时 execute 不会永久阻塞。
|
|
1647
|
+
*/
|
|
1648
|
+
rejectAllQuestionWaiters(reason) {
|
|
1649
|
+
if (this.questionWaiters.size === 0)
|
|
1650
|
+
return;
|
|
1651
|
+
for (const [, waiter] of this.questionWaiters) {
|
|
1652
|
+
waiter.reject(new Error(`ask_user_question ${reason}`));
|
|
1653
|
+
}
|
|
1654
|
+
this.questionWaiters.clear();
|
|
1179
1655
|
}
|
|
1180
1656
|
/** 终止 ACode 会话并发出 session_complete。 */
|
|
1181
1657
|
async stop() {
|
|
@@ -1187,6 +1663,8 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1187
1663
|
this.abortRequested = this.activeTurn;
|
|
1188
1664
|
this.abortController?.abort();
|
|
1189
1665
|
this.abortController = null;
|
|
1666
|
+
// ask_user_question 等待用户回答期间被 stop,需 reject 所有 waiter
|
|
1667
|
+
this.rejectAllQuestionWaiters("session terminated");
|
|
1190
1668
|
await runBestEffort("ACode active turn shutdown", this.submitQueue, (message) => {
|
|
1191
1669
|
this.emitAcodeEvent("error", { message });
|
|
1192
1670
|
});
|
|
@@ -1199,6 +1677,18 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1199
1677
|
this.sessionCompleteEmitted = true;
|
|
1200
1678
|
this.emitAcodeEvent("session_complete", { exitCode: 0 });
|
|
1201
1679
|
}
|
|
1680
|
+
this.stopCompactionDebugLog();
|
|
1681
|
+
this.stopSkillWatchers();
|
|
1682
|
+
if (COMPACTION_DEBUG_ENABLED) {
|
|
1683
|
+
try {
|
|
1684
|
+
fs.promises.appendFile(COMPACTION_DEBUG_LOG, `[${new Date().toISOString()}] runtime stopped | ---\n`, "utf-8").catch(() => {
|
|
1685
|
+
// 忽略
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
catch {
|
|
1689
|
+
// 忽略
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1202
1692
|
}
|
|
1203
1693
|
resolveCommandDirs(acodeConfig) {
|
|
1204
1694
|
const configured = [
|
|
@@ -1215,29 +1705,31 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1215
1705
|
let finalUsage;
|
|
1216
1706
|
let finalResponseBody;
|
|
1217
1707
|
let finalFinishReason;
|
|
1708
|
+
let accumulatedAssistantText = ""; // 累加多轮迭代的 output 文本长度
|
|
1709
|
+
let accumulatedPromptTokens = 0; // 累加多轮迭代的 prompt_tokens
|
|
1218
1710
|
// 日志:开始 runModelTurn
|
|
1219
|
-
|
|
1711
|
+
runtimeDebugLog(`[ACodeRuntime] ▶️ runModelTurn started (contextVersion: ${contextVersion})`);
|
|
1220
1712
|
// 移除5次限制,添加安全上限防止真正的无限循环
|
|
1221
1713
|
const SAFETY_LIMIT = 50; // 安全上限,正常情况下不会达到
|
|
1222
1714
|
for (let step = 0; step < SAFETY_LIMIT; step += 1) {
|
|
1223
1715
|
// 详细日志:记录每次迭代
|
|
1224
|
-
|
|
1716
|
+
runtimeDebugLog(`[ACodeRuntime] 🔄 Iteration ${step + 1} starting...`);
|
|
1225
1717
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1226
|
-
|
|
1227
|
-
return { text: "" };
|
|
1718
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Turn superseded at iteration ${step + 1}, aborting`);
|
|
1719
|
+
return { text: "", totalOutputTextLength: accumulatedAssistantText.length };
|
|
1228
1720
|
}
|
|
1229
1721
|
const requestStartTime = Date.now();
|
|
1230
1722
|
const response = await this.requestChatCompletion(commandAllowedTools);
|
|
1231
1723
|
const requestDuration = Date.now() - requestStartTime;
|
|
1232
1724
|
// 日志:AI请求完成
|
|
1233
|
-
|
|
1725
|
+
runtimeDebugLog(`[ACodeRuntime] 🤖 AI request completed in ${requestDuration}ms, ` +
|
|
1234
1726
|
`choices: ${response.choices?.length || 0}`);
|
|
1235
1727
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1236
|
-
|
|
1237
|
-
return { text: "" };
|
|
1728
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Turn superseded after AI response at iteration ${step + 1}`);
|
|
1729
|
+
return { text: "", totalOutputTextLength: accumulatedAssistantText.length };
|
|
1238
1730
|
}
|
|
1239
1731
|
if (this.state.terminated) {
|
|
1240
|
-
|
|
1732
|
+
runtimeDebugLog(`[ACodeRuntime] Runtime terminated at iteration ${step + 1}`);
|
|
1241
1733
|
throw new Error("ACode runtime was stopped during the active turn");
|
|
1242
1734
|
}
|
|
1243
1735
|
const choice = response.choices?.[0];
|
|
@@ -1252,7 +1744,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1252
1744
|
? message.tool_calls
|
|
1253
1745
|
: [];
|
|
1254
1746
|
// 日志:AI响应详情
|
|
1255
|
-
|
|
1747
|
+
runtimeDebugLog(`[ACodeRuntime] AI response: ` +
|
|
1256
1748
|
`content=${content ? content.slice(0, 100) + (content.length > 100 ? '...' : '') : '(empty)'}, ` +
|
|
1257
1749
|
`toolCalls=${toolCalls.length}, ` +
|
|
1258
1750
|
`finishReason=${finalFinishReason || '(none)'}`);
|
|
@@ -1261,11 +1753,25 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1261
1753
|
const rtOutputTokens = response.usage.completion_tokens ?? response.usage.output_tokens;
|
|
1262
1754
|
if (typeof rtInputTokens === "number" &&
|
|
1263
1755
|
typeof rtOutputTokens === "number") {
|
|
1264
|
-
finalUsage
|
|
1265
|
-
inputTokens
|
|
1266
|
-
outputTokens
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1756
|
+
if (finalUsage) {
|
|
1757
|
+
finalUsage.inputTokens += rtInputTokens;
|
|
1758
|
+
finalUsage.outputTokens += rtOutputTokens;
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
finalUsage = {
|
|
1762
|
+
inputTokens: rtInputTokens,
|
|
1763
|
+
outputTokens: rtOutputTokens,
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
accumulatedPromptTokens += rtInputTokens;
|
|
1767
|
+
// ★ 更新 state 为当前迭代的实际 token 用量(非累加)
|
|
1768
|
+
// prompt_tokens 代表本次请求时 provider 看到的完整上下文大小,
|
|
1769
|
+
// 是当前上下文大小的最准确度量。
|
|
1770
|
+
// 之前用 += 累加导致 actualUsed 只增不减,shouldCompact 永远为 true。
|
|
1771
|
+
this.state.accumulatedInputTokens = rtInputTokens;
|
|
1772
|
+
this.state.accumulatedOutputTokens = rtOutputTokens;
|
|
1773
|
+
runtimeDebugLog(`[ACodeRuntime] 💰 Token usage: input=${rtInputTokens}, output=${rtOutputTokens} ` +
|
|
1774
|
+
`(turn accumulated: input=${accumulatedPromptTokens}, output=${finalUsage.outputTokens})`);
|
|
1269
1775
|
}
|
|
1270
1776
|
}
|
|
1271
1777
|
this.state.conversation.push({
|
|
@@ -1275,31 +1781,33 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1275
1781
|
});
|
|
1276
1782
|
if (content) {
|
|
1277
1783
|
finalText = content;
|
|
1278
|
-
|
|
1784
|
+
accumulatedAssistantText += content;
|
|
1785
|
+
runtimeDebugLog(`[ACodeRuntime] ✨ Captured assistant text: ${content.slice(0, 80)}...`);
|
|
1279
1786
|
}
|
|
1280
1787
|
if (toolCalls.length === 0) {
|
|
1281
|
-
|
|
1788
|
+
runtimeDebugLog(`[ACodeRuntime] ✅ No tool calls at iteration ${step + 1}, completing turn`);
|
|
1282
1789
|
return {
|
|
1283
1790
|
text: finalText,
|
|
1284
1791
|
usage: finalUsage,
|
|
1285
1792
|
responseBody: finalResponseBody,
|
|
1286
1793
|
finishReason: finalFinishReason,
|
|
1794
|
+
totalOutputTextLength: accumulatedAssistantText.length,
|
|
1287
1795
|
};
|
|
1288
1796
|
}
|
|
1289
1797
|
// 日志:即将执行工具调用
|
|
1290
1798
|
const toolNames = toolCalls.map(tc => tc.function.name).join(", ");
|
|
1291
|
-
|
|
1799
|
+
runtimeDebugLog(`[ACodeRuntime] 🔧 Executing ${toolCalls.length} tool(s) at iteration ${step + 1}: ${toolNames}`);
|
|
1292
1800
|
const toolStart = Date.now();
|
|
1293
1801
|
await this.handleToolCalls(toolCalls, commandAllowedTools, contextVersion);
|
|
1294
1802
|
const toolDuration = Date.now() - toolStart;
|
|
1295
1803
|
// 日志:工具执行完成
|
|
1296
|
-
|
|
1804
|
+
runtimeDebugLog(`[ACodeRuntime] ⚙️ Tool execution completed in ${toolDuration}ms at iteration ${step + 1}`);
|
|
1297
1805
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1298
|
-
|
|
1299
|
-
return { text: "" };
|
|
1806
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Turn superseded after tool execution at iteration ${step + 1}`);
|
|
1807
|
+
return { text: "", totalOutputTextLength: accumulatedAssistantText.length };
|
|
1300
1808
|
}
|
|
1301
1809
|
// 日志:准备下一次迭代
|
|
1302
|
-
|
|
1810
|
+
runtimeDebugLog(`[ACodeRuntime] 🔄 Preparing iteration ${step + 2}...`);
|
|
1303
1811
|
}
|
|
1304
1812
|
// 达到安全上限(理论上不应该发生)
|
|
1305
1813
|
console.error(`[ACodeRuntime] 🚨 CRITICAL: Reached safety limit of ${SAFETY_LIMIT} iterations! ` +
|
|
@@ -1309,19 +1817,162 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1309
1817
|
usage: finalUsage,
|
|
1310
1818
|
responseBody: finalResponseBody,
|
|
1311
1819
|
finishReason: finalFinishReason || "safety_iteration_limit_reached",
|
|
1820
|
+
totalOutputTextLength: accumulatedAssistantText.length,
|
|
1312
1821
|
};
|
|
1313
1822
|
}
|
|
1823
|
+
/**
|
|
1824
|
+
* LLM 摘要器:调用当前 provider 生成结构化对话摘要。
|
|
1825
|
+
*
|
|
1826
|
+
* 这是把 OpenCode 的 LLM 摘要能力赋能到 ACode 的核心方法:
|
|
1827
|
+
* - OpenCode 在 compaction.process 中调用独立的 compaction agent
|
|
1828
|
+
* - ACode 复用当前 provider 配置,发起一次非流式 chat completion 请求
|
|
1829
|
+
* - 使用 COMPACTION_LLM_PROMPT_TEMPLATE 作为 prompt 模板
|
|
1830
|
+
*
|
|
1831
|
+
* 设计要点:
|
|
1832
|
+
* 1. 非流式调用(不需要流式,简化处理)
|
|
1833
|
+
* 2. 不带 tools(纯文本生成)
|
|
1834
|
+
* 3. 失败时抛出异常,由 summarizeConversation 降级到规则摘要
|
|
1835
|
+
* 4. 独立的 AbortController,避免主请求取消影响摘要
|
|
1836
|
+
* 5. 复用 provider 的 timeout 配置
|
|
1837
|
+
*
|
|
1838
|
+
* @param input 摘要输入(conversationText + messageCount + contextLimit)
|
|
1839
|
+
* @returns LLM 生成的结构化摘要文本
|
|
1840
|
+
*/
|
|
1841
|
+
async summarizeConversationWithLLM(input) {
|
|
1842
|
+
const provider = this.state.provider;
|
|
1843
|
+
const baseURL = provider.baseURL;
|
|
1844
|
+
if (!baseURL) {
|
|
1845
|
+
throw new Error("ACode provider baseURL is not configured for LLM summarization");
|
|
1846
|
+
}
|
|
1847
|
+
const headers = {
|
|
1848
|
+
"Content-Type": "application/json",
|
|
1849
|
+
};
|
|
1850
|
+
if (provider.apiKey) {
|
|
1851
|
+
headers.Authorization = `Bearer ${provider.apiKey}`;
|
|
1852
|
+
}
|
|
1853
|
+
const requestBody = {
|
|
1854
|
+
model: provider.model,
|
|
1855
|
+
stream: false,
|
|
1856
|
+
messages: [
|
|
1857
|
+
{
|
|
1858
|
+
role: "system",
|
|
1859
|
+
content: "You are a helpful AI assistant tasked with summarizing conversations. " +
|
|
1860
|
+
"When asked to summarize, provide a detailed but concise summary of the conversation. " +
|
|
1861
|
+
"Focus on information that would be helpful for continuing the conversation. " +
|
|
1862
|
+
"Do not respond to any questions in the conversation, only output the summary.",
|
|
1863
|
+
},
|
|
1864
|
+
{
|
|
1865
|
+
role: "user",
|
|
1866
|
+
content: `Below is an earlier conversation (${input.messageCount} messages) that needs to be summarized ` +
|
|
1867
|
+
`to free context space (current context limit: ${input.contextLimit} tokens).\n\n` +
|
|
1868
|
+
`--- Conversation Start ---\n${input.conversationText}\n--- Conversation End ---\n\n` +
|
|
1869
|
+
COMPACTION_LLM_PROMPT_TEMPLATE,
|
|
1870
|
+
},
|
|
1871
|
+
],
|
|
1872
|
+
};
|
|
1873
|
+
const abortController = new AbortController();
|
|
1874
|
+
this.abortController = abortController;
|
|
1875
|
+
const response = await withTimeout(fetch(normalizeChatCompletionsUrl(baseURL), {
|
|
1876
|
+
method: "POST",
|
|
1877
|
+
headers,
|
|
1878
|
+
body: JSON.stringify(requestBody),
|
|
1879
|
+
signal: abortController.signal,
|
|
1880
|
+
}), provider.requestTimeoutMs, "ACode LLM summarization", () => {
|
|
1881
|
+
abortController.abort();
|
|
1882
|
+
});
|
|
1883
|
+
if (!response.ok) {
|
|
1884
|
+
throw new Error(`ACode LLM summarization request failed: ${response.status} ${await response.text()}`);
|
|
1885
|
+
}
|
|
1886
|
+
const payload = (await response.json());
|
|
1887
|
+
const content = payload.choices?.[0]?.message?.content;
|
|
1888
|
+
if (!content || content.trim().length === 0) {
|
|
1889
|
+
throw new Error("ACode LLM summarization returned empty content");
|
|
1890
|
+
}
|
|
1891
|
+
return content;
|
|
1892
|
+
}
|
|
1314
1893
|
async requestChatCompletion(commandAllowedTools) {
|
|
1315
1894
|
const provider = this.state.provider;
|
|
1316
1895
|
const baseURL = provider.baseURL;
|
|
1317
1896
|
if (!baseURL) {
|
|
1318
1897
|
throw new Error("ACode provider baseURL is not configured");
|
|
1319
1898
|
}
|
|
1320
|
-
|
|
1899
|
+
// 主动上下文压缩:在发送 AI 请求前检测 token 是否溢出
|
|
1900
|
+
// 使用 provider 配置中的 contextLimit/inputLimit/maxOutput(来自模板配置)。
|
|
1901
|
+
// 不依赖 MODEL_CONTEXT_LIMITS 硬编码查找表,若未配置则使用 FALLBACK_CONTEXT_LIMIT(64K)。
|
|
1902
|
+
const contextLimit = (provider.contextLimit && provider.contextLimit > 0)
|
|
1903
|
+
? provider.contextLimit
|
|
1904
|
+
: FALLBACK_CONTEXT_LIMIT;
|
|
1905
|
+
const maxOutput = (provider.maxOutput && provider.maxOutput > 0)
|
|
1906
|
+
? provider.maxOutput
|
|
1907
|
+
: 8_192;
|
|
1908
|
+
// ★ 融合 OpenCode 预算思路:优先用 inputLimit(部分 Provider input+output 总额 ≠ context 上限)
|
|
1909
|
+
// 未配置 inputLimit 时降级到 contextLimit,行为与改进前一致。
|
|
1910
|
+
const inputLimit = (provider.inputLimit && provider.inputLimit > 0)
|
|
1911
|
+
? provider.inputLimit
|
|
1912
|
+
: undefined;
|
|
1913
|
+
// ★ 阈值 = min(contextLimit × 55%, hardLimit - reserved)
|
|
1914
|
+
// hardLimit = inputLimit ?? contextLimit
|
|
1915
|
+
// reserved = min(COMPACTION_BUFFER, maxOutput) // buffer 不超过输出上限
|
|
1916
|
+
const compactionThreshold = calculateCompactionThreshold(contextLimit, maxOutput, inputLimit);
|
|
1917
|
+
// 使用 effectiveUsed = max(actualUsed, estimatedUsed)
|
|
1918
|
+
// 因为部分 provider 不返回 usage 数据(actualUsed=0),此时降级到文本估算
|
|
1919
|
+
const actualUsed = this.state.accumulatedInputTokens + this.state.accumulatedOutputTokens;
|
|
1920
|
+
const estimatedUsed = estimateConversationTokens(this.state.conversation);
|
|
1921
|
+
const effectiveUsed = Math.max(actualUsed, estimatedUsed);
|
|
1922
|
+
const shouldCompact = effectiveUsed >= compactionThreshold;
|
|
1923
|
+
runtimeDebugLog(`[ACodeRuntime] 🔍 Compaction check: ` +
|
|
1924
|
+
`actualUsed=${actualUsed}, estimation=${estimatedUsed}, ` +
|
|
1925
|
+
`effectiveUsed=${effectiveUsed}, contextLimit=${contextLimit}, ` +
|
|
1926
|
+
`inputLimit=${inputLimit ?? "n/a"}, maxOutput=${maxOutput}, ` +
|
|
1927
|
+
`triggerThreshold=${compactionThreshold}, shouldCompact=${shouldCompact}`);
|
|
1928
|
+
if (shouldCompact) {
|
|
1929
|
+
const sizeBeforeKB = conversationSizeKB(this.state.conversation);
|
|
1930
|
+
const compactionId = `compaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1931
|
+
this.emitAcodeEvent("context_compaction", {
|
|
1932
|
+
action: "start",
|
|
1933
|
+
compactionId,
|
|
1934
|
+
sizeKB: sizeBeforeKB,
|
|
1935
|
+
estimatedTokens: estimatedUsed,
|
|
1936
|
+
actualInputTokens: this.state.accumulatedInputTokens,
|
|
1937
|
+
actualOutputTokens: this.state.accumulatedOutputTokens,
|
|
1938
|
+
messageCount: this.state.conversation.length,
|
|
1939
|
+
model: provider.model,
|
|
1940
|
+
trigger: actualUsed >= compactionThreshold ? "actual_usage" : "estimated_overflow",
|
|
1941
|
+
});
|
|
1942
|
+
// ★ 发射压缩子事件(检测到溢出)
|
|
1943
|
+
this.emitCompactionPhase(compactionId, "detect", "检测到上下文溢出", `有效用量: ${effectiveUsed} tokens, 阈值: ${compactionThreshold}, 触发: ${actualUsed >= compactionThreshold ? "实际用量" : "估算溢出"}`);
|
|
1944
|
+
const result = await performCompaction(this.state.conversation, contextLimit, maxOutput, {
|
|
1945
|
+
llmSummarizer: (input) => this.summarizeConversationWithLLM(input),
|
|
1946
|
+
onPhase: (phaseEvent) => this.handleCompactionPhase(compactionId, phaseEvent),
|
|
1947
|
+
});
|
|
1948
|
+
const sizeAfterKB = conversationSizeKB(this.state.conversation);
|
|
1949
|
+
this.emitAcodeEvent("context_compaction", {
|
|
1950
|
+
action: "end",
|
|
1951
|
+
compactionId,
|
|
1952
|
+
sizeBeforeKB,
|
|
1953
|
+
sizeAfterKB,
|
|
1954
|
+
freedKB: Math.round((sizeBeforeKB - sizeAfterKB) * 10) / 10,
|
|
1955
|
+
strategy: result.strategy,
|
|
1956
|
+
freedTokens: result.freedTokens,
|
|
1957
|
+
prunedToolMessages: result.prunedToolMessages,
|
|
1958
|
+
summarizedMessages: result.summarizedMessages,
|
|
1959
|
+
messageCountBefore: result.messageCountBefore,
|
|
1960
|
+
messageCountAfter: result.messageCountAfter,
|
|
1961
|
+
});
|
|
1962
|
+
// ★ 压缩后重置累积 token 计数,使下一次请求时 actualUsed 反映压缩后的真实上下文大小。
|
|
1963
|
+
// 若不重置,actualUsed 仍为压缩前的旧值,shouldCompact 会持续为 true 导致无效压缩循环。
|
|
1964
|
+
this.state.accumulatedInputTokens = 0;
|
|
1965
|
+
this.state.accumulatedOutputTokens = 0;
|
|
1966
|
+
}
|
|
1967
|
+
const truncatedCount = await truncateConversationIfNeeded(this.state.conversation, MAX_REQUEST_BODY_BYTES, contextLimit, maxOutput, (input) => this.summarizeConversationWithLLM(input));
|
|
1321
1968
|
if (truncatedCount > 0) {
|
|
1322
1969
|
this.emitAcodeEvent("warning", {
|
|
1323
1970
|
message: `Conversation truncated: removed ${truncatedCount} older messages to fit within ${MAX_REQUEST_BODY_BYTES} byte request size limit`,
|
|
1324
1971
|
});
|
|
1972
|
+
// ★ HTTP body 兜底压缩后同样重置累积 token 计数,避免下一轮 actualUsed 仍为
|
|
1973
|
+
// 压缩前旧值导致 shouldCompact 误判(与主动压缩路径行为一致,见上方 shouldCompact 块)。
|
|
1974
|
+
this.state.accumulatedInputTokens = 0;
|
|
1975
|
+
this.state.accumulatedOutputTokens = 0;
|
|
1325
1976
|
}
|
|
1326
1977
|
const allowedTools = this.state.exposedTools.filter((tool) => isToolAllowed(tool, this.state.permissions, commandAllowedTools));
|
|
1327
1978
|
const buildBody = () => {
|
|
@@ -1343,7 +1994,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1343
1994
|
const emitAiRequest = (body) => {
|
|
1344
1995
|
const requestBody = JSON.stringify(body, null, 2);
|
|
1345
1996
|
// [DEBUG] ACode Runtime 实际发送的模型名称
|
|
1346
|
-
|
|
1997
|
+
runtimeDebugLog(`[ACodeRuntime] ★★★ DEBUG ★★★ emitAiRequest: 发送请求 model=${provider.model}, endpoint=${requestUrl}, messageCount=${this.state.conversation.length}`);
|
|
1347
1998
|
this.emitAcodeEvent("ai_request", {
|
|
1348
1999
|
model: provider.model,
|
|
1349
2000
|
endpoint: requestUrl,
|
|
@@ -1415,11 +2066,57 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1415
2066
|
!this.abortRequested &&
|
|
1416
2067
|
!this.state.terminated &&
|
|
1417
2068
|
isContextOverflowProviderRequestError(requestError)) {
|
|
1418
|
-
|
|
2069
|
+
// 响应式上下文压缩:Provider 返回上下文溢出错误时强制执行
|
|
2070
|
+
// 这里不通过 performCompaction(它会先检查 isOverflow),
|
|
2071
|
+
// 而是直接调用 summarizeConversation 进行强制摘要化
|
|
2072
|
+
const sizeBeforeKB = conversationSizeKB(this.state.conversation);
|
|
2073
|
+
const msgCountBefore = this.state.conversation.length;
|
|
2074
|
+
const compactionId = `compaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2075
|
+
this.emitAcodeEvent("context_compaction", {
|
|
2076
|
+
action: "start",
|
|
2077
|
+
compactionId,
|
|
2078
|
+
sizeKB: sizeBeforeKB,
|
|
2079
|
+
messageCount: msgCountBefore,
|
|
2080
|
+
model: provider.model,
|
|
2081
|
+
trigger: "provider_overflow_error",
|
|
2082
|
+
});
|
|
2083
|
+
// ★ 发射压缩子事件(检测到溢出)
|
|
2084
|
+
this.emitCompactionPhase(compactionId, "detect", "Provider 返回上下文溢出", `消息: ${msgCountBefore}条, 大小: ${sizeBeforeKB}KB`);
|
|
2085
|
+
// 先尝试 prune 旧工具输出
|
|
2086
|
+
this.emitCompactionPhase(compactionId, "prune", "阶段1: 裁剪工具输出", "正在裁剪...");
|
|
2087
|
+
const pruneResult = pruneToolOutputs(this.state.conversation, contextLimit);
|
|
2088
|
+
this.emitCompactionPhase(compactionId, "prune_done", "阶段1: 裁剪工具输出", `裁剪了 ${pruneResult.prunedCount} 条工具输出, 释放 ${pruneResult.freedTokens} tokens`);
|
|
2089
|
+
// 再强制 summarize 早期对话(使用 LLM 摘要器提升摘要质量,失败降级到规则摘要)
|
|
2090
|
+
this.emitCompactionPhase(compactionId, "summarize", "阶段2: 摘要化对话", "正在摘要...");
|
|
2091
|
+
const summarizedCount = await summarizeConversation(this.state.conversation, {
|
|
2092
|
+
llmSummarizer: (input) => this.summarizeConversationWithLLM(input),
|
|
2093
|
+
contextLimit,
|
|
2094
|
+
});
|
|
2095
|
+
this.emitCompactionPhase(compactionId, "summarize_done", "阶段2: 摘要化对话", `摘要化了 ${summarizedCount} 条消息`);
|
|
2096
|
+
const sizeAfterKB = conversationSizeKB(this.state.conversation);
|
|
2097
|
+
const msgCountAfter = this.state.conversation.length;
|
|
1419
2098
|
contextOverflowCompacted = true;
|
|
2099
|
+
const compactedCount = pruneResult.prunedCount + summarizedCount;
|
|
2100
|
+
this.emitAcodeEvent("context_compaction", {
|
|
2101
|
+
action: "end",
|
|
2102
|
+
compactionId,
|
|
2103
|
+
sizeBeforeKB,
|
|
2104
|
+
sizeAfterKB,
|
|
2105
|
+
freedKB: Math.round((sizeBeforeKB - sizeAfterKB) * 10) / 10,
|
|
2106
|
+
messageCountBefore: msgCountBefore,
|
|
2107
|
+
messageCountAfter: msgCountAfter,
|
|
2108
|
+
strategy: summarizedCount > 0 ? "summarize" : "prune",
|
|
2109
|
+
freedTokens: pruneResult.freedTokens,
|
|
2110
|
+
prunedToolMessages: pruneResult.prunedCount,
|
|
2111
|
+
summarizedMessages: summarizedCount,
|
|
2112
|
+
trigger: "provider_overflow_error",
|
|
2113
|
+
});
|
|
1420
2114
|
if (compactedCount === 0) {
|
|
1421
2115
|
throw requestError;
|
|
1422
2116
|
}
|
|
2117
|
+
// ★ 响应式压缩后同样重置累积 token 计数
|
|
2118
|
+
this.state.accumulatedInputTokens = 0;
|
|
2119
|
+
this.state.accumulatedOutputTokens = 0;
|
|
1423
2120
|
this.emitAcodeEvent("warning", {
|
|
1424
2121
|
message: `ACode provider context limit exceeded; summarized ${compactedCount} older messages and rebuilt the request instead of retrying the same oversized payload`,
|
|
1425
2122
|
});
|
|
@@ -1457,6 +2154,8 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1457
2154
|
const toolCallAccumulators = new Map();
|
|
1458
2155
|
let text = "";
|
|
1459
2156
|
let buffer = "";
|
|
2157
|
+
/** 从 SSE 流最终 chunk 中提取的 usage(OpenAI 格式在最后一个 chunk 的 usage 字段中) */
|
|
2158
|
+
let streamUsage;
|
|
1460
2159
|
const consumeFrame = (frame) => {
|
|
1461
2160
|
for (const data of parseSseDataLines(frame)) {
|
|
1462
2161
|
if (data === "[DONE]") {
|
|
@@ -1466,6 +2165,11 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1466
2165
|
if (!isRecord(payload)) {
|
|
1467
2166
|
continue;
|
|
1468
2167
|
}
|
|
2168
|
+
// ★ 提取 SSE chunk 中的 usage(OpenAI 流式 API 在最后一个 chunk 返回完整 usage,此时 choices 为空数组)
|
|
2169
|
+
const chunkUsage = payload.usage;
|
|
2170
|
+
if (chunkUsage) {
|
|
2171
|
+
streamUsage = chunkUsage;
|
|
2172
|
+
}
|
|
1469
2173
|
const choice = payload.choices?.[0];
|
|
1470
2174
|
const delta = choice?.delta;
|
|
1471
2175
|
const reasoningText = extractReasoningText(delta);
|
|
@@ -1510,17 +2214,18 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1510
2214
|
},
|
|
1511
2215
|
},
|
|
1512
2216
|
],
|
|
2217
|
+
...(streamUsage ? { usage: streamUsage } : {}),
|
|
1513
2218
|
};
|
|
1514
2219
|
}
|
|
1515
2220
|
async handleToolCalls(toolCalls, commandAllowedTools, contextVersion = this.contextVersion) {
|
|
1516
|
-
|
|
2221
|
+
runtimeDebugLog(`[ACodeRuntime] handleToolCalls started with ${toolCalls.length} tool(s)`);
|
|
1517
2222
|
for (const toolCall of toolCalls) {
|
|
1518
2223
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1519
|
-
|
|
2224
|
+
runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded, skipping remaining tools`);
|
|
1520
2225
|
return;
|
|
1521
2226
|
}
|
|
1522
2227
|
const toolName = toolCall.function.name;
|
|
1523
|
-
|
|
2228
|
+
runtimeDebugLog(`[ACodeRuntime] 🔍 Looking up tool: ${toolName}`);
|
|
1524
2229
|
const tool = this.state.exposedTools.find((item) => item.exposedName === toolCall.function.name);
|
|
1525
2230
|
if (!tool) {
|
|
1526
2231
|
console.error(`[ACodeRuntime] ❌ Tool not found: ${toolCall.function.name}, ` +
|
|
@@ -1532,11 +2237,11 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1532
2237
|
});
|
|
1533
2238
|
continue;
|
|
1534
2239
|
}
|
|
1535
|
-
|
|
2240
|
+
runtimeDebugLog(`[ACodeRuntime] Found tool: ${tool.toolName} (kind: ${tool.kind}, source: ${tool.kind === 'mcp' ? tool.serverName : 'builtin'})`);
|
|
1536
2241
|
try {
|
|
1537
2242
|
if (tool.kind === "builtin") {
|
|
1538
2243
|
const toolArguments = coerceArguments(toolCall.function.arguments);
|
|
1539
|
-
|
|
2244
|
+
runtimeDebugLog(`[ACodeRuntime] Built-in tool arguments: ${JSON.stringify(toolArguments).slice(0, 200)}`);
|
|
1540
2245
|
assertToolPermission(tool, this.state.permissions, commandAllowedTools);
|
|
1541
2246
|
const preResults = await this.hookBus.dispatch("PreToolUse", {
|
|
1542
2247
|
source: "builtin",
|
|
@@ -1553,7 +2258,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1553
2258
|
throw new Error(denied.message ||
|
|
1554
2259
|
`ACode tool denied by hook: ${tool.exposedName}`);
|
|
1555
2260
|
}
|
|
1556
|
-
|
|
2261
|
+
runtimeDebugLog(`[ACodeRuntime] Emitting mcp_status call_tool for ${tool.exposedName}`);
|
|
1557
2262
|
this.emitAcodeEvent("mcp_status", {
|
|
1558
2263
|
action: "call_tool",
|
|
1559
2264
|
server: "builtin",
|
|
@@ -1564,7 +2269,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1564
2269
|
argumentsPreview: summarizeToolArguments(toolArguments),
|
|
1565
2270
|
});
|
|
1566
2271
|
const toolStart = Date.now();
|
|
1567
|
-
|
|
2272
|
+
runtimeDebugLog(`[ACodeRuntime] ▶️ Executing built-in tool: ${tool.exposedName}`);
|
|
1568
2273
|
const result = await withTimeout(tool.execute(toolArguments, {
|
|
1569
2274
|
onOutput: (outputEvent) => {
|
|
1570
2275
|
this.emitAcodeEvent("command_output", {
|
|
@@ -1582,9 +2287,10 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1582
2287
|
: {}),
|
|
1583
2288
|
});
|
|
1584
2289
|
},
|
|
2290
|
+
toolCallId: toolCall.id,
|
|
1585
2291
|
}), getBuiltInToolTimeoutMs(tool, toolArguments, this.state.provider.mcpToolTimeoutMs), `ACode built-in tool ${tool.toolName}`);
|
|
1586
2292
|
const toolDuration = Date.now() - toolStart;
|
|
1587
|
-
|
|
2293
|
+
runtimeDebugLog(`[ACodeRuntime] ✅ Built-in tool ${tool.exposedName} completed in ${toolDuration}ms`);
|
|
1588
2294
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1589
2295
|
return;
|
|
1590
2296
|
}
|
|
@@ -1598,7 +2304,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1598
2304
|
return;
|
|
1599
2305
|
}
|
|
1600
2306
|
const resultText = toToolResultText(result);
|
|
1601
|
-
|
|
2307
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitting mcp_status tool_result for ${tool.exposedName}, ` +
|
|
1602
2308
|
`result length: ${resultText.length} chars`);
|
|
1603
2309
|
this.emitAcodeEvent("mcp_status", {
|
|
1604
2310
|
action: "tool_result",
|
|
@@ -1611,7 +2317,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1611
2317
|
tool_call_id: toolCall.id,
|
|
1612
2318
|
content: resultText,
|
|
1613
2319
|
});
|
|
1614
|
-
await this.appendSyntheticMultimodalUserMessage(resultText, contextVersion);
|
|
2320
|
+
await this.appendSyntheticMultimodalUserMessage(resultText, toolCall.id, contextVersion);
|
|
1615
2321
|
continue;
|
|
1616
2322
|
}
|
|
1617
2323
|
// MCP工具处理
|
|
@@ -1627,7 +2333,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1627
2333
|
continue;
|
|
1628
2334
|
}
|
|
1629
2335
|
const toolArguments = coerceArguments(toolCall.function.arguments);
|
|
1630
|
-
|
|
2336
|
+
runtimeDebugLog(`[ACodeRuntime] MCP tool arguments: ${JSON.stringify(toolArguments).slice(0, 200)}`);
|
|
1631
2337
|
assertToolPermission(tool, this.state.permissions, commandAllowedTools);
|
|
1632
2338
|
const preResults = await this.hookBus.dispatch("PreToolUse", {
|
|
1633
2339
|
source: "mcp",
|
|
@@ -1644,7 +2350,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1644
2350
|
console.error(`[ACodeRuntime] ❌ MCP tool denied by hook: ${tool.exposedName}: ${denied.message}`);
|
|
1645
2351
|
throw new Error(denied.message || `ACode tool denied by hook: ${tool.exposedName}`);
|
|
1646
2352
|
}
|
|
1647
|
-
|
|
2353
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitting mcp_status call_tool for MCP ${tool.exposedName}`);
|
|
1648
2354
|
this.emitAcodeEvent("mcp_status", {
|
|
1649
2355
|
action: "call_tool",
|
|
1650
2356
|
server: tool.serverName,
|
|
@@ -1659,11 +2365,11 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1659
2365
|
? this.state.provider.mcpPollingToolTimeoutMs
|
|
1660
2366
|
: this.state.provider.mcpToolTimeoutMs;
|
|
1661
2367
|
const toolStart = Date.now();
|
|
1662
|
-
|
|
2368
|
+
runtimeDebugLog(`[ACodeRuntime] ▶️ Executing MCP tool: ${tool.exposedName} ` +
|
|
1663
2369
|
`(timeout: ${toolTimeoutMs}ms, polling: ${isPollingTool})`);
|
|
1664
2370
|
const result = await this.callMcpToolWithTimeout(connection, tool, toolArguments, toolTimeoutMs, isPollingTool);
|
|
1665
2371
|
const toolDuration = Date.now() - toolStart;
|
|
1666
|
-
|
|
2372
|
+
runtimeDebugLog(`[ACodeRuntime] ✅ MCP tool ${tool.exposedName} completed in ${toolDuration}ms`);
|
|
1667
2373
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1668
2374
|
return;
|
|
1669
2375
|
}
|
|
@@ -1678,7 +2384,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1678
2384
|
return;
|
|
1679
2385
|
}
|
|
1680
2386
|
const resultText = toToolResultText(result);
|
|
1681
|
-
|
|
2387
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitting mcp_status tool_result for MCP ${tool.exposedName}, ` +
|
|
1682
2388
|
`result length: ${resultText.length} chars`);
|
|
1683
2389
|
this.emitAcodeEvent("mcp_status", {
|
|
1684
2390
|
action: "tool_result",
|
|
@@ -1691,7 +2397,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1691
2397
|
tool_call_id: toolCall.id,
|
|
1692
2398
|
content: resultText,
|
|
1693
2399
|
});
|
|
1694
|
-
await this.appendSyntheticMultimodalUserMessage(resultText, contextVersion);
|
|
2400
|
+
await this.appendSyntheticMultimodalUserMessage(resultText, toolCall.id, contextVersion);
|
|
1695
2401
|
}
|
|
1696
2402
|
catch (error) {
|
|
1697
2403
|
console.error(`[ACodeRuntime] ❌ Tool execution failed: ${toolCall.function.name}`, error instanceof Error ? error.message : String(error));
|
|
@@ -1699,13 +2405,13 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1699
2405
|
return;
|
|
1700
2406
|
}
|
|
1701
2407
|
if (this.abortRequested || this.state.terminated) {
|
|
1702
|
-
|
|
2408
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Rethrowing error due to abort/termination`);
|
|
1703
2409
|
throw error instanceof Error ? error : new Error(String(error));
|
|
1704
2410
|
}
|
|
1705
2411
|
this.appendToolFailureResult(toolCall, tool, error);
|
|
1706
2412
|
}
|
|
1707
2413
|
}
|
|
1708
|
-
|
|
2414
|
+
runtimeDebugLog(`[ACodeRuntime] handleToolCalls completed for all ${toolCalls.length} tool(s)`);
|
|
1709
2415
|
}
|
|
1710
2416
|
async callMcpToolWithTimeout(connection, tool, toolArguments, timeoutMs, retrySdkTimeout) {
|
|
1711
2417
|
const sdkTimeout = timeoutMs > 0 ? timeoutMs : MCP_SDK_MAX_SAFE_TIMEOUT_MS;
|
|
@@ -1776,7 +2482,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1776
2482
|
notifyStop();
|
|
1777
2483
|
}
|
|
1778
2484
|
}
|
|
1779
|
-
async appendSyntheticMultimodalUserMessage(toolResultText, contextVersion = this.contextVersion) {
|
|
2485
|
+
async appendSyntheticMultimodalUserMessage(toolResultText, toolCallId, contextVersion = this.contextVersion) {
|
|
1780
2486
|
const parsedContent = parseAttachmentMessage(toolResultText);
|
|
1781
2487
|
if (!parsedContent.hasImages) {
|
|
1782
2488
|
return;
|
|
@@ -1792,11 +2498,48 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1792
2498
|
return;
|
|
1793
2499
|
}
|
|
1794
2500
|
this.state.conversation.push({ role: "user", content });
|
|
2501
|
+
// 去重:将对应 tool 消息中的图片/附件数据替换为占位符,
|
|
2502
|
+
// 避免 conversation 中同时存在原始 tool 结果和解析后的 user 消息导致重复。
|
|
2503
|
+
const toolMsg = this.state.conversation.find((m) => m.role === "tool" && m.tool_call_id === toolCallId);
|
|
2504
|
+
if (toolMsg && typeof toolMsg.content === "string") {
|
|
2505
|
+
let cleaned;
|
|
2506
|
+
// 尝试 JSON 格式:移除 attachments 数组,保留 text 和 messages
|
|
2507
|
+
try {
|
|
2508
|
+
const parsed = JSON.parse(toolMsg.content);
|
|
2509
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
2510
|
+
const clone = { ...parsed };
|
|
2511
|
+
if ("attachments" in clone) {
|
|
2512
|
+
delete clone.attachments;
|
|
2513
|
+
cleaned = JSON.stringify(clone);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
catch {
|
|
2518
|
+
// 不是 JSON,尝试 marker 格式
|
|
2519
|
+
}
|
|
2520
|
+
if (cleaned === undefined) {
|
|
2521
|
+
const markerCleaned = toolMsg.content
|
|
2522
|
+
.replace(/\[(图片|文件)\s*:\s*([^\]\n]*?)\s*\]/gi, "")
|
|
2523
|
+
.trim();
|
|
2524
|
+
if (markerCleaned !== toolMsg.content) {
|
|
2525
|
+
cleaned = markerCleaned;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
if (cleaned !== undefined && cleaned.length > 0) {
|
|
2529
|
+
toolMsg.content = cleaned;
|
|
2530
|
+
}
|
|
2531
|
+
else if (cleaned !== undefined) {
|
|
2532
|
+
toolMsg.content = "[Tool result contained images; see user message above]";
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
1795
2535
|
}
|
|
1796
2536
|
appendToolFailureResult(toolCall, tool, error) {
|
|
1797
2537
|
const resultText = toToolErrorResultText(tool, error);
|
|
2538
|
+
// 工具执行失败是正常运行过程的一部分(Agent 会继续处理失败结果)。
|
|
2539
|
+
// 标记 nonFatal=true 让 adapter 不变更运行态为 "error",避免面板按钮异常。
|
|
1798
2540
|
this.emitAcodeEvent("error", {
|
|
1799
2541
|
message: `ACode tool failed: ${tool.exposedName}: ${getErrorMessage(error)}`,
|
|
2542
|
+
nonFatal: true,
|
|
1800
2543
|
});
|
|
1801
2544
|
this.emitAcodeEvent("mcp_status", {
|
|
1802
2545
|
action: "tool_result",
|
|
@@ -1869,6 +2612,63 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1869
2612
|
requestInit,
|
|
1870
2613
|
});
|
|
1871
2614
|
}
|
|
2615
|
+
/**
|
|
2616
|
+
* 通过 MCP 连接调用 get_profile 工具获取用户 profile 信息,
|
|
2617
|
+
* 格式化为文本后返回,用于拼接到 rolePrompt 下方。
|
|
2618
|
+
* 主流程:遍历已连接的 MCP -> 找到 get_profile 工具 -> 调用 -> 格式化结果。
|
|
2619
|
+
* 容错:任何步骤失败都返回空字符串,不阻塞启动。
|
|
2620
|
+
*/
|
|
2621
|
+
async fetchProfilePrompt(mcpConnections) {
|
|
2622
|
+
for (const connection of mcpConnections) {
|
|
2623
|
+
const profileTool = connection.tools.find((tool) => tool.toolName === "get_profile");
|
|
2624
|
+
if (!profileTool) {
|
|
2625
|
+
continue;
|
|
2626
|
+
}
|
|
2627
|
+
try {
|
|
2628
|
+
const result = await withTimeout(connection.client.callTool({ name: "get_profile", arguments: {} }, undefined, { timeout: DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS }), DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS, "ACode startup get_profile");
|
|
2629
|
+
const profile = this.extractProfileFromToolResult(result);
|
|
2630
|
+
return buildProfilePrompt(profile);
|
|
2631
|
+
}
|
|
2632
|
+
catch (error) {
|
|
2633
|
+
this.emitAcodeEvent("warning", {
|
|
2634
|
+
message: `ACode failed to fetch profile via ${connection.serverName}/get_profile: ${error instanceof Error ? error.message : String(error)}`,
|
|
2635
|
+
});
|
|
2636
|
+
return "";
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
return "";
|
|
2640
|
+
}
|
|
2641
|
+
/**
|
|
2642
|
+
* 从 MCP callTool 返回值中提取 profile 对象。
|
|
2643
|
+
* 主流程:优先取 content[0].text JSON -> 回退到 result 本身为对象。
|
|
2644
|
+
*/
|
|
2645
|
+
extractProfileFromToolResult(result) {
|
|
2646
|
+
if (!isRecord(result)) {
|
|
2647
|
+
return null;
|
|
2648
|
+
}
|
|
2649
|
+
const content = result.content;
|
|
2650
|
+
if (Array.isArray(content)) {
|
|
2651
|
+
for (const item of content) {
|
|
2652
|
+
if (!isRecord(item))
|
|
2653
|
+
continue;
|
|
2654
|
+
if (typeof item.text === "string" && item.text.trim()) {
|
|
2655
|
+
try {
|
|
2656
|
+
const parsed = JSON.parse(item.text);
|
|
2657
|
+
if (isRecord(parsed)) {
|
|
2658
|
+
return parsed;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
catch {
|
|
2662
|
+
// text 不是 JSON,继续
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
if (isRecord(result)) {
|
|
2668
|
+
return result;
|
|
2669
|
+
}
|
|
2670
|
+
return null;
|
|
2671
|
+
}
|
|
1872
2672
|
async disconnectMcpServers() {
|
|
1873
2673
|
const connections = this.state.mcpConnections;
|
|
1874
2674
|
this.state.mcpConnections = [];
|
|
@@ -1878,3 +2678,5 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1878
2678
|
}, this.state.provider.mcpToolTimeoutMs)));
|
|
1879
2679
|
}
|
|
1880
2680
|
}
|
|
2681
|
+
/** skill 重扫防抖间隔(毫秒) */
|
|
2682
|
+
ACodeRuntime.SKILL_REFRESH_DEBOUNCE_MS = 500;
|