aws-runtime-bridge 1.9.33 → 1.9.41
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 +66 -5
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/AcodeSdkAdapter.js +771 -69
- package/dist/adapter/ClaudeSdkAdapter.d.ts +6 -0
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +24 -1
- package/dist/adapter/OpencodeSdkAdapter.d.ts +19 -0
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +62 -0
- 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 +86 -9
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/adapter/types.js +15 -5
- package/dist/index.js +15 -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/ai-sources.js +4 -1
- package/dist/routes/dashboard.d.ts.map +1 -1
- package/dist/routes/dashboard.js +1 -1
- 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/runtime-binding.d.ts.map +1 -1
- package/dist/routes/runtime-binding.js +6 -3
- package/dist/routes/sessions.js +89 -4
- 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 +267 -0
- package/dist/routes/terminal.d.ts +12 -1
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +326 -24
- package/dist/services/acode-package.d.ts +8 -1
- package/dist/services/acode-package.d.ts.map +1 -1
- package/dist/services/acode-package.js +33 -4
- package/dist/services/agent-event-store.d.ts +1 -0
- package/dist/services/agent-event-store.d.ts.map +1 -1
- package/dist/services/aws-client-agent-mcp.d.ts.map +1 -1
- package/dist/services/aws-client-agent-mcp.js +11 -4
- package/dist/services/event-delta-pusher.js +4 -4
- package/dist/services/fileSnapshotStore.js +1 -1
- package/dist/services/legacy-path-migration.d.ts +3 -0
- package/dist/services/legacy-path-migration.d.ts.map +1 -0
- package/dist/services/legacy-path-migration.js +66 -0
- package/dist/services/lifecycle-state.d.ts.map +1 -1
- package/dist/services/lifecycle-state.js +1 -1
- package/dist/services/runtime-binding.d.ts +5 -0
- package/dist/services/runtime-binding.d.ts.map +1 -1
- package/dist/services/runtime-binding.js +33 -1
- package/dist/services/session-output.d.ts +10 -1
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +54 -4
- 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/built-in-file-tools.js +504 -3
- package/package/acode/dist/compaction.d.ts +242 -0
- package/package/acode/dist/compaction.d.ts.map +1 -0
- package/package/acode/dist/compaction.js +919 -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 +257 -0
- package/package/acode/dist/runtime.d.ts.map +1 -1
- package/package/acode/dist/runtime.js +1843 -191
- 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 +264 -0
- package/package/acode/dist/sub-agent-manager.d.ts.map +1 -0
- package/package/acode/dist/sub-agent-manager.js +825 -0
- package/package/acode/dist/sub-agent-store.d.ts +40 -0
- package/package/acode/dist/sub-agent-store.d.ts.map +1 -0
- package/package/acode/dist/sub-agent-store.js +164 -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 +85 -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 +41 -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/agent-client.d.ts +9 -0
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.js +69 -10
- package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/config.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/config.js +4 -0
- package/package/aws-client-agent-mcp/dist/config.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/constants.d.ts +2 -0
- package/package/aws-client-agent-mcp/dist/constants.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/constants.js +3 -1
- package/package/aws-client-agent-mcp/dist/constants.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/context-manager.d.ts +4 -1
- package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/context-manager.js +6 -2
- package/package/aws-client-agent-mcp/dist/context-manager.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/context-manager.test.js +5 -3
- package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts +5 -0
- package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.js +15 -1
- package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +162 -71
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +1007 -504
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +371 -253
- 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 +6 -17
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.js +20 -75
- package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts +45 -45
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.js +114 -162
- package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.test.js +175 -164
- 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 -9
- package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.js +31 -58
- package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +40 -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 +123 -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 +13 -13
- 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 +23 -16
- 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 +12 -25
- package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +34 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.js +146 -18
- package/package/aws-client-agent-mcp/dist/websocket-client.js.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,15 +1,23 @@
|
|
|
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
|
-
import { isMcpPollingToolName } from "./mcpToolNames.js";
|
|
20
|
+
import { isMcpPollingToolName, getSemanticToolName } from "./mcpToolNames.js";
|
|
13
21
|
function cloneChatContentPart(part) {
|
|
14
22
|
if (part.type === "text") {
|
|
15
23
|
return { type: "text", text: part.text };
|
|
@@ -58,19 +66,94 @@ function normalizeText(value) {
|
|
|
58
66
|
return text || undefined;
|
|
59
67
|
}
|
|
60
68
|
const DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 60_000;
|
|
69
|
+
/**
|
|
70
|
+
* 单次 provider 请求的总时长上限(毫秒)。默认 10 分钟。
|
|
71
|
+
* 超过该总预算即终止整个请求(含流式读取),不论是否仍在持续接收 chunk。
|
|
72
|
+
* 与 requestTimeoutMs(流式空闲超时 / 非流式分段超时)配合:只要每个 chunk 间隔不超过 60s,
|
|
73
|
+
* 但整体跑满 10min 仍会断开,避免长流式请求无限挂起。
|
|
74
|
+
* 0 = 不设总时长上限(仅靠 requestTimeoutMs 保护)。
|
|
75
|
+
*/
|
|
76
|
+
const DEFAULT_PROVIDER_REQUEST_TOTAL_TIMEOUT_MS = 10 * 60_000;
|
|
61
77
|
const DEFAULT_PROVIDER_REQUEST_RETRY_COUNT = 2;
|
|
78
|
+
/**
|
|
79
|
+
* 无限重试哨兵值:配置 requestRetryCount 为 -1 时,
|
|
80
|
+
* provider 请求在遇到可重试错误(超时/网络错误等)时无限重试,
|
|
81
|
+
* 直到请求成功、用户中止或会话终止。
|
|
82
|
+
* 启用后同时禁用总时长上限(requestTotalTimeoutMs 无效),
|
|
83
|
+
* 仅依靠单次请求超时(requestTimeoutMs)保护。
|
|
84
|
+
*/
|
|
85
|
+
const INFINITE_RETRY_SENTINEL = -1;
|
|
62
86
|
const DEFAULT_PROVIDER_REQUEST_RETRY_DELAY_MS = 1000;
|
|
63
87
|
const DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS = 60_000;
|
|
64
88
|
const DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS = 0;
|
|
65
89
|
const MCP_SDK_MAX_SAFE_TIMEOUT_MS = 2_147_483_647;
|
|
90
|
+
/**
|
|
91
|
+
* 子 agent 通过共享 MCP 连接允许调用的工具白名单。
|
|
92
|
+
* 与 aws-client-agent-mcp 的 MEMORY_TOOL_NAMES 保持一致,
|
|
93
|
+
* 防止子 agent 调用 poll_message/send_group 等与父 agent 冲突的工具。
|
|
94
|
+
*/
|
|
95
|
+
const SUB_AGENT_ALLOWED_MCP_TOOLS = new Set([
|
|
96
|
+
"create_memory",
|
|
97
|
+
"read_memory",
|
|
98
|
+
"update_memory",
|
|
99
|
+
"delete_memory",
|
|
100
|
+
"search_memory",
|
|
101
|
+
"list_memories",
|
|
102
|
+
"get_memory_stats",
|
|
103
|
+
"list_memories_by_file",
|
|
104
|
+
// Memory 过期回忆工具:memory_expiration 委托子 agent 需调用以整理过期记忆
|
|
105
|
+
"memory_recurrence",
|
|
106
|
+
"memory_recurrence_complete",
|
|
107
|
+
]);
|
|
66
108
|
const DEFAULT_BASH_BUILT_IN_TOOL_TIMEOUT_MS = 10 * 60_000 + 2000;
|
|
67
109
|
const DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
110
|
+
/**
|
|
111
|
+
* 当 poll_message 返回的全部消息被 memory 自动委托处理后,重新 poll 前的等待间隔。
|
|
112
|
+
* 防止紧密循环:MCP doPollMessage 对 updating memory 每次都返回通知,
|
|
113
|
+
* 而 processMemoryAutoDelegates 因 URI 已在处理中而返回 null,
|
|
114
|
+
* 导致 while(true) 无延迟重 poll,引发状态快速跳动(思考→MCP→跳过)最终连接丢失。
|
|
115
|
+
*/
|
|
116
|
+
const DELEGATE_REPOLL_DELAY_MS = 5_000;
|
|
68
117
|
const MAX_INLINE_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
69
118
|
const MAX_REQUEST_BODY_BYTES = 800 * 1024;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
119
|
+
/**
|
|
120
|
+
* 上下文压缩调试日志开关:仅当设置环境变量 ACODE_COMPACTION_DEBUG=1 时启用。
|
|
121
|
+
* 默认关闭,避免生产环境同步写盘阻塞事件循环。
|
|
122
|
+
*/
|
|
123
|
+
const COMPACTION_DEBUG_ENABLED = process.env.ACODE_COMPACTION_DEBUG === "1";
|
|
124
|
+
const COMPACTION_DEBUG_LOG = COMPACTION_DEBUG_ENABLED
|
|
125
|
+
? path.join(os.tmpdir(), "acode-compaction-debug.log")
|
|
126
|
+
: "";
|
|
127
|
+
/**
|
|
128
|
+
* Runtime 调试日志:受 ACODE_COMPACTION_DEBUG=1 开关控制。
|
|
129
|
+
* 默认关闭以避免生产环境同步写盘阻塞事件循环、泄漏用户消息内容。
|
|
130
|
+
* 文件中所有原 `console.log` 调用均改为走此 helper。
|
|
131
|
+
*/
|
|
132
|
+
function runtimeDebugLog(...args) {
|
|
133
|
+
if (!COMPACTION_DEBUG_ENABLED)
|
|
134
|
+
return;
|
|
135
|
+
console.log(...args);
|
|
136
|
+
}
|
|
137
|
+
/** 当 provider 未配置 contextLimit 时使用的默认值 */
|
|
138
|
+
const FALLBACK_CONTEXT_LIMIT = 64_000;
|
|
139
|
+
/**
|
|
140
|
+
* 解析 provider 的压缩配置,返回实际用于触发压缩的 contextLimit/maxOutput/inputLimit/threshold。
|
|
141
|
+
* 供 session_started 事件、startCompactionDebugLog、requestChatCompletion 共用,
|
|
142
|
+
* 确保前端显示的压缩点与后端实际触发阈值完全一致(消除前端自算公式的脱节)。
|
|
143
|
+
*/
|
|
144
|
+
function resolveCompactionConfig(provider) {
|
|
145
|
+
const contextLimit = (provider.contextLimit && provider.contextLimit > 0)
|
|
146
|
+
? provider.contextLimit
|
|
147
|
+
: FALLBACK_CONTEXT_LIMIT;
|
|
148
|
+
const maxOutput = (provider.maxOutput && provider.maxOutput > 0)
|
|
149
|
+
? provider.maxOutput
|
|
150
|
+
: 8_192;
|
|
151
|
+
const inputLimit = (provider.inputLimit && provider.inputLimit > 0)
|
|
152
|
+
? provider.inputLimit
|
|
153
|
+
: undefined;
|
|
154
|
+
const threshold = calculateCompactionThreshold(contextLimit, maxOutput, inputLimit);
|
|
155
|
+
return { contextLimit, maxOutput, inputLimit, threshold };
|
|
156
|
+
}
|
|
74
157
|
async function withTimeout(operation, timeoutMs, label, onTimeout) {
|
|
75
158
|
if (timeoutMs <= 0) {
|
|
76
159
|
return await operation;
|
|
@@ -104,6 +187,10 @@ function normalizeNonNegativeInteger(value, fallback) {
|
|
|
104
187
|
return 0;
|
|
105
188
|
}
|
|
106
189
|
const numeric = typeof value === "number" ? value : Number(value);
|
|
190
|
+
// -1 是无限重试哨兵值,直接透传
|
|
191
|
+
if (numeric === INFINITE_RETRY_SENTINEL) {
|
|
192
|
+
return INFINITE_RETRY_SENTINEL;
|
|
193
|
+
}
|
|
107
194
|
return Number.isFinite(numeric) && numeric > 0
|
|
108
195
|
? Math.floor(numeric)
|
|
109
196
|
: fallback;
|
|
@@ -145,6 +232,18 @@ function estimateContextTokens(systemPrompt, exposedTools) {
|
|
|
145
232
|
}
|
|
146
233
|
return Math.ceil(charCount / ESTIMATED_CHARS_PER_TOKEN);
|
|
147
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* 估算 exposedTools 定义的 token 开销。
|
|
237
|
+
* tools 的 JSON schema(函数名、描述、参数定义)会被序列化进请求体,
|
|
238
|
+
* 占用可观 token 数(十几个工具可达数千 tokens),必须计入上下文估算。
|
|
239
|
+
* 否则 estimateConversationTokens 系统性偏低,导致主动压缩阈值无法触达。
|
|
240
|
+
*/
|
|
241
|
+
function estimateToolsTokens(exposedTools) {
|
|
242
|
+
if (exposedTools.length === 0)
|
|
243
|
+
return 0;
|
|
244
|
+
const toolsJson = JSON.stringify(toOpenAiTools(exposedTools));
|
|
245
|
+
return estimateTokensFromText(toolsJson);
|
|
246
|
+
}
|
|
148
247
|
async function delay(ms) {
|
|
149
248
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
150
249
|
}
|
|
@@ -172,6 +271,36 @@ function modelSupportsImageInput(model, defaultProvider, providerOptions) {
|
|
|
172
271
|
* 解析 ACode 当前会话模型:优先显式启动参数,其次兼容 cc-switch 写入的
|
|
173
272
|
* OpenCode-style `provider.default.model`,最后回落到旧版 top-level model。
|
|
174
273
|
*/
|
|
274
|
+
/**
|
|
275
|
+
* 按优先级从 provider 配置中查找字段值:
|
|
276
|
+
* 1. options.*(通用配置)
|
|
277
|
+
* 2. models.<currentModel>.*(当前模型专属配置,面板初始化写入的位置)
|
|
278
|
+
* 3. provider.*(provider 级别兜底)
|
|
279
|
+
*
|
|
280
|
+
* @param optionsValue providerOptions 上的值(已取出)
|
|
281
|
+
* @param defaultProvider provider.default 对象
|
|
282
|
+
* @param currentModel 当前使用的模型名
|
|
283
|
+
* @param fieldName 要查找的字段名(如 "contextLimit")
|
|
284
|
+
*/
|
|
285
|
+
function resolveProviderConfigValue(optionsValue, defaultProvider, currentModel, fieldName) {
|
|
286
|
+
// 1. options 级别优先
|
|
287
|
+
if (optionsValue !== undefined)
|
|
288
|
+
return optionsValue;
|
|
289
|
+
if (!defaultProvider)
|
|
290
|
+
return undefined;
|
|
291
|
+
// 2. models.<modelName>.<fieldName>
|
|
292
|
+
const models = defaultProvider.models;
|
|
293
|
+
if (isRecord(models)) {
|
|
294
|
+
const modelConfig = models[currentModel];
|
|
295
|
+
if (isRecord(modelConfig)) {
|
|
296
|
+
const modelValue = modelConfig[fieldName];
|
|
297
|
+
if (modelValue !== undefined)
|
|
298
|
+
return modelValue;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// 3. provider 级别兜底
|
|
302
|
+
return defaultProvider[fieldName];
|
|
303
|
+
}
|
|
175
304
|
function resolveProviderConfig(config, acodeConfig) {
|
|
176
305
|
const defaultProvider = acodeConfig.provider?.default;
|
|
177
306
|
const providerOptions = defaultProvider?.options;
|
|
@@ -179,17 +308,36 @@ function resolveProviderConfig(config, acodeConfig) {
|
|
|
179
308
|
defaultProvider?.model ||
|
|
180
309
|
acodeConfig.model ||
|
|
181
310
|
"acode-default";
|
|
311
|
+
// effort 优先级:runtime config (per-session/per-instance) > provider options (acode.json) > provider default
|
|
312
|
+
const effort = normalizeText(config.effort) ||
|
|
313
|
+
normalizeText(providerOptions?.effort) ||
|
|
314
|
+
normalizeText(defaultProvider?.effort) ||
|
|
315
|
+
undefined;
|
|
182
316
|
return {
|
|
183
317
|
model,
|
|
184
318
|
baseURL: normalizeText(providerOptions?.baseURL || providerOptions?.baseUrl),
|
|
185
319
|
apiKey: normalizeText(providerOptions?.apiKey),
|
|
320
|
+
effort,
|
|
186
321
|
systemPrompt: config.systemPrompt || acodeConfig.systemPrompt,
|
|
187
322
|
stream: providerOptions?.stream !== false,
|
|
188
|
-
|
|
323
|
+
// ★ 安全修复:使用 normalizePositiveInteger 禁止设为 0,
|
|
324
|
+
// 0 会导致 withTimeout 直接返回 operation(禁用超时),
|
|
325
|
+
// fetch 挂起时请求永久不返回,agent 死锁。
|
|
326
|
+
requestTimeoutMs: normalizePositiveInteger(providerOptions?.requestTimeoutMs ?? providerOptions?.timeoutMs, DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS),
|
|
327
|
+
requestTotalTimeoutMs: normalizePositiveInteger(providerOptions?.requestTotalTimeoutMs, DEFAULT_PROVIDER_REQUEST_TOTAL_TIMEOUT_MS),
|
|
189
328
|
requestRetryCount: normalizeNonNegativeInteger(providerOptions?.requestRetryCount, DEFAULT_PROVIDER_REQUEST_RETRY_COUNT),
|
|
190
329
|
mcpToolTimeoutMs: normalizePositiveInteger(providerOptions?.mcpToolTimeoutMs, DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS),
|
|
191
330
|
mcpPollingToolTimeoutMs: normalizeNonNegativeInteger(providerOptions?.mcpPollingToolTimeoutMs, DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS),
|
|
192
331
|
supportsImageInput: modelSupportsImageInput(model, defaultProvider, providerOptions),
|
|
332
|
+
// ★ 从 provider 配置中读取 contextLimit/inputLimit/maxOutput,
|
|
333
|
+
// requestChatCompletion 和 startCompactionDebugLog 使用这些值计算压缩触发阈值。
|
|
334
|
+
// 按优先级查找三个位置:
|
|
335
|
+
// 1. provider.default.options.*(options 级别,通用配置)
|
|
336
|
+
// 2. provider.default.models.<modelName>.*(模型级别,面板初始化写入的位置)
|
|
337
|
+
// 3. provider.default.*(provider 级别兜底)
|
|
338
|
+
contextLimit: normalizeNonNegativeInteger(resolveProviderConfigValue(providerOptions?.contextLimit, defaultProvider, model, "contextLimit"), 0) || undefined,
|
|
339
|
+
inputLimit: normalizeNonNegativeInteger(resolveProviderConfigValue(providerOptions?.inputLimit, defaultProvider, model, "inputLimit"), 0) || undefined,
|
|
340
|
+
maxOutput: normalizeNonNegativeInteger(resolveProviderConfigValue(providerOptions?.maxOutput, defaultProvider, model, "maxOutput"), 0) || undefined,
|
|
193
341
|
};
|
|
194
342
|
}
|
|
195
343
|
function appendTextPart(parts, text) {
|
|
@@ -393,38 +541,6 @@ function contentPreview(content) {
|
|
|
393
541
|
.join(" ")
|
|
394
542
|
.trim();
|
|
395
543
|
}
|
|
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
544
|
/**
|
|
429
545
|
* 解析 ACode 工具权限配置。
|
|
430
546
|
* 主流程:运行时配置优先 -> `.acode/config.json` 次之 -> 兼容旧式 allowedTools/deniedTools 字段。
|
|
@@ -530,6 +646,16 @@ function coerceArguments(rawArguments) {
|
|
|
530
646
|
}
|
|
531
647
|
}
|
|
532
648
|
function getBuiltInToolTimeoutMs(tool, args, fallbackTimeoutMs) {
|
|
649
|
+
// 阻塞等待型工具不受超时限制:
|
|
650
|
+
// - ask_user_question: 阻塞等待用户回答
|
|
651
|
+
// - await_sub_agents: 永久阻塞等待指定子 agent 数组到达终态
|
|
652
|
+
const BLOCKING_BUILT_IN_TOOL_NAMES = new Set([
|
|
653
|
+
ASK_USER_QUESTION_TOOL_NAME,
|
|
654
|
+
"await_sub_agents",
|
|
655
|
+
]);
|
|
656
|
+
if (BLOCKING_BUILT_IN_TOOL_NAMES.has(tool.toolName)) {
|
|
657
|
+
return 0;
|
|
658
|
+
}
|
|
533
659
|
if (tool.toolName !== "Bash") {
|
|
534
660
|
return fallbackTimeoutMs;
|
|
535
661
|
}
|
|
@@ -594,12 +720,16 @@ function estimateMessageSizeBytes(message) {
|
|
|
594
720
|
}
|
|
595
721
|
return size;
|
|
596
722
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
723
|
+
/**
|
|
724
|
+
* 按字节大小截断对话,防止 HTTP 请求体过大。
|
|
725
|
+
* 这是与 token 压缩不同的关注点:即使 token 在上下文窗口内,
|
|
726
|
+
* HTTP 请求体也可能因 base64 图片等内容超过网关限制。
|
|
727
|
+
* 使用 compaction 模块的 performCompaction 进行压缩。
|
|
728
|
+
*
|
|
729
|
+
* ★ 支持 LLM 摘要器(与 performCompaction 一致):
|
|
730
|
+
* 若提供 llmSummarizer,则 summarize 阶段使用 LLM 生成语义化摘要。
|
|
731
|
+
*/
|
|
732
|
+
async function truncateConversationIfNeeded(conversation, maxBytes, contextLimit, maxOutput, llmSummarizer) {
|
|
603
733
|
let totalBytes = 0;
|
|
604
734
|
for (const message of conversation) {
|
|
605
735
|
totalBytes += estimateMessageSizeBytes(message);
|
|
@@ -607,52 +737,14 @@ function truncateConversationIfNeeded(conversation, maxBytes) {
|
|
|
607
737
|
if (totalBytes <= maxBytes) {
|
|
608
738
|
return 0;
|
|
609
739
|
}
|
|
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"),
|
|
740
|
+
const result = await performCompaction(conversation, contextLimit, maxOutput, {
|
|
741
|
+
llmSummarizer,
|
|
653
742
|
});
|
|
654
|
-
|
|
655
|
-
|
|
743
|
+
if (result.strategy !== "none") {
|
|
744
|
+
return result.prunedToolMessages + result.summarizedMessages;
|
|
745
|
+
}
|
|
746
|
+
// performCompaction 未生效时,强制 summarize
|
|
747
|
+
return summarizeConversation(conversation, { llmSummarizer, contextLimit });
|
|
656
748
|
}
|
|
657
749
|
function isContextOverflowProviderRequestError(error) {
|
|
658
750
|
const message = getErrorMessage(error).toLowerCase();
|
|
@@ -742,10 +834,10 @@ function buildSkillPrompt(skills) {
|
|
|
742
834
|
return "";
|
|
743
835
|
}
|
|
744
836
|
return [
|
|
745
|
-
"
|
|
746
|
-
|
|
747
|
-
"
|
|
748
|
-
].join("
|
|
837
|
+
"Skills are available for this session.",
|
|
838
|
+
"Use the `skill` tool to load specialized skill instructions when a user task matches one of the available skills listed in the tool description.",
|
|
839
|
+
"Do not attempt to follow skill instructions from memory — always invoke the `skill` tool first to load the full content.",
|
|
840
|
+
].join(" ");
|
|
749
841
|
}
|
|
750
842
|
function buildCommandPrompt(commands) {
|
|
751
843
|
if (commands.length === 0) {
|
|
@@ -757,6 +849,49 @@ function buildCommandPrompt(commands) {
|
|
|
757
849
|
"When the user invokes a slash command, ACode expands it before model execution.",
|
|
758
850
|
].join("\n");
|
|
759
851
|
}
|
|
852
|
+
/**
|
|
853
|
+
* 格式化 profile 信息为文本,供 rolePrompt 拼接使用。
|
|
854
|
+
* 主流程:提取关键字段并格式化为可读文本,返回空字符串当 profile 为空。
|
|
855
|
+
*/
|
|
856
|
+
function buildProfilePrompt(profile) {
|
|
857
|
+
if (!profile || typeof profile !== "object") {
|
|
858
|
+
return "";
|
|
859
|
+
}
|
|
860
|
+
const p = profile;
|
|
861
|
+
const parts = [];
|
|
862
|
+
const displayName = p.displayName;
|
|
863
|
+
const roleName = p.roleName;
|
|
864
|
+
const instanceName = p.instanceName;
|
|
865
|
+
const projectName = p.projectName;
|
|
866
|
+
const workspacePath = p.workspacePath;
|
|
867
|
+
const runtimeMode = p.runtimeMode;
|
|
868
|
+
const describe = p.describe;
|
|
869
|
+
if (typeof displayName === "string" && displayName.trim()) {
|
|
870
|
+
parts.push(`Display Name: ${displayName.trim()}`);
|
|
871
|
+
}
|
|
872
|
+
if (typeof roleName === "string" && roleName.trim()) {
|
|
873
|
+
parts.push(`Role: ${roleName.trim()}`);
|
|
874
|
+
}
|
|
875
|
+
if (typeof instanceName === "string" && instanceName.trim()) {
|
|
876
|
+
parts.push(`Instance: ${instanceName.trim()}`);
|
|
877
|
+
}
|
|
878
|
+
if (typeof projectName === "string" && projectName.trim()) {
|
|
879
|
+
parts.push(`Project: ${projectName.trim()}`);
|
|
880
|
+
}
|
|
881
|
+
if (typeof workspacePath === "string" && workspacePath.trim()) {
|
|
882
|
+
parts.push(`Workspace: ${workspacePath.trim()}`);
|
|
883
|
+
}
|
|
884
|
+
if (typeof runtimeMode === "string" && runtimeMode.trim()) {
|
|
885
|
+
parts.push(`Runtime Mode: ${runtimeMode.trim()}`);
|
|
886
|
+
}
|
|
887
|
+
if (typeof describe === "string" && describe.trim()) {
|
|
888
|
+
parts.push(`Description: ${describe.trim()}`);
|
|
889
|
+
}
|
|
890
|
+
if (parts.length === 0) {
|
|
891
|
+
return "";
|
|
892
|
+
}
|
|
893
|
+
return ["[User Profile]", ...parts].join("\n");
|
|
894
|
+
}
|
|
760
895
|
function toOpenAiTools(tools) {
|
|
761
896
|
return tools.map((tool) => ({
|
|
762
897
|
type: "function",
|
|
@@ -843,6 +978,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
843
978
|
model: "acode-default",
|
|
844
979
|
stream: false,
|
|
845
980
|
requestTimeoutMs: DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS,
|
|
981
|
+
requestTotalTimeoutMs: DEFAULT_PROVIDER_REQUEST_TOTAL_TIMEOUT_MS,
|
|
846
982
|
requestRetryCount: DEFAULT_PROVIDER_REQUEST_RETRY_COUNT,
|
|
847
983
|
mcpToolTimeoutMs: DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS,
|
|
848
984
|
mcpPollingToolTimeoutMs: DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS,
|
|
@@ -850,18 +986,41 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
850
986
|
},
|
|
851
987
|
permissions: { mode: "default", allow: [], deny: [] },
|
|
852
988
|
mcpConnections: [],
|
|
989
|
+
ownsMcpConnections: true,
|
|
853
990
|
exposedTools: [],
|
|
854
991
|
commands: [],
|
|
855
992
|
commandRegistry: new ACodeCommandRegistry([]),
|
|
856
993
|
conversation: [],
|
|
994
|
+
accumulatedInputTokens: 0,
|
|
995
|
+
accumulatedOutputTokens: 0,
|
|
857
996
|
};
|
|
858
997
|
this.abortController = null;
|
|
998
|
+
/**
|
|
999
|
+
* 正在处理中的 memory 子 agent 跟踪表。
|
|
1000
|
+
* key = memory URI,value = subId(用于防止同一 memory 被重复委托)。
|
|
1001
|
+
* 子 agent 完成后通过 SubAgentManager 事件回调移除对应 URI。
|
|
1002
|
+
*/
|
|
1003
|
+
this.pendingMemorySubAgents = new Map();
|
|
859
1004
|
this.submitQueue = Promise.resolve();
|
|
860
1005
|
this.activeTurn = false;
|
|
861
1006
|
this.abortRequested = false;
|
|
862
1007
|
this.sessionCompleteEmitted = false;
|
|
863
1008
|
this.contextVersion = 0;
|
|
864
1009
|
this.stopWaiters = new Set();
|
|
1010
|
+
/** 上下文压缩调试定时器(每 5 秒输出一次) */
|
|
1011
|
+
this.compactionDebugTimer = null;
|
|
1012
|
+
/** 可动态刷新的 SkillTool 引用(含 refresh 方法),运行中变更时调用其 refresh */
|
|
1013
|
+
this.skillTool = null;
|
|
1014
|
+
/** 监听 skills 目录变更的 FSWatcher 列表 */
|
|
1015
|
+
this.skillWatchers = [];
|
|
1016
|
+
/** skill 目录变更后的防抖定时器,避免短时间多次触发重扫 */
|
|
1017
|
+
this.skillRefreshTimer = null;
|
|
1018
|
+
/**
|
|
1019
|
+
* ask_user_question 工具的挂起等待队列。
|
|
1020
|
+
* key: toolCallId;value: resolve/reject 回调 + 创建时的 contextVersion。
|
|
1021
|
+
* submitQuestionAnswer() 调用时 resolve;abort/stop/supersede 时 reject。
|
|
1022
|
+
*/
|
|
1023
|
+
this.questionWaiters = new Map();
|
|
865
1024
|
this.hookBus = new ACodeHookBus({
|
|
866
1025
|
sessionId: config.sessionId,
|
|
867
1026
|
onError: (event, error) => {
|
|
@@ -874,6 +1033,10 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
874
1033
|
this.hookBus.register(handler);
|
|
875
1034
|
}
|
|
876
1035
|
}
|
|
1036
|
+
/** 获取当前已暴露的工具名称列表(供 SubAgentManager 记录) */
|
|
1037
|
+
getAvailableTools() {
|
|
1038
|
+
return this.state.exposedTools.map((tool) => tool.exposedName);
|
|
1039
|
+
}
|
|
877
1040
|
emitAcodeEvent(type, data) {
|
|
878
1041
|
const event = {
|
|
879
1042
|
type,
|
|
@@ -883,6 +1046,44 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
883
1046
|
};
|
|
884
1047
|
this.emit("event", event);
|
|
885
1048
|
}
|
|
1049
|
+
/**
|
|
1050
|
+
* 发射压缩子事件(phase 事件)到面板时间线。
|
|
1051
|
+
* 子事件通过 parentId 关联到父压缩事件,在前端以嵌套子元素形式展示。
|
|
1052
|
+
*
|
|
1053
|
+
* @param compactionId 父压缩事件 ID
|
|
1054
|
+
* @param phase 阶段标识(detect/prune/prune_done/summarize/summarize_done)
|
|
1055
|
+
* @param phaseLabel 阶段显示标签(如"阶段1: 裁剪工具输出")
|
|
1056
|
+
* @param phaseDetail 阶段详情(如"裁剪了 5 条工具输出")
|
|
1057
|
+
*/
|
|
1058
|
+
emitCompactionPhase(compactionId, phase, phaseLabel, phaseDetail) {
|
|
1059
|
+
this.emitAcodeEvent("context_compaction", {
|
|
1060
|
+
action: "phase",
|
|
1061
|
+
compactionId,
|
|
1062
|
+
parentId: compactionId,
|
|
1063
|
+
phase,
|
|
1064
|
+
phaseLabel,
|
|
1065
|
+
phaseDetail,
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* performCompaction 的 onPhase 回调处理:将 CompactionPhaseEvent 转换为面板子事件。
|
|
1070
|
+
*/
|
|
1071
|
+
handleCompactionPhase(compactionId, event) {
|
|
1072
|
+
switch (event.phase) {
|
|
1073
|
+
case "prune_start":
|
|
1074
|
+
this.emitCompactionPhase(compactionId, "prune", "阶段1: 裁剪工具输出", `正在裁剪 ${event.messageCount ?? 0} 条消息中的工具输出...`);
|
|
1075
|
+
break;
|
|
1076
|
+
case "prune_end":
|
|
1077
|
+
this.emitCompactionPhase(compactionId, "prune_done", "阶段1: 裁剪工具输出", `裁剪了 ${event.prunedCount ?? 0} 条工具输出, 释放 ${event.freedTokens ?? 0} tokens`);
|
|
1078
|
+
break;
|
|
1079
|
+
case "summarize_start":
|
|
1080
|
+
this.emitCompactionPhase(compactionId, "summarize", "阶段2: 摘要化对话", `正在摘要 ${event.messageCount ?? 0} 条早期对话...`);
|
|
1081
|
+
break;
|
|
1082
|
+
case "summarize_end":
|
|
1083
|
+
this.emitCompactionPhase(compactionId, "summarize_done", "阶段2: 摘要化对话", `摘要化了 ${event.summarizedCount ?? 0} 条消息${event.usedLLM ? " (LLM 摘要)" : " (规则摘要)"}`);
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
886
1087
|
/**
|
|
887
1088
|
* 启动 ACode headless 会话。
|
|
888
1089
|
* 主流程:加载 `.acode/config.json` 与 skills 目录 -> 发出 MCP/Skill 状态 ->
|
|
@@ -890,20 +1091,48 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
890
1091
|
*/
|
|
891
1092
|
async start() {
|
|
892
1093
|
const acodeConfig = await loadAcodeConfig(this.config.configPath);
|
|
893
|
-
|
|
1094
|
+
// Skills 来源(按优先级,后加载的覆盖先加载的同名 skill):
|
|
1095
|
+
// 1. 全局目录(~/.acode/skills,由 config.skillsDir 指定)
|
|
1096
|
+
// 2. 项目目录(<workingDirectory>/.acode/skills)
|
|
1097
|
+
// 3. 项目目录(<workingDirectory>/.agentswork/skills)
|
|
1098
|
+
// 各目录的 skills 合并后供 SkillTool / prompt 使用;分组信息供前端按目录展示。
|
|
1099
|
+
// 即使目录不存在也会作为空节点返回,前端按目录展示。
|
|
1100
|
+
const { skills, skillGroups, watchedDirs } = await this.loadAllSkills();
|
|
894
1101
|
const provider = resolveProviderConfig(this.config, acodeConfig);
|
|
895
1102
|
// [DEBUG] ACode Runtime 最终解析的 provider 配置(写入 stdout,bridge 会捕获)
|
|
896
|
-
|
|
1103
|
+
runtimeDebugLog(`[ACodeRuntime] ★★★ DEBUG ★★★ resolveProviderConfig 结果: config.model=${this.config.model ?? "(undefined)"}, ` +
|
|
897
1104
|
`resolvedModel=${provider.model}, baseURL=${provider.baseURL ?? "(none)"}, apiKey=${provider.apiKey ? "[set]" : "(none)"}, ` +
|
|
898
1105
|
`configPath=${this.config.configPath ?? "(none)"}`);
|
|
899
1106
|
const permissions = resolvePermissionConfig(this.config, acodeConfig);
|
|
900
|
-
|
|
1107
|
+
// 子 agent 复用父 agent 的 MCP 连接,避免启动新 MCP server 进程后 401
|
|
1108
|
+
// 共享连接时只暴露 memory 工具,防止子 agent 调用 poll_message/send_group 等
|
|
1109
|
+
// 与父 agent 冲突的工具(原设计通过 AWS_SUB_AGENT=true 限制,共享连接绕过了该机制)
|
|
1110
|
+
const mcpConnections = this.config.sharedMcpConnections
|
|
1111
|
+
? this.filterSharedMcpConnectionsForSubAgent(this.config.sharedMcpConnections)
|
|
1112
|
+
: await this.connectMcpServers(acodeConfig.mcpServers ?? {}, provider.mcpToolTimeoutMs);
|
|
1113
|
+
const ownsMcpConnections = !this.config.sharedMcpConnections;
|
|
1114
|
+
// 获取用户 profile 信息并拼接在 rolePrompt 下方
|
|
1115
|
+
const profilePrompt = await this.fetchProfilePrompt(mcpConnections);
|
|
901
1116
|
const agentId = this.config.agentId || "unknown";
|
|
902
1117
|
// 重启时清理旧 spillover 输出
|
|
903
1118
|
void cleanupAcodeCmdDist(agentId);
|
|
904
1119
|
const builtInTools = createBuiltInFileTools(this.config.workingDirectory, agentId);
|
|
1120
|
+
const askUserQuestionTool = createAskUserQuestionTool(this.buildAskUserQuestionDeps(), () => this.contextVersion);
|
|
1121
|
+
const skillTool = createSkillTool(skills);
|
|
1122
|
+
this.skillTool = skillTool;
|
|
1123
|
+
const todoWriteTool = createTodoWriteTool(agentId);
|
|
1124
|
+
const todoReadTool = createTodoReadTool(agentId);
|
|
1125
|
+
// 注册 sub-agent 工具集(仅当注入了 subAgentManager)
|
|
1126
|
+
const subAgentTools = this.config.subAgentManager
|
|
1127
|
+
? createSubAgentTools(this.buildSubAgentToolDeps(this.config.subAgentManager))
|
|
1128
|
+
: [];
|
|
905
1129
|
const exposedTools = [
|
|
906
1130
|
...builtInTools,
|
|
1131
|
+
askUserQuestionTool,
|
|
1132
|
+
skillTool,
|
|
1133
|
+
todoWriteTool,
|
|
1134
|
+
todoReadTool,
|
|
1135
|
+
...subAgentTools,
|
|
907
1136
|
...mcpConnections.flatMap((connection) => connection.tools),
|
|
908
1137
|
];
|
|
909
1138
|
const commandDirs = this.resolveCommandDirs(acodeConfig);
|
|
@@ -922,9 +1151,14 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
922
1151
|
.join("\n\n");
|
|
923
1152
|
// 辅助函数:将非空文本构造为 system role 消息,消除重复的 spread+ternary 模式
|
|
924
1153
|
const makeSystem = (content) => content?.trim() ? [{ role: 'system', content: content.trim() }] : [];
|
|
1154
|
+
// 将 rolePrompt 与 profilePrompt 合并为第二条 system 消息
|
|
1155
|
+
const roleWithProfile = [this.config.rolePrompt, profilePrompt]
|
|
1156
|
+
.map((item) => item?.trim())
|
|
1157
|
+
.filter((item) => Boolean(item))
|
|
1158
|
+
.join("\n\n");
|
|
925
1159
|
const initialConversation = [
|
|
926
1160
|
...makeSystem(this.config.globalPrompt),
|
|
927
|
-
...makeSystem(
|
|
1161
|
+
...makeSystem(roleWithProfile),
|
|
928
1162
|
...makeSystem(this.config.memory),
|
|
929
1163
|
...(runtimePrompt ? [{ role: 'system', content: runtimePrompt }] : []),
|
|
930
1164
|
];
|
|
@@ -941,21 +1175,35 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
941
1175
|
provider,
|
|
942
1176
|
permissions,
|
|
943
1177
|
mcpConnections,
|
|
1178
|
+
ownsMcpConnections,
|
|
944
1179
|
exposedTools,
|
|
945
1180
|
commands,
|
|
946
1181
|
commandRegistry,
|
|
947
1182
|
conversation: cloneChatConversation(initialConversation),
|
|
1183
|
+
accumulatedInputTokens: 0,
|
|
1184
|
+
accumulatedOutputTokens: 0,
|
|
948
1185
|
};
|
|
949
1186
|
await this.hookBus.dispatch("SessionStart", {
|
|
950
1187
|
workingDirectory: this.config.workingDirectory,
|
|
951
1188
|
model: provider.model,
|
|
952
1189
|
commandCount: commands.length,
|
|
953
1190
|
});
|
|
1191
|
+
// ★ emit 实际用于触发压缩的配置,前端 AgentContextPanel 直接显示此阈值,
|
|
1192
|
+
// 消除前端自算公式与后端实际触发阈值(min(动态比例, hardLimit-reserved))的脱节。
|
|
1193
|
+
// 注意:此处的 contextLimit/maxOutput/inputLimit 来自 resolveProviderConfig(读 provider.default.* 三级),
|
|
1194
|
+
// 与 adapter 的 totalUsage.contextLimit(读顶层+查表)可能不同源——前端应以这里 emit 的值为准。
|
|
1195
|
+
const compactionConfig = resolveCompactionConfig(provider);
|
|
954
1196
|
this.emitAcodeEvent("session_started", {
|
|
955
1197
|
workingDirectory: this.config.workingDirectory,
|
|
956
1198
|
model: provider.model,
|
|
957
1199
|
commandCount: commands.length,
|
|
958
1200
|
estimatedContextTokens: estimateContextTokens(systemMessagesPlainText, exposedTools),
|
|
1201
|
+
contextLimit: compactionConfig.contextLimit,
|
|
1202
|
+
maxOutput: compactionConfig.maxOutput,
|
|
1203
|
+
...(compactionConfig.inputLimit !== undefined
|
|
1204
|
+
? { inputLimit: compactionConfig.inputLimit }
|
|
1205
|
+
: {}),
|
|
1206
|
+
compactionThreshold: compactionConfig.threshold,
|
|
959
1207
|
});
|
|
960
1208
|
this.emitAcodeEvent("mcp_status", {
|
|
961
1209
|
configPath: this.config.configPath,
|
|
@@ -972,6 +1220,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
972
1220
|
source: tool.kind === "mcp"
|
|
973
1221
|
? `${tool.serverName}.${tool.toolName}`
|
|
974
1222
|
: `builtin.${tool.toolName}`,
|
|
1223
|
+
description: tool.description || "",
|
|
975
1224
|
})),
|
|
976
1225
|
});
|
|
977
1226
|
this.emitAcodeEvent("skill_status", {
|
|
@@ -980,6 +1229,16 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
980
1229
|
skills: skills.map((skill) => ({
|
|
981
1230
|
name: skill.name,
|
|
982
1231
|
description: skill.description,
|
|
1232
|
+
directory: skill.directory,
|
|
1233
|
+
})),
|
|
1234
|
+
skillGroups: skillGroups.map((group) => ({
|
|
1235
|
+
dir: group.dir,
|
|
1236
|
+
exists: group.exists,
|
|
1237
|
+
skills: group.skills.map((skill) => ({
|
|
1238
|
+
name: skill.name,
|
|
1239
|
+
description: skill.description,
|
|
1240
|
+
directory: skill.directory,
|
|
1241
|
+
})),
|
|
983
1242
|
})),
|
|
984
1243
|
});
|
|
985
1244
|
this.emitAcodeEvent("command_status", {
|
|
@@ -994,6 +1253,226 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
994
1253
|
if (this.config.initialPrompt) {
|
|
995
1254
|
await this.submit(this.config.initialPrompt);
|
|
996
1255
|
}
|
|
1256
|
+
// 启动上下文压缩调试日志(仅当 ACODE_COMPACTION_DEBUG=1 时启用)
|
|
1257
|
+
this.startCompactionDebugLog();
|
|
1258
|
+
// 启动 skills 目录监听,运行中变更会触发重扫并刷新 SkillTool + 重发 skill_status
|
|
1259
|
+
this.startSkillWatchers(watchedDirs);
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* 加载所有 skills 来源目录并合并。
|
|
1263
|
+
*
|
|
1264
|
+
* 三个来源(按优先级,后加载的覆盖先加载的同名 skill):
|
|
1265
|
+
* 1. 全局目录(~/.acode/skills,由 config.skillsDir 指定)
|
|
1266
|
+
* 2. 项目目录(<workingDirectory>/.acode/skills)
|
|
1267
|
+
* 3. 项目目录(<workingDirectory>/.agentswork/skills)
|
|
1268
|
+
*
|
|
1269
|
+
* 返回合并后的 skills 列表、按目录分组的 skillGroups,以及需要监听的目录列表
|
|
1270
|
+
* (已去重、规范化)。watchedDirs 用于启动 fs.watch 监听运行时变更。
|
|
1271
|
+
*/
|
|
1272
|
+
async loadAllSkills() {
|
|
1273
|
+
const globalSkillsDir = this.config.skillsDir ?? "";
|
|
1274
|
+
const projectAcodeSkillsDir = path.join(this.config.workingDirectory, ".acode", "skills");
|
|
1275
|
+
const projectAgentsworkSkillsDir = path.join(this.config.workingDirectory, ".agentswork", "skills");
|
|
1276
|
+
// 按规范化路径去重,避免全局目录与项目目录路径相同时重复加载
|
|
1277
|
+
const seenDirs = new Set();
|
|
1278
|
+
const skillGroups = [];
|
|
1279
|
+
const watchedDirs = [];
|
|
1280
|
+
for (const dir of [globalSkillsDir, projectAcodeSkillsDir, projectAgentsworkSkillsDir]) {
|
|
1281
|
+
if (!dir)
|
|
1282
|
+
continue;
|
|
1283
|
+
const normalized = path.resolve(dir);
|
|
1284
|
+
if (seenDirs.has(normalized))
|
|
1285
|
+
continue;
|
|
1286
|
+
seenDirs.add(normalized);
|
|
1287
|
+
skillGroups.push(await loadAcodeSkillGroup(dir));
|
|
1288
|
+
// 即使目录不存在也加入监听列表:fs.watch 会失败但被静默忽略,
|
|
1289
|
+
// 这样用户后续创建该目录时(如果父目录存在)能通过其他途径感知。
|
|
1290
|
+
// 实际上 fs.watch 不存在的路径会抛错,所以这里只加入存在的目录。
|
|
1291
|
+
try {
|
|
1292
|
+
await fs.promises.access(normalized);
|
|
1293
|
+
watchedDirs.push(normalized);
|
|
1294
|
+
}
|
|
1295
|
+
catch {
|
|
1296
|
+
// 目录不存在,跳过监听
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
// 合并各目录 skills,按 name 去重(项目优先覆盖全局)
|
|
1300
|
+
const skillsByName = new Map();
|
|
1301
|
+
for (const group of skillGroups) {
|
|
1302
|
+
for (const skill of group.skills) {
|
|
1303
|
+
skillsByName.set(skill.name, skill);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
const skills = [...skillsByName.values()];
|
|
1307
|
+
return { skills, skillGroups, watchedDirs };
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* 启动 fs.watch 监听 skills 目录变更。
|
|
1311
|
+
*
|
|
1312
|
+
* 使用 recursive: true(Windows/macOS 原生支持,Linux 在 Node 19+ 支持)。
|
|
1313
|
+
* Linux 旧版本不支持时会抛 ERR_FEATURE_UNAVAILABLE,fallback 到非 recursive
|
|
1314
|
+
* 监听顶层目录(只能感知 skill 子目录的新增/删除,无法感知 SKILL.md 内容修改)。
|
|
1315
|
+
*
|
|
1316
|
+
* 变更事件经防抖后触发 refreshSkills(),重新扫描目录、刷新 SkillTool、重发 skill_status。
|
|
1317
|
+
*/
|
|
1318
|
+
startSkillWatchers(dirs) {
|
|
1319
|
+
for (const dir of dirs) {
|
|
1320
|
+
let watcher = null;
|
|
1321
|
+
try {
|
|
1322
|
+
watcher = fs.watch(dir, { recursive: true }, () => {
|
|
1323
|
+
this.scheduleSkillRefresh();
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
catch {
|
|
1327
|
+
// recursive 在当前平台不支持(如 Linux 旧版本),fallback 到非 recursive
|
|
1328
|
+
try {
|
|
1329
|
+
watcher = fs.watch(dir, () => {
|
|
1330
|
+
this.scheduleSkillRefresh();
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
catch {
|
|
1334
|
+
// 监听失败(如权限问题),跳过该目录
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (watcher) {
|
|
1339
|
+
watcher.on("error", () => {
|
|
1340
|
+
// 静默处理 watcher 错误(如目录被删除),避免未捕获异常
|
|
1341
|
+
});
|
|
1342
|
+
this.skillWatchers.push(watcher);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* 防抖调度 skill 重扫。短时间内多次变更事件只触发一次重扫。
|
|
1348
|
+
*/
|
|
1349
|
+
scheduleSkillRefresh() {
|
|
1350
|
+
if (this.skillRefreshTimer) {
|
|
1351
|
+
clearTimeout(this.skillRefreshTimer);
|
|
1352
|
+
}
|
|
1353
|
+
this.skillRefreshTimer = setTimeout(() => {
|
|
1354
|
+
this.skillRefreshTimer = null;
|
|
1355
|
+
void this.refreshSkills();
|
|
1356
|
+
}, ACodeRuntime.SKILL_REFRESH_DEBOUNCE_MS);
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* 重新扫描 skills 目录并刷新 SkillTool 与 state。
|
|
1360
|
+
*
|
|
1361
|
+
* 调用时机:fs.watch 监听到目录变更后(经防抖)。
|
|
1362
|
+
* 副作用:
|
|
1363
|
+
* - 更新 this.skillTool 的内部 Map 与工具描述(让下一轮 turn 的 LLM 感知新 skill)
|
|
1364
|
+
* - 更新 this.state.skills(影响 fallback 响应等)
|
|
1365
|
+
* - 重发 skill_status 事件(让 DevTools 面板刷新"能力"标签页)
|
|
1366
|
+
*/
|
|
1367
|
+
async refreshSkills() {
|
|
1368
|
+
if (!this.state.started || this.state.terminated)
|
|
1369
|
+
return;
|
|
1370
|
+
try {
|
|
1371
|
+
const { skills, skillGroups } = await this.loadAllSkills();
|
|
1372
|
+
if (this.skillTool) {
|
|
1373
|
+
this.skillTool.refresh(skills);
|
|
1374
|
+
}
|
|
1375
|
+
this.state.skills = skills;
|
|
1376
|
+
this.emitAcodeEvent("skill_status", {
|
|
1377
|
+
skillsDir: this.config.skillsDir,
|
|
1378
|
+
installedCount: skills.length,
|
|
1379
|
+
skills: skills.map((skill) => ({
|
|
1380
|
+
name: skill.name,
|
|
1381
|
+
description: skill.description,
|
|
1382
|
+
directory: skill.directory,
|
|
1383
|
+
})),
|
|
1384
|
+
skillGroups: skillGroups.map((group) => ({
|
|
1385
|
+
dir: group.dir,
|
|
1386
|
+
exists: group.exists,
|
|
1387
|
+
skills: group.skills.map((skill) => ({
|
|
1388
|
+
name: skill.name,
|
|
1389
|
+
description: skill.description,
|
|
1390
|
+
directory: skill.directory,
|
|
1391
|
+
})),
|
|
1392
|
+
})),
|
|
1393
|
+
});
|
|
1394
|
+
runtimeDebugLog(`[skill-watch] Skills refreshed: ${skills.length} skills ` +
|
|
1395
|
+
`(${skills.map((s) => s.name).join(", ") || "none"})`, { sessionId: this.config.sessionId });
|
|
1396
|
+
}
|
|
1397
|
+
catch (error) {
|
|
1398
|
+
console.error(`[skill-watch] Failed to refresh skills:`, error instanceof Error ? error.message : error);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* 关闭所有 skills 目录监听器与防抖定时器。在 stop() 中调用。
|
|
1403
|
+
*/
|
|
1404
|
+
stopSkillWatchers() {
|
|
1405
|
+
if (this.skillRefreshTimer) {
|
|
1406
|
+
clearTimeout(this.skillRefreshTimer);
|
|
1407
|
+
this.skillRefreshTimer = null;
|
|
1408
|
+
}
|
|
1409
|
+
for (const watcher of this.skillWatchers) {
|
|
1410
|
+
try {
|
|
1411
|
+
watcher.close();
|
|
1412
|
+
}
|
|
1413
|
+
catch {
|
|
1414
|
+
// 忽略关闭错误
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
this.skillWatchers = [];
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* 启动上下文压缩调试日志,每 5 秒写入系统临时目录下的 acode-compaction-debug.log。
|
|
1421
|
+
* 仅当环境变量 ACODE_COMPACTION_DEBUG=1 时启用,使用异步写入避免阻塞事件循环。
|
|
1422
|
+
*/
|
|
1423
|
+
startCompactionDebugLog() {
|
|
1424
|
+
if (!COMPACTION_DEBUG_ENABLED)
|
|
1425
|
+
return;
|
|
1426
|
+
if (this.compactionDebugTimer)
|
|
1427
|
+
return;
|
|
1428
|
+
const provider = this.state.provider;
|
|
1429
|
+
const { contextLimit, maxOutput, inputLimit, threshold: triggerThreshold } = resolveCompactionConfig(provider);
|
|
1430
|
+
this.compactionDebugTimer = setInterval(() => {
|
|
1431
|
+
try {
|
|
1432
|
+
const actualUsed = this.state.accumulatedInputTokens + this.state.accumulatedOutputTokens;
|
|
1433
|
+
const estimatedUsed = estimateConversationTokens(this.state.conversation)
|
|
1434
|
+
+ estimateToolsTokens(this.state.exposedTools);
|
|
1435
|
+
const effectiveUsed = Math.max(actualUsed, estimatedUsed);
|
|
1436
|
+
const sizeKB = conversationSizeKB(this.state.conversation);
|
|
1437
|
+
const isRunning = this.state.started && !this.state.terminated;
|
|
1438
|
+
const lines = [
|
|
1439
|
+
`[${new Date().toISOString()}]`,
|
|
1440
|
+
`started=${this.state.started}`,
|
|
1441
|
+
`terminated=${this.state.terminated}`,
|
|
1442
|
+
`activeTurn=${this.activeTurn}`,
|
|
1443
|
+
`conversationSize=${this.state.conversation.length}msgs`,
|
|
1444
|
+
`sizeKB=${sizeKB}KB`,
|
|
1445
|
+
`estimation=${estimatedUsed} tokens`,
|
|
1446
|
+
`actualInput=${this.state.accumulatedInputTokens}`,
|
|
1447
|
+
`actualOutput=${this.state.accumulatedOutputTokens}`,
|
|
1448
|
+
`actualUsed=${actualUsed}`,
|
|
1449
|
+
`contextLimit=${contextLimit}`,
|
|
1450
|
+
`inputLimit=${inputLimit ?? "n/a"}`,
|
|
1451
|
+
`maxOutput=${maxOutput}`,
|
|
1452
|
+
`triggerThreshold=${triggerThreshold}`,
|
|
1453
|
+
`remainingToTrigger=${Math.max(0, triggerThreshold - actualUsed)}`,
|
|
1454
|
+
`isRunning=${isRunning}`,
|
|
1455
|
+
`shouldCompact=${isRunning ? effectiveUsed >= triggerThreshold : false}`,
|
|
1456
|
+
`---`,
|
|
1457
|
+
].join(" | ");
|
|
1458
|
+
// 异步写入,避免阻塞事件循环
|
|
1459
|
+
fs.promises.appendFile(COMPACTION_DEBUG_LOG, lines + "\n", "utf-8").catch(() => {
|
|
1460
|
+
// 忽略日志写入错误
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
catch {
|
|
1464
|
+
// 忽略日志写入错误
|
|
1465
|
+
}
|
|
1466
|
+
}, 5000);
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* 停止上下文压缩调试日志。
|
|
1470
|
+
*/
|
|
1471
|
+
stopCompactionDebugLog() {
|
|
1472
|
+
if (this.compactionDebugTimer) {
|
|
1473
|
+
clearInterval(this.compactionDebugTimer);
|
|
1474
|
+
this.compactionDebugTimer = null;
|
|
1475
|
+
}
|
|
997
1476
|
}
|
|
998
1477
|
/**
|
|
999
1478
|
* 处理一轮用户输入。
|
|
@@ -1002,8 +1481,17 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1002
1481
|
*/
|
|
1003
1482
|
async submit(message) {
|
|
1004
1483
|
const contextVersion = this.contextVersion;
|
|
1005
|
-
|
|
1006
|
-
|
|
1484
|
+
runtimeDebugLog(`[ACodeRuntime] 📬 submit() queued: contextVersion=${contextVersion}, ` +
|
|
1485
|
+
`activeTurn=${this.activeTurn}, abortRequested=${this.abortRequested}, ` +
|
|
1486
|
+
`terminated=${this.state.terminated}, message.length=${message.length}`);
|
|
1487
|
+
const queued = this.submitQueue.then(() => {
|
|
1488
|
+
runtimeDebugLog(`[ACodeRuntime] ▶️ executeSubmit dequeued: contextVersion=${contextVersion}`);
|
|
1489
|
+
return this.executeSubmit(message, contextVersion);
|
|
1490
|
+
});
|
|
1491
|
+
this.submitQueue = queued.catch(() => {
|
|
1492
|
+
runtimeDebugLog(`[ACodeRuntime] 📬 submit() queued item rejected (catch swallowed)`);
|
|
1493
|
+
});
|
|
1494
|
+
runtimeDebugLog(`[ACodeRuntime] 📬 submit() returning queued promise`);
|
|
1007
1495
|
return queued;
|
|
1008
1496
|
}
|
|
1009
1497
|
/**
|
|
@@ -1019,17 +1507,19 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1019
1507
|
this.abortController?.abort();
|
|
1020
1508
|
this.abortController = null;
|
|
1021
1509
|
this.state.conversation = cloneChatConversation(this.initialConversation);
|
|
1510
|
+
this.state.accumulatedInputTokens = 0;
|
|
1511
|
+
this.state.accumulatedOutputTokens = 0;
|
|
1022
1512
|
}
|
|
1023
1513
|
isTurnSuperseded(contextVersion) {
|
|
1024
1514
|
return contextVersion !== this.contextVersion;
|
|
1025
1515
|
}
|
|
1026
1516
|
async executeSubmit(message, contextVersion) {
|
|
1027
1517
|
const turnStartTime = Date.now();
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1518
|
+
runtimeDebugLog(`[ACodeRuntime] ══════════════════════════════════════════`);
|
|
1519
|
+
runtimeDebugLog(`[ACodeRuntime] ▶️ executeSubmit STARTED`);
|
|
1520
|
+
runtimeDebugLog(`[ACodeRuntime] Message: <redacted, length=${message.length}>`);
|
|
1521
|
+
runtimeDebugLog(`[ACodeRuntime] 🔢 Context version: ${contextVersion}`);
|
|
1522
|
+
runtimeDebugLog(`[ACodeRuntime] ═══════════════════════════════════════════`);
|
|
1033
1523
|
if (!this.state.started || this.state.terminated) {
|
|
1034
1524
|
console.error(`[ACodeRuntime] ❌ Runtime not active (started: ${this.state.started}, terminated: ${this.state.terminated})`);
|
|
1035
1525
|
throw new Error("ACode runtime is not active");
|
|
@@ -1039,14 +1529,14 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1039
1529
|
const originalPrompt = message.trim();
|
|
1040
1530
|
let turnCompleted = false;
|
|
1041
1531
|
try {
|
|
1042
|
-
|
|
1532
|
+
runtimeDebugLog(`[ACodeRuntime] 🔍 Dispatching UserPromptSubmit hook...`);
|
|
1043
1533
|
const promptHookStartTime = Date.now();
|
|
1044
1534
|
const promptHookResults = await this.hookBus.dispatch("UserPromptSubmit", { prompt: originalPrompt });
|
|
1045
1535
|
const promptHookDuration = Date.now() - promptHookStartTime;
|
|
1046
|
-
|
|
1536
|
+
runtimeDebugLog(`[ACodeRuntime] ✅ UserPromptSubmit hook completed in ${promptHookDuration}ms, ` +
|
|
1047
1537
|
`results: ${promptHookResults.length}`);
|
|
1048
1538
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1049
|
-
|
|
1539
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Turn superseded after hook, aborting`);
|
|
1050
1540
|
return;
|
|
1051
1541
|
}
|
|
1052
1542
|
const promptDeny = findDeniedHookResult(promptHookResults);
|
|
@@ -1057,15 +1547,15 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1057
1547
|
});
|
|
1058
1548
|
this.emitAcodeEvent("turn_complete", { failed: true });
|
|
1059
1549
|
turnCompleted = true;
|
|
1060
|
-
|
|
1550
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitted turn_complete (failed=true) due to hook deny`);
|
|
1061
1551
|
return;
|
|
1062
1552
|
}
|
|
1063
|
-
|
|
1553
|
+
runtimeDebugLog(`[ACodeRuntime] 🔍 Expanding command if any...`);
|
|
1064
1554
|
const expandedCommand = this.state.commandRegistry.expand(originalPrompt);
|
|
1065
1555
|
const trimmed = expandedCommand?.prompt.trim() || originalPrompt;
|
|
1066
1556
|
const commandAllowedTools = expandedCommand?.command.allowedTools;
|
|
1067
1557
|
if (expandedCommand) {
|
|
1068
|
-
|
|
1558
|
+
runtimeDebugLog(`[ACodeRuntime] 📋 Slash command detected: ${expandedCommand.command.name}`);
|
|
1069
1559
|
this.emitAcodeEvent("command_status", {
|
|
1070
1560
|
action: "slash_command",
|
|
1071
1561
|
command: expandedCommand.command.name,
|
|
@@ -1074,76 +1564,85 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1074
1564
|
});
|
|
1075
1565
|
}
|
|
1076
1566
|
if (!this.state.provider.baseURL || !this.state.provider.apiKey) {
|
|
1077
|
-
|
|
1567
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ No provider configured, using fallback response`);
|
|
1078
1568
|
const text = createFallbackResponse(trimmed, this.state);
|
|
1079
1569
|
this.emitAcodeEvent("assistant_delta", { text });
|
|
1080
1570
|
this.emitAcodeEvent("turn_complete", {
|
|
1081
1571
|
usage: { inputTokens: trimmed.length, outputTokens: text.length },
|
|
1082
1572
|
});
|
|
1083
1573
|
turnCompleted = true;
|
|
1084
|
-
|
|
1574
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitted turn_complete (fallback mode)`);
|
|
1085
1575
|
return;
|
|
1086
1576
|
}
|
|
1087
|
-
|
|
1577
|
+
runtimeDebugLog(`[ACodeRuntime] 📎 Parsing attachment message...`);
|
|
1088
1578
|
const parsedContent = parseAttachmentMessage(trimmed);
|
|
1089
1579
|
if (parsedContent.hasImages && !this.state.provider.supportsImageInput) {
|
|
1090
1580
|
console.error(`[ACodeRuntime] ❌ Image input not supported by current provider`);
|
|
1091
1581
|
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
1582
|
}
|
|
1093
|
-
|
|
1583
|
+
runtimeDebugLog(`[ACodeRuntime] 🖼️ Materializing image content if any...`);
|
|
1094
1584
|
const userContent = await materializeImageContent(parsedContent.content);
|
|
1095
1585
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1096
|
-
|
|
1586
|
+
runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded after materializing content, aborting`);
|
|
1097
1587
|
return;
|
|
1098
1588
|
}
|
|
1099
|
-
|
|
1589
|
+
runtimeDebugLog(`[ACodeRuntime] 💬 Adding user message to conversation`);
|
|
1100
1590
|
this.state.conversation.push({ role: "user", content: userContent });
|
|
1101
|
-
|
|
1102
|
-
|
|
1591
|
+
runtimeDebugLog(`[ACodeRuntime] Conversation size: ${this.state.conversation.length} messages`);
|
|
1592
|
+
runtimeDebugLog(`[ACodeRuntime] Starting runModelTurn...`);
|
|
1103
1593
|
const modelTurnStartTime = Date.now();
|
|
1104
1594
|
const result = await this.runModelTurn(commandAllowedTools, contextVersion);
|
|
1105
1595
|
const modelTurnDuration = Date.now() - modelTurnStartTime;
|
|
1106
|
-
|
|
1107
|
-
|
|
1596
|
+
runtimeDebugLog(`[ACodeRuntime] ✅ runModelTurn completed in ${modelTurnDuration}ms`);
|
|
1597
|
+
runtimeDebugLog(`[ACodeRuntime] 📝 Result: text=${result.text ? result.text.slice(0, 80) + (result.text.length > 80 ? '...' : '') : '(empty)'}, ` +
|
|
1108
1598
|
`finishReason=${result.finishReason || '(none)'}`);
|
|
1109
1599
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1110
|
-
|
|
1600
|
+
runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded after runModelTurn, aborting`);
|
|
1111
1601
|
return;
|
|
1112
1602
|
}
|
|
1113
1603
|
if (result.text && !this.state.provider.stream) {
|
|
1114
|
-
|
|
1604
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitting assistant_delta (non-streaming mode)`);
|
|
1115
1605
|
this.emitAcodeEvent("assistant_delta", { text: result.text });
|
|
1116
1606
|
}
|
|
1117
|
-
|
|
1607
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitting turn_complete...`);
|
|
1118
1608
|
this.emitAcodeEvent("turn_complete", {
|
|
1609
|
+
// ★ 使用 result.totalOutputTextLength 替代 result.text.length,
|
|
1610
|
+
// result.text 只保留最后一次迭代的文本,导致多轮迭代时 outputTokens 被严重低估
|
|
1119
1611
|
usage: result.usage ?? {
|
|
1120
1612
|
inputTokens: trimmed.length,
|
|
1121
|
-
outputTokens: result.text.length,
|
|
1613
|
+
outputTokens: result.totalOutputTextLength ?? result.text.length,
|
|
1122
1614
|
},
|
|
1123
1615
|
...(result.responseBody ? { responseBody: result.responseBody } : {}),
|
|
1124
1616
|
...(result.finishReason ? { finishReason: result.finishReason } : {}),
|
|
1125
1617
|
});
|
|
1618
|
+
// ★ token 用量已在 runModelTurn 中按迭代更新到 state(非累加),
|
|
1619
|
+
// 此处不再累加,避免 actualUsed 跨轮只增不减导致 shouldCompact 永远为 true。
|
|
1620
|
+
// finalUsage 用于 turn_complete 事件的 usage 上报(当前迭代的上下文用量,覆盖式)。
|
|
1621
|
+
runtimeDebugLog(`[ACodeRuntime] 💰 Current context usage: ` +
|
|
1622
|
+
`input=${this.state.accumulatedInputTokens}, ` +
|
|
1623
|
+
`output=${this.state.accumulatedOutputTokens}` +
|
|
1624
|
+
(result.usage ? '' : ' (estimated, no provider usage)'));
|
|
1126
1625
|
turnCompleted = true;
|
|
1127
1626
|
const totalDuration = Date.now() - turnStartTime;
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1627
|
+
runtimeDebugLog(`[ACodeRuntime] ═══════════════════════════════════════════`);
|
|
1628
|
+
runtimeDebugLog(`[ACodeRuntime] ✅ executeSubmit COMPLETED in ${totalDuration}ms`);
|
|
1629
|
+
runtimeDebugLog(`[ACodeRuntime] ══════════════════════════════════════════`);
|
|
1131
1630
|
}
|
|
1132
1631
|
catch (error) {
|
|
1133
1632
|
const errorDuration = Date.now() - turnStartTime;
|
|
1134
1633
|
console.error(`[ACodeRuntime] ❌ executeSubmit FAILED after ${errorDuration}ms:`, error instanceof Error ? error.message : String(error));
|
|
1135
1634
|
console.error(`[ACodeRuntime] 🔍 Error stack:`, error instanceof Error ? error.stack : '(no stack)');
|
|
1136
1635
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1137
|
-
|
|
1636
|
+
runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded in error handler, skipping recovery`);
|
|
1138
1637
|
return;
|
|
1139
1638
|
}
|
|
1140
1639
|
if (this.abortRequested) {
|
|
1141
|
-
|
|
1640
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Abort requested, emitting aborted turn_complete`);
|
|
1142
1641
|
this.emitAcodeEvent("turn_complete", { aborted: true });
|
|
1143
1642
|
turnCompleted = true;
|
|
1144
1643
|
return;
|
|
1145
1644
|
}
|
|
1146
|
-
|
|
1645
|
+
runtimeDebugLog(`[ACodeRuntime] Emitting error and failed turn_complete...`);
|
|
1147
1646
|
this.emitAcodeEvent("error", {
|
|
1148
1647
|
message: error instanceof Error ? error.message : String(error),
|
|
1149
1648
|
});
|
|
@@ -1152,30 +1651,186 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1152
1651
|
}
|
|
1153
1652
|
finally {
|
|
1154
1653
|
const finallyDuration = Date.now() - turnStartTime;
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
`abortRequested=${this.abortRequested}`
|
|
1654
|
+
runtimeDebugLog(`[ACodeRuntime] 🔚 executeSubmit finally block entered after ${finallyDuration}ms`);
|
|
1655
|
+
runtimeDebugLog(`[ACodeRuntime] 🔍 turnCompleted=${turnCompleted}, activeTurn=${this.activeTurn}, ` +
|
|
1656
|
+
`abortRequested=${this.abortRequested}, contextVersion=${contextVersion}, ` +
|
|
1657
|
+
`currentContextVersion=${this.contextVersion}`);
|
|
1158
1658
|
this.activeTurn = false;
|
|
1159
1659
|
this.abortRequested = false;
|
|
1160
1660
|
// 关键修复:如果异常路径没有发射 turn_complete,强制发射一个
|
|
1161
1661
|
// 这确保 adapter 层的 recoverResolvedTurnWithoutTerminalEvent 能够正确触发
|
|
1162
1662
|
if (!turnCompleted && !this.isTurnSuperseded(contextVersion)) {
|
|
1163
|
-
console.error(`[ACodeRuntime] 🚨 CRITICAL: executeSubmit finally block reached without turn_complete event
|
|
1164
|
-
|
|
1165
|
-
|
|
1663
|
+
console.error(`[ACodeRuntime] 🚨 CRITICAL: executeSubmit finally block reached without turn_complete event! ` +
|
|
1664
|
+
`Duration: ${finallyDuration}ms. ` +
|
|
1665
|
+
`This indicates a code path that should have emitted turn_complete but didn't. ` +
|
|
1666
|
+
`Forcing failed turn_complete to prevent state stuck.`);
|
|
1166
1667
|
this.emitAcodeEvent("error", {
|
|
1167
1668
|
message: "ACode turn completed without proper completion event (internal error)",
|
|
1168
1669
|
});
|
|
1169
1670
|
this.emitAcodeEvent("turn_complete", { failed: true });
|
|
1170
|
-
|
|
1671
|
+
runtimeDebugLog(`[ACodeRuntime] Emergency turn_complete (failed=true) emitted`);
|
|
1171
1672
|
}
|
|
1172
1673
|
}
|
|
1173
1674
|
}
|
|
1174
1675
|
/** 中止当前轮次;由执行轮次统一发出一个终态事件,避免 aborted/failed 双终态。 */
|
|
1175
1676
|
async abort() {
|
|
1677
|
+
const hadController = this.abortController !== null;
|
|
1678
|
+
const wasActiveTurn = this.activeTurn;
|
|
1176
1679
|
this.abortRequested = this.activeTurn;
|
|
1177
|
-
this.abortController
|
|
1680
|
+
if (this.abortController) {
|
|
1681
|
+
runtimeDebugLog(`[ACodeRuntime] ⛔ abort() calling abortController.abort() (activeTurn=${wasActiveTurn})`);
|
|
1682
|
+
this.abortController.abort();
|
|
1683
|
+
}
|
|
1684
|
+
else {
|
|
1685
|
+
console.warn(`[ACodeRuntime] ⛔ abort() called but abortController is null! ` +
|
|
1686
|
+
`activeTurn=${wasActiveTurn}, abortRequested will be set to ${wasActiveTurn}. ` +
|
|
1687
|
+
`This means abort() CANNOT cancel any running fetch/stream — ` +
|
|
1688
|
+
`if a built-in tool is executing, it will NOT be interrupted.`);
|
|
1689
|
+
}
|
|
1178
1690
|
this.abortController = null;
|
|
1691
|
+
// ask_user_question 等待用户回答期间被 abort,需 reject 所有 waiter
|
|
1692
|
+
this.rejectAllQuestionWaiters("aborted");
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* 构造 ask_user_question 工具的依赖注入。
|
|
1696
|
+
* onQuestion: 发出 question_request 事件,让 adapter 转发到前端。
|
|
1697
|
+
* waitForAnswer: 阻塞等待 submitQuestionAnswer() 调用 resolve,或 abort/stop/supersede 时 reject。
|
|
1698
|
+
*/
|
|
1699
|
+
buildAskUserQuestionDeps() {
|
|
1700
|
+
return {
|
|
1701
|
+
onQuestion: (toolCallId, questions) => {
|
|
1702
|
+
this.emitAcodeEvent("question_request", {
|
|
1703
|
+
toolCallId,
|
|
1704
|
+
questions: questions,
|
|
1705
|
+
});
|
|
1706
|
+
},
|
|
1707
|
+
waitForAnswer: (toolCallId, contextVersion) => {
|
|
1708
|
+
return new Promise((resolve, reject) => {
|
|
1709
|
+
this.questionWaiters.set(toolCallId, {
|
|
1710
|
+
resolve,
|
|
1711
|
+
reject,
|
|
1712
|
+
contextVersion,
|
|
1713
|
+
});
|
|
1714
|
+
});
|
|
1715
|
+
},
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* 构造 sub-agent 工具集依赖(注入 SubAgentManager 句柄)。
|
|
1720
|
+
*
|
|
1721
|
+
* 5 个工具的依赖:
|
|
1722
|
+
* - spawnSubAgent: 创建子 agent(不阻塞,不自动 start)
|
|
1723
|
+
* - listSubAgents: 列出当前 agent 的所有子 agent
|
|
1724
|
+
* - getSubAgentStatus: 查询某子 agent 实时状态
|
|
1725
|
+
* - awaitSubAgents: 阻塞等待指定子 agent 数组完成
|
|
1726
|
+
* - abortSubAgent: 终止某子 agent
|
|
1727
|
+
*/
|
|
1728
|
+
buildSubAgentToolDeps(manager) {
|
|
1729
|
+
const parentAgentId = this.config.agentId ?? "unknown";
|
|
1730
|
+
const parentSessionId = this.config.sessionId;
|
|
1731
|
+
const workingDirectory = this.config.workingDirectory;
|
|
1732
|
+
const parentConfig = this.config;
|
|
1733
|
+
return {
|
|
1734
|
+
// 非阻塞 spawn:创建后不等待,立即返回 subId
|
|
1735
|
+
spawnSubAgent: async (opts) => {
|
|
1736
|
+
const subId = await manager.spawn({
|
|
1737
|
+
parentAgentId,
|
|
1738
|
+
parentSessionId,
|
|
1739
|
+
subagentType: opts.subagentType,
|
|
1740
|
+
description: opts.description,
|
|
1741
|
+
prompt: opts.prompt,
|
|
1742
|
+
workingDirectory,
|
|
1743
|
+
parentConfig: {
|
|
1744
|
+
...parentConfig,
|
|
1745
|
+
// 注入父 agent 的 MCP 连接,子 agent 复用而非启动新 MCP server 进程
|
|
1746
|
+
// 避免 launch binding 已被父 agent 消费后子 agent 401
|
|
1747
|
+
sharedMcpConnections: this.state?.mcpConnections,
|
|
1748
|
+
},
|
|
1749
|
+
});
|
|
1750
|
+
// 自动 start(非阻塞,立即返回)
|
|
1751
|
+
await manager.start(subId);
|
|
1752
|
+
return { subId };
|
|
1753
|
+
},
|
|
1754
|
+
// 列出当前 agent 的所有子 agent(运行中 + 历史)
|
|
1755
|
+
listSubAgents: async () => {
|
|
1756
|
+
const entries = manager.listByParent(parentAgentId);
|
|
1757
|
+
const running = [];
|
|
1758
|
+
const history = [];
|
|
1759
|
+
for (const entry of entries) {
|
|
1760
|
+
const info = {
|
|
1761
|
+
subId: entry.subId,
|
|
1762
|
+
subagentType: entry.subagentType,
|
|
1763
|
+
description: entry.description,
|
|
1764
|
+
status: entry.status,
|
|
1765
|
+
startedAt: entry.startedAt,
|
|
1766
|
+
endedAt: entry.endedAt,
|
|
1767
|
+
durationMs: entry.durationMs,
|
|
1768
|
+
};
|
|
1769
|
+
if (entry.status === "pending" || entry.status === "running") {
|
|
1770
|
+
running.push(info);
|
|
1771
|
+
}
|
|
1772
|
+
else {
|
|
1773
|
+
history.push(info);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
// history 按 endedAt 倒序
|
|
1777
|
+
history.sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0));
|
|
1778
|
+
return { running, history };
|
|
1779
|
+
},
|
|
1780
|
+
// 查询某子 agent 实时状态
|
|
1781
|
+
getSubAgentStatus: async (subId) => {
|
|
1782
|
+
const entry = manager.getSubAgent(subId);
|
|
1783
|
+
if (!entry)
|
|
1784
|
+
return null;
|
|
1785
|
+
return {
|
|
1786
|
+
subId: entry.subId,
|
|
1787
|
+
subagentType: entry.subagentType,
|
|
1788
|
+
description: entry.description,
|
|
1789
|
+
status: entry.status,
|
|
1790
|
+
startedAt: entry.startedAt,
|
|
1791
|
+
endedAt: entry.endedAt,
|
|
1792
|
+
durationMs: entry.durationMs,
|
|
1793
|
+
prompt: entry.prompt,
|
|
1794
|
+
result: entry.result,
|
|
1795
|
+
errorMessage: entry.errorMessage,
|
|
1796
|
+
contextSnapshot: entry.contextSnapshot,
|
|
1797
|
+
};
|
|
1798
|
+
},
|
|
1799
|
+
// 阻塞等待指定子 agent 数组完成
|
|
1800
|
+
awaitSubAgents: (subIds, timeoutMs) => manager.awaitCompletion(subIds, timeoutMs),
|
|
1801
|
+
// 终止某子 agent
|
|
1802
|
+
abortSubAgent: (subId) => manager.abort(subId),
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* 接收用户对 ask_user_question 的回答(由 AcodeSdkAdapter.sendQuestionAnswer 调用)。
|
|
1807
|
+
* 若当前 turn 已被 supersede(contextVersion 变更),返回 false 让 adapter 知晓。
|
|
1808
|
+
*/
|
|
1809
|
+
submitQuestionAnswer(toolCallId, answers) {
|
|
1810
|
+
const waiter = this.questionWaiters.get(toolCallId);
|
|
1811
|
+
if (!waiter) {
|
|
1812
|
+
return false;
|
|
1813
|
+
}
|
|
1814
|
+
this.questionWaiters.delete(toolCallId);
|
|
1815
|
+
// 若 turn 已被 supersede,不 resolve(避免过期答案污染新轮次)
|
|
1816
|
+
if (this.isTurnSuperseded(waiter.contextVersion)) {
|
|
1817
|
+
waiter.reject(new Error("turn superseded before user answered"));
|
|
1818
|
+
return false;
|
|
1819
|
+
}
|
|
1820
|
+
waiter.resolve(answers);
|
|
1821
|
+
return true;
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* 中止所有 ask_user_question 等待,让工具 execute() 抛错走 appendToolFailureResult。
|
|
1825
|
+
* 在 abort()/stop() 中调用,确保用户点停止或会话终止时 execute 不会永久阻塞。
|
|
1826
|
+
*/
|
|
1827
|
+
rejectAllQuestionWaiters(reason) {
|
|
1828
|
+
if (this.questionWaiters.size === 0)
|
|
1829
|
+
return;
|
|
1830
|
+
for (const [, waiter] of this.questionWaiters) {
|
|
1831
|
+
waiter.reject(new Error(`ask_user_question ${reason}`));
|
|
1832
|
+
}
|
|
1833
|
+
this.questionWaiters.clear();
|
|
1179
1834
|
}
|
|
1180
1835
|
/** 终止 ACode 会话并发出 session_complete。 */
|
|
1181
1836
|
async stop() {
|
|
@@ -1187,6 +1842,8 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1187
1842
|
this.abortRequested = this.activeTurn;
|
|
1188
1843
|
this.abortController?.abort();
|
|
1189
1844
|
this.abortController = null;
|
|
1845
|
+
// ask_user_question 等待用户回答期间被 stop,需 reject 所有 waiter
|
|
1846
|
+
this.rejectAllQuestionWaiters("session terminated");
|
|
1190
1847
|
await runBestEffort("ACode active turn shutdown", this.submitQueue, (message) => {
|
|
1191
1848
|
this.emitAcodeEvent("error", { message });
|
|
1192
1849
|
});
|
|
@@ -1199,6 +1856,18 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1199
1856
|
this.sessionCompleteEmitted = true;
|
|
1200
1857
|
this.emitAcodeEvent("session_complete", { exitCode: 0 });
|
|
1201
1858
|
}
|
|
1859
|
+
this.stopCompactionDebugLog();
|
|
1860
|
+
this.stopSkillWatchers();
|
|
1861
|
+
if (COMPACTION_DEBUG_ENABLED) {
|
|
1862
|
+
try {
|
|
1863
|
+
fs.promises.appendFile(COMPACTION_DEBUG_LOG, `[${new Date().toISOString()}] runtime stopped | ---\n`, "utf-8").catch(() => {
|
|
1864
|
+
// 忽略
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
catch {
|
|
1868
|
+
// 忽略
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1202
1871
|
}
|
|
1203
1872
|
resolveCommandDirs(acodeConfig) {
|
|
1204
1873
|
const configured = [
|
|
@@ -1215,29 +1884,31 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1215
1884
|
let finalUsage;
|
|
1216
1885
|
let finalResponseBody;
|
|
1217
1886
|
let finalFinishReason;
|
|
1887
|
+
let accumulatedAssistantText = ""; // 累加多轮迭代的 output 文本长度
|
|
1888
|
+
let accumulatedPromptTokens = 0; // 累加多轮迭代的 prompt_tokens
|
|
1218
1889
|
// 日志:开始 runModelTurn
|
|
1219
|
-
|
|
1890
|
+
runtimeDebugLog(`[ACodeRuntime] ▶️ runModelTurn started (contextVersion: ${contextVersion})`);
|
|
1220
1891
|
// 移除5次限制,添加安全上限防止真正的无限循环
|
|
1221
1892
|
const SAFETY_LIMIT = 50; // 安全上限,正常情况下不会达到
|
|
1222
1893
|
for (let step = 0; step < SAFETY_LIMIT; step += 1) {
|
|
1223
1894
|
// 详细日志:记录每次迭代
|
|
1224
|
-
|
|
1895
|
+
runtimeDebugLog(`[ACodeRuntime] 🔄 Iteration ${step + 1} starting...`);
|
|
1225
1896
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1226
|
-
|
|
1227
|
-
return { text: "" };
|
|
1897
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Turn superseded at iteration ${step + 1}, aborting`);
|
|
1898
|
+
return { text: "", totalOutputTextLength: accumulatedAssistantText.length };
|
|
1228
1899
|
}
|
|
1229
1900
|
const requestStartTime = Date.now();
|
|
1230
1901
|
const response = await this.requestChatCompletion(commandAllowedTools);
|
|
1231
1902
|
const requestDuration = Date.now() - requestStartTime;
|
|
1232
1903
|
// 日志:AI请求完成
|
|
1233
|
-
|
|
1904
|
+
runtimeDebugLog(`[ACodeRuntime] 🤖 AI request completed in ${requestDuration}ms, ` +
|
|
1234
1905
|
`choices: ${response.choices?.length || 0}`);
|
|
1235
1906
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1236
|
-
|
|
1237
|
-
return { text: "" };
|
|
1907
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Turn superseded after AI response at iteration ${step + 1}`);
|
|
1908
|
+
return { text: "", totalOutputTextLength: accumulatedAssistantText.length };
|
|
1238
1909
|
}
|
|
1239
1910
|
if (this.state.terminated) {
|
|
1240
|
-
|
|
1911
|
+
runtimeDebugLog(`[ACodeRuntime] Runtime terminated at iteration ${step + 1}`);
|
|
1241
1912
|
throw new Error("ACode runtime was stopped during the active turn");
|
|
1242
1913
|
}
|
|
1243
1914
|
const choice = response.choices?.[0];
|
|
@@ -1252,7 +1923,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1252
1923
|
? message.tool_calls
|
|
1253
1924
|
: [];
|
|
1254
1925
|
// 日志:AI响应详情
|
|
1255
|
-
|
|
1926
|
+
runtimeDebugLog(`[ACodeRuntime] AI response: ` +
|
|
1256
1927
|
`content=${content ? content.slice(0, 100) + (content.length > 100 ? '...' : '') : '(empty)'}, ` +
|
|
1257
1928
|
`toolCalls=${toolCalls.length}, ` +
|
|
1258
1929
|
`finishReason=${finalFinishReason || '(none)'}`);
|
|
@@ -1261,11 +1932,25 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1261
1932
|
const rtOutputTokens = response.usage.completion_tokens ?? response.usage.output_tokens;
|
|
1262
1933
|
if (typeof rtInputTokens === "number" &&
|
|
1263
1934
|
typeof rtOutputTokens === "number") {
|
|
1935
|
+
// ★ finalUsage 记录当前迭代的实际 token 用量(覆盖式,非累加)。
|
|
1936
|
+
// prompt_tokens 代表本次请求时 provider 看到的完整上下文大小,
|
|
1937
|
+
// 若跨迭代累加会得到 N × 上下文大小 的无意义总和,导致 turn_complete
|
|
1938
|
+
// 上报的 usage 远超真实上下文,前端"上下文"标签会瞬间跳到几 M 再跳回。
|
|
1939
|
+
// 改为覆盖式,与 state.accumulatedInputTokens 及 AcodeSdkAdapter 的
|
|
1940
|
+
// "当前上下文"语义保持一致。
|
|
1264
1941
|
finalUsage = {
|
|
1265
1942
|
inputTokens: rtInputTokens,
|
|
1266
1943
|
outputTokens: rtOutputTokens,
|
|
1267
1944
|
};
|
|
1268
|
-
|
|
1945
|
+
accumulatedPromptTokens += rtInputTokens;
|
|
1946
|
+
// ★ 更新 state 为当前迭代的实际 token 用量(非累加)
|
|
1947
|
+
// prompt_tokens 代表本次请求时 provider 看到的完整上下文大小,
|
|
1948
|
+
// 是当前上下文大小的最准确度量。
|
|
1949
|
+
// 之前用 += 累加导致 actualUsed 只增不减,shouldCompact 永远为 true。
|
|
1950
|
+
this.state.accumulatedInputTokens = rtInputTokens;
|
|
1951
|
+
this.state.accumulatedOutputTokens = rtOutputTokens;
|
|
1952
|
+
runtimeDebugLog(`[ACodeRuntime] 💰 Token usage: input=${rtInputTokens}, output=${rtOutputTokens} ` +
|
|
1953
|
+
`(turn accumulated cost: input=${accumulatedPromptTokens})`);
|
|
1269
1954
|
}
|
|
1270
1955
|
}
|
|
1271
1956
|
this.state.conversation.push({
|
|
@@ -1275,31 +1960,33 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1275
1960
|
});
|
|
1276
1961
|
if (content) {
|
|
1277
1962
|
finalText = content;
|
|
1278
|
-
|
|
1963
|
+
accumulatedAssistantText += content;
|
|
1964
|
+
runtimeDebugLog(`[ACodeRuntime] ✨ Captured assistant text: ${content.slice(0, 80)}...`);
|
|
1279
1965
|
}
|
|
1280
1966
|
if (toolCalls.length === 0) {
|
|
1281
|
-
|
|
1967
|
+
runtimeDebugLog(`[ACodeRuntime] ✅ No tool calls at iteration ${step + 1}, completing turn`);
|
|
1282
1968
|
return {
|
|
1283
1969
|
text: finalText,
|
|
1284
1970
|
usage: finalUsage,
|
|
1285
1971
|
responseBody: finalResponseBody,
|
|
1286
1972
|
finishReason: finalFinishReason,
|
|
1973
|
+
totalOutputTextLength: accumulatedAssistantText.length,
|
|
1287
1974
|
};
|
|
1288
1975
|
}
|
|
1289
1976
|
// 日志:即将执行工具调用
|
|
1290
1977
|
const toolNames = toolCalls.map(tc => tc.function.name).join(", ");
|
|
1291
|
-
|
|
1978
|
+
runtimeDebugLog(`[ACodeRuntime] 🔧 Executing ${toolCalls.length} tool(s) at iteration ${step + 1}: ${toolNames}`);
|
|
1292
1979
|
const toolStart = Date.now();
|
|
1293
1980
|
await this.handleToolCalls(toolCalls, commandAllowedTools, contextVersion);
|
|
1294
1981
|
const toolDuration = Date.now() - toolStart;
|
|
1295
1982
|
// 日志:工具执行完成
|
|
1296
|
-
|
|
1983
|
+
runtimeDebugLog(`[ACodeRuntime] ⚙️ Tool execution completed in ${toolDuration}ms at iteration ${step + 1}`);
|
|
1297
1984
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1298
|
-
|
|
1299
|
-
return { text: "" };
|
|
1985
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Turn superseded after tool execution at iteration ${step + 1}`);
|
|
1986
|
+
return { text: "", totalOutputTextLength: accumulatedAssistantText.length };
|
|
1300
1987
|
}
|
|
1301
1988
|
// 日志:准备下一次迭代
|
|
1302
|
-
|
|
1989
|
+
runtimeDebugLog(`[ACodeRuntime] 🔄 Preparing iteration ${step + 2}...`);
|
|
1303
1990
|
}
|
|
1304
1991
|
// 达到安全上限(理论上不应该发生)
|
|
1305
1992
|
console.error(`[ACodeRuntime] 🚨 CRITICAL: Reached safety limit of ${SAFETY_LIMIT} iterations! ` +
|
|
@@ -1309,19 +1996,163 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1309
1996
|
usage: finalUsage,
|
|
1310
1997
|
responseBody: finalResponseBody,
|
|
1311
1998
|
finishReason: finalFinishReason || "safety_iteration_limit_reached",
|
|
1999
|
+
totalOutputTextLength: accumulatedAssistantText.length,
|
|
1312
2000
|
};
|
|
1313
2001
|
}
|
|
2002
|
+
/**
|
|
2003
|
+
* LLM 摘要器:调用当前 provider 生成结构化对话摘要。
|
|
2004
|
+
*
|
|
2005
|
+
* 这是把 OpenCode 的 LLM 摘要能力赋能到 ACode 的核心方法:
|
|
2006
|
+
* - OpenCode 在 compaction.process 中调用独立的 compaction agent
|
|
2007
|
+
* - ACode 复用当前 provider 配置,发起一次非流式 chat completion 请求
|
|
2008
|
+
* - 使用 COMPACTION_LLM_PROMPT_TEMPLATE 作为 prompt 模板
|
|
2009
|
+
*
|
|
2010
|
+
* 设计要点:
|
|
2011
|
+
* 1. 非流式调用(不需要流式,简化处理)
|
|
2012
|
+
* 2. 不带 tools(纯文本生成)
|
|
2013
|
+
* 3. 失败时抛出异常,由 summarizeConversation 降级到规则摘要
|
|
2014
|
+
* 4. 独立的 AbortController,避免主请求取消影响摘要
|
|
2015
|
+
* 5. 复用 provider 的 timeout 配置
|
|
2016
|
+
*
|
|
2017
|
+
* @param input 摘要输入(conversationText + messageCount + contextLimit)
|
|
2018
|
+
* @returns LLM 生成的结构化摘要文本
|
|
2019
|
+
*/
|
|
2020
|
+
async summarizeConversationWithLLM(input) {
|
|
2021
|
+
const provider = this.state.provider;
|
|
2022
|
+
const baseURL = provider.baseURL;
|
|
2023
|
+
if (!baseURL) {
|
|
2024
|
+
throw new Error("ACode provider baseURL is not configured for LLM summarization");
|
|
2025
|
+
}
|
|
2026
|
+
const headers = {
|
|
2027
|
+
"Content-Type": "application/json",
|
|
2028
|
+
};
|
|
2029
|
+
if (provider.apiKey) {
|
|
2030
|
+
headers.Authorization = `Bearer ${provider.apiKey}`;
|
|
2031
|
+
}
|
|
2032
|
+
const requestBody = {
|
|
2033
|
+
model: provider.model,
|
|
2034
|
+
stream: false,
|
|
2035
|
+
messages: [
|
|
2036
|
+
{
|
|
2037
|
+
role: "system",
|
|
2038
|
+
content: "You are a helpful AI assistant tasked with summarizing conversations. " +
|
|
2039
|
+
"When asked to summarize, provide a detailed but concise summary of the conversation. " +
|
|
2040
|
+
"Focus on information that would be helpful for continuing the conversation. " +
|
|
2041
|
+
"Do not respond to any questions in the conversation, only output the summary.",
|
|
2042
|
+
},
|
|
2043
|
+
{
|
|
2044
|
+
role: "user",
|
|
2045
|
+
content: `Below is an earlier conversation (${input.messageCount} messages) that needs to be summarized ` +
|
|
2046
|
+
`to free context space (current context limit: ${input.contextLimit} tokens).\n\n` +
|
|
2047
|
+
`--- Conversation Start ---\n${input.conversationText}\n--- Conversation End ---\n\n` +
|
|
2048
|
+
COMPACTION_LLM_PROMPT_TEMPLATE,
|
|
2049
|
+
},
|
|
2050
|
+
],
|
|
2051
|
+
};
|
|
2052
|
+
const abortController = new AbortController();
|
|
2053
|
+
this.abortController = abortController;
|
|
2054
|
+
const response = await withTimeout(fetch(normalizeChatCompletionsUrl(baseURL), {
|
|
2055
|
+
method: "POST",
|
|
2056
|
+
headers,
|
|
2057
|
+
body: JSON.stringify(requestBody),
|
|
2058
|
+
signal: abortController.signal,
|
|
2059
|
+
}), provider.requestTimeoutMs, "ACode LLM summarization", () => {
|
|
2060
|
+
abortController.abort();
|
|
2061
|
+
});
|
|
2062
|
+
if (!response.ok) {
|
|
2063
|
+
// ★ 防御修复:用 withTimeout 包裹 response.text(),防止错误响应 body stream 挂起
|
|
2064
|
+
const errorBody = await withTimeout(response.text(), provider.requestTimeoutMs, "ACode LLM summarization error response body", () => {
|
|
2065
|
+
abortController.abort();
|
|
2066
|
+
});
|
|
2067
|
+
const truncatedBody = errorBody.length > 500
|
|
2068
|
+
? `${errorBody.slice(0, 500)}...(truncated, total ${errorBody.length} chars)`
|
|
2069
|
+
: errorBody;
|
|
2070
|
+
throw new Error(`ACode LLM summarization request failed: ${response.status} ${truncatedBody}`);
|
|
2071
|
+
}
|
|
2072
|
+
const payload = (await response.json());
|
|
2073
|
+
const content = payload.choices?.[0]?.message?.content;
|
|
2074
|
+
if (!content || content.trim().length === 0) {
|
|
2075
|
+
throw new Error("ACode LLM summarization returned empty content");
|
|
2076
|
+
}
|
|
2077
|
+
return content;
|
|
2078
|
+
}
|
|
1314
2079
|
async requestChatCompletion(commandAllowedTools) {
|
|
1315
2080
|
const provider = this.state.provider;
|
|
1316
2081
|
const baseURL = provider.baseURL;
|
|
1317
2082
|
if (!baseURL) {
|
|
1318
2083
|
throw new Error("ACode provider baseURL is not configured");
|
|
1319
2084
|
}
|
|
1320
|
-
|
|
2085
|
+
// 主动上下文压缩:在发送 AI 请求前检测 token 是否溢出。
|
|
2086
|
+
// ★ 与 session_started 事件 emit 的阈值同源(resolveCompactionConfig),保证前端显示 = 实际触发。
|
|
2087
|
+
const { contextLimit, maxOutput, inputLimit, threshold: compactionThreshold } = resolveCompactionConfig(provider);
|
|
2088
|
+
// 使用 effectiveUsed = max(actualUsed, estimatedUsed)
|
|
2089
|
+
// 因为部分 provider 不返回 usage 数据(actualUsed=0),此时降级到文本估算
|
|
2090
|
+
const actualUsed = this.state.accumulatedInputTokens + this.state.accumulatedOutputTokens;
|
|
2091
|
+
// ★ 估算时加入 tools 定义开销,使估算更接近实际请求 token 数。
|
|
2092
|
+
// 之前只估算 conversation 消息,遗漏了 tools 的 JSON schema(可达数千 tokens),
|
|
2093
|
+
// 导致估算系统性偏低、主动压缩阈值无法触达。
|
|
2094
|
+
const estimatedUsed = estimateConversationTokens(this.state.conversation)
|
|
2095
|
+
+ estimateToolsTokens(this.state.exposedTools);
|
|
2096
|
+
const effectiveUsed = Math.max(actualUsed, estimatedUsed);
|
|
2097
|
+
const shouldCompact = effectiveUsed >= compactionThreshold;
|
|
2098
|
+
runtimeDebugLog(`[ACodeRuntime] 🔍 Compaction check: ` +
|
|
2099
|
+
`actualUsed=${actualUsed}, estimation=${estimatedUsed}, ` +
|
|
2100
|
+
`effectiveUsed=${effectiveUsed}, contextLimit=${contextLimit}, ` +
|
|
2101
|
+
`inputLimit=${inputLimit ?? "n/a"}, maxOutput=${maxOutput}, ` +
|
|
2102
|
+
`triggerThreshold=${compactionThreshold}, shouldCompact=${shouldCompact}`);
|
|
2103
|
+
if (shouldCompact) {
|
|
2104
|
+
const sizeBeforeKB = conversationSizeKB(this.state.conversation);
|
|
2105
|
+
const compactionId = `compaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2106
|
+
this.emitAcodeEvent("context_compaction", {
|
|
2107
|
+
action: "start",
|
|
2108
|
+
compactionId,
|
|
2109
|
+
sizeKB: sizeBeforeKB,
|
|
2110
|
+
estimatedTokens: estimatedUsed,
|
|
2111
|
+
actualInputTokens: this.state.accumulatedInputTokens,
|
|
2112
|
+
actualOutputTokens: this.state.accumulatedOutputTokens,
|
|
2113
|
+
messageCount: this.state.conversation.length,
|
|
2114
|
+
model: provider.model,
|
|
2115
|
+
trigger: actualUsed >= compactionThreshold ? "actual_usage" : "estimated_overflow",
|
|
2116
|
+
});
|
|
2117
|
+
// ★ 发射压缩子事件(检测到溢出)
|
|
2118
|
+
this.emitCompactionPhase(compactionId, "detect", "检测到上下文溢出", `有效用量: ${effectiveUsed} tokens, 阈值: ${compactionThreshold}, 触发: ${actualUsed >= compactionThreshold ? "实际用量" : "估算溢出"}`);
|
|
2119
|
+
const result = await performCompaction(this.state.conversation, contextLimit, maxOutput, {
|
|
2120
|
+
llmSummarizer: (input) => this.summarizeConversationWithLLM(input),
|
|
2121
|
+
onPhase: (phaseEvent) => this.handleCompactionPhase(compactionId, phaseEvent),
|
|
2122
|
+
// 传入 effectiveUsed 修复文本估算低于实际 token 数时
|
|
2123
|
+
// post-prune isOverflow 误判导致 summarize 被跳过的问题
|
|
2124
|
+
actualUsedTokens: effectiveUsed,
|
|
2125
|
+
});
|
|
2126
|
+
const sizeAfterKB = conversationSizeKB(this.state.conversation);
|
|
2127
|
+
this.emitAcodeEvent("context_compaction", {
|
|
2128
|
+
action: "end",
|
|
2129
|
+
compactionId,
|
|
2130
|
+
sizeBeforeKB,
|
|
2131
|
+
sizeAfterKB,
|
|
2132
|
+
freedKB: Math.round((sizeBeforeKB - sizeAfterKB) * 10) / 10,
|
|
2133
|
+
strategy: result.strategy,
|
|
2134
|
+
freedTokens: result.freedTokens,
|
|
2135
|
+
tokensBefore: result.tokensBefore,
|
|
2136
|
+
tokensAfter: result.tokensAfter,
|
|
2137
|
+
prunedToolMessages: result.prunedToolMessages,
|
|
2138
|
+
summarizedMessages: result.summarizedMessages,
|
|
2139
|
+
messageCountBefore: result.messageCountBefore,
|
|
2140
|
+
messageCountAfter: result.messageCountAfter,
|
|
2141
|
+
});
|
|
2142
|
+
// ★ 压缩后重置累积 token 计数,使下一次请求时 actualUsed 反映压缩后的真实上下文大小。
|
|
2143
|
+
// 若不重置,actualUsed 仍为压缩前的旧值,shouldCompact 会持续为 true 导致无效压缩循环。
|
|
2144
|
+
this.state.accumulatedInputTokens = 0;
|
|
2145
|
+
this.state.accumulatedOutputTokens = 0;
|
|
2146
|
+
}
|
|
2147
|
+
const truncatedCount = await truncateConversationIfNeeded(this.state.conversation, MAX_REQUEST_BODY_BYTES, contextLimit, maxOutput, (input) => this.summarizeConversationWithLLM(input));
|
|
1321
2148
|
if (truncatedCount > 0) {
|
|
1322
2149
|
this.emitAcodeEvent("warning", {
|
|
1323
2150
|
message: `Conversation truncated: removed ${truncatedCount} older messages to fit within ${MAX_REQUEST_BODY_BYTES} byte request size limit`,
|
|
1324
2151
|
});
|
|
2152
|
+
// ★ HTTP body 兜底压缩后同样重置累积 token 计数,避免下一轮 actualUsed 仍为
|
|
2153
|
+
// 压缩前旧值导致 shouldCompact 误判(与主动压缩路径行为一致,见上方 shouldCompact 块)。
|
|
2154
|
+
this.state.accumulatedInputTokens = 0;
|
|
2155
|
+
this.state.accumulatedOutputTokens = 0;
|
|
1325
2156
|
}
|
|
1326
2157
|
const allowedTools = this.state.exposedTools.filter((tool) => isToolAllowed(tool, this.state.permissions, commandAllowedTools));
|
|
1327
2158
|
const buildBody = () => {
|
|
@@ -1330,6 +2161,16 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1330
2161
|
messages: this.state.conversation,
|
|
1331
2162
|
stream: provider.stream,
|
|
1332
2163
|
};
|
|
2164
|
+
// ★ 思考强度:写入 reasoning_effort 字段,兼容 OpenAI o-series 及兼容 API。
|
|
2165
|
+
// 未配置 effort 时不写入,由 provider 使用默认值。
|
|
2166
|
+
if (provider.effort) {
|
|
2167
|
+
body.reasoning_effort = provider.effort;
|
|
2168
|
+
}
|
|
2169
|
+
// ★ 流式请求必须设置 stream_options.include_usage,否则 provider 不返回 usage,
|
|
2170
|
+
// 导致 accumulatedInputTokens 永远为 0,主动压缩无法基于实际用量触发。
|
|
2171
|
+
if (provider.stream) {
|
|
2172
|
+
body.stream_options = { include_usage: true };
|
|
2173
|
+
}
|
|
1333
2174
|
if (allowedTools.length > 0) {
|
|
1334
2175
|
body.tools = toOpenAiTools(allowedTools);
|
|
1335
2176
|
body.tool_choice = "auto";
|
|
@@ -1343,7 +2184,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1343
2184
|
const emitAiRequest = (body) => {
|
|
1344
2185
|
const requestBody = JSON.stringify(body, null, 2);
|
|
1345
2186
|
// [DEBUG] ACode Runtime 实际发送的模型名称
|
|
1346
|
-
|
|
2187
|
+
runtimeDebugLog(`[ACodeRuntime] ★★★ DEBUG ★★★ emitAiRequest: 发送请求 model=${provider.model}, endpoint=${requestUrl}, messageCount=${this.state.conversation.length}`);
|
|
1347
2188
|
this.emitAcodeEvent("ai_request", {
|
|
1348
2189
|
model: provider.model,
|
|
1349
2190
|
endpoint: requestUrl,
|
|
@@ -1360,9 +2201,10 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1360
2201
|
};
|
|
1361
2202
|
let body = buildBody();
|
|
1362
2203
|
emitAiRequest(body);
|
|
1363
|
-
const
|
|
2204
|
+
const infiniteRetries = provider.requestRetryCount === INFINITE_RETRY_SENTINEL;
|
|
2205
|
+
const maxAttempts = infiniteRetries ? Infinity : provider.requestRetryCount + 1;
|
|
1364
2206
|
let contextOverflowCompacted = false;
|
|
1365
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
2207
|
+
for (let attempt = 1; infiniteRetries || attempt <= maxAttempts; attempt += 1) {
|
|
1366
2208
|
const requestAbortController = new AbortController();
|
|
1367
2209
|
this.abortController = requestAbortController;
|
|
1368
2210
|
let timedOut = false;
|
|
@@ -1378,7 +2220,20 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1378
2220
|
requestAbortController.abort();
|
|
1379
2221
|
});
|
|
1380
2222
|
if (!response.ok) {
|
|
1381
|
-
|
|
2223
|
+
// ★ 防御修复:用 withTimeout 包裹 response.text(),
|
|
2224
|
+
// 防止 502/503 等错误响应的 body stream 挂起导致永久阻塞。
|
|
2225
|
+
// 外层 withTimeout(requestTotalTimeoutMs, 10min) 兜底太晚,
|
|
2226
|
+
// 且看门狗可能已被重试 error 事件禁用。
|
|
2227
|
+
const errorBody = await withTimeout(response.text(), provider.requestTimeoutMs, "ACode provider error response body", () => {
|
|
2228
|
+
timedOut = true;
|
|
2229
|
+
requestAbortController.abort();
|
|
2230
|
+
});
|
|
2231
|
+
// ★ 截断过长的错误响应体(如 Cloudflare 502 HTML 页面可达数 KB),
|
|
2232
|
+
// 避免错误消息通过事件链路传播时造成 HTTP POST body 过大或日志膨胀
|
|
2233
|
+
const truncatedBody = errorBody.length > 500
|
|
2234
|
+
? `${errorBody.slice(0, 500)}...(truncated, total ${errorBody.length} chars)`
|
|
2235
|
+
: errorBody;
|
|
2236
|
+
throw new Error(`ACode provider request failed: ${response.status} ${truncatedBody}`);
|
|
1382
2237
|
}
|
|
1383
2238
|
const contentType = response.headers?.get?.("content-type") || "";
|
|
1384
2239
|
if (provider.stream &&
|
|
@@ -1398,7 +2253,14 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1398
2253
|
return {};
|
|
1399
2254
|
}
|
|
1400
2255
|
return payload;
|
|
1401
|
-
})(),
|
|
2256
|
+
})(),
|
|
2257
|
+
// ★ 安全修复:即使无限重试也保留总预算超时兜底,
|
|
2258
|
+
// 防止 provider 持续发送 SSE keep-alive 导致 idle timeout 永不触发、
|
|
2259
|
+
// 请求永久挂起。用户可通过配置更大的 requestTotalTimeoutMs 值来放宽限制。
|
|
2260
|
+
provider.requestTotalTimeoutMs, "ACode provider request total", () => {
|
|
2261
|
+
// 总预算超时:abort 底层 fetch / stream reader,避免泄漏
|
|
2262
|
+
requestAbortController.abort();
|
|
2263
|
+
});
|
|
1402
2264
|
}
|
|
1403
2265
|
catch (error) {
|
|
1404
2266
|
let requestError;
|
|
@@ -1415,11 +2277,62 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1415
2277
|
!this.abortRequested &&
|
|
1416
2278
|
!this.state.terminated &&
|
|
1417
2279
|
isContextOverflowProviderRequestError(requestError)) {
|
|
1418
|
-
|
|
2280
|
+
// 响应式上下文压缩:Provider 返回上下文溢出错误时强制执行
|
|
2281
|
+
// 这里不通过 performCompaction(它会先检查 isOverflow),
|
|
2282
|
+
// 而是直接调用 summarizeConversation 进行强制摘要化
|
|
2283
|
+
const sizeBeforeKB = conversationSizeKB(this.state.conversation);
|
|
2284
|
+
const tokensBeforeOC = estimateConversationTokens(this.state.conversation); // tokensBefore 命名冲突,加 OC 后缀
|
|
2285
|
+
const msgCountBefore = this.state.conversation.length;
|
|
2286
|
+
const compactionId = `compaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2287
|
+
this.emitAcodeEvent("context_compaction", {
|
|
2288
|
+
action: "start",
|
|
2289
|
+
compactionId,
|
|
2290
|
+
sizeKB: sizeBeforeKB,
|
|
2291
|
+
estimatedTokens: tokensBeforeOC,
|
|
2292
|
+
messageCount: msgCountBefore,
|
|
2293
|
+
model: provider.model,
|
|
2294
|
+
trigger: "provider_overflow_error",
|
|
2295
|
+
});
|
|
2296
|
+
// ★ 发射压缩子事件(检测到溢出)
|
|
2297
|
+
this.emitCompactionPhase(compactionId, "detect", "Provider 返回上下文溢出", `消息: ${msgCountBefore}条, 大小: ${sizeBeforeKB}KB`);
|
|
2298
|
+
// 先尝试 prune 旧工具输出
|
|
2299
|
+
this.emitCompactionPhase(compactionId, "prune", "阶段1: 裁剪工具输出", "正在裁剪...");
|
|
2300
|
+
const pruneResult = pruneToolOutputs(this.state.conversation, contextLimit);
|
|
2301
|
+
this.emitCompactionPhase(compactionId, "prune_done", "阶段1: 裁剪工具输出", `裁剪了 ${pruneResult.prunedCount} 条工具输出, 释放 ${pruneResult.freedTokens} tokens`);
|
|
2302
|
+
// 再强制 summarize 早期对话(使用 LLM 摘要器提升摘要质量,失败降级到规则摘要)
|
|
2303
|
+
this.emitCompactionPhase(compactionId, "summarize", "阶段2: 摘要化对话", "正在摘要...");
|
|
2304
|
+
const summarizedCount = await summarizeConversation(this.state.conversation, {
|
|
2305
|
+
llmSummarizer: (input) => this.summarizeConversationWithLLM(input),
|
|
2306
|
+
contextLimit,
|
|
2307
|
+
});
|
|
2308
|
+
this.emitCompactionPhase(compactionId, "summarize_done", "阶段2: 摘要化对话", `摘要化了 ${summarizedCount} 条消息`);
|
|
2309
|
+
const sizeAfterKB = conversationSizeKB(this.state.conversation);
|
|
2310
|
+
const tokensAfterOC = estimateConversationTokens(this.state.conversation);
|
|
2311
|
+
const msgCountAfter = this.state.conversation.length;
|
|
1419
2312
|
contextOverflowCompacted = true;
|
|
2313
|
+
const compactedCount = pruneResult.prunedCount + summarizedCount;
|
|
2314
|
+
this.emitAcodeEvent("context_compaction", {
|
|
2315
|
+
action: "end",
|
|
2316
|
+
compactionId,
|
|
2317
|
+
sizeBeforeKB,
|
|
2318
|
+
sizeAfterKB,
|
|
2319
|
+
freedKB: Math.round((sizeBeforeKB - sizeAfterKB) * 10) / 10,
|
|
2320
|
+
tokensBefore: tokensBeforeOC,
|
|
2321
|
+
tokensAfter: tokensAfterOC,
|
|
2322
|
+
messageCountBefore: msgCountBefore,
|
|
2323
|
+
messageCountAfter: msgCountAfter,
|
|
2324
|
+
strategy: summarizedCount > 0 ? "summarize" : "prune",
|
|
2325
|
+
freedTokens: pruneResult.freedTokens,
|
|
2326
|
+
prunedToolMessages: pruneResult.prunedCount,
|
|
2327
|
+
summarizedMessages: summarizedCount,
|
|
2328
|
+
trigger: "provider_overflow_error",
|
|
2329
|
+
});
|
|
1420
2330
|
if (compactedCount === 0) {
|
|
1421
2331
|
throw requestError;
|
|
1422
2332
|
}
|
|
2333
|
+
// ★ 响应式压缩后同样重置累积 token 计数
|
|
2334
|
+
this.state.accumulatedInputTokens = 0;
|
|
2335
|
+
this.state.accumulatedOutputTokens = 0;
|
|
1423
2336
|
this.emitAcodeEvent("warning", {
|
|
1424
2337
|
message: `ACode provider context limit exceeded; summarized ${compactedCount} older messages and rebuilt the request instead of retrying the same oversized payload`,
|
|
1425
2338
|
});
|
|
@@ -1428,7 +2341,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1428
2341
|
attempt -= 1;
|
|
1429
2342
|
continue;
|
|
1430
2343
|
}
|
|
1431
|
-
const shouldRetry = attempt < maxAttempts &&
|
|
2344
|
+
const shouldRetry = (infiniteRetries || attempt < maxAttempts) &&
|
|
1432
2345
|
!this.abortRequested &&
|
|
1433
2346
|
!this.state.terminated &&
|
|
1434
2347
|
isRetryableProviderRequestError(requestError);
|
|
@@ -1436,7 +2349,11 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1436
2349
|
throw requestError;
|
|
1437
2350
|
}
|
|
1438
2351
|
this.emitAcodeEvent("error", {
|
|
1439
|
-
message: `ACode provider request attempt ${attempt}
|
|
2352
|
+
message: `ACode provider request attempt ${attempt}${infiniteRetries ? '' : `/${maxAttempts}`} failed; retrying: ${requestError.message}`,
|
|
2353
|
+
// ★ 关键修复:重试中的错误是瞬态的,不是致命错误。
|
|
2354
|
+
// 如果不标记 nonFatal,适配器会把 status 设为 "error" 并取消看门狗,
|
|
2355
|
+
// 导致后续重试尝试挂起时看门狗无法检测,agent 永久卡死。
|
|
2356
|
+
nonFatal: true,
|
|
1440
2357
|
});
|
|
1441
2358
|
await delay(DEFAULT_PROVIDER_REQUEST_RETRY_DELAY_MS * attempt);
|
|
1442
2359
|
}
|
|
@@ -1457,6 +2374,8 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1457
2374
|
const toolCallAccumulators = new Map();
|
|
1458
2375
|
let text = "";
|
|
1459
2376
|
let buffer = "";
|
|
2377
|
+
/** 从 SSE 流最终 chunk 中提取的 usage(OpenAI 格式在最后一个 chunk 的 usage 字段中) */
|
|
2378
|
+
let streamUsage;
|
|
1460
2379
|
const consumeFrame = (frame) => {
|
|
1461
2380
|
for (const data of parseSseDataLines(frame)) {
|
|
1462
2381
|
if (data === "[DONE]") {
|
|
@@ -1466,6 +2385,11 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1466
2385
|
if (!isRecord(payload)) {
|
|
1467
2386
|
continue;
|
|
1468
2387
|
}
|
|
2388
|
+
// ★ 提取 SSE chunk 中的 usage(OpenAI 流式 API 在最后一个 chunk 返回完整 usage,此时 choices 为空数组)
|
|
2389
|
+
const chunkUsage = payload.usage;
|
|
2390
|
+
if (chunkUsage) {
|
|
2391
|
+
streamUsage = chunkUsage;
|
|
2392
|
+
}
|
|
1469
2393
|
const choice = payload.choices?.[0];
|
|
1470
2394
|
const delta = choice?.delta;
|
|
1471
2395
|
const reasoningText = extractReasoningText(delta);
|
|
@@ -1510,33 +2434,40 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1510
2434
|
},
|
|
1511
2435
|
},
|
|
1512
2436
|
],
|
|
2437
|
+
...(streamUsage ? { usage: streamUsage } : {}),
|
|
1513
2438
|
};
|
|
1514
2439
|
}
|
|
1515
2440
|
async handleToolCalls(toolCalls, commandAllowedTools, contextVersion = this.contextVersion) {
|
|
1516
|
-
|
|
2441
|
+
runtimeDebugLog(`[ACodeRuntime] handleToolCalls started with ${toolCalls.length} tool(s)`);
|
|
1517
2442
|
for (const toolCall of toolCalls) {
|
|
1518
2443
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1519
|
-
|
|
2444
|
+
runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded, skipping remaining tools`);
|
|
1520
2445
|
return;
|
|
1521
2446
|
}
|
|
1522
2447
|
const toolName = toolCall.function.name;
|
|
1523
|
-
|
|
1524
|
-
|
|
2448
|
+
runtimeDebugLog(`[ACodeRuntime] 🔍 Looking up tool: ${toolName}`);
|
|
2449
|
+
let tool = this.state.exposedTools.find((item) => item.exposedName === toolName);
|
|
1525
2450
|
if (!tool) {
|
|
1526
|
-
|
|
2451
|
+
tool = this.state.exposedTools.find((item) => item.toolName === toolName);
|
|
2452
|
+
if (tool) {
|
|
2453
|
+
runtimeDebugLog(`[ACodeRuntime] ↩️ 裸名回退匹配: ${toolName} -> ${tool.exposedName}`);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
if (!tool) {
|
|
2457
|
+
console.error(`[ACodeRuntime] ❌ Tool not found: ${toolName}, ` +
|
|
1527
2458
|
`available tools: ${this.state.exposedTools.map(t => t.exposedName).join(", ")}`);
|
|
1528
2459
|
this.state.conversation.push({
|
|
1529
2460
|
role: "tool",
|
|
1530
2461
|
tool_call_id: toolCall.id,
|
|
1531
|
-
content: `ACode MCP tool not found: ${
|
|
2462
|
+
content: `ACode MCP tool not found: ${toolName}`,
|
|
1532
2463
|
});
|
|
1533
2464
|
continue;
|
|
1534
2465
|
}
|
|
1535
|
-
|
|
2466
|
+
runtimeDebugLog(`[ACodeRuntime] Found tool: ${tool.toolName} (kind: ${tool.kind}, source: ${tool.kind === 'mcp' ? tool.serverName : 'builtin'})`);
|
|
1536
2467
|
try {
|
|
1537
2468
|
if (tool.kind === "builtin") {
|
|
1538
2469
|
const toolArguments = coerceArguments(toolCall.function.arguments);
|
|
1539
|
-
|
|
2470
|
+
runtimeDebugLog(`[ACodeRuntime] Built-in tool arguments: ${JSON.stringify(toolArguments).slice(0, 200)}`);
|
|
1540
2471
|
assertToolPermission(tool, this.state.permissions, commandAllowedTools);
|
|
1541
2472
|
const preResults = await this.hookBus.dispatch("PreToolUse", {
|
|
1542
2473
|
source: "builtin",
|
|
@@ -1553,7 +2484,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1553
2484
|
throw new Error(denied.message ||
|
|
1554
2485
|
`ACode tool denied by hook: ${tool.exposedName}`);
|
|
1555
2486
|
}
|
|
1556
|
-
|
|
2487
|
+
runtimeDebugLog(`[ACodeRuntime] Emitting mcp_status call_tool for ${tool.exposedName}`);
|
|
1557
2488
|
this.emitAcodeEvent("mcp_status", {
|
|
1558
2489
|
action: "call_tool",
|
|
1559
2490
|
server: "builtin",
|
|
@@ -1564,7 +2495,13 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1564
2495
|
argumentsPreview: summarizeToolArguments(toolArguments),
|
|
1565
2496
|
});
|
|
1566
2497
|
const toolStart = Date.now();
|
|
1567
|
-
|
|
2498
|
+
const toolTimeoutMs = getBuiltInToolTimeoutMs(tool, toolArguments, this.state.provider.mcpToolTimeoutMs);
|
|
2499
|
+
runtimeDebugLog(`[ACodeRuntime] ▶️ Executing built-in tool: ${tool.exposedName} ` +
|
|
2500
|
+
`(timeout: ${toolTimeoutMs}ms, abortRequested: ${this.abortRequested})`);
|
|
2501
|
+
if (toolTimeoutMs <= 0) {
|
|
2502
|
+
console.warn(`[ACodeRuntime] ⚠️ Built-in tool ${tool.exposedName} has NO timeout (timeoutMs=${toolTimeoutMs})! ` +
|
|
2503
|
+
`If this tool hangs, executeSubmit will NEVER resolve, blocking the submit queue permanently.`);
|
|
2504
|
+
}
|
|
1568
2505
|
const result = await withTimeout(tool.execute(toolArguments, {
|
|
1569
2506
|
onOutput: (outputEvent) => {
|
|
1570
2507
|
this.emitAcodeEvent("command_output", {
|
|
@@ -1582,9 +2519,10 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1582
2519
|
: {}),
|
|
1583
2520
|
});
|
|
1584
2521
|
},
|
|
1585
|
-
|
|
2522
|
+
toolCallId: toolCall.id,
|
|
2523
|
+
}), toolTimeoutMs, `ACode built-in tool ${tool.toolName}`);
|
|
1586
2524
|
const toolDuration = Date.now() - toolStart;
|
|
1587
|
-
|
|
2525
|
+
runtimeDebugLog(`[ACodeRuntime] ✅ Built-in tool ${tool.exposedName} completed in ${toolDuration}ms`);
|
|
1588
2526
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1589
2527
|
return;
|
|
1590
2528
|
}
|
|
@@ -1598,7 +2536,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1598
2536
|
return;
|
|
1599
2537
|
}
|
|
1600
2538
|
const resultText = toToolResultText(result);
|
|
1601
|
-
|
|
2539
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitting mcp_status tool_result for ${tool.exposedName}, ` +
|
|
1602
2540
|
`result length: ${resultText.length} chars`);
|
|
1603
2541
|
this.emitAcodeEvent("mcp_status", {
|
|
1604
2542
|
action: "tool_result",
|
|
@@ -1611,7 +2549,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1611
2549
|
tool_call_id: toolCall.id,
|
|
1612
2550
|
content: resultText,
|
|
1613
2551
|
});
|
|
1614
|
-
await this.appendSyntheticMultimodalUserMessage(resultText, contextVersion);
|
|
2552
|
+
await this.appendSyntheticMultimodalUserMessage(resultText, toolCall.id, contextVersion);
|
|
1615
2553
|
continue;
|
|
1616
2554
|
}
|
|
1617
2555
|
// MCP工具处理
|
|
@@ -1627,7 +2565,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1627
2565
|
continue;
|
|
1628
2566
|
}
|
|
1629
2567
|
const toolArguments = coerceArguments(toolCall.function.arguments);
|
|
1630
|
-
|
|
2568
|
+
runtimeDebugLog(`[ACodeRuntime] MCP tool arguments: ${JSON.stringify(toolArguments).slice(0, 200)}`);
|
|
1631
2569
|
assertToolPermission(tool, this.state.permissions, commandAllowedTools);
|
|
1632
2570
|
const preResults = await this.hookBus.dispatch("PreToolUse", {
|
|
1633
2571
|
source: "mcp",
|
|
@@ -1644,7 +2582,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1644
2582
|
console.error(`[ACodeRuntime] ❌ MCP tool denied by hook: ${tool.exposedName}: ${denied.message}`);
|
|
1645
2583
|
throw new Error(denied.message || `ACode tool denied by hook: ${tool.exposedName}`);
|
|
1646
2584
|
}
|
|
1647
|
-
|
|
2585
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitting mcp_status call_tool for MCP ${tool.exposedName}`);
|
|
1648
2586
|
this.emitAcodeEvent("mcp_status", {
|
|
1649
2587
|
action: "call_tool",
|
|
1650
2588
|
server: tool.serverName,
|
|
@@ -1659,11 +2597,15 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1659
2597
|
? this.state.provider.mcpPollingToolTimeoutMs
|
|
1660
2598
|
: this.state.provider.mcpToolTimeoutMs;
|
|
1661
2599
|
const toolStart = Date.now();
|
|
1662
|
-
|
|
2600
|
+
runtimeDebugLog(`[ACodeRuntime] ▶️ Executing MCP tool: ${tool.exposedName} ` +
|
|
1663
2601
|
`(timeout: ${toolTimeoutMs}ms, polling: ${isPollingTool})`);
|
|
1664
|
-
|
|
2602
|
+
// poll_message 工具需要同时检测子 agent 完成事件(有就返回类型),
|
|
2603
|
+
// 使用 sub-agent race 包装器替代直接 MCP 调用。
|
|
2604
|
+
const result = isPollingTool && getSemanticToolName(tool.toolName) === "poll_message"
|
|
2605
|
+
? await this.callPollMessageWithSubAgentRace(connection, tool, toolArguments, toolTimeoutMs)
|
|
2606
|
+
: await this.callMcpToolWithTimeout(connection, tool, toolArguments, toolTimeoutMs, isPollingTool);
|
|
1665
2607
|
const toolDuration = Date.now() - toolStart;
|
|
1666
|
-
|
|
2608
|
+
runtimeDebugLog(`[ACodeRuntime] ✅ MCP tool ${tool.exposedName} completed in ${toolDuration}ms`);
|
|
1667
2609
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
1668
2610
|
return;
|
|
1669
2611
|
}
|
|
@@ -1678,7 +2620,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1678
2620
|
return;
|
|
1679
2621
|
}
|
|
1680
2622
|
const resultText = toToolResultText(result);
|
|
1681
|
-
|
|
2623
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitting mcp_status tool_result for MCP ${tool.exposedName}, ` +
|
|
1682
2624
|
`result length: ${resultText.length} chars`);
|
|
1683
2625
|
this.emitAcodeEvent("mcp_status", {
|
|
1684
2626
|
action: "tool_result",
|
|
@@ -1691,31 +2633,60 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1691
2633
|
tool_call_id: toolCall.id,
|
|
1692
2634
|
content: resultText,
|
|
1693
2635
|
});
|
|
1694
|
-
await this.appendSyntheticMultimodalUserMessage(resultText, contextVersion);
|
|
2636
|
+
await this.appendSyntheticMultimodalUserMessage(resultText, toolCall.id, contextVersion);
|
|
1695
2637
|
}
|
|
1696
2638
|
catch (error) {
|
|
1697
|
-
|
|
2639
|
+
const isTimeout = error instanceof Error &&
|
|
2640
|
+
error.message.includes("timed out after");
|
|
2641
|
+
const isAbortError = error instanceof Error &&
|
|
2642
|
+
error.name === "AbortError";
|
|
2643
|
+
console.error(`[ACodeRuntime] ❌ Tool execution failed: ${toolCall.function.name}`, `isTimeout=${isTimeout}, isAbortError=${isAbortError}`, error instanceof Error ? error.message : String(error));
|
|
1698
2644
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
2645
|
+
runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded after tool error, skipping`);
|
|
1699
2646
|
return;
|
|
1700
2647
|
}
|
|
1701
2648
|
if (this.abortRequested || this.state.terminated) {
|
|
1702
|
-
|
|
2649
|
+
// ★ 修复 BUG(时间线永久卡在"编辑文件"):
|
|
2650
|
+
// abort/termination 时原实现直接 rethrow,跳过 appendToolFailureResult
|
|
2651
|
+
// (3752 行的 mcp_status tool_result 发射),导致 adapter 永远收不到 tool_result,
|
|
2652
|
+
// tool_use_end 永不发射,前端时间线永久卡在 tool_use_start 创建的工具条目。
|
|
2653
|
+
// 修复:在 rethrow 前补发一个带 failed=true 的 tool_result,关闭时间线条目。
|
|
2654
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Rethrowing error due to abort/termination ` +
|
|
2655
|
+
`(abortRequested=${this.abortRequested}, terminated=${this.state.terminated})`);
|
|
2656
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitting mcp_status tool_result (failed, aborted) ` +
|
|
2657
|
+
`for ${tool.exposedName} before rethrow to close timeline entry`);
|
|
2658
|
+
this.emitAcodeEvent("mcp_status", {
|
|
2659
|
+
action: "tool_result",
|
|
2660
|
+
server: tool.kind === "mcp" ? tool.serverName : "builtin",
|
|
2661
|
+
tool: tool.toolName,
|
|
2662
|
+
result: `[aborted] ${error instanceof Error ? error.message : String(error)}`,
|
|
2663
|
+
failed: true,
|
|
2664
|
+
aborted: true,
|
|
2665
|
+
});
|
|
1703
2666
|
throw error instanceof Error ? error : new Error(String(error));
|
|
1704
2667
|
}
|
|
2668
|
+
runtimeDebugLog(`[ACodeRuntime] Calling appendToolFailureResult for ${tool.exposedName} ` +
|
|
2669
|
+
`(isTimeout=${isTimeout})`);
|
|
1705
2670
|
this.appendToolFailureResult(toolCall, tool, error);
|
|
1706
2671
|
}
|
|
1707
2672
|
}
|
|
1708
|
-
|
|
2673
|
+
runtimeDebugLog(`[ACodeRuntime] handleToolCalls completed for all ${toolCalls.length} tool(s)`);
|
|
1709
2674
|
}
|
|
1710
2675
|
async callMcpToolWithTimeout(connection, tool, toolArguments, timeoutMs, retrySdkTimeout) {
|
|
1711
2676
|
const sdkTimeout = timeoutMs > 0 ? timeoutMs : MCP_SDK_MAX_SAFE_TIMEOUT_MS;
|
|
2677
|
+
// 子 agent 共享父 agent 的 MCP 连接时,注入标记让 MCP server 跳过状态上报
|
|
2678
|
+
// (StatusReporter 绑定父 agentId,子 agent 的事件不应出现在父 agent 时间线上)
|
|
2679
|
+
const isSubAgent = Boolean(this.config.sharedMcpConnections);
|
|
2680
|
+
const finalArguments = isSubAgent
|
|
2681
|
+
? { ...toolArguments, _aws_sub_agent: true }
|
|
2682
|
+
: toolArguments;
|
|
1712
2683
|
while (true) {
|
|
1713
2684
|
const stopWaiter = this.createRuntimeStopWaiter(`ACode MCP tool ${tool.serverName}/${tool.toolName}`);
|
|
1714
2685
|
try {
|
|
1715
2686
|
return await withTimeout(Promise.race([
|
|
1716
2687
|
connection.client.callTool({
|
|
1717
2688
|
name: tool.toolName,
|
|
1718
|
-
arguments:
|
|
2689
|
+
arguments: finalArguments,
|
|
1719
2690
|
}, undefined, { timeout: sdkTimeout }),
|
|
1720
2691
|
stopWaiter.promise,
|
|
1721
2692
|
]), timeoutMs, `ACode MCP tool ${tool.serverName}/${tool.toolName}`);
|
|
@@ -1735,6 +2706,568 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1735
2706
|
}
|
|
1736
2707
|
}
|
|
1737
2708
|
}
|
|
2709
|
+
/**
|
|
2710
|
+
* 将子 agent 终态通知转为 poll_message 格式的合成 DirectMessage。
|
|
2711
|
+
* 主流程:构建符合 MCP poll_message 返回格式的 JSON 字符串。
|
|
2712
|
+
*/
|
|
2713
|
+
static formatSubAgentCompletionAsPollResult(notifications) {
|
|
2714
|
+
const directMessages = notifications.map((n) => {
|
|
2715
|
+
const summary = n.error
|
|
2716
|
+
? `Sub-agent ${n.subId} [${n.status}]: ${n.description}\nError: ${n.error}`
|
|
2717
|
+
: `Sub-agent ${n.subId} [${n.status}]: ${n.description}\nOutput: ${n.output.length > 500 ? n.output.slice(0, 500) + "... (truncated)" : n.output}`;
|
|
2718
|
+
return {
|
|
2719
|
+
msgId: `subagent-completion-${n.subId}-${Date.now()}`,
|
|
2720
|
+
senderId: "ACode-Runtime",
|
|
2721
|
+
senderName: "系统",
|
|
2722
|
+
content: summary,
|
|
2723
|
+
requireReply: false,
|
|
2724
|
+
prompt: `Sub-agent ${n.subId} (${n.description}) has reached terminal state: ${n.status}. Review its output and decide next steps.`,
|
|
2725
|
+
timestamp: new Date().toISOString(),
|
|
2726
|
+
};
|
|
2727
|
+
});
|
|
2728
|
+
return {
|
|
2729
|
+
content: [
|
|
2730
|
+
{
|
|
2731
|
+
type: "text",
|
|
2732
|
+
text: JSON.stringify({
|
|
2733
|
+
directMessages,
|
|
2734
|
+
groupMessages: [],
|
|
2735
|
+
fromBuffer: false,
|
|
2736
|
+
systemMessages: [],
|
|
2737
|
+
}, null, 2),
|
|
2738
|
+
},
|
|
2739
|
+
],
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* 将子 agent 终态通知合并到已有的 MCP poll_message 返回结果中。
|
|
2744
|
+
* 主流程:解析 MCP 返回的 JSON -> 将通知转为 DirectMessage 插入到 directMessages 前面 -> 重新序列化。
|
|
2745
|
+
*/
|
|
2746
|
+
static mergeSubAgentCompletionsIntoMcpResult(mcpResult, notifications) {
|
|
2747
|
+
if (notifications.length === 0)
|
|
2748
|
+
return mcpResult;
|
|
2749
|
+
// 提取 MCP 结果中的文本
|
|
2750
|
+
const resultText = toToolResultText(mcpResult);
|
|
2751
|
+
let parsed;
|
|
2752
|
+
try {
|
|
2753
|
+
parsed = JSON.parse(resultText);
|
|
2754
|
+
}
|
|
2755
|
+
catch {
|
|
2756
|
+
// 如果 MCP 结果不是 JSON(异常情况),直接返回原结果
|
|
2757
|
+
return mcpResult;
|
|
2758
|
+
}
|
|
2759
|
+
const subAgentMessages = notifications.map((n) => {
|
|
2760
|
+
const summary = n.error
|
|
2761
|
+
? `Sub-agent ${n.subId} [${n.status}]: ${n.description}\nError: ${n.error}`
|
|
2762
|
+
: `Sub-agent ${n.subId} [${n.status}]: ${n.description}\nOutput: ${n.output.length > 500 ? n.output.slice(0, 500) + "... (truncated)" : n.output}`;
|
|
2763
|
+
return {
|
|
2764
|
+
msgId: `subagent-completion-${n.subId}-${Date.now()}`,
|
|
2765
|
+
senderId: "ACode-Runtime",
|
|
2766
|
+
senderName: "系统",
|
|
2767
|
+
content: summary,
|
|
2768
|
+
requireReply: false,
|
|
2769
|
+
prompt: `Sub-agent ${n.subId} (${n.description}) has reached terminal state: ${n.status}. Review its output and decide next steps.`,
|
|
2770
|
+
timestamp: new Date().toISOString(),
|
|
2771
|
+
};
|
|
2772
|
+
});
|
|
2773
|
+
const mergedDirectMessages = [
|
|
2774
|
+
...subAgentMessages,
|
|
2775
|
+
...(parsed.directMessages ?? []),
|
|
2776
|
+
];
|
|
2777
|
+
return {
|
|
2778
|
+
content: [
|
|
2779
|
+
{
|
|
2780
|
+
type: "text",
|
|
2781
|
+
text: JSON.stringify({
|
|
2782
|
+
...parsed,
|
|
2783
|
+
directMessages: mergedDirectMessages,
|
|
2784
|
+
}, null, 2),
|
|
2785
|
+
},
|
|
2786
|
+
],
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* Memory 自动委托 sub-agent 完成一轮后的外部重新检查。
|
|
2791
|
+
*
|
|
2792
|
+
* 查询服务端 memory 真实状态(通过 MCP 工具直接调用,非 LLM 自报),
|
|
2793
|
+
* 判断 sub-agent 是否真正完成了处理:
|
|
2794
|
+
* - memory_file_change:检查每条 memory 的 memory_status 是否已回到 normal
|
|
2795
|
+
* - memory_expiration:检查每条 memory 是否已续期(expiresAt 在未来)或已清理(read 返回 null)
|
|
2796
|
+
* - memory_cleanup:检查 get_memory_stats 中对应类型/域是否仍 exceeded
|
|
2797
|
+
*
|
|
2798
|
+
* 若仍有未完成项,返回 { done: false, continueMessage } 包含 "经检查,未完成对xxx的处理,请继续"
|
|
2799
|
+
* 及具体未完成列表,由 SubAgentManager 注入同一 sub-agent 的下一轮 submit。
|
|
2800
|
+
*
|
|
2801
|
+
* recheck 自身出错(MCP 连接不可用、工具调用超时等)时返回 { done: true },不阻塞 sub-agent 结束。
|
|
2802
|
+
*/
|
|
2803
|
+
async recheckMemoryDelegate(type, uris, output, turn) {
|
|
2804
|
+
const connection = this.state.mcpConnections.find((c) => c.serverName === 'aws-client-agent-mcp');
|
|
2805
|
+
if (!connection) {
|
|
2806
|
+
runtimeDebugLog(`[ACodeRuntime] recheckMemoryDelegate: aws-client-agent-mcp connection not found, treating as done`);
|
|
2807
|
+
return { done: true };
|
|
2808
|
+
}
|
|
2809
|
+
try {
|
|
2810
|
+
if (type === 'memory_file_change') {
|
|
2811
|
+
return await this.recheckMemoryFileChange(connection, uris);
|
|
2812
|
+
}
|
|
2813
|
+
if (type === 'memory_expiration') {
|
|
2814
|
+
return await this.recheckMemoryExpiration(connection, uris);
|
|
2815
|
+
}
|
|
2816
|
+
return await this.recheckMemoryCleanup(connection, uris);
|
|
2817
|
+
}
|
|
2818
|
+
catch (err) {
|
|
2819
|
+
runtimeDebugLog(`[ACodeRuntime] recheckMemoryDelegate failed (type=${type}, turn=${turn}): ` +
|
|
2820
|
+
`${err instanceof Error ? err.message : String(err)}, treating as done`);
|
|
2821
|
+
return { done: true };
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* 通过 MCP read_memory 工具读取单条 memory 记录。
|
|
2826
|
+
* 注入 _aws_sub_agent=true 避免状态上报污染父 agent 时间线。
|
|
2827
|
+
* 返回 null 表示 memory 不存在或读取失败。
|
|
2828
|
+
*/
|
|
2829
|
+
async readMemoryViaMcp(connection, uri) {
|
|
2830
|
+
try {
|
|
2831
|
+
const result = await withTimeout(connection.client.callTool({
|
|
2832
|
+
name: 'read_memory',
|
|
2833
|
+
arguments: { uri, _aws_sub_agent: true },
|
|
2834
|
+
}, undefined, { timeout: 30_000 }), 30_000, `recheck read_memory ${uri}`);
|
|
2835
|
+
const text = toToolResultText(result);
|
|
2836
|
+
const parsed = JSON.parse(text);
|
|
2837
|
+
if (parsed === null || parsed === undefined)
|
|
2838
|
+
return null;
|
|
2839
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed))
|
|
2840
|
+
return null;
|
|
2841
|
+
return parsed;
|
|
2842
|
+
}
|
|
2843
|
+
catch {
|
|
2844
|
+
return null;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
/** recheck memory_file_change:检查 memory_status 是否仍为 updating */
|
|
2848
|
+
async recheckMemoryFileChange(connection, uris) {
|
|
2849
|
+
const stillUpdating = [];
|
|
2850
|
+
await Promise.all(uris.map(async (uri) => {
|
|
2851
|
+
const mem = await this.readMemoryViaMcp(connection, uri);
|
|
2852
|
+
if (mem && mem.memory_status === 'updating') {
|
|
2853
|
+
stillUpdating.push(uri);
|
|
2854
|
+
}
|
|
2855
|
+
}));
|
|
2856
|
+
if (stillUpdating.length === 0) {
|
|
2857
|
+
runtimeDebugLog(`[ACodeRuntime] recheck memory_file_change: all ${uris.length} URIs done (memory_status=normal or deleted)`);
|
|
2858
|
+
return { done: true };
|
|
2859
|
+
}
|
|
2860
|
+
const list = stillUpdating.map((u) => `- ${u}`).join('\n');
|
|
2861
|
+
runtimeDebugLog(`[ACodeRuntime] recheck memory_file_change: ${stillUpdating.length}/${uris.length} URIs still updating`);
|
|
2862
|
+
return {
|
|
2863
|
+
done: false,
|
|
2864
|
+
continueMessage: `经检查,未完成对以下 memory 的更新,请继续:\n${list}\n\n` +
|
|
2865
|
+
`这些 memory 的 memory_status 仍为 "updating"。请重新 read_memory 查看内容,` +
|
|
2866
|
+
`查看绑定文件最新内容,并调用 update_memory 完成更新。`,
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
/** recheck memory_expiration:检查 expiresAt 是否仍过期 或 memory_status 是否仍为 recalling */
|
|
2870
|
+
async recheckMemoryExpiration(connection, uris) {
|
|
2871
|
+
const stillExpired = [];
|
|
2872
|
+
const now = Date.now();
|
|
2873
|
+
await Promise.all(uris.map(async (uri) => {
|
|
2874
|
+
const mem = await this.readMemoryViaMcp(connection, uri);
|
|
2875
|
+
if (!mem)
|
|
2876
|
+
return; // memory 已删除/不存在 → 视为已处理
|
|
2877
|
+
const expiresAt = mem.expiresAt ?? mem.expires_at;
|
|
2878
|
+
const isRecalling = mem.memory_status === 'recalling';
|
|
2879
|
+
const isExpired = typeof expiresAt === 'string' && new Date(expiresAt).getTime() < now;
|
|
2880
|
+
if (isRecalling || isExpired) {
|
|
2881
|
+
stillExpired.push(uri);
|
|
2882
|
+
}
|
|
2883
|
+
}));
|
|
2884
|
+
if (stillExpired.length === 0) {
|
|
2885
|
+
runtimeDebugLog(`[ACodeRuntime] recheck memory_expiration: all ${uris.length} URIs done (renewed, cleaned, or no longer expired)`);
|
|
2886
|
+
return { done: true };
|
|
2887
|
+
}
|
|
2888
|
+
const list = stillExpired.map((u) => `- ${u}`).join('\n');
|
|
2889
|
+
runtimeDebugLog(`[ACodeRuntime] recheck memory_expiration: ${stillExpired.length}/${uris.length} URIs still expired or recalling`);
|
|
2890
|
+
return {
|
|
2891
|
+
done: false,
|
|
2892
|
+
continueMessage: `经检查,未完成对以下 memory 的处理,请继续:\n${list}\n\n` +
|
|
2893
|
+
`这些 memory 仍处于过期或回忆中状态。请调用 memory_recurrence 获取内容,` +
|
|
2894
|
+
`对需要保留的调用 update_memory 续期,对无价值的调用 memory_recurrence_complete 清理。`,
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
/** recheck memory_cleanup:检查 get_memory_stats 中对应类型/域是否仍 exceeded */
|
|
2898
|
+
async recheckMemoryCleanup(connection, uris) {
|
|
2899
|
+
// delegate URIs 格式为 "${memoryType}/${domain}"
|
|
2900
|
+
let stats;
|
|
2901
|
+
try {
|
|
2902
|
+
const result = await withTimeout(connection.client.callTool({
|
|
2903
|
+
name: 'get_memory_stats',
|
|
2904
|
+
arguments: { _aws_sub_agent: true },
|
|
2905
|
+
}, undefined, { timeout: 30_000 }), 30_000, 'recheck get_memory_stats');
|
|
2906
|
+
const text = toToolResultText(result);
|
|
2907
|
+
stats = JSON.parse(text);
|
|
2908
|
+
}
|
|
2909
|
+
catch {
|
|
2910
|
+
// stats 查询失败 → 视为完成
|
|
2911
|
+
return { done: true };
|
|
2912
|
+
}
|
|
2913
|
+
const byType = (stats.by_type ?? {});
|
|
2914
|
+
const stillExceeded = [];
|
|
2915
|
+
for (const uri of uris) {
|
|
2916
|
+
const sepIdx = uri.indexOf('/');
|
|
2917
|
+
if (sepIdx < 0)
|
|
2918
|
+
continue;
|
|
2919
|
+
const memoryType = uri.slice(0, sepIdx);
|
|
2920
|
+
const domain = uri.slice(sepIdx + 1);
|
|
2921
|
+
const typeStats = byType[memoryType];
|
|
2922
|
+
const domainStats = typeStats?.by_domain?.[domain];
|
|
2923
|
+
if (domainStats?.exceeded) {
|
|
2924
|
+
stillExceeded.push(`${memoryType}/${domain} (当前 ${domainStats.count ?? '?'} 条,建议上限 ${domainStats.limit ?? '?'})`);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
if (stillExceeded.length === 0) {
|
|
2928
|
+
runtimeDebugLog(`[ACodeRuntime] recheck memory_cleanup: all ${uris.length} type/domain pairs within limits`);
|
|
2929
|
+
return { done: true };
|
|
2930
|
+
}
|
|
2931
|
+
const list = stillExceeded.map((u) => `- ${u}`).join('\n');
|
|
2932
|
+
runtimeDebugLog(`[ACodeRuntime] recheck memory_cleanup: ${stillExceeded.length}/${uris.length} type/domain pairs still exceeded`);
|
|
2933
|
+
return {
|
|
2934
|
+
done: false,
|
|
2935
|
+
continueMessage: `经检查,以下类型/域尚未整理完成,请继续:\n${list}\n\n` +
|
|
2936
|
+
`请调用 list_memories 查看超限记忆,删除不再需要的 recently_memory 或对 agent_memory 调用 update_memory 续期。`,
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
/**
|
|
2940
|
+
* 构造 memory 自动委托子 agent 的 prompt。
|
|
2941
|
+
* 主流程:根据 delegate 类型(过期整理/文件变更更新)生成自包含指令。
|
|
2942
|
+
*/
|
|
2943
|
+
buildMemoryDelegatePrompt(delegate) {
|
|
2944
|
+
const urisText = delegate.uris.map(u => `- ${u}`).join('\n');
|
|
2945
|
+
if (delegate.type === 'memory_expiration') {
|
|
2946
|
+
return `你是一条专门处理 memory 过期整理的子 agent。
|
|
2947
|
+
|
|
2948
|
+
当前有以下 ${delegate.uris.length} 条即将遗忘的记忆需要整理:
|
|
2949
|
+
${urisText}
|
|
2950
|
+
|
|
2951
|
+
请执行以下步骤(核心逻辑:处理 → 验证 → 未完成则继续):
|
|
2952
|
+
|
|
2953
|
+
=== 处理阶段 ===
|
|
2954
|
+
1. 调用 memory_recurrence(limit=10) 分批获取这些过期记忆的完整内容(最早过期优先)
|
|
2955
|
+
2. 逐条分析:这些内容是否仍然有价值?是否需要保留、更新、归档或删除?
|
|
2956
|
+
3. 对于需要保留的,调用 update_memory 续期;对于无价值的,调用 memory_recurrence_complete 将其清理到永久记忆或删除
|
|
2957
|
+
|
|
2958
|
+
=== 验证阶段(关键,不可跳过) ===
|
|
2959
|
+
4. 再次调用 memory_recurrence(limit=5) 检查是否还有未处理的过期记忆
|
|
2960
|
+
- 如果返回结果中有之前处理过的 URI,说明更新/清理未生效
|
|
2961
|
+
- 如果仍有待处理条目,说明部分操作未完成
|
|
2962
|
+
5. 仔细检查返回结果:
|
|
2963
|
+
- ❌ 若仍有过期记忆未处理(或处理未成功):输出 "经检查,未完成对以下 memory 的处理,请继续:" 列出还记忆,然后回到步骤 1 继续处理
|
|
2964
|
+
- ✅ 若全部处理完毕(返回空或无过期条目):静默结束
|
|
2965
|
+
|
|
2966
|
+
注意:你运行在独立的上下文窗口中,不影响主 agent 的主线任务。请高效完成,不要询问用户。
|
|
2967
|
+
完成后系统会通过查询服务端真实状态进行二次验证(不依赖你的自报结果)。若验证发现仍有 memory 未处理,会发送 "经检查,未完成对以下 memory 的处理,请继续" 消息让你继续处理。`;
|
|
2968
|
+
}
|
|
2969
|
+
if (delegate.type === 'memory_cleanup') {
|
|
2970
|
+
return `你是一条专门处理 memory 容量整理的子 agent。
|
|
2971
|
+
|
|
2972
|
+
以下 memory 类型/域已超过建议容量,需要整理:
|
|
2973
|
+
${urisText}
|
|
2974
|
+
|
|
2975
|
+
请执行以下步骤(核心逻辑:处理 → 验证 → 未完成则继续):
|
|
2976
|
+
|
|
2977
|
+
=== 处理阶段 ===
|
|
2978
|
+
1. 调用 get_memory_stats 查看容量详情
|
|
2979
|
+
2. 调用 list_memories 按类型查看超限记忆
|
|
2980
|
+
3. 删除不再需要的 recently_memory,或更新 agent_memory 续期
|
|
2981
|
+
|
|
2982
|
+
=== 验证阶段(关键,不可跳过) ===
|
|
2983
|
+
4. 再次调用 get_memory_stats 检查各类型/域的 count 是否已降到建议上限以下
|
|
2984
|
+
5. 仔细检查返回结果:
|
|
2985
|
+
- ❌ 若仍有类型/域超过容量上限:输出 "经检查,以下类型/域尚未整理完成,请继续:" 列出具体类型/域和当前容量,然后回到步骤 1 继续处理
|
|
2986
|
+
- ✅ 若全部在建议上限内:静默结束
|
|
2987
|
+
|
|
2988
|
+
注意:你运行在独立的上下文窗口中,不影响主 agent 的主线任务。请高效完成,不要询问用户。
|
|
2989
|
+
完成后系统会通过查询服务端真实状态进行二次验证(不依赖你的自报结果)。若验证发现仍有类型/域超过容量上限,会发送 "经检查,以下类型/域尚未整理完成,请继续" 消息让你继续处理。`;
|
|
2990
|
+
}
|
|
2991
|
+
return `你是一条专门处理永久记忆文件变更更新的子 agent。
|
|
2992
|
+
|
|
2993
|
+
以下 ${delegate.uris.length} 个永久记忆绑定的文件已发生变更:
|
|
2994
|
+
${urisText}
|
|
2995
|
+
|
|
2996
|
+
请执行以下步骤(核心逻辑:更新 → 验证 → 未完成则继续):
|
|
2997
|
+
|
|
2998
|
+
=== 更新阶段 ===
|
|
2999
|
+
1. 调用 read_memory 读取这些 memory 的当前内容
|
|
3000
|
+
2. 查看相关绑定文件的最新内容(bound_files 中列出),理解变更点
|
|
3001
|
+
3. 调用 update_memory 更新 memory 内容,确保与文件最新状态同步
|
|
3002
|
+
|
|
3003
|
+
=== 验证阶段(关键,不可跳过) ===
|
|
3004
|
+
4. 调用 read_memory 重新读取这些 memory,检查 memory_status 字段:
|
|
3005
|
+
- 正常情况:Java 服务端会在 update_memory 后自动将 memory_status 恢复为 normal
|
|
3006
|
+
5. 仔细检查每个 memory 的状态:
|
|
3007
|
+
- ❌ 若仍有 memory 的 memory_status 为 "updating":说明更新未生效,输出 "经检查,未完成对以下 memory 的更新,请继续:" 列出具体 memory URI 和当前状态,然后回到步骤 1 继续处理
|
|
3008
|
+
- ✅ 若全部 memory_status 为 "normal":静默结束
|
|
3009
|
+
|
|
3010
|
+
注意:你运行在独立的上下文窗口中,不影响主 agent 的主线任务。请高效完成,不要询问用户。
|
|
3011
|
+
完成后系统会通过查询服务端真实状态进行二次验证(不依赖你的自报结果)。若验证发现仍有 memory 的 memory_status 为 "updating",会发送 "经检查,未完成对以下 memory 的更新,请继续" 消息让你继续处理。`;
|
|
3012
|
+
}
|
|
3013
|
+
/**
|
|
3014
|
+
* 处理 poll_message 结果中的 memory 自动委托标记 (_acodeAutoDelegate)。
|
|
3015
|
+
* 主流程:解析 MCP 返回 -> 识别标记 -> spawn 子 agent -> 返回简化结果。
|
|
3016
|
+
* 非 ACode 运行时(无 subAgentManager)直接透传原结果,不受影响。
|
|
3017
|
+
* 返回 null 表示全部消息已被委托,调用方应重新 poll_message 继续等待真正的消息。
|
|
3018
|
+
*/
|
|
3019
|
+
async processMemoryAutoDelegates(mcpResult) {
|
|
3020
|
+
const manager = this.config.subAgentManager;
|
|
3021
|
+
if (!manager) {
|
|
3022
|
+
return mcpResult;
|
|
3023
|
+
}
|
|
3024
|
+
// 提取 MCP 结果中的 JSON 文本
|
|
3025
|
+
let resultText;
|
|
3026
|
+
try {
|
|
3027
|
+
resultText = toToolResultText(mcpResult);
|
|
3028
|
+
}
|
|
3029
|
+
catch {
|
|
3030
|
+
return mcpResult;
|
|
3031
|
+
}
|
|
3032
|
+
let parsed;
|
|
3033
|
+
try {
|
|
3034
|
+
parsed = JSON.parse(resultText);
|
|
3035
|
+
}
|
|
3036
|
+
catch {
|
|
3037
|
+
return mcpResult;
|
|
3038
|
+
}
|
|
3039
|
+
const directMessages = parsed.directMessages;
|
|
3040
|
+
if (!Array.isArray(directMessages) || directMessages.length === 0) {
|
|
3041
|
+
return mcpResult;
|
|
3042
|
+
}
|
|
3043
|
+
const remainingMessages = [];
|
|
3044
|
+
let delegateEncountered = 0;
|
|
3045
|
+
for (const msg of directMessages) {
|
|
3046
|
+
const delegate = msg._acodeAutoDelegate;
|
|
3047
|
+
if (delegate && (delegate.type === 'memory_expiration' || delegate.type === 'memory_file_change' || delegate.type === 'memory_cleanup')) {
|
|
3048
|
+
delegateEncountered++;
|
|
3049
|
+
// 过滤掉已在处理中的 URI
|
|
3050
|
+
const newUris = delegate.uris.filter(uri => !this.pendingMemorySubAgents.has(uri));
|
|
3051
|
+
if (newUris.length === 0) {
|
|
3052
|
+
// 所有 URI 都在处理中,跳过此通知(不委托也不返回给主 agent)
|
|
3053
|
+
runtimeDebugLog(`[ACodeRuntime] Memory ${delegate.type} skipped: all ${delegate.uris.length} URIs already being processed by sub-agents`);
|
|
3054
|
+
continue;
|
|
3055
|
+
}
|
|
3056
|
+
// 用过滤后的 URI 构造 prompt
|
|
3057
|
+
const prompt = this.buildMemoryDelegatePrompt({
|
|
3058
|
+
type: delegate.type,
|
|
3059
|
+
uris: newUris,
|
|
3060
|
+
});
|
|
3061
|
+
try {
|
|
3062
|
+
const subId = await manager.spawn({
|
|
3063
|
+
parentAgentId: this.config.agentId || 'unknown',
|
|
3064
|
+
parentSessionId: this.config.sessionId,
|
|
3065
|
+
subagentType: 'general',
|
|
3066
|
+
description: `Memory ${delegate.type} auto-delegate (${newUris.length} items)`,
|
|
3067
|
+
prompt,
|
|
3068
|
+
workingDirectory: this.config.workingDirectory,
|
|
3069
|
+
parentConfig: {
|
|
3070
|
+
...this.config,
|
|
3071
|
+
// 注入父 agent 的 MCP 连接,子 agent 复用而非启动新 MCP server 进程
|
|
3072
|
+
sharedMcpConnections: this.state?.mcpConnections,
|
|
3073
|
+
},
|
|
3074
|
+
silent: true,
|
|
3075
|
+
});
|
|
3076
|
+
// 注册完成回调:子 agent 终态时从跟踪表中移除对应 URI
|
|
3077
|
+
for (const uri of newUris) {
|
|
3078
|
+
this.pendingMemorySubAgents.set(uri, subId);
|
|
3079
|
+
}
|
|
3080
|
+
const uriSet = new Set(newUris);
|
|
3081
|
+
manager.subscribe(subId, () => {
|
|
3082
|
+
const entry = manager.getSubAgent(subId);
|
|
3083
|
+
if (entry && (entry.status === 'completed' || entry.status === 'failed' || entry.status === 'cancelled')) {
|
|
3084
|
+
for (const uri of uriSet) {
|
|
3085
|
+
if (this.pendingMemorySubAgents.get(uri) === subId) {
|
|
3086
|
+
this.pendingMemorySubAgents.delete(uri);
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
});
|
|
3091
|
+
// 使用带 recheck 循环的启动方式:
|
|
3092
|
+
// 每轮 submit 完成后,通过 recheckMemoryDelegate 查询服务端真实状态,
|
|
3093
|
+
// 若 memory 仍未处理好(memory_status=updating / 仍过期 / 容量仍超限),
|
|
3094
|
+
// 则将 "经检查,未完成对xxx的处理,请继续" 注入同一 sub-agent 的下一轮 submit。
|
|
3095
|
+
// 最多重试 3 次,提供系统级硬保证(不依赖 LLM 自报结果)。
|
|
3096
|
+
const delegateType = delegate.type;
|
|
3097
|
+
const recheckFn = async (output, turn) => {
|
|
3098
|
+
return this.recheckMemoryDelegate(delegateType, newUris, output, turn);
|
|
3099
|
+
};
|
|
3100
|
+
await manager.startWithRecheck(subId, recheckFn, 3);
|
|
3101
|
+
runtimeDebugLog(`[ACodeRuntime] Auto-delegated memory ${delegate.type} to sub-agent ${subId} (${newUris.length} URIs, ${delegate.uris.length - newUris.length} already in progress)`);
|
|
3102
|
+
}
|
|
3103
|
+
catch (e) {
|
|
3104
|
+
// 委托失败时不返回给主 agent:memory 通知不应打断主 agent 的主线任务,
|
|
3105
|
+
// 下次 poll_message 会重新检测并重试委托
|
|
3106
|
+
runtimeDebugLog(`[ACodeRuntime] Failed to auto-delegate memory ${delegate.type} (suppressed, will retry next poll): ${e instanceof Error ? e.message : String(e)}`);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
else {
|
|
3110
|
+
remainingMessages.push(msg);
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
// 未遇到任何 delegate 消息,原样返回(保留原始格式,避免不必要的对象创建)
|
|
3114
|
+
if (delegateEncountered === 0) {
|
|
3115
|
+
return mcpResult;
|
|
3116
|
+
}
|
|
3117
|
+
// 全部 delegate 消息已处理(spawned/skipped/failed),不返回给主 agent
|
|
3118
|
+
if (remainingMessages.length === 0) {
|
|
3119
|
+
return null;
|
|
3120
|
+
}
|
|
3121
|
+
// 有非 delegate 类型的剩余消息,返回简化后的结果
|
|
3122
|
+
const simplified = {
|
|
3123
|
+
...parsed,
|
|
3124
|
+
directMessages: remainingMessages,
|
|
3125
|
+
};
|
|
3126
|
+
return {
|
|
3127
|
+
content: [
|
|
3128
|
+
{
|
|
3129
|
+
type: 'text',
|
|
3130
|
+
text: JSON.stringify(simplified, null, 2),
|
|
3131
|
+
},
|
|
3132
|
+
],
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
/**
|
|
3136
|
+
* poll_message MCP 工具调用的子 agent 完成事件 race 包装器。
|
|
3137
|
+
*
|
|
3138
|
+
* 主流程:
|
|
3139
|
+
* 1. 先检查 completionQueue 中是否有已完成的子 agent -> 有则直接返回合成结果(不调 MCP)
|
|
3140
|
+
* 2. 启动 MCP poll_message 调用与 waitForAnyCompletion 的 race
|
|
3141
|
+
* 3. 子 agent 先完成 -> 返回合成结果(MCP 调用被放弃但仍在运行,下次 poll_message 会复用 pendingPollMessage)
|
|
3142
|
+
* 4. MCP 先返回 -> 合并新完成的子 agent 事件后返回
|
|
3143
|
+
*
|
|
3144
|
+
* 设计说明:
|
|
3145
|
+
* - MCP server 的 poll_message 内部有 while(true) 永久阻塞,只在有消息时返回
|
|
3146
|
+
* - 子 agent 完成事件由 SubAgentManager 在同进程内产生,通过 completionQueue 传递
|
|
3147
|
+
* - 当子 agent 先完成时,MCP 调用被放弃(Promise 不再被 await),MCP server 侧的
|
|
3148
|
+
* pendingPollMessage 仍在运行,下次 poll_message 调用会通过 pendingPollMessage 复用
|
|
3149
|
+
*/
|
|
3150
|
+
async callPollMessageWithSubAgentRace(connection, tool, toolArguments, timeoutMs) {
|
|
3151
|
+
const manager = this.config.subAgentManager;
|
|
3152
|
+
// 没有 SubAgentManager 时,直接走普通 MCP 调用
|
|
3153
|
+
if (!manager) {
|
|
3154
|
+
return await this.callMcpToolWithTimeout(connection, tool, toolArguments, timeoutMs, true);
|
|
3155
|
+
}
|
|
3156
|
+
// 步骤1: 先检查是否有已完成的子 agent -> 有则直接返回合成结果(不调 MCP)
|
|
3157
|
+
const completedBefore = manager.takeCompletedSubAgents();
|
|
3158
|
+
if (completedBefore.length > 0) {
|
|
3159
|
+
runtimeDebugLog(`[ACodeRuntime] poll_message: ${completedBefore.length} sub-agent(s) already completed, returning synthetic result`);
|
|
3160
|
+
return ACodeRuntime.formatSubAgentCompletionAsPollResult(completedBefore);
|
|
3161
|
+
}
|
|
3162
|
+
// 循环:MCP 返回的 memory 通知被全部委托时,重新 poll 继续等待真正的消息
|
|
3163
|
+
let isDelegateRepoll = false; // 首次正常上报,后续委托循环中抑制状态跳动
|
|
3164
|
+
while (true) {
|
|
3165
|
+
// 步骤2: 启动 MCP poll_message 调用与 waitForAnyCompletion 的 race
|
|
3166
|
+
const subAgentWaiter = manager.waitForAnyCompletion();
|
|
3167
|
+
// 委托等待循环中的后续 poll 注入标记:MCP server 跳过 reportToolUse/reportToolResult,
|
|
3168
|
+
// 只调用 reportThinking 保持"思考中"状态,避免"调用MCP↔思考中"反复跳动
|
|
3169
|
+
const pollArgs = isDelegateRepoll
|
|
3170
|
+
? { ...toolArguments, _aws_silent_poll: true }
|
|
3171
|
+
: toolArguments;
|
|
3172
|
+
const mcpCallPromise = this.callMcpToolWithTimeout(connection, tool, pollArgs, timeoutMs, true);
|
|
3173
|
+
try {
|
|
3174
|
+
const raceWinner = await Promise.race([
|
|
3175
|
+
mcpCallPromise.then((result) => ({ type: "mcp", result })),
|
|
3176
|
+
subAgentWaiter.promise.then((notif) => ({ type: "subagent", notif })),
|
|
3177
|
+
]);
|
|
3178
|
+
if (raceWinner.type === "subagent") {
|
|
3179
|
+
// 步骤3: 子 agent 先完成 -> 返回合成结果
|
|
3180
|
+
// 收集所有已完成的子 agent(race 中 resolve 的 + 队列中可能已积累的)
|
|
3181
|
+
const notifications = manager.takeCompletedSubAgents();
|
|
3182
|
+
// raceWinner.notif 可能已从队列中取出(waitForAnyCompletion 内部处理),
|
|
3183
|
+
// 也可能仍在队列中(如果 takeCompletedSubAgents 先执行)
|
|
3184
|
+
if (notifications.length === 0) {
|
|
3185
|
+
notifications.push(raceWinner.notif);
|
|
3186
|
+
}
|
|
3187
|
+
runtimeDebugLog(`[ACodeRuntime] poll_message: sub-agent ${raceWinner.notif.subId} completed during race, returning synthetic result (${notifications.length} notification(s))`);
|
|
3188
|
+
return ACodeRuntime.formatSubAgentCompletionAsPollResult(notifications);
|
|
3189
|
+
}
|
|
3190
|
+
// 步骤4: MCP 先返回 -> 合并新完成的子 agent 事件
|
|
3191
|
+
const completedAfter = manager.takeCompletedSubAgents();
|
|
3192
|
+
if (completedAfter.length > 0) {
|
|
3193
|
+
runtimeDebugLog(`[ACodeRuntime] poll_message: MCP returned, merging ${completedAfter.length} sub-agent completion(s)`);
|
|
3194
|
+
const merged = ACodeRuntime.mergeSubAgentCompletionsIntoMcpResult(raceWinner.result, completedAfter);
|
|
3195
|
+
// 合并后仍需检查 memory 自动委托
|
|
3196
|
+
const processedMerged = await this.processMemoryAutoDelegates(merged);
|
|
3197
|
+
if (processedMerged !== null) {
|
|
3198
|
+
// 校验:结果必须包含实际消息才返回,否则继续 poll
|
|
3199
|
+
if (this.pollResultHasMessages(processedMerged)) {
|
|
3200
|
+
return processedMerged;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
// 全部被委托或结果为空,等待后再重新 poll(防止紧密循环)
|
|
3204
|
+
await this.waitForDelegateRepoll();
|
|
3205
|
+
isDelegateRepoll = true;
|
|
3206
|
+
continue;
|
|
3207
|
+
}
|
|
3208
|
+
// 自动委托:识别 memory 过期/更新通知,spawn 子 agent 处理,避免打断主 agent 主线
|
|
3209
|
+
const processedResult = await this.processMemoryAutoDelegates(raceWinner.result);
|
|
3210
|
+
if (processedResult !== null) {
|
|
3211
|
+
// 校验:结果必须包含实际消息才返回,否则继续 poll
|
|
3212
|
+
if (this.pollResultHasMessages(processedResult)) {
|
|
3213
|
+
return processedResult;
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
// 全部被委托或结果为空,等待后再重新 poll(防止紧密循环)
|
|
3217
|
+
await this.waitForDelegateRepoll();
|
|
3218
|
+
isDelegateRepoll = true;
|
|
3219
|
+
}
|
|
3220
|
+
finally {
|
|
3221
|
+
subAgentWaiter.cancel();
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* 检查 poll_message 结果是否包含实际消息(directMessages 或 groupMessages 非空)。
|
|
3227
|
+
* 用于循环中过滤空结果,确保 poll_message 只在有消息时返回。
|
|
3228
|
+
*/
|
|
3229
|
+
pollResultHasMessages(result) {
|
|
3230
|
+
try {
|
|
3231
|
+
const text = toToolResultText(result);
|
|
3232
|
+
const parsed = JSON.parse(text);
|
|
3233
|
+
const dm = Array.isArray(parsed.directMessages) ? parsed.directMessages.length : 0;
|
|
3234
|
+
const gm = Array.isArray(parsed.groupMessages) ? parsed.groupMessages.length : 0;
|
|
3235
|
+
return dm > 0 || gm > 0;
|
|
3236
|
+
}
|
|
3237
|
+
catch {
|
|
3238
|
+
return true; // 无法解析时保守返回 true,避免无限循环
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
/**
|
|
3242
|
+
* 在 memory 委托消息被全部处理后,等待一段时间再重新 poll_message。
|
|
3243
|
+
*
|
|
3244
|
+
* 背景:MCP doPollMessage 对 updating memory 每次都返回通知(无去重),
|
|
3245
|
+
* 而 processMemoryAutoDelegates 因 URI 已在 pendingMemorySubAgents 中而返回 null,
|
|
3246
|
+
* 导致 while(true) 无延迟重 poll,每轮触发 handleToolCall 的 reportToolUse/reportThinking
|
|
3247
|
+
* 状态上报,引发"思考→MCP→跳过"快速循环,最终连接丢失。
|
|
3248
|
+
*
|
|
3249
|
+
* 等待策略:
|
|
3250
|
+
* 1. 固定延迟 DELEGATE_REPOLL_DELAY_MS(5s),给子 agent 时间完成 memory 更新
|
|
3251
|
+
* 2. 可被 runtime stop 中断(通过 createRuntimeStopWaiter)
|
|
3252
|
+
* 3. 不消费 completionQueue(下一轮 takeCompletedSubAgents 会取到)
|
|
3253
|
+
*/
|
|
3254
|
+
async waitForDelegateRepoll() {
|
|
3255
|
+
runtimeDebugLog(`[ACodeRuntime] poll_message: all messages delegated, waiting ${DELEGATE_REPOLL_DELAY_MS}ms before re-poll`);
|
|
3256
|
+
const stopWaiter = this.createRuntimeStopWaiter("delegate re-poll delay");
|
|
3257
|
+
try {
|
|
3258
|
+
await Promise.race([
|
|
3259
|
+
new Promise((resolve) => {
|
|
3260
|
+
const timer = setTimeout(resolve, DELEGATE_REPOLL_DELAY_MS);
|
|
3261
|
+
if (timer.unref)
|
|
3262
|
+
timer.unref();
|
|
3263
|
+
}),
|
|
3264
|
+
stopWaiter.promise,
|
|
3265
|
+
]);
|
|
3266
|
+
}
|
|
3267
|
+
finally {
|
|
3268
|
+
stopWaiter.cancel();
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
1738
3271
|
/**
|
|
1739
3272
|
* Build a one-shot waiter that rejects long-running MCP calls when the runtime stops.
|
|
1740
3273
|
* Main flow: register a stop callback -> race it with the SDK call -> remove it after either side settles.
|
|
@@ -1776,7 +3309,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1776
3309
|
notifyStop();
|
|
1777
3310
|
}
|
|
1778
3311
|
}
|
|
1779
|
-
async appendSyntheticMultimodalUserMessage(toolResultText, contextVersion = this.contextVersion) {
|
|
3312
|
+
async appendSyntheticMultimodalUserMessage(toolResultText, toolCallId, contextVersion = this.contextVersion) {
|
|
1780
3313
|
const parsedContent = parseAttachmentMessage(toolResultText);
|
|
1781
3314
|
if (!parsedContent.hasImages) {
|
|
1782
3315
|
return;
|
|
@@ -1792,11 +3325,50 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1792
3325
|
return;
|
|
1793
3326
|
}
|
|
1794
3327
|
this.state.conversation.push({ role: "user", content });
|
|
3328
|
+
// 去重:将对应 tool 消息中的图片/附件数据替换为占位符,
|
|
3329
|
+
// 避免 conversation 中同时存在原始 tool 结果和解析后的 user 消息导致重复。
|
|
3330
|
+
const toolMsg = this.state.conversation.find((m) => m.role === "tool" && m.tool_call_id === toolCallId);
|
|
3331
|
+
if (toolMsg && typeof toolMsg.content === "string") {
|
|
3332
|
+
let cleaned;
|
|
3333
|
+
// 尝试 JSON 格式:移除 attachments 数组,保留 text 和 messages
|
|
3334
|
+
try {
|
|
3335
|
+
const parsed = JSON.parse(toolMsg.content);
|
|
3336
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
3337
|
+
const clone = { ...parsed };
|
|
3338
|
+
if ("attachments" in clone) {
|
|
3339
|
+
delete clone.attachments;
|
|
3340
|
+
cleaned = JSON.stringify(clone);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
catch {
|
|
3345
|
+
// 不是 JSON,尝试 marker 格式
|
|
3346
|
+
}
|
|
3347
|
+
if (cleaned === undefined) {
|
|
3348
|
+
const markerCleaned = toolMsg.content
|
|
3349
|
+
.replace(/\[(图片|文件)\s*:\s*([^\]\n]*?)\s*\]/gi, "")
|
|
3350
|
+
.trim();
|
|
3351
|
+
if (markerCleaned !== toolMsg.content) {
|
|
3352
|
+
cleaned = markerCleaned;
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
if (cleaned !== undefined && cleaned.length > 0) {
|
|
3356
|
+
toolMsg.content = cleaned;
|
|
3357
|
+
}
|
|
3358
|
+
else if (cleaned !== undefined) {
|
|
3359
|
+
toolMsg.content = "[Tool result contained images; see user message above]";
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
1795
3362
|
}
|
|
1796
3363
|
appendToolFailureResult(toolCall, tool, error) {
|
|
1797
3364
|
const resultText = toToolErrorResultText(tool, error);
|
|
3365
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 appendToolFailureResult: emitting mcp_status tool_result ` +
|
|
3366
|
+
`(failed: true) for ${tool.exposedName}, result length: ${resultText.length} chars`);
|
|
3367
|
+
// 工具执行失败是正常运行过程的一部分(Agent 会继续处理失败结果)。
|
|
3368
|
+
// 标记 nonFatal=true 让 adapter 不变更运行态为 "error",避免面板按钮异常。
|
|
1798
3369
|
this.emitAcodeEvent("error", {
|
|
1799
3370
|
message: `ACode tool failed: ${tool.exposedName}: ${getErrorMessage(error)}`,
|
|
3371
|
+
nonFatal: true,
|
|
1800
3372
|
});
|
|
1801
3373
|
this.emitAcodeEvent("mcp_status", {
|
|
1802
3374
|
action: "tool_result",
|
|
@@ -1811,6 +3383,21 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1811
3383
|
content: resultText,
|
|
1812
3384
|
});
|
|
1813
3385
|
}
|
|
3386
|
+
/**
|
|
3387
|
+
* 为子 agent 过滤共享 MCP 连接:只保留 memory 工具。
|
|
3388
|
+
*
|
|
3389
|
+
* 原 MCP server 进程通过 AWS_SUB_AGENT=true 限制子 agent 只能调用 memory 工具,
|
|
3390
|
+
* 共享连接后绕过了该机制。此方法在 ACode runtime 层重新施加限制:
|
|
3391
|
+
* - 复用同一 client/transport(不创建新连接)
|
|
3392
|
+
* - 仅暴露 MEMORY_TOOL_NAMES 中的工具
|
|
3393
|
+
* - 防止子 agent 调用 poll_message(与父 agent 冲突)、send_group 等
|
|
3394
|
+
*/
|
|
3395
|
+
filterSharedMcpConnectionsForSubAgent(connections) {
|
|
3396
|
+
return connections.map((conn) => ({
|
|
3397
|
+
...conn,
|
|
3398
|
+
tools: conn.tools.filter((tool) => SUB_AGENT_ALLOWED_MCP_TOOLS.has(tool.toolName)),
|
|
3399
|
+
}));
|
|
3400
|
+
}
|
|
1814
3401
|
async connectMcpServers(servers, timeoutMs) {
|
|
1815
3402
|
const connections = [];
|
|
1816
3403
|
for (const [serverName, serverSpec] of Object.entries(servers)) {
|
|
@@ -1869,7 +3456,70 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1869
3456
|
requestInit,
|
|
1870
3457
|
});
|
|
1871
3458
|
}
|
|
3459
|
+
/**
|
|
3460
|
+
* 通过 MCP 连接调用 get_profile 工具获取用户 profile 信息,
|
|
3461
|
+
* 格式化为文本后返回,用于拼接到 rolePrompt 下方。
|
|
3462
|
+
* 主流程:遍历已连接的 MCP -> 找到 get_profile 工具 -> 调用 -> 格式化结果。
|
|
3463
|
+
* 容错:任何步骤失败都返回空字符串,不阻塞启动。
|
|
3464
|
+
*/
|
|
3465
|
+
async fetchProfilePrompt(mcpConnections) {
|
|
3466
|
+
for (const connection of mcpConnections) {
|
|
3467
|
+
const profileTool = connection.tools.find((tool) => tool.toolName === "get_profile");
|
|
3468
|
+
if (!profileTool) {
|
|
3469
|
+
continue;
|
|
3470
|
+
}
|
|
3471
|
+
try {
|
|
3472
|
+
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");
|
|
3473
|
+
const profile = this.extractProfileFromToolResult(result);
|
|
3474
|
+
return buildProfilePrompt(profile);
|
|
3475
|
+
}
|
|
3476
|
+
catch (error) {
|
|
3477
|
+
this.emitAcodeEvent("warning", {
|
|
3478
|
+
message: `ACode failed to fetch profile via ${connection.serverName}/get_profile: ${error instanceof Error ? error.message : String(error)}`,
|
|
3479
|
+
});
|
|
3480
|
+
return "";
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
return "";
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* 从 MCP callTool 返回值中提取 profile 对象。
|
|
3487
|
+
* 主流程:优先取 content[0].text JSON -> 回退到 result 本身为对象。
|
|
3488
|
+
*/
|
|
3489
|
+
extractProfileFromToolResult(result) {
|
|
3490
|
+
if (!isRecord(result)) {
|
|
3491
|
+
return null;
|
|
3492
|
+
}
|
|
3493
|
+
const content = result.content;
|
|
3494
|
+
if (Array.isArray(content)) {
|
|
3495
|
+
for (const item of content) {
|
|
3496
|
+
if (!isRecord(item))
|
|
3497
|
+
continue;
|
|
3498
|
+
if (typeof item.text === "string" && item.text.trim()) {
|
|
3499
|
+
try {
|
|
3500
|
+
const parsed = JSON.parse(item.text);
|
|
3501
|
+
if (isRecord(parsed)) {
|
|
3502
|
+
return parsed;
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
catch {
|
|
3506
|
+
// text 不是 JSON,继续
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
if (isRecord(result)) {
|
|
3512
|
+
return result;
|
|
3513
|
+
}
|
|
3514
|
+
return null;
|
|
3515
|
+
}
|
|
1872
3516
|
async disconnectMcpServers() {
|
|
3517
|
+
// 共享连接(子 agent)不关闭,由所有者(父 agent)管理生命周期
|
|
3518
|
+
if (!this.state.ownsMcpConnections) {
|
|
3519
|
+
this.state.mcpConnections = [];
|
|
3520
|
+
this.state.exposedTools = [];
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
1873
3523
|
const connections = this.state.mcpConnections;
|
|
1874
3524
|
this.state.mcpConnections = [];
|
|
1875
3525
|
this.state.exposedTools = [];
|
|
@@ -1878,3 +3528,5 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1878
3528
|
}, this.state.provider.mcpToolTimeoutMs)));
|
|
1879
3529
|
}
|
|
1880
3530
|
}
|
|
3531
|
+
/** skill 重扫防抖间隔(毫秒) */
|
|
3532
|
+
ACodeRuntime.SKILL_REFRESH_DEBOUNCE_MS = 500;
|