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.
- package/dist/adapter/AcodeSdkAdapter.d.ts +104 -0
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -0
- package/dist/adapter/AcodeSdkAdapter.js +644 -0
- package/dist/adapter/AcodeSdkAdapter.test.d.ts +2 -0
- package/dist/adapter/AcodeSdkAdapter.test.d.ts.map +1 -0
- package/dist/adapter/AcodeSdkAdapter.test.js +1280 -0
- package/dist/adapter/AdapterRegistry.d.ts +1 -1
- package/dist/adapter/AdapterRegistry.d.ts.map +1 -1
- package/dist/adapter/AdapterRegistry.js +2 -0
- package/dist/adapter/AdapterRegistry.test.js +10 -1
- 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/SdkProviderSpi.js +10 -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/index.d.ts +1 -0
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +1 -0
- 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/adapters/cc-switch/index.d.ts +2 -0
- package/dist/adapters/cc-switch/index.d.ts.map +1 -1
- package/dist/adapters/cc-switch/index.js +6 -0
- package/dist/adapters/cc-switch/mcp-acode.d.ts +17 -0
- package/dist/adapters/cc-switch/mcp-acode.d.ts.map +1 -0
- package/dist/adapters/cc-switch/mcp-acode.js +71 -0
- package/dist/adapters/cc-switch/mcp-acode.test.d.ts +2 -0
- package/dist/adapters/cc-switch/mcp-acode.test.d.ts.map +1 -0
- package/dist/adapters/cc-switch/mcp-acode.test.js +80 -0
- package/dist/adapters/cc-switch/mcp-claude.d.ts.map +1 -1
- package/dist/adapters/cc-switch/mcp-claude.js +1 -0
- package/dist/adapters/cc-switch/mcp-claudecode.d.ts.map +1 -1
- package/dist/adapters/cc-switch/mcp-claudecode.js +1 -0
- package/dist/adapters/cc-switch/mcp-claudecode.test.js +1 -0
- package/dist/adapters/cc-switch/mcp-codex.d.ts.map +1 -1
- package/dist/adapters/cc-switch/mcp-codex.js +1 -0
- package/dist/adapters/cc-switch/mcp-codex.test.js +1 -0
- package/dist/adapters/cc-switch/mcp-opencode.d.ts.map +1 -1
- package/dist/adapters/cc-switch/mcp-opencode.js +1 -0
- package/dist/adapters/cc-switch/mcp-opencode.test.js +1 -0
- package/dist/adapters/cc-switch/skill-acode.d.ts +11 -0
- package/dist/adapters/cc-switch/skill-acode.d.ts.map +1 -0
- package/dist/adapters/cc-switch/skill-acode.js +12 -0
- package/dist/adapters/cc-switch/skill-directory.test.js +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -0
- package/dist/index.js +2 -0
- package/dist/routes/ai-sources.js +3 -2
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +7 -8
- package/dist/routes/instance.test.js +2 -1
- package/dist/routes/mcp.js +2 -2
- package/dist/routes/mcp.test.js +2 -1
- package/dist/routes/runtime-binding.d.ts +3 -2
- package/dist/routes/runtime-binding.d.ts.map +1 -1
- package/dist/routes/runtime-binding.js +11 -58
- package/dist/routes/runtime-binding.test.js +4 -80
- package/dist/routes/sessions.d.ts +5 -0
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +36 -16
- package/dist/routes/sessions.test.js +13 -0
- package/dist/routes/skills.js +2 -2
- package/dist/routes/skills.test.js +2 -1
- package/dist/routes/terminal.d.ts +27 -7
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +439 -171
- package/dist/routes/terminal.test.js +385 -197
- package/dist/services/acode-package.d.ts +8 -0
- package/dist/services/acode-package.d.ts.map +1 -0
- package/dist/services/acode-package.js +82 -0
- package/dist/services/ai-source-apply.d.ts +4 -0
- package/dist/services/ai-source-apply.d.ts.map +1 -1
- package/dist/services/ai-source-apply.js +8 -0
- package/dist/services/ai-source-apply.test.js +8 -1
- package/dist/services/cc-switch-sdk.d.ts.map +1 -1
- package/dist/services/cc-switch-sdk.js +9 -1
- package/dist/services/instance-init-service.d.ts.map +1 -1
- package/dist/services/instance-init-service.js +3 -2
- package/dist/services/instance-init-service.test.js +6 -5
- package/dist/services/instance-service.d.ts.map +1 -1
- package/dist/services/instance-service.js +1 -0
- package/dist/services/instance-state.js +1 -1
- package/dist/services/instance-state.test.js +4 -4
- 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/dist/services/tool-installer.d.ts.map +1 -1
- package/dist/services/tool-installer.js +10 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +1 -0
- package/dist/utils/mcp-utils.d.ts.map +1 -1
- package/dist/utils/mcp-utils.js +4 -1
- package/dist/utils/validation.js +1 -1
- package/node_modules/@cc-switch/sdk/dist/constants.d.ts.map +1 -1
- package/node_modules/@cc-switch/sdk/dist/constants.js +7 -0
- package/node_modules/@cc-switch/sdk/dist/schemas.d.ts.map +1 -1
- package/node_modules/@cc-switch/sdk/dist/schemas.js +1 -0
- package/node_modules/@cc-switch/sdk/dist/sdk.d.ts.map +1 -1
- package/node_modules/@cc-switch/sdk/dist/sdk.js +7 -3
- package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.d.ts +6 -0
- package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.d.ts.map +1 -1
- package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.js +27 -1
- package/node_modules/@cc-switch/sdk/dist/types.d.ts +4 -1
- package/node_modules/@cc-switch/sdk/dist/types.d.ts.map +1 -1
- package/package/acode/README.md +88 -0
- package/package/acode/dist/built-in-file-tools.d.ts +16 -0
- package/package/acode/dist/built-in-file-tools.d.ts.map +1 -0
- package/package/acode/dist/built-in-file-tools.js +569 -0
- package/package/acode/dist/cli.d.ts +3 -0
- package/package/acode/dist/cli.d.ts.map +1 -0
- package/package/acode/dist/cli.js +37 -0
- package/package/acode/dist/commands/commandLoader.d.ts +7 -0
- package/package/acode/dist/commands/commandLoader.d.ts.map +1 -0
- package/package/acode/dist/commands/commandLoader.js +33 -0
- package/package/acode/dist/commands/commandParser.d.ts +7 -0
- package/package/acode/dist/commands/commandParser.d.ts.map +1 -0
- package/package/acode/dist/commands/commandParser.js +77 -0
- package/package/acode/dist/commands/commandRegistry.d.ts +13 -0
- package/package/acode/dist/commands/commandRegistry.d.ts.map +1 -0
- package/package/acode/dist/commands/commandRegistry.js +30 -0
- package/package/acode/dist/config.d.ts +4 -0
- package/package/acode/dist/config.d.ts.map +1 -0
- package/package/acode/dist/config.js +80 -0
- package/package/acode/dist/hooks/hookBus.d.ts +19 -0
- package/package/acode/dist/hooks/hookBus.d.ts.map +1 -0
- package/package/acode/dist/hooks/hookBus.js +40 -0
- package/package/acode/dist/index.d.ts +8 -0
- package/package/acode/dist/index.d.ts.map +1 -0
- package/package/acode/dist/index.js +7 -0
- package/package/acode/dist/runtime.d.ts +42 -0
- package/package/acode/dist/runtime.d.ts.map +1 -0
- package/package/acode/dist/runtime.js +1283 -0
- package/package/acode/dist/types.d.ts +112 -0
- package/package/acode/dist/types.d.ts.map +1 -0
- package/package/acode/dist/types.js +1 -0
- package/package/acode/package.json +24 -0
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts +4 -4
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.js +9 -6
- package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.test.js +21 -0
- package/package/aws-client-agent-mcp/dist/agent-client.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts +1 -17
- package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.js +7 -113
- package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.test.js +22 -2
- package/package/aws-client-agent-mcp/dist/http-client.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +0 -5
- 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 -24
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +90 -5
- 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 +4 -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 +0 -13
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.js +5 -100
- 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/cc-switch-sdk/dist/constants.d.ts.map +1 -1
- package/package/cc-switch-sdk/dist/constants.js +7 -0
- package/package/cc-switch-sdk/dist/schemas.d.ts.map +1 -1
- package/package/cc-switch-sdk/dist/schemas.js +1 -0
- package/package/cc-switch-sdk/dist/sdk.d.ts.map +1 -1
- package/package/cc-switch-sdk/dist/sdk.js +7 -3
- package/package/cc-switch-sdk/dist/services/ai-config-service.d.ts +6 -0
- package/package/cc-switch-sdk/dist/services/ai-config-service.d.ts.map +1 -1
- package/package/cc-switch-sdk/dist/services/ai-config-service.js +27 -1
- package/package/cc-switch-sdk/dist/types.d.ts +4 -1
- package/package/cc-switch-sdk/dist/types.d.ts.map +1 -1
- package/package.json +8 -4
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACode SDK Adapter
|
|
3
|
+
*
|
|
4
|
+
* 主流程:直接加载随 aws-runtime-bridge 内置发布的 ACode headless runtime,
|
|
5
|
+
* 不依赖外部 CLI 安装;将 ACode 的结构化事件映射为 runtime-bridge 统一事件流。
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { v4 as uuidv4 } from "uuid";
|
|
11
|
+
import { getAcodeConfigFile, getAcodeSkillsDir, getRuntimeHomeDir, } from "../config.js";
|
|
12
|
+
import { getReleasedAcodeEntryPath } from "../services/acode-package.js";
|
|
13
|
+
import { IdlePollLoop } from "./idle-poll-loop.js";
|
|
14
|
+
import { getCanonicalMcpDetail, getToolActionInfo, } from "./types.js";
|
|
15
|
+
const ACODE_AUTO_POLL_PROMPT = [
|
|
16
|
+
"Continue the AgentsWork message loop now.",
|
|
17
|
+
"You MUST call the exact MCP tool named aws-mcp__poll_message as your next action and wait for the next assigned message or task.",
|
|
18
|
+
"Do not answer in plain text before calling aws-mcp__poll_message.",
|
|
19
|
+
"After handling any received work, reply through the appropriate aws-mcp messaging tool, then continue listening again.",
|
|
20
|
+
].join(" ");
|
|
21
|
+
const ACODE_AUTO_POLL_DELAY_MS = 250;
|
|
22
|
+
// Backoff used when a polling-driven turn fails (e.g., 60s MCP tool timeout,
|
|
23
|
+
// model 5xx, provider unreachable). Larger than ACODE_AUTO_POLL_DELAY_MS to
|
|
24
|
+
// avoid busy-looping when the same failure recurs on every retry.
|
|
25
|
+
const ACODE_AUTO_POLL_FAILURE_DELAY_MS = 2000;
|
|
26
|
+
const ACODE_MESSAGE_POLLING_IDLE_PROMPT = "系统提醒:当前工作已完成或运行时已进入空闲状态。请立即调用 poll_message 工具阻塞等待新消息和流程线任务提醒;收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理;完成任务后调用 submit_task_result 提交结果,然后继续 poll_message。";
|
|
27
|
+
function isMessagePollingIdleCommand(command) {
|
|
28
|
+
const normalized = command.trim();
|
|
29
|
+
if (!normalized)
|
|
30
|
+
return false;
|
|
31
|
+
const lower = normalized.toLowerCase();
|
|
32
|
+
return (lower.includes("poll_message") ||
|
|
33
|
+
lower.includes("get_message") ||
|
|
34
|
+
normalized.includes("取消息"));
|
|
35
|
+
}
|
|
36
|
+
function getAcodePackageEntryPath() {
|
|
37
|
+
return path.resolve(getReleasedAcodeEntryPath());
|
|
38
|
+
}
|
|
39
|
+
export function toAcodeEsmImportSpecifier(modulePath) {
|
|
40
|
+
return pathToFileURL(path.resolve(modulePath)).href;
|
|
41
|
+
}
|
|
42
|
+
function toUsage(value) {
|
|
43
|
+
if (!value || typeof value !== "object") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const usage = value;
|
|
47
|
+
const inputTokens = toFiniteTokenCount(usage.inputTokens, usage.input_tokens, usage.promptTokens, usage.prompt_tokens);
|
|
48
|
+
const outputTokens = toFiniteTokenCount(usage.outputTokens, usage.output_tokens, usage.completionTokens, usage.completion_tokens);
|
|
49
|
+
return {
|
|
50
|
+
inputTokens,
|
|
51
|
+
outputTokens,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function toFiniteTokenCount(...values) {
|
|
55
|
+
for (const value of values) {
|
|
56
|
+
const count = Number(value);
|
|
57
|
+
if (Number.isFinite(count) && count > 0) {
|
|
58
|
+
return count;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check whether an ACode turn_complete event represents a failed turn.
|
|
65
|
+
* Main flow: only explicit failed=true prevents error status from being reset to waiting_input.
|
|
66
|
+
*/
|
|
67
|
+
export function isFailedAcodeTurnComplete(data) {
|
|
68
|
+
return data.failed === true;
|
|
69
|
+
}
|
|
70
|
+
function toAcodeMcpActionDetail(actionDetail, server, tool) {
|
|
71
|
+
if (!actionDetail) {
|
|
72
|
+
return tool ? `${server}/${tool}` : server;
|
|
73
|
+
}
|
|
74
|
+
const prefix = `${server}__`;
|
|
75
|
+
if (actionDetail.startsWith(prefix)) {
|
|
76
|
+
return getCanonicalMcpDetail(`${server}__${actionDetail.slice(prefix.length)}`);
|
|
77
|
+
}
|
|
78
|
+
return getCanonicalMcpDetail(actionDetail.replace("/", "__"));
|
|
79
|
+
}
|
|
80
|
+
export class AcodeSdkAdapter extends EventEmitter {
|
|
81
|
+
constructor() {
|
|
82
|
+
super(...arguments);
|
|
83
|
+
this.providerId = "acode";
|
|
84
|
+
this.displayName = "ACode";
|
|
85
|
+
this.sessions = new Map();
|
|
86
|
+
}
|
|
87
|
+
async startSession(sessionId, config) {
|
|
88
|
+
const runtimeHome = getRuntimeHomeDir();
|
|
89
|
+
const session = {
|
|
90
|
+
sessionId,
|
|
91
|
+
status: "starting",
|
|
92
|
+
messages: [],
|
|
93
|
+
createdAt: new Date().toISOString(),
|
|
94
|
+
totalUsage: { inputTokens: 0, outputTokens: 0 },
|
|
95
|
+
};
|
|
96
|
+
this.sessions.set(sessionId, {
|
|
97
|
+
adapterSession: session,
|
|
98
|
+
config,
|
|
99
|
+
runtime: await this.createRuntime(sessionId, config, runtimeHome),
|
|
100
|
+
autoPollActive: false,
|
|
101
|
+
});
|
|
102
|
+
this.emit("status-change", sessionId, "starting");
|
|
103
|
+
const entry = this.sessions.get(sessionId);
|
|
104
|
+
if (!entry) {
|
|
105
|
+
throw new Error("ACode session was not initialized");
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await entry.runtime.start();
|
|
109
|
+
session.status = "waiting_input";
|
|
110
|
+
this.emit("status-change", sessionId, "waiting_input");
|
|
111
|
+
if (config.initialPrompt?.trim()) {
|
|
112
|
+
this.startInitialPromptTurn(sessionId, config.initialPrompt);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// R1 fix: when no initial prompt is provided, explicitly arm the
|
|
116
|
+
// auto-poll loop. Without this, the session sits at 'waiting_input'
|
|
117
|
+
// forever if setIdleCommands hasn't been called yet (or is configured
|
|
118
|
+
// with a non-polling command). setIdleCommands will cancel this timer
|
|
119
|
+
// and replace it with the configured idle loop when invoked.
|
|
120
|
+
this.armIdleLoopOrAutoPoll(sessionId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
session.status = "error";
|
|
125
|
+
this.emitRuntimeErrorEvent(sessionId, error);
|
|
126
|
+
this.emit("status-change", sessionId, "error");
|
|
127
|
+
this.sessions.delete(sessionId);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async sendMessage(sessionId, message) {
|
|
132
|
+
const entry = this.requireSession(sessionId);
|
|
133
|
+
this.cancelScheduledAutoPoll(entry);
|
|
134
|
+
entry.failedTurnRecoveryScheduled = false;
|
|
135
|
+
entry.idlePollLoop?.stop();
|
|
136
|
+
const userMessage = {
|
|
137
|
+
id: uuidv4(),
|
|
138
|
+
sessionId,
|
|
139
|
+
role: "user",
|
|
140
|
+
content: message,
|
|
141
|
+
timestamp: new Date().toISOString(),
|
|
142
|
+
};
|
|
143
|
+
entry.adapterSession.messages.push(userMessage);
|
|
144
|
+
this.emit("conversation-message", sessionId, userMessage);
|
|
145
|
+
entry.adapterSession.status = "thinking";
|
|
146
|
+
this.emit("status-change", sessionId, "thinking");
|
|
147
|
+
try {
|
|
148
|
+
await entry.runtime.submit(message);
|
|
149
|
+
this.recoverResolvedTurnWithoutTerminalEvent(sessionId);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.emitRuntimeError(sessionId, error, { recover: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async sendConfirmation(_sessionId, _accept) { }
|
|
156
|
+
async abortCurrentTurn(sessionId) {
|
|
157
|
+
await this.requireSession(sessionId).runtime.abort();
|
|
158
|
+
}
|
|
159
|
+
async terminateSession(sessionId) {
|
|
160
|
+
const entry = this.sessions.get(sessionId);
|
|
161
|
+
if (!entry) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.cancelScheduledAutoPoll(entry);
|
|
165
|
+
entry.failedTurnRecoveryScheduled = false;
|
|
166
|
+
entry.idlePollLoop?.stop();
|
|
167
|
+
await entry.runtime.stop();
|
|
168
|
+
entry.adapterSession.status = "terminated";
|
|
169
|
+
this.sessions.delete(sessionId);
|
|
170
|
+
this.emit("status-change", sessionId, "terminated");
|
|
171
|
+
}
|
|
172
|
+
getConversation(sessionId) {
|
|
173
|
+
return this.sessions.get(sessionId)?.adapterSession.messages ?? [];
|
|
174
|
+
}
|
|
175
|
+
hasSession(sessionId) {
|
|
176
|
+
return this.sessions.has(sessionId);
|
|
177
|
+
}
|
|
178
|
+
getProviderSessionId(sessionId) {
|
|
179
|
+
return this.sessions.has(sessionId) ? sessionId : undefined;
|
|
180
|
+
}
|
|
181
|
+
getSessionStatus(sessionId) {
|
|
182
|
+
return this.sessions.get(sessionId)?.adapterSession.status;
|
|
183
|
+
}
|
|
184
|
+
cleanup() {
|
|
185
|
+
for (const sessionId of this.sessions.keys()) {
|
|
186
|
+
void this.terminateSession(sessionId);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
setIdleCommands(sessionId, commands) {
|
|
190
|
+
const entry = this.sessions.get(sessionId);
|
|
191
|
+
if (!entry)
|
|
192
|
+
return;
|
|
193
|
+
entry.idleCommands = commands;
|
|
194
|
+
this.cancelScheduledAutoPoll(entry);
|
|
195
|
+
if (commands.idleInputCommand &&
|
|
196
|
+
isMessagePollingIdleCommand(commands.idleInputCommand)) {
|
|
197
|
+
const loop = this.getIdlePollLoop(entry);
|
|
198
|
+
loop.start();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (entry.adapterSession.status === "waiting_input" &&
|
|
202
|
+
(commands.idleInputCommand || commands.nonInputCommand)) {
|
|
203
|
+
this.getIdlePollLoop(entry).start();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** Injects a runtime constructor for adapter tests without loading the packaged ACode module. */
|
|
207
|
+
setRuntimeConstructorForTest(runtimeConstructor) {
|
|
208
|
+
this.runtimeConstructor = runtimeConstructor;
|
|
209
|
+
}
|
|
210
|
+
async createRuntime(sessionId, config, runtimeHome) {
|
|
211
|
+
const ACodeRuntime = await this.loadRuntimeConstructor();
|
|
212
|
+
const runtime = new ACodeRuntime({
|
|
213
|
+
sessionId,
|
|
214
|
+
workingDirectory: config.workingDirectory,
|
|
215
|
+
configPath: config.mcpConfigPath || getAcodeConfigFile(runtimeHome),
|
|
216
|
+
skillsDir: getAcodeSkillsDir(runtimeHome),
|
|
217
|
+
model: config.model,
|
|
218
|
+
systemPrompt: typeof config.systemPrompt === "string"
|
|
219
|
+
? config.systemPrompt
|
|
220
|
+
: undefined,
|
|
221
|
+
env: config.envOverrides,
|
|
222
|
+
});
|
|
223
|
+
runtime.on("event", (event) => this.forwardAcodeEvent(event));
|
|
224
|
+
return runtime;
|
|
225
|
+
}
|
|
226
|
+
async loadRuntimeConstructor() {
|
|
227
|
+
if (this.runtimeConstructor) {
|
|
228
|
+
return this.runtimeConstructor;
|
|
229
|
+
}
|
|
230
|
+
const modulePath = getAcodePackageEntryPath();
|
|
231
|
+
const module = (await import(toAcodeEsmImportSpecifier(modulePath)));
|
|
232
|
+
if (!module.ACodeRuntime) {
|
|
233
|
+
throw new Error(`ACode runtime export not found: ${modulePath}`);
|
|
234
|
+
}
|
|
235
|
+
this.runtimeConstructor = module.ACodeRuntime;
|
|
236
|
+
return this.runtimeConstructor;
|
|
237
|
+
}
|
|
238
|
+
requireSession(sessionId) {
|
|
239
|
+
const entry = this.sessions.get(sessionId);
|
|
240
|
+
if (!entry) {
|
|
241
|
+
throw new Error("ACode session not found");
|
|
242
|
+
}
|
|
243
|
+
return entry;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Run the startup prompt after the HTTP launch has already completed.
|
|
247
|
+
* Main flow: mark the session busy -> submit the prompt in the background ->
|
|
248
|
+
* rely on normal ACode events to publish turn completion or errors.
|
|
249
|
+
*/
|
|
250
|
+
startInitialPromptTurn(sessionId, initialPrompt) {
|
|
251
|
+
const entry = this.sessions.get(sessionId);
|
|
252
|
+
if (!entry) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
entry.adapterSession.status = "thinking";
|
|
256
|
+
this.emit("status-change", sessionId, "thinking");
|
|
257
|
+
void entry.runtime
|
|
258
|
+
.submit(initialPrompt)
|
|
259
|
+
.then(() => {
|
|
260
|
+
this.recoverResolvedTurnWithoutTerminalEvent(sessionId);
|
|
261
|
+
})
|
|
262
|
+
.catch((error) => {
|
|
263
|
+
const currentEntry = this.sessions.get(sessionId);
|
|
264
|
+
if (!currentEntry) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// R4 fix: a failed initial prompt turn used to leave the session
|
|
268
|
+
// permanently in 'error' because autoPollActive was false at this
|
|
269
|
+
// point (only set by submitAutoPollPrompt), so the R2 recovery in
|
|
270
|
+
// forwardAcodeEvent skipped it, and no idle loop was armed.
|
|
271
|
+
// Now we always recover to 'waiting_input' and arm the idle loop
|
|
272
|
+
// so the agent can retry via poll_message.
|
|
273
|
+
this.emitRuntimeError(sessionId, error, { recover: true });
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
forwardAcodeEvent(event) {
|
|
277
|
+
const entry = this.sessions.get(event.sessionId);
|
|
278
|
+
if (!entry) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (event.type === "assistant_delta") {
|
|
282
|
+
const text = typeof event.data.text === "string" ? event.data.text : "";
|
|
283
|
+
entry.adapterSession.messages.push({
|
|
284
|
+
id: uuidv4(),
|
|
285
|
+
sessionId: event.sessionId,
|
|
286
|
+
role: "assistant",
|
|
287
|
+
content: text,
|
|
288
|
+
timestamp: event.timestamp,
|
|
289
|
+
});
|
|
290
|
+
this.emitProviderEvent(event, "text_delta", { text });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (event.type === "thinking_delta") {
|
|
294
|
+
const text = typeof event.data.text === "string" ? event.data.text : "";
|
|
295
|
+
this.emitProviderEvent(event, "thinking", { text });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (event.type === "ai_request") {
|
|
299
|
+
this.emitProviderEvent(event, "ai_request", {
|
|
300
|
+
model: typeof event.data.model === "string" ? event.data.model : undefined,
|
|
301
|
+
endpoint: typeof event.data.endpoint === "string"
|
|
302
|
+
? event.data.endpoint
|
|
303
|
+
: undefined,
|
|
304
|
+
stream: typeof event.data.stream === "boolean"
|
|
305
|
+
? event.data.stream
|
|
306
|
+
: undefined,
|
|
307
|
+
messageCount: typeof event.data.messageCount === "number"
|
|
308
|
+
? event.data.messageCount
|
|
309
|
+
: undefined,
|
|
310
|
+
toolCount: typeof event.data.toolCount === "number"
|
|
311
|
+
? event.data.toolCount
|
|
312
|
+
: undefined,
|
|
313
|
+
lastUserMessagePreview: typeof event.data.lastUserMessagePreview === "string"
|
|
314
|
+
? event.data.lastUserMessagePreview
|
|
315
|
+
: undefined,
|
|
316
|
+
requestBody: typeof event.data.requestBody === "string"
|
|
317
|
+
? event.data.requestBody
|
|
318
|
+
: undefined,
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (event.type === "turn_complete") {
|
|
323
|
+
const usage = toUsage(event.data.usage);
|
|
324
|
+
if (usage) {
|
|
325
|
+
entry.adapterSession.totalUsage.inputTokens += usage.inputTokens;
|
|
326
|
+
entry.adapterSession.totalUsage.outputTokens += usage.outputTokens;
|
|
327
|
+
}
|
|
328
|
+
if (isFailedAcodeTurnComplete(event.data)) {
|
|
329
|
+
// R2 fix: a failed polling-driven turn (e.g., 60s MCP tool timeout,
|
|
330
|
+
// model 5xx, provider unreachable) used to leave the session parked
|
|
331
|
+
// in 'error' or 'thinking' forever, so the agent stopped calling
|
|
332
|
+
// poll_message permanently. Recover to 'waiting_input' and re-arm
|
|
333
|
+
// the auto-poll with a longer backoff so a transient failure
|
|
334
|
+
// doesn't break the message loop.
|
|
335
|
+
//
|
|
336
|
+
// R4 fix: previously this only recovered when autoPollActive was
|
|
337
|
+
// true (i.e. during a submitAutoPollPrompt-driven turn). When the
|
|
338
|
+
// initial prompt turn failed, autoPollActive was false and the
|
|
339
|
+
// session got permanently stuck in 'error'. Now we always recover
|
|
340
|
+
// for non-terminal sessions so the idle loop can retry.
|
|
341
|
+
this.recoverTurnFailure(event.sessionId, entry);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.emitProviderEvent(event, "turn_complete", {
|
|
345
|
+
usage: entry.adapterSession.totalUsage,
|
|
346
|
+
responseBody: typeof event.data.responseBody === "string"
|
|
347
|
+
? event.data.responseBody
|
|
348
|
+
: undefined,
|
|
349
|
+
finishReason: typeof event.data.finishReason === "string"
|
|
350
|
+
? event.data.finishReason
|
|
351
|
+
: undefined,
|
|
352
|
+
});
|
|
353
|
+
entry.adapterSession.status = "waiting_input";
|
|
354
|
+
this.emit("status-change", event.sessionId, "waiting_input", {
|
|
355
|
+
usage: entry.adapterSession.totalUsage,
|
|
356
|
+
});
|
|
357
|
+
this.armIdleLoopOrAutoPoll(event.sessionId);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (event.type === "session_complete") {
|
|
361
|
+
this.cancelScheduledAutoPoll(entry);
|
|
362
|
+
entry.failedTurnRecoveryScheduled = false;
|
|
363
|
+
entry.idlePollLoop?.stop();
|
|
364
|
+
entry.adapterSession.status = "completed";
|
|
365
|
+
this.emitProviderEvent(event, "session_complete", {
|
|
366
|
+
exitCode: Number(event.data.exitCode) || 0,
|
|
367
|
+
});
|
|
368
|
+
this.emit("status-change", event.sessionId, "completed");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (event.type === "error") {
|
|
372
|
+
entry.adapterSession.status = "error";
|
|
373
|
+
this.emitProviderEvent(event, "error", {
|
|
374
|
+
text: String(event.data.message || "ACode runtime error"),
|
|
375
|
+
});
|
|
376
|
+
this.emit("status-change", event.sessionId, "error");
|
|
377
|
+
if (!entry.autoPollActive) {
|
|
378
|
+
this.recoverTurnFailure(event.sessionId, entry);
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (event.type === "mcp_status") {
|
|
383
|
+
// R3 fix: the ACode runtime emits a startup mcp_status event with no
|
|
384
|
+
// `action` field but containing a `tools` array describing what the
|
|
385
|
+
// LLM can actually call. Validate that a poll_message tool is exposed,
|
|
386
|
+
// otherwise the message loop cannot continue.
|
|
387
|
+
//
|
|
388
|
+
// The previous version of this check matched the literal string
|
|
389
|
+
// 'aws-mcp' against the user-configured MCP server keys in the
|
|
390
|
+
// `servers` field, which produced false positives when the operator
|
|
391
|
+
// named the entry differently (e.g. 'aws-mcp-server' or 'agent-mcp')
|
|
392
|
+
// while still exposing aws-mcp's tools to the LLM. Worse, on a
|
|
393
|
+
// mismatch it emitted a generic `type: 'error'` ProviderEvent, which
|
|
394
|
+
// the rest of the bridge promotes to a fatal session-level 'error'
|
|
395
|
+
// status that the dashboard cannot recover from — the LLM keeps
|
|
396
|
+
// working but the timeline header gets stuck on "发生错误" forever.
|
|
397
|
+
//
|
|
398
|
+
// The fix below validates against the actual `tools` list (the same
|
|
399
|
+
// tools the LLM sees) and on a missing poll_message it only logs a
|
|
400
|
+
// warning. The runtime will naturally emit a real 'error' event the
|
|
401
|
+
// first time the LLM tries to call a missing tool, which the existing
|
|
402
|
+
// error handling already routes correctly without permanently locking
|
|
403
|
+
// the session status.
|
|
404
|
+
if (!event.data.action && Array.isArray(event.data.tools)) {
|
|
405
|
+
const tools = event.data.tools;
|
|
406
|
+
const hasPollMessage = tools.some((tool) => typeof tool?.name === "string" &&
|
|
407
|
+
tool.name.includes("poll_message"));
|
|
408
|
+
if (!hasPollMessage) {
|
|
409
|
+
const message = "ACode runtime is not exposing a poll_message tool. The LLM cannot call poll_message. " +
|
|
410
|
+
"Please configure an MCP server that provides poll_message (e.g. aws-mcp) in the dashboard panel for this runtime.";
|
|
411
|
+
console.warn(`[${this.displayName}] ${message}`);
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const server = typeof event.data.server === "string" ? event.data.server : "mcp";
|
|
416
|
+
const tool = typeof event.data.tool === "string" ? event.data.tool : "";
|
|
417
|
+
const toolName = server && tool ? `${server}__${tool}` : tool || server;
|
|
418
|
+
const actionInfo = getToolActionInfo(toolName);
|
|
419
|
+
const actionDetail = toAcodeMcpActionDetail(actionInfo.actionDetail, server, tool);
|
|
420
|
+
const toolUseId = typeof event.data.toolUseId === "string"
|
|
421
|
+
? event.data.toolUseId
|
|
422
|
+
: undefined;
|
|
423
|
+
const eventData = {
|
|
424
|
+
toolName: actionDetail,
|
|
425
|
+
actionType: actionInfo.actionType,
|
|
426
|
+
actionLabel: actionInfo.actionLabel,
|
|
427
|
+
actionDetail,
|
|
428
|
+
...(toolUseId ? { toolUseId, actionId: toolUseId } : {}),
|
|
429
|
+
};
|
|
430
|
+
if (event.data.action === "tool_result") {
|
|
431
|
+
this.emitProviderEvent(event, "tool_use_end", {
|
|
432
|
+
...eventData,
|
|
433
|
+
toolResult: typeof event.data.result === "string"
|
|
434
|
+
? event.data.result
|
|
435
|
+
: JSON.stringify(event.data.result ?? ""),
|
|
436
|
+
});
|
|
437
|
+
entry.adapterSession.status = "thinking";
|
|
438
|
+
this.emit("status-change", event.sessionId, "thinking");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const status = actionInfo.actionType === "idle" ? "waiting_input" : "tool_using";
|
|
442
|
+
entry.adapterSession.status = status;
|
|
443
|
+
this.emitProviderEvent(event, "tool_use_start", eventData);
|
|
444
|
+
this.emit("status-change", event.sessionId, status, eventData);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
this.emitProviderEvent(event, "thinking", {
|
|
448
|
+
text: JSON.stringify(event.data),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
emitProviderEvent(event, type, data) {
|
|
452
|
+
this.emit("event", {
|
|
453
|
+
type,
|
|
454
|
+
sessionId: event.sessionId,
|
|
455
|
+
timestamp: event.timestamp,
|
|
456
|
+
data,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* After a completed turn, prefer the configured IdlePollLoop (set via setIdleCommands).
|
|
461
|
+
* Fall back to the legacy auto-poll mechanism when no idle commands are configured.
|
|
462
|
+
*/
|
|
463
|
+
armIdleLoopOrAutoPoll(sessionId) {
|
|
464
|
+
const entry = this.sessions.get(sessionId);
|
|
465
|
+
if (!entry || this.isTerminalStatus(entry.adapterSession.status)) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (entry.idleCommands?.idleInputCommand) {
|
|
469
|
+
entry.idlePollLoop?.start();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
this.scheduleAutoPoll(sessionId);
|
|
473
|
+
}
|
|
474
|
+
getIdlePollLoop(entry) {
|
|
475
|
+
if (entry.idlePollLoop)
|
|
476
|
+
return entry.idlePollLoop;
|
|
477
|
+
const sessionId = entry.adapterSession.sessionId;
|
|
478
|
+
entry.idlePollLoop = new IdlePollLoop({
|
|
479
|
+
getStatus: () => this.sessions.get(sessionId)?.adapterSession.status,
|
|
480
|
+
getCommand: () => this.sessions.get(sessionId)?.idleCommands?.idleInputCommand,
|
|
481
|
+
isPollingCommand: (command) => isMessagePollingIdleCommand(command),
|
|
482
|
+
dispatchPollingPrompt: () => this.submitAutoPollPrompt(sessionId, ACODE_MESSAGE_POLLING_IDLE_PROMPT),
|
|
483
|
+
dispatchRegularIdleCommand: (command) => this.submitAutoPollPrompt(sessionId, command),
|
|
484
|
+
});
|
|
485
|
+
return entry.idlePollLoop;
|
|
486
|
+
}
|
|
487
|
+
submitAutoPollPrompt(sessionId, prompt) {
|
|
488
|
+
const entry = this.sessions.get(sessionId);
|
|
489
|
+
if (!entry || this.isTerminalStatus(entry.adapterSession.status))
|
|
490
|
+
return;
|
|
491
|
+
entry.autoPollActive = true;
|
|
492
|
+
entry.adapterSession.status = "thinking";
|
|
493
|
+
this.emit("status-change", sessionId, "thinking");
|
|
494
|
+
void entry.runtime
|
|
495
|
+
.submit(prompt)
|
|
496
|
+
.catch((error) => {
|
|
497
|
+
this.emitRuntimeError(sessionId, error);
|
|
498
|
+
})
|
|
499
|
+
.finally(() => {
|
|
500
|
+
const latestEntry = this.sessions.get(sessionId);
|
|
501
|
+
if (!latestEntry)
|
|
502
|
+
return;
|
|
503
|
+
latestEntry.autoPollActive = false;
|
|
504
|
+
// If the R2 recovery path already scheduled a delayed re-arm for a
|
|
505
|
+
// failed polling-driven turn, skip armIdleLoopOrAutoPoll so we do
|
|
506
|
+
// not double-arm (immediate fire via IdlePollLoop AND a 2s timer).
|
|
507
|
+
// The recovery timer is the source of truth here: the flag is just
|
|
508
|
+
// a defensive marker; the timer itself is what matters.
|
|
509
|
+
if (latestEntry.failedTurnRecoveryScheduled ||
|
|
510
|
+
latestEntry.autoPollTimer) {
|
|
511
|
+
latestEntry.failedTurnRecoveryScheduled = false;
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (latestEntry.adapterSession.status === "error") {
|
|
515
|
+
this.recoverTurnFailure(sessionId, latestEntry);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (latestEntry.adapterSession.status === "thinking" ||
|
|
519
|
+
latestEntry.adapterSession.status === "tool_using") {
|
|
520
|
+
this.recoverResolvedTurnWithoutTerminalEvent(sessionId);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (latestEntry.adapterSession.status === "waiting_input") {
|
|
524
|
+
this.armIdleLoopOrAutoPoll(sessionId);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Continue ACode's headless agent loop after a completed turn.
|
|
530
|
+
* Main flow: debounce duplicate scheduling -> mark busy -> submit an internal
|
|
531
|
+
* poll instruction -> let normal ACode events publish MCP/tool/turn states.
|
|
532
|
+
*
|
|
533
|
+
* @param delayMs Optional override for the default 250ms backoff. The R2
|
|
534
|
+
* recovery path uses ACODE_AUTO_POLL_FAILURE_DELAY_MS (2s) to avoid
|
|
535
|
+
* busy-looping when the same failure recurs on every retry.
|
|
536
|
+
*/
|
|
537
|
+
scheduleAutoPoll(sessionId, delayMs = ACODE_AUTO_POLL_DELAY_MS) {
|
|
538
|
+
const entry = this.sessions.get(sessionId);
|
|
539
|
+
if (!entry ||
|
|
540
|
+
entry.autoPollActive ||
|
|
541
|
+
entry.autoPollTimer ||
|
|
542
|
+
this.isTerminalStatus(entry.adapterSession.status)) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
entry.autoPollTimer = setTimeout(() => {
|
|
546
|
+
const currentEntry = this.sessions.get(sessionId);
|
|
547
|
+
if (!currentEntry ||
|
|
548
|
+
currentEntry.autoPollActive ||
|
|
549
|
+
this.isTerminalStatus(currentEntry.adapterSession.status)) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
currentEntry.autoPollTimer = undefined;
|
|
553
|
+
this.submitAutoPollPrompt(sessionId, ACODE_AUTO_POLL_PROMPT);
|
|
554
|
+
}, delayMs);
|
|
555
|
+
}
|
|
556
|
+
cancelScheduledAutoPoll(entry) {
|
|
557
|
+
if (!entry.autoPollTimer) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
clearTimeout(entry.autoPollTimer);
|
|
561
|
+
entry.autoPollTimer = undefined;
|
|
562
|
+
}
|
|
563
|
+
isTerminalStatus(status) {
|
|
564
|
+
return status === "completed" || status === "terminated";
|
|
565
|
+
}
|
|
566
|
+
emitRuntimeError(sessionId, error, options) {
|
|
567
|
+
const currentEntry = this.sessions.get(sessionId);
|
|
568
|
+
if (!currentEntry) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
currentEntry.adapterSession.status = "error";
|
|
572
|
+
this.emitRuntimeErrorEvent(sessionId, error);
|
|
573
|
+
this.emit("status-change", sessionId, "error");
|
|
574
|
+
if (options?.recover) {
|
|
575
|
+
this.recoverTurnFailure(sessionId, currentEntry);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
emitRuntimeErrorEvent(sessionId, error) {
|
|
579
|
+
const timestamp = new Date().toISOString();
|
|
580
|
+
this.emit("event", {
|
|
581
|
+
type: "error",
|
|
582
|
+
sessionId,
|
|
583
|
+
timestamp,
|
|
584
|
+
data: { text: error instanceof Error ? error.message : String(error) },
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
recoverTurnFailure(sessionId, entry) {
|
|
588
|
+
if (this.isTerminalStatus(entry.adapterSession.status)) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (entry.autoPollTimer || entry.failedTurnRecoveryScheduled) {
|
|
592
|
+
if (entry.adapterSession.status !== "waiting_input") {
|
|
593
|
+
entry.adapterSession.status = "waiting_input";
|
|
594
|
+
this.emit("status-change", sessionId, "waiting_input");
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
this.cancelScheduledAutoPoll(entry);
|
|
599
|
+
entry.adapterSession.status = "waiting_input";
|
|
600
|
+
this.emit("status-change", sessionId, "waiting_input");
|
|
601
|
+
entry.failedTurnRecoveryScheduled = true;
|
|
602
|
+
entry.autoPollTimer = setTimeout(() => {
|
|
603
|
+
const currentEntry = this.sessions.get(sessionId);
|
|
604
|
+
if (!currentEntry ||
|
|
605
|
+
currentEntry.autoPollActive ||
|
|
606
|
+
this.isTerminalStatus(currentEntry.adapterSession.status)) {
|
|
607
|
+
if (currentEntry) {
|
|
608
|
+
currentEntry.failedTurnRecoveryScheduled = false;
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
currentEntry.autoPollTimer = undefined;
|
|
613
|
+
currentEntry.failedTurnRecoveryScheduled = false;
|
|
614
|
+
if (currentEntry.idleCommands?.idleInputCommand) {
|
|
615
|
+
this.getIdlePollLoop(currentEntry).start();
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
this.submitAutoPollPrompt(sessionId, ACODE_AUTO_POLL_PROMPT);
|
|
619
|
+
}
|
|
620
|
+
}, ACODE_AUTO_POLL_FAILURE_DELAY_MS);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Recover turns where runtime.submit() resolved but no terminal turn event arrived.
|
|
624
|
+
*
|
|
625
|
+
* Main flow: ACode should normally emit turn_complete/session_complete/error before
|
|
626
|
+
* submit resolves. Some edge paths can emit only intermediate events (for example
|
|
627
|
+
* command_status or tool_result), then resolve. At that point there is no active
|
|
628
|
+
* turn anymore, so keeping the adapter at thinking/tool_using makes the dashboard
|
|
629
|
+
* look permanently stuck. Recover to waiting_input and arm the listening loop.
|
|
630
|
+
*/
|
|
631
|
+
recoverResolvedTurnWithoutTerminalEvent(sessionId) {
|
|
632
|
+
const entry = this.sessions.get(sessionId);
|
|
633
|
+
if (!entry || this.isTerminalStatus(entry.adapterSession.status)) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (entry.adapterSession.status !== "thinking" &&
|
|
637
|
+
entry.adapterSession.status !== "tool_using") {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
entry.adapterSession.status = "waiting_input";
|
|
641
|
+
this.emit("status-change", sessionId, "waiting_input");
|
|
642
|
+
this.armIdleLoopOrAutoPoll(sessionId);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AcodeSdkAdapter.test.d.ts","sourceRoot":"","sources":["../../src/adapter/AcodeSdkAdapter.test.ts"],"names":[],"mappings":""}
|