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
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ACodeRuntime } from "./runtime.js";
|
|
3
|
+
/** 已完成 entry 在内存中的保留时长(30 分钟),超时后清理释放内存 */
|
|
4
|
+
const COMPLETED_ENTRY_TTL_MS = 30 * 60 * 1000;
|
|
5
|
+
/**
|
|
6
|
+
* SubAgentManager:管理独立 ACodeRuntime 实例池。
|
|
7
|
+
*
|
|
8
|
+
* 核心职责:
|
|
9
|
+
* 1. spawn 创建独立 ACodeRuntime 实例(避免父 agent 的 submitQueue 死锁)
|
|
10
|
+
* 2. start 异步启动子 agent(不阻塞),awaitCompletion 显式等待完成
|
|
11
|
+
* 3. abort 取消运行中的 sub-agent
|
|
12
|
+
* 4. 维护父子关系映射(parentAgentId -> subIds)
|
|
13
|
+
* 5. SSE 事件订阅(subscribe/unsubscribe)
|
|
14
|
+
* 6. 状态持久化到 SubAgentStore
|
|
15
|
+
*
|
|
16
|
+
* 线程模型:单进程 Node.js,所有 ACodeRuntime 实例共享事件循环。
|
|
17
|
+
* 非阻塞模式:spawn 后调 start 立即返回,主 agent 可继续工作;
|
|
18
|
+
* 主 agent 通过 awaitCompletion 显式阻塞等待指定 sub-agent 数组完成。
|
|
19
|
+
*
|
|
20
|
+
* 重要:ACodeRuntime.submit() 返回 Promise<void>,assistant 文本通过
|
|
21
|
+
* "event" 上的 "assistant_delta" 事件传递,turn 结束时发出 "turn_complete"。
|
|
22
|
+
* 因此 start 内部注册事件监听器累积输出文本,awaitCompletion 通过 Promise 等待。
|
|
23
|
+
*
|
|
24
|
+
* Entry 生命周期:
|
|
25
|
+
* - pending: spawn 后未 start
|
|
26
|
+
* - running: start 后运行中
|
|
27
|
+
* - completed/failed/cancelled: 终态,entry 保留在内存中 TTL=30min,
|
|
28
|
+
* 供 list/get_status 查询;超时后从内存清理(store 仍持久化)
|
|
29
|
+
*/
|
|
30
|
+
export class SubAgentManager {
|
|
31
|
+
constructor(opts) {
|
|
32
|
+
this.subAgents = new Map();
|
|
33
|
+
this.parentToSubs = new Map();
|
|
34
|
+
this.subscribers = new Map(); // subId 或 "*" -> subs
|
|
35
|
+
/** 已完成 entry 的清理定时器 */
|
|
36
|
+
this.cleanupTimer = null;
|
|
37
|
+
/**
|
|
38
|
+
* 终态通知队列:子 agent 到达终态时入队,takeCompletedSubAgents 消费。
|
|
39
|
+
* 供 poll_message 在 ACode 运行时层检测子 agent 完成事件。
|
|
40
|
+
*/
|
|
41
|
+
this.completionQueue = [];
|
|
42
|
+
/**
|
|
43
|
+
* waitForAnyCompletion 的 resolve 回调,终态事件到达时调用。
|
|
44
|
+
* null 表示当前没有等待者。
|
|
45
|
+
*/
|
|
46
|
+
this.completionWaiterResolve = null;
|
|
47
|
+
this.store = opts.store;
|
|
48
|
+
this.maxConcurrent = opts.maxConcurrent ?? 5;
|
|
49
|
+
// 启动定期清理已完成 entry 的定时器(每 5 分钟扫一次)
|
|
50
|
+
this.cleanupTimer = setInterval(() => this.cleanupCompletedEntries(), 5 * 60 * 1000);
|
|
51
|
+
// 不阻止进程退出
|
|
52
|
+
if (this.cleanupTimer.unref)
|
|
53
|
+
this.cleanupTimer.unref();
|
|
54
|
+
}
|
|
55
|
+
/** 创建 sub-agent(不立即运行;调 start 才执行) */
|
|
56
|
+
async spawn(opts) {
|
|
57
|
+
// 并发上限检查(pending + running 都算占用配额)
|
|
58
|
+
const activeCount = this.countByStatus(opts.parentAgentId, "running", "pending");
|
|
59
|
+
if (activeCount >= this.maxConcurrent) {
|
|
60
|
+
throw new Error(`concurrent sub-agent limit reached (${this.maxConcurrent}) for agent ${opts.parentAgentId}`);
|
|
61
|
+
}
|
|
62
|
+
const subId = `sub-${randomUUID()}`;
|
|
63
|
+
const startedAt = Date.now();
|
|
64
|
+
const abortController = new AbortController();
|
|
65
|
+
// 构造子 agent 的 config(继承父 agent 的 model/prompt,但独立 sessionId)
|
|
66
|
+
// 若父 agent 配置了 subAgentModel/subAgentEffort,则覆盖子 agent 的 model/effort
|
|
67
|
+
const childConfig = {
|
|
68
|
+
...opts.parentConfig,
|
|
69
|
+
sessionId: subId,
|
|
70
|
+
workingDirectory: opts.workingDirectory,
|
|
71
|
+
agentId: `${opts.parentAgentId}__sub__${subId}`,
|
|
72
|
+
// sub-agent 不继承 initialPrompt(用 submit 触发)
|
|
73
|
+
initialPrompt: undefined,
|
|
74
|
+
// sub-agent 不允许再 spawn 自己的子 agent(禁止递归嵌套)
|
|
75
|
+
subAgentManager: undefined,
|
|
76
|
+
// Sub Agent 模型/强度覆盖:有值则覆盖继承的 model/effort
|
|
77
|
+
...(opts.parentConfig.subAgentModel
|
|
78
|
+
? { model: opts.parentConfig.subAgentModel }
|
|
79
|
+
: {}),
|
|
80
|
+
...(opts.parentConfig.subAgentEffort
|
|
81
|
+
? { effort: opts.parentConfig.subAgentEffort }
|
|
82
|
+
: {}),
|
|
83
|
+
// 子 agent 不再继承 subAgent 专用覆盖字段(防止孙 agent 二次覆盖)
|
|
84
|
+
subAgentModel: undefined,
|
|
85
|
+
subAgentEffort: undefined,
|
|
86
|
+
};
|
|
87
|
+
const runtime = new ACodeRuntime(childConfig);
|
|
88
|
+
const entry = {
|
|
89
|
+
subId,
|
|
90
|
+
parentAgentId: opts.parentAgentId,
|
|
91
|
+
parentSessionId: opts.parentSessionId,
|
|
92
|
+
subagentType: opts.subagentType,
|
|
93
|
+
description: opts.description,
|
|
94
|
+
prompt: opts.prompt,
|
|
95
|
+
status: "pending",
|
|
96
|
+
startedAt,
|
|
97
|
+
runtime,
|
|
98
|
+
abortController,
|
|
99
|
+
silent: opts.silent,
|
|
100
|
+
contextSnapshot: {
|
|
101
|
+
tokenUsage: { input: 0, output: 0 },
|
|
102
|
+
messageCount: 0,
|
|
103
|
+
updatedAt: new Date().toISOString(),
|
|
104
|
+
},
|
|
105
|
+
availableTools: [],
|
|
106
|
+
};
|
|
107
|
+
this.subAgents.set(subId, entry);
|
|
108
|
+
this.addToParentMap(opts.parentAgentId, subId);
|
|
109
|
+
// 持久化初始记录
|
|
110
|
+
const record = this.toRecord(entry);
|
|
111
|
+
await this.store.upsert(opts.parentAgentId, record);
|
|
112
|
+
// 监听 runtime 事件以更新 contextSnapshot
|
|
113
|
+
runtime.on("event", (event) => {
|
|
114
|
+
this.handleRuntimeEvent(subId, event);
|
|
115
|
+
});
|
|
116
|
+
this.emitEvent(subId, "spawned", {});
|
|
117
|
+
return subId;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 异步启动 sub-agent(不阻塞,立即返回)。
|
|
121
|
+
*
|
|
122
|
+
* 内部创建 completionPromise 供后续 awaitCompletion 等待。
|
|
123
|
+
* 启动后立即返回,主 agent 可继续做其他事。
|
|
124
|
+
*
|
|
125
|
+
* 同一 sub-agent 只能 start 一次;重复 start 抛错。
|
|
126
|
+
*/
|
|
127
|
+
async start(subId) {
|
|
128
|
+
const entry = this.subAgents.get(subId);
|
|
129
|
+
if (!entry) {
|
|
130
|
+
throw new Error(`sub-agent not found: ${subId}`);
|
|
131
|
+
}
|
|
132
|
+
if (entry.status !== "pending") {
|
|
133
|
+
throw new Error(`sub-agent ${subId} cannot start from status: ${entry.status}`);
|
|
134
|
+
}
|
|
135
|
+
// 创建 completionPromise(在 finally 中 settle)
|
|
136
|
+
entry.completionPromise = new Promise((resolve, reject) => {
|
|
137
|
+
entry.completionResolve = resolve;
|
|
138
|
+
entry.completionReject = reject;
|
|
139
|
+
});
|
|
140
|
+
// 异步启动,不阻塞调用方
|
|
141
|
+
void this.runEntryToCompletion(entry).catch(() => {
|
|
142
|
+
// runEntryToCompletion 内部已处理异常并更新状态;
|
|
143
|
+
// 这里捕获防止 unhandled rejection
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 带 recheck 循环的异步启动(不阻塞,立即返回)。
|
|
148
|
+
*
|
|
149
|
+
* 与 start 的区别:每轮 submit 完成后调用 recheck 回调查询服务端真实状态。
|
|
150
|
+
* 若 recheck 返回 { done: false, continueMessage },将 continueMessage 作为
|
|
151
|
+
* 下一轮 submit 注入同一 sub-agent(保持上下文连续性),最多重试 maxRetries 次。
|
|
152
|
+
*
|
|
153
|
+
* 适用场景:memory 过期整理 / 文件变更更新 / 容量整理等需要系统级硬保证
|
|
154
|
+
* "确实完成"的场景,不依赖 LLM 自报结果。
|
|
155
|
+
*/
|
|
156
|
+
async startWithRecheck(subId, recheck, maxRetries = 3) {
|
|
157
|
+
const entry = this.subAgents.get(subId);
|
|
158
|
+
if (!entry) {
|
|
159
|
+
throw new Error(`sub-agent not found: ${subId}`);
|
|
160
|
+
}
|
|
161
|
+
if (entry.status !== "pending") {
|
|
162
|
+
throw new Error(`sub-agent ${subId} cannot start from status: ${entry.status}`);
|
|
163
|
+
}
|
|
164
|
+
// 创建 completionPromise(在 finally 中 settle)
|
|
165
|
+
entry.completionPromise = new Promise((resolve, reject) => {
|
|
166
|
+
entry.completionResolve = resolve;
|
|
167
|
+
entry.completionReject = reject;
|
|
168
|
+
});
|
|
169
|
+
// 异步启动,不阻塞调用方
|
|
170
|
+
void this.runEntryToCompletionWithRecheck(entry, recheck, maxRetries).catch(() => {
|
|
171
|
+
// 内部已处理异常并更新状态;这里捕获防止 unhandled rejection
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 阻塞等待指定 sub-agent 数组全部完成(completed/failed/cancelled)。
|
|
176
|
+
*
|
|
177
|
+
* - 已完成的立即 resolve
|
|
178
|
+
* - 未 start 的会先 start(自动启动)
|
|
179
|
+
* - 返回每个 sub-agent 的最终结果
|
|
180
|
+
*
|
|
181
|
+
* 注意:本方法不取消 sub-agent,仅等待。如需取消用 abort。
|
|
182
|
+
*/
|
|
183
|
+
async awaitCompletion(subIds, timeoutMs = 0) {
|
|
184
|
+
if (subIds.length === 0)
|
|
185
|
+
return [];
|
|
186
|
+
const results = [];
|
|
187
|
+
// 对每个 subId 准备等待 Promise
|
|
188
|
+
const waitPromises = subIds.map(async (subId) => {
|
|
189
|
+
const entry = this.subAgents.get(subId);
|
|
190
|
+
if (!entry) {
|
|
191
|
+
// entry 已从内存清理,从 store 查
|
|
192
|
+
const record = await this.findRecordInStore(subId);
|
|
193
|
+
if (record) {
|
|
194
|
+
results.push({
|
|
195
|
+
subId,
|
|
196
|
+
output: record.result ?? "",
|
|
197
|
+
error: record.errorMessage,
|
|
198
|
+
status: record.status,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
results.push({
|
|
203
|
+
subId,
|
|
204
|
+
output: "",
|
|
205
|
+
error: `sub-agent not found: ${subId}`,
|
|
206
|
+
status: "failed",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// 标记为 awaited:防止 enqueueCompletion 将终态通知推入 completionQueue
|
|
212
|
+
// (避免后续 poll_message 重复返回已被 await 消费的结果)
|
|
213
|
+
entry.awaited = true;
|
|
214
|
+
// 如果还在 pending,自动 start
|
|
215
|
+
if (entry.status === "pending") {
|
|
216
|
+
await this.start(subId);
|
|
217
|
+
}
|
|
218
|
+
// 等待 completionPromise(可能已被 start 创建)
|
|
219
|
+
const promise = entry.completionPromise ?? Promise.resolve({ output: "", error: "not started" });
|
|
220
|
+
try {
|
|
221
|
+
const result = await promise;
|
|
222
|
+
results.push({
|
|
223
|
+
subId,
|
|
224
|
+
output: result.output,
|
|
225
|
+
error: result.error,
|
|
226
|
+
status: entry.status,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
results.push({
|
|
231
|
+
subId,
|
|
232
|
+
output: "",
|
|
233
|
+
error: err instanceof Error ? err.message : String(err),
|
|
234
|
+
status: entry.status,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
// 等待全部完成后,清理 completionQueue 中可能已存在的这些 subId 的通知
|
|
239
|
+
// (处理子 agent 在 awaited 标记设置前已完成入队的情况)
|
|
240
|
+
const subIdSet = new Set(subIds);
|
|
241
|
+
for (let i = this.completionQueue.length - 1; i >= 0; i--) {
|
|
242
|
+
if (subIdSet.has(this.completionQueue[i].subId)) {
|
|
243
|
+
this.completionQueue.splice(i, 1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (timeoutMs > 0) {
|
|
247
|
+
await Promise.race([
|
|
248
|
+
Promise.allSettled(waitPromises),
|
|
249
|
+
new Promise((resolve) => setTimeout(() => resolve(), timeoutMs)),
|
|
250
|
+
]);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
await Promise.allSettled(waitPromises);
|
|
254
|
+
}
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* 内部方法:执行 entry 直到完成。
|
|
259
|
+
*
|
|
260
|
+
* 实际 ACodeRuntime.submit() 返回 Promise<void>,assistant 文本通过
|
|
261
|
+
* assistant_delta 事件传递。本方法注册临时监听器累积输出文本,
|
|
262
|
+
* 同时检测 turn_complete 事件中的 failed 标志以处理运行时内部错误。
|
|
263
|
+
*
|
|
264
|
+
* 完成后:
|
|
265
|
+
* - 更新 entry 状态(completed/failed)
|
|
266
|
+
* - 持久化到 store
|
|
267
|
+
* - settle completionPromise
|
|
268
|
+
* - 清理 runtime 实例(释放内存)
|
|
269
|
+
* - **不删除 entry**(保留在内存供 list/get_status 查询,30min 后由定时器清理)
|
|
270
|
+
*/
|
|
271
|
+
async runEntryToCompletion(entry) {
|
|
272
|
+
// 收集 assistant_delta 文本 + 检测 turn 失败标志
|
|
273
|
+
const chunks = [];
|
|
274
|
+
let turnFailed = false;
|
|
275
|
+
let turnErrorMessage;
|
|
276
|
+
const onEvent = (event) => {
|
|
277
|
+
if (!event)
|
|
278
|
+
return;
|
|
279
|
+
switch (event.type) {
|
|
280
|
+
case "assistant_delta":
|
|
281
|
+
if (typeof event.data?.text === "string") {
|
|
282
|
+
chunks.push(event.data.text);
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
case "turn_complete":
|
|
286
|
+
if (event.data?.failed) {
|
|
287
|
+
turnFailed = true;
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
case "error":
|
|
291
|
+
if (typeof event.data?.message === "string") {
|
|
292
|
+
turnErrorMessage = event.data.message;
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
entry.runtime.on("event", onEvent);
|
|
298
|
+
// 状态 -> running
|
|
299
|
+
entry.status = "running";
|
|
300
|
+
await this.store.updateStatus(entry.parentAgentId, entry.subId, { status: "running" });
|
|
301
|
+
this.emitEvent(entry.subId, "running", {});
|
|
302
|
+
try {
|
|
303
|
+
await entry.runtime.start();
|
|
304
|
+
// 记录启动后的可用工具列表
|
|
305
|
+
entry.availableTools = entry.runtime.getAvailableTools();
|
|
306
|
+
await entry.runtime.submit(entry.prompt);
|
|
307
|
+
// 实际 API 中 submit 解析后所有事件已同步触发完毕(EventEmitter.emit 是同步的)
|
|
308
|
+
// 若 turn_complete 标记 failed,抛错走 catch
|
|
309
|
+
if (turnFailed) {
|
|
310
|
+
throw new Error(turnErrorMessage ?? "sub-agent turn failed");
|
|
311
|
+
}
|
|
312
|
+
const output = chunks.join("");
|
|
313
|
+
entry.status = "completed";
|
|
314
|
+
entry.result = output;
|
|
315
|
+
entry.endedAt = Date.now();
|
|
316
|
+
entry.durationMs = entry.endedAt - entry.startedAt;
|
|
317
|
+
await this.store.updateStatus(entry.parentAgentId, entry.subId, {
|
|
318
|
+
status: "completed",
|
|
319
|
+
result: output,
|
|
320
|
+
endedAt: entry.endedAt,
|
|
321
|
+
durationMs: entry.durationMs,
|
|
322
|
+
contextSnapshot: undefined, // 完成后清空快照
|
|
323
|
+
});
|
|
324
|
+
this.emitEvent(entry.subId, "completed", { result: output });
|
|
325
|
+
entry.completionResolve?.({ output });
|
|
326
|
+
this.enqueueCompletion(entry, output, undefined);
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
330
|
+
entry.status = "failed";
|
|
331
|
+
entry.errorMessage = errorMessage;
|
|
332
|
+
entry.endedAt = Date.now();
|
|
333
|
+
entry.durationMs = entry.endedAt - entry.startedAt;
|
|
334
|
+
await this.store.updateStatus(entry.parentAgentId, entry.subId, {
|
|
335
|
+
status: "failed",
|
|
336
|
+
errorMessage,
|
|
337
|
+
endedAt: entry.endedAt,
|
|
338
|
+
durationMs: entry.durationMs,
|
|
339
|
+
contextSnapshot: undefined,
|
|
340
|
+
});
|
|
341
|
+
this.emitEvent(entry.subId, "failed", { error: errorMessage });
|
|
342
|
+
entry.completionResolve?.({ output: "", error: errorMessage });
|
|
343
|
+
this.enqueueCompletion(entry, "", errorMessage);
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
// 移除临时监听器
|
|
347
|
+
try {
|
|
348
|
+
entry.runtime.removeListener?.("event", onEvent);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
/* 忽略移除失败 */
|
|
352
|
+
}
|
|
353
|
+
// 完成后清理 runtime 实例(释放内存),但保留 entry 供查询
|
|
354
|
+
try {
|
|
355
|
+
await entry.runtime.stop();
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
/* 忽略 stop 失败 */
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* 带 recheck 循环的 entry 执行(多轮 submit)。
|
|
364
|
+
*
|
|
365
|
+
* 与 runEntryToCompletion 的区别:
|
|
366
|
+
* 1. 第一轮 submit(entry.prompt) 完成后,调用 recheck 回调查询服务端真实状态
|
|
367
|
+
* 2. 若 recheck 返回 { done: false, continueMessage },将 continueMessage 注入下一轮 submit
|
|
368
|
+
* 3. 最多重试 maxRetries 次;超过后视为完成(不再继续)
|
|
369
|
+
* 4. onEvent 监听器跨所有轮次累积 assistant_delta,最终 output 包含全部轮次的文本
|
|
370
|
+
*
|
|
371
|
+
* turnFailed 标志在每轮 submit 前重置,确保上一轮的失败状态不影响下一轮判断。
|
|
372
|
+
* recheck 自身抛错时视为 done(不阻塞 sub-agent 结束),仅记录日志。
|
|
373
|
+
*/
|
|
374
|
+
async runEntryToCompletionWithRecheck(entry, recheck, maxRetries) {
|
|
375
|
+
const chunks = [];
|
|
376
|
+
let turnFailed = false;
|
|
377
|
+
let turnErrorMessage;
|
|
378
|
+
const onEvent = (event) => {
|
|
379
|
+
if (!event)
|
|
380
|
+
return;
|
|
381
|
+
switch (event.type) {
|
|
382
|
+
case "assistant_delta":
|
|
383
|
+
if (typeof event.data?.text === "string") {
|
|
384
|
+
chunks.push(event.data.text);
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
case "turn_complete":
|
|
388
|
+
if (event.data?.failed) {
|
|
389
|
+
turnFailed = true;
|
|
390
|
+
}
|
|
391
|
+
break;
|
|
392
|
+
case "error":
|
|
393
|
+
if (typeof event.data?.message === "string") {
|
|
394
|
+
turnErrorMessage = event.data.message;
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
entry.runtime.on("event", onEvent);
|
|
400
|
+
// 状态 -> running
|
|
401
|
+
entry.status = "running";
|
|
402
|
+
await this.store.updateStatus(entry.parentAgentId, entry.subId, { status: "running" });
|
|
403
|
+
this.emitEvent(entry.subId, "running", {});
|
|
404
|
+
try {
|
|
405
|
+
await entry.runtime.start();
|
|
406
|
+
entry.availableTools = entry.runtime.getAvailableTools();
|
|
407
|
+
// 第一轮:使用初始 prompt
|
|
408
|
+
await entry.runtime.submit(entry.prompt);
|
|
409
|
+
if (turnFailed) {
|
|
410
|
+
throw new Error(turnErrorMessage ?? "sub-agent turn failed");
|
|
411
|
+
}
|
|
412
|
+
let turn = 1;
|
|
413
|
+
// recheck 循环:最多 maxRetries 次
|
|
414
|
+
while (turn <= maxRetries) {
|
|
415
|
+
let recheckResult;
|
|
416
|
+
try {
|
|
417
|
+
recheckResult = await recheck(chunks.join(""), turn);
|
|
418
|
+
}
|
|
419
|
+
catch (recheckErr) {
|
|
420
|
+
// recheck 自身出错:不阻塞 sub-agent 结束,视为完成
|
|
421
|
+
console.error(`[SubAgentManager] recheck threw error for subId=${entry.subId} turn=${turn}, treating as done:`, recheckErr instanceof Error ? recheckErr.message : String(recheckErr));
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
if (recheckResult.done) {
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
// 未完成:注入 continueMessage 进行下一轮
|
|
428
|
+
const continueMessage = recheckResult.continueMessage ?? "经检查,未完成处理,请继续。";
|
|
429
|
+
// 重置 turn 失败标记,确保本轮失败状态不影响下一轮判断
|
|
430
|
+
turnFailed = false;
|
|
431
|
+
turnErrorMessage = undefined;
|
|
432
|
+
console.error(`[SubAgentManager] recheck not done for subId=${entry.subId} turn=${turn}/${maxRetries}, ` +
|
|
433
|
+
`injecting continue message (${continueMessage.length} chars)`);
|
|
434
|
+
await entry.runtime.submit(continueMessage);
|
|
435
|
+
if (turnFailed) {
|
|
436
|
+
throw new Error(turnErrorMessage ?? `sub-agent turn ${turn + 1} failed during recheck continuation`);
|
|
437
|
+
}
|
|
438
|
+
turn++;
|
|
439
|
+
}
|
|
440
|
+
if (turn > maxRetries) {
|
|
441
|
+
console.error(`[SubAgentManager] recheck reached maxRetries=${maxRetries} for subId=${entry.subId}, ` +
|
|
442
|
+
`completing with potentially unfinished work`);
|
|
443
|
+
}
|
|
444
|
+
const output = chunks.join("");
|
|
445
|
+
entry.status = "completed";
|
|
446
|
+
entry.result = output;
|
|
447
|
+
entry.endedAt = Date.now();
|
|
448
|
+
entry.durationMs = entry.endedAt - entry.startedAt;
|
|
449
|
+
await this.store.updateStatus(entry.parentAgentId, entry.subId, {
|
|
450
|
+
status: "completed",
|
|
451
|
+
result: output,
|
|
452
|
+
endedAt: entry.endedAt,
|
|
453
|
+
durationMs: entry.durationMs,
|
|
454
|
+
contextSnapshot: undefined,
|
|
455
|
+
});
|
|
456
|
+
this.emitEvent(entry.subId, "completed", { result: output });
|
|
457
|
+
entry.completionResolve?.({ output });
|
|
458
|
+
this.enqueueCompletion(entry, output, undefined);
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
462
|
+
entry.status = "failed";
|
|
463
|
+
entry.errorMessage = errorMessage;
|
|
464
|
+
entry.endedAt = Date.now();
|
|
465
|
+
entry.durationMs = entry.endedAt - entry.startedAt;
|
|
466
|
+
await this.store.updateStatus(entry.parentAgentId, entry.subId, {
|
|
467
|
+
status: "failed",
|
|
468
|
+
errorMessage,
|
|
469
|
+
endedAt: entry.endedAt,
|
|
470
|
+
durationMs: entry.durationMs,
|
|
471
|
+
contextSnapshot: undefined,
|
|
472
|
+
});
|
|
473
|
+
this.emitEvent(entry.subId, "failed", { error: errorMessage });
|
|
474
|
+
entry.completionResolve?.({ output: "", error: errorMessage });
|
|
475
|
+
this.enqueueCompletion(entry, "", errorMessage);
|
|
476
|
+
}
|
|
477
|
+
finally {
|
|
478
|
+
// 移除临时监听器
|
|
479
|
+
try {
|
|
480
|
+
entry.runtime.removeListener?.("event", onEvent);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
/* 忽略移除失败 */
|
|
484
|
+
}
|
|
485
|
+
// 完成后清理 runtime 实例(释放内存),但保留 entry 供查询
|
|
486
|
+
try {
|
|
487
|
+
await entry.runtime.stop();
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
/* 忽略 stop 失败 */
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* @deprecated 兼容旧接口:spawn + 阻塞等待完成。
|
|
496
|
+
* 新代码应使用 spawn + start + awaitCompletion。
|
|
497
|
+
*/
|
|
498
|
+
async runToCompletion(subId) {
|
|
499
|
+
const entry = this.subAgents.get(subId);
|
|
500
|
+
if (!entry) {
|
|
501
|
+
throw new Error(`sub-agent not found: ${subId}`);
|
|
502
|
+
}
|
|
503
|
+
if (entry.status === "running") {
|
|
504
|
+
throw new Error(`sub-agent already running: ${subId}`);
|
|
505
|
+
}
|
|
506
|
+
// 重置 status 以便 runEntryToCompletion 能进入 running
|
|
507
|
+
if (entry.status !== "pending") {
|
|
508
|
+
entry.status = "pending";
|
|
509
|
+
}
|
|
510
|
+
await this.start(subId);
|
|
511
|
+
return (await entry.completionPromise) ?? { output: "", error: "not started" };
|
|
512
|
+
}
|
|
513
|
+
/** 取消运行中的 sub-agent */
|
|
514
|
+
async abort(subId) {
|
|
515
|
+
const entry = this.subAgents.get(subId);
|
|
516
|
+
if (!entry)
|
|
517
|
+
return;
|
|
518
|
+
if (entry.status === "completed" || entry.status === "failed" || entry.status === "cancelled") {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
entry.abortController.abort();
|
|
522
|
+
try {
|
|
523
|
+
await entry.runtime.abort();
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
/* 忽略 */
|
|
527
|
+
}
|
|
528
|
+
entry.status = "cancelled";
|
|
529
|
+
entry.endedAt = Date.now();
|
|
530
|
+
entry.durationMs = entry.endedAt - entry.startedAt;
|
|
531
|
+
await this.store.updateStatus(entry.parentAgentId, subId, {
|
|
532
|
+
status: "cancelled",
|
|
533
|
+
endedAt: entry.endedAt,
|
|
534
|
+
durationMs: entry.durationMs,
|
|
535
|
+
contextSnapshot: undefined,
|
|
536
|
+
});
|
|
537
|
+
this.emitEvent(subId, "cancelled", {});
|
|
538
|
+
// settle completionPromise(cancelled 视为 error)
|
|
539
|
+
entry.completionResolve?.({ output: "", error: "aborted" });
|
|
540
|
+
this.enqueueCompletion(entry, "", "aborted");
|
|
541
|
+
}
|
|
542
|
+
/** 获取内存中的 entry */
|
|
543
|
+
getSubAgent(subId) {
|
|
544
|
+
return this.subAgents.get(subId);
|
|
545
|
+
}
|
|
546
|
+
/** 列出某 agent 的全部内存中 sub-agent(含运行中和已完成未清理) */
|
|
547
|
+
listByParent(parentAgentId) {
|
|
548
|
+
const subIds = this.parentToSubs.get(parentAgentId);
|
|
549
|
+
if (!subIds)
|
|
550
|
+
return [];
|
|
551
|
+
return Array.from(subIds)
|
|
552
|
+
.map((id) => this.subAgents.get(id))
|
|
553
|
+
.filter((e) => e !== undefined);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* 列出某父 agent 下处于活跃状态(pending / running)的 sub-agent。
|
|
557
|
+
*
|
|
558
|
+
* 已完成/失败/取消的 entry 在内存中仍保留 TTL=30min 供查询,但不应计入
|
|
559
|
+
* "进行中" 列表。前端 "进行中" tab 与 SSE snapshot 都应使用此方法。
|
|
560
|
+
*/
|
|
561
|
+
listActiveByParent(parentAgentId) {
|
|
562
|
+
return this.listByParent(parentAgentId).filter((e) => e.status === "pending" || e.status === "running");
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* 取走并清空终态通知队列。
|
|
566
|
+
*
|
|
567
|
+
* 主流程:返回 completionQueue 中的所有通知并清空队列。
|
|
568
|
+
* 供 ACode 运行时在 poll_message 调用前后检查子 agent 是否已完成。
|
|
569
|
+
* 每次 poll_message 调用前先调此方法,若有已完成子 agent 则直接返回,无需等待 MCP。
|
|
570
|
+
*/
|
|
571
|
+
takeCompletedSubAgents() {
|
|
572
|
+
if (this.completionQueue.length === 0)
|
|
573
|
+
return [];
|
|
574
|
+
const drained = this.completionQueue.splice(0);
|
|
575
|
+
return drained;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* 等待任意子 agent 到达终态(completed/failed/cancelled)。
|
|
579
|
+
*
|
|
580
|
+
* 主流程:如果 completionQueue 中已有通知,立即 resolve;
|
|
581
|
+
* 否则注册一个 resolve 回调,在下次子 agent 终态事件到达时触发。
|
|
582
|
+
* 供 ACode 运行时在 poll_message 期间 race 子 agent 完成事件。
|
|
583
|
+
*
|
|
584
|
+
* 返回 { promise, cancel }:
|
|
585
|
+
* - promise: 子 agent 终态时 resolve
|
|
586
|
+
* - cancel: 取消等待(poll_message 先返回时调用,清理 resolve 回调)
|
|
587
|
+
*/
|
|
588
|
+
waitForAnyCompletion() {
|
|
589
|
+
// 如果队列中已有通知,立即返回
|
|
590
|
+
if (this.completionQueue.length > 0) {
|
|
591
|
+
const notif = this.completionQueue.shift();
|
|
592
|
+
return {
|
|
593
|
+
promise: Promise.resolve(notif),
|
|
594
|
+
cancel: () => { },
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
let cancelled = false;
|
|
598
|
+
const promise = new Promise((resolve) => {
|
|
599
|
+
this.completionWaiterResolve = (notif) => {
|
|
600
|
+
if (!cancelled)
|
|
601
|
+
resolve(notif);
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
const cancel = () => {
|
|
605
|
+
cancelled = true;
|
|
606
|
+
this.completionWaiterResolve = null;
|
|
607
|
+
};
|
|
608
|
+
return { promise, cancel };
|
|
609
|
+
}
|
|
610
|
+
/** 订阅 sub-agent 事件(subId 或 "*" 全局) */
|
|
611
|
+
subscribe(subId, cb) {
|
|
612
|
+
let set = this.subscribers.get(subId);
|
|
613
|
+
if (!set) {
|
|
614
|
+
set = new Set();
|
|
615
|
+
this.subscribers.set(subId, set);
|
|
616
|
+
}
|
|
617
|
+
set.add(cb);
|
|
618
|
+
return () => {
|
|
619
|
+
set?.delete(cb);
|
|
620
|
+
if (set && set.size === 0)
|
|
621
|
+
this.subscribers.delete(subId);
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
/** 清理所有运行中的 sub-agent(bridge 关闭时调用) */
|
|
625
|
+
async dispose() {
|
|
626
|
+
if (this.cleanupTimer) {
|
|
627
|
+
clearInterval(this.cleanupTimer);
|
|
628
|
+
this.cleanupTimer = null;
|
|
629
|
+
}
|
|
630
|
+
const entries = Array.from(this.subAgents.values());
|
|
631
|
+
await Promise.allSettled(entries.map((e) => this.abort(e.subId)));
|
|
632
|
+
this.subAgents.clear();
|
|
633
|
+
this.parentToSubs.clear();
|
|
634
|
+
this.subscribers.clear();
|
|
635
|
+
}
|
|
636
|
+
// ---------- 内部方法 ----------
|
|
637
|
+
/**
|
|
638
|
+
* 将子 agent 终态事件入队并通知等待者。
|
|
639
|
+
* 主流程:构建通知对象 -> 入队 completionQueue -> 若有 waitForAnyCompletion 等待者则立即 resolve。
|
|
640
|
+
*/
|
|
641
|
+
enqueueCompletion(entry, output, error) {
|
|
642
|
+
// 静默完成或已被 awaitCompletion 消费的子 agent 不进入 completionQueue
|
|
643
|
+
if (entry.silent || entry.awaited) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const notif = {
|
|
647
|
+
subId: entry.subId,
|
|
648
|
+
subagentType: entry.subagentType,
|
|
649
|
+
description: entry.description,
|
|
650
|
+
status: entry.status,
|
|
651
|
+
output,
|
|
652
|
+
error,
|
|
653
|
+
durationMs: entry.durationMs,
|
|
654
|
+
};
|
|
655
|
+
this.completionQueue.push(notif);
|
|
656
|
+
// 如果有 poll_message 正在等待子 agent 完成事件,立即通知
|
|
657
|
+
if (this.completionWaiterResolve) {
|
|
658
|
+
const resolve = this.completionWaiterResolve;
|
|
659
|
+
this.completionWaiterResolve = null;
|
|
660
|
+
// 从队列中取出刚入队的通知(即此条)
|
|
661
|
+
const queued = this.completionQueue.pop();
|
|
662
|
+
resolve(queued ?? notif);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
countByStatus(parentAgentId, ...statuses) {
|
|
666
|
+
return this.listByParent(parentAgentId).filter((e) => statuses.includes(e.status)).length;
|
|
667
|
+
}
|
|
668
|
+
addToParentMap(parentAgentId, subId) {
|
|
669
|
+
let set = this.parentToSubs.get(parentAgentId);
|
|
670
|
+
if (!set) {
|
|
671
|
+
set = new Set();
|
|
672
|
+
this.parentToSubs.set(parentAgentId, set);
|
|
673
|
+
}
|
|
674
|
+
set.add(subId);
|
|
675
|
+
}
|
|
676
|
+
removeFromParentMap(parentAgentId, subId) {
|
|
677
|
+
const set = this.parentToSubs.get(parentAgentId);
|
|
678
|
+
if (set) {
|
|
679
|
+
set.delete(subId);
|
|
680
|
+
if (set.size === 0)
|
|
681
|
+
this.parentToSubs.delete(parentAgentId);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* 清理已超过 TTL 的终态 entry(completed/failed/cancelled)。
|
|
686
|
+
* 释放内存,store 仍保留历史记录供查询。
|
|
687
|
+
*/
|
|
688
|
+
cleanupCompletedEntries() {
|
|
689
|
+
const now = Date.now();
|
|
690
|
+
for (const [subId, entry] of this.subAgents) {
|
|
691
|
+
if ((entry.status === "completed" || entry.status === "failed" || entry.status === "cancelled") &&
|
|
692
|
+
entry.endedAt &&
|
|
693
|
+
now - entry.endedAt > COMPLETED_ENTRY_TTL_MS) {
|
|
694
|
+
this.subAgents.delete(subId);
|
|
695
|
+
this.removeFromParentMap(entry.parentAgentId, subId);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/** 从 store 中查找记录(entry 已从内存清理时使用) */
|
|
700
|
+
async findRecordInStore(subId) {
|
|
701
|
+
// store 按父 agent 组织,无法直接按 subId 查;遍历所有 parent
|
|
702
|
+
// 注意:此方法仅在 awaitCompletion 时调用,频率低
|
|
703
|
+
for (const parentAgentId of this.parentToSubs.keys()) {
|
|
704
|
+
const doc = await this.store.read(parentAgentId);
|
|
705
|
+
const record = doc.subAgents.find((r) => r.subId === subId);
|
|
706
|
+
if (record)
|
|
707
|
+
return record;
|
|
708
|
+
}
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
711
|
+
handleRuntimeEvent(subId, event) {
|
|
712
|
+
const entry = this.subAgents.get(subId);
|
|
713
|
+
if (!entry)
|
|
714
|
+
return;
|
|
715
|
+
// 更新 contextSnapshot
|
|
716
|
+
const snapshot = entry.contextSnapshot;
|
|
717
|
+
snapshot.updatedAt = new Date().toISOString();
|
|
718
|
+
switch (event?.type) {
|
|
719
|
+
case "ai_request":
|
|
720
|
+
// estimatedContextTokens 是请求前的上下文估算,可作为 input 近似值
|
|
721
|
+
if (event.data?.estimatedContextTokens != null) {
|
|
722
|
+
snapshot.tokenUsage.input = (event.data.estimatedContextTokens ?? snapshot.tokenUsage.input);
|
|
723
|
+
}
|
|
724
|
+
break;
|
|
725
|
+
case "turn_complete":
|
|
726
|
+
// turn_complete 携带实际的 token 用量,覆盖 ai_request 的估算值
|
|
727
|
+
if (event.data?.usage) {
|
|
728
|
+
snapshot.tokenUsage.input = (event.data.usage.inputTokens ?? snapshot.tokenUsage.input);
|
|
729
|
+
snapshot.tokenUsage.output = (event.data.usage.outputTokens ?? snapshot.tokenUsage.output);
|
|
730
|
+
}
|
|
731
|
+
else if (event.data?.failed || event.data?.aborted) {
|
|
732
|
+
// 失败/中止路径不携带 usage,重置 output 避免残留上一轮值
|
|
733
|
+
snapshot.tokenUsage.output = 0;
|
|
734
|
+
}
|
|
735
|
+
break;
|
|
736
|
+
case "assistant_delta":
|
|
737
|
+
// 累积文本,截断到最近 500 字符
|
|
738
|
+
snapshot.lastAssistantText = ((snapshot.lastAssistantText ?? "") + (event.data?.text ?? "")).slice(-500);
|
|
739
|
+
snapshot.messageCount += 1;
|
|
740
|
+
this.emitEvent(subId, "delta", { text: event.data?.text });
|
|
741
|
+
break;
|
|
742
|
+
case "mcp_status":
|
|
743
|
+
// runtime 发射的 mcp_status 使用 action 字段(而非 phase)
|
|
744
|
+
if (event.data?.action === "call_tool") {
|
|
745
|
+
snapshot.currentTool = event.data?.tool;
|
|
746
|
+
this.emitEvent(subId, "tool_start", { tool: snapshot.currentTool, snapshot: { ...snapshot } });
|
|
747
|
+
}
|
|
748
|
+
else if (event.data?.action === "tool_result") {
|
|
749
|
+
this.emitEvent(subId, "tool_end", { tool: snapshot.currentTool, snapshot: { ...snapshot } });
|
|
750
|
+
snapshot.currentTool = undefined;
|
|
751
|
+
}
|
|
752
|
+
break;
|
|
753
|
+
case "session_complete":
|
|
754
|
+
snapshot.messageCount += 1;
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
emitEvent(subId, type, data) {
|
|
759
|
+
const event = {
|
|
760
|
+
type,
|
|
761
|
+
subId,
|
|
762
|
+
timestamp: new Date().toISOString(),
|
|
763
|
+
data,
|
|
764
|
+
};
|
|
765
|
+
// 通知该 subId 的订阅者
|
|
766
|
+
const subs = this.subscribers.get(subId);
|
|
767
|
+
if (subs) {
|
|
768
|
+
for (const cb of subs) {
|
|
769
|
+
try {
|
|
770
|
+
cb(event);
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
/* 忽略订阅者错误 */
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
// 通知全局订阅者
|
|
778
|
+
const globalSubs = this.subscribers.get("*");
|
|
779
|
+
if (globalSubs) {
|
|
780
|
+
for (const cb of globalSubs) {
|
|
781
|
+
try {
|
|
782
|
+
cb(event);
|
|
783
|
+
}
|
|
784
|
+
catch {
|
|
785
|
+
/* 忽略 */
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// tool_start / tool_end 异步落盘(fire-and-forget,不阻塞事件发射)
|
|
790
|
+
if (type === "tool_start" || type === "tool_end") {
|
|
791
|
+
const entry = this.subAgents.get(subId);
|
|
792
|
+
if (entry) {
|
|
793
|
+
console.error(`[SubAgentManager] appendEvent: subId=${subId} type=${type} tool=${data.tool ?? "?"} parentAgentId=${entry.parentAgentId}`);
|
|
794
|
+
void this.store.appendEvent(entry.parentAgentId, subId, event).catch((err) => {
|
|
795
|
+
console.error(`[SubAgentManager] appendEvent FAILED: subId=${subId} error=${err instanceof Error ? err.message : String(err)}`);
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
console.error(`[SubAgentManager] appendEvent SKIPPED (entry not found): subId=${subId} type=${type}`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
toRecord(entry) {
|
|
804
|
+
return {
|
|
805
|
+
subId: entry.subId,
|
|
806
|
+
parentAgentId: entry.parentAgentId,
|
|
807
|
+
parentSessionId: entry.parentSessionId,
|
|
808
|
+
subagentType: entry.subagentType,
|
|
809
|
+
description: entry.description,
|
|
810
|
+
prompt: entry.prompt,
|
|
811
|
+
status: entry.status,
|
|
812
|
+
result: entry.result,
|
|
813
|
+
errorMessage: entry.errorMessage,
|
|
814
|
+
startedAt: entry.startedAt,
|
|
815
|
+
endedAt: entry.endedAt,
|
|
816
|
+
durationMs: entry.durationMs,
|
|
817
|
+
contextSnapshot: entry.status === "running" ? entry.contextSnapshot : undefined,
|
|
818
|
+
availableTools: entry.availableTools,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/** 获取当前上下文快照(供 HTTP 路由调用) */
|
|
822
|
+
getContextSnapshot(subId) {
|
|
823
|
+
return this.subAgents.get(subId)?.contextSnapshot;
|
|
824
|
+
}
|
|
825
|
+
}
|