aws-runtime-bridge 1.7.49 → 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.
Files changed (199) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +104 -0
  2. package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -0
  3. package/dist/adapter/AcodeSdkAdapter.js +644 -0
  4. package/dist/adapter/AcodeSdkAdapter.test.d.ts +2 -0
  5. package/dist/adapter/AcodeSdkAdapter.test.d.ts.map +1 -0
  6. package/dist/adapter/AcodeSdkAdapter.test.js +1280 -0
  7. package/dist/adapter/AdapterRegistry.d.ts +1 -1
  8. package/dist/adapter/AdapterRegistry.d.ts.map +1 -1
  9. package/dist/adapter/AdapterRegistry.js +2 -0
  10. package/dist/adapter/AdapterRegistry.test.js +10 -1
  11. package/dist/adapter/ClaudeSdkAdapter.test.js +31 -8
  12. package/dist/adapter/CodexSdkAdapter.d.ts.map +1 -1
  13. package/dist/adapter/CodexSdkAdapter.js +2 -0
  14. package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
  15. package/dist/adapter/OpencodeSdkAdapter.js +2 -0
  16. package/dist/adapter/SdkProviderSpi.d.ts.map +1 -1
  17. package/dist/adapter/SdkProviderSpi.js +10 -1
  18. package/dist/adapter/adapter.test.js +2 -2
  19. package/dist/adapter/idle-poll-loop.d.ts +4 -4
  20. package/dist/adapter/idle-poll-loop.d.ts.map +1 -1
  21. package/dist/adapter/idle-poll-loop.js +3 -3
  22. package/dist/adapter/idle-poll-loop.test.js +15 -1
  23. package/dist/adapter/index.d.ts +1 -0
  24. package/dist/adapter/index.d.ts.map +1 -1
  25. package/dist/adapter/index.js +1 -0
  26. package/dist/adapter/types.d.ts +25 -6
  27. package/dist/adapter/types.d.ts.map +1 -1
  28. package/dist/adapter/types.js +221 -110
  29. package/dist/adapter/types.test.js +10 -3
  30. package/dist/adapters/cc-switch/index.d.ts +2 -0
  31. package/dist/adapters/cc-switch/index.d.ts.map +1 -1
  32. package/dist/adapters/cc-switch/index.js +6 -0
  33. package/dist/adapters/cc-switch/mcp-acode.d.ts +17 -0
  34. package/dist/adapters/cc-switch/mcp-acode.d.ts.map +1 -0
  35. package/dist/adapters/cc-switch/mcp-acode.js +71 -0
  36. package/dist/adapters/cc-switch/mcp-acode.test.d.ts +2 -0
  37. package/dist/adapters/cc-switch/mcp-acode.test.d.ts.map +1 -0
  38. package/dist/adapters/cc-switch/mcp-acode.test.js +80 -0
  39. package/dist/adapters/cc-switch/mcp-claude.d.ts.map +1 -1
  40. package/dist/adapters/cc-switch/mcp-claude.js +1 -0
  41. package/dist/adapters/cc-switch/mcp-claudecode.d.ts.map +1 -1
  42. package/dist/adapters/cc-switch/mcp-claudecode.js +1 -0
  43. package/dist/adapters/cc-switch/mcp-claudecode.test.js +1 -0
  44. package/dist/adapters/cc-switch/mcp-codex.d.ts.map +1 -1
  45. package/dist/adapters/cc-switch/mcp-codex.js +1 -0
  46. package/dist/adapters/cc-switch/mcp-codex.test.js +1 -0
  47. package/dist/adapters/cc-switch/mcp-opencode.d.ts.map +1 -1
  48. package/dist/adapters/cc-switch/mcp-opencode.js +1 -0
  49. package/dist/adapters/cc-switch/mcp-opencode.test.js +1 -0
  50. package/dist/adapters/cc-switch/skill-acode.d.ts +11 -0
  51. package/dist/adapters/cc-switch/skill-acode.d.ts.map +1 -0
  52. package/dist/adapters/cc-switch/skill-acode.js +12 -0
  53. package/dist/adapters/cc-switch/skill-directory.test.js +1 -0
  54. package/dist/config.d.ts +4 -0
  55. package/dist/config.d.ts.map +1 -1
  56. package/dist/config.js +10 -0
  57. package/dist/index.js +2 -0
  58. package/dist/routes/ai-sources.js +3 -2
  59. package/dist/routes/instance.d.ts.map +1 -1
  60. package/dist/routes/instance.js +7 -8
  61. package/dist/routes/instance.test.js +2 -1
  62. package/dist/routes/mcp.js +2 -2
  63. package/dist/routes/mcp.test.js +2 -1
  64. package/dist/routes/runtime-binding.d.ts +3 -2
  65. package/dist/routes/runtime-binding.d.ts.map +1 -1
  66. package/dist/routes/runtime-binding.js +11 -58
  67. package/dist/routes/runtime-binding.test.js +4 -80
  68. package/dist/routes/sessions.d.ts +5 -0
  69. package/dist/routes/sessions.d.ts.map +1 -1
  70. package/dist/routes/sessions.js +36 -16
  71. package/dist/routes/sessions.test.js +13 -0
  72. package/dist/routes/skills.js +2 -2
  73. package/dist/routes/skills.test.js +2 -1
  74. package/dist/routes/terminal.d.ts +27 -7
  75. package/dist/routes/terminal.d.ts.map +1 -1
  76. package/dist/routes/terminal.js +439 -171
  77. package/dist/routes/terminal.test.js +385 -197
  78. package/dist/services/acode-package.d.ts +8 -0
  79. package/dist/services/acode-package.d.ts.map +1 -0
  80. package/dist/services/acode-package.js +82 -0
  81. package/dist/services/ai-source-apply.d.ts +4 -0
  82. package/dist/services/ai-source-apply.d.ts.map +1 -1
  83. package/dist/services/ai-source-apply.js +8 -0
  84. package/dist/services/ai-source-apply.test.js +8 -1
  85. package/dist/services/cc-switch-sdk.d.ts.map +1 -1
  86. package/dist/services/cc-switch-sdk.js +9 -1
  87. package/dist/services/instance-init-service.d.ts.map +1 -1
  88. package/dist/services/instance-init-service.js +3 -2
  89. package/dist/services/instance-init-service.test.js +6 -5
  90. package/dist/services/instance-service.d.ts.map +1 -1
  91. package/dist/services/instance-service.js +1 -0
  92. package/dist/services/instance-state.js +1 -1
  93. package/dist/services/instance-state.test.js +4 -4
  94. package/dist/services/mcp-launch-binding-queue.d.ts +1 -0
  95. package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -1
  96. package/dist/services/mcp-launch-binding-queue.js +8 -2
  97. package/dist/services/mcp-launch-binding-queue.test.js +47 -4
  98. package/dist/services/session-output.d.ts +10 -6
  99. package/dist/services/session-output.d.ts.map +1 -1
  100. package/dist/services/session-output.js +29 -14
  101. package/dist/services/session-output.test.js +93 -39
  102. package/dist/services/tool-installer.d.ts.map +1 -1
  103. package/dist/services/tool-installer.js +10 -0
  104. package/dist/types.d.ts +5 -0
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/utils/file-utils.d.ts.map +1 -1
  107. package/dist/utils/file-utils.js +1 -0
  108. package/dist/utils/mcp-utils.d.ts.map +1 -1
  109. package/dist/utils/mcp-utils.js +4 -1
  110. package/dist/utils/validation.js +1 -1
  111. package/node_modules/@cc-switch/sdk/dist/constants.d.ts.map +1 -1
  112. package/node_modules/@cc-switch/sdk/dist/constants.js +7 -0
  113. package/node_modules/@cc-switch/sdk/dist/schemas.d.ts.map +1 -1
  114. package/node_modules/@cc-switch/sdk/dist/schemas.js +1 -0
  115. package/node_modules/@cc-switch/sdk/dist/sdk.d.ts.map +1 -1
  116. package/node_modules/@cc-switch/sdk/dist/sdk.js +7 -3
  117. package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.d.ts +6 -0
  118. package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.d.ts.map +1 -1
  119. package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.js +27 -1
  120. package/node_modules/@cc-switch/sdk/dist/types.d.ts +4 -1
  121. package/node_modules/@cc-switch/sdk/dist/types.d.ts.map +1 -1
  122. package/package/acode/README.md +88 -0
  123. package/package/acode/dist/built-in-file-tools.d.ts +16 -0
  124. package/package/acode/dist/built-in-file-tools.d.ts.map +1 -0
  125. package/package/acode/dist/built-in-file-tools.js +569 -0
  126. package/package/acode/dist/cli.d.ts +3 -0
  127. package/package/acode/dist/cli.d.ts.map +1 -0
  128. package/package/acode/dist/cli.js +37 -0
  129. package/package/acode/dist/commands/commandLoader.d.ts +7 -0
  130. package/package/acode/dist/commands/commandLoader.d.ts.map +1 -0
  131. package/package/acode/dist/commands/commandLoader.js +33 -0
  132. package/package/acode/dist/commands/commandParser.d.ts +7 -0
  133. package/package/acode/dist/commands/commandParser.d.ts.map +1 -0
  134. package/package/acode/dist/commands/commandParser.js +77 -0
  135. package/package/acode/dist/commands/commandRegistry.d.ts +13 -0
  136. package/package/acode/dist/commands/commandRegistry.d.ts.map +1 -0
  137. package/package/acode/dist/commands/commandRegistry.js +30 -0
  138. package/package/acode/dist/config.d.ts +4 -0
  139. package/package/acode/dist/config.d.ts.map +1 -0
  140. package/package/acode/dist/config.js +80 -0
  141. package/package/acode/dist/hooks/hookBus.d.ts +19 -0
  142. package/package/acode/dist/hooks/hookBus.d.ts.map +1 -0
  143. package/package/acode/dist/hooks/hookBus.js +40 -0
  144. package/package/acode/dist/index.d.ts +8 -0
  145. package/package/acode/dist/index.d.ts.map +1 -0
  146. package/package/acode/dist/index.js +7 -0
  147. package/package/acode/dist/runtime.d.ts +42 -0
  148. package/package/acode/dist/runtime.d.ts.map +1 -0
  149. package/package/acode/dist/runtime.js +1283 -0
  150. package/package/acode/dist/types.d.ts +112 -0
  151. package/package/acode/dist/types.d.ts.map +1 -0
  152. package/package/acode/dist/types.js +1 -0
  153. package/package/acode/package.json +24 -0
  154. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +4 -4
  155. package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
  156. package/package/aws-client-agent-mcp/dist/agent-client.js +9 -6
  157. package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
  158. package/package/aws-client-agent-mcp/dist/agent-client.test.js +21 -0
  159. package/package/aws-client-agent-mcp/dist/agent-client.test.js.map +1 -1
  160. package/package/aws-client-agent-mcp/dist/http-client.d.ts +1 -17
  161. package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
  162. package/package/aws-client-agent-mcp/dist/http-client.js +7 -113
  163. package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
  164. package/package/aws-client-agent-mcp/dist/http-client.test.js +22 -2
  165. package/package/aws-client-agent-mcp/dist/http-client.test.js.map +1 -1
  166. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +0 -5
  167. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  168. package/package/aws-client-agent-mcp/dist/mcp-server.js +3 -24
  169. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  170. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +90 -5
  171. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  172. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts.map +1 -1
  173. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js +1 -0
  174. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js.map +1 -1
  175. package/package/aws-client-agent-mcp/dist/status-reporter.d.ts +2 -0
  176. package/package/aws-client-agent-mcp/dist/status-reporter.d.ts.map +1 -1
  177. package/package/aws-client-agent-mcp/dist/status-reporter.js +31 -3
  178. package/package/aws-client-agent-mcp/dist/status-reporter.js.map +1 -1
  179. package/package/aws-client-agent-mcp/dist/types.d.ts +4 -1
  180. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  181. package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
  182. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +0 -13
  183. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  184. package/package/aws-client-agent-mcp/dist/websocket-client.js +5 -100
  185. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  186. package/package/aws-client-agent-mcp/dist/websocket-client.test.js +43 -0
  187. package/package/aws-client-agent-mcp/dist/websocket-client.test.js.map +1 -1
  188. package/package/cc-switch-sdk/dist/constants.d.ts.map +1 -1
  189. package/package/cc-switch-sdk/dist/constants.js +7 -0
  190. package/package/cc-switch-sdk/dist/schemas.d.ts.map +1 -1
  191. package/package/cc-switch-sdk/dist/schemas.js +1 -0
  192. package/package/cc-switch-sdk/dist/sdk.d.ts.map +1 -1
  193. package/package/cc-switch-sdk/dist/sdk.js +7 -3
  194. package/package/cc-switch-sdk/dist/services/ai-config-service.d.ts +6 -0
  195. package/package/cc-switch-sdk/dist/services/ai-config-service.d.ts.map +1 -1
  196. package/package/cc-switch-sdk/dist/services/ai-config-service.js +27 -1
  197. package/package/cc-switch-sdk/dist/types.d.ts +4 -1
  198. package/package/cc-switch-sdk/dist/types.d.ts.map +1 -1
  199. package/package.json +8 -4
