aws-runtime-bridge 1.9.40 → 1.9.45
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 +31 -4
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/AcodeSdkAdapter.js +382 -57
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +1 -0
- 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/types.d.ts +21 -6
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/adapter/types.js +10 -1
- package/dist/index.js +11 -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/file-browser.d.ts.map +1 -1
- package/dist/routes/file-browser.js +47 -1
- package/dist/routes/processes.d.ts.map +1 -1
- package/dist/routes/processes.js +7 -0
- package/dist/routes/runtime-binding.d.ts.map +1 -1
- package/dist/routes/runtime-binding.js +6 -3
- package/dist/routes/sessions.js +93 -5
- package/dist/routes/sub-agents.js +15 -1
- package/dist/routes/terminal.d.ts +15 -0
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +162 -5
- 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/aws-client-agent-mcp.d.ts.map +1 -1
- package/dist/services/aws-client-agent-mcp.js +11 -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 +7 -1
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +16 -2
- package/dist/services/workspace-files.d.ts +48 -0
- package/dist/services/workspace-files.d.ts.map +1 -1
- package/dist/services/workspace-files.js +285 -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 +43 -7
- package/package/acode/dist/compaction.d.ts.map +1 -1
- package/package/acode/dist/compaction.js +286 -31
- package/package/acode/dist/runtime.d.ts +130 -0
- package/package/acode/dist/runtime.d.ts.map +1 -1
- package/package/acode/dist/runtime.js +972 -71
- package/package/acode/dist/sub-agent-manager.d.ts +96 -1
- package/package/acode/dist/sub-agent-manager.d.ts.map +1 -1
- package/package/acode/dist/sub-agent-manager.js +293 -3
- package/package/acode/dist/sub-agent-store.d.ts +8 -1
- package/package/acode/dist/sub-agent-store.d.ts.map +1 -1
- package/package/acode/dist/sub-agent-store.js +55 -0
- package/package/acode/dist/sub-agent-tools.d.ts.map +1 -1
- package/package/acode/dist/sub-agent-tools.js +5 -4
- package/package/acode/dist/sub-agent-types.d.ts +4 -0
- package/package/acode/dist/sub-agent-types.d.ts.map +1 -1
- package/package/acode/dist/types.d.ts +19 -0
- package/package/acode/dist/types.d.ts.map +1 -1
- 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 -0
- 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 +102 -34
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +637 -209
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +151 -51
- 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 +7 -0
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.js +18 -0
- package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts +32 -9
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.js +58 -19
- package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.js +3 -3
- package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +21 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.js +47 -21
- 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 -0
- 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 +18 -0
- package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +9 -0
- 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 +1 -1
|
@@ -17,7 +17,7 @@ import { ACodeCommandRegistry } from "./commands/commandRegistry.js";
|
|
|
17
17
|
import { calculateCompactionThreshold, COMPACTION_LLM_PROMPT_TEMPLATE, conversationSizeKB, estimateConversationTokens, performCompaction, pruneToolOutputs, summarizeConversation, } from "./compaction.js";
|
|
18
18
|
import { loadAcodeConfig, loadAcodeSkillGroup } from "./config.js";
|
|
19
19
|
import { ACodeHookBus, findDeniedHookResult } from "./hooks/hookBus.js";
|
|
20
|
-
import { isMcpPollingToolName } from "./mcpToolNames.js";
|
|
20
|
+
import { isMcpPollingToolName, getSemanticToolName } from "./mcpToolNames.js";
|
|
21
21
|
function cloneChatContentPart(part) {
|
|
22
22
|
if (part.type === "text") {
|
|
23
23
|
return { type: "text", text: part.text };
|
|
@@ -66,13 +66,60 @@ function normalizeText(value) {
|
|
|
66
66
|
return text || undefined;
|
|
67
67
|
}
|
|
68
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;
|
|
69
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;
|
|
70
86
|
const DEFAULT_PROVIDER_REQUEST_RETRY_DELAY_MS = 1000;
|
|
71
87
|
const DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS = 60_000;
|
|
72
88
|
const DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS = 0;
|
|
73
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
|
+
]);
|
|
74
108
|
const DEFAULT_BASH_BUILT_IN_TOOL_TIMEOUT_MS = 10 * 60_000 + 2000;
|
|
75
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;
|
|
117
|
+
/**
|
|
118
|
+
* 子 agent 完成(completed/failed/cancelled)后,对应 memory URI 的委托冷却窗口。
|
|
119
|
+
* 在此期间即使 memory 仍处 updating / 过期 / 容量超限,运行层也不会再次 spawn 子任务,
|
|
120
|
+
* 仅保留巡检。用于切断「委托完成(但 memory 未真正处理)→ 下一轮 poll 又 spawn 相同子任务」的循环。
|
|
121
|
+
*/
|
|
122
|
+
const MEMORY_DELEGATE_COOLDOWN_MS = 10 * 60 * 1000;
|
|
76
123
|
const MAX_INLINE_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
77
124
|
const MAX_REQUEST_BODY_BYTES = 800 * 1024;
|
|
78
125
|
/**
|
|
@@ -95,6 +142,24 @@ function runtimeDebugLog(...args) {
|
|
|
95
142
|
}
|
|
96
143
|
/** 当 provider 未配置 contextLimit 时使用的默认值 */
|
|
97
144
|
const FALLBACK_CONTEXT_LIMIT = 64_000;
|
|
145
|
+
/**
|
|
146
|
+
* 解析 provider 的压缩配置,返回实际用于触发压缩的 contextLimit/maxOutput/inputLimit/threshold。
|
|
147
|
+
* 供 session_started 事件、startCompactionDebugLog、requestChatCompletion 共用,
|
|
148
|
+
* 确保前端显示的压缩点与后端实际触发阈值完全一致(消除前端自算公式的脱节)。
|
|
149
|
+
*/
|
|
150
|
+
function resolveCompactionConfig(provider) {
|
|
151
|
+
const contextLimit = (provider.contextLimit && provider.contextLimit > 0)
|
|
152
|
+
? provider.contextLimit
|
|
153
|
+
: FALLBACK_CONTEXT_LIMIT;
|
|
154
|
+
const maxOutput = (provider.maxOutput && provider.maxOutput > 0)
|
|
155
|
+
? provider.maxOutput
|
|
156
|
+
: 8_192;
|
|
157
|
+
const inputLimit = (provider.inputLimit && provider.inputLimit > 0)
|
|
158
|
+
? provider.inputLimit
|
|
159
|
+
: undefined;
|
|
160
|
+
const threshold = calculateCompactionThreshold(contextLimit, maxOutput, inputLimit);
|
|
161
|
+
return { contextLimit, maxOutput, inputLimit, threshold };
|
|
162
|
+
}
|
|
98
163
|
async function withTimeout(operation, timeoutMs, label, onTimeout) {
|
|
99
164
|
if (timeoutMs <= 0) {
|
|
100
165
|
return await operation;
|
|
@@ -128,6 +193,10 @@ function normalizeNonNegativeInteger(value, fallback) {
|
|
|
128
193
|
return 0;
|
|
129
194
|
}
|
|
130
195
|
const numeric = typeof value === "number" ? value : Number(value);
|
|
196
|
+
// -1 是无限重试哨兵值,直接透传
|
|
197
|
+
if (numeric === INFINITE_RETRY_SENTINEL) {
|
|
198
|
+
return INFINITE_RETRY_SENTINEL;
|
|
199
|
+
}
|
|
131
200
|
return Number.isFinite(numeric) && numeric > 0
|
|
132
201
|
? Math.floor(numeric)
|
|
133
202
|
: fallback;
|
|
@@ -169,6 +238,18 @@ function estimateContextTokens(systemPrompt, exposedTools) {
|
|
|
169
238
|
}
|
|
170
239
|
return Math.ceil(charCount / ESTIMATED_CHARS_PER_TOKEN);
|
|
171
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* 估算 exposedTools 定义的 token 开销。
|
|
243
|
+
* tools 的 JSON schema(函数名、描述、参数定义)会被序列化进请求体,
|
|
244
|
+
* 占用可观 token 数(十几个工具可达数千 tokens),必须计入上下文估算。
|
|
245
|
+
* 否则 estimateConversationTokens 系统性偏低,导致主动压缩阈值无法触达。
|
|
246
|
+
*/
|
|
247
|
+
function estimateToolsTokens(exposedTools) {
|
|
248
|
+
if (exposedTools.length === 0)
|
|
249
|
+
return 0;
|
|
250
|
+
const toolsJson = JSON.stringify(toOpenAiTools(exposedTools));
|
|
251
|
+
return estimateTokensFromText(toolsJson);
|
|
252
|
+
}
|
|
172
253
|
async function delay(ms) {
|
|
173
254
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
174
255
|
}
|
|
@@ -196,6 +277,36 @@ function modelSupportsImageInput(model, defaultProvider, providerOptions) {
|
|
|
196
277
|
* 解析 ACode 当前会话模型:优先显式启动参数,其次兼容 cc-switch 写入的
|
|
197
278
|
* OpenCode-style `provider.default.model`,最后回落到旧版 top-level model。
|
|
198
279
|
*/
|
|
280
|
+
/**
|
|
281
|
+
* 按优先级从 provider 配置中查找字段值:
|
|
282
|
+
* 1. options.*(通用配置)
|
|
283
|
+
* 2. models.<currentModel>.*(当前模型专属配置,面板初始化写入的位置)
|
|
284
|
+
* 3. provider.*(provider 级别兜底)
|
|
285
|
+
*
|
|
286
|
+
* @param optionsValue providerOptions 上的值(已取出)
|
|
287
|
+
* @param defaultProvider provider.default 对象
|
|
288
|
+
* @param currentModel 当前使用的模型名
|
|
289
|
+
* @param fieldName 要查找的字段名(如 "contextLimit")
|
|
290
|
+
*/
|
|
291
|
+
function resolveProviderConfigValue(optionsValue, defaultProvider, currentModel, fieldName) {
|
|
292
|
+
// 1. options 级别优先
|
|
293
|
+
if (optionsValue !== undefined)
|
|
294
|
+
return optionsValue;
|
|
295
|
+
if (!defaultProvider)
|
|
296
|
+
return undefined;
|
|
297
|
+
// 2. models.<modelName>.<fieldName>
|
|
298
|
+
const models = defaultProvider.models;
|
|
299
|
+
if (isRecord(models)) {
|
|
300
|
+
const modelConfig = models[currentModel];
|
|
301
|
+
if (isRecord(modelConfig)) {
|
|
302
|
+
const modelValue = modelConfig[fieldName];
|
|
303
|
+
if (modelValue !== undefined)
|
|
304
|
+
return modelValue;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// 3. provider 级别兜底
|
|
308
|
+
return defaultProvider[fieldName];
|
|
309
|
+
}
|
|
199
310
|
function resolveProviderConfig(config, acodeConfig) {
|
|
200
311
|
const defaultProvider = acodeConfig.provider?.default;
|
|
201
312
|
const providerOptions = defaultProvider?.options;
|
|
@@ -203,17 +314,36 @@ function resolveProviderConfig(config, acodeConfig) {
|
|
|
203
314
|
defaultProvider?.model ||
|
|
204
315
|
acodeConfig.model ||
|
|
205
316
|
"acode-default";
|
|
317
|
+
// effort 优先级:runtime config (per-session/per-instance) > provider options (acode.json) > provider default
|
|
318
|
+
const effort = normalizeText(config.effort) ||
|
|
319
|
+
normalizeText(providerOptions?.effort) ||
|
|
320
|
+
normalizeText(defaultProvider?.effort) ||
|
|
321
|
+
undefined;
|
|
206
322
|
return {
|
|
207
323
|
model,
|
|
208
324
|
baseURL: normalizeText(providerOptions?.baseURL || providerOptions?.baseUrl),
|
|
209
325
|
apiKey: normalizeText(providerOptions?.apiKey),
|
|
326
|
+
effort,
|
|
210
327
|
systemPrompt: config.systemPrompt || acodeConfig.systemPrompt,
|
|
211
328
|
stream: providerOptions?.stream !== false,
|
|
212
|
-
|
|
329
|
+
// ★ 安全修复:使用 normalizePositiveInteger 禁止设为 0,
|
|
330
|
+
// 0 会导致 withTimeout 直接返回 operation(禁用超时),
|
|
331
|
+
// fetch 挂起时请求永久不返回,agent 死锁。
|
|
332
|
+
requestTimeoutMs: normalizePositiveInteger(providerOptions?.requestTimeoutMs ?? providerOptions?.timeoutMs, DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS),
|
|
333
|
+
requestTotalTimeoutMs: normalizePositiveInteger(providerOptions?.requestTotalTimeoutMs, DEFAULT_PROVIDER_REQUEST_TOTAL_TIMEOUT_MS),
|
|
213
334
|
requestRetryCount: normalizeNonNegativeInteger(providerOptions?.requestRetryCount, DEFAULT_PROVIDER_REQUEST_RETRY_COUNT),
|
|
214
335
|
mcpToolTimeoutMs: normalizePositiveInteger(providerOptions?.mcpToolTimeoutMs, DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS),
|
|
215
336
|
mcpPollingToolTimeoutMs: normalizeNonNegativeInteger(providerOptions?.mcpPollingToolTimeoutMs, DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS),
|
|
216
337
|
supportsImageInput: modelSupportsImageInput(model, defaultProvider, providerOptions),
|
|
338
|
+
// ★ 从 provider 配置中读取 contextLimit/inputLimit/maxOutput,
|
|
339
|
+
// requestChatCompletion 和 startCompactionDebugLog 使用这些值计算压缩触发阈值。
|
|
340
|
+
// 按优先级查找三个位置:
|
|
341
|
+
// 1. provider.default.options.*(options 级别,通用配置)
|
|
342
|
+
// 2. provider.default.models.<modelName>.*(模型级别,面板初始化写入的位置)
|
|
343
|
+
// 3. provider.default.*(provider 级别兜底)
|
|
344
|
+
contextLimit: normalizeNonNegativeInteger(resolveProviderConfigValue(providerOptions?.contextLimit, defaultProvider, model, "contextLimit"), 0) || undefined,
|
|
345
|
+
inputLimit: normalizeNonNegativeInteger(resolveProviderConfigValue(providerOptions?.inputLimit, defaultProvider, model, "inputLimit"), 0) || undefined,
|
|
346
|
+
maxOutput: normalizeNonNegativeInteger(resolveProviderConfigValue(providerOptions?.maxOutput, defaultProvider, model, "maxOutput"), 0) || undefined,
|
|
217
347
|
};
|
|
218
348
|
}
|
|
219
349
|
function appendTextPart(parts, text) {
|
|
@@ -522,8 +652,14 @@ function coerceArguments(rawArguments) {
|
|
|
522
652
|
}
|
|
523
653
|
}
|
|
524
654
|
function getBuiltInToolTimeoutMs(tool, args, fallbackTimeoutMs) {
|
|
525
|
-
//
|
|
526
|
-
|
|
655
|
+
// 阻塞等待型工具不受超时限制:
|
|
656
|
+
// - ask_user_question: 阻塞等待用户回答
|
|
657
|
+
// - await_sub_agents: 永久阻塞等待指定子 agent 数组到达终态
|
|
658
|
+
const BLOCKING_BUILT_IN_TOOL_NAMES = new Set([
|
|
659
|
+
ASK_USER_QUESTION_TOOL_NAME,
|
|
660
|
+
"await_sub_agents",
|
|
661
|
+
]);
|
|
662
|
+
if (BLOCKING_BUILT_IN_TOOL_NAMES.has(tool.toolName)) {
|
|
527
663
|
return 0;
|
|
528
664
|
}
|
|
529
665
|
if (tool.toolName !== "Bash") {
|
|
@@ -848,6 +984,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
848
984
|
model: "acode-default",
|
|
849
985
|
stream: false,
|
|
850
986
|
requestTimeoutMs: DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS,
|
|
987
|
+
requestTotalTimeoutMs: DEFAULT_PROVIDER_REQUEST_TOTAL_TIMEOUT_MS,
|
|
851
988
|
requestRetryCount: DEFAULT_PROVIDER_REQUEST_RETRY_COUNT,
|
|
852
989
|
mcpToolTimeoutMs: DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS,
|
|
853
990
|
mcpPollingToolTimeoutMs: DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS,
|
|
@@ -855,6 +992,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
855
992
|
},
|
|
856
993
|
permissions: { mode: "default", allow: [], deny: [] },
|
|
857
994
|
mcpConnections: [],
|
|
995
|
+
ownsMcpConnections: true,
|
|
858
996
|
exposedTools: [],
|
|
859
997
|
commands: [],
|
|
860
998
|
commandRegistry: new ACodeCommandRegistry([]),
|
|
@@ -863,6 +1001,20 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
863
1001
|
accumulatedOutputTokens: 0,
|
|
864
1002
|
};
|
|
865
1003
|
this.abortController = null;
|
|
1004
|
+
/**
|
|
1005
|
+
* 正在处理中的 memory 子 agent 跟踪表。
|
|
1006
|
+
* key = memory URI,value = subId(用于防止同一 memory 被重复委托)。
|
|
1007
|
+
* 子 agent 完成后通过 SubAgentManager 事件回调移除对应 URI。
|
|
1008
|
+
*/
|
|
1009
|
+
this.pendingMemorySubAgents = new Map();
|
|
1010
|
+
/**
|
|
1011
|
+
* memory 委托冷却表(「已解决/已处理」守卫,区别于 pendingMemorySubAgents 的「进行中」守卫)。
|
|
1012
|
+
* key = memory URI,value = 冷却到期时间戳(ms)。
|
|
1013
|
+
* 子 agent 进入终态时写入;冷却期内即使 memory 仍待处理也不再 spawn 新子任务。
|
|
1014
|
+
* 这是切断「委托完成→复发」循环的关键:原实现只有进行中守卫,子 agent 一结束 URI 立即释放,
|
|
1015
|
+
* 导致下一轮 poll 对 sticky 的 updating memory 再次委托。
|
|
1016
|
+
*/
|
|
1017
|
+
this.memoryDelegateCooldownUntil = new Map();
|
|
866
1018
|
this.submitQueue = Promise.resolve();
|
|
867
1019
|
this.activeTurn = false;
|
|
868
1020
|
this.abortRequested = false;
|
|
@@ -895,6 +1047,10 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
895
1047
|
this.hookBus.register(handler);
|
|
896
1048
|
}
|
|
897
1049
|
}
|
|
1050
|
+
/** 获取当前已暴露的工具名称列表(供 SubAgentManager 记录) */
|
|
1051
|
+
getAvailableTools() {
|
|
1052
|
+
return this.state.exposedTools.map((tool) => tool.exposedName);
|
|
1053
|
+
}
|
|
898
1054
|
emitAcodeEvent(type, data) {
|
|
899
1055
|
const event = {
|
|
900
1056
|
type,
|
|
@@ -962,7 +1118,13 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
962
1118
|
`resolvedModel=${provider.model}, baseURL=${provider.baseURL ?? "(none)"}, apiKey=${provider.apiKey ? "[set]" : "(none)"}, ` +
|
|
963
1119
|
`configPath=${this.config.configPath ?? "(none)"}`);
|
|
964
1120
|
const permissions = resolvePermissionConfig(this.config, acodeConfig);
|
|
965
|
-
|
|
1121
|
+
// 子 agent 复用父 agent 的 MCP 连接,避免启动新 MCP server 进程后 401
|
|
1122
|
+
// 共享连接时只暴露 memory 工具,防止子 agent 调用 poll_message/send_group 等
|
|
1123
|
+
// 与父 agent 冲突的工具(原设计通过 AWS_SUB_AGENT=true 限制,共享连接绕过了该机制)
|
|
1124
|
+
const mcpConnections = this.config.sharedMcpConnections
|
|
1125
|
+
? this.filterSharedMcpConnectionsForSubAgent(this.config.sharedMcpConnections)
|
|
1126
|
+
: await this.connectMcpServers(acodeConfig.mcpServers ?? {}, provider.mcpToolTimeoutMs);
|
|
1127
|
+
const ownsMcpConnections = !this.config.sharedMcpConnections;
|
|
966
1128
|
// 获取用户 profile 信息并拼接在 rolePrompt 下方
|
|
967
1129
|
const profilePrompt = await this.fetchProfilePrompt(mcpConnections);
|
|
968
1130
|
const agentId = this.config.agentId || "unknown";
|
|
@@ -1027,6 +1189,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1027
1189
|
provider,
|
|
1028
1190
|
permissions,
|
|
1029
1191
|
mcpConnections,
|
|
1192
|
+
ownsMcpConnections,
|
|
1030
1193
|
exposedTools,
|
|
1031
1194
|
commands,
|
|
1032
1195
|
commandRegistry,
|
|
@@ -1039,11 +1202,22 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1039
1202
|
model: provider.model,
|
|
1040
1203
|
commandCount: commands.length,
|
|
1041
1204
|
});
|
|
1205
|
+
// ★ emit 实际用于触发压缩的配置,前端 AgentContextPanel 直接显示此阈值,
|
|
1206
|
+
// 消除前端自算公式与后端实际触发阈值(min(动态比例, hardLimit-reserved))的脱节。
|
|
1207
|
+
// 注意:此处的 contextLimit/maxOutput/inputLimit 来自 resolveProviderConfig(读 provider.default.* 三级),
|
|
1208
|
+
// 与 adapter 的 totalUsage.contextLimit(读顶层+查表)可能不同源——前端应以这里 emit 的值为准。
|
|
1209
|
+
const compactionConfig = resolveCompactionConfig(provider);
|
|
1042
1210
|
this.emitAcodeEvent("session_started", {
|
|
1043
1211
|
workingDirectory: this.config.workingDirectory,
|
|
1044
1212
|
model: provider.model,
|
|
1045
1213
|
commandCount: commands.length,
|
|
1046
1214
|
estimatedContextTokens: estimateContextTokens(systemMessagesPlainText, exposedTools),
|
|
1215
|
+
contextLimit: compactionConfig.contextLimit,
|
|
1216
|
+
maxOutput: compactionConfig.maxOutput,
|
|
1217
|
+
...(compactionConfig.inputLimit !== undefined
|
|
1218
|
+
? { inputLimit: compactionConfig.inputLimit }
|
|
1219
|
+
: {}),
|
|
1220
|
+
compactionThreshold: compactionConfig.threshold,
|
|
1047
1221
|
});
|
|
1048
1222
|
this.emitAcodeEvent("mcp_status", {
|
|
1049
1223
|
configPath: this.config.configPath,
|
|
@@ -1266,20 +1440,12 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1266
1440
|
if (this.compactionDebugTimer)
|
|
1267
1441
|
return;
|
|
1268
1442
|
const provider = this.state.provider;
|
|
1269
|
-
const contextLimit
|
|
1270
|
-
? provider.contextLimit
|
|
1271
|
-
: FALLBACK_CONTEXT_LIMIT;
|
|
1272
|
-
const maxOutput = (provider.maxOutput && provider.maxOutput > 0)
|
|
1273
|
-
? provider.maxOutput
|
|
1274
|
-
: 8_192;
|
|
1275
|
-
const inputLimit = (provider.inputLimit && provider.inputLimit > 0)
|
|
1276
|
-
? provider.inputLimit
|
|
1277
|
-
: undefined;
|
|
1278
|
-
const triggerThreshold = calculateCompactionThreshold(contextLimit, maxOutput, inputLimit);
|
|
1443
|
+
const { contextLimit, maxOutput, inputLimit, threshold: triggerThreshold } = resolveCompactionConfig(provider);
|
|
1279
1444
|
this.compactionDebugTimer = setInterval(() => {
|
|
1280
1445
|
try {
|
|
1281
1446
|
const actualUsed = this.state.accumulatedInputTokens + this.state.accumulatedOutputTokens;
|
|
1282
|
-
const estimatedUsed = estimateConversationTokens(this.state.conversation)
|
|
1447
|
+
const estimatedUsed = estimateConversationTokens(this.state.conversation)
|
|
1448
|
+
+ estimateToolsTokens(this.state.exposedTools);
|
|
1283
1449
|
const effectiveUsed = Math.max(actualUsed, estimatedUsed);
|
|
1284
1450
|
const sizeKB = conversationSizeKB(this.state.conversation);
|
|
1285
1451
|
const isRunning = this.state.started && !this.state.terminated;
|
|
@@ -1329,8 +1495,17 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1329
1495
|
*/
|
|
1330
1496
|
async submit(message) {
|
|
1331
1497
|
const contextVersion = this.contextVersion;
|
|
1332
|
-
|
|
1333
|
-
|
|
1498
|
+
runtimeDebugLog(`[ACodeRuntime] 📬 submit() queued: contextVersion=${contextVersion}, ` +
|
|
1499
|
+
`activeTurn=${this.activeTurn}, abortRequested=${this.abortRequested}, ` +
|
|
1500
|
+
`terminated=${this.state.terminated}, message.length=${message.length}`);
|
|
1501
|
+
const queued = this.submitQueue.then(() => {
|
|
1502
|
+
runtimeDebugLog(`[ACodeRuntime] ▶️ executeSubmit dequeued: contextVersion=${contextVersion}`);
|
|
1503
|
+
return this.executeSubmit(message, contextVersion);
|
|
1504
|
+
});
|
|
1505
|
+
this.submitQueue = queued.catch(() => {
|
|
1506
|
+
runtimeDebugLog(`[ACodeRuntime] 📬 submit() queued item rejected (catch swallowed)`);
|
|
1507
|
+
});
|
|
1508
|
+
runtimeDebugLog(`[ACodeRuntime] 📬 submit() returning queued promise`);
|
|
1334
1509
|
return queued;
|
|
1335
1510
|
}
|
|
1336
1511
|
/**
|
|
@@ -1456,7 +1631,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1456
1631
|
});
|
|
1457
1632
|
// ★ token 用量已在 runModelTurn 中按迭代更新到 state(非累加),
|
|
1458
1633
|
// 此处不再累加,避免 actualUsed 跨轮只增不减导致 shouldCompact 永远为 true。
|
|
1459
|
-
// finalUsage
|
|
1634
|
+
// finalUsage 用于 turn_complete 事件的 usage 上报(当前迭代的上下文用量,覆盖式)。
|
|
1460
1635
|
runtimeDebugLog(`[ACodeRuntime] 💰 Current context usage: ` +
|
|
1461
1636
|
`input=${this.state.accumulatedInputTokens}, ` +
|
|
1462
1637
|
`output=${this.state.accumulatedOutputTokens}` +
|
|
@@ -1492,15 +1667,17 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1492
1667
|
const finallyDuration = Date.now() - turnStartTime;
|
|
1493
1668
|
runtimeDebugLog(`[ACodeRuntime] 🔚 executeSubmit finally block entered after ${finallyDuration}ms`);
|
|
1494
1669
|
runtimeDebugLog(`[ACodeRuntime] 🔍 turnCompleted=${turnCompleted}, activeTurn=${this.activeTurn}, ` +
|
|
1495
|
-
`abortRequested=${this.abortRequested}`
|
|
1670
|
+
`abortRequested=${this.abortRequested}, contextVersion=${contextVersion}, ` +
|
|
1671
|
+
`currentContextVersion=${this.contextVersion}`);
|
|
1496
1672
|
this.activeTurn = false;
|
|
1497
1673
|
this.abortRequested = false;
|
|
1498
1674
|
// 关键修复:如果异常路径没有发射 turn_complete,强制发射一个
|
|
1499
1675
|
// 这确保 adapter 层的 recoverResolvedTurnWithoutTerminalEvent 能够正确触发
|
|
1500
1676
|
if (!turnCompleted && !this.isTurnSuperseded(contextVersion)) {
|
|
1501
|
-
console.error(`[ACodeRuntime] 🚨 CRITICAL: executeSubmit finally block reached without turn_complete event
|
|
1502
|
-
|
|
1503
|
-
|
|
1677
|
+
console.error(`[ACodeRuntime] 🚨 CRITICAL: executeSubmit finally block reached without turn_complete event! ` +
|
|
1678
|
+
`Duration: ${finallyDuration}ms. ` +
|
|
1679
|
+
`This indicates a code path that should have emitted turn_complete but didn't. ` +
|
|
1680
|
+
`Forcing failed turn_complete to prevent state stuck.`);
|
|
1504
1681
|
this.emitAcodeEvent("error", {
|
|
1505
1682
|
message: "ACode turn completed without proper completion event (internal error)",
|
|
1506
1683
|
});
|
|
@@ -1511,8 +1688,19 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1511
1688
|
}
|
|
1512
1689
|
/** 中止当前轮次;由执行轮次统一发出一个终态事件,避免 aborted/failed 双终态。 */
|
|
1513
1690
|
async abort() {
|
|
1691
|
+
const hadController = this.abortController !== null;
|
|
1692
|
+
const wasActiveTurn = this.activeTurn;
|
|
1514
1693
|
this.abortRequested = this.activeTurn;
|
|
1515
|
-
this.abortController
|
|
1694
|
+
if (this.abortController) {
|
|
1695
|
+
runtimeDebugLog(`[ACodeRuntime] ⛔ abort() calling abortController.abort() (activeTurn=${wasActiveTurn})`);
|
|
1696
|
+
this.abortController.abort();
|
|
1697
|
+
}
|
|
1698
|
+
else {
|
|
1699
|
+
console.warn(`[ACodeRuntime] ⛔ abort() called but abortController is null! ` +
|
|
1700
|
+
`activeTurn=${wasActiveTurn}, abortRequested will be set to ${wasActiveTurn}. ` +
|
|
1701
|
+
`This means abort() CANNOT cancel any running fetch/stream — ` +
|
|
1702
|
+
`if a built-in tool is executing, it will NOT be interrupted.`);
|
|
1703
|
+
}
|
|
1516
1704
|
this.abortController = null;
|
|
1517
1705
|
// ask_user_question 等待用户回答期间被 abort,需 reject 所有 waiter
|
|
1518
1706
|
this.rejectAllQuestionWaiters("aborted");
|
|
@@ -1566,7 +1754,12 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1566
1754
|
description: opts.description,
|
|
1567
1755
|
prompt: opts.prompt,
|
|
1568
1756
|
workingDirectory,
|
|
1569
|
-
parentConfig
|
|
1757
|
+
parentConfig: {
|
|
1758
|
+
...parentConfig,
|
|
1759
|
+
// 注入父 agent 的 MCP 连接,子 agent 复用而非启动新 MCP server 进程
|
|
1760
|
+
// 避免 launch binding 已被父 agent 消费后子 agent 401
|
|
1761
|
+
sharedMcpConnections: this.state?.mcpConnections,
|
|
1762
|
+
},
|
|
1570
1763
|
});
|
|
1571
1764
|
// 自动 start(非阻塞,立即返回)
|
|
1572
1765
|
await manager.start(subId);
|
|
@@ -1753,16 +1946,16 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1753
1946
|
const rtOutputTokens = response.usage.completion_tokens ?? response.usage.output_tokens;
|
|
1754
1947
|
if (typeof rtInputTokens === "number" &&
|
|
1755
1948
|
typeof rtOutputTokens === "number") {
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
}
|
|
1949
|
+
// ★ finalUsage 记录当前迭代的实际 token 用量(覆盖式,非累加)。
|
|
1950
|
+
// prompt_tokens 代表本次请求时 provider 看到的完整上下文大小,
|
|
1951
|
+
// 若跨迭代累加会得到 N × 上下文大小 的无意义总和,导致 turn_complete
|
|
1952
|
+
// 上报的 usage 远超真实上下文,前端"上下文"标签会瞬间跳到几 M 再跳回。
|
|
1953
|
+
// 改为覆盖式,与 state.accumulatedInputTokens 及 AcodeSdkAdapter 的
|
|
1954
|
+
// "当前上下文"语义保持一致。
|
|
1955
|
+
finalUsage = {
|
|
1956
|
+
inputTokens: rtInputTokens,
|
|
1957
|
+
outputTokens: rtOutputTokens,
|
|
1958
|
+
};
|
|
1766
1959
|
accumulatedPromptTokens += rtInputTokens;
|
|
1767
1960
|
// ★ 更新 state 为当前迭代的实际 token 用量(非累加)
|
|
1768
1961
|
// prompt_tokens 代表本次请求时 provider 看到的完整上下文大小,
|
|
@@ -1771,7 +1964,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1771
1964
|
this.state.accumulatedInputTokens = rtInputTokens;
|
|
1772
1965
|
this.state.accumulatedOutputTokens = rtOutputTokens;
|
|
1773
1966
|
runtimeDebugLog(`[ACodeRuntime] 💰 Token usage: input=${rtInputTokens}, output=${rtOutputTokens} ` +
|
|
1774
|
-
`(turn accumulated: input=${accumulatedPromptTokens}
|
|
1967
|
+
`(turn accumulated cost: input=${accumulatedPromptTokens})`);
|
|
1775
1968
|
}
|
|
1776
1969
|
}
|
|
1777
1970
|
this.state.conversation.push({
|
|
@@ -1881,7 +2074,14 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1881
2074
|
abortController.abort();
|
|
1882
2075
|
});
|
|
1883
2076
|
if (!response.ok) {
|
|
1884
|
-
|
|
2077
|
+
// ★ 防御修复:用 withTimeout 包裹 response.text(),防止错误响应 body stream 挂起
|
|
2078
|
+
const errorBody = await withTimeout(response.text(), provider.requestTimeoutMs, "ACode LLM summarization error response body", () => {
|
|
2079
|
+
abortController.abort();
|
|
2080
|
+
});
|
|
2081
|
+
const truncatedBody = errorBody.length > 500
|
|
2082
|
+
? `${errorBody.slice(0, 500)}...(truncated, total ${errorBody.length} chars)`
|
|
2083
|
+
: errorBody;
|
|
2084
|
+
throw new Error(`ACode LLM summarization request failed: ${response.status} ${truncatedBody}`);
|
|
1885
2085
|
}
|
|
1886
2086
|
const payload = (await response.json());
|
|
1887
2087
|
const content = payload.choices?.[0]?.message?.content;
|
|
@@ -1896,28 +2096,17 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1896
2096
|
if (!baseURL) {
|
|
1897
2097
|
throw new Error("ACode provider baseURL is not configured");
|
|
1898
2098
|
}
|
|
1899
|
-
// 主动上下文压缩:在发送 AI 请求前检测 token
|
|
1900
|
-
//
|
|
1901
|
-
|
|
1902
|
-
const contextLimit = (provider.contextLimit && provider.contextLimit > 0)
|
|
1903
|
-
? provider.contextLimit
|
|
1904
|
-
: FALLBACK_CONTEXT_LIMIT;
|
|
1905
|
-
const maxOutput = (provider.maxOutput && provider.maxOutput > 0)
|
|
1906
|
-
? provider.maxOutput
|
|
1907
|
-
: 8_192;
|
|
1908
|
-
// ★ 融合 OpenCode 预算思路:优先用 inputLimit(部分 Provider input+output 总额 ≠ context 上限)
|
|
1909
|
-
// 未配置 inputLimit 时降级到 contextLimit,行为与改进前一致。
|
|
1910
|
-
const inputLimit = (provider.inputLimit && provider.inputLimit > 0)
|
|
1911
|
-
? provider.inputLimit
|
|
1912
|
-
: undefined;
|
|
1913
|
-
// ★ 阈值 = min(contextLimit × 55%, hardLimit - reserved)
|
|
1914
|
-
// hardLimit = inputLimit ?? contextLimit
|
|
1915
|
-
// reserved = min(COMPACTION_BUFFER, maxOutput) // buffer 不超过输出上限
|
|
1916
|
-
const compactionThreshold = calculateCompactionThreshold(contextLimit, maxOutput, inputLimit);
|
|
2099
|
+
// 主动上下文压缩:在发送 AI 请求前检测 token 是否溢出。
|
|
2100
|
+
// ★ 与 session_started 事件 emit 的阈值同源(resolveCompactionConfig),保证前端显示 = 实际触发。
|
|
2101
|
+
const { contextLimit, maxOutput, inputLimit, threshold: compactionThreshold } = resolveCompactionConfig(provider);
|
|
1917
2102
|
// 使用 effectiveUsed = max(actualUsed, estimatedUsed)
|
|
1918
2103
|
// 因为部分 provider 不返回 usage 数据(actualUsed=0),此时降级到文本估算
|
|
1919
2104
|
const actualUsed = this.state.accumulatedInputTokens + this.state.accumulatedOutputTokens;
|
|
1920
|
-
|
|
2105
|
+
// ★ 估算时加入 tools 定义开销,使估算更接近实际请求 token 数。
|
|
2106
|
+
// 之前只估算 conversation 消息,遗漏了 tools 的 JSON schema(可达数千 tokens),
|
|
2107
|
+
// 导致估算系统性偏低、主动压缩阈值无法触达。
|
|
2108
|
+
const estimatedUsed = estimateConversationTokens(this.state.conversation)
|
|
2109
|
+
+ estimateToolsTokens(this.state.exposedTools);
|
|
1921
2110
|
const effectiveUsed = Math.max(actualUsed, estimatedUsed);
|
|
1922
2111
|
const shouldCompact = effectiveUsed >= compactionThreshold;
|
|
1923
2112
|
runtimeDebugLog(`[ACodeRuntime] 🔍 Compaction check: ` +
|
|
@@ -1944,6 +2133,9 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1944
2133
|
const result = await performCompaction(this.state.conversation, contextLimit, maxOutput, {
|
|
1945
2134
|
llmSummarizer: (input) => this.summarizeConversationWithLLM(input),
|
|
1946
2135
|
onPhase: (phaseEvent) => this.handleCompactionPhase(compactionId, phaseEvent),
|
|
2136
|
+
// 传入 effectiveUsed 修复文本估算低于实际 token 数时
|
|
2137
|
+
// post-prune isOverflow 误判导致 summarize 被跳过的问题
|
|
2138
|
+
actualUsedTokens: effectiveUsed,
|
|
1947
2139
|
});
|
|
1948
2140
|
const sizeAfterKB = conversationSizeKB(this.state.conversation);
|
|
1949
2141
|
this.emitAcodeEvent("context_compaction", {
|
|
@@ -1954,6 +2146,8 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1954
2146
|
freedKB: Math.round((sizeBeforeKB - sizeAfterKB) * 10) / 10,
|
|
1955
2147
|
strategy: result.strategy,
|
|
1956
2148
|
freedTokens: result.freedTokens,
|
|
2149
|
+
tokensBefore: result.tokensBefore,
|
|
2150
|
+
tokensAfter: result.tokensAfter,
|
|
1957
2151
|
prunedToolMessages: result.prunedToolMessages,
|
|
1958
2152
|
summarizedMessages: result.summarizedMessages,
|
|
1959
2153
|
messageCountBefore: result.messageCountBefore,
|
|
@@ -1981,6 +2175,16 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
1981
2175
|
messages: this.state.conversation,
|
|
1982
2176
|
stream: provider.stream,
|
|
1983
2177
|
};
|
|
2178
|
+
// ★ 思考强度:写入 reasoning_effort 字段,兼容 OpenAI o-series 及兼容 API。
|
|
2179
|
+
// 未配置 effort 时不写入,由 provider 使用默认值。
|
|
2180
|
+
if (provider.effort) {
|
|
2181
|
+
body.reasoning_effort = provider.effort;
|
|
2182
|
+
}
|
|
2183
|
+
// ★ 流式请求必须设置 stream_options.include_usage,否则 provider 不返回 usage,
|
|
2184
|
+
// 导致 accumulatedInputTokens 永远为 0,主动压缩无法基于实际用量触发。
|
|
2185
|
+
if (provider.stream) {
|
|
2186
|
+
body.stream_options = { include_usage: true };
|
|
2187
|
+
}
|
|
1984
2188
|
if (allowedTools.length > 0) {
|
|
1985
2189
|
body.tools = toOpenAiTools(allowedTools);
|
|
1986
2190
|
body.tool_choice = "auto";
|
|
@@ -2011,9 +2215,10 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2011
2215
|
};
|
|
2012
2216
|
let body = buildBody();
|
|
2013
2217
|
emitAiRequest(body);
|
|
2014
|
-
const
|
|
2218
|
+
const infiniteRetries = provider.requestRetryCount === INFINITE_RETRY_SENTINEL;
|
|
2219
|
+
const maxAttempts = infiniteRetries ? Infinity : provider.requestRetryCount + 1;
|
|
2015
2220
|
let contextOverflowCompacted = false;
|
|
2016
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
2221
|
+
for (let attempt = 1; infiniteRetries || attempt <= maxAttempts; attempt += 1) {
|
|
2017
2222
|
const requestAbortController = new AbortController();
|
|
2018
2223
|
this.abortController = requestAbortController;
|
|
2019
2224
|
let timedOut = false;
|
|
@@ -2029,7 +2234,20 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2029
2234
|
requestAbortController.abort();
|
|
2030
2235
|
});
|
|
2031
2236
|
if (!response.ok) {
|
|
2032
|
-
|
|
2237
|
+
// ★ 防御修复:用 withTimeout 包裹 response.text(),
|
|
2238
|
+
// 防止 502/503 等错误响应的 body stream 挂起导致永久阻塞。
|
|
2239
|
+
// 外层 withTimeout(requestTotalTimeoutMs, 10min) 兜底太晚,
|
|
2240
|
+
// 且看门狗可能已被重试 error 事件禁用。
|
|
2241
|
+
const errorBody = await withTimeout(response.text(), provider.requestTimeoutMs, "ACode provider error response body", () => {
|
|
2242
|
+
timedOut = true;
|
|
2243
|
+
requestAbortController.abort();
|
|
2244
|
+
});
|
|
2245
|
+
// ★ 截断过长的错误响应体(如 Cloudflare 502 HTML 页面可达数 KB),
|
|
2246
|
+
// 避免错误消息通过事件链路传播时造成 HTTP POST body 过大或日志膨胀
|
|
2247
|
+
const truncatedBody = errorBody.length > 500
|
|
2248
|
+
? `${errorBody.slice(0, 500)}...(truncated, total ${errorBody.length} chars)`
|
|
2249
|
+
: errorBody;
|
|
2250
|
+
throw new Error(`ACode provider request failed: ${response.status} ${truncatedBody}`);
|
|
2033
2251
|
}
|
|
2034
2252
|
const contentType = response.headers?.get?.("content-type") || "";
|
|
2035
2253
|
if (provider.stream &&
|
|
@@ -2049,7 +2267,14 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2049
2267
|
return {};
|
|
2050
2268
|
}
|
|
2051
2269
|
return payload;
|
|
2052
|
-
})(),
|
|
2270
|
+
})(),
|
|
2271
|
+
// ★ 安全修复:即使无限重试也保留总预算超时兜底,
|
|
2272
|
+
// 防止 provider 持续发送 SSE keep-alive 导致 idle timeout 永不触发、
|
|
2273
|
+
// 请求永久挂起。用户可通过配置更大的 requestTotalTimeoutMs 值来放宽限制。
|
|
2274
|
+
provider.requestTotalTimeoutMs, "ACode provider request total", () => {
|
|
2275
|
+
// 总预算超时:abort 底层 fetch / stream reader,避免泄漏
|
|
2276
|
+
requestAbortController.abort();
|
|
2277
|
+
});
|
|
2053
2278
|
}
|
|
2054
2279
|
catch (error) {
|
|
2055
2280
|
let requestError;
|
|
@@ -2070,12 +2295,14 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2070
2295
|
// 这里不通过 performCompaction(它会先检查 isOverflow),
|
|
2071
2296
|
// 而是直接调用 summarizeConversation 进行强制摘要化
|
|
2072
2297
|
const sizeBeforeKB = conversationSizeKB(this.state.conversation);
|
|
2298
|
+
const tokensBeforeOC = estimateConversationTokens(this.state.conversation); // tokensBefore 命名冲突,加 OC 后缀
|
|
2073
2299
|
const msgCountBefore = this.state.conversation.length;
|
|
2074
2300
|
const compactionId = `compaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2075
2301
|
this.emitAcodeEvent("context_compaction", {
|
|
2076
2302
|
action: "start",
|
|
2077
2303
|
compactionId,
|
|
2078
2304
|
sizeKB: sizeBeforeKB,
|
|
2305
|
+
estimatedTokens: tokensBeforeOC,
|
|
2079
2306
|
messageCount: msgCountBefore,
|
|
2080
2307
|
model: provider.model,
|
|
2081
2308
|
trigger: "provider_overflow_error",
|
|
@@ -2094,6 +2321,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2094
2321
|
});
|
|
2095
2322
|
this.emitCompactionPhase(compactionId, "summarize_done", "阶段2: 摘要化对话", `摘要化了 ${summarizedCount} 条消息`);
|
|
2096
2323
|
const sizeAfterKB = conversationSizeKB(this.state.conversation);
|
|
2324
|
+
const tokensAfterOC = estimateConversationTokens(this.state.conversation);
|
|
2097
2325
|
const msgCountAfter = this.state.conversation.length;
|
|
2098
2326
|
contextOverflowCompacted = true;
|
|
2099
2327
|
const compactedCount = pruneResult.prunedCount + summarizedCount;
|
|
@@ -2103,6 +2331,8 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2103
2331
|
sizeBeforeKB,
|
|
2104
2332
|
sizeAfterKB,
|
|
2105
2333
|
freedKB: Math.round((sizeBeforeKB - sizeAfterKB) * 10) / 10,
|
|
2334
|
+
tokensBefore: tokensBeforeOC,
|
|
2335
|
+
tokensAfter: tokensAfterOC,
|
|
2106
2336
|
messageCountBefore: msgCountBefore,
|
|
2107
2337
|
messageCountAfter: msgCountAfter,
|
|
2108
2338
|
strategy: summarizedCount > 0 ? "summarize" : "prune",
|
|
@@ -2125,7 +2355,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2125
2355
|
attempt -= 1;
|
|
2126
2356
|
continue;
|
|
2127
2357
|
}
|
|
2128
|
-
const shouldRetry = attempt < maxAttempts &&
|
|
2358
|
+
const shouldRetry = (infiniteRetries || attempt < maxAttempts) &&
|
|
2129
2359
|
!this.abortRequested &&
|
|
2130
2360
|
!this.state.terminated &&
|
|
2131
2361
|
isRetryableProviderRequestError(requestError);
|
|
@@ -2133,7 +2363,11 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2133
2363
|
throw requestError;
|
|
2134
2364
|
}
|
|
2135
2365
|
this.emitAcodeEvent("error", {
|
|
2136
|
-
message: `ACode provider request attempt ${attempt}
|
|
2366
|
+
message: `ACode provider request attempt ${attempt}${infiniteRetries ? '' : `/${maxAttempts}`} failed; retrying: ${requestError.message}`,
|
|
2367
|
+
// ★ 关键修复:重试中的错误是瞬态的,不是致命错误。
|
|
2368
|
+
// 如果不标记 nonFatal,适配器会把 status 设为 "error" 并取消看门狗,
|
|
2369
|
+
// 导致后续重试尝试挂起时看门狗无法检测,agent 永久卡死。
|
|
2370
|
+
nonFatal: true,
|
|
2137
2371
|
});
|
|
2138
2372
|
await delay(DEFAULT_PROVIDER_REQUEST_RETRY_DELAY_MS * attempt);
|
|
2139
2373
|
}
|
|
@@ -2226,14 +2460,20 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2226
2460
|
}
|
|
2227
2461
|
const toolName = toolCall.function.name;
|
|
2228
2462
|
runtimeDebugLog(`[ACodeRuntime] 🔍 Looking up tool: ${toolName}`);
|
|
2229
|
-
|
|
2463
|
+
let tool = this.state.exposedTools.find((item) => item.exposedName === toolName);
|
|
2230
2464
|
if (!tool) {
|
|
2231
|
-
|
|
2465
|
+
tool = this.state.exposedTools.find((item) => item.toolName === toolName);
|
|
2466
|
+
if (tool) {
|
|
2467
|
+
runtimeDebugLog(`[ACodeRuntime] ↩️ 裸名回退匹配: ${toolName} -> ${tool.exposedName}`);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
if (!tool) {
|
|
2471
|
+
console.error(`[ACodeRuntime] ❌ Tool not found: ${toolName}, ` +
|
|
2232
2472
|
`available tools: ${this.state.exposedTools.map(t => t.exposedName).join(", ")}`);
|
|
2233
2473
|
this.state.conversation.push({
|
|
2234
2474
|
role: "tool",
|
|
2235
2475
|
tool_call_id: toolCall.id,
|
|
2236
|
-
content: `ACode MCP tool not found: ${
|
|
2476
|
+
content: `ACode MCP tool not found: ${toolName}`,
|
|
2237
2477
|
});
|
|
2238
2478
|
continue;
|
|
2239
2479
|
}
|
|
@@ -2269,7 +2509,13 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2269
2509
|
argumentsPreview: summarizeToolArguments(toolArguments),
|
|
2270
2510
|
});
|
|
2271
2511
|
const toolStart = Date.now();
|
|
2272
|
-
|
|
2512
|
+
const toolTimeoutMs = getBuiltInToolTimeoutMs(tool, toolArguments, this.state.provider.mcpToolTimeoutMs);
|
|
2513
|
+
runtimeDebugLog(`[ACodeRuntime] ▶️ Executing built-in tool: ${tool.exposedName} ` +
|
|
2514
|
+
`(timeout: ${toolTimeoutMs}ms, abortRequested: ${this.abortRequested})`);
|
|
2515
|
+
if (toolTimeoutMs <= 0) {
|
|
2516
|
+
console.warn(`[ACodeRuntime] ⚠️ Built-in tool ${tool.exposedName} has NO timeout (timeoutMs=${toolTimeoutMs})! ` +
|
|
2517
|
+
`If this tool hangs, executeSubmit will NEVER resolve, blocking the submit queue permanently.`);
|
|
2518
|
+
}
|
|
2273
2519
|
const result = await withTimeout(tool.execute(toolArguments, {
|
|
2274
2520
|
onOutput: (outputEvent) => {
|
|
2275
2521
|
this.emitAcodeEvent("command_output", {
|
|
@@ -2288,7 +2534,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2288
2534
|
});
|
|
2289
2535
|
},
|
|
2290
2536
|
toolCallId: toolCall.id,
|
|
2291
|
-
}),
|
|
2537
|
+
}), toolTimeoutMs, `ACode built-in tool ${tool.toolName}`);
|
|
2292
2538
|
const toolDuration = Date.now() - toolStart;
|
|
2293
2539
|
runtimeDebugLog(`[ACodeRuntime] ✅ Built-in tool ${tool.exposedName} completed in ${toolDuration}ms`);
|
|
2294
2540
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
@@ -2367,7 +2613,11 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2367
2613
|
const toolStart = Date.now();
|
|
2368
2614
|
runtimeDebugLog(`[ACodeRuntime] ▶️ Executing MCP tool: ${tool.exposedName} ` +
|
|
2369
2615
|
`(timeout: ${toolTimeoutMs}ms, polling: ${isPollingTool})`);
|
|
2370
|
-
|
|
2616
|
+
// poll_message 工具需要同时检测子 agent 完成事件(有就返回类型),
|
|
2617
|
+
// 使用 sub-agent race 包装器替代直接 MCP 调用。
|
|
2618
|
+
const result = isPollingTool && getSemanticToolName(tool.toolName) === "poll_message"
|
|
2619
|
+
? await this.callPollMessageWithSubAgentRace(connection, tool, toolArguments, toolTimeoutMs)
|
|
2620
|
+
: await this.callMcpToolWithTimeout(connection, tool, toolArguments, toolTimeoutMs, isPollingTool);
|
|
2371
2621
|
const toolDuration = Date.now() - toolStart;
|
|
2372
2622
|
runtimeDebugLog(`[ACodeRuntime] ✅ MCP tool ${tool.exposedName} completed in ${toolDuration}ms`);
|
|
2373
2623
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
@@ -2400,14 +2650,37 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2400
2650
|
await this.appendSyntheticMultimodalUserMessage(resultText, toolCall.id, contextVersion);
|
|
2401
2651
|
}
|
|
2402
2652
|
catch (error) {
|
|
2403
|
-
|
|
2653
|
+
const isTimeout = error instanceof Error &&
|
|
2654
|
+
error.message.includes("timed out after");
|
|
2655
|
+
const isAbortError = error instanceof Error &&
|
|
2656
|
+
error.name === "AbortError";
|
|
2657
|
+
console.error(`[ACodeRuntime] ❌ Tool execution failed: ${toolCall.function.name}`, `isTimeout=${isTimeout}, isAbortError=${isAbortError}`, error instanceof Error ? error.message : String(error));
|
|
2404
2658
|
if (this.isTurnSuperseded(contextVersion)) {
|
|
2659
|
+
runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded after tool error, skipping`);
|
|
2405
2660
|
return;
|
|
2406
2661
|
}
|
|
2407
2662
|
if (this.abortRequested || this.state.terminated) {
|
|
2408
|
-
|
|
2663
|
+
// ★ 修复 BUG(时间线永久卡在"编辑文件"):
|
|
2664
|
+
// abort/termination 时原实现直接 rethrow,跳过 appendToolFailureResult
|
|
2665
|
+
// (3752 行的 mcp_status tool_result 发射),导致 adapter 永远收不到 tool_result,
|
|
2666
|
+
// tool_use_end 永不发射,前端时间线永久卡在 tool_use_start 创建的工具条目。
|
|
2667
|
+
// 修复:在 rethrow 前补发一个带 failed=true 的 tool_result,关闭时间线条目。
|
|
2668
|
+
runtimeDebugLog(`[ACodeRuntime] ⚠️ Rethrowing error due to abort/termination ` +
|
|
2669
|
+
`(abortRequested=${this.abortRequested}, terminated=${this.state.terminated})`);
|
|
2670
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 Emitting mcp_status tool_result (failed, aborted) ` +
|
|
2671
|
+
`for ${tool.exposedName} before rethrow to close timeline entry`);
|
|
2672
|
+
this.emitAcodeEvent("mcp_status", {
|
|
2673
|
+
action: "tool_result",
|
|
2674
|
+
server: tool.kind === "mcp" ? tool.serverName : "builtin",
|
|
2675
|
+
tool: tool.toolName,
|
|
2676
|
+
result: `[aborted] ${error instanceof Error ? error.message : String(error)}`,
|
|
2677
|
+
failed: true,
|
|
2678
|
+
aborted: true,
|
|
2679
|
+
});
|
|
2409
2680
|
throw error instanceof Error ? error : new Error(String(error));
|
|
2410
2681
|
}
|
|
2682
|
+
runtimeDebugLog(`[ACodeRuntime] Calling appendToolFailureResult for ${tool.exposedName} ` +
|
|
2683
|
+
`(isTimeout=${isTimeout})`);
|
|
2411
2684
|
this.appendToolFailureResult(toolCall, tool, error);
|
|
2412
2685
|
}
|
|
2413
2686
|
}
|
|
@@ -2415,13 +2688,19 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2415
2688
|
}
|
|
2416
2689
|
async callMcpToolWithTimeout(connection, tool, toolArguments, timeoutMs, retrySdkTimeout) {
|
|
2417
2690
|
const sdkTimeout = timeoutMs > 0 ? timeoutMs : MCP_SDK_MAX_SAFE_TIMEOUT_MS;
|
|
2691
|
+
// 子 agent 共享父 agent 的 MCP 连接时,注入标记让 MCP server 跳过状态上报
|
|
2692
|
+
// (StatusReporter 绑定父 agentId,子 agent 的事件不应出现在父 agent 时间线上)
|
|
2693
|
+
const isSubAgent = Boolean(this.config.sharedMcpConnections);
|
|
2694
|
+
const finalArguments = isSubAgent
|
|
2695
|
+
? { ...toolArguments, _aws_sub_agent: true }
|
|
2696
|
+
: toolArguments;
|
|
2418
2697
|
while (true) {
|
|
2419
2698
|
const stopWaiter = this.createRuntimeStopWaiter(`ACode MCP tool ${tool.serverName}/${tool.toolName}`);
|
|
2420
2699
|
try {
|
|
2421
2700
|
return await withTimeout(Promise.race([
|
|
2422
2701
|
connection.client.callTool({
|
|
2423
2702
|
name: tool.toolName,
|
|
2424
|
-
arguments:
|
|
2703
|
+
arguments: finalArguments,
|
|
2425
2704
|
}, undefined, { timeout: sdkTimeout }),
|
|
2426
2705
|
stopWaiter.promise,
|
|
2427
2706
|
]), timeoutMs, `ACode MCP tool ${tool.serverName}/${tool.toolName}`);
|
|
@@ -2441,6 +2720,605 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2441
2720
|
}
|
|
2442
2721
|
}
|
|
2443
2722
|
}
|
|
2723
|
+
/**
|
|
2724
|
+
* 将子 agent 终态通知转为 poll_message 格式的合成 DirectMessage。
|
|
2725
|
+
* 主流程:构建符合 MCP poll_message 返回格式的 JSON 字符串。
|
|
2726
|
+
*/
|
|
2727
|
+
static formatSubAgentCompletionAsPollResult(notifications) {
|
|
2728
|
+
const directMessages = notifications.map((n) => {
|
|
2729
|
+
const summary = n.error
|
|
2730
|
+
? `Sub-agent ${n.subId} [${n.status}]: ${n.description}\nError: ${n.error}`
|
|
2731
|
+
: `Sub-agent ${n.subId} [${n.status}]: ${n.description}\nOutput: ${n.output.length > 500 ? n.output.slice(0, 500) + "... (truncated)" : n.output}`;
|
|
2732
|
+
return {
|
|
2733
|
+
msgId: `subagent-completion-${n.subId}-${Date.now()}`,
|
|
2734
|
+
senderId: "ACode-Runtime",
|
|
2735
|
+
senderName: "系统",
|
|
2736
|
+
content: summary,
|
|
2737
|
+
requireReply: false,
|
|
2738
|
+
prompt: `Sub-agent ${n.subId} (${n.description}) has reached terminal state: ${n.status}. Review its output and decide next steps.`,
|
|
2739
|
+
timestamp: new Date().toISOString(),
|
|
2740
|
+
};
|
|
2741
|
+
});
|
|
2742
|
+
return {
|
|
2743
|
+
content: [
|
|
2744
|
+
{
|
|
2745
|
+
type: "text",
|
|
2746
|
+
text: JSON.stringify({
|
|
2747
|
+
directMessages,
|
|
2748
|
+
groupMessages: [],
|
|
2749
|
+
fromBuffer: false,
|
|
2750
|
+
systemMessages: [],
|
|
2751
|
+
}, null, 2),
|
|
2752
|
+
},
|
|
2753
|
+
],
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* 将子 agent 终态通知合并到已有的 MCP poll_message 返回结果中。
|
|
2758
|
+
* 主流程:解析 MCP 返回的 JSON -> 将通知转为 DirectMessage 插入到 directMessages 前面 -> 重新序列化。
|
|
2759
|
+
*/
|
|
2760
|
+
static mergeSubAgentCompletionsIntoMcpResult(mcpResult, notifications) {
|
|
2761
|
+
if (notifications.length === 0)
|
|
2762
|
+
return mcpResult;
|
|
2763
|
+
// 提取 MCP 结果中的文本
|
|
2764
|
+
const resultText = toToolResultText(mcpResult);
|
|
2765
|
+
let parsed;
|
|
2766
|
+
try {
|
|
2767
|
+
parsed = JSON.parse(resultText);
|
|
2768
|
+
}
|
|
2769
|
+
catch {
|
|
2770
|
+
// 如果 MCP 结果不是 JSON(异常情况),直接返回原结果
|
|
2771
|
+
return mcpResult;
|
|
2772
|
+
}
|
|
2773
|
+
const subAgentMessages = notifications.map((n) => {
|
|
2774
|
+
const summary = n.error
|
|
2775
|
+
? `Sub-agent ${n.subId} [${n.status}]: ${n.description}\nError: ${n.error}`
|
|
2776
|
+
: `Sub-agent ${n.subId} [${n.status}]: ${n.description}\nOutput: ${n.output.length > 500 ? n.output.slice(0, 500) + "... (truncated)" : n.output}`;
|
|
2777
|
+
return {
|
|
2778
|
+
msgId: `subagent-completion-${n.subId}-${Date.now()}`,
|
|
2779
|
+
senderId: "ACode-Runtime",
|
|
2780
|
+
senderName: "系统",
|
|
2781
|
+
content: summary,
|
|
2782
|
+
requireReply: false,
|
|
2783
|
+
prompt: `Sub-agent ${n.subId} (${n.description}) has reached terminal state: ${n.status}. Review its output and decide next steps.`,
|
|
2784
|
+
timestamp: new Date().toISOString(),
|
|
2785
|
+
};
|
|
2786
|
+
});
|
|
2787
|
+
const mergedDirectMessages = [
|
|
2788
|
+
...subAgentMessages,
|
|
2789
|
+
...(parsed.directMessages ?? []),
|
|
2790
|
+
];
|
|
2791
|
+
return {
|
|
2792
|
+
content: [
|
|
2793
|
+
{
|
|
2794
|
+
type: "text",
|
|
2795
|
+
text: JSON.stringify({
|
|
2796
|
+
...parsed,
|
|
2797
|
+
directMessages: mergedDirectMessages,
|
|
2798
|
+
}, null, 2),
|
|
2799
|
+
},
|
|
2800
|
+
],
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
/**
|
|
2804
|
+
* Memory 自动委托 sub-agent 完成一轮后的外部重新检查。
|
|
2805
|
+
*
|
|
2806
|
+
* 查询服务端 memory 真实状态(通过 MCP 工具直接调用,非 LLM 自报),
|
|
2807
|
+
* 判断 sub-agent 是否真正完成了处理:
|
|
2808
|
+
* - memory_file_change:检查每条 memory 的 memory_status 是否已回到 normal
|
|
2809
|
+
* - memory_expiration:检查每条 memory 是否已续期(expiresAt 在未来)或已清理(read 返回 null)
|
|
2810
|
+
* - memory_cleanup:检查 get_memory_stats 中对应类型/域是否仍 exceeded
|
|
2811
|
+
*
|
|
2812
|
+
* 若仍有未完成项,返回 { done: false, continueMessage } 包含 "经检查,未完成对xxx的处理,请继续"
|
|
2813
|
+
* 及具体未完成列表,由 SubAgentManager 注入同一 sub-agent 的下一轮 submit。
|
|
2814
|
+
*
|
|
2815
|
+
* recheck 自身出错(MCP 连接不可用、工具调用超时等)时返回 { done: true },不阻塞 sub-agent 结束。
|
|
2816
|
+
*/
|
|
2817
|
+
async recheckMemoryDelegate(type, uris, output, turn) {
|
|
2818
|
+
const connection = this.state.mcpConnections.find((c) => c.serverName === 'aws-client-agent-mcp');
|
|
2819
|
+
if (!connection) {
|
|
2820
|
+
runtimeDebugLog(`[ACodeRuntime] recheckMemoryDelegate: aws-client-agent-mcp connection not found, treating as done`);
|
|
2821
|
+
return { done: true };
|
|
2822
|
+
}
|
|
2823
|
+
try {
|
|
2824
|
+
if (type === 'memory_file_change') {
|
|
2825
|
+
return await this.recheckMemoryFileChange(connection, uris);
|
|
2826
|
+
}
|
|
2827
|
+
if (type === 'memory_expiration') {
|
|
2828
|
+
return await this.recheckMemoryExpiration(connection, uris);
|
|
2829
|
+
}
|
|
2830
|
+
return await this.recheckMemoryCleanup(connection, uris);
|
|
2831
|
+
}
|
|
2832
|
+
catch (err) {
|
|
2833
|
+
runtimeDebugLog(`[ACodeRuntime] recheckMemoryDelegate failed (type=${type}, turn=${turn}): ` +
|
|
2834
|
+
`${err instanceof Error ? err.message : String(err)}, treating as NOT done (retry via recheck loop)`);
|
|
2835
|
+
// 校验异常(读取失败/超时/服务端错误)一律判「未完成」,由 startWithRecheck 的
|
|
2836
|
+
// 重试循环(最多 3 次)兜底;既不提前释放 URI,也不会因一次抖动就把子 agent 判为完成。
|
|
2837
|
+
return {
|
|
2838
|
+
done: false,
|
|
2839
|
+
continueMessage: `经检查,无法确认以下 memory 是否已处理(校验异常:${err instanceof Error ? err.message : String(err)})。` +
|
|
2840
|
+
`请重新执行验证步骤(read_memory / get_memory_stats)确认真实状态后再继续。`,
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* 通过 MCP read_memory 工具读取单条 memory 记录。
|
|
2846
|
+
* 注入 _aws_sub_agent=true 避免状态上报污染父 agent 时间线。
|
|
2847
|
+
* 返回 null 表示 memory 明确不存在(服务端返回 null/undefined 或非对象)。
|
|
2848
|
+
* 读取/解析异常(网络抖动、超时、服务端错误)会向上抛出,交由 recheckMemoryDelegate
|
|
2849
|
+
* 统一判定为「未完成」,避免被误当作「已处理」。
|
|
2850
|
+
*/
|
|
2851
|
+
async readMemoryViaMcp(connection, uri) {
|
|
2852
|
+
try {
|
|
2853
|
+
const result = await withTimeout(connection.client.callTool({
|
|
2854
|
+
name: 'read_memory',
|
|
2855
|
+
arguments: { uri, _aws_sub_agent: true },
|
|
2856
|
+
}, undefined, { timeout: 30_000 }), 30_000, `recheck read_memory ${uri}`);
|
|
2857
|
+
const text = toToolResultText(result);
|
|
2858
|
+
const parsed = JSON.parse(text);
|
|
2859
|
+
if (parsed === null || parsed === undefined)
|
|
2860
|
+
return null;
|
|
2861
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed))
|
|
2862
|
+
return null;
|
|
2863
|
+
return parsed;
|
|
2864
|
+
}
|
|
2865
|
+
catch (err) {
|
|
2866
|
+
// 读取/解析异常(网络抖动、超时、服务端错误)应向上传播,交由 recheckMemoryDelegate
|
|
2867
|
+
// 统一判定为「未完成」(done:false),而非被误当作「memory 不存在 = 已处理」。
|
|
2868
|
+
// 仅当服务端正返回明确的 null/undefined 时才表示 memory 不存在。
|
|
2869
|
+
throw err;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
/** recheck memory_file_change:检查 memory_status 是否仍为 updating */
|
|
2873
|
+
async recheckMemoryFileChange(connection, uris) {
|
|
2874
|
+
const stillUpdating = [];
|
|
2875
|
+
await Promise.all(uris.map(async (uri) => {
|
|
2876
|
+
const mem = await this.readMemoryViaMcp(connection, uri);
|
|
2877
|
+
if (mem && mem.memory_status === 'updating') {
|
|
2878
|
+
stillUpdating.push(uri);
|
|
2879
|
+
}
|
|
2880
|
+
}));
|
|
2881
|
+
if (stillUpdating.length === 0) {
|
|
2882
|
+
runtimeDebugLog(`[ACodeRuntime] recheck memory_file_change: all ${uris.length} URIs done (memory_status=normal or deleted)`);
|
|
2883
|
+
return { done: true };
|
|
2884
|
+
}
|
|
2885
|
+
const list = stillUpdating.map((u) => `- ${u}`).join('\n');
|
|
2886
|
+
runtimeDebugLog(`[ACodeRuntime] recheck memory_file_change: ${stillUpdating.length}/${uris.length} URIs still updating`);
|
|
2887
|
+
return {
|
|
2888
|
+
done: false,
|
|
2889
|
+
continueMessage: `经检查,未完成对以下 memory 的更新,请继续:\n${list}\n\n` +
|
|
2890
|
+
`这些 memory 的 memory_status 仍为 "updating"。请重新 read_memory 查看内容,` +
|
|
2891
|
+
`查看绑定文件最新内容,并调用 update_memory 完成更新。`,
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
/** recheck memory_expiration:检查 expiresAt 是否仍过期 或 memory_status 是否仍为 recalling */
|
|
2895
|
+
async recheckMemoryExpiration(connection, uris) {
|
|
2896
|
+
const stillExpired = [];
|
|
2897
|
+
const now = Date.now();
|
|
2898
|
+
await Promise.all(uris.map(async (uri) => {
|
|
2899
|
+
const mem = await this.readMemoryViaMcp(connection, uri);
|
|
2900
|
+
if (!mem)
|
|
2901
|
+
return; // memory 已删除/不存在 → 视为已处理
|
|
2902
|
+
const expiresAt = mem.expiresAt ?? mem.expires_at;
|
|
2903
|
+
const isRecalling = mem.memory_status === 'recalling';
|
|
2904
|
+
const isExpired = typeof expiresAt === 'string' && new Date(expiresAt).getTime() < now;
|
|
2905
|
+
if (isRecalling || isExpired) {
|
|
2906
|
+
stillExpired.push(uri);
|
|
2907
|
+
}
|
|
2908
|
+
}));
|
|
2909
|
+
if (stillExpired.length === 0) {
|
|
2910
|
+
runtimeDebugLog(`[ACodeRuntime] recheck memory_expiration: all ${uris.length} URIs done (renewed, cleaned, or no longer expired)`);
|
|
2911
|
+
return { done: true };
|
|
2912
|
+
}
|
|
2913
|
+
const list = stillExpired.map((u) => `- ${u}`).join('\n');
|
|
2914
|
+
runtimeDebugLog(`[ACodeRuntime] recheck memory_expiration: ${stillExpired.length}/${uris.length} URIs still expired or recalling`);
|
|
2915
|
+
return {
|
|
2916
|
+
done: false,
|
|
2917
|
+
continueMessage: `经检查,未完成对以下 memory 的处理,请继续:\n${list}\n\n` +
|
|
2918
|
+
`这些 memory 仍处于过期或回忆中状态。请调用 memory_recurrence 获取内容,` +
|
|
2919
|
+
`对需要保留的调用 update_memory 续期,对无价值的调用 memory_recurrence_complete 清理。`,
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
/** recheck memory_cleanup:检查 get_memory_stats 中对应类型/域是否仍 exceeded */
|
|
2923
|
+
async recheckMemoryCleanup(connection, uris) {
|
|
2924
|
+
// delegate URIs 格式为 "${memoryType}/${domain}"
|
|
2925
|
+
let stats;
|
|
2926
|
+
try {
|
|
2927
|
+
const result = await withTimeout(connection.client.callTool({
|
|
2928
|
+
name: 'get_memory_stats',
|
|
2929
|
+
arguments: { _aws_sub_agent: true },
|
|
2930
|
+
}, undefined, { timeout: 30_000 }), 30_000, 'recheck get_memory_stats');
|
|
2931
|
+
const text = toToolResultText(result);
|
|
2932
|
+
stats = JSON.parse(text);
|
|
2933
|
+
}
|
|
2934
|
+
catch {
|
|
2935
|
+
// stats 查询失败 → 保守判为「未完成」,避免一次抖动就把清理判为完成并释放 URI。
|
|
2936
|
+
// 交由 startWithRecheck 的重试循环兜底;与 recheckMemoryDelegate 中央 catch 行为一致。
|
|
2937
|
+
return {
|
|
2938
|
+
done: false,
|
|
2939
|
+
continueMessage: 'get_memory_stats 查询失败,无法确认容量是否已达标,请重新验证后再继续。',
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
const byType = (stats.by_type ?? {});
|
|
2943
|
+
const stillExceeded = [];
|
|
2944
|
+
for (const uri of uris) {
|
|
2945
|
+
const sepIdx = uri.indexOf('/');
|
|
2946
|
+
if (sepIdx < 0)
|
|
2947
|
+
continue;
|
|
2948
|
+
const memoryType = uri.slice(0, sepIdx);
|
|
2949
|
+
const domain = uri.slice(sepIdx + 1);
|
|
2950
|
+
const typeStats = byType[memoryType];
|
|
2951
|
+
const domainStats = typeStats?.by_domain?.[domain];
|
|
2952
|
+
if (domainStats?.exceeded) {
|
|
2953
|
+
stillExceeded.push(`${memoryType}/${domain} (当前 ${domainStats.count ?? '?'} 条,建议上限 ${domainStats.limit ?? '?'})`);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
if (stillExceeded.length === 0) {
|
|
2957
|
+
runtimeDebugLog(`[ACodeRuntime] recheck memory_cleanup: all ${uris.length} type/domain pairs within limits`);
|
|
2958
|
+
return { done: true };
|
|
2959
|
+
}
|
|
2960
|
+
const list = stillExceeded.map((u) => `- ${u}`).join('\n');
|
|
2961
|
+
runtimeDebugLog(`[ACodeRuntime] recheck memory_cleanup: ${stillExceeded.length}/${uris.length} type/domain pairs still exceeded`);
|
|
2962
|
+
return {
|
|
2963
|
+
done: false,
|
|
2964
|
+
continueMessage: `经检查,以下类型/域尚未整理完成,请继续:\n${list}\n\n` +
|
|
2965
|
+
`请调用 list_memories 查看超限记忆,删除不再需要的 recently_memory 或对 agent_memory 调用 update_memory 续期。`,
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* 构造 memory 自动委托子 agent 的 prompt。
|
|
2970
|
+
* 主流程:根据 delegate 类型(过期整理/文件变更更新)生成自包含指令。
|
|
2971
|
+
*/
|
|
2972
|
+
buildMemoryDelegatePrompt(delegate) {
|
|
2973
|
+
const urisText = delegate.uris.map(u => `- ${u}`).join('\n');
|
|
2974
|
+
if (delegate.type === 'memory_expiration') {
|
|
2975
|
+
return `你是一条专门处理 memory 过期整理的子 agent。
|
|
2976
|
+
|
|
2977
|
+
当前有以下 ${delegate.uris.length} 条即将遗忘的记忆需要整理:
|
|
2978
|
+
${urisText}
|
|
2979
|
+
|
|
2980
|
+
请执行以下步骤(核心逻辑:处理 → 验证 → 未完成则继续):
|
|
2981
|
+
|
|
2982
|
+
=== 处理阶段 ===
|
|
2983
|
+
1. 调用 memory_recurrence(limit=10) 分批获取这些过期记忆的完整内容(最早过期优先)
|
|
2984
|
+
2. 逐条分析:这些内容是否仍然有价值?是否需要保留、更新、归档或删除?
|
|
2985
|
+
3. 对于需要保留的,调用 update_memory 续期;对于无价值的,调用 memory_recurrence_complete 将其清理到永久记忆或删除
|
|
2986
|
+
|
|
2987
|
+
=== 验证阶段(关键,不可跳过) ===
|
|
2988
|
+
4. 再次调用 memory_recurrence(limit=5) 检查是否还有未处理的过期记忆
|
|
2989
|
+
- 如果返回结果中有之前处理过的 URI,说明更新/清理未生效
|
|
2990
|
+
- 如果仍有待处理条目,说明部分操作未完成
|
|
2991
|
+
5. 仔细检查返回结果:
|
|
2992
|
+
- ❌ 若仍有过期记忆未处理(或处理未成功):输出 "经检查,未完成对以下 memory 的处理,请继续:" 列出还记忆,然后回到步骤 1 继续处理
|
|
2993
|
+
- ✅ 若全部处理完毕(返回空或无过期条目):静默结束
|
|
2994
|
+
|
|
2995
|
+
注意:你运行在独立的上下文窗口中,不影响主 agent 的主线任务。请高效完成,不要询问用户。
|
|
2996
|
+
完成后系统会通过查询服务端真实状态进行二次验证(不依赖你的自报结果)。若验证发现仍有 memory 未处理,会发送 "经检查,未完成对以下 memory 的处理,请继续" 消息让你继续处理。`;
|
|
2997
|
+
}
|
|
2998
|
+
if (delegate.type === 'memory_cleanup') {
|
|
2999
|
+
return `你是一条专门处理 memory 容量整理的子 agent。
|
|
3000
|
+
|
|
3001
|
+
以下 memory 类型/域已超过建议容量,需要整理:
|
|
3002
|
+
${urisText}
|
|
3003
|
+
|
|
3004
|
+
请执行以下步骤(核心逻辑:处理 → 验证 → 未完成则继续):
|
|
3005
|
+
|
|
3006
|
+
=== 处理阶段 ===
|
|
3007
|
+
1. 调用 get_memory_stats 查看容量详情
|
|
3008
|
+
2. 调用 list_memories 按类型查看超限记忆
|
|
3009
|
+
3. 删除不再需要的 recently_memory,或更新 agent_memory 续期
|
|
3010
|
+
|
|
3011
|
+
=== 验证阶段(关键,不可跳过) ===
|
|
3012
|
+
4. 再次调用 get_memory_stats 检查各类型/域的 count 是否已降到建议上限以下
|
|
3013
|
+
5. 仔细检查返回结果:
|
|
3014
|
+
- ❌ 若仍有类型/域超过容量上限:输出 "经检查,以下类型/域尚未整理完成,请继续:" 列出具体类型/域和当前容量,然后回到步骤 1 继续处理
|
|
3015
|
+
- ✅ 若全部在建议上限内:静默结束
|
|
3016
|
+
|
|
3017
|
+
注意:你运行在独立的上下文窗口中,不影响主 agent 的主线任务。请高效完成,不要询问用户。
|
|
3018
|
+
完成后系统会通过查询服务端真实状态进行二次验证(不依赖你的自报结果)。若验证发现仍有类型/域超过容量上限,会发送 "经检查,以下类型/域尚未整理完成,请继续" 消息让你继续处理。`;
|
|
3019
|
+
}
|
|
3020
|
+
return `你是一条专门处理永久记忆文件变更更新的子 agent。
|
|
3021
|
+
|
|
3022
|
+
以下 ${delegate.uris.length} 个永久记忆绑定的文件已发生变更:
|
|
3023
|
+
${urisText}
|
|
3024
|
+
|
|
3025
|
+
请执行以下步骤(核心逻辑:更新 → 验证 → 未完成则继续):
|
|
3026
|
+
|
|
3027
|
+
=== 更新阶段 ===
|
|
3028
|
+
1. 调用 read_memory 读取这些 memory 的当前内容
|
|
3029
|
+
2. 查看相关绑定文件的最新内容(bound_files 中列出),理解变更点
|
|
3030
|
+
3. 调用 update_memory 更新 memory 内容,确保与文件最新状态同步
|
|
3031
|
+
|
|
3032
|
+
=== 验证阶段(关键,不可跳过) ===
|
|
3033
|
+
4. 调用 read_memory 重新读取这些 memory,检查 memory_status 字段:
|
|
3034
|
+
- 正常情况:Java 服务端会在 update_memory 后自动将 memory_status 恢复为 normal
|
|
3035
|
+
5. 仔细检查每个 memory 的状态:
|
|
3036
|
+
- ❌ 若仍有 memory 的 memory_status 为 "updating":说明更新未生效,输出 "经检查,未完成对以下 memory 的更新,请继续:" 列出具体 memory URI 和当前状态,然后回到步骤 1 继续处理
|
|
3037
|
+
- ✅ 若全部 memory_status 为 "normal":静默结束
|
|
3038
|
+
|
|
3039
|
+
注意:你运行在独立的上下文窗口中,不影响主 agent 的主线任务。请高效完成,不要询问用户。
|
|
3040
|
+
完成后系统会通过查询服务端真实状态进行二次验证(不依赖你的自报结果)。若验证发现仍有 memory 的 memory_status 为 "updating",会发送 "经检查,未完成对以下 memory 的更新,请继续" 消息让你继续处理。`;
|
|
3041
|
+
}
|
|
3042
|
+
/**
|
|
3043
|
+
* 处理 poll_message 结果中的 memory 自动委托标记 (_acodeAutoDelegate)。
|
|
3044
|
+
* 主流程:解析 MCP 返回 -> 识别标记 -> spawn 子 agent -> 返回简化结果。
|
|
3045
|
+
* 非 ACode 运行时(无 subAgentManager)直接透传原结果,不受影响。
|
|
3046
|
+
* 返回 null 表示全部消息已被委托,调用方应重新 poll_message 继续等待真正的消息。
|
|
3047
|
+
*/
|
|
3048
|
+
async processMemoryAutoDelegates(mcpResult) {
|
|
3049
|
+
const manager = this.config.subAgentManager;
|
|
3050
|
+
if (!manager) {
|
|
3051
|
+
return mcpResult;
|
|
3052
|
+
}
|
|
3053
|
+
// 提取 MCP 结果中的 JSON 文本
|
|
3054
|
+
let resultText;
|
|
3055
|
+
try {
|
|
3056
|
+
resultText = toToolResultText(mcpResult);
|
|
3057
|
+
}
|
|
3058
|
+
catch {
|
|
3059
|
+
return mcpResult;
|
|
3060
|
+
}
|
|
3061
|
+
let parsed;
|
|
3062
|
+
try {
|
|
3063
|
+
parsed = JSON.parse(resultText);
|
|
3064
|
+
}
|
|
3065
|
+
catch {
|
|
3066
|
+
return mcpResult;
|
|
3067
|
+
}
|
|
3068
|
+
const directMessages = parsed.directMessages;
|
|
3069
|
+
if (!Array.isArray(directMessages) || directMessages.length === 0) {
|
|
3070
|
+
return mcpResult;
|
|
3071
|
+
}
|
|
3072
|
+
const remainingMessages = [];
|
|
3073
|
+
let delegateEncountered = 0;
|
|
3074
|
+
for (const msg of directMessages) {
|
|
3075
|
+
const delegate = msg._acodeAutoDelegate;
|
|
3076
|
+
if (delegate && (delegate.type === 'memory_expiration' || delegate.type === 'memory_file_change' || delegate.type === 'memory_cleanup')) {
|
|
3077
|
+
delegateEncountered++;
|
|
3078
|
+
// 过滤掉:① 正在处理中的 URI;② 处于冷却期内的 URI(子 agent 刚结束,尚未到重新委托时机)
|
|
3079
|
+
const nowForFilter = Date.now();
|
|
3080
|
+
const inProgressCount = delegate.uris.filter(uri => this.pendingMemorySubAgents.has(uri)).length;
|
|
3081
|
+
const cooledCount = delegate.uris.filter(uri => {
|
|
3082
|
+
const until = this.memoryDelegateCooldownUntil.get(uri);
|
|
3083
|
+
return until !== undefined && nowForFilter < until;
|
|
3084
|
+
}).length;
|
|
3085
|
+
const newUris = delegate.uris.filter(uri => {
|
|
3086
|
+
if (this.pendingMemorySubAgents.has(uri))
|
|
3087
|
+
return false;
|
|
3088
|
+
const until = this.memoryDelegateCooldownUntil.get(uri);
|
|
3089
|
+
return !(until !== undefined && nowForFilter < until);
|
|
3090
|
+
});
|
|
3091
|
+
if (newUris.length === 0) {
|
|
3092
|
+
// 所有 URI 都在处理中或冷却中,跳过此通知(不委托也不返回给主 agent)
|
|
3093
|
+
runtimeDebugLog(`[ACodeRuntime] Memory ${delegate.type} skipped: ${inProgressCount} in-progress, ${cooledCount} in cooldown (total ${delegate.uris.length} URIs)`);
|
|
3094
|
+
continue;
|
|
3095
|
+
}
|
|
3096
|
+
// 用过滤后的 URI 构造 prompt
|
|
3097
|
+
const prompt = this.buildMemoryDelegatePrompt({
|
|
3098
|
+
type: delegate.type,
|
|
3099
|
+
uris: newUris,
|
|
3100
|
+
});
|
|
3101
|
+
try {
|
|
3102
|
+
const subId = await manager.spawn({
|
|
3103
|
+
parentAgentId: this.config.agentId || 'unknown',
|
|
3104
|
+
parentSessionId: this.config.sessionId,
|
|
3105
|
+
subagentType: 'general',
|
|
3106
|
+
description: `Memory ${delegate.type} auto-delegate (${newUris.length} items)`,
|
|
3107
|
+
prompt,
|
|
3108
|
+
workingDirectory: this.config.workingDirectory,
|
|
3109
|
+
parentConfig: {
|
|
3110
|
+
...this.config,
|
|
3111
|
+
// 注入父 agent 的 MCP 连接,子 agent 复用而非启动新 MCP server 进程
|
|
3112
|
+
sharedMcpConnections: this.state?.mcpConnections,
|
|
3113
|
+
},
|
|
3114
|
+
silent: true,
|
|
3115
|
+
});
|
|
3116
|
+
// 注册完成回调:子 agent 终态时从跟踪表中移除对应 URI
|
|
3117
|
+
for (const uri of newUris) {
|
|
3118
|
+
this.pendingMemorySubAgents.set(uri, subId);
|
|
3119
|
+
}
|
|
3120
|
+
const uriSet = new Set(newUris);
|
|
3121
|
+
manager.subscribe(subId, () => {
|
|
3122
|
+
const entry = manager.getSubAgent(subId);
|
|
3123
|
+
if (entry && (entry.status === 'completed' || entry.status === 'failed' || entry.status === 'cancelled')) {
|
|
3124
|
+
const finishTs = Date.now();
|
|
3125
|
+
for (const uri of uriSet) {
|
|
3126
|
+
if (this.pendingMemorySubAgents.get(uri) === subId) {
|
|
3127
|
+
this.pendingMemorySubAgents.delete(uri);
|
|
3128
|
+
}
|
|
3129
|
+
// 写入冷却窗口:子 agent 终态后一段时间内不再重新委托,切断「完成→复发」循环。
|
|
3130
|
+
// 即使 memory 仍处 updating/过期/超限,冷却期内也仅保留巡检,不 spawn 新子任务。
|
|
3131
|
+
this.memoryDelegateCooldownUntil.set(uri, finishTs + MEMORY_DELEGATE_COOLDOWN_MS);
|
|
3132
|
+
}
|
|
3133
|
+
// 惰性清理过期的冷却条目,避免内存堆积
|
|
3134
|
+
for (const [uri, until] of this.memoryDelegateCooldownUntil) {
|
|
3135
|
+
if (until <= finishTs) {
|
|
3136
|
+
this.memoryDelegateCooldownUntil.delete(uri);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
runtimeDebugLog(`[ACodeRuntime] Memory delegate sub-agent ${subId} finished (${entry.status}), ${uriSet.size} URI(s) entered ${MEMORY_DELEGATE_COOLDOWN_MS}ms cooldown`);
|
|
3140
|
+
}
|
|
3141
|
+
});
|
|
3142
|
+
// 使用带 recheck 循环的启动方式:
|
|
3143
|
+
// 每轮 submit 完成后,通过 recheckMemoryDelegate 查询服务端真实状态,
|
|
3144
|
+
// 若 memory 仍未处理好(memory_status=updating / 仍过期 / 容量仍超限),
|
|
3145
|
+
// 则将 "经检查,未完成对xxx的处理,请继续" 注入同一 sub-agent 的下一轮 submit。
|
|
3146
|
+
// 最多重试 3 次,提供系统级硬保证(不依赖 LLM 自报结果)。
|
|
3147
|
+
const delegateType = delegate.type;
|
|
3148
|
+
const recheckFn = async (output, turn) => {
|
|
3149
|
+
return this.recheckMemoryDelegate(delegateType, newUris, output, turn);
|
|
3150
|
+
};
|
|
3151
|
+
await manager.startWithRecheck(subId, recheckFn, 3);
|
|
3152
|
+
runtimeDebugLog(`[ACodeRuntime] Auto-delegated memory ${delegate.type} to sub-agent ${subId} (${newUris.length} URIs, ${delegate.uris.length - newUris.length} already in progress)`);
|
|
3153
|
+
}
|
|
3154
|
+
catch (e) {
|
|
3155
|
+
// 委托失败时不返回给主 agent:memory 通知不应打断主 agent 的主线任务,
|
|
3156
|
+
// 下次 poll_message 会重新检测并重试委托
|
|
3157
|
+
runtimeDebugLog(`[ACodeRuntime] Failed to auto-delegate memory ${delegate.type} (suppressed, will retry next poll): ${e instanceof Error ? e.message : String(e)}`);
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
else {
|
|
3161
|
+
remainingMessages.push(msg);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
// 未遇到任何 delegate 消息,原样返回(保留原始格式,避免不必要的对象创建)
|
|
3165
|
+
if (delegateEncountered === 0) {
|
|
3166
|
+
return mcpResult;
|
|
3167
|
+
}
|
|
3168
|
+
// 全部 delegate 消息已处理(spawned/skipped/failed),不返回给主 agent
|
|
3169
|
+
if (remainingMessages.length === 0) {
|
|
3170
|
+
return null;
|
|
3171
|
+
}
|
|
3172
|
+
// 有非 delegate 类型的剩余消息,返回简化后的结果
|
|
3173
|
+
const simplified = {
|
|
3174
|
+
...parsed,
|
|
3175
|
+
directMessages: remainingMessages,
|
|
3176
|
+
};
|
|
3177
|
+
return {
|
|
3178
|
+
content: [
|
|
3179
|
+
{
|
|
3180
|
+
type: 'text',
|
|
3181
|
+
text: JSON.stringify(simplified, null, 2),
|
|
3182
|
+
},
|
|
3183
|
+
],
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
/**
|
|
3187
|
+
* poll_message MCP 工具调用的子 agent 完成事件 race 包装器。
|
|
3188
|
+
*
|
|
3189
|
+
* 主流程:
|
|
3190
|
+
* 1. 先检查 completionQueue 中是否有已完成的子 agent -> 有则直接返回合成结果(不调 MCP)
|
|
3191
|
+
* 2. 启动 MCP poll_message 调用与 waitForAnyCompletion 的 race
|
|
3192
|
+
* 3. 子 agent 先完成 -> 返回合成结果(MCP 调用被放弃但仍在运行,下次 poll_message 会复用 pendingPollMessage)
|
|
3193
|
+
* 4. MCP 先返回 -> 合并新完成的子 agent 事件后返回
|
|
3194
|
+
*
|
|
3195
|
+
* 设计说明:
|
|
3196
|
+
* - MCP server 的 poll_message 内部有 while(true) 永久阻塞,只在有消息时返回
|
|
3197
|
+
* - 子 agent 完成事件由 SubAgentManager 在同进程内产生,通过 completionQueue 传递
|
|
3198
|
+
* - 当子 agent 先完成时,MCP 调用被放弃(Promise 不再被 await),MCP server 侧的
|
|
3199
|
+
* pendingPollMessage 仍在运行,下次 poll_message 调用会通过 pendingPollMessage 复用
|
|
3200
|
+
*/
|
|
3201
|
+
async callPollMessageWithSubAgentRace(connection, tool, toolArguments, timeoutMs) {
|
|
3202
|
+
const manager = this.config.subAgentManager;
|
|
3203
|
+
// 没有 SubAgentManager 时,直接走普通 MCP 调用
|
|
3204
|
+
if (!manager) {
|
|
3205
|
+
return await this.callMcpToolWithTimeout(connection, tool, toolArguments, timeoutMs, true);
|
|
3206
|
+
}
|
|
3207
|
+
// 步骤1: 先检查是否有已完成的子 agent -> 有则直接返回合成结果(不调 MCP)
|
|
3208
|
+
const completedBefore = manager.takeCompletedSubAgents();
|
|
3209
|
+
if (completedBefore.length > 0) {
|
|
3210
|
+
runtimeDebugLog(`[ACodeRuntime] poll_message: ${completedBefore.length} sub-agent(s) already completed, returning synthetic result`);
|
|
3211
|
+
return ACodeRuntime.formatSubAgentCompletionAsPollResult(completedBefore);
|
|
3212
|
+
}
|
|
3213
|
+
// 循环:MCP 返回的 memory 通知被全部委托时,重新 poll 继续等待真正的消息
|
|
3214
|
+
let isDelegateRepoll = false; // 首次正常上报,后续委托循环中抑制状态跳动
|
|
3215
|
+
while (true) {
|
|
3216
|
+
// 步骤2: 启动 MCP poll_message 调用与 waitForAnyCompletion 的 race
|
|
3217
|
+
const subAgentWaiter = manager.waitForAnyCompletion();
|
|
3218
|
+
// 委托等待循环中的后续 poll 注入标记:MCP server 跳过 reportToolUse/reportToolResult,
|
|
3219
|
+
// 只调用 reportThinking 保持"思考中"状态,避免"调用MCP↔思考中"反复跳动
|
|
3220
|
+
const pollArgs = isDelegateRepoll
|
|
3221
|
+
? { ...toolArguments, _aws_silent_poll: true }
|
|
3222
|
+
: toolArguments;
|
|
3223
|
+
const mcpCallPromise = this.callMcpToolWithTimeout(connection, tool, pollArgs, timeoutMs, true);
|
|
3224
|
+
try {
|
|
3225
|
+
const raceWinner = await Promise.race([
|
|
3226
|
+
mcpCallPromise.then((result) => ({ type: "mcp", result })),
|
|
3227
|
+
subAgentWaiter.promise.then((notif) => ({ type: "subagent", notif })),
|
|
3228
|
+
]);
|
|
3229
|
+
if (raceWinner.type === "subagent") {
|
|
3230
|
+
// 步骤3: 子 agent 先完成 -> 返回合成结果
|
|
3231
|
+
// 收集所有已完成的子 agent(race 中 resolve 的 + 队列中可能已积累的)
|
|
3232
|
+
const notifications = manager.takeCompletedSubAgents();
|
|
3233
|
+
// raceWinner.notif 可能已从队列中取出(waitForAnyCompletion 内部处理),
|
|
3234
|
+
// 也可能仍在队列中(如果 takeCompletedSubAgents 先执行)
|
|
3235
|
+
if (notifications.length === 0) {
|
|
3236
|
+
notifications.push(raceWinner.notif);
|
|
3237
|
+
}
|
|
3238
|
+
runtimeDebugLog(`[ACodeRuntime] poll_message: sub-agent ${raceWinner.notif.subId} completed during race, returning synthetic result (${notifications.length} notification(s))`);
|
|
3239
|
+
return ACodeRuntime.formatSubAgentCompletionAsPollResult(notifications);
|
|
3240
|
+
}
|
|
3241
|
+
// 步骤4: MCP 先返回 -> 合并新完成的子 agent 事件
|
|
3242
|
+
const completedAfter = manager.takeCompletedSubAgents();
|
|
3243
|
+
if (completedAfter.length > 0) {
|
|
3244
|
+
runtimeDebugLog(`[ACodeRuntime] poll_message: MCP returned, merging ${completedAfter.length} sub-agent completion(s)`);
|
|
3245
|
+
const merged = ACodeRuntime.mergeSubAgentCompletionsIntoMcpResult(raceWinner.result, completedAfter);
|
|
3246
|
+
// 合并后仍需检查 memory 自动委托
|
|
3247
|
+
const processedMerged = await this.processMemoryAutoDelegates(merged);
|
|
3248
|
+
if (processedMerged !== null) {
|
|
3249
|
+
// 校验:结果必须包含实际消息才返回,否则继续 poll
|
|
3250
|
+
if (this.pollResultHasMessages(processedMerged)) {
|
|
3251
|
+
return processedMerged;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
// 全部被委托或结果为空,等待后再重新 poll(防止紧密循环)
|
|
3255
|
+
await this.waitForDelegateRepoll();
|
|
3256
|
+
isDelegateRepoll = true;
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
// 自动委托:识别 memory 过期/更新通知,spawn 子 agent 处理,避免打断主 agent 主线
|
|
3260
|
+
const processedResult = await this.processMemoryAutoDelegates(raceWinner.result);
|
|
3261
|
+
if (processedResult !== null) {
|
|
3262
|
+
// 校验:结果必须包含实际消息才返回,否则继续 poll
|
|
3263
|
+
if (this.pollResultHasMessages(processedResult)) {
|
|
3264
|
+
return processedResult;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
// 全部被委托或结果为空,等待后再重新 poll(防止紧密循环)
|
|
3268
|
+
await this.waitForDelegateRepoll();
|
|
3269
|
+
isDelegateRepoll = true;
|
|
3270
|
+
}
|
|
3271
|
+
finally {
|
|
3272
|
+
subAgentWaiter.cancel();
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* 检查 poll_message 结果是否包含实际消息(directMessages 或 groupMessages 非空)。
|
|
3278
|
+
* 用于循环中过滤空结果,确保 poll_message 只在有消息时返回。
|
|
3279
|
+
*/
|
|
3280
|
+
pollResultHasMessages(result) {
|
|
3281
|
+
try {
|
|
3282
|
+
const text = toToolResultText(result);
|
|
3283
|
+
const parsed = JSON.parse(text);
|
|
3284
|
+
const dm = Array.isArray(parsed.directMessages) ? parsed.directMessages.length : 0;
|
|
3285
|
+
const gm = Array.isArray(parsed.groupMessages) ? parsed.groupMessages.length : 0;
|
|
3286
|
+
return dm > 0 || gm > 0;
|
|
3287
|
+
}
|
|
3288
|
+
catch {
|
|
3289
|
+
return true; // 无法解析时保守返回 true,避免无限循环
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
/**
|
|
3293
|
+
* 在 memory 委托消息被全部处理后,等待一段时间再重新 poll_message。
|
|
3294
|
+
*
|
|
3295
|
+
* 背景:MCP doPollMessage 对 updating memory 每次都返回通知(无去重),
|
|
3296
|
+
* 而 processMemoryAutoDelegates 因 URI 已在 pendingMemorySubAgents 中而返回 null,
|
|
3297
|
+
* 导致 while(true) 无延迟重 poll,每轮触发 handleToolCall 的 reportToolUse/reportThinking
|
|
3298
|
+
* 状态上报,引发"思考→MCP→跳过"快速循环,最终连接丢失。
|
|
3299
|
+
*
|
|
3300
|
+
* 等待策略:
|
|
3301
|
+
* 1. 固定延迟 DELEGATE_REPOLL_DELAY_MS(5s),给子 agent 时间完成 memory 更新
|
|
3302
|
+
* 2. 可被 runtime stop 中断(通过 createRuntimeStopWaiter)
|
|
3303
|
+
* 3. 不消费 completionQueue(下一轮 takeCompletedSubAgents 会取到)
|
|
3304
|
+
*/
|
|
3305
|
+
async waitForDelegateRepoll() {
|
|
3306
|
+
runtimeDebugLog(`[ACodeRuntime] poll_message: all messages delegated, waiting ${DELEGATE_REPOLL_DELAY_MS}ms before re-poll`);
|
|
3307
|
+
const stopWaiter = this.createRuntimeStopWaiter("delegate re-poll delay");
|
|
3308
|
+
try {
|
|
3309
|
+
await Promise.race([
|
|
3310
|
+
new Promise((resolve) => {
|
|
3311
|
+
const timer = setTimeout(resolve, DELEGATE_REPOLL_DELAY_MS);
|
|
3312
|
+
if (timer.unref)
|
|
3313
|
+
timer.unref();
|
|
3314
|
+
}),
|
|
3315
|
+
stopWaiter.promise,
|
|
3316
|
+
]);
|
|
3317
|
+
}
|
|
3318
|
+
finally {
|
|
3319
|
+
stopWaiter.cancel();
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
2444
3322
|
/**
|
|
2445
3323
|
* Build a one-shot waiter that rejects long-running MCP calls when the runtime stops.
|
|
2446
3324
|
* Main flow: register a stop callback -> race it with the SDK call -> remove it after either side settles.
|
|
@@ -2535,6 +3413,8 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2535
3413
|
}
|
|
2536
3414
|
appendToolFailureResult(toolCall, tool, error) {
|
|
2537
3415
|
const resultText = toToolErrorResultText(tool, error);
|
|
3416
|
+
runtimeDebugLog(`[ACodeRuntime] 📤 appendToolFailureResult: emitting mcp_status tool_result ` +
|
|
3417
|
+
`(failed: true) for ${tool.exposedName}, result length: ${resultText.length} chars`);
|
|
2538
3418
|
// 工具执行失败是正常运行过程的一部分(Agent 会继续处理失败结果)。
|
|
2539
3419
|
// 标记 nonFatal=true 让 adapter 不变更运行态为 "error",避免面板按钮异常。
|
|
2540
3420
|
this.emitAcodeEvent("error", {
|
|
@@ -2554,6 +3434,21 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2554
3434
|
content: resultText,
|
|
2555
3435
|
});
|
|
2556
3436
|
}
|
|
3437
|
+
/**
|
|
3438
|
+
* 为子 agent 过滤共享 MCP 连接:只保留 memory 工具。
|
|
3439
|
+
*
|
|
3440
|
+
* 原 MCP server 进程通过 AWS_SUB_AGENT=true 限制子 agent 只能调用 memory 工具,
|
|
3441
|
+
* 共享连接后绕过了该机制。此方法在 ACode runtime 层重新施加限制:
|
|
3442
|
+
* - 复用同一 client/transport(不创建新连接)
|
|
3443
|
+
* - 仅暴露 MEMORY_TOOL_NAMES 中的工具
|
|
3444
|
+
* - 防止子 agent 调用 poll_message(与父 agent 冲突)、send_group 等
|
|
3445
|
+
*/
|
|
3446
|
+
filterSharedMcpConnectionsForSubAgent(connections) {
|
|
3447
|
+
return connections.map((conn) => ({
|
|
3448
|
+
...conn,
|
|
3449
|
+
tools: conn.tools.filter((tool) => SUB_AGENT_ALLOWED_MCP_TOOLS.has(tool.toolName)),
|
|
3450
|
+
}));
|
|
3451
|
+
}
|
|
2557
3452
|
async connectMcpServers(servers, timeoutMs) {
|
|
2558
3453
|
const connections = [];
|
|
2559
3454
|
for (const [serverName, serverSpec] of Object.entries(servers)) {
|
|
@@ -2670,6 +3565,12 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
2670
3565
|
return null;
|
|
2671
3566
|
}
|
|
2672
3567
|
async disconnectMcpServers() {
|
|
3568
|
+
// 共享连接(子 agent)不关闭,由所有者(父 agent)管理生命周期
|
|
3569
|
+
if (!this.state.ownsMcpConnections) {
|
|
3570
|
+
this.state.mcpConnections = [];
|
|
3571
|
+
this.state.exposedTools = [];
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
2673
3574
|
const connections = this.state.mcpConnections;
|
|
2674
3575
|
this.state.mcpConnections = [];
|
|
2675
3576
|
this.state.exposedTools = [];
|