aws-runtime-bridge 1.7.49 → 1.7.50
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 +77 -0
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -0
- package/dist/adapter/AcodeSdkAdapter.js +322 -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 +273 -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/SdkProviderSpi.d.ts.map +1 -1
- package/dist/adapter/SdkProviderSpi.js +10 -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/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.js +1 -1
- 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 +9 -56
- 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 +9 -4
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +26 -8
- package/dist/routes/terminal.test.js +24 -8
- 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/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 +231 -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 +34 -0
- package/package/acode/dist/runtime.d.ts.map +1 -0
- package/package/acode/dist/runtime.js +571 -0
- package/package/acode/dist/types.d.ts +99 -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 +1 -23
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +89 -6
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +3 -0
- package/package/aws-client-agent-mcp/dist/types.d.ts.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 +3 -100
- package/package/aws-client-agent-mcp/dist/websocket-client.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,77 @@
|
|
|
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 type { AdapterSessionConfig, BaseProviderAdapter, ConversationMessage, SessionStatus } from './types.js';
|
|
9
|
+
type ACodeEvent = {
|
|
10
|
+
type: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
data: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
type ACodeRuntimeInstance = {
|
|
16
|
+
on(eventName: 'event', listener: (event: ACodeEvent) => void): void;
|
|
17
|
+
start(): Promise<void>;
|
|
18
|
+
submit(message: string): Promise<void>;
|
|
19
|
+
abort(): Promise<void>;
|
|
20
|
+
stop(): Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
type ACodeRuntimeConstructor = new (config: {
|
|
23
|
+
sessionId: string;
|
|
24
|
+
workingDirectory: string;
|
|
25
|
+
configPath?: string;
|
|
26
|
+
skillsDir?: string;
|
|
27
|
+
initialPrompt?: string;
|
|
28
|
+
model?: string;
|
|
29
|
+
systemPrompt?: string;
|
|
30
|
+
env?: Record<string, string>;
|
|
31
|
+
}) => ACodeRuntimeInstance;
|
|
32
|
+
export declare function toAcodeEsmImportSpecifier(modulePath: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Check whether an ACode turn_complete event represents a failed turn.
|
|
35
|
+
* Main flow: only explicit failed=true prevents error status from being reset to waiting_input.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isFailedAcodeTurnComplete(data: Record<string, unknown>): boolean;
|
|
38
|
+
export declare class AcodeSdkAdapter extends EventEmitter implements BaseProviderAdapter {
|
|
39
|
+
readonly providerId = "acode";
|
|
40
|
+
readonly displayName = "ACode";
|
|
41
|
+
private sessions;
|
|
42
|
+
private runtimeConstructor?;
|
|
43
|
+
startSession(sessionId: string, config: AdapterSessionConfig): Promise<void>;
|
|
44
|
+
sendMessage(sessionId: string, message: string): Promise<void>;
|
|
45
|
+
sendConfirmation(_sessionId: string, _accept: boolean): Promise<void>;
|
|
46
|
+
abortCurrentTurn(sessionId: string): Promise<void>;
|
|
47
|
+
terminateSession(sessionId: string): Promise<void>;
|
|
48
|
+
getConversation(sessionId: string): ConversationMessage[];
|
|
49
|
+
hasSession(sessionId: string): boolean;
|
|
50
|
+
getProviderSessionId(sessionId: string): string | undefined;
|
|
51
|
+
getSessionStatus(sessionId: string): SessionStatus | undefined;
|
|
52
|
+
cleanup(): void;
|
|
53
|
+
/** Injects a runtime constructor for adapter tests without loading the packaged ACode module. */
|
|
54
|
+
setRuntimeConstructorForTest(runtimeConstructor: ACodeRuntimeConstructor): void;
|
|
55
|
+
private createRuntime;
|
|
56
|
+
private loadRuntimeConstructor;
|
|
57
|
+
private requireSession;
|
|
58
|
+
/**
|
|
59
|
+
* Run the startup prompt after the HTTP launch has already completed.
|
|
60
|
+
* Main flow: mark the session busy -> submit the prompt in the background ->
|
|
61
|
+
* rely on normal ACode events to publish turn completion or errors.
|
|
62
|
+
*/
|
|
63
|
+
private startInitialPromptTurn;
|
|
64
|
+
private forwardAcodeEvent;
|
|
65
|
+
private emitProviderEvent;
|
|
66
|
+
/**
|
|
67
|
+
* Continue ACode's headless agent loop after a completed turn.
|
|
68
|
+
* Main flow: debounce duplicate scheduling -> mark busy -> submit an internal
|
|
69
|
+
* poll instruction -> let normal ACode events publish MCP/tool/turn states.
|
|
70
|
+
*/
|
|
71
|
+
private scheduleAutoPoll;
|
|
72
|
+
private cancelScheduledAutoPoll;
|
|
73
|
+
private isTerminalStatus;
|
|
74
|
+
private emitRuntimeError;
|
|
75
|
+
}
|
|
76
|
+
export {};
|
|
77
|
+
//# sourceMappingURL=AcodeSdkAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AcodeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/AcodeSdkAdapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,322 @@
|
|
|
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
|
+
const ACODE_AUTO_POLL_PROMPT = [
|
|
14
|
+
'Continue the AgentsWork message loop now.',
|
|
15
|
+
'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.',
|
|
16
|
+
'Do not answer in plain text before calling aws-mcp__poll_message.',
|
|
17
|
+
'After handling any received work, reply through the appropriate aws-mcp messaging tool, then continue listening again.',
|
|
18
|
+
].join(' ');
|
|
19
|
+
const ACODE_AUTO_POLL_DELAY_MS = 250;
|
|
20
|
+
function getAcodePackageEntryPath() {
|
|
21
|
+
return path.resolve(getReleasedAcodeEntryPath());
|
|
22
|
+
}
|
|
23
|
+
export function toAcodeEsmImportSpecifier(modulePath) {
|
|
24
|
+
return pathToFileURL(path.resolve(modulePath)).href;
|
|
25
|
+
}
|
|
26
|
+
function toUsage(value) {
|
|
27
|
+
if (!value || typeof value !== 'object') {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const usage = value;
|
|
31
|
+
return {
|
|
32
|
+
inputTokens: Number(usage.inputTokens) || 0,
|
|
33
|
+
outputTokens: Number(usage.outputTokens) || 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check whether an ACode turn_complete event represents a failed turn.
|
|
38
|
+
* Main flow: only explicit failed=true prevents error status from being reset to waiting_input.
|
|
39
|
+
*/
|
|
40
|
+
export function isFailedAcodeTurnComplete(data) {
|
|
41
|
+
return data.failed === true;
|
|
42
|
+
}
|
|
43
|
+
export class AcodeSdkAdapter extends EventEmitter {
|
|
44
|
+
constructor() {
|
|
45
|
+
super(...arguments);
|
|
46
|
+
this.providerId = 'acode';
|
|
47
|
+
this.displayName = 'ACode';
|
|
48
|
+
this.sessions = new Map();
|
|
49
|
+
}
|
|
50
|
+
async startSession(sessionId, config) {
|
|
51
|
+
const runtimeHome = getRuntimeHomeDir();
|
|
52
|
+
const session = {
|
|
53
|
+
sessionId,
|
|
54
|
+
status: 'starting',
|
|
55
|
+
messages: [],
|
|
56
|
+
createdAt: new Date().toISOString(),
|
|
57
|
+
totalUsage: { inputTokens: 0, outputTokens: 0 },
|
|
58
|
+
};
|
|
59
|
+
this.sessions.set(sessionId, {
|
|
60
|
+
adapterSession: session,
|
|
61
|
+
config,
|
|
62
|
+
runtime: await this.createRuntime(sessionId, config, runtimeHome),
|
|
63
|
+
autoPollActive: false,
|
|
64
|
+
});
|
|
65
|
+
this.emit('status-change', sessionId, 'starting');
|
|
66
|
+
const entry = this.sessions.get(sessionId);
|
|
67
|
+
if (!entry) {
|
|
68
|
+
throw new Error('ACode session was not initialized');
|
|
69
|
+
}
|
|
70
|
+
await entry.runtime.start();
|
|
71
|
+
session.status = 'waiting_input';
|
|
72
|
+
this.emit('status-change', sessionId, 'waiting_input');
|
|
73
|
+
if (config.initialPrompt?.trim()) {
|
|
74
|
+
this.startInitialPromptTurn(sessionId, config.initialPrompt);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async sendMessage(sessionId, message) {
|
|
78
|
+
const entry = this.requireSession(sessionId);
|
|
79
|
+
this.cancelScheduledAutoPoll(entry);
|
|
80
|
+
const userMessage = {
|
|
81
|
+
id: uuidv4(),
|
|
82
|
+
sessionId,
|
|
83
|
+
role: 'user',
|
|
84
|
+
content: message,
|
|
85
|
+
timestamp: new Date().toISOString(),
|
|
86
|
+
};
|
|
87
|
+
entry.adapterSession.messages.push(userMessage);
|
|
88
|
+
this.emit('conversation-message', sessionId, userMessage);
|
|
89
|
+
entry.adapterSession.status = 'thinking';
|
|
90
|
+
this.emit('status-change', sessionId, 'thinking');
|
|
91
|
+
await entry.runtime.submit(message);
|
|
92
|
+
}
|
|
93
|
+
async sendConfirmation(_sessionId, _accept) { }
|
|
94
|
+
async abortCurrentTurn(sessionId) {
|
|
95
|
+
await this.requireSession(sessionId).runtime.abort();
|
|
96
|
+
}
|
|
97
|
+
async terminateSession(sessionId) {
|
|
98
|
+
const entry = this.sessions.get(sessionId);
|
|
99
|
+
if (!entry) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.cancelScheduledAutoPoll(entry);
|
|
103
|
+
await entry.runtime.stop();
|
|
104
|
+
entry.adapterSession.status = 'terminated';
|
|
105
|
+
this.sessions.delete(sessionId);
|
|
106
|
+
this.emit('status-change', sessionId, 'terminated');
|
|
107
|
+
}
|
|
108
|
+
getConversation(sessionId) {
|
|
109
|
+
return this.sessions.get(sessionId)?.adapterSession.messages ?? [];
|
|
110
|
+
}
|
|
111
|
+
hasSession(sessionId) {
|
|
112
|
+
return this.sessions.has(sessionId);
|
|
113
|
+
}
|
|
114
|
+
getProviderSessionId(sessionId) {
|
|
115
|
+
return this.sessions.has(sessionId) ? sessionId : undefined;
|
|
116
|
+
}
|
|
117
|
+
getSessionStatus(sessionId) {
|
|
118
|
+
return this.sessions.get(sessionId)?.adapterSession.status;
|
|
119
|
+
}
|
|
120
|
+
cleanup() {
|
|
121
|
+
for (const sessionId of this.sessions.keys()) {
|
|
122
|
+
void this.terminateSession(sessionId);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Injects a runtime constructor for adapter tests without loading the packaged ACode module. */
|
|
126
|
+
setRuntimeConstructorForTest(runtimeConstructor) {
|
|
127
|
+
this.runtimeConstructor = runtimeConstructor;
|
|
128
|
+
}
|
|
129
|
+
async createRuntime(sessionId, config, runtimeHome) {
|
|
130
|
+
const ACodeRuntime = await this.loadRuntimeConstructor();
|
|
131
|
+
const runtime = new ACodeRuntime({
|
|
132
|
+
sessionId,
|
|
133
|
+
workingDirectory: config.workingDirectory,
|
|
134
|
+
configPath: config.mcpConfigPath || getAcodeConfigFile(runtimeHome),
|
|
135
|
+
skillsDir: getAcodeSkillsDir(runtimeHome),
|
|
136
|
+
model: config.model,
|
|
137
|
+
systemPrompt: typeof config.systemPrompt === 'string' ? config.systemPrompt : undefined,
|
|
138
|
+
env: config.envOverrides,
|
|
139
|
+
});
|
|
140
|
+
runtime.on('event', (event) => this.forwardAcodeEvent(event));
|
|
141
|
+
return runtime;
|
|
142
|
+
}
|
|
143
|
+
async loadRuntimeConstructor() {
|
|
144
|
+
if (this.runtimeConstructor) {
|
|
145
|
+
return this.runtimeConstructor;
|
|
146
|
+
}
|
|
147
|
+
const modulePath = getAcodePackageEntryPath();
|
|
148
|
+
const module = await import(toAcodeEsmImportSpecifier(modulePath));
|
|
149
|
+
if (!module.ACodeRuntime) {
|
|
150
|
+
throw new Error(`ACode runtime export not found: ${modulePath}`);
|
|
151
|
+
}
|
|
152
|
+
this.runtimeConstructor = module.ACodeRuntime;
|
|
153
|
+
return this.runtimeConstructor;
|
|
154
|
+
}
|
|
155
|
+
requireSession(sessionId) {
|
|
156
|
+
const entry = this.sessions.get(sessionId);
|
|
157
|
+
if (!entry) {
|
|
158
|
+
throw new Error('ACode session not found');
|
|
159
|
+
}
|
|
160
|
+
return entry;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Run the startup prompt after the HTTP launch has already completed.
|
|
164
|
+
* Main flow: mark the session busy -> submit the prompt in the background ->
|
|
165
|
+
* rely on normal ACode events to publish turn completion or errors.
|
|
166
|
+
*/
|
|
167
|
+
startInitialPromptTurn(sessionId, initialPrompt) {
|
|
168
|
+
const entry = this.sessions.get(sessionId);
|
|
169
|
+
if (!entry) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
entry.adapterSession.status = 'thinking';
|
|
173
|
+
this.emit('status-change', sessionId, 'thinking');
|
|
174
|
+
void entry.runtime.submit(initialPrompt).catch((error) => {
|
|
175
|
+
const currentEntry = this.sessions.get(sessionId);
|
|
176
|
+
if (!currentEntry) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
currentEntry.adapterSession.status = 'error';
|
|
180
|
+
const timestamp = new Date().toISOString();
|
|
181
|
+
this.emit('event', {
|
|
182
|
+
type: 'error',
|
|
183
|
+
sessionId,
|
|
184
|
+
timestamp,
|
|
185
|
+
data: { text: error instanceof Error ? error.message : String(error) },
|
|
186
|
+
});
|
|
187
|
+
this.emit('status-change', sessionId, 'error');
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
forwardAcodeEvent(event) {
|
|
191
|
+
const entry = this.sessions.get(event.sessionId);
|
|
192
|
+
if (!entry) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (event.type === 'assistant_delta') {
|
|
196
|
+
const text = typeof event.data.text === 'string' ? event.data.text : '';
|
|
197
|
+
entry.adapterSession.messages.push({
|
|
198
|
+
id: uuidv4(),
|
|
199
|
+
sessionId: event.sessionId,
|
|
200
|
+
role: 'assistant',
|
|
201
|
+
content: text,
|
|
202
|
+
timestamp: event.timestamp,
|
|
203
|
+
});
|
|
204
|
+
this.emitProviderEvent(event, 'text_delta', { text });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (event.type === 'turn_complete') {
|
|
208
|
+
const usage = toUsage(event.data.usage);
|
|
209
|
+
if (usage) {
|
|
210
|
+
entry.adapterSession.totalUsage.inputTokens += usage.inputTokens;
|
|
211
|
+
entry.adapterSession.totalUsage.outputTokens += usage.outputTokens;
|
|
212
|
+
}
|
|
213
|
+
if (isFailedAcodeTurnComplete(event.data)) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.emitProviderEvent(event, 'turn_complete', { usage: entry.adapterSession.totalUsage });
|
|
217
|
+
entry.adapterSession.status = 'waiting_input';
|
|
218
|
+
this.emit('status-change', event.sessionId, 'waiting_input', { usage: entry.adapterSession.totalUsage });
|
|
219
|
+
this.scheduleAutoPoll(event.sessionId);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (event.type === 'session_complete') {
|
|
223
|
+
entry.adapterSession.status = 'completed';
|
|
224
|
+
this.emitProviderEvent(event, 'session_complete', { exitCode: Number(event.data.exitCode) || 0 });
|
|
225
|
+
this.emit('status-change', event.sessionId, 'completed');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (event.type === 'error') {
|
|
229
|
+
entry.adapterSession.status = 'error';
|
|
230
|
+
this.emitProviderEvent(event, 'error', { text: String(event.data.message || 'ACode runtime error') });
|
|
231
|
+
this.emit('status-change', event.sessionId, 'error');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (event.type === 'mcp_status') {
|
|
235
|
+
const server = typeof event.data.server === 'string' ? event.data.server : 'mcp';
|
|
236
|
+
const tool = typeof event.data.tool === 'string' ? event.data.tool : '';
|
|
237
|
+
const actionDetail = tool ? `${server}/${tool}` : server;
|
|
238
|
+
entry.adapterSession.status = 'tool_using';
|
|
239
|
+
this.emitProviderEvent(event, 'tool_use_start', {
|
|
240
|
+
toolName: actionDetail,
|
|
241
|
+
actionType: 'mcp',
|
|
242
|
+
actionLabel: 'Calling MCP',
|
|
243
|
+
actionDetail,
|
|
244
|
+
});
|
|
245
|
+
this.emit('status-change', event.sessionId, 'tool_using', {
|
|
246
|
+
actionType: 'mcp',
|
|
247
|
+
actionLabel: 'Calling MCP',
|
|
248
|
+
actionDetail,
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.emitProviderEvent(event, 'thinking', {
|
|
253
|
+
text: JSON.stringify(event.data),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
emitProviderEvent(event, type, data) {
|
|
257
|
+
this.emit('event', {
|
|
258
|
+
type,
|
|
259
|
+
sessionId: event.sessionId,
|
|
260
|
+
timestamp: event.timestamp,
|
|
261
|
+
data,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Continue ACode's headless agent loop after a completed turn.
|
|
266
|
+
* Main flow: debounce duplicate scheduling -> mark busy -> submit an internal
|
|
267
|
+
* poll instruction -> let normal ACode events publish MCP/tool/turn states.
|
|
268
|
+
*/
|
|
269
|
+
scheduleAutoPoll(sessionId) {
|
|
270
|
+
const entry = this.sessions.get(sessionId);
|
|
271
|
+
if (!entry || entry.autoPollActive || entry.autoPollTimer || this.isTerminalStatus(entry.adapterSession.status)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
entry.autoPollTimer = setTimeout(() => {
|
|
275
|
+
const currentEntry = this.sessions.get(sessionId);
|
|
276
|
+
if (!currentEntry || currentEntry.autoPollActive || this.isTerminalStatus(currentEntry.adapterSession.status)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
currentEntry.autoPollTimer = undefined;
|
|
280
|
+
currentEntry.autoPollActive = true;
|
|
281
|
+
currentEntry.adapterSession.status = 'thinking';
|
|
282
|
+
this.emit('status-change', sessionId, 'thinking');
|
|
283
|
+
void currentEntry.runtime.submit(ACODE_AUTO_POLL_PROMPT).catch((error) => {
|
|
284
|
+
this.emitRuntimeError(sessionId, error);
|
|
285
|
+
}).finally(() => {
|
|
286
|
+
const latestEntry = this.sessions.get(sessionId);
|
|
287
|
+
if (!latestEntry) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
latestEntry.autoPollActive = false;
|
|
291
|
+
if (latestEntry.adapterSession.status === 'waiting_input') {
|
|
292
|
+
this.scheduleAutoPoll(sessionId);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}, ACODE_AUTO_POLL_DELAY_MS);
|
|
296
|
+
}
|
|
297
|
+
cancelScheduledAutoPoll(entry) {
|
|
298
|
+
if (!entry.autoPollTimer) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
clearTimeout(entry.autoPollTimer);
|
|
302
|
+
entry.autoPollTimer = undefined;
|
|
303
|
+
}
|
|
304
|
+
isTerminalStatus(status) {
|
|
305
|
+
return status === 'completed' || status === 'terminated' || status === 'error';
|
|
306
|
+
}
|
|
307
|
+
emitRuntimeError(sessionId, error) {
|
|
308
|
+
const currentEntry = this.sessions.get(sessionId);
|
|
309
|
+
if (!currentEntry) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
currentEntry.adapterSession.status = 'error';
|
|
313
|
+
const timestamp = new Date().toISOString();
|
|
314
|
+
this.emit('event', {
|
|
315
|
+
type: 'error',
|
|
316
|
+
sessionId,
|
|
317
|
+
timestamp,
|
|
318
|
+
data: { text: error instanceof Error ? error.message : String(error) },
|
|
319
|
+
});
|
|
320
|
+
this.emit('status-change', sessionId, 'error');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AcodeSdkAdapter.test.d.ts","sourceRoot":"","sources":["../../src/adapter/AcodeSdkAdapter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { AcodeSdkAdapter, isFailedAcodeTurnComplete } from './AcodeSdkAdapter.js';
|
|
4
|
+
class FailedTurnRuntime extends EventEmitter {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
super();
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
async start() { }
|
|
10
|
+
async submit(_message) {
|
|
11
|
+
this.emit('event', {
|
|
12
|
+
type: 'error',
|
|
13
|
+
sessionId: this.config.sessionId,
|
|
14
|
+
timestamp: new Date(0).toISOString(),
|
|
15
|
+
data: { message: 'provider down' },
|
|
16
|
+
});
|
|
17
|
+
this.emit('event', {
|
|
18
|
+
type: 'turn_complete',
|
|
19
|
+
sessionId: this.config.sessionId,
|
|
20
|
+
timestamp: new Date(0).toISOString(),
|
|
21
|
+
data: { failed: true, usage: { inputTokens: 1, outputTokens: 0 } },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async abort() { }
|
|
25
|
+
async stop() { }
|
|
26
|
+
}
|
|
27
|
+
class McpStatusRuntime extends EventEmitter {
|
|
28
|
+
constructor(config) {
|
|
29
|
+
super();
|
|
30
|
+
this.config = config;
|
|
31
|
+
}
|
|
32
|
+
async start() { }
|
|
33
|
+
async submit(_message) {
|
|
34
|
+
this.emit('event', {
|
|
35
|
+
type: 'mcp_status',
|
|
36
|
+
sessionId: this.config.sessionId,
|
|
37
|
+
timestamp: new Date(0).toISOString(),
|
|
38
|
+
data: { action: 'call_tool', server: 'aws-mcp', tool: 'create_memory' },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async abort() { }
|
|
42
|
+
async stop() { }
|
|
43
|
+
}
|
|
44
|
+
class CommandStatusRuntime extends EventEmitter {
|
|
45
|
+
constructor(config) {
|
|
46
|
+
super();
|
|
47
|
+
this.config = config;
|
|
48
|
+
}
|
|
49
|
+
async start() { }
|
|
50
|
+
async submit(_message) {
|
|
51
|
+
this.emit('event', {
|
|
52
|
+
type: 'command_status',
|
|
53
|
+
sessionId: this.config.sessionId,
|
|
54
|
+
timestamp: new Date(0).toISOString(),
|
|
55
|
+
data: { action: 'slash_command', command: 'review' },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async abort() { }
|
|
59
|
+
async stop() { }
|
|
60
|
+
}
|
|
61
|
+
class McpThenSessionCompleteRuntime extends EventEmitter {
|
|
62
|
+
constructor(config) {
|
|
63
|
+
super();
|
|
64
|
+
this.config = config;
|
|
65
|
+
}
|
|
66
|
+
async start() { }
|
|
67
|
+
async submit(_message) {
|
|
68
|
+
this.emit('event', {
|
|
69
|
+
type: 'mcp_status',
|
|
70
|
+
sessionId: this.config.sessionId,
|
|
71
|
+
timestamp: new Date(0).toISOString(),
|
|
72
|
+
data: { action: 'call_tool', server: 'aws-mcp', tool: 'send_private' },
|
|
73
|
+
});
|
|
74
|
+
this.emit('event', {
|
|
75
|
+
type: 'session_complete',
|
|
76
|
+
sessionId: this.config.sessionId,
|
|
77
|
+
timestamp: new Date(0).toISOString(),
|
|
78
|
+
data: { exitCode: 0 },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async abort() { }
|
|
82
|
+
async stop() { }
|
|
83
|
+
}
|
|
84
|
+
class DeferredInitialPromptRuntime extends EventEmitter {
|
|
85
|
+
constructor(config) {
|
|
86
|
+
super();
|
|
87
|
+
DeferredInitialPromptRuntime.receivedInitialPrompt = config.initialPrompt;
|
|
88
|
+
}
|
|
89
|
+
async start() { }
|
|
90
|
+
async submit(_message) {
|
|
91
|
+
DeferredInitialPromptRuntime.submitCalls += 1;
|
|
92
|
+
await new Promise(() => undefined);
|
|
93
|
+
}
|
|
94
|
+
async abort() { }
|
|
95
|
+
async stop() { }
|
|
96
|
+
}
|
|
97
|
+
DeferredInitialPromptRuntime.submitCalls = 0;
|
|
98
|
+
class AutoPollRuntime extends EventEmitter {
|
|
99
|
+
constructor(config) {
|
|
100
|
+
super();
|
|
101
|
+
this.config = config;
|
|
102
|
+
}
|
|
103
|
+
async start() { }
|
|
104
|
+
async submit(message) {
|
|
105
|
+
AutoPollRuntime.submitMessages.push(message);
|
|
106
|
+
if (AutoPollRuntime.submitMessages.length === 1) {
|
|
107
|
+
this.emit('event', {
|
|
108
|
+
type: 'turn_complete',
|
|
109
|
+
sessionId: this.config.sessionId,
|
|
110
|
+
timestamp: new Date(0).toISOString(),
|
|
111
|
+
data: { usage: { inputTokens: 1, outputTokens: 1 } },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async abort() { }
|
|
116
|
+
async stop() { }
|
|
117
|
+
}
|
|
118
|
+
AutoPollRuntime.submitMessages = [];
|
|
119
|
+
describe('AcodeSdkAdapter turn completion state', () => {
|
|
120
|
+
it('keeps error status for failed turn_complete events', () => {
|
|
121
|
+
expect(isFailedAcodeTurnComplete({ failed: true })).toBe(true);
|
|
122
|
+
expect(isFailedAcodeTurnComplete({ failed: false })).toBe(false);
|
|
123
|
+
expect(isFailedAcodeTurnComplete({ usage: { inputTokens: 1, outputTokens: 2 } })).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
it('does not forward failed turn_complete into waiting_input status updates', async () => {
|
|
126
|
+
const adapter = new AcodeSdkAdapter();
|
|
127
|
+
adapter.setRuntimeConstructorForTest(FailedTurnRuntime);
|
|
128
|
+
const events = [];
|
|
129
|
+
const statuses = [];
|
|
130
|
+
adapter.on('event', (event) => events.push(event));
|
|
131
|
+
adapter.on('status-change', (_sessionId, status) => statuses.push(status));
|
|
132
|
+
await adapter.startSession('session-failed', {
|
|
133
|
+
command: 'acode',
|
|
134
|
+
workingDirectory: process.cwd(),
|
|
135
|
+
autoAccept: true,
|
|
136
|
+
extraMcpServers: {},
|
|
137
|
+
envOverrides: {},
|
|
138
|
+
});
|
|
139
|
+
await adapter.sendMessage('session-failed', 'fail please');
|
|
140
|
+
expect(events.map((event) => event.type)).toEqual(['error']);
|
|
141
|
+
expect(statuses).toEqual(['starting', 'waiting_input', 'thinking', 'error']);
|
|
142
|
+
expect(adapter.getSessionStatus('session-failed')).toBe('error');
|
|
143
|
+
});
|
|
144
|
+
it('maps ACode MCP status events to tool_using instead of thinking', async () => {
|
|
145
|
+
const adapter = new AcodeSdkAdapter();
|
|
146
|
+
adapter.setRuntimeConstructorForTest(McpStatusRuntime);
|
|
147
|
+
const events = [];
|
|
148
|
+
const statuses = [];
|
|
149
|
+
adapter.on('event', (event) => events.push(event));
|
|
150
|
+
adapter.on('status-change', (_sessionId, status, actionInfo) => {
|
|
151
|
+
statuses.push({ status, actionType: actionInfo?.actionType, actionDetail: actionInfo?.actionDetail });
|
|
152
|
+
});
|
|
153
|
+
await adapter.startSession('session-mcp', {
|
|
154
|
+
command: 'acode',
|
|
155
|
+
workingDirectory: process.cwd(),
|
|
156
|
+
autoAccept: true,
|
|
157
|
+
extraMcpServers: {},
|
|
158
|
+
envOverrides: {},
|
|
159
|
+
});
|
|
160
|
+
await adapter.sendMessage('session-mcp', 'call mcp');
|
|
161
|
+
expect(events.map((event) => event.type)).toEqual(['tool_use_start']);
|
|
162
|
+
expect(events[0]?.data).toMatchObject({
|
|
163
|
+
actionType: 'mcp',
|
|
164
|
+
actionLabel: 'Calling MCP',
|
|
165
|
+
actionDetail: 'aws-mcp/create_memory',
|
|
166
|
+
});
|
|
167
|
+
expect(statuses.at(-1)).toEqual({
|
|
168
|
+
status: 'tool_using',
|
|
169
|
+
actionType: 'mcp',
|
|
170
|
+
actionDetail: 'aws-mcp/create_memory',
|
|
171
|
+
});
|
|
172
|
+
expect(adapter.getSessionStatus('session-mcp')).toBe('tool_using');
|
|
173
|
+
});
|
|
174
|
+
it('forwards ACode command status as thinking without changing session status', async () => {
|
|
175
|
+
const adapter = new AcodeSdkAdapter();
|
|
176
|
+
adapter.setRuntimeConstructorForTest(CommandStatusRuntime);
|
|
177
|
+
const events = [];
|
|
178
|
+
const statuses = [];
|
|
179
|
+
adapter.on('event', (event) => events.push(event));
|
|
180
|
+
adapter.on('status-change', (_sessionId, status) => statuses.push(status));
|
|
181
|
+
await adapter.startSession('session-command-status', {
|
|
182
|
+
command: 'acode',
|
|
183
|
+
workingDirectory: process.cwd(),
|
|
184
|
+
autoAccept: true,
|
|
185
|
+
extraMcpServers: {},
|
|
186
|
+
envOverrides: {},
|
|
187
|
+
});
|
|
188
|
+
await adapter.sendMessage('session-command-status', '/review src');
|
|
189
|
+
expect(events).toHaveLength(1);
|
|
190
|
+
expect(events[0]?.type).toBe('thinking');
|
|
191
|
+
expect(String(events[0]?.data.text)).toContain('slash_command');
|
|
192
|
+
expect(statuses).toEqual(['starting', 'waiting_input', 'thinking']);
|
|
193
|
+
expect(adapter.getSessionStatus('session-command-status')).toBe('thinking');
|
|
194
|
+
});
|
|
195
|
+
it('forwards session_complete status after MCP calls so UI can clear active tool state', async () => {
|
|
196
|
+
const adapter = new AcodeSdkAdapter();
|
|
197
|
+
adapter.setRuntimeConstructorForTest(McpThenSessionCompleteRuntime);
|
|
198
|
+
const events = [];
|
|
199
|
+
const statuses = [];
|
|
200
|
+
adapter.on('event', (event) => events.push(event));
|
|
201
|
+
adapter.on('status-change', (_sessionId, status, actionInfo) => {
|
|
202
|
+
statuses.push({ status, actionType: actionInfo?.actionType, actionDetail: actionInfo?.actionDetail });
|
|
203
|
+
});
|
|
204
|
+
await adapter.startSession('session-mcp-complete', {
|
|
205
|
+
command: 'acode',
|
|
206
|
+
workingDirectory: process.cwd(),
|
|
207
|
+
autoAccept: true,
|
|
208
|
+
extraMcpServers: {},
|
|
209
|
+
envOverrides: {},
|
|
210
|
+
});
|
|
211
|
+
await adapter.sendMessage('session-mcp-complete', 'send private');
|
|
212
|
+
expect(events.map((event) => event.type)).toEqual(['tool_use_start', 'session_complete']);
|
|
213
|
+
expect(statuses.at(-2)).toEqual({
|
|
214
|
+
status: 'tool_using',
|
|
215
|
+
actionType: 'mcp',
|
|
216
|
+
actionDetail: 'aws-mcp/send_private',
|
|
217
|
+
});
|
|
218
|
+
expect(statuses.at(-1)).toEqual({
|
|
219
|
+
status: 'completed',
|
|
220
|
+
actionType: undefined,
|
|
221
|
+
actionDetail: undefined,
|
|
222
|
+
});
|
|
223
|
+
expect(adapter.getSessionStatus('session-mcp-complete')).toBe('completed');
|
|
224
|
+
});
|
|
225
|
+
it('does not block launch completion while the initial prompt runs in the background', async () => {
|
|
226
|
+
DeferredInitialPromptRuntime.submitCalls = 0;
|
|
227
|
+
DeferredInitialPromptRuntime.receivedInitialPrompt = undefined;
|
|
228
|
+
const adapter = new AcodeSdkAdapter();
|
|
229
|
+
adapter.setRuntimeConstructorForTest(DeferredInitialPromptRuntime);
|
|
230
|
+
const statuses = [];
|
|
231
|
+
adapter.on('status-change', (_sessionId, status) => statuses.push(status));
|
|
232
|
+
await adapter.startSession('session-initial-prompt', {
|
|
233
|
+
command: 'acode',
|
|
234
|
+
workingDirectory: process.cwd(),
|
|
235
|
+
autoAccept: true,
|
|
236
|
+
initialPrompt: 'start working loop',
|
|
237
|
+
extraMcpServers: {},
|
|
238
|
+
envOverrides: {},
|
|
239
|
+
});
|
|
240
|
+
expect(DeferredInitialPromptRuntime.receivedInitialPrompt).toBeUndefined();
|
|
241
|
+
expect(DeferredInitialPromptRuntime.submitCalls).toBe(1);
|
|
242
|
+
expect(statuses).toEqual(['starting', 'waiting_input', 'thinking']);
|
|
243
|
+
expect(adapter.getSessionStatus('session-initial-prompt')).toBe('thinking');
|
|
244
|
+
});
|
|
245
|
+
it('continues the message loop by auto-submitting poll_message after a completed turn', async () => {
|
|
246
|
+
vi.useFakeTimers();
|
|
247
|
+
AutoPollRuntime.submitMessages = [];
|
|
248
|
+
const adapter = new AcodeSdkAdapter();
|
|
249
|
+
adapter.setRuntimeConstructorForTest(AutoPollRuntime);
|
|
250
|
+
const statuses = [];
|
|
251
|
+
adapter.on('status-change', (_sessionId, status) => statuses.push(status));
|
|
252
|
+
try {
|
|
253
|
+
await adapter.startSession('session-auto-poll', {
|
|
254
|
+
command: 'acode',
|
|
255
|
+
workingDirectory: process.cwd(),
|
|
256
|
+
autoAccept: true,
|
|
257
|
+
extraMcpServers: {},
|
|
258
|
+
envOverrides: {},
|
|
259
|
+
});
|
|
260
|
+
await adapter.sendMessage('session-auto-poll', 'handle current work');
|
|
261
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
262
|
+
expect(AutoPollRuntime.submitMessages).toHaveLength(2);
|
|
263
|
+
expect(AutoPollRuntime.submitMessages[0]).toBe('handle current work');
|
|
264
|
+
expect(AutoPollRuntime.submitMessages[1]).toContain('poll_message');
|
|
265
|
+
expect(statuses).toEqual(['starting', 'waiting_input', 'thinking', 'waiting_input', 'thinking']);
|
|
266
|
+
expect(adapter.getSessionStatus('session-auto-poll')).toBe('thinking');
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
vi.useRealTimers();
|
|
270
|
+
adapter.cleanup();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|