aws-runtime-bridge 1.7.50 → 1.8.0
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 +30 -3
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/AcodeSdkAdapter.js +427 -105
- package/dist/adapter/AcodeSdkAdapter.test.js +1093 -86
- package/dist/adapter/ClaudeSdkAdapter.test.js +31 -8
- package/dist/adapter/CodexSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/CodexSdkAdapter.js +2 -0
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +2 -0
- package/dist/adapter/SdkProviderSpi.d.ts.map +1 -1
- package/dist/adapter/adapter.test.js +2 -2
- package/dist/adapter/idle-poll-loop.d.ts +4 -4
- package/dist/adapter/idle-poll-loop.d.ts.map +1 -1
- package/dist/adapter/idle-poll-loop.js +3 -3
- package/dist/adapter/idle-poll-loop.test.js +15 -1
- package/dist/adapter/types.d.ts +25 -6
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/adapter/types.js +221 -110
- package/dist/adapter/types.test.js +10 -3
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +6 -7
- package/dist/routes/runtime-binding.js +2 -2
- package/dist/routes/terminal.d.ts +20 -5
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +420 -170
- package/dist/routes/terminal.test.js +380 -208
- package/dist/services/mcp-launch-binding-queue.d.ts +1 -0
- package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -1
- package/dist/services/mcp-launch-binding-queue.js +8 -2
- package/dist/services/mcp-launch-binding-queue.test.js +47 -4
- package/dist/services/session-output.d.ts +10 -6
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +29 -14
- package/dist/services/session-output.test.js +93 -39
- package/package/acode/dist/built-in-file-tools.d.ts +1 -1
- package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
- package/package/acode/dist/built-in-file-tools.js +388 -50
- package/package/acode/dist/runtime.d.ts +11 -3
- package/package/acode/dist/runtime.d.ts.map +1 -1
- package/package/acode/dist/runtime.js +902 -190
- package/package/acode/dist/types.d.ts +18 -5
- package/package/acode/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +3 -2
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +11 -9
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js +1 -0
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.d.ts +2 -0
- package/package/aws-client-agent-mcp/dist/status-reporter.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.js +31 -3
- package/package/aws-client-agent-mcp/dist/status-reporter.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +1 -1
- 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.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.js +2 -0
- package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.test.js +43 -0
- package/package/aws-client-agent-mcp/dist/websocket-client.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,26 +1,103 @@
|
|
|
1
|
-
import { EventEmitter } from
|
|
2
|
-
import { Client } from
|
|
3
|
-
import { SSEClientTransport } from
|
|
4
|
-
import { StdioClientTransport } from
|
|
5
|
-
import { StreamableHTTPClientTransport } from
|
|
6
|
-
import { createBuiltInFileTools } from
|
|
7
|
-
import { loadAcodeCommands } from
|
|
8
|
-
import { ACodeCommandRegistry } from
|
|
9
|
-
import { loadAcodeConfig, loadAcodeSkills } from
|
|
10
|
-
import { ACodeHookBus, findDeniedHookResult } from
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
4
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
|
+
import { createBuiltInFileTools, } from "./built-in-file-tools.js";
|
|
7
|
+
import { loadAcodeCommands } from "./commands/commandLoader.js";
|
|
8
|
+
import { ACodeCommandRegistry } from "./commands/commandRegistry.js";
|
|
9
|
+
import { loadAcodeConfig, loadAcodeSkills } from "./config.js";
|
|
10
|
+
import { ACodeHookBus, findDeniedHookResult } from "./hooks/hookBus.js";
|
|
11
11
|
function nowIso() {
|
|
12
12
|
return new Date().toISOString();
|
|
13
13
|
}
|
|
14
14
|
function isRecord(value) {
|
|
15
|
-
return Boolean(value && typeof value ===
|
|
15
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
16
16
|
}
|
|
17
17
|
function isJsonObject(value) {
|
|
18
18
|
return isRecord(value);
|
|
19
19
|
}
|
|
20
20
|
function normalizeText(value) {
|
|
21
|
-
const text = String(value ??
|
|
21
|
+
const text = String(value ?? "").trim();
|
|
22
22
|
return text || undefined;
|
|
23
23
|
}
|
|
24
|
+
const DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 60_000;
|
|
25
|
+
const DEFAULT_PROVIDER_REQUEST_RETRY_COUNT = 2;
|
|
26
|
+
const DEFAULT_PROVIDER_REQUEST_RETRY_DELAY_MS = 1000;
|
|
27
|
+
const DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS = 60_000;
|
|
28
|
+
const DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS = 30 * 60_000;
|
|
29
|
+
const DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
30
|
+
const MAX_INLINE_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
31
|
+
async function withTimeout(operation, timeoutMs, label, onTimeout) {
|
|
32
|
+
if (timeoutMs <= 0) {
|
|
33
|
+
return await operation;
|
|
34
|
+
}
|
|
35
|
+
let timeout;
|
|
36
|
+
try {
|
|
37
|
+
return await new Promise((resolve, reject) => {
|
|
38
|
+
timeout = setTimeout(() => {
|
|
39
|
+
onTimeout?.();
|
|
40
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
operation.then(resolve, reject);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
if (timeout) {
|
|
47
|
+
clearTimeout(timeout);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function runBestEffort(label, operation, onError, timeoutMs = DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT_MS) {
|
|
52
|
+
try {
|
|
53
|
+
await withTimeout(operation, timeoutMs, label);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
onError(error instanceof Error ? error.message : String(error));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function normalizeNonNegativeInteger(value, fallback) {
|
|
60
|
+
if (value === 0 || value === "0") {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
64
|
+
return Number.isFinite(numeric) && numeric > 0
|
|
65
|
+
? Math.floor(numeric)
|
|
66
|
+
: fallback;
|
|
67
|
+
}
|
|
68
|
+
function normalizePositiveInteger(value, fallback) {
|
|
69
|
+
const numeric = normalizeNonNegativeInteger(value, fallback);
|
|
70
|
+
return numeric > 0 ? numeric : fallback;
|
|
71
|
+
}
|
|
72
|
+
async function delay(ms) {
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
74
|
+
}
|
|
75
|
+
function isMcpPollingToolName(toolName) {
|
|
76
|
+
const normalized = toolName.trim().toLowerCase();
|
|
77
|
+
return (normalized === "poll_message" ||
|
|
78
|
+
normalized === "get_message" ||
|
|
79
|
+
normalized.includes("poll_message"));
|
|
80
|
+
}
|
|
81
|
+
function isBoolean(value) {
|
|
82
|
+
return typeof value === "boolean";
|
|
83
|
+
}
|
|
84
|
+
function modelSupportsImageInput(model, defaultProvider, providerOptions) {
|
|
85
|
+
if (isBoolean(providerOptions?.supportsImageInput)) {
|
|
86
|
+
return providerOptions.supportsImageInput;
|
|
87
|
+
}
|
|
88
|
+
const models = isRecord(defaultProvider) ? defaultProvider.models : undefined;
|
|
89
|
+
const modelConfig = isRecord(models) ? models[model] : undefined;
|
|
90
|
+
if (!isRecord(modelConfig)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const modalities = modelConfig.modalities;
|
|
94
|
+
if (!isRecord(modalities) || !Array.isArray(modalities.input)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return modalities.input.some((item) => String(item || "")
|
|
98
|
+
.trim()
|
|
99
|
+
.toLowerCase() === "image");
|
|
100
|
+
}
|
|
24
101
|
/**
|
|
25
102
|
* 解析 ACode 当前会话模型:优先显式启动参数,其次兼容 cc-switch 写入的
|
|
26
103
|
* OpenCode-style `provider.default.model`,最后回落到旧版 top-level model。
|
|
@@ -28,41 +105,263 @@ function normalizeText(value) {
|
|
|
28
105
|
function resolveProviderConfig(config, acodeConfig) {
|
|
29
106
|
const defaultProvider = acodeConfig.provider?.default;
|
|
30
107
|
const providerOptions = defaultProvider?.options;
|
|
108
|
+
const model = config.model ||
|
|
109
|
+
defaultProvider?.model ||
|
|
110
|
+
acodeConfig.model ||
|
|
111
|
+
"acode-default";
|
|
31
112
|
return {
|
|
32
|
-
model
|
|
113
|
+
model,
|
|
33
114
|
baseURL: normalizeText(providerOptions?.baseURL || providerOptions?.baseUrl),
|
|
34
115
|
apiKey: normalizeText(providerOptions?.apiKey),
|
|
35
116
|
systemPrompt: config.systemPrompt || acodeConfig.systemPrompt,
|
|
117
|
+
stream: providerOptions?.stream !== false,
|
|
118
|
+
requestTimeoutMs: normalizeNonNegativeInteger(providerOptions?.requestTimeoutMs ?? providerOptions?.timeoutMs, DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS),
|
|
119
|
+
requestRetryCount: normalizeNonNegativeInteger(providerOptions?.requestRetryCount, DEFAULT_PROVIDER_REQUEST_RETRY_COUNT),
|
|
120
|
+
mcpToolTimeoutMs: normalizePositiveInteger(providerOptions?.mcpToolTimeoutMs, DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS),
|
|
121
|
+
mcpPollingToolTimeoutMs: normalizePositiveInteger(providerOptions?.mcpPollingToolTimeoutMs, DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS),
|
|
122
|
+
supportsImageInput: modelSupportsImageInput(model, defaultProvider, providerOptions),
|
|
36
123
|
};
|
|
37
124
|
}
|
|
125
|
+
function appendTextPart(parts, text) {
|
|
126
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
127
|
+
if (normalized) {
|
|
128
|
+
parts.push({ type: "text", text: normalized });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function toDataImageUrl(mediaType, rawBase64) {
|
|
132
|
+
const trimmed = rawBase64.trim();
|
|
133
|
+
if (/^data:image\/[a-z0-9.+-]+;base64,/i.test(trimmed)) {
|
|
134
|
+
return trimmed;
|
|
135
|
+
}
|
|
136
|
+
return `data:${mediaType || "image/png"};base64,${trimmed}`;
|
|
137
|
+
}
|
|
138
|
+
function isDataImageUrl(url) {
|
|
139
|
+
return /^data:image\/[a-z0-9.+-]+;base64,/i.test(url.trim());
|
|
140
|
+
}
|
|
141
|
+
function assertFetchableImageUrl(url) {
|
|
142
|
+
let parsedUrl;
|
|
143
|
+
try {
|
|
144
|
+
parsedUrl = new URL(url);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
throw new Error(`ACode image URL is invalid: ${url}`);
|
|
148
|
+
}
|
|
149
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
150
|
+
throw new Error(`ACode image URL protocol is not supported: ${parsedUrl.protocol}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function responseToBase64(response) {
|
|
154
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
155
|
+
if (arrayBuffer.byteLength > MAX_INLINE_IMAGE_BYTES) {
|
|
156
|
+
throw new Error(`ACode image is too large to inline: ${arrayBuffer.byteLength} bytes exceeds ${MAX_INLINE_IMAGE_BYTES} bytes`);
|
|
157
|
+
}
|
|
158
|
+
return Buffer.from(arrayBuffer).toString("base64");
|
|
159
|
+
}
|
|
160
|
+
async function materializeImageUrl(url) {
|
|
161
|
+
const trimmedUrl = url.trim();
|
|
162
|
+
if (isDataImageUrl(trimmedUrl)) {
|
|
163
|
+
return trimmedUrl;
|
|
164
|
+
}
|
|
165
|
+
assertFetchableImageUrl(trimmedUrl);
|
|
166
|
+
const response = await fetch(trimmedUrl);
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error(`ACode image fetch failed: ${response.status} ${await response.text()}`);
|
|
169
|
+
}
|
|
170
|
+
const contentType = response.headers.get("content-type") || "";
|
|
171
|
+
const mediaType = contentType.split(";")[0]?.trim().toLowerCase() || "";
|
|
172
|
+
if (!mediaType.startsWith("image/")) {
|
|
173
|
+
throw new Error(`ACode image URL did not return an image content type: ${contentType || "unknown"}`);
|
|
174
|
+
}
|
|
175
|
+
return `data:${mediaType};base64,${await responseToBase64(response)}`;
|
|
176
|
+
}
|
|
177
|
+
function normalizeImageAttachmentUrl(attachment) {
|
|
178
|
+
const base64 = normalizeText(attachment.base64Data ?? attachment.base64 ?? attachment.data);
|
|
179
|
+
if (base64) {
|
|
180
|
+
return toDataImageUrl(normalizeText(attachment.mediaType ?? attachment.mimeType) || "image/png", base64);
|
|
181
|
+
}
|
|
182
|
+
return normalizeText(attachment.url ?? attachment.serverUrl);
|
|
183
|
+
}
|
|
184
|
+
function fileAttachmentText(name, url, mediaType, size) {
|
|
185
|
+
const details = [
|
|
186
|
+
name ? `name=${name}` : undefined,
|
|
187
|
+
url ? `url=${url}` : undefined,
|
|
188
|
+
mediaType ? `mediaType=${mediaType}` : undefined,
|
|
189
|
+
size ? `size=${size}` : undefined,
|
|
190
|
+
].filter((item) => Boolean(item));
|
|
191
|
+
return `[Attached file: ${details.join(", ")}]`;
|
|
192
|
+
}
|
|
193
|
+
function parseMarkerAttachmentMessage(message) {
|
|
194
|
+
const markerRegex = /\[(图片|文件)\s*:\s*([^\]\n]*?)\s*\]/gi;
|
|
195
|
+
const parts = [];
|
|
196
|
+
let hasImages = false;
|
|
197
|
+
let lastIndex = 0;
|
|
198
|
+
let match = markerRegex.exec(message);
|
|
199
|
+
while (match !== null) {
|
|
200
|
+
appendTextPart(parts, message.slice(lastIndex, match.index));
|
|
201
|
+
const markerType = match[1];
|
|
202
|
+
const markerValue = (match[2] || "").trim();
|
|
203
|
+
if (markerType === "图片" && markerValue) {
|
|
204
|
+
parts.push({ type: "image_url", image_url: { url: markerValue } });
|
|
205
|
+
hasImages = true;
|
|
206
|
+
}
|
|
207
|
+
else if (markerType === "文件" && markerValue) {
|
|
208
|
+
const [name = "", url = "", mediaType = "", size = ""] = markerValue
|
|
209
|
+
.split("|")
|
|
210
|
+
.map((item) => item.trim());
|
|
211
|
+
appendTextPart(parts, fileAttachmentText(name, url, mediaType, size));
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
appendTextPart(parts, match[0]);
|
|
215
|
+
}
|
|
216
|
+
lastIndex = match.index + match[0].length;
|
|
217
|
+
match = markerRegex.exec(message);
|
|
218
|
+
}
|
|
219
|
+
if (parts.length === 0) {
|
|
220
|
+
return { content: message, hasImages: false };
|
|
221
|
+
}
|
|
222
|
+
appendTextPart(parts, message.slice(lastIndex));
|
|
223
|
+
return { content: parts, hasImages };
|
|
224
|
+
}
|
|
225
|
+
function appendPolledMessagesContent(parts, parsed) {
|
|
226
|
+
let hasImages = false;
|
|
227
|
+
const messageGroups = [parsed.directMessages, parsed.groupMessages];
|
|
228
|
+
for (const group of messageGroups) {
|
|
229
|
+
if (!Array.isArray(group)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
for (const item of group) {
|
|
233
|
+
if (!isRecord(item)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const content = normalizeText(item.content);
|
|
237
|
+
if (!content) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const senderName = normalizeText(item.senderName);
|
|
241
|
+
appendTextPart(parts, senderName ? `Message from ${senderName}:` : "Message:");
|
|
242
|
+
const parsedContent = parseAttachmentMessage(content);
|
|
243
|
+
if (Array.isArray(parsedContent.content)) {
|
|
244
|
+
parts.push(...parsedContent.content);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
appendTextPart(parts, parsedContent.content);
|
|
248
|
+
}
|
|
249
|
+
hasImages = hasImages || parsedContent.hasImages;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return hasImages;
|
|
253
|
+
}
|
|
254
|
+
function parseJsonAttachmentMessage(message) {
|
|
255
|
+
let parsed;
|
|
256
|
+
try {
|
|
257
|
+
parsed = JSON.parse(message);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
if (!isRecord(parsed)) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
const parts = [];
|
|
266
|
+
let hasImages = appendPolledMessagesContent(parts, parsed);
|
|
267
|
+
const text = normalizeText(parsed.text) || "";
|
|
268
|
+
const rawAttachments = Array.isArray(parsed.attachments)
|
|
269
|
+
? parsed.attachments
|
|
270
|
+
: [];
|
|
271
|
+
appendTextPart(parts, text);
|
|
272
|
+
for (const item of rawAttachments) {
|
|
273
|
+
if (!isRecord(item)) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const attachment = item;
|
|
277
|
+
const kind = String(attachment.type ?? attachment.kind ?? "").toLowerCase();
|
|
278
|
+
if (kind === "image") {
|
|
279
|
+
const imageUrl = normalizeImageAttachmentUrl(attachment);
|
|
280
|
+
if (imageUrl) {
|
|
281
|
+
parts.push({ type: "image_url", image_url: { url: imageUrl } });
|
|
282
|
+
hasImages = true;
|
|
283
|
+
}
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const fileUrl = normalizeText(attachment.url ?? attachment.serverUrl) || "";
|
|
287
|
+
appendTextPart(parts, fileAttachmentText(normalizeText(attachment.name) || "", fileUrl, normalizeText(attachment.mediaType ?? attachment.mimeType) || "", normalizeText(attachment.size) || ""));
|
|
288
|
+
}
|
|
289
|
+
if (parts.length === 0) {
|
|
290
|
+
return { content: message, hasImages: false };
|
|
291
|
+
}
|
|
292
|
+
return { content: parts, hasImages };
|
|
293
|
+
}
|
|
294
|
+
function parseAttachmentMessage(message) {
|
|
295
|
+
return (parseJsonAttachmentMessage(message) ?? parseMarkerAttachmentMessage(message));
|
|
296
|
+
}
|
|
297
|
+
async function materializeImageContent(content) {
|
|
298
|
+
if (!Array.isArray(content)) {
|
|
299
|
+
return content;
|
|
300
|
+
}
|
|
301
|
+
const materializedParts = [];
|
|
302
|
+
for (const part of content) {
|
|
303
|
+
if (part.type === "text") {
|
|
304
|
+
materializedParts.push(part);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
materializedParts.push({
|
|
308
|
+
type: "image_url",
|
|
309
|
+
image_url: { url: await materializeImageUrl(part.image_url.url) },
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return materializedParts;
|
|
313
|
+
}
|
|
314
|
+
function contentPreview(content) {
|
|
315
|
+
if (typeof content === "string") {
|
|
316
|
+
return content;
|
|
317
|
+
}
|
|
318
|
+
if (!Array.isArray(content)) {
|
|
319
|
+
return "";
|
|
320
|
+
}
|
|
321
|
+
return content
|
|
322
|
+
.map((part) => (part.type === "text" ? part.text : "[Image]"))
|
|
323
|
+
.join(" ")
|
|
324
|
+
.trim();
|
|
325
|
+
}
|
|
38
326
|
/**
|
|
39
327
|
* 解析 ACode 工具权限配置。
|
|
40
328
|
* 主流程:运行时配置优先 -> `.acode/config.json` 次之 -> 兼容旧式 allowedTools/deniedTools 字段。
|
|
41
329
|
*/
|
|
42
330
|
function resolvePermissionConfig(config, acodeConfig) {
|
|
43
331
|
const configured = config.permissions ?? acodeConfig.permissions ?? {};
|
|
44
|
-
const legacyAllow = config.permissions
|
|
45
|
-
|
|
332
|
+
const legacyAllow = config.permissions
|
|
333
|
+
? []
|
|
334
|
+
: (acodeConfig.allowedTools ?? []);
|
|
335
|
+
const legacyDeny = config.permissions ? [] : (acodeConfig.deniedTools ?? []);
|
|
46
336
|
return {
|
|
47
|
-
mode: configured.mode ??
|
|
48
|
-
allow: [...(configured.allow ?? []), ...legacyAllow]
|
|
49
|
-
|
|
337
|
+
mode: configured.mode ?? "default",
|
|
338
|
+
allow: [...(configured.allow ?? []), ...legacyAllow]
|
|
339
|
+
.map((rule) => rule.trim())
|
|
340
|
+
.filter(Boolean),
|
|
341
|
+
deny: [...(configured.deny ?? []), ...legacyDeny]
|
|
342
|
+
.map((rule) => rule.trim())
|
|
343
|
+
.filter(Boolean),
|
|
50
344
|
};
|
|
51
345
|
}
|
|
52
346
|
function wildcardMatches(pattern, value) {
|
|
53
|
-
if (pattern ===
|
|
347
|
+
if (pattern === "*") {
|
|
54
348
|
return true;
|
|
55
349
|
}
|
|
56
|
-
if (pattern.endsWith(
|
|
350
|
+
if (pattern.endsWith("*")) {
|
|
57
351
|
return value.startsWith(pattern.slice(0, -1));
|
|
58
352
|
}
|
|
59
353
|
return pattern === value;
|
|
60
354
|
}
|
|
61
355
|
function toolPermissionNames(tool) {
|
|
62
|
-
if (tool.kind ===
|
|
356
|
+
if (tool.kind === "builtin") {
|
|
63
357
|
return [tool.exposedName, tool.toolName, `builtin.${tool.toolName}`];
|
|
64
358
|
}
|
|
65
|
-
return [
|
|
359
|
+
return [
|
|
360
|
+
tool.exposedName,
|
|
361
|
+
tool.toolName,
|
|
362
|
+
`${tool.serverName}.${tool.toolName}`,
|
|
363
|
+
`mcp.${tool.serverName}.${tool.toolName}`,
|
|
364
|
+
];
|
|
66
365
|
}
|
|
67
366
|
function matchesToolRule(rule, tool) {
|
|
68
367
|
return toolPermissionNames(tool).some((name) => wildcardMatches(rule, name));
|
|
@@ -73,7 +372,7 @@ function assertToolAllowedByRules(tool, rules, message) {
|
|
|
73
372
|
}
|
|
74
373
|
}
|
|
75
374
|
function assertToolPermission(tool, permissions, commandAllowedTools) {
|
|
76
|
-
if (permissions.mode ===
|
|
375
|
+
if (permissions.mode === "bypassPermissions") {
|
|
77
376
|
return;
|
|
78
377
|
}
|
|
79
378
|
if (permissions.deny.some((rule) => matchesToolRule(rule, tool))) {
|
|
@@ -92,12 +391,33 @@ function isToolAllowed(tool, permissions, commandAllowedTools) {
|
|
|
92
391
|
}
|
|
93
392
|
}
|
|
94
393
|
function normalizeChatCompletionsUrl(baseURL) {
|
|
95
|
-
const trimmed = baseURL.replace(/\/+$/g,
|
|
394
|
+
const trimmed = baseURL.replace(/\/+$/g, "");
|
|
96
395
|
if (/\/chat\/completions$/i.test(trimmed)) {
|
|
97
396
|
return trimmed;
|
|
98
397
|
}
|
|
99
398
|
return `${trimmed}/chat/completions`;
|
|
100
399
|
}
|
|
400
|
+
function summarizeProviderUrl(baseURL) {
|
|
401
|
+
try {
|
|
402
|
+
const url = new URL(baseURL);
|
|
403
|
+
url.search = "";
|
|
404
|
+
url.hash = "";
|
|
405
|
+
const normalizedUrl = new URL(normalizeChatCompletionsUrl(url.toString()));
|
|
406
|
+
return `${normalizedUrl.origin}${normalizedUrl.pathname}`;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return normalizeChatCompletionsUrl(baseURL).replace(/([?&](?:api[_-]?key|token|authorization)=)[^&]+/gi, "$1[redacted]");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function summarizeLastUserMessage(messages) {
|
|
413
|
+
const lastUserMessage = [...messages]
|
|
414
|
+
.reverse()
|
|
415
|
+
.find((message) => message.role === "user");
|
|
416
|
+
const content = contentPreview(lastUserMessage?.content ?? null).trim();
|
|
417
|
+
if (!content)
|
|
418
|
+
return undefined;
|
|
419
|
+
return content.length > 300 ? `${content.slice(0, 300)}...` : content;
|
|
420
|
+
}
|
|
101
421
|
function coerceArguments(rawArguments) {
|
|
102
422
|
try {
|
|
103
423
|
const parsed = JSON.parse(rawArguments);
|
|
@@ -107,56 +427,132 @@ function coerceArguments(rawArguments) {
|
|
|
107
427
|
return {};
|
|
108
428
|
}
|
|
109
429
|
}
|
|
430
|
+
function getBuiltInToolTimeoutMs(tool, args, fallbackTimeoutMs) {
|
|
431
|
+
if (tool.toolName !== "Bash") {
|
|
432
|
+
return fallbackTimeoutMs;
|
|
433
|
+
}
|
|
434
|
+
const rawTimeout = args.timeout_ms ?? args.timeoutMs;
|
|
435
|
+
const numericTimeout = typeof rawTimeout === "number"
|
|
436
|
+
? rawTimeout
|
|
437
|
+
: typeof rawTimeout === "string"
|
|
438
|
+
? Number(rawTimeout)
|
|
439
|
+
: NaN;
|
|
440
|
+
if (!Number.isFinite(numericTimeout) || numericTimeout <= 0) {
|
|
441
|
+
return fallbackTimeoutMs;
|
|
442
|
+
}
|
|
443
|
+
return Math.min(Math.floor(numericTimeout), 10 * 60_000) + 2000;
|
|
444
|
+
}
|
|
110
445
|
function createFallbackResponse(message, state) {
|
|
111
446
|
const trimmed = message.trim();
|
|
112
|
-
return `ACode 已接收任务。当前已加载 ${state.configMcpCount} 个 MCP 配置、${state.exposedTools.length} 个 MCP Tool 与 ${state.skills.length} 个 Skill。${trimmed ? `输入摘要:${trimmed.slice(0, 200)}` :
|
|
447
|
+
return `ACode 已接收任务。当前已加载 ${state.configMcpCount} 个 MCP 配置、${state.exposedTools.length} 个 MCP Tool 与 ${state.skills.length} 个 Skill。${trimmed ? `输入摘要:${trimmed.slice(0, 200)}` : ""}`;
|
|
113
448
|
}
|
|
114
449
|
function toToolResultText(result) {
|
|
115
450
|
if (!isRecord(result)) {
|
|
116
|
-
return String(result ??
|
|
451
|
+
return String(result ?? "");
|
|
117
452
|
}
|
|
118
453
|
const content = result.content;
|
|
119
454
|
if (Array.isArray(content)) {
|
|
120
455
|
const parts = content
|
|
121
456
|
.map((item) => {
|
|
122
457
|
if (!isRecord(item)) {
|
|
123
|
-
return
|
|
458
|
+
return "";
|
|
124
459
|
}
|
|
125
|
-
if (typeof item.text ===
|
|
460
|
+
if (typeof item.text === "string") {
|
|
126
461
|
return item.text;
|
|
127
462
|
}
|
|
128
463
|
return JSON.stringify(item);
|
|
129
464
|
})
|
|
130
465
|
.filter(Boolean);
|
|
131
466
|
if (parts.length > 0) {
|
|
132
|
-
return parts.join(
|
|
467
|
+
return parts.join("\n");
|
|
133
468
|
}
|
|
134
469
|
}
|
|
135
470
|
return JSON.stringify(result);
|
|
136
471
|
}
|
|
472
|
+
function getErrorMessage(error) {
|
|
473
|
+
return error instanceof Error ? error.message : String(error);
|
|
474
|
+
}
|
|
475
|
+
function isRetryableProviderRequestError(error) {
|
|
476
|
+
const message = getErrorMessage(error).toLowerCase();
|
|
477
|
+
return (message.includes("fetch failed") ||
|
|
478
|
+
message.includes("timed out") ||
|
|
479
|
+
message.includes("econnreset") ||
|
|
480
|
+
message.includes("econnrefused") ||
|
|
481
|
+
message.includes("etimedout") ||
|
|
482
|
+
message.includes("enotfound") ||
|
|
483
|
+
message.includes("502") ||
|
|
484
|
+
message.includes("503") ||
|
|
485
|
+
message.includes("504"));
|
|
486
|
+
}
|
|
487
|
+
function getErrorType(error) {
|
|
488
|
+
if (typeof error === "object" && error !== null) {
|
|
489
|
+
const code = Reflect.get(error, "code");
|
|
490
|
+
if (typeof code === "string" && code) {
|
|
491
|
+
return code;
|
|
492
|
+
}
|
|
493
|
+
const name = Reflect.get(error, "name");
|
|
494
|
+
if (typeof name === "string" && name) {
|
|
495
|
+
return name;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return "tool_error";
|
|
499
|
+
}
|
|
500
|
+
function toToolErrorResultText(tool, error) {
|
|
501
|
+
return JSON.stringify({
|
|
502
|
+
ok: false,
|
|
503
|
+
tool: tool.exposedName,
|
|
504
|
+
error: {
|
|
505
|
+
type: getErrorType(error),
|
|
506
|
+
message: getErrorMessage(error),
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
function summarizeToolArguments(toolArguments) {
|
|
511
|
+
const serialized = JSON.stringify(toolArguments);
|
|
512
|
+
return serialized.length > 500 ? `${serialized.slice(0, 500)}…` : serialized;
|
|
513
|
+
}
|
|
514
|
+
function extractReasoningText(delta) {
|
|
515
|
+
const candidates = [
|
|
516
|
+
delta?.reasoning_content,
|
|
517
|
+
delta?.reasoning_text,
|
|
518
|
+
delta?.reasoning,
|
|
519
|
+
];
|
|
520
|
+
for (const candidate of candidates) {
|
|
521
|
+
if (typeof candidate === "string" && candidate) {
|
|
522
|
+
return candidate;
|
|
523
|
+
}
|
|
524
|
+
if (isRecord(candidate)) {
|
|
525
|
+
const text = candidate.text ?? candidate.content;
|
|
526
|
+
if (typeof text === "string" && text) {
|
|
527
|
+
return text;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return "";
|
|
532
|
+
}
|
|
137
533
|
function buildSkillPrompt(skills) {
|
|
138
534
|
if (skills.length === 0) {
|
|
139
|
-
return
|
|
535
|
+
return "";
|
|
140
536
|
}
|
|
141
537
|
return [
|
|
142
|
-
|
|
143
|
-
...skills.map((skill) => `- ${skill.name}${skill.description ? `: ${skill.description}` :
|
|
144
|
-
|
|
145
|
-
].join(
|
|
538
|
+
"Available skills:",
|
|
539
|
+
...skills.map((skill) => `- ${skill.name}${skill.description ? `: ${skill.description}` : ""}`),
|
|
540
|
+
"Use these skills as guidance when they match the user task.",
|
|
541
|
+
].join("\n");
|
|
146
542
|
}
|
|
147
543
|
function buildCommandPrompt(commands) {
|
|
148
544
|
if (commands.length === 0) {
|
|
149
|
-
return
|
|
545
|
+
return "";
|
|
150
546
|
}
|
|
151
547
|
return [
|
|
152
|
-
|
|
153
|
-
...commands.map((command) => `- /${command.name}${command.description ? `: ${command.description}` :
|
|
154
|
-
|
|
155
|
-
].join(
|
|
548
|
+
"Available slash commands:",
|
|
549
|
+
...commands.map((command) => `- /${command.name}${command.description ? `: ${command.description}` : ""}`),
|
|
550
|
+
"When the user invokes a slash command, ACode expands it before model execution.",
|
|
551
|
+
].join("\n");
|
|
156
552
|
}
|
|
157
553
|
function toOpenAiTools(tools) {
|
|
158
554
|
return tools.map((tool) => ({
|
|
159
|
-
type:
|
|
555
|
+
type: "function",
|
|
160
556
|
function: {
|
|
161
557
|
name: tool.exposedName,
|
|
162
558
|
description: tool.description || tool.toolName,
|
|
@@ -165,16 +561,49 @@ function toOpenAiTools(tools) {
|
|
|
165
561
|
}));
|
|
166
562
|
}
|
|
167
563
|
function normalizeToolName(serverName, toolName) {
|
|
168
|
-
const normalized = `${serverName}__${toolName}`.replace(/[^a-zA-Z0-9_-]/g,
|
|
169
|
-
return normalized.slice(0, 64) ||
|
|
564
|
+
const normalized = `${serverName}__${toolName}`.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
565
|
+
return normalized.slice(0, 64) || "mcp_tool";
|
|
170
566
|
}
|
|
171
567
|
function buildRequestHeaders(provider) {
|
|
172
|
-
const headers = {
|
|
568
|
+
const headers = {
|
|
569
|
+
"Content-Type": "application/json",
|
|
570
|
+
};
|
|
173
571
|
if (provider.apiKey) {
|
|
174
572
|
headers.Authorization = `Bearer ${provider.apiKey}`;
|
|
175
573
|
}
|
|
176
574
|
return headers;
|
|
177
575
|
}
|
|
576
|
+
function parseSseDataLines(chunk) {
|
|
577
|
+
return chunk
|
|
578
|
+
.split(/\r?\n/)
|
|
579
|
+
.filter((line) => line.startsWith("data:"))
|
|
580
|
+
.map((line) => line.slice("data:".length).trim())
|
|
581
|
+
.filter(Boolean);
|
|
582
|
+
}
|
|
583
|
+
function updateToolCallAccumulator(accumulators, delta) {
|
|
584
|
+
const index = Number.isFinite(delta.index) ? Number(delta.index) : 0;
|
|
585
|
+
const current = accumulators.get(index) ?? {
|
|
586
|
+
id: delta.id || `call-${index}`,
|
|
587
|
+
type: "function",
|
|
588
|
+
function: { name: "", arguments: "" },
|
|
589
|
+
};
|
|
590
|
+
if (delta.id) {
|
|
591
|
+
current.id = delta.id;
|
|
592
|
+
}
|
|
593
|
+
if (delta.function?.name) {
|
|
594
|
+
current.function.name += delta.function.name;
|
|
595
|
+
}
|
|
596
|
+
if (delta.function?.arguments) {
|
|
597
|
+
current.function.arguments += delta.function.arguments;
|
|
598
|
+
}
|
|
599
|
+
accumulators.set(index, current);
|
|
600
|
+
}
|
|
601
|
+
function toAccumulatedToolCalls(accumulators) {
|
|
602
|
+
return [...accumulators.entries()]
|
|
603
|
+
.sort(([left], [right]) => left - right)
|
|
604
|
+
.map(([, value]) => value)
|
|
605
|
+
.filter((toolCall) => toolCall.function.name.trim().length > 0);
|
|
606
|
+
}
|
|
178
607
|
export class ACodeRuntime extends EventEmitter {
|
|
179
608
|
constructor(config) {
|
|
180
609
|
super();
|
|
@@ -184,8 +613,16 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
184
613
|
terminated: false,
|
|
185
614
|
configMcpCount: 0,
|
|
186
615
|
skills: [],
|
|
187
|
-
provider: {
|
|
188
|
-
|
|
616
|
+
provider: {
|
|
617
|
+
model: "acode-default",
|
|
618
|
+
stream: false,
|
|
619
|
+
requestTimeoutMs: DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS,
|
|
620
|
+
requestRetryCount: DEFAULT_PROVIDER_REQUEST_RETRY_COUNT,
|
|
621
|
+
mcpToolTimeoutMs: DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS,
|
|
622
|
+
mcpPollingToolTimeoutMs: DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS,
|
|
623
|
+
supportsImageInput: false,
|
|
624
|
+
},
|
|
625
|
+
permissions: { mode: "default", allow: [], deny: [] },
|
|
189
626
|
mcpConnections: [],
|
|
190
627
|
exposedTools: [],
|
|
191
628
|
commands: [],
|
|
@@ -193,10 +630,14 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
193
630
|
conversation: [],
|
|
194
631
|
};
|
|
195
632
|
this.abortController = null;
|
|
633
|
+
this.submitQueue = Promise.resolve();
|
|
634
|
+
this.activeTurn = false;
|
|
635
|
+
this.abortRequested = false;
|
|
636
|
+
this.sessionCompleteEmitted = false;
|
|
196
637
|
this.hookBus = new ACodeHookBus({
|
|
197
638
|
sessionId: config.sessionId,
|
|
198
639
|
onError: (event, error) => {
|
|
199
|
-
this.emitAcodeEvent(
|
|
640
|
+
this.emitAcodeEvent("error", {
|
|
200
641
|
message: `ACode hook failed: ${event.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
201
642
|
});
|
|
202
643
|
},
|
|
@@ -212,7 +653,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
212
653
|
timestamp: nowIso(),
|
|
213
654
|
data,
|
|
214
655
|
};
|
|
215
|
-
this.emit(
|
|
656
|
+
this.emit("event", event);
|
|
216
657
|
}
|
|
217
658
|
/**
|
|
218
659
|
* 启动 ACode headless 会话。
|
|
@@ -224,9 +665,12 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
224
665
|
const skills = await loadAcodeSkills(this.config.skillsDir);
|
|
225
666
|
const provider = resolveProviderConfig(this.config, acodeConfig);
|
|
226
667
|
const permissions = resolvePermissionConfig(this.config, acodeConfig);
|
|
227
|
-
const mcpConnections = await this.connectMcpServers(acodeConfig.mcpServers ?? {});
|
|
668
|
+
const mcpConnections = await this.connectMcpServers(acodeConfig.mcpServers ?? {}, provider.mcpToolTimeoutMs);
|
|
228
669
|
const builtInTools = createBuiltInFileTools(this.config.workingDirectory);
|
|
229
|
-
const exposedTools = [
|
|
670
|
+
const exposedTools = [
|
|
671
|
+
...builtInTools,
|
|
672
|
+
...mcpConnections.flatMap((connection) => connection.tools),
|
|
673
|
+
];
|
|
230
674
|
const commandDirs = this.resolveCommandDirs(acodeConfig);
|
|
231
675
|
const commands = await loadAcodeCommands(commandDirs);
|
|
232
676
|
const commandRegistry = new ACodeCommandRegistry(commands);
|
|
@@ -235,7 +679,7 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
235
679
|
const systemPrompt = [provider.systemPrompt, skillPrompt, commandPrompt]
|
|
236
680
|
.map((item) => item?.trim())
|
|
237
681
|
.filter((item) => Boolean(item))
|
|
238
|
-
.join(
|
|
682
|
+
.join("\n\n");
|
|
239
683
|
this.state = {
|
|
240
684
|
started: true,
|
|
241
685
|
terminated: false,
|
|
@@ -247,19 +691,21 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
247
691
|
exposedTools,
|
|
248
692
|
commands,
|
|
249
693
|
commandRegistry,
|
|
250
|
-
conversation: systemPrompt
|
|
694
|
+
conversation: systemPrompt
|
|
695
|
+
? [{ role: "system", content: systemPrompt }]
|
|
696
|
+
: [],
|
|
251
697
|
};
|
|
252
|
-
await this.hookBus.dispatch(
|
|
698
|
+
await this.hookBus.dispatch("SessionStart", {
|
|
253
699
|
workingDirectory: this.config.workingDirectory,
|
|
254
700
|
model: provider.model,
|
|
255
701
|
commandCount: commands.length,
|
|
256
702
|
});
|
|
257
|
-
this.emitAcodeEvent(
|
|
703
|
+
this.emitAcodeEvent("session_started", {
|
|
258
704
|
workingDirectory: this.config.workingDirectory,
|
|
259
705
|
model: provider.model,
|
|
260
706
|
commandCount: commands.length,
|
|
261
707
|
});
|
|
262
|
-
this.emitAcodeEvent(
|
|
708
|
+
this.emitAcodeEvent("mcp_status", {
|
|
263
709
|
configPath: this.config.configPath,
|
|
264
710
|
installedCount: this.state.configMcpCount,
|
|
265
711
|
servers: Object.keys(acodeConfig.mcpServers ?? {}),
|
|
@@ -271,19 +717,27 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
271
717
|
deniedToolRules: permissions.deny,
|
|
272
718
|
tools: exposedTools.map((tool) => ({
|
|
273
719
|
name: tool.exposedName,
|
|
274
|
-
source: tool.kind ===
|
|
720
|
+
source: tool.kind === "mcp"
|
|
721
|
+
? `${tool.serverName}.${tool.toolName}`
|
|
722
|
+
: `builtin.${tool.toolName}`,
|
|
275
723
|
})),
|
|
276
724
|
});
|
|
277
|
-
this.emitAcodeEvent(
|
|
725
|
+
this.emitAcodeEvent("skill_status", {
|
|
278
726
|
skillsDir: this.config.skillsDir,
|
|
279
727
|
installedCount: skills.length,
|
|
280
|
-
skills: skills.map((skill) => ({
|
|
728
|
+
skills: skills.map((skill) => ({
|
|
729
|
+
name: skill.name,
|
|
730
|
+
description: skill.description,
|
|
731
|
+
})),
|
|
281
732
|
});
|
|
282
|
-
this.emitAcodeEvent(
|
|
283
|
-
action:
|
|
733
|
+
this.emitAcodeEvent("command_status", {
|
|
734
|
+
action: "commands_loaded",
|
|
284
735
|
installedCommandCount: commands.length,
|
|
285
736
|
commandDirs,
|
|
286
|
-
commands: commands.map((command) => ({
|
|
737
|
+
commands: commands.map((command) => ({
|
|
738
|
+
name: command.name,
|
|
739
|
+
description: command.description,
|
|
740
|
+
})),
|
|
287
741
|
});
|
|
288
742
|
if (this.config.initialPrompt) {
|
|
289
743
|
await this.submit(this.config.initialPrompt);
|
|
@@ -295,68 +749,107 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
295
749
|
* MCP tool call 则执行工具并继续同一轮,最后输出 assistant 文本与 usage。
|
|
296
750
|
*/
|
|
297
751
|
async submit(message) {
|
|
752
|
+
const queued = this.submitQueue.then(() => this.executeSubmit(message));
|
|
753
|
+
this.submitQueue = queued.catch(() => undefined);
|
|
754
|
+
return queued;
|
|
755
|
+
}
|
|
756
|
+
async executeSubmit(message) {
|
|
298
757
|
if (!this.state.started || this.state.terminated) {
|
|
299
|
-
throw new Error(
|
|
758
|
+
throw new Error("ACode runtime is not active");
|
|
300
759
|
}
|
|
760
|
+
this.activeTurn = true;
|
|
761
|
+
this.abortRequested = false;
|
|
301
762
|
const originalPrompt = message.trim();
|
|
302
|
-
const promptHookResults = await this.hookBus.dispatch('UserPromptSubmit', { prompt: originalPrompt });
|
|
303
|
-
const promptDeny = findDeniedHookResult(promptHookResults);
|
|
304
|
-
if (promptDeny) {
|
|
305
|
-
this.emitAcodeEvent('error', { message: promptDeny.message || 'ACode prompt was denied by hook' });
|
|
306
|
-
this.emitAcodeEvent('turn_complete', { failed: true });
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
const expandedCommand = this.state.commandRegistry.expand(originalPrompt);
|
|
310
|
-
const trimmed = expandedCommand?.prompt.trim() || originalPrompt;
|
|
311
|
-
const commandAllowedTools = expandedCommand?.command.allowedTools;
|
|
312
|
-
if (expandedCommand) {
|
|
313
|
-
this.emitAcodeEvent('command_status', {
|
|
314
|
-
action: 'slash_command',
|
|
315
|
-
command: expandedCommand.command.name,
|
|
316
|
-
arguments: expandedCommand.argumentsText,
|
|
317
|
-
sourcePath: expandedCommand.command.sourcePath,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
if (!this.state.provider.baseURL || !this.state.provider.apiKey) {
|
|
321
|
-
const text = createFallbackResponse(trimmed, this.state);
|
|
322
|
-
this.emitAcodeEvent('assistant_delta', { text });
|
|
323
|
-
this.emitAcodeEvent('turn_complete', { usage: { inputTokens: trimmed.length, outputTokens: text.length } });
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
this.state.conversation.push({ role: 'user', content: trimmed });
|
|
327
763
|
try {
|
|
764
|
+
const promptHookResults = await this.hookBus.dispatch("UserPromptSubmit", { prompt: originalPrompt });
|
|
765
|
+
const promptDeny = findDeniedHookResult(promptHookResults);
|
|
766
|
+
if (promptDeny) {
|
|
767
|
+
this.emitAcodeEvent("error", {
|
|
768
|
+
message: promptDeny.message || "ACode prompt was denied by hook",
|
|
769
|
+
});
|
|
770
|
+
this.emitAcodeEvent("turn_complete", { failed: true });
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const expandedCommand = this.state.commandRegistry.expand(originalPrompt);
|
|
774
|
+
const trimmed = expandedCommand?.prompt.trim() || originalPrompt;
|
|
775
|
+
const commandAllowedTools = expandedCommand?.command.allowedTools;
|
|
776
|
+
if (expandedCommand) {
|
|
777
|
+
this.emitAcodeEvent("command_status", {
|
|
778
|
+
action: "slash_command",
|
|
779
|
+
command: expandedCommand.command.name,
|
|
780
|
+
arguments: expandedCommand.argumentsText,
|
|
781
|
+
sourcePath: expandedCommand.command.sourcePath,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
if (!this.state.provider.baseURL || !this.state.provider.apiKey) {
|
|
785
|
+
const text = createFallbackResponse(trimmed, this.state);
|
|
786
|
+
this.emitAcodeEvent("assistant_delta", { text });
|
|
787
|
+
this.emitAcodeEvent("turn_complete", {
|
|
788
|
+
usage: { inputTokens: trimmed.length, outputTokens: text.length },
|
|
789
|
+
});
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const parsedContent = parseAttachmentMessage(trimmed);
|
|
793
|
+
if (parsedContent.hasImages && !this.state.provider.supportsImageInput) {
|
|
794
|
+
throw new Error("ACode provider/model is not configured for image input. Enable provider.default.models[model].modalities.input including 'image' or options.supportsImageInput=true.");
|
|
795
|
+
}
|
|
796
|
+
const userContent = await materializeImageContent(parsedContent.content);
|
|
797
|
+
this.state.conversation.push({ role: "user", content: userContent });
|
|
328
798
|
const result = await this.runModelTurn(commandAllowedTools);
|
|
329
|
-
if (result.text) {
|
|
330
|
-
this.emitAcodeEvent(
|
|
799
|
+
if (result.text && !this.state.provider.stream) {
|
|
800
|
+
this.emitAcodeEvent("assistant_delta", { text: result.text });
|
|
331
801
|
}
|
|
332
|
-
this.emitAcodeEvent(
|
|
333
|
-
usage: result.usage ?? {
|
|
802
|
+
this.emitAcodeEvent("turn_complete", {
|
|
803
|
+
usage: result.usage ?? {
|
|
804
|
+
inputTokens: trimmed.length,
|
|
805
|
+
outputTokens: result.text.length,
|
|
806
|
+
},
|
|
807
|
+
...(result.responseBody ? { responseBody: result.responseBody } : {}),
|
|
808
|
+
...(result.finishReason ? { finishReason: result.finishReason } : {}),
|
|
334
809
|
});
|
|
335
810
|
}
|
|
336
811
|
catch (error) {
|
|
337
|
-
this.
|
|
812
|
+
if (this.abortRequested) {
|
|
813
|
+
this.emitAcodeEvent("turn_complete", { aborted: true });
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
this.emitAcodeEvent("error", {
|
|
338
817
|
message: error instanceof Error ? error.message : String(error),
|
|
339
818
|
});
|
|
340
|
-
this.emitAcodeEvent(
|
|
819
|
+
this.emitAcodeEvent("turn_complete", { failed: true });
|
|
820
|
+
}
|
|
821
|
+
finally {
|
|
822
|
+
this.activeTurn = false;
|
|
823
|
+
this.abortRequested = false;
|
|
341
824
|
}
|
|
342
825
|
}
|
|
343
|
-
/**
|
|
826
|
+
/** 中止当前轮次;由执行轮次统一发出一个终态事件,避免 aborted/failed 双终态。 */
|
|
344
827
|
async abort() {
|
|
828
|
+
this.abortRequested = this.activeTurn;
|
|
345
829
|
this.abortController?.abort();
|
|
346
830
|
this.abortController = null;
|
|
347
|
-
if (!this.state.terminated) {
|
|
348
|
-
this.emitAcodeEvent('turn_complete', { aborted: true });
|
|
349
|
-
}
|
|
350
831
|
}
|
|
351
832
|
/** 终止 ACode 会话并发出 session_complete。 */
|
|
352
833
|
async stop() {
|
|
353
834
|
if (this.state.terminated) {
|
|
354
835
|
return;
|
|
355
836
|
}
|
|
837
|
+
this.state.terminated = true;
|
|
838
|
+
this.abortRequested = this.activeTurn;
|
|
839
|
+
this.abortController?.abort();
|
|
840
|
+
this.abortController = null;
|
|
841
|
+
await runBestEffort("ACode active turn shutdown", this.submitQueue, (message) => {
|
|
842
|
+
this.emitAcodeEvent("error", { message });
|
|
843
|
+
});
|
|
356
844
|
await this.disconnectMcpServers();
|
|
357
|
-
await this.hookBus.dispatch(
|
|
845
|
+
await runBestEffort("ACode Stop hooks", this.hookBus.dispatch("Stop", { reason: "session_stop" }), (message) => {
|
|
846
|
+
this.emitAcodeEvent("error", { message });
|
|
847
|
+
});
|
|
358
848
|
this.state.terminated = true;
|
|
359
|
-
this.
|
|
849
|
+
if (!this.sessionCompleteEmitted) {
|
|
850
|
+
this.sessionCompleteEmitted = true;
|
|
851
|
+
this.emitAcodeEvent("session_complete", { exitCode: 0 });
|
|
852
|
+
}
|
|
360
853
|
}
|
|
361
854
|
resolveCommandDirs(acodeConfig) {
|
|
362
855
|
const configured = [
|
|
@@ -364,23 +857,39 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
364
857
|
...(acodeConfig.commandDirs ?? []),
|
|
365
858
|
...(acodeConfig.commands?.directories ?? []),
|
|
366
859
|
];
|
|
367
|
-
return [
|
|
860
|
+
return [
|
|
861
|
+
...new Set(configured.map((directory) => directory.trim()).filter(Boolean)),
|
|
862
|
+
];
|
|
368
863
|
}
|
|
369
864
|
async runModelTurn(commandAllowedTools) {
|
|
370
|
-
let finalText =
|
|
865
|
+
let finalText = "";
|
|
371
866
|
let finalUsage;
|
|
867
|
+
let finalResponseBody;
|
|
868
|
+
let finalFinishReason;
|
|
372
869
|
for (let step = 0; step < 5; step += 1) {
|
|
373
870
|
const response = await this.requestChatCompletion(commandAllowedTools);
|
|
871
|
+
if (this.state.terminated) {
|
|
872
|
+
throw new Error("ACode runtime was stopped during the active turn");
|
|
873
|
+
}
|
|
374
874
|
const choice = response.choices?.[0];
|
|
875
|
+
finalResponseBody = JSON.stringify(response, null, 2);
|
|
876
|
+
finalFinishReason =
|
|
877
|
+
typeof choice?.finish_reason === "string"
|
|
878
|
+
? choice.finish_reason
|
|
879
|
+
: undefined;
|
|
375
880
|
const message = choice?.message;
|
|
376
|
-
const content = typeof message?.content ===
|
|
377
|
-
const toolCalls = Array.isArray(message?.tool_calls)
|
|
881
|
+
const content = typeof message?.content === "string" ? message.content : "";
|
|
882
|
+
const toolCalls = Array.isArray(message?.tool_calls)
|
|
883
|
+
? message.tool_calls
|
|
884
|
+
: [];
|
|
378
885
|
finalUsage = {
|
|
379
886
|
inputTokens: response.usage?.prompt_tokens ?? response.usage?.input_tokens ?? 0,
|
|
380
|
-
outputTokens: response.usage?.completion_tokens ??
|
|
887
|
+
outputTokens: response.usage?.completion_tokens ??
|
|
888
|
+
response.usage?.output_tokens ??
|
|
889
|
+
0,
|
|
381
890
|
};
|
|
382
891
|
this.state.conversation.push({
|
|
383
|
-
role:
|
|
892
|
+
role: "assistant",
|
|
384
893
|
content: content || null,
|
|
385
894
|
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
386
895
|
});
|
|
@@ -388,57 +897,241 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
388
897
|
finalText = content;
|
|
389
898
|
}
|
|
390
899
|
if (toolCalls.length === 0) {
|
|
391
|
-
return {
|
|
900
|
+
return {
|
|
901
|
+
text: finalText,
|
|
902
|
+
usage: finalUsage,
|
|
903
|
+
responseBody: finalResponseBody,
|
|
904
|
+
finishReason: finalFinishReason,
|
|
905
|
+
};
|
|
392
906
|
}
|
|
393
907
|
await this.handleToolCalls(toolCalls, commandAllowedTools);
|
|
394
908
|
}
|
|
395
|
-
return {
|
|
909
|
+
return {
|
|
910
|
+
text: finalText || "ACode 已达到本轮 MCP 工具调用上限。",
|
|
911
|
+
usage: finalUsage,
|
|
912
|
+
responseBody: finalResponseBody,
|
|
913
|
+
finishReason: finalFinishReason,
|
|
914
|
+
};
|
|
396
915
|
}
|
|
397
916
|
async requestChatCompletion(commandAllowedTools) {
|
|
398
917
|
const provider = this.state.provider;
|
|
399
|
-
|
|
400
|
-
|
|
918
|
+
const baseURL = provider.baseURL;
|
|
919
|
+
if (!baseURL) {
|
|
920
|
+
throw new Error("ACode provider baseURL is not configured");
|
|
401
921
|
}
|
|
402
|
-
this.abortController = new AbortController();
|
|
403
922
|
const body = {
|
|
404
923
|
model: provider.model,
|
|
405
924
|
messages: this.state.conversation,
|
|
406
|
-
stream:
|
|
925
|
+
stream: provider.stream,
|
|
407
926
|
};
|
|
408
927
|
const allowedTools = this.state.exposedTools.filter((tool) => isToolAllowed(tool, this.state.permissions, commandAllowedTools));
|
|
409
928
|
if (allowedTools.length > 0) {
|
|
410
929
|
body.tools = toOpenAiTools(allowedTools);
|
|
411
|
-
body.tool_choice =
|
|
930
|
+
body.tool_choice = "auto";
|
|
412
931
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
932
|
+
this.emitAcodeEvent("ai_request", {
|
|
933
|
+
model: provider.model,
|
|
934
|
+
endpoint: summarizeProviderUrl(baseURL),
|
|
935
|
+
stream: provider.stream,
|
|
936
|
+
messageCount: this.state.conversation.length,
|
|
937
|
+
toolCount: allowedTools.length,
|
|
938
|
+
lastUserMessagePreview: summarizeLastUserMessage(this.state.conversation),
|
|
939
|
+
requestBody: JSON.stringify(body, null, 2),
|
|
418
940
|
});
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
941
|
+
const maxAttempts = provider.requestRetryCount + 1;
|
|
942
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
943
|
+
this.abortController = new AbortController();
|
|
944
|
+
let timedOut = false;
|
|
945
|
+
try {
|
|
946
|
+
return await withTimeout((async () => {
|
|
947
|
+
const response = await withTimeout(fetch(normalizeChatCompletionsUrl(baseURL), {
|
|
948
|
+
method: "POST",
|
|
949
|
+
headers: buildRequestHeaders(provider),
|
|
950
|
+
body: JSON.stringify(body),
|
|
951
|
+
signal: this.abortController?.signal,
|
|
952
|
+
}), provider.requestTimeoutMs, "ACode provider request", () => {
|
|
953
|
+
timedOut = true;
|
|
954
|
+
this.abortController?.abort();
|
|
955
|
+
});
|
|
956
|
+
if (!response.ok) {
|
|
957
|
+
throw new Error(`ACode provider request failed: ${response.status} ${await response.text()}`);
|
|
958
|
+
}
|
|
959
|
+
const contentType = response.headers?.get?.("content-type") || "";
|
|
960
|
+
if (provider.stream &&
|
|
961
|
+
contentType.toLowerCase().includes("text/event-stream")) {
|
|
962
|
+
return await this.readChatCompletionStream(response, provider.requestTimeoutMs);
|
|
963
|
+
}
|
|
964
|
+
const payload = (await withTimeout(response.json(), provider.requestTimeoutMs, "ACode provider response body", () => {
|
|
965
|
+
timedOut = true;
|
|
966
|
+
this.abortController?.abort();
|
|
967
|
+
}));
|
|
968
|
+
return isRecord(payload) ? payload : {};
|
|
969
|
+
})(), 0, "ACode provider request");
|
|
970
|
+
}
|
|
971
|
+
catch (error) {
|
|
972
|
+
let requestError;
|
|
973
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
974
|
+
requestError = timedOut
|
|
975
|
+
? new Error(`ACode provider request timed out after ${provider.requestTimeoutMs}ms`)
|
|
976
|
+
: new Error("ACode provider request aborted");
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
requestError =
|
|
980
|
+
error instanceof Error ? error : new Error(String(error));
|
|
981
|
+
}
|
|
982
|
+
const shouldRetry = attempt < maxAttempts &&
|
|
983
|
+
!this.abortRequested &&
|
|
984
|
+
!this.state.terminated &&
|
|
985
|
+
isRetryableProviderRequestError(requestError);
|
|
986
|
+
if (!shouldRetry) {
|
|
987
|
+
throw requestError;
|
|
988
|
+
}
|
|
989
|
+
this.emitAcodeEvent("error", {
|
|
990
|
+
message: `ACode provider request attempt ${attempt}/${maxAttempts} failed; retrying: ${requestError.message}`,
|
|
991
|
+
});
|
|
992
|
+
await delay(DEFAULT_PROVIDER_REQUEST_RETRY_DELAY_MS * attempt);
|
|
993
|
+
}
|
|
994
|
+
finally {
|
|
995
|
+
this.abortController = null;
|
|
996
|
+
}
|
|
422
997
|
}
|
|
423
|
-
|
|
424
|
-
|
|
998
|
+
throw new Error("ACode provider request failed after all retries");
|
|
999
|
+
}
|
|
1000
|
+
async readChatCompletionStream(response, idleTimeoutMs) {
|
|
1001
|
+
if (!response.body) {
|
|
1002
|
+
throw new Error("ACode provider stream response has no body");
|
|
1003
|
+
}
|
|
1004
|
+
const reader = response.body.getReader();
|
|
1005
|
+
const decoder = new TextDecoder();
|
|
1006
|
+
const toolCallAccumulators = new Map();
|
|
1007
|
+
let text = "";
|
|
1008
|
+
let buffer = "";
|
|
1009
|
+
const consumeFrame = (frame) => {
|
|
1010
|
+
for (const data of parseSseDataLines(frame)) {
|
|
1011
|
+
if (data === "[DONE]") {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const payload = JSON.parse(data);
|
|
1015
|
+
if (!isRecord(payload)) {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
const choice = payload.choices?.[0];
|
|
1019
|
+
const delta = choice?.delta;
|
|
1020
|
+
const reasoningText = extractReasoningText(delta);
|
|
1021
|
+
if (reasoningText) {
|
|
1022
|
+
this.emitAcodeEvent("thinking_delta", { text: reasoningText });
|
|
1023
|
+
}
|
|
1024
|
+
const content = typeof delta?.content === "string" ? delta.content : "";
|
|
1025
|
+
if (content) {
|
|
1026
|
+
text += content;
|
|
1027
|
+
this.emitAcodeEvent("assistant_delta", { text: content });
|
|
1028
|
+
}
|
|
1029
|
+
const toolDeltas = Array.isArray(delta?.tool_calls)
|
|
1030
|
+
? delta.tool_calls
|
|
1031
|
+
: [];
|
|
1032
|
+
for (const toolDelta of toolDeltas) {
|
|
1033
|
+
updateToolCallAccumulator(toolCallAccumulators, toolDelta);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
while (true) {
|
|
1038
|
+
const { done, value } = await withTimeout(reader.read(), idleTimeoutMs, "ACode provider stream idle", () => this.abortController?.abort());
|
|
1039
|
+
if (done) {
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1043
|
+
const frames = buffer.split(/\r?\n\r?\n/);
|
|
1044
|
+
buffer = frames.pop() ?? "";
|
|
1045
|
+
for (const frame of frames) {
|
|
1046
|
+
consumeFrame(frame);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
buffer += decoder.decode();
|
|
1050
|
+
if (buffer.trim()) {
|
|
1051
|
+
consumeFrame(buffer);
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
choices: [
|
|
1055
|
+
{
|
|
1056
|
+
message: {
|
|
1057
|
+
content: text || null,
|
|
1058
|
+
tool_calls: toAccumulatedToolCalls(toolCallAccumulators),
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
],
|
|
1062
|
+
};
|
|
425
1063
|
}
|
|
426
1064
|
async handleToolCalls(toolCalls, commandAllowedTools) {
|
|
427
1065
|
for (const toolCall of toolCalls) {
|
|
428
1066
|
const tool = this.state.exposedTools.find((item) => item.exposedName === toolCall.function.name);
|
|
429
1067
|
if (!tool) {
|
|
430
1068
|
this.state.conversation.push({
|
|
431
|
-
role:
|
|
1069
|
+
role: "tool",
|
|
432
1070
|
tool_call_id: toolCall.id,
|
|
433
1071
|
content: `ACode MCP tool not found: ${toolCall.function.name}`,
|
|
434
1072
|
});
|
|
435
1073
|
continue;
|
|
436
1074
|
}
|
|
437
|
-
|
|
1075
|
+
try {
|
|
1076
|
+
if (tool.kind === "builtin") {
|
|
1077
|
+
const toolArguments = coerceArguments(toolCall.function.arguments);
|
|
1078
|
+
assertToolPermission(tool, this.state.permissions, commandAllowedTools);
|
|
1079
|
+
const preResults = await this.hookBus.dispatch("PreToolUse", {
|
|
1080
|
+
source: "builtin",
|
|
1081
|
+
toolName: tool.toolName,
|
|
1082
|
+
exposedName: tool.exposedName,
|
|
1083
|
+
arguments: toolArguments,
|
|
1084
|
+
});
|
|
1085
|
+
const denied = findDeniedHookResult(preResults);
|
|
1086
|
+
if (denied) {
|
|
1087
|
+
throw new Error(denied.message ||
|
|
1088
|
+
`ACode tool denied by hook: ${tool.exposedName}`);
|
|
1089
|
+
}
|
|
1090
|
+
this.emitAcodeEvent("mcp_status", {
|
|
1091
|
+
action: "call_tool",
|
|
1092
|
+
server: "builtin",
|
|
1093
|
+
tool: tool.toolName,
|
|
1094
|
+
exposedName: tool.exposedName,
|
|
1095
|
+
toolUseId: toolCall.id,
|
|
1096
|
+
arguments: toolArguments,
|
|
1097
|
+
argumentsPreview: summarizeToolArguments(toolArguments),
|
|
1098
|
+
});
|
|
1099
|
+
const result = await withTimeout(tool.execute(toolArguments), getBuiltInToolTimeoutMs(tool, toolArguments, this.state.provider.mcpToolTimeoutMs), `ACode built-in tool ${tool.toolName}`);
|
|
1100
|
+
await this.hookBus.dispatch("PostToolUse", {
|
|
1101
|
+
source: "builtin",
|
|
1102
|
+
toolName: tool.toolName,
|
|
1103
|
+
exposedName: tool.exposedName,
|
|
1104
|
+
result: toToolResultText(result),
|
|
1105
|
+
});
|
|
1106
|
+
const resultText = toToolResultText(result);
|
|
1107
|
+
this.emitAcodeEvent("mcp_status", {
|
|
1108
|
+
action: "tool_result",
|
|
1109
|
+
server: "builtin",
|
|
1110
|
+
tool: tool.toolName,
|
|
1111
|
+
result: resultText,
|
|
1112
|
+
});
|
|
1113
|
+
this.state.conversation.push({
|
|
1114
|
+
role: "tool",
|
|
1115
|
+
tool_call_id: toolCall.id,
|
|
1116
|
+
content: resultText,
|
|
1117
|
+
});
|
|
1118
|
+
await this.appendSyntheticMultimodalUserMessage(resultText);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
const connection = this.state.mcpConnections.find((item) => item.serverName === tool.serverName);
|
|
1122
|
+
if (!connection) {
|
|
1123
|
+
this.state.conversation.push({
|
|
1124
|
+
role: "tool",
|
|
1125
|
+
tool_call_id: toolCall.id,
|
|
1126
|
+
content: `ACode MCP server not connected: ${tool.serverName}`,
|
|
1127
|
+
});
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
438
1130
|
const toolArguments = coerceArguments(toolCall.function.arguments);
|
|
439
1131
|
assertToolPermission(tool, this.state.permissions, commandAllowedTools);
|
|
440
|
-
const preResults = await this.hookBus.dispatch(
|
|
441
|
-
source:
|
|
1132
|
+
const preResults = await this.hookBus.dispatch("PreToolUse", {
|
|
1133
|
+
source: "mcp",
|
|
1134
|
+
serverName: tool.serverName,
|
|
442
1135
|
toolName: tool.toolName,
|
|
443
1136
|
exposedName: tool.exposedName,
|
|
444
1137
|
arguments: toolArguments,
|
|
@@ -447,70 +1140,88 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
447
1140
|
if (denied) {
|
|
448
1141
|
throw new Error(denied.message || `ACode tool denied by hook: ${tool.exposedName}`);
|
|
449
1142
|
}
|
|
450
|
-
this.emitAcodeEvent(
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
1143
|
+
this.emitAcodeEvent("mcp_status", {
|
|
1144
|
+
action: "call_tool",
|
|
1145
|
+
server: tool.serverName,
|
|
1146
|
+
tool: tool.toolName,
|
|
1147
|
+
exposedName: tool.exposedName,
|
|
1148
|
+
toolUseId: toolCall.id,
|
|
1149
|
+
arguments: toolArguments,
|
|
1150
|
+
argumentsPreview: summarizeToolArguments(toolArguments),
|
|
1151
|
+
});
|
|
1152
|
+
const toolTimeoutMs = isMcpPollingToolName(tool.toolName)
|
|
1153
|
+
? this.state.provider.mcpPollingToolTimeoutMs
|
|
1154
|
+
: this.state.provider.mcpToolTimeoutMs;
|
|
1155
|
+
const result = await withTimeout(connection.client.callTool({
|
|
1156
|
+
name: tool.toolName,
|
|
1157
|
+
arguments: toolArguments,
|
|
1158
|
+
}, undefined, { timeout: toolTimeoutMs }), toolTimeoutMs, `ACode MCP tool ${tool.serverName}/${tool.toolName}`);
|
|
1159
|
+
await this.hookBus.dispatch("PostToolUse", {
|
|
1160
|
+
source: "mcp",
|
|
1161
|
+
serverName: tool.serverName,
|
|
454
1162
|
toolName: tool.toolName,
|
|
455
1163
|
exposedName: tool.exposedName,
|
|
456
1164
|
result: toToolResultText(result),
|
|
457
1165
|
});
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
1166
|
+
const resultText = toToolResultText(result);
|
|
1167
|
+
this.emitAcodeEvent("mcp_status", {
|
|
1168
|
+
action: "tool_result",
|
|
1169
|
+
server: tool.serverName,
|
|
1170
|
+
tool: tool.toolName,
|
|
1171
|
+
result: resultText,
|
|
462
1172
|
});
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
const connection = this.state.mcpConnections.find((item) => item.serverName === tool.serverName);
|
|
466
|
-
if (!connection) {
|
|
467
1173
|
this.state.conversation.push({
|
|
468
|
-
role:
|
|
1174
|
+
role: "tool",
|
|
469
1175
|
tool_call_id: toolCall.id,
|
|
470
|
-
content:
|
|
1176
|
+
content: resultText,
|
|
471
1177
|
});
|
|
472
|
-
|
|
1178
|
+
await this.appendSyntheticMultimodalUserMessage(resultText);
|
|
473
1179
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const preResults = await this.hookBus.dispatch('PreToolUse', {
|
|
477
|
-
source: 'mcp',
|
|
478
|
-
serverName: tool.serverName,
|
|
479
|
-
toolName: tool.toolName,
|
|
480
|
-
exposedName: tool.exposedName,
|
|
481
|
-
arguments: toolArguments,
|
|
482
|
-
});
|
|
483
|
-
const denied = findDeniedHookResult(preResults);
|
|
484
|
-
if (denied) {
|
|
485
|
-
throw new Error(denied.message || `ACode tool denied by hook: ${tool.exposedName}`);
|
|
1180
|
+
catch (error) {
|
|
1181
|
+
this.appendToolFailureResult(toolCall, tool, error);
|
|
486
1182
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
result: toToolResultText(result),
|
|
498
|
-
});
|
|
499
|
-
this.state.conversation.push({
|
|
500
|
-
role: 'tool',
|
|
501
|
-
tool_call_id: toolCall.id,
|
|
502
|
-
content: toToolResultText(result),
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
async appendSyntheticMultimodalUserMessage(toolResultText) {
|
|
1186
|
+
const parsedContent = parseAttachmentMessage(toolResultText);
|
|
1187
|
+
if (!parsedContent.hasImages) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
if (!this.state.provider.supportsImageInput) {
|
|
1191
|
+
this.emitAcodeEvent("error", {
|
|
1192
|
+
message: "ACode skipped image content from tool result because the provider/model is not configured for image input.",
|
|
503
1193
|
});
|
|
1194
|
+
return;
|
|
504
1195
|
}
|
|
1196
|
+
const content = await materializeImageContent(parsedContent.content);
|
|
1197
|
+
this.state.conversation.push({ role: "user", content });
|
|
1198
|
+
}
|
|
1199
|
+
appendToolFailureResult(toolCall, tool, error) {
|
|
1200
|
+
const resultText = toToolErrorResultText(tool, error);
|
|
1201
|
+
this.emitAcodeEvent("error", {
|
|
1202
|
+
message: `ACode tool failed: ${tool.exposedName}: ${getErrorMessage(error)}`,
|
|
1203
|
+
});
|
|
1204
|
+
this.emitAcodeEvent("mcp_status", {
|
|
1205
|
+
action: "tool_result",
|
|
1206
|
+
server: tool.kind === "mcp" ? tool.serverName : "builtin",
|
|
1207
|
+
tool: tool.toolName,
|
|
1208
|
+
result: resultText,
|
|
1209
|
+
failed: true,
|
|
1210
|
+
});
|
|
1211
|
+
this.state.conversation.push({
|
|
1212
|
+
role: "tool",
|
|
1213
|
+
tool_call_id: toolCall.id,
|
|
1214
|
+
content: resultText,
|
|
1215
|
+
});
|
|
505
1216
|
}
|
|
506
|
-
async connectMcpServers(servers) {
|
|
1217
|
+
async connectMcpServers(servers, timeoutMs) {
|
|
507
1218
|
const connections = [];
|
|
508
1219
|
for (const [serverName, serverSpec] of Object.entries(servers)) {
|
|
509
1220
|
try {
|
|
510
|
-
const client = new Client({ name:
|
|
1221
|
+
const client = new Client({ name: "acode", version: "0.1.0" });
|
|
511
1222
|
const transport = this.createMcpTransport(serverSpec);
|
|
512
|
-
await client.connect(transport);
|
|
513
|
-
const { tools } = await client.listTools();
|
|
1223
|
+
await withTimeout(client.connect(transport), timeoutMs, `ACode MCP server ${serverName} connect`);
|
|
1224
|
+
const { tools } = await withTimeout(client.listTools(), timeoutMs, `ACode MCP server ${serverName} listTools`);
|
|
514
1225
|
connections.push({
|
|
515
1226
|
serverName,
|
|
516
1227
|
client,
|
|
@@ -518,15 +1229,17 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
518
1229
|
tools: tools.map((tool) => ({
|
|
519
1230
|
serverName,
|
|
520
1231
|
toolName: tool.name,
|
|
521
|
-
kind:
|
|
1232
|
+
kind: "mcp",
|
|
522
1233
|
exposedName: normalizeToolName(serverName, tool.name),
|
|
523
1234
|
description: tool.description,
|
|
524
|
-
inputSchema: isJsonObject(tool.inputSchema)
|
|
1235
|
+
inputSchema: isJsonObject(tool.inputSchema)
|
|
1236
|
+
? tool.inputSchema
|
|
1237
|
+
: { type: "object", properties: {} },
|
|
525
1238
|
})),
|
|
526
1239
|
});
|
|
527
1240
|
}
|
|
528
1241
|
catch (error) {
|
|
529
|
-
this.emitAcodeEvent(
|
|
1242
|
+
this.emitAcodeEvent("error", {
|
|
530
1243
|
message: `ACode MCP server failed to connect: ${serverName}: ${error instanceof Error ? error.message : String(error)}`,
|
|
531
1244
|
});
|
|
532
1245
|
}
|
|
@@ -534,38 +1247,37 @@ export class ACodeRuntime extends EventEmitter {
|
|
|
534
1247
|
return connections;
|
|
535
1248
|
}
|
|
536
1249
|
createMcpTransport(serverSpec) {
|
|
537
|
-
if (serverSpec.type ===
|
|
1250
|
+
if (serverSpec.type === "stdio" || serverSpec.command) {
|
|
538
1251
|
if (!serverSpec.command) {
|
|
539
|
-
throw new Error(
|
|
1252
|
+
throw new Error("stdio MCP server requires command");
|
|
540
1253
|
}
|
|
541
1254
|
return new StdioClientTransport({
|
|
542
1255
|
command: serverSpec.command,
|
|
543
1256
|
args: serverSpec.args,
|
|
544
1257
|
cwd: serverSpec.cwd || this.config.workingDirectory,
|
|
545
1258
|
env: { ...(this.config.env ?? {}), ...(serverSpec.env ?? {}) },
|
|
546
|
-
stderr:
|
|
1259
|
+
stderr: "pipe",
|
|
547
1260
|
});
|
|
548
1261
|
}
|
|
549
1262
|
if (!serverSpec.url) {
|
|
550
|
-
throw new Error(
|
|
1263
|
+
throw new Error("remote MCP server requires url");
|
|
551
1264
|
}
|
|
552
|
-
const requestInit = serverSpec.headers
|
|
553
|
-
|
|
1265
|
+
const requestInit = serverSpec.headers
|
|
1266
|
+
? { headers: serverSpec.headers }
|
|
1267
|
+
: undefined;
|
|
1268
|
+
if (serverSpec.type === "sse") {
|
|
554
1269
|
return new SSEClientTransport(new URL(serverSpec.url), { requestInit });
|
|
555
1270
|
}
|
|
556
|
-
return new StreamableHTTPClientTransport(new URL(serverSpec.url), {
|
|
1271
|
+
return new StreamableHTTPClientTransport(new URL(serverSpec.url), {
|
|
1272
|
+
requestInit,
|
|
1273
|
+
});
|
|
557
1274
|
}
|
|
558
1275
|
async disconnectMcpServers() {
|
|
559
1276
|
const connections = this.state.mcpConnections;
|
|
560
1277
|
this.state.mcpConnections = [];
|
|
561
1278
|
this.state.exposedTools = [];
|
|
562
|
-
await Promise.all(connections.map(
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
catch {
|
|
567
|
-
// Best-effort shutdown; stale MCP transports must not block runtime termination.
|
|
568
|
-
}
|
|
569
|
-
}));
|
|
1279
|
+
await Promise.all(connections.map((connection) => runBestEffort(`ACode MCP server ${connection.serverName} close`, connection.transport.close(), (message) => {
|
|
1280
|
+
this.emitAcodeEvent("error", { message });
|
|
1281
|
+
}, this.state.provider.mcpToolTimeoutMs)));
|
|
570
1282
|
}
|
|
571
1283
|
}
|