@@ -0,0 +1,1283 @@
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
+ function nowIso() {
12
+ return new Date().toISOString();
13
+ }
14
+ function isRecord(value) {
15
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
16
+ }
17
+ function isJsonObject(value) {
18
+ return isRecord(value);
19
+ }
20
+ function normalizeText(value) {
21
+ const text = String(value ?? "").trim();
22
+ return text || undefined;
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
+ }
101
+ /**
102
+ * 解析 ACode 当前会话模型:优先显式启动参数,其次兼容 cc-switch 写入的
103
+ * OpenCode-style `provider.default.model`,最后回落到旧版 top-level model。
104
+ */
105
+ function resolveProviderConfig(config, acodeConfig) {
106
+ const defaultProvider = acodeConfig.provider?.default;
107
+ const providerOptions = defaultProvider?.options;
108
+ const model = config.model ||
109
+ defaultProvider?.model ||
110
+ acodeConfig.model ||
111
+ "acode-default";
112
+ return {
113
+ model,
114
+ baseURL: normalizeText(providerOptions?.baseURL || providerOptions?.baseUrl),
115
+ apiKey: normalizeText(providerOptions?.apiKey),
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),
123
+ };
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
+ }
326
+ /**
327
+ * 解析 ACode 工具权限配置。
328
+ * 主流程:运行时配置优先 -> `.acode/config.json` 次之 -> 兼容旧式 allowedTools/deniedTools 字段。
329
+ */
330
+ function resolvePermissionConfig(config, acodeConfig) {
331
+ const configured = config.permissions ?? acodeConfig.permissions ?? {};
332
+ const legacyAllow = config.permissions
333
+ ? []
334
+ : (acodeConfig.allowedTools ?? []);
335
+ const legacyDeny = config.permissions ? [] : (acodeConfig.deniedTools ?? []);
336
+ return {
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),
344
+ };
345
+ }
346
+ function wildcardMatches(pattern, value) {
347
+ if (pattern === "*") {
348
+ return true;
349
+ }
350
+ if (pattern.endsWith("*")) {
351
+ return value.startsWith(pattern.slice(0, -1));
352
+ }
353
+ return pattern === value;
354
+ }
355
+ function toolPermissionNames(tool) {
356
+ if (tool.kind === "builtin") {
357
+ return [tool.exposedName, tool.toolName, `builtin.${tool.toolName}`];
358
+ }
359
+ return [
360
+ tool.exposedName,
361
+ tool.toolName,
362
+ `${tool.serverName}.${tool.toolName}`,
363
+ `mcp.${tool.serverName}.${tool.toolName}`,
364
+ ];
365
+ }
366
+ function matchesToolRule(rule, tool) {
367
+ return toolPermissionNames(tool).some((name) => wildcardMatches(rule, name));
368
+ }
369
+ function assertToolAllowedByRules(tool, rules, message) {
370
+ if (rules.length > 0 && !rules.some((rule) => matchesToolRule(rule, tool))) {
371
+ throw new Error(message);
372
+ }
373
+ }
374
+ function assertToolPermission(tool, permissions, commandAllowedTools) {
375
+ if (permissions.mode === "bypassPermissions") {
376
+ return;
377
+ }
378
+ if (permissions.deny.some((rule) => matchesToolRule(rule, tool))) {
379
+ throw new Error(`ACode tool denied by permission rule: ${tool.exposedName}`);
380
+ }
381
+ assertToolAllowedByRules(tool, permissions.allow, `ACode tool is not in allowed permission rules: ${tool.exposedName}`);
382
+ assertToolAllowedByRules(tool, commandAllowedTools ?? [], `ACode slash command does not allow tool: ${tool.exposedName}`);
383
+ }
384
+ function isToolAllowed(tool, permissions, commandAllowedTools) {
385
+ try {
386
+ assertToolPermission(tool, permissions, commandAllowedTools);
387
+ return true;
388
+ }
389
+ catch {
390
+ return false;
391
+ }
392
+ }
393
+ function normalizeChatCompletionsUrl(baseURL) {
394
+ const trimmed = baseURL.replace(/\/+$/g, "");
395
+ if (/\/chat\/completions$/i.test(trimmed)) {
396
+ return trimmed;
397
+ }
398
+ return `${trimmed}/chat/completions`;
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
+ }
421
+ function coerceArguments(rawArguments) {
422
+ try {
423
+ const parsed = JSON.parse(rawArguments);
424
+ return isJsonObject(parsed) ? parsed : {};
425
+ }
426
+ catch {
427
+ return {};
428
+ }
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
+ }
445
+ function createFallbackResponse(message, state) {
446
+ const trimmed = message.trim();
447
+ return `ACode 已接收任务。当前已加载 ${state.configMcpCount} 个 MCP 配置、${state.exposedTools.length} 个 MCP Tool 与 ${state.skills.length} 个 Skill。${trimmed ? `输入摘要:${trimmed.slice(0, 200)}` : ""}`;
448
+ }
449
+ function toToolResultText(result) {
450
+ if (!isRecord(result)) {
451
+ return String(result ?? "");
452
+ }
453
+ const content = result.content;
454
+ if (Array.isArray(content)) {
455
+ const parts = content
456
+ .map((item) => {
457
+ if (!isRecord(item)) {
458
+ return "";
459
+ }
460
+ if (typeof item.text === "string") {
461
+ return item.text;
462
+ }
463
+ return JSON.stringify(item);
464
+ })
465
+ .filter(Boolean);
466
+ if (parts.length > 0) {
467
+ return parts.join("\n");
468
+ }
469
+ }
470
+ return JSON.stringify(result);
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
+ }
533
+ function buildSkillPrompt(skills) {
534
+ if (skills.length === 0) {
535
+ return "";
536
+ }
537
+ return [
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");
542
+ }
543
+ function buildCommandPrompt(commands) {
544
+ if (commands.length === 0) {
545
+ return "";
546
+ }
547
+ return [
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");
552
+ }
553
+ function toOpenAiTools(tools) {
554
+ return tools.map((tool) => ({
555
+ type: "function",
556
+ function: {
557
+ name: tool.exposedName,
558
+ description: tool.description || tool.toolName,
559
+ parameters: tool.inputSchema,
560
+ },
561
+ }));
562
+ }
563
+ function normalizeToolName(serverName, toolName) {
564
+ const normalized = `${serverName}__${toolName}`.replace(/[^a-zA-Z0-9_-]/g, "_");
565
+ return normalized.slice(0, 64) || "mcp_tool";
566
+ }
567
+ function buildRequestHeaders(provider) {
568
+ const headers = {
569
+ "Content-Type": "application/json",
570
+ };
571
+ if (provider.apiKey) {
572
+ headers.Authorization = `Bearer ${provider.apiKey}`;
573
+ }
574
+ return headers;
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
+ }
607
+ export class ACodeRuntime extends EventEmitter {
608
+ constructor(config) {
609
+ super();
610
+ this.config = config;
611
+ this.state = {
612
+ started: false,
613
+ terminated: false,
614
+ configMcpCount: 0,
615
+ skills: [],
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: [] },
626
+ mcpConnections: [],
627
+ exposedTools: [],
628
+ commands: [],
629
+ commandRegistry: new ACodeCommandRegistry([]),
630
+ conversation: [],
631
+ };
632
+ this.abortController = null;
633
+ this.submitQueue = Promise.resolve();
634
+ this.activeTurn = false;
635
+ this.abortRequested = false;
636
+ this.sessionCompleteEmitted = false;
637
+ this.hookBus = new ACodeHookBus({
638
+ sessionId: config.sessionId,
639
+ onError: (event, error) => {
640
+ this.emitAcodeEvent("error", {
641
+ message: `ACode hook failed: ${event.name}: ${error instanceof Error ? error.message : String(error)}`,
642
+ });
643
+ },
644
+ });
645
+ for (const handler of config.hooks ?? []) {
646
+ this.hookBus.register(handler);
647
+ }
648
+ }
649
+ emitAcodeEvent(type, data) {
650
+ const event = {
651
+ type,
652
+ sessionId: this.config.sessionId,
653
+ timestamp: nowIso(),
654
+ data,
655
+ };
656
+ this.emit("event", event);
657
+ }
658
+ /**
659
+ * 启动 ACode headless 会话。
660
+ * 主流程:加载 `.acode/config.json` 与 skills 目录 -> 发出 MCP/Skill 状态 ->
661
+ * 如有初始 prompt 则执行一轮响应。
662
+ */
663
+ async start() {
664
+ const acodeConfig = await loadAcodeConfig(this.config.configPath);
665
+ const skills = await loadAcodeSkills(this.config.skillsDir);
666
+ const provider = resolveProviderConfig(this.config, acodeConfig);
667
+ const permissions = resolvePermissionConfig(this.config, acodeConfig);
668
+ const mcpConnections = await this.connectMcpServers(acodeConfig.mcpServers ?? {}, provider.mcpToolTimeoutMs);
669
+ const builtInTools = createBuiltInFileTools(this.config.workingDirectory);
670
+ const exposedTools = [
671
+ ...builtInTools,
672
+ ...mcpConnections.flatMap((connection) => connection.tools),
673
+ ];
674
+ const commandDirs = this.resolveCommandDirs(acodeConfig);
675
+ const commands = await loadAcodeCommands(commandDirs);
676
+ const commandRegistry = new ACodeCommandRegistry(commands);
677
+ const skillPrompt = buildSkillPrompt(skills);
678
+ const commandPrompt = buildCommandPrompt(commands);
679
+ const systemPrompt = [provider.systemPrompt, skillPrompt, commandPrompt]
680
+ .map((item) => item?.trim())
681
+ .filter((item) => Boolean(item))
682
+ .join("\n\n");
683
+ this.state = {
684
+ started: true,
685
+ terminated: false,
686
+ configMcpCount: Object.keys(acodeConfig.mcpServers ?? {}).length,
687
+ skills,
688
+ provider,
689
+ permissions,
690
+ mcpConnections,
691
+ exposedTools,
692
+ commands,
693
+ commandRegistry,
694
+ conversation: systemPrompt
695
+ ? [{ role: "system", content: systemPrompt }]
696
+ : [],
697
+ };
698
+ await this.hookBus.dispatch("SessionStart", {
699
+ workingDirectory: this.config.workingDirectory,
700
+ model: provider.model,
701
+ commandCount: commands.length,
702
+ });
703
+ this.emitAcodeEvent("session_started", {
704
+ workingDirectory: this.config.workingDirectory,
705
+ model: provider.model,
706
+ commandCount: commands.length,
707
+ });
708
+ this.emitAcodeEvent("mcp_status", {
709
+ configPath: this.config.configPath,
710
+ installedCount: this.state.configMcpCount,
711
+ servers: Object.keys(acodeConfig.mcpServers ?? {}),
712
+ connectedCount: mcpConnections.length,
713
+ builtInToolCount: builtInTools.length,
714
+ toolCount: exposedTools.length,
715
+ permissionMode: permissions.mode,
716
+ allowedToolRules: permissions.allow,
717
+ deniedToolRules: permissions.deny,
718
+ tools: exposedTools.map((tool) => ({
719
+ name: tool.exposedName,
720
+ source: tool.kind === "mcp"
721
+ ? `${tool.serverName}.${tool.toolName}`
722
+ : `builtin.${tool.toolName}`,
723
+ })),
724
+ });
725
+ this.emitAcodeEvent("skill_status", {
726
+ skillsDir: this.config.skillsDir,
727
+ installedCount: skills.length,
728
+ skills: skills.map((skill) => ({
729
+ name: skill.name,
730
+ description: skill.description,
731
+ })),
732
+ });
733
+ this.emitAcodeEvent("command_status", {
734
+ action: "commands_loaded",
735
+ installedCommandCount: commands.length,
736
+ commandDirs,
737
+ commands: commands.map((command) => ({
738
+ name: command.name,
739
+ description: command.description,
740
+ })),
741
+ });
742
+ if (this.config.initialPrompt) {
743
+ await this.submit(this.config.initialPrompt);
744
+ }
745
+ }
746
+ /**
747
+ * 处理一轮用户输入。
748
+ * 主流程:将用户输入追加到会话 -> 调用 OpenAI-compatible 模型 -> 如模型请求
749
+ * MCP tool call 则执行工具并继续同一轮,最后输出 assistant 文本与 usage。
750
+ */
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) {
757
+ if (!this.state.started || this.state.terminated) {
758
+ throw new Error("ACode runtime is not active");
759
+ }
760
+ this.activeTurn = true;
761
+ this.abortRequested = false;
762
+ const originalPrompt = message.trim();
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 });
798
+ const result = await this.runModelTurn(commandAllowedTools);
799
+ if (result.text && !this.state.provider.stream) {
800
+ this.emitAcodeEvent("assistant_delta", { text: result.text });
801
+ }
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 } : {}),
809
+ });
810
+ }
811
+ catch (error) {
812
+ if (this.abortRequested) {
813
+ this.emitAcodeEvent("turn_complete", { aborted: true });
814
+ return;
815
+ }
816
+ this.emitAcodeEvent("error", {
817
+ message: error instanceof Error ? error.message : String(error),
818
+ });
819
+ this.emitAcodeEvent("turn_complete", { failed: true });
820
+ }
821
+ finally {
822
+ this.activeTurn = false;
823
+ this.abortRequested = false;
824
+ }
825
+ }
826
+ /** 中止当前轮次;由执行轮次统一发出一个终态事件,避免 aborted/failed 双终态。 */
827
+ async abort() {
828
+ this.abortRequested = this.activeTurn;
829
+ this.abortController?.abort();
830
+ this.abortController = null;
831
+ }
832
+ /** 终止 ACode 会话并发出 session_complete。 */
833
+ async stop() {
834
+ if (this.state.terminated) {
835
+ return;
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
+ });
844
+ await this.disconnectMcpServers();
845
+ await runBestEffort("ACode Stop hooks", this.hookBus.dispatch("Stop", { reason: "session_stop" }), (message) => {
846
+ this.emitAcodeEvent("error", { message });
847
+ });
848
+ this.state.terminated = true;
849
+ if (!this.sessionCompleteEmitted) {
850
+ this.sessionCompleteEmitted = true;
851
+ this.emitAcodeEvent("session_complete", { exitCode: 0 });
852
+ }
853
+ }
854
+ resolveCommandDirs(acodeConfig) {
855
+ const configured = [
856
+ ...(this.config.commandDirs ?? []),
857
+ ...(acodeConfig.commandDirs ?? []),
858
+ ...(acodeConfig.commands?.directories ?? []),
859
+ ];
860
+ return [
861
+ ...new Set(configured.map((directory) => directory.trim()).filter(Boolean)),
862
+ ];
863
+ }
864
+ async runModelTurn(commandAllowedTools) {
865
+ let finalText = "";
866
+ let finalUsage;
867
+ let finalResponseBody;
868
+ let finalFinishReason;
869
+ for (let step = 0; step < 5; step += 1) {
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
+ }
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;
880
+ const message = choice?.message;
881
+ const content = typeof message?.content === "string" ? message.content : "";
882
+ const toolCalls = Array.isArray(message?.tool_calls)
883
+ ? message.tool_calls
884
+ : [];
885
+ finalUsage = {
886
+ inputTokens: response.usage?.prompt_tokens ?? response.usage?.input_tokens ?? 0,
887
+ outputTokens: response.usage?.completion_tokens ??
888
+ response.usage?.output_tokens ??
889
+ 0,
890
+ };
891
+ this.state.conversation.push({
892
+ role: "assistant",
893
+ content: content || null,
894
+ tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
895
+ });
896
+ if (content) {
897
+ finalText = content;
898
+ }
899
+ if (toolCalls.length === 0) {
900
+ return {
901
+ text: finalText,
902
+ usage: finalUsage,
903
+ responseBody: finalResponseBody,
904
+ finishReason: finalFinishReason,
905
+ };
906
+ }
907
+ await this.handleToolCalls(toolCalls, commandAllowedTools);
908
+ }
909
+ return {
910
+ text: finalText || "ACode 已达到本轮 MCP 工具调用上限。",
911
+ usage: finalUsage,
912
+ responseBody: finalResponseBody,
913
+ finishReason: finalFinishReason,
914
+ };
915
+ }
916
+ async requestChatCompletion(commandAllowedTools) {
917
+ const provider = this.state.provider;
918
+ const baseURL = provider.baseURL;
919
+ if (!baseURL) {
920
+ throw new Error("ACode provider baseURL is not configured");
921
+ }
922
+ const body = {
923
+ model: provider.model,
924
+ messages: this.state.conversation,
925
+ stream: provider.stream,
926
+ };
927
+ const allowedTools = this.state.exposedTools.filter((tool) => isToolAllowed(tool, this.state.permissions, commandAllowedTools));
928
+ if (allowedTools.length > 0) {
929
+ body.tools = toOpenAiTools(allowedTools);
930
+ body.tool_choice = "auto";
931
+ }
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),
940
+ });
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
+ }
997
+ }
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
+ };
1063
+ }
1064
+ async handleToolCalls(toolCalls, commandAllowedTools) {
1065
+ for (const toolCall of toolCalls) {
1066
+ const tool = this.state.exposedTools.find((item) => item.exposedName === toolCall.function.name);
1067
+ if (!tool) {
1068
+ this.state.conversation.push({
1069
+ role: "tool",
1070
+ tool_call_id: toolCall.id,
1071
+ content: `ACode MCP tool not found: ${toolCall.function.name}`,
1072
+ });
1073
+ continue;
1074
+ }
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
+ }
1130
+ const toolArguments = coerceArguments(toolCall.function.arguments);
1131
+ assertToolPermission(tool, this.state.permissions, commandAllowedTools);
1132
+ const preResults = await this.hookBus.dispatch("PreToolUse", {
1133
+ source: "mcp",
1134
+ serverName: tool.serverName,
1135
+ toolName: tool.toolName,
1136
+ exposedName: tool.exposedName,
1137
+ arguments: toolArguments,
1138
+ });
1139
+ const denied = findDeniedHookResult(preResults);
1140
+ if (denied) {
1141
+ throw new Error(denied.message || `ACode tool denied by hook: ${tool.exposedName}`);
1142
+ }
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,
1162
+ toolName: tool.toolName,
1163
+ exposedName: tool.exposedName,
1164
+ result: toToolResultText(result),
1165
+ });
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,
1172
+ });
1173
+ this.state.conversation.push({
1174
+ role: "tool",
1175
+ tool_call_id: toolCall.id,
1176
+ content: resultText,
1177
+ });
1178
+ await this.appendSyntheticMultimodalUserMessage(resultText);
1179
+ }
1180
+ catch (error) {
1181
+ this.appendToolFailureResult(toolCall, tool, error);
1182
+ }
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.",
1193
+ });
1194
+ return;
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
+ });
1216
+ }
1217
+ async connectMcpServers(servers, timeoutMs) {
1218
+ const connections = [];
1219
+ for (const [serverName, serverSpec] of Object.entries(servers)) {
1220
+ try {
1221
+ const client = new Client({ name: "acode", version: "0.1.0" });
1222
+ const transport = this.createMcpTransport(serverSpec);
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`);
1225
+ connections.push({
1226
+ serverName,
1227
+ client,
1228
+ transport,
1229
+ tools: tools.map((tool) => ({
1230
+ serverName,
1231
+ toolName: tool.name,
1232
+ kind: "mcp",
1233
+ exposedName: normalizeToolName(serverName, tool.name),
1234
+ description: tool.description,
1235
+ inputSchema: isJsonObject(tool.inputSchema)
1236
+ ? tool.inputSchema
1237
+ : { type: "object", properties: {} },
1238
+ })),
1239
+ });
1240
+ }
1241
+ catch (error) {
1242
+ this.emitAcodeEvent("error", {
1243
+ message: `ACode MCP server failed to connect: ${serverName}: ${error instanceof Error ? error.message : String(error)}`,
1244
+ });
1245
+ }
1246
+ }
1247
+ return connections;
1248
+ }
1249
+ createMcpTransport(serverSpec) {
1250
+ if (serverSpec.type === "stdio" || serverSpec.command) {
1251
+ if (!serverSpec.command) {
1252
+ throw new Error("stdio MCP server requires command");
1253
+ }
1254
+ return new StdioClientTransport({
1255
+ command: serverSpec.command,
1256
+ args: serverSpec.args,
1257
+ cwd: serverSpec.cwd || this.config.workingDirectory,
1258
+ env: { ...(this.config.env ?? {}), ...(serverSpec.env ?? {}) },
1259
+ stderr: "pipe",
1260
+ });
1261
+ }
1262
+ if (!serverSpec.url) {
1263
+ throw new Error("remote MCP server requires url");
1264
+ }
1265
+ const requestInit = serverSpec.headers
1266
+ ? { headers: serverSpec.headers }
1267
+ : undefined;
1268
+ if (serverSpec.type === "sse") {
1269
+ return new SSEClientTransport(new URL(serverSpec.url), { requestInit });
1270
+ }
1271
+ return new StreamableHTTPClientTransport(new URL(serverSpec.url), {
1272
+ requestInit,
1273
+ });
1274
+ }
1275
+ async disconnectMcpServers() {
1276
+ const connections = this.state.mcpConnections;
1277
+ this.state.mcpConnections = [];
1278
+ this.state.exposedTools = [];
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)));
1282
+ }
1283
+ }