agent-gear 1.1.4 → 1.1.6
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/acp/bridge.d.ts +70 -3
- package/dist/acp/bridge.d.ts.map +1 -1
- package/dist/acp/bridge.js +261 -59
- package/dist/acp/bridge.js.map +1 -1
- package/dist/acp/bridge.test.js +1 -1
- package/dist/acp/bridge.test.js.map +1 -1
- package/dist/acp/detector.d.ts +2 -0
- package/dist/acp/detector.d.ts.map +1 -1
- package/dist/acp/detector.js +28 -2
- package/dist/acp/detector.js.map +1 -1
- package/dist/acp/sync-bridge.d.ts +4 -1
- package/dist/acp/sync-bridge.d.ts.map +1 -1
- package/dist/acp/sync-bridge.js +17 -3
- package/dist/acp/sync-bridge.js.map +1 -1
- package/dist/socketio-client.d.ts.map +1 -1
- package/dist/socketio-client.js +131 -1
- package/dist/socketio-client.js.map +1 -1
- package/package.json +2 -2
- package/src/acp/bridge.test.ts +1 -1
- package/src/acp/bridge.ts +327 -60
- package/src/acp/detector.ts +28 -2
- package/src/acp/sync-bridge.ts +19 -3
- package/src/socketio-client.ts +154 -2
- package/tests/acp-bridge.test.ts +250 -31
- package/tests/acp-detector.test.ts +66 -6
- package/tests/acp-sync-bridge.test.ts +39 -1
- package/tests/socketio-client.test.ts +211 -0
package/src/acp/bridge.ts
CHANGED
|
@@ -8,6 +8,7 @@ const ACP_PROTOCOL_VERSION = 1;
|
|
|
8
8
|
export interface AcpBridgeOptions {
|
|
9
9
|
sessionId: string;
|
|
10
10
|
cwd: string;
|
|
11
|
+
additionalDirectories?: string[];
|
|
11
12
|
mcpServers?: Array<{ name: string; command: string; args?: string[]; env?: Array<{ name: string; value: string }> }>;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -16,6 +17,39 @@ interface QueuedInput {
|
|
|
16
17
|
content: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
export interface SessionConfigOptionData {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string | null;
|
|
24
|
+
category?: string | null;
|
|
25
|
+
type: 'select' | 'boolean';
|
|
26
|
+
currentValue: string | null;
|
|
27
|
+
options: Array<{ value: string; name?: string }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AcpAuthMethodData {
|
|
31
|
+
id: string;
|
|
32
|
+
kind: 'env_var' | 'terminal' | 'agent' | null;
|
|
33
|
+
description?: string | null;
|
|
34
|
+
url?: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AcpInitializedPayload {
|
|
38
|
+
sessionId: string;
|
|
39
|
+
acpSessionId: string | null;
|
|
40
|
+
modes: Array<{ id: string; name: string; description?: string }>;
|
|
41
|
+
currentModeId: string | null;
|
|
42
|
+
currentModel: { id: string; name: string; provider?: string } | null;
|
|
43
|
+
availableModels: Array<{ id: string; name: string; provider?: string }>;
|
|
44
|
+
currentModelId: string | null;
|
|
45
|
+
configOptions: SessionConfigOptionData[];
|
|
46
|
+
promptCapabilities: { image: boolean; audio: boolean; embeddedContext: boolean } | null;
|
|
47
|
+
agentInfo: { name: string; version?: string | null } | null;
|
|
48
|
+
authMethods: AcpAuthMethodData[];
|
|
49
|
+
supportsAdditionalDirectories: boolean;
|
|
50
|
+
protocolVersion?: number | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
19
53
|
function toMcpServers(servers: AcpBridgeOptions['mcpServers']): acp.McpServer[] {
|
|
20
54
|
return (servers ?? []).map((s) => ({
|
|
21
55
|
name: s.name,
|
|
@@ -25,17 +59,104 @@ function toMcpServers(servers: AcpBridgeOptions['mcpServers']): acp.McpServer[]
|
|
|
25
59
|
}));
|
|
26
60
|
}
|
|
27
61
|
|
|
62
|
+
export function extractConfigOptions(
|
|
63
|
+
configOptions: acp.SessionConfigOption[] | null | undefined,
|
|
64
|
+
): SessionConfigOptionData[] {
|
|
65
|
+
if (!configOptions) return [];
|
|
66
|
+
return configOptions.map((opt) => {
|
|
67
|
+
const base = {
|
|
68
|
+
id: opt.id,
|
|
69
|
+
name: opt.name,
|
|
70
|
+
description: opt.description ?? null,
|
|
71
|
+
category: opt.category ?? null,
|
|
72
|
+
};
|
|
73
|
+
if (opt.type === 'boolean') {
|
|
74
|
+
return {
|
|
75
|
+
...base,
|
|
76
|
+
type: 'boolean' as const,
|
|
77
|
+
currentValue: String(opt.currentValue),
|
|
78
|
+
options: [],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const options = Array.isArray(opt.options)
|
|
82
|
+
? (opt.options as Array<acp.SessionConfigSelectOption | acp.SessionConfigSelectGroup>).flatMap((o) =>
|
|
83
|
+
'group' in o
|
|
84
|
+
? o.options.map((inner) => ({ value: inner.value, name: `${o.name}/${inner.name}` }))
|
|
85
|
+
: [{ value: o.value, name: o.name }],
|
|
86
|
+
)
|
|
87
|
+
: [];
|
|
88
|
+
return {
|
|
89
|
+
...base,
|
|
90
|
+
type: 'select' as const,
|
|
91
|
+
currentValue: opt.currentValue,
|
|
92
|
+
options,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function findConfigByCategory(
|
|
98
|
+
configOptions: SessionConfigOptionData[],
|
|
99
|
+
category: string,
|
|
100
|
+
): SessionConfigOptionData | null {
|
|
101
|
+
return configOptions.find((o) => o.category === category) ?? null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function extractConfigModes(
|
|
105
|
+
configOptions: SessionConfigOptionData[],
|
|
106
|
+
): { modes: Array<{ id: string; name: string }>; currentModeId: string | null } | null {
|
|
107
|
+
const opt = findConfigByCategory(configOptions, 'mode');
|
|
108
|
+
if (!opt || opt.type !== 'select') return null;
|
|
109
|
+
return {
|
|
110
|
+
modes: opt.options.map((o) => ({ id: o.value, name: o.name ?? o.value })),
|
|
111
|
+
currentModeId: opt.currentValue,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractConfigModels(
|
|
116
|
+
configOptions: SessionConfigOptionData[],
|
|
117
|
+
): {
|
|
118
|
+
availableModels: Array<{ id: string; name: string; provider?: string }>;
|
|
119
|
+
currentModelId: string | null;
|
|
120
|
+
currentModel: { id: string; name: string; provider?: string } | null;
|
|
121
|
+
} | null {
|
|
122
|
+
const opt = findConfigByCategory(configOptions, 'model');
|
|
123
|
+
if (!opt || opt.type !== 'select') return null;
|
|
124
|
+
const availableModels = opt.options.map((o) => ({ id: o.value, name: o.name ?? o.value }));
|
|
125
|
+
const currentModelId = opt.currentValue;
|
|
126
|
+
const current = availableModels.find((m) => m.id === currentModelId) ?? null;
|
|
127
|
+
return { availableModels, currentModelId, currentModel: current };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function toAuthMethods(authMethods: acp.AuthMethod[] | undefined): AcpAuthMethodData[] {
|
|
131
|
+
if (!authMethods || authMethods.length === 0) return [];
|
|
132
|
+
return authMethods.map((m) => {
|
|
133
|
+
if ('type' in m) {
|
|
134
|
+
const kind = m.type === 'env_var' ? 'env_var' : m.type === 'terminal' ? 'terminal' : 'agent';
|
|
135
|
+
return {
|
|
136
|
+
id: m.id,
|
|
137
|
+
kind,
|
|
138
|
+
description: (m as { description?: string | null }).description ?? null,
|
|
139
|
+
url: (m as { url?: string | null }).url ?? null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// AuthMethodAgent 无 type 字段(SDK 1.3.0),是默认认证方式
|
|
143
|
+
return { id: m.id, kind: 'agent', description: null, url: null };
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
28
147
|
export class AcpSessionBridge extends EventEmitter implements acp.Client {
|
|
29
148
|
private spawner: AcpSpawner;
|
|
30
149
|
private socket: Socket;
|
|
31
150
|
private sessionId: string;
|
|
32
151
|
private cwd: string;
|
|
152
|
+
private additionalDirectories: string[];
|
|
33
153
|
private mcpServers: AcpBridgeOptions['mcpServers'];
|
|
34
154
|
private connection: acp.ClientSideConnection | null = null;
|
|
35
155
|
private acpSessionId: string | null = null;
|
|
36
156
|
private activePrompt: Promise<acp.PromptResponse> | null = null;
|
|
37
157
|
private inputQueue: QueuedInput[] = [];
|
|
38
158
|
private textChunkBuffer = '';
|
|
159
|
+
private textChunkMessageId: string | null | undefined;
|
|
39
160
|
private textChunkTimer: ReturnType<typeof setTimeout> | null = null;
|
|
40
161
|
private initializing = false;
|
|
41
162
|
private stopped = false;
|
|
@@ -44,11 +165,20 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
|
|
|
44
165
|
timeout: NodeJS.Timeout;
|
|
45
166
|
}>();
|
|
46
167
|
|
|
168
|
+
// Capability snapshot from initialize / session/new / resume
|
|
169
|
+
private configOptions: SessionConfigOptionData[] = [];
|
|
170
|
+
private promptCapabilities: AcpInitializedPayload['promptCapabilities'] = null;
|
|
171
|
+
private agentInfo: AcpInitializedPayload['agentInfo'] = null;
|
|
172
|
+
private authMethods: AcpAuthMethodData[] = [];
|
|
173
|
+
private supportsAdditionalDirectories = false;
|
|
174
|
+
private agentProtocolVersion: number | null = null;
|
|
175
|
+
|
|
47
176
|
constructor(socket: Socket, options: AcpBridgeOptions, spawner?: AcpSpawner) {
|
|
48
177
|
super();
|
|
49
178
|
this.socket = socket;
|
|
50
179
|
this.sessionId = options.sessionId;
|
|
51
180
|
this.cwd = options.cwd;
|
|
181
|
+
this.additionalDirectories = options.additionalDirectories ?? [];
|
|
52
182
|
this.mcpServers = options.mcpServers;
|
|
53
183
|
this.spawner = spawner ?? new AcpSpawner({ sessionId: options.sessionId, cwd: options.cwd });
|
|
54
184
|
}
|
|
@@ -63,20 +193,12 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
|
|
|
63
193
|
const result = await this.connection!.newSession({
|
|
64
194
|
cwd: this.cwd,
|
|
65
195
|
mcpServers: toMcpServers(this.mcpServers),
|
|
196
|
+
additionalDirectories: this.additionalDirectories.length > 0 ? this.additionalDirectories : undefined,
|
|
66
197
|
});
|
|
67
198
|
this.acpSessionId = result.sessionId;
|
|
68
199
|
this.initializing = false;
|
|
69
200
|
|
|
70
|
-
this.emit('acp:initialized',
|
|
71
|
-
sessionId: this.sessionId,
|
|
72
|
-
acpSessionId: this.acpSessionId,
|
|
73
|
-
modes: this.extractModes(result.modes),
|
|
74
|
-
currentModeId: result.modes?.currentModeId ?? null,
|
|
75
|
-
currentModel: this.extractCurrentModel(result.models),
|
|
76
|
-
availableModels: this.extractAvailableModels(result.models),
|
|
77
|
-
currentModelId: result.models?.currentModelId ?? null,
|
|
78
|
-
});
|
|
79
|
-
|
|
201
|
+
this.emit('acp:initialized', this.buildInitializedPayload(result));
|
|
80
202
|
this.flushInputQueue();
|
|
81
203
|
} catch (error) {
|
|
82
204
|
this.initializing = false;
|
|
@@ -94,19 +216,12 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
|
|
|
94
216
|
const result = await this.connection!.resumeSession({
|
|
95
217
|
sessionId: externalSessionId,
|
|
96
218
|
cwd: this.cwd,
|
|
219
|
+
additionalDirectories: this.additionalDirectories.length > 0 ? this.additionalDirectories : undefined,
|
|
97
220
|
});
|
|
98
221
|
this.acpSessionId = externalSessionId;
|
|
99
222
|
this.initializing = false;
|
|
100
223
|
|
|
101
|
-
this.emit('acp:initialized',
|
|
102
|
-
sessionId: this.sessionId,
|
|
103
|
-
acpSessionId: this.acpSessionId,
|
|
104
|
-
modes: this.extractModes(result.modes),
|
|
105
|
-
currentModeId: result.modes?.currentModeId ?? null,
|
|
106
|
-
currentModel: this.extractCurrentModel(result.models),
|
|
107
|
-
availableModels: this.extractAvailableModels(result.models),
|
|
108
|
-
currentModelId: result.models?.currentModelId ?? null,
|
|
109
|
-
});
|
|
224
|
+
this.emit('acp:initialized', this.buildInitializedPayload(result));
|
|
110
225
|
} catch (error) {
|
|
111
226
|
// resumeSession may fail if agent doesn't support session.resume
|
|
112
227
|
// fallback to newSession
|
|
@@ -114,19 +229,12 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
|
|
|
114
229
|
const result = await this.connection!.newSession({
|
|
115
230
|
cwd: this.cwd,
|
|
116
231
|
mcpServers: toMcpServers(this.mcpServers),
|
|
232
|
+
additionalDirectories: this.additionalDirectories.length > 0 ? this.additionalDirectories : undefined,
|
|
117
233
|
});
|
|
118
234
|
this.acpSessionId = result.sessionId;
|
|
119
235
|
this.initializing = false;
|
|
120
236
|
|
|
121
|
-
this.emit('acp:initialized',
|
|
122
|
-
sessionId: this.sessionId,
|
|
123
|
-
acpSessionId: this.acpSessionId,
|
|
124
|
-
modes: this.extractModes(result.modes),
|
|
125
|
-
currentModeId: result.modes?.currentModeId ?? null,
|
|
126
|
-
currentModel: this.extractCurrentModel(result.models),
|
|
127
|
-
availableModels: this.extractAvailableModels(result.models),
|
|
128
|
-
currentModelId: result.models?.currentModelId ?? null,
|
|
129
|
-
});
|
|
237
|
+
this.emit('acp:initialized', this.buildInitializedPayload(result));
|
|
130
238
|
} catch (fallbackError) {
|
|
131
239
|
this.initializing = false;
|
|
132
240
|
this.handleError(fallbackError);
|
|
@@ -155,6 +263,16 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
|
|
|
155
263
|
|
|
156
264
|
async setMode(modeId: string): Promise<void> {
|
|
157
265
|
if (!this.connection || !this.acpSessionId) return;
|
|
266
|
+
const modeConfig = findConfigByCategory(this.configOptions, 'mode');
|
|
267
|
+
if (modeConfig) {
|
|
268
|
+
// 配置项通道:Agent 提供 configOptions 时以配置项为准
|
|
269
|
+
await this.connection.setSessionConfigOption({
|
|
270
|
+
sessionId: this.acpSessionId,
|
|
271
|
+
configId: modeConfig.id,
|
|
272
|
+
value: modeId,
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
158
276
|
await this.connection.setSessionMode({
|
|
159
277
|
sessionId: this.acpSessionId,
|
|
160
278
|
modeId,
|
|
@@ -163,13 +281,70 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
|
|
|
163
281
|
|
|
164
282
|
async setModel(modelId: string): Promise<void> {
|
|
165
283
|
if (!this.connection || !this.acpSessionId) return;
|
|
166
|
-
|
|
284
|
+
const modelConfig = findConfigByCategory(this.configOptions, 'model');
|
|
285
|
+
if (modelConfig) {
|
|
286
|
+
await this.connection.setSessionConfigOption({
|
|
287
|
+
sessionId: this.acpSessionId,
|
|
288
|
+
configId: modelConfig.id,
|
|
289
|
+
value: modelId,
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// SDK 1.x 已移除 unstable_setSessionModel:无 configOptions 的旧 Agent 无法走配置项通道。
|
|
294
|
+
// 模型切换暂不支持,由上层提示(不保留 unstable 调用)。
|
|
295
|
+
throw new Error('Model switching is not supported: agent does not expose a model config option');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async setConfigOption(configId: string, value: string | boolean): Promise<acp.SetSessionConfigOptionResponse> {
|
|
299
|
+
if (!this.connection || !this.acpSessionId) {
|
|
300
|
+
throw new Error('ACP bridge is not initialized');
|
|
301
|
+
}
|
|
302
|
+
const response = await this.connection.setSessionConfigOption({
|
|
167
303
|
sessionId: this.acpSessionId,
|
|
168
|
-
|
|
304
|
+
configId,
|
|
305
|
+
...(typeof value === 'boolean' ? { value, type: 'boolean' as const } : { value }),
|
|
169
306
|
});
|
|
307
|
+
if (response.configOptions) {
|
|
308
|
+
// SDK 1.3.0:set_config_option 响应携带全量快照,与 config_option_update 通知同源
|
|
309
|
+
this.configOptions = extractConfigOptions(response.configOptions);
|
|
310
|
+
this.socket.emit('acp:config-options', {
|
|
311
|
+
sessionId: this.sessionId,
|
|
312
|
+
configOptions: this.configOptions,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return response;
|
|
170
316
|
}
|
|
171
317
|
|
|
172
|
-
|
|
318
|
+
async authenticate(methodId: string): Promise<void> {
|
|
319
|
+
if (!this.connection) {
|
|
320
|
+
throw new Error('ACP bridge is not initialized');
|
|
321
|
+
}
|
|
322
|
+
await this.connection.authenticate({ methodId });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async logout(): Promise<void> {
|
|
326
|
+
if (!this.connection) {
|
|
327
|
+
throw new Error('ACP bridge is not initialized');
|
|
328
|
+
}
|
|
329
|
+
await this.connection.logout({});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async deleteSession(): Promise<void> {
|
|
333
|
+
if (!this.connection || !this.acpSessionId) {
|
|
334
|
+
throw new Error('ACP bridge is not initialized');
|
|
335
|
+
}
|
|
336
|
+
await this.connection.deleteSession({ sessionId: this.acpSessionId });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
getConfigOptions(): SessionConfigOptionData[] {
|
|
340
|
+
return this.configOptions;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
getSupportsAdditionalDirectories(): boolean {
|
|
344
|
+
return this.supportsAdditionalDirectories;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
sendInput(message: { type: string; content: string; attachments?: Array<Record<string, unknown>> }): void {
|
|
173
348
|
if (this.activePrompt) {
|
|
174
349
|
this.inputQueue.push(message);
|
|
175
350
|
return;
|
|
@@ -177,12 +352,18 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
|
|
|
177
352
|
this.processPrompt(message);
|
|
178
353
|
}
|
|
179
354
|
|
|
180
|
-
private async processPrompt(message: QueuedInput): Promise<void> {
|
|
355
|
+
private async processPrompt(message: QueuedInput & { attachments?: Array<Record<string, unknown>> }): Promise<void> {
|
|
181
356
|
if (!this.connection || !this.acpSessionId || this.stopped) return;
|
|
182
357
|
|
|
358
|
+
const prompt: acp.ContentBlock[] = [];
|
|
359
|
+
for (const attachment of message.attachments ?? []) {
|
|
360
|
+
prompt.push(attachment as acp.ContentBlock);
|
|
361
|
+
}
|
|
362
|
+
prompt.push({ type: 'text' as const, text: message.content });
|
|
363
|
+
|
|
183
364
|
this.activePrompt = this.connection.prompt({
|
|
184
365
|
sessionId: this.acpSessionId,
|
|
185
|
-
prompt
|
|
366
|
+
prompt,
|
|
186
367
|
});
|
|
187
368
|
|
|
188
369
|
this.socket.emit('acp:prompt:start', { sessionId: this.sessionId });
|
|
@@ -212,44 +393,106 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
|
|
|
212
393
|
const output = this.spawner.stdout;
|
|
213
394
|
const stream = acp.ndJsonStream(input, output);
|
|
214
395
|
this.connection = new acp.ClientSideConnection(() => this, stream);
|
|
215
|
-
await this.connection.initialize({
|
|
396
|
+
const result = await this.connection.initialize({
|
|
216
397
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
217
|
-
clientCapabilities: {
|
|
398
|
+
clientCapabilities: {
|
|
399
|
+
session: {
|
|
400
|
+
configOptions: { boolean: {} },
|
|
401
|
+
},
|
|
402
|
+
},
|
|
218
403
|
clientInfo: {
|
|
219
404
|
name: 'agent-gear',
|
|
220
|
-
version: '1.
|
|
405
|
+
version: '1.1.5',
|
|
221
406
|
},
|
|
222
407
|
});
|
|
223
|
-
}
|
|
224
408
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
409
|
+
this.agentProtocolVersion = result.protocolVersion ?? null;
|
|
410
|
+
if (result.protocolVersion != null && result.protocolVersion !== ACP_PROTOCOL_VERSION) {
|
|
411
|
+
// 版本不兼容:不断连,由上层提示用户
|
|
412
|
+
this.socket.emit('acp:protocol-version-mismatch', {
|
|
413
|
+
sessionId: this.sessionId,
|
|
414
|
+
expectedVersion: ACP_PROTOCOL_VERSION,
|
|
415
|
+
agentVersion: result.protocolVersion,
|
|
416
|
+
});
|
|
228
417
|
}
|
|
229
|
-
}
|
|
230
418
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
419
|
+
this.promptCapabilities = result.agentCapabilities?.promptCapabilities
|
|
420
|
+
? {
|
|
421
|
+
image: result.agentCapabilities.promptCapabilities.image ?? false,
|
|
422
|
+
audio: result.agentCapabilities.promptCapabilities.audio ?? false,
|
|
423
|
+
embeddedContext: result.agentCapabilities.promptCapabilities.embeddedContext ?? false,
|
|
424
|
+
}
|
|
425
|
+
: null;
|
|
426
|
+
this.agentInfo = result.agentInfo
|
|
427
|
+
? { name: result.agentInfo.name, version: result.agentInfo.version ?? null }
|
|
428
|
+
: null;
|
|
429
|
+
this.authMethods = toAuthMethods(result.authMethods);
|
|
430
|
+
this.supportsAdditionalDirectories =
|
|
431
|
+
result.agentCapabilities?.sessionCapabilities?.additionalDirectories !== undefined &&
|
|
432
|
+
result.agentCapabilities?.sessionCapabilities?.additionalDirectories !== null;
|
|
433
|
+
|
|
434
|
+
if (this.authMethods.length > 0) {
|
|
435
|
+
this.socket.emit('acp:auth:required', {
|
|
436
|
+
sessionId: this.sessionId,
|
|
437
|
+
authMethods: this.authMethods,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
238
440
|
}
|
|
239
441
|
|
|
240
|
-
private
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
442
|
+
private buildInitializedPayload(
|
|
443
|
+
result: acp.NewSessionResponse | acp.ResumeSessionResponse,
|
|
444
|
+
): AcpInitializedPayload {
|
|
445
|
+
const configOptions = extractConfigOptions(result.configOptions);
|
|
446
|
+
this.configOptions = configOptions.length > 0 ? configOptions : this.configOptions;
|
|
447
|
+
|
|
448
|
+
// 协议要求:响应含 configOptions 时以其为准(忽略 modes)
|
|
449
|
+
const configModes = extractConfigModes(this.configOptions);
|
|
450
|
+
const configModels = extractConfigModels(this.configOptions);
|
|
451
|
+
|
|
452
|
+
let modes: Array<{ id: string; name: string; description?: string }> = [];
|
|
453
|
+
let currentModeId: string | null = null;
|
|
454
|
+
if (configModes) {
|
|
455
|
+
modes = configModes.modes;
|
|
456
|
+
currentModeId = configModes.currentModeId;
|
|
457
|
+
} else if (result.modes?.availableModes) {
|
|
458
|
+
modes = result.modes.availableModes.map((m) => ({
|
|
459
|
+
id: m.id,
|
|
460
|
+
name: m.name,
|
|
461
|
+
description: m.description ?? undefined,
|
|
462
|
+
}));
|
|
463
|
+
currentModeId = result.modes.currentModeId ?? null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let currentModel: AcpInitializedPayload['currentModel'] = null;
|
|
467
|
+
let availableModels: Array<{ id: string; name: string; provider?: string }> = [];
|
|
468
|
+
let currentModelId: string | null = null;
|
|
469
|
+
if (configModels) {
|
|
470
|
+
availableModels = configModels.availableModels;
|
|
471
|
+
currentModelId = configModels.currentModelId;
|
|
472
|
+
currentModel = configModels.currentModel;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
sessionId: this.sessionId,
|
|
477
|
+
acpSessionId: this.acpSessionId,
|
|
478
|
+
modes,
|
|
479
|
+
currentModeId,
|
|
480
|
+
currentModel,
|
|
481
|
+
availableModels,
|
|
482
|
+
currentModelId,
|
|
483
|
+
configOptions: this.configOptions,
|
|
484
|
+
promptCapabilities: this.promptCapabilities,
|
|
485
|
+
agentInfo: this.agentInfo,
|
|
486
|
+
authMethods: this.authMethods,
|
|
487
|
+
supportsAdditionalDirectories: this.supportsAdditionalDirectories,
|
|
488
|
+
protocolVersion: this.agentProtocolVersion,
|
|
489
|
+
};
|
|
245
490
|
}
|
|
246
491
|
|
|
247
|
-
private
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
name: m.name,
|
|
252
|
-
}));
|
|
492
|
+
private flushInputQueue(): void {
|
|
493
|
+
while (this.inputQueue.length > 0) {
|
|
494
|
+
this.processPrompt(this.inputQueue.shift()!);
|
|
495
|
+
}
|
|
253
496
|
}
|
|
254
497
|
|
|
255
498
|
// ── acp.Client implementation ──
|
|
@@ -259,8 +502,22 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
|
|
|
259
502
|
switch (update.sessionUpdate) {
|
|
260
503
|
case 'agent_message_chunk':
|
|
261
504
|
if (update.content.type === 'text') {
|
|
505
|
+
if (this.textChunkBuffer && this.textChunkMessageId !== update.messageId) {
|
|
506
|
+
// messageId 边界:不同消息立即 flush
|
|
507
|
+
this.flushTextBuffer();
|
|
508
|
+
}
|
|
262
509
|
this.textChunkBuffer += update.content.text;
|
|
263
|
-
this.
|
|
510
|
+
this.textChunkMessageId = update.messageId;
|
|
511
|
+
if (update.messageId) {
|
|
512
|
+
// 有 messageId 时跳过 50ms debounce 猜测,按 ID 边界 flush
|
|
513
|
+
this.flushTextBuffer();
|
|
514
|
+
} else {
|
|
515
|
+
this.scheduleTextFlush();
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
// 非 text chunk(image/audio/resource)原样透传
|
|
519
|
+
this.flushTextBuffer();
|
|
520
|
+
this.sendUpdate(update);
|
|
264
521
|
}
|
|
265
522
|
break;
|
|
266
523
|
case 'tool_call':
|
|
@@ -270,6 +527,14 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
|
|
|
270
527
|
this.flushTextBuffer();
|
|
271
528
|
this.sendUpdate(update);
|
|
272
529
|
break;
|
|
530
|
+
case 'config_option_update':
|
|
531
|
+
this.flushTextBuffer();
|
|
532
|
+
this.configOptions = extractConfigOptions(update.configOptions);
|
|
533
|
+
this.socket.emit('acp:config-options', {
|
|
534
|
+
sessionId: this.sessionId,
|
|
535
|
+
configOptions: this.configOptions,
|
|
536
|
+
});
|
|
537
|
+
break;
|
|
273
538
|
case 'user_message_chunk':
|
|
274
539
|
break;
|
|
275
540
|
default:
|
|
@@ -331,8 +596,10 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
|
|
|
331
596
|
this.sendUpdate({
|
|
332
597
|
sessionUpdate: 'agent_message_chunk' as const,
|
|
333
598
|
content: { type: 'text' as const, text: this.textChunkBuffer },
|
|
599
|
+
...(this.textChunkMessageId ? { messageId: this.textChunkMessageId } : {}),
|
|
334
600
|
} as acp.SessionUpdate);
|
|
335
601
|
this.textChunkBuffer = '';
|
|
602
|
+
this.textChunkMessageId = undefined;
|
|
336
603
|
}
|
|
337
604
|
}
|
|
338
605
|
|
package/src/acp/detector.ts
CHANGED
|
@@ -11,14 +11,16 @@ interface AcpServerInfo {
|
|
|
11
11
|
|
|
12
12
|
export class AcpDetector {
|
|
13
13
|
async detect(): Promise<AcpServerInfo[]> {
|
|
14
|
-
const [opencode, copilot, claudeCode, geminiCli, codex] = await Promise.all([
|
|
14
|
+
const [opencode, copilot, claudeCode, geminiCli, codex, mimoCode, kimiCode] = await Promise.all([
|
|
15
15
|
this.detectOpenCode(),
|
|
16
16
|
this.detectGitHubCopilot(),
|
|
17
17
|
this.detectClaudeCode(),
|
|
18
18
|
this.detectGeminiCli(),
|
|
19
19
|
this.detectCodex(),
|
|
20
|
+
this.detectMiMoCode(),
|
|
21
|
+
this.detectKimiCode(),
|
|
20
22
|
]);
|
|
21
|
-
return [opencode, copilot, claudeCode, geminiCli, codex];
|
|
23
|
+
return [opencode, copilot, claudeCode, geminiCli, codex, mimoCode, kimiCode];
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
private async detectOpenCode(): Promise<AcpServerInfo> {
|
|
@@ -71,6 +73,26 @@ export class AcpDetector {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
private async detectMiMoCode(): Promise<AcpServerInfo> {
|
|
77
|
+
try {
|
|
78
|
+
const { stdout } = await execAsync('mimo --version');
|
|
79
|
+
const version = this.parseVersion(stdout);
|
|
80
|
+
return { type: 'mimo_code', available: true, version };
|
|
81
|
+
} catch {
|
|
82
|
+
return { type: 'mimo_code', available: false };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async detectKimiCode(): Promise<AcpServerInfo> {
|
|
87
|
+
try {
|
|
88
|
+
const { stdout } = await execAsync('kimi --version');
|
|
89
|
+
const version = this.parseVersion(stdout);
|
|
90
|
+
return { type: 'kimi_code', available: true, version };
|
|
91
|
+
} catch {
|
|
92
|
+
return { type: 'kimi_code', available: false };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
74
96
|
getSpawnerConfig(serverType: string): { command: string; args: string[] } {
|
|
75
97
|
switch (serverType) {
|
|
76
98
|
case 'github_copilot':
|
|
@@ -81,6 +103,10 @@ export class AcpDetector {
|
|
|
81
103
|
return { command: 'gemini', args: ['--acp'] };
|
|
82
104
|
case 'codex':
|
|
83
105
|
return { command: 'codex-acp', args: [] };
|
|
106
|
+
case 'mimo_code':
|
|
107
|
+
return { command: 'mimo', args: ['acp'] };
|
|
108
|
+
case 'kimi_code':
|
|
109
|
+
return { command: 'kimi', args: ['acp'] };
|
|
84
110
|
case 'opencode':
|
|
85
111
|
default:
|
|
86
112
|
return { command: 'opencode', args: ['acp'] };
|
package/src/acp/sync-bridge.ts
CHANGED
|
@@ -29,6 +29,7 @@ export class AcpSyncBridge implements acp.Client {
|
|
|
29
29
|
private spawner: AcpSpawner;
|
|
30
30
|
private connection: acp.ClientSideConnection | null = null;
|
|
31
31
|
private updates: acp.SessionNotification[] = [];
|
|
32
|
+
private capabilities: acp.AgentCapabilities | null = null;
|
|
32
33
|
|
|
33
34
|
constructor(options: AcpSyncBridgeOptions, spawner?: AcpSpawner) {
|
|
34
35
|
this.spawner = spawner ?? new AcpSpawner({
|
|
@@ -53,15 +54,30 @@ export class AcpSyncBridge implements acp.Client {
|
|
|
53
54
|
clientInfo: { name: 'agent-gear', version: '1.0.0' },
|
|
54
55
|
});
|
|
55
56
|
|
|
56
|
-
await withTimeout(
|
|
57
|
+
const result = await withTimeout(
|
|
57
58
|
Promise.race([initPromise, exitPromise]),
|
|
58
59
|
timeoutMs,
|
|
59
60
|
`ACP connect timed out after ${timeoutMs}ms`,
|
|
60
61
|
);
|
|
62
|
+
this.capabilities = result.agentCapabilities ?? null;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
return this.
|
|
65
|
+
supportsDelete(): boolean {
|
|
66
|
+
return Boolean(this.capabilities?.sessionCapabilities?.delete);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async listSessions(cwd: string, cursor?: string | null): Promise<acp.ListSessionsResponse> {
|
|
70
|
+
if (!this.connection) {
|
|
71
|
+
throw new Error('ACP connection not established');
|
|
72
|
+
}
|
|
73
|
+
return this.connection.listSessions({ cwd, ...(cursor ? { cursor } : {}) });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async deleteSession(sessionId: string): Promise<void> {
|
|
77
|
+
if (!this.connection) {
|
|
78
|
+
throw new Error('ACP connection not established');
|
|
79
|
+
}
|
|
80
|
+
await this.connection.deleteSession({ sessionId });
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
async loadSession(
|