@linkshell/host 0.4.19 → 0.4.21
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/README.md +2 -2
- package/dist/host/src/acp-settings.d.ts +12 -0
- package/dist/host/src/acp-settings.js +41 -0
- package/dist/host/src/acp-settings.js.map +1 -0
- package/dist/host/src/carry.js +13 -2
- package/dist/host/src/carry.js.map +1 -1
- package/dist/host/src/drivers/acp/client-services.d.ts +29 -0
- package/dist/host/src/drivers/acp/client-services.js +192 -0
- package/dist/host/src/drivers/acp/client-services.js.map +1 -0
- package/dist/host/src/drivers/acp/connection.d.ts +37 -8
- package/dist/host/src/drivers/acp/connection.js +127 -110
- package/dist/host/src/drivers/acp/connection.js.map +1 -1
- package/dist/host/src/drivers/acp/driver.d.ts +92 -2
- package/dist/host/src/drivers/acp/driver.js +599 -52
- package/dist/host/src/drivers/acp/driver.js.map +1 -1
- package/dist/host/src/drivers/acp/editor.d.ts +31 -0
- package/dist/host/src/drivers/acp/editor.js +242 -0
- package/dist/host/src/drivers/acp/editor.js.map +1 -0
- package/dist/host/src/drivers/acp/mapper.d.ts +2 -1
- package/dist/host/src/drivers/acp/mapper.js +88 -35
- package/dist/host/src/drivers/acp/mapper.js.map +1 -1
- package/dist/host/src/drivers/acp/mcp.d.ts +20 -0
- package/dist/host/src/drivers/acp/mcp.js +139 -0
- package/dist/host/src/drivers/acp/mcp.js.map +1 -0
- package/dist/host/src/drivers/acp/questions.js +14 -0
- package/dist/host/src/drivers/acp/questions.js.map +1 -1
- package/dist/host/src/drivers/acp/updates.d.ts +10 -0
- package/dist/host/src/drivers/acp/updates.js +137 -0
- package/dist/host/src/drivers/acp/updates.js.map +1 -0
- package/dist/host/src/drivers/claude/activity.d.ts +1 -0
- package/dist/host/src/drivers/claude/activity.js +17 -3
- package/dist/host/src/drivers/claude/activity.js.map +1 -1
- package/dist/host/src/drivers/claude/children.d.ts +7 -0
- package/dist/host/src/drivers/claude/children.js +31 -2
- package/dist/host/src/drivers/claude/children.js.map +1 -1
- package/dist/host/src/drivers/claude/driver.js +1 -0
- package/dist/host/src/drivers/claude/driver.js.map +1 -1
- package/dist/host/src/drivers/claude/tasks.d.ts +3 -3
- package/dist/host/src/drivers/claude/transcript.d.ts +497 -38
- package/dist/host/src/drivers/codex/driver.d.ts +3 -0
- package/dist/host/src/drivers/codex/driver.js +36 -1
- package/dist/host/src/drivers/codex/driver.js.map +1 -1
- package/dist/host/src/drivers/registry.d.ts +6 -0
- package/dist/host/src/drivers/registry.js +8 -0
- package/dist/host/src/drivers/registry.js.map +1 -1
- package/dist/host/src/drivers/types.d.ts +34 -1
- package/dist/host/src/fs.d.ts +3 -3
- package/dist/host/src/host.js +21 -3
- package/dist/host/src/host.js.map +1 -1
- package/dist/host/src/hub.d.ts +249 -2
- package/dist/host/src/hub.js +150 -9
- package/dist/host/src/hub.js.map +1 -1
- package/dist/host/src/input.d.ts +5 -3
- package/dist/host/src/input.js +8 -6
- package/dist/host/src/input.js.map +1 -1
- package/dist/host/src/questions.d.ts +3 -1
- package/dist/host/src/questions.js +33 -7
- package/dist/host/src/questions.js.map +1 -1
- package/dist/host/src/rpc/server.js +14 -0
- package/dist/host/src/rpc/server.js.map +1 -1
- package/dist/host/src/screen-metrics.d.ts +3 -0
- package/dist/host/src/screen-metrics.js +27 -0
- package/dist/host/src/screen-metrics.js.map +1 -0
- package/dist/host/src/screen.js +7 -6
- package/dist/host/src/screen.js.map +1 -1
- package/dist/host/src/slim.js +15 -4
- package/dist/host/src/slim.js.map +1 -1
- package/dist/host/src/store.js +7 -2
- package/dist/host/src/store.js.map +1 -1
- package/dist/host/src/terminals.d.ts +8 -3
- package/dist/host/src/terminals.js +3 -2
- package/dist/host/src/terminals.js.map +1 -1
- package/dist/host/tsconfig.tsbuildinfo +1 -1
- package/dist/wire/src/acp.d.ts +510 -0
- package/dist/wire/src/acp.js +39 -0
- package/dist/wire/src/acp.js.map +1 -0
- package/dist/wire/src/async-questions.d.ts +2 -2
- package/dist/wire/src/computer-preview.d.ts +2 -2
- package/dist/wire/src/content.d.ts +465 -0
- package/dist/wire/src/content.js +58 -0
- package/dist/wire/src/content.js.map +1 -0
- package/dist/wire/src/editor.d.ts +702 -0
- package/dist/wire/src/editor.js +23 -0
- package/dist/wire/src/editor.js.map +1 -0
- package/dist/wire/src/index.d.ts +3 -0
- package/dist/wire/src/index.js +3 -0
- package/dist/wire/src/index.js.map +1 -1
- package/dist/wire/src/model.d.ts +1383 -79
- package/dist/wire/src/model.js +20 -1
- package/dist/wire/src/model.js.map +1 -1
- package/dist/wire/src/peer.d.ts +1 -0
- package/dist/wire/src/peer.js +22 -6
- package/dist/wire/src/peer.js.map +1 -1
- package/dist/wire/src/questions.d.ts +3 -0
- package/dist/wire/src/questions.js +65 -0
- package/dist/wire/src/questions.js.map +1 -0
- package/dist/wire/src/relay.d.ts +4 -4
- package/dist/wire/src/rpc.d.ts +19023 -2518
- package/dist/wire/src/rpc.js +41 -3
- package/dist/wire/src/rpc.js.map +1 -1
- package/dist/wire/src/updates.d.ts +3848 -1114
- package/dist/wire/src/updates.js +42 -40
- package/dist/wire/src/updates.js.map +1 -1
- package/dist/wire/src/workflow.d.ts +22 -22
- package/package.json +4 -3
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import {
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { realpath, stat } from "node:fs/promises";
|
|
5
|
+
import { isAbsolute } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { RpcError, acpAgentSettingsSchema, acpProviderSchema, questionAnswerError, } from "@linkshell/wire";
|
|
8
|
+
import { AcpClientServices } from "./client-services.js";
|
|
9
|
+
import { AcpUpdates, usageUpdate } from "./updates.js";
|
|
10
|
+
import { AcpMcpBridge } from "./mcp.js";
|
|
11
|
+
import { AcpEditor } from "./editor.js";
|
|
4
12
|
import { AcpConnection, AcpProtocolError } from "./connection.js";
|
|
5
13
|
import { mapAcpQuestions, questionMethod } from "./questions.js";
|
|
6
14
|
import { AcpItemTracker, mapPermissionRequest, normalizeAcpUpdate, toAcpPrompt, withContext, toConfigOptions, toHistory, toStopReason, } from "./mapper.js";
|
|
@@ -39,6 +47,7 @@ export class AcpDriver {
|
|
|
39
47
|
label;
|
|
40
48
|
tier;
|
|
41
49
|
capabilities = {
|
|
50
|
+
acp: true,
|
|
42
51
|
interrupt: true,
|
|
43
52
|
steer: false,
|
|
44
53
|
permissions: true,
|
|
@@ -58,12 +67,50 @@ export class AcpDriver {
|
|
|
58
67
|
stopped = false;
|
|
59
68
|
nextPermissionId = 1;
|
|
60
69
|
authenticating;
|
|
70
|
+
credentialsChanged = false;
|
|
71
|
+
services;
|
|
72
|
+
mcp;
|
|
73
|
+
edits;
|
|
74
|
+
serviceRequests = new Map();
|
|
75
|
+
terminalCalls = new Map();
|
|
76
|
+
interactions = new Map();
|
|
77
|
+
children = new Map();
|
|
78
|
+
discoveredDirectories = new Map();
|
|
61
79
|
constructor(spec, options) {
|
|
62
80
|
this.spec = spec;
|
|
63
81
|
this.options = options;
|
|
64
82
|
this.id = spec.id;
|
|
65
83
|
this.label = spec.label;
|
|
66
84
|
this.tier = spec.tier;
|
|
85
|
+
this.services = new AcpClientServices({ env: this.env,
|
|
86
|
+
scope: (id) => { const state = this.sessions.get(id); return state ? { cwd: state.cwd, additionalDirectories: state.settings.additionalDirectories } : undefined; },
|
|
87
|
+
output: (id, terminalId, text, snapshot) => {
|
|
88
|
+
for (const call of this.terminalCalls.get(`${id}\n${terminalId}`) ?? []) {
|
|
89
|
+
// The first terminal reference carries the backlog; later output is already a delta.
|
|
90
|
+
if (text)
|
|
91
|
+
this.emit(id, { sessionUpdate: "tool_call_update", toolCallId: call, appendOutput: text });
|
|
92
|
+
if (snapshot.exitStatus)
|
|
93
|
+
this.emit(id, { sessionUpdate: "tool_call_update", toolCallId: call, rawOutput: { truncated: snapshot.truncated, ...snapshot.exitStatus } });
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
this.mcp = new AcpMcpBridge({ env: this.env,
|
|
98
|
+
notify: (params) => this.connection?.notify("mcp/message", params),
|
|
99
|
+
request: async (sessionId, method, params, id) => {
|
|
100
|
+
if (method === "ping")
|
|
101
|
+
return {};
|
|
102
|
+
const state = sessionId ? this.sessions.get(sessionId) : undefined;
|
|
103
|
+
if (method === "roots/list" && state)
|
|
104
|
+
return { roots: [state.cwd, ...state.settings.additionalDirectories].map((path) => ({ uri: pathToFileURL(path).href, name: path })) };
|
|
105
|
+
if (method === "elicitation/create" && state)
|
|
106
|
+
return this.onQuestions(method, { mode: "form", ...params, sessionId }, id);
|
|
107
|
+
throw new RpcError(-32601, `MCP 客户端不支持 ${method}`);
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
this.edits = new AcpEditor({ capabilities: () => this.connection?.capabilities ?? {}, settings: () => this.settings(),
|
|
111
|
+
request: (method, params) => this.connection?.request(method, params, method === "nes/close" ? 5000 : 30_000) ?? Promise.reject(RpcError.app("offline", "ACP 连接已关闭")),
|
|
112
|
+
notify: (method, params) => this.connection?.notify(method, params),
|
|
113
|
+
});
|
|
67
114
|
}
|
|
68
115
|
status() {
|
|
69
116
|
return this.current;
|
|
@@ -95,8 +142,173 @@ export class AcpDriver {
|
|
|
95
142
|
clearTimeout(this.restartTimer);
|
|
96
143
|
for (const state of this.sessions.values())
|
|
97
144
|
this.cancelPermissions(state);
|
|
145
|
+
for (const id of [...this.interactions.keys()])
|
|
146
|
+
this.resolveInteraction(id, "cancel");
|
|
147
|
+
await this.edits.close();
|
|
98
148
|
await this.connection?.stop();
|
|
99
149
|
this.connection = undefined;
|
|
150
|
+
for (const controller of this.serviceRequests.values())
|
|
151
|
+
controller.abort();
|
|
152
|
+
this.serviceRequests.clear();
|
|
153
|
+
this.terminalCalls.clear();
|
|
154
|
+
await this.services.close();
|
|
155
|
+
await this.mcp.close();
|
|
156
|
+
}
|
|
157
|
+
async acpInfo(nativeId) {
|
|
158
|
+
await this.ensureStarted();
|
|
159
|
+
if (!this.connection?.alive)
|
|
160
|
+
throw RpcError.app("agent_unavailable", this.current.problem ?? "ACP 启动失败");
|
|
161
|
+
const caps = this.connection.capabilities;
|
|
162
|
+
return { features: {
|
|
163
|
+
protocolVersion: this.connection.protocolVersion,
|
|
164
|
+
authMethods: (this.connection.initializeResult.authMethods ?? []).filter((method) => !method.type || ["terminal", "agent"].includes(method.type)).map((method) => ({ id: method.id, name: method.name ?? method.id, description: method.description ?? undefined, type: method.type === "terminal" ? "terminal" : "agent" })),
|
|
165
|
+
logout: this.connection.protocolVersion === 2 ? caps.auth !== undefined : Boolean(caps.auth?.logout), providers: Boolean(caps.providers) && this.settings().experimental,
|
|
166
|
+
nes: Boolean(caps.nes) && this.settings().experimental,
|
|
167
|
+
additionalDirectories: Boolean(caps.sessionCapabilities?.additionalDirectories), mcpHttp: caps.mcpCapabilities?.http === true, mcpSse: caps.mcpCapabilities?.sse === true,
|
|
168
|
+
}, settings: nativeId ? this.sessions.get(nativeId)?.settings ?? this.settings() : this.settings(), interactions: this.pendingInteractions() };
|
|
169
|
+
}
|
|
170
|
+
async editor(input) { await this.ensureStarted(); return this.edits.run(input); }
|
|
171
|
+
pendingInteractions() { return [...this.interactions.values()].map((entry) => entry.request.update); }
|
|
172
|
+
async configureAcp(settings, nativeId) {
|
|
173
|
+
settings = acpAgentSettingsSchema.parse(settings);
|
|
174
|
+
await this.validateDirectories(settings);
|
|
175
|
+
const affected = nativeId ? [this.sessions.get(nativeId)].filter((state) => !!state) : [...this.sessions.values()];
|
|
176
|
+
if (affected.some((state) => state.turnActive || state.inflight || state.permissions.size || state.questions.size) || this.interactions.size)
|
|
177
|
+
throw RpcError.app("busy", "请先结束运行和待处理的交互,再修改 ACP 配置");
|
|
178
|
+
if (nativeId) {
|
|
179
|
+
if (settings.protocolVersion !== this.settings().protocolVersion || settings.experimental !== this.settings().experimental)
|
|
180
|
+
throw RpcError.app("invalid_params", "协议版本和实验功能需要在 AI 全局设置中修改");
|
|
181
|
+
this.validateSessionSettings(settings);
|
|
182
|
+
await this.detach(nativeId);
|
|
183
|
+
this.host?.state(this.id, nativeId).set("acp-settings", JSON.stringify(settings));
|
|
184
|
+
const state = this.sessions.get(nativeId);
|
|
185
|
+
if (state)
|
|
186
|
+
state.settings = settings;
|
|
187
|
+
this.host?.detached(this.id, nativeId);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
await this.stop();
|
|
191
|
+
this.stopped = false;
|
|
192
|
+
this.children.clear();
|
|
193
|
+
for (const [id, state] of this.sessions) {
|
|
194
|
+
state.loaded = false;
|
|
195
|
+
if (!this.host?.state(this.id, id).get("acp-settings"))
|
|
196
|
+
state.settings = settings;
|
|
197
|
+
this.host?.detached(this.id, id);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async authenticate(methodId, logout = false) {
|
|
202
|
+
const { features } = await this.acpInfo();
|
|
203
|
+
if (logout) {
|
|
204
|
+
if (!features.logout)
|
|
205
|
+
throw RpcError.app("not_supported", "这个 AI 不支持退出登录");
|
|
206
|
+
if ([...this.sessions.values()].some((state) => state.turnActive))
|
|
207
|
+
throw RpcError.app("busy", "请先结束正在运行的会话");
|
|
208
|
+
await this.rpc(this.connection.protocolVersion === 2 ? "auth/logout" : "logout", {});
|
|
209
|
+
this.signedOut = { state: "missing" };
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
const method = this.connection.initializeResult.authMethods?.find((entry) => entry.id === methodId);
|
|
213
|
+
if (!method || (method.type && !["agent", "terminal"].includes(method.type)))
|
|
214
|
+
throw RpcError.app("invalid_params", "请选择可用的登录方式");
|
|
215
|
+
if (method.type === "terminal") {
|
|
216
|
+
if (!this.host?.loginTerminal || ["http", "websocket"].includes(this.spec.transport ?? ""))
|
|
217
|
+
throw RpcError.app("not_supported", "这个连接不支持终端登录");
|
|
218
|
+
const launch = await this.host.loginTerminal(this.id, { command: this.spec.command, args: [...this.spec.args, ...(method.args ?? [])], env: { ...this.spec.env, ...method.env } });
|
|
219
|
+
void launch.exited.then((code) => {
|
|
220
|
+
if (this.stopped)
|
|
221
|
+
return;
|
|
222
|
+
this.signedOut = code === 0 ? { state: "ok" } : { state: "missing", hint: "终端登录未成功,请重试" };
|
|
223
|
+
this.credentialsChanged = code === 0;
|
|
224
|
+
this.host?.authChanged?.(this.id);
|
|
225
|
+
});
|
|
226
|
+
return { terminalId: launch.terminalId };
|
|
227
|
+
}
|
|
228
|
+
await this.rpc(this.connection.protocolVersion === 2 ? "auth/login" : "authenticate", { methodId }, 0);
|
|
229
|
+
this.signedOut = { state: "ok" };
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
async providers(operation, config) {
|
|
233
|
+
const { features } = await this.acpInfo();
|
|
234
|
+
if (!features.providers)
|
|
235
|
+
throw RpcError.app("not_supported", "这个 AI 不支持供应商配置");
|
|
236
|
+
const list = async () => {
|
|
237
|
+
const response = await this.rpc("providers/list", {});
|
|
238
|
+
return response.providers.map((provider) => acpProviderSchema.parse(provider));
|
|
239
|
+
};
|
|
240
|
+
if (operation === "list")
|
|
241
|
+
return list();
|
|
242
|
+
const provider = (await list()).find((entry) => entry.providerId === config?.providerId);
|
|
243
|
+
if (!provider || !config)
|
|
244
|
+
throw RpcError.app("invalid_params", "供应商不存在");
|
|
245
|
+
if (operation === "disable" && provider.required)
|
|
246
|
+
throw RpcError.app("invalid_params", "不能停用必需的供应商");
|
|
247
|
+
if (operation === "set" && (!config.apiType || !provider.supported.includes(config.apiType) || !config.baseUrl))
|
|
248
|
+
throw RpcError.app("invalid_params", "供应商地址或协议无效");
|
|
249
|
+
await this.rpc(`providers/${operation}`, operation === "disable" ? { providerId: config.providerId } : { ...config, headers: config.headers ?? {} });
|
|
250
|
+
return list();
|
|
251
|
+
}
|
|
252
|
+
async respondInteraction(requestId, response) {
|
|
253
|
+
const pending = this.interactions.get(requestId);
|
|
254
|
+
if (!pending)
|
|
255
|
+
throw RpcError.app("not_found", "这个请求已结束");
|
|
256
|
+
if (response.answers) {
|
|
257
|
+
const error = (pending.request.update.questions ?? []).map((question) => questionAnswerError(question, response.answers.find((answer) => answer.id === question.id))).find(Boolean);
|
|
258
|
+
if (error)
|
|
259
|
+
throw RpcError.app("invalid_params", error);
|
|
260
|
+
const result = pending.request.answer(response.answers);
|
|
261
|
+
this.interactions.delete(requestId);
|
|
262
|
+
pending.resolve(result);
|
|
263
|
+
this.host?.interaction?.(this.id, { requestId, resolved: true });
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
if (!pending.request.update.options.some((option) => option.optionId === response.optionId))
|
|
267
|
+
throw RpcError.app("invalid_params", "选项已失效");
|
|
268
|
+
this.resolveInteraction(requestId, response.optionId);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
resolveInteraction(requestId, optionId) {
|
|
272
|
+
const pending = this.interactions.get(requestId);
|
|
273
|
+
if (!pending)
|
|
274
|
+
return;
|
|
275
|
+
this.interactions.delete(requestId);
|
|
276
|
+
pending.resolve(pending.request.respond(optionId));
|
|
277
|
+
this.host?.interaction?.(this.id, { requestId, resolved: true });
|
|
278
|
+
}
|
|
279
|
+
async validateDirectories(settings) {
|
|
280
|
+
for (const path of settings.additionalDirectories) {
|
|
281
|
+
if (!isAbsolute(path) || !await stat(await realpath(path)).then((info) => info.isDirectory()).catch(() => false))
|
|
282
|
+
throw RpcError.app("invalid_params", `附加目录不存在或不是绝对路径:${path}`);
|
|
283
|
+
}
|
|
284
|
+
if (new Set(settings.mcpServers.map((server) => server.name)).size !== settings.mcpServers.length)
|
|
285
|
+
throw RpcError.app("invalid_params", "MCP 服务名称不能重复");
|
|
286
|
+
}
|
|
287
|
+
validateSessionSettings(settings) {
|
|
288
|
+
const caps = this.connection?.capabilities;
|
|
289
|
+
if (settings.additionalDirectories.length && !caps?.sessionCapabilities?.additionalDirectories)
|
|
290
|
+
throw RpcError.app("not_supported", "这个 AI 未声明支持附加目录");
|
|
291
|
+
for (const server of settings.mcpServers) {
|
|
292
|
+
if ((server.type === "http" || server.type === "sse") && !caps?.mcpCapabilities?.[server.type])
|
|
293
|
+
throw RpcError.app("not_supported", `这个 AI 不支持 ${server.type} MCP 服务`);
|
|
294
|
+
if (server.type === "stdio" && this.connection?.protocolVersion === 2 && !caps?.mcpCapabilities?.stdio)
|
|
295
|
+
throw RpcError.app("not_supported", "这个 AI 不支持 stdio MCP 服务");
|
|
296
|
+
if (server.type === "acp" && !caps?.mcpCapabilities?.acp)
|
|
297
|
+
throw RpcError.app("not_supported", "这个 AI 不支持通过 ACP 连接 MCP 服务");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async sessionParameters(cwd, settings = this.settings()) {
|
|
301
|
+
await this.ensureStarted();
|
|
302
|
+
await this.validateDirectories(settings);
|
|
303
|
+
this.validateSessionSettings(settings);
|
|
304
|
+
const v2 = this.connection?.protocolVersion === 2;
|
|
305
|
+
return { cwd, mcpServers: settings.mcpServers.map((server) => {
|
|
306
|
+
if (server.type === "stdio")
|
|
307
|
+
return { ...(v2 ? { type: "stdio" } : {}), name: server.name, command: server.command, args: server.args, env: Object.entries(server.env).map(([name, value]) => ({ name, value })) };
|
|
308
|
+
if (server.type === "acp")
|
|
309
|
+
return { type: "acp", name: server.name, serverId: this.mcp.open(server, cwd) };
|
|
310
|
+
return { type: server.type, name: server.name, url: server.url, headers: Object.entries(server.headers).map(([name, value]) => ({ name, value })) };
|
|
311
|
+
}), ...(settings.additionalDirectories.length ? { additionalDirectories: settings.additionalDirectories } : {}), ...this.sessionMeta() };
|
|
100
312
|
}
|
|
101
313
|
async listSessions(limit) {
|
|
102
314
|
if (!this.connection?.alive || !this.connection.capabilities.sessionCapabilities?.list)
|
|
@@ -106,6 +318,8 @@ export class AcpDriver {
|
|
|
106
318
|
do {
|
|
107
319
|
const page = await this.rpc("session/list", cursor ? { cursor } : {});
|
|
108
320
|
for (const info of page.sessions ?? []) {
|
|
321
|
+
if (Array.isArray(info.additionalDirectories) && info.additionalDirectories.every((path) => typeof path === "string" && isAbsolute(path)))
|
|
322
|
+
this.discoveredDirectories.set(info.sessionId, info.additionalDirectories);
|
|
109
323
|
const updatedAt = info.updatedAt ? Date.parse(info.updatedAt) || Date.now() : Date.now();
|
|
110
324
|
found.push({
|
|
111
325
|
nativeId: info.sessionId, cwd: info.cwd, title: info.title ?? undefined, createdAt: updatedAt, updatedAt,
|
|
@@ -126,7 +340,10 @@ export class AcpDriver {
|
|
|
126
340
|
const state = this.sessions.get(nativeId);
|
|
127
341
|
if (state?.permissions.size || state?.questions.size)
|
|
128
342
|
return "waiting";
|
|
129
|
-
|
|
343
|
+
for (const [id, child] of this.sessions)
|
|
344
|
+
if ((child.permissions.size || child.questions.size) && this.isChildOf(id, nativeId))
|
|
345
|
+
return "waiting";
|
|
346
|
+
return state?.reportedState ?? (state?.turnActive ? "running" : "idle");
|
|
130
347
|
}
|
|
131
348
|
/** ACP's session/fork, for agents that offer it: the whole conversation, in `cwd`. */
|
|
132
349
|
async fork(nativeId, options) {
|
|
@@ -136,7 +353,9 @@ export class AcpDriver {
|
|
|
136
353
|
throw RpcError.app("not_supported", `${this.label} 不支持从会话分叉`);
|
|
137
354
|
if (options.upTo)
|
|
138
355
|
throw RpcError.app("not_supported", `${this.label} 只能分叉整个会话`);
|
|
139
|
-
const
|
|
356
|
+
const settings = this.sessions.get(nativeId)?.settings ?? this.settings();
|
|
357
|
+
const response = await this.rpc("session/fork", { sessionId: nativeId, ...await this.sessionParameters(options.cwd, settings) });
|
|
358
|
+
this.stateFor(response.sessionId, options.cwd).settings = settings;
|
|
140
359
|
const now = Date.now();
|
|
141
360
|
return { nativeId: response.sessionId, cwd: options.cwd, createdAt: now, updatedAt: now };
|
|
142
361
|
}
|
|
@@ -148,12 +367,13 @@ export class AcpDriver {
|
|
|
148
367
|
return this.id === "claude" ? { _meta: { claudeCode: { emitRawSDKMessages: [{ type: "active_goal" }] } } } : {};
|
|
149
368
|
}
|
|
150
369
|
async createSession(options) {
|
|
370
|
+
await this.ensureStarted();
|
|
371
|
+
if (this.connection?.capabilities.sessions === false)
|
|
372
|
+
throw RpcError.app("not_supported", "这个 ACP Agent 不支持会话,可在文件中使用编辑建议");
|
|
151
373
|
let response;
|
|
152
374
|
try {
|
|
153
375
|
response = await this.rpc("session/new", {
|
|
154
|
-
|
|
155
|
-
mcpServers: [],
|
|
156
|
-
...this.sessionMeta(),
|
|
376
|
+
...await this.sessionParameters(options.cwd),
|
|
157
377
|
});
|
|
158
378
|
}
|
|
159
379
|
catch (error) {
|
|
@@ -163,6 +383,7 @@ export class AcpDriver {
|
|
|
163
383
|
const state = this.stateFor(response.sessionId, options.cwd);
|
|
164
384
|
state.loaded = true;
|
|
165
385
|
state.config = toConfigOptions(response);
|
|
386
|
+
this.sessionCommands(state, response);
|
|
166
387
|
if (options.model)
|
|
167
388
|
await this.setConfig(response.sessionId, "model", options.model).catch(() => { });
|
|
168
389
|
const now = Date.now();
|
|
@@ -176,23 +397,30 @@ export class AcpDriver {
|
|
|
176
397
|
const state = this.stateFor(nativeId, context.cwd);
|
|
177
398
|
if (state.loaded) {
|
|
178
399
|
this.emitConfig(nativeId, state);
|
|
179
|
-
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: this.localState(nativeId) });
|
|
400
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: this.localState(nativeId), turnActive: state.turnActive });
|
|
180
401
|
return [];
|
|
181
402
|
}
|
|
182
403
|
const capabilities = this.connection?.capabilities;
|
|
183
404
|
let history = [];
|
|
184
|
-
|
|
405
|
+
state.config = [];
|
|
406
|
+
state.commands = undefined;
|
|
407
|
+
if (capabilities?.loadSession || this.connection.protocolVersion === 2) {
|
|
185
408
|
state.replaying = [];
|
|
409
|
+
state.updates = new AcpUpdates();
|
|
186
410
|
try {
|
|
187
|
-
const response = await this.rpc("session/load", {
|
|
411
|
+
const response = await this.rpc(this.connection.protocolVersion === 2 ? "session/resume" : "session/load", {
|
|
188
412
|
sessionId: nativeId,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
...this.sessionMeta(),
|
|
413
|
+
...await this.sessionParameters(context.cwd, state.settings),
|
|
414
|
+
...(this.connection.protocolVersion === 2 ? { replayFrom: { type: "start" } } : {}),
|
|
192
415
|
});
|
|
193
|
-
|
|
416
|
+
if (response.configOptions !== undefined || response.modes !== undefined || response.models !== undefined)
|
|
417
|
+
state.config = this.mergeConfig(state, toConfigOptions(response));
|
|
418
|
+
this.sessionCommands(state, response);
|
|
194
419
|
state.tracker = new AcpItemTracker();
|
|
195
|
-
|
|
420
|
+
const unfinished = [];
|
|
421
|
+
history = toHistory(state.replaying, state.tracker, undefined, unfinished);
|
|
422
|
+
if (unfinished.length)
|
|
423
|
+
history.push({ itemId: `acp:snapshot:${createHash("sha256").update(JSON.stringify(unfinished)).digest("hex")}`, updates: unfinished });
|
|
196
424
|
}
|
|
197
425
|
finally {
|
|
198
426
|
state.replaying = undefined;
|
|
@@ -201,11 +429,10 @@ export class AcpDriver {
|
|
|
201
429
|
else if (capabilities?.sessionCapabilities?.resume) {
|
|
202
430
|
const response = await this.rpc("session/resume", {
|
|
203
431
|
sessionId: nativeId,
|
|
204
|
-
|
|
205
|
-
mcpServers: [],
|
|
206
|
-
...this.sessionMeta(),
|
|
432
|
+
...await this.sessionParameters(context.cwd, state.settings),
|
|
207
433
|
});
|
|
208
434
|
state.config = toConfigOptions(response);
|
|
435
|
+
this.sessionCommands(state, response);
|
|
209
436
|
history.push({
|
|
210
437
|
itemId: "linkshell:acp-history-unavailable",
|
|
211
438
|
updates: [{
|
|
@@ -219,7 +446,7 @@ export class AcpDriver {
|
|
|
219
446
|
}
|
|
220
447
|
state.loaded = true;
|
|
221
448
|
this.emitConfig(nativeId, state);
|
|
222
|
-
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: this.localState(nativeId) });
|
|
449
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: this.localState(nativeId), turnActive: state.turnActive });
|
|
223
450
|
return history;
|
|
224
451
|
}
|
|
225
452
|
async detach(nativeId) {
|
|
@@ -228,6 +455,11 @@ export class AcpDriver {
|
|
|
228
455
|
return;
|
|
229
456
|
state.loaded = false;
|
|
230
457
|
this.cancelPermissions(state);
|
|
458
|
+
await this.services.close(nativeId);
|
|
459
|
+
await this.mcp.close(nativeId);
|
|
460
|
+
for (const key of this.terminalCalls.keys())
|
|
461
|
+
if (key.startsWith(`${nativeId}\n`))
|
|
462
|
+
this.terminalCalls.delete(key);
|
|
231
463
|
if (this.connection?.capabilities.sessionCapabilities?.close) {
|
|
232
464
|
await this.rpc("session/close", { sessionId: nativeId }).catch(() => { });
|
|
233
465
|
}
|
|
@@ -236,13 +468,19 @@ export class AcpDriver {
|
|
|
236
468
|
const state = this.sessions.get(nativeId);
|
|
237
469
|
if (!state?.loaded)
|
|
238
470
|
throw RpcError.app("not_ready", "session is not open");
|
|
471
|
+
if (state.reportedState === "unknown")
|
|
472
|
+
throw RpcError.app("not_ready", "Agent 尚未确认当前执行状态,请刷新会话或停止后重试");
|
|
473
|
+
for (const block of content) {
|
|
474
|
+
if (block.type === "image" && !this.capabilities.images || block.type === "audio" && !this.capabilities.audio || block.type === "resource" && !this.capabilities.embeddedContext)
|
|
475
|
+
throw RpcError.app("not_supported", `${this.label} 未声明支持这种附件`);
|
|
476
|
+
}
|
|
239
477
|
const prompt = toAcpPrompt(content);
|
|
240
478
|
if (prompt.length === 0)
|
|
241
479
|
throw RpcError.app("invalid_params", "nothing to send");
|
|
242
480
|
if (state.turnActive) {
|
|
243
481
|
if (this.capabilities.steer) {
|
|
244
482
|
this.echoUserMessage(nativeId, content, clientMessageId);
|
|
245
|
-
void this.sendPrompt(nativeId, state, withContext(prompt, context));
|
|
483
|
+
void this.sendPrompt(nativeId, state, withContext(prompt, context), clientMessageId);
|
|
246
484
|
return "steered";
|
|
247
485
|
}
|
|
248
486
|
state.queue.push({ content, clientMessageId, context });
|
|
@@ -262,6 +500,12 @@ export class AcpDriver {
|
|
|
262
500
|
this.cancelPermissions(state);
|
|
263
501
|
this.connection?.notify("session/cancel", { sessionId: nativeId });
|
|
264
502
|
}
|
|
503
|
+
async cancelSubagent(nativeId, childId) {
|
|
504
|
+
const permitted = [...this.children.get(childId)?.entries() ?? []].some(([parent, association]) => association.canCancel && (parent === nativeId || this.isChildOf(parent, nativeId)));
|
|
505
|
+
if (!permitted)
|
|
506
|
+
throw RpcError.app("not_supported", "这个子代理未允许单独停止");
|
|
507
|
+
this.connection?.notify("session/cancel", { sessionId: childId });
|
|
508
|
+
}
|
|
265
509
|
/** Stops the running turn only: the first queued message starts as soon as it has ended. */
|
|
266
510
|
async sendQueuedNow(nativeId) {
|
|
267
511
|
const state = this.sessions.get(nativeId);
|
|
@@ -271,20 +515,21 @@ export class AcpDriver {
|
|
|
271
515
|
this.connection?.notify("session/cancel", { sessionId: nativeId });
|
|
272
516
|
}
|
|
273
517
|
async respondPermission(nativeId, requestId, optionId) {
|
|
274
|
-
const
|
|
518
|
+
const owner = this.requestOwner(nativeId, requestId);
|
|
519
|
+
const question = owner?.state.questions.get(requestId);
|
|
275
520
|
if (question) {
|
|
276
521
|
// Not answering: skip (the agent goes on without an answer) or stop.
|
|
277
|
-
|
|
522
|
+
owner.state.questions.delete(requestId);
|
|
278
523
|
question.resolve(question.request.respond(optionId));
|
|
279
|
-
this.
|
|
524
|
+
this.publish(owner.id, { sessionUpdate: "ls_permission_resolved", requestId, optionId });
|
|
280
525
|
return;
|
|
281
526
|
}
|
|
282
|
-
const
|
|
283
|
-
if (!
|
|
527
|
+
const pending = owner?.state.permissions.get(requestId);
|
|
528
|
+
if (!pending)
|
|
284
529
|
throw RpcError.app("not_found", "this permission request is no longer pending");
|
|
285
|
-
|
|
286
|
-
resolve({ outcome: { outcome: "selected", optionId } });
|
|
287
|
-
this.
|
|
530
|
+
owner.state.permissions.delete(requestId);
|
|
531
|
+
pending.resolve({ outcome: { outcome: "selected", optionId } });
|
|
532
|
+
this.publish(owner.id, { sessionUpdate: "ls_permission_resolved", requestId, optionId });
|
|
288
533
|
}
|
|
289
534
|
async setConfig(nativeId, optionId, value) {
|
|
290
535
|
const state = this.sessions.get(nativeId);
|
|
@@ -301,13 +546,24 @@ export class AcpDriver {
|
|
|
301
546
|
await this.rpc("session/set_model", { sessionId: nativeId, modelId: value });
|
|
302
547
|
}
|
|
303
548
|
else {
|
|
304
|
-
await this.rpc("session/set_config_option", {
|
|
549
|
+
const response = await this.rpc("session/set_config_option", {
|
|
550
|
+
sessionId: nativeId, configId: optionId, ...(option.type === "boolean" ? { type: "boolean", value: value === "on" } : { ...(this.connection?.protocolVersion === 2 ? { type: "select" } : {}), value }),
|
|
551
|
+
});
|
|
552
|
+
if (Array.isArray(response.configOptions)) {
|
|
553
|
+
state.config = this.mergeConfig(state, toConfigOptions(response));
|
|
554
|
+
this.emitConfig(nativeId, state);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
305
557
|
}
|
|
306
558
|
option.current = value;
|
|
307
559
|
this.emitConfig(nativeId, state);
|
|
308
560
|
}
|
|
309
561
|
// ── internals ──────────────────────────────────────────────────────
|
|
310
562
|
async detectVersion() {
|
|
563
|
+
if (this.spec.transport === "http" || this.spec.transport === "websocket")
|
|
564
|
+
return "remote";
|
|
565
|
+
if (this.spec.transport === "stdio")
|
|
566
|
+
return "";
|
|
311
567
|
const probe = this.spec.version ?? { command: this.spec.command, args: ["--version"] };
|
|
312
568
|
try {
|
|
313
569
|
const { stdout, stderr } = await execFileAsync(probe.command, probe.args, { env: this.env, timeout: 15_000 });
|
|
@@ -322,22 +578,63 @@ export class AcpDriver {
|
|
|
322
578
|
booting;
|
|
323
579
|
/** Starts the agent process once; concurrent callers share the attempt. */
|
|
324
580
|
ensureStarted() {
|
|
325
|
-
|
|
581
|
+
const refresh = this.credentialsChanged && ![...this.sessions.values()].some((state) => state.turnActive);
|
|
582
|
+
if (this.connection?.alive && !refresh)
|
|
326
583
|
return Promise.resolve();
|
|
327
|
-
this.booting ??=
|
|
584
|
+
this.booting ??= (async () => {
|
|
585
|
+
if (refresh) {
|
|
586
|
+
this.credentialsChanged = false;
|
|
587
|
+
await this.connection?.stop();
|
|
588
|
+
this.connection = undefined;
|
|
589
|
+
for (const [id, state] of this.sessions) {
|
|
590
|
+
state.loaded = false;
|
|
591
|
+
this.host?.detached(this.id, id);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
await this.boot();
|
|
595
|
+
})().finally(() => {
|
|
328
596
|
this.booting = undefined;
|
|
329
597
|
});
|
|
330
598
|
return this.booting;
|
|
331
599
|
}
|
|
332
600
|
async boot() {
|
|
601
|
+
const settings = this.settings();
|
|
602
|
+
const remote = this.spec.transport === "http" || this.spec.transport === "websocket";
|
|
603
|
+
const headers = {};
|
|
604
|
+
for (const [key, name] of Object.entries(this.spec.headerEnv ?? {})) {
|
|
605
|
+
const value = this.env[name];
|
|
606
|
+
if (value)
|
|
607
|
+
headers[key] = value;
|
|
608
|
+
}
|
|
333
609
|
const connection = new AcpConnection({
|
|
334
610
|
command: this.spec.command,
|
|
335
611
|
args: this.spec.args,
|
|
336
612
|
env: this.env,
|
|
613
|
+
protocolVersion: settings.protocolVersion,
|
|
614
|
+
experimental: settings.experimental,
|
|
615
|
+
transport: remote ? { type: this.spec.transport, url: this.spec.url, headers } : undefined,
|
|
337
616
|
clientVersion: this.options.hostVersion,
|
|
338
617
|
onUpdate: (sessionId, update) => this.onUpdate(sessionId, update),
|
|
339
618
|
onRequest: (method, params, id) => this.onRequest(method, params, id),
|
|
340
619
|
onCancelRequest: (id) => this.cancelQuestionRequest(id),
|
|
620
|
+
onNotification: (method, params) => {
|
|
621
|
+
if (method !== "elicitation/complete")
|
|
622
|
+
return;
|
|
623
|
+
const id = params?.elicitationId;
|
|
624
|
+
if (!id)
|
|
625
|
+
return;
|
|
626
|
+
for (const [requestId, pending] of this.interactions)
|
|
627
|
+
if (pending.request.update.url?.elicitationId === id)
|
|
628
|
+
this.resolveInteraction(requestId, "cancel");
|
|
629
|
+
for (const [sessionId, state] of this.sessions)
|
|
630
|
+
for (const [requestId, pending] of state.questions) {
|
|
631
|
+
if (pending.request.update.url?.elicitationId !== id)
|
|
632
|
+
continue;
|
|
633
|
+
state.questions.delete(requestId);
|
|
634
|
+
pending.resolve(pending.request.respond("cancel"));
|
|
635
|
+
this.host?.update(this.id, sessionId, { sessionUpdate: "ls_permission_resolved", requestId });
|
|
636
|
+
}
|
|
637
|
+
},
|
|
341
638
|
onExit: (reason) => this.onExit(reason),
|
|
342
639
|
});
|
|
343
640
|
this.connection = connection;
|
|
@@ -351,6 +648,8 @@ export class AcpDriver {
|
|
|
351
648
|
...this.capabilities,
|
|
352
649
|
steer: steering || queueing,
|
|
353
650
|
images: caps.promptCapabilities?.image === true,
|
|
651
|
+
audio: caps.promptCapabilities?.audio === true,
|
|
652
|
+
embeddedContext: caps.promptCapabilities?.embeddedContext === true,
|
|
354
653
|
fork: Boolean(caps.sessionCapabilities?.fork),
|
|
355
654
|
};
|
|
356
655
|
this.current = { installed: true, version: this.current.version };
|
|
@@ -384,6 +683,15 @@ export class AcpDriver {
|
|
|
384
683
|
if (this.stopped)
|
|
385
684
|
return;
|
|
386
685
|
this.host?.log(`[${this.id}] ${reason}`);
|
|
686
|
+
for (const controller of this.serviceRequests.values())
|
|
687
|
+
controller.abort();
|
|
688
|
+
this.serviceRequests.clear();
|
|
689
|
+
this.terminalCalls.clear();
|
|
690
|
+
void this.services.close();
|
|
691
|
+
void this.mcp.close();
|
|
692
|
+
void this.edits.close();
|
|
693
|
+
for (const id of [...this.interactions.keys()])
|
|
694
|
+
this.resolveInteraction(id, "cancel");
|
|
387
695
|
for (const [nativeId, state] of this.sessions) {
|
|
388
696
|
if (!state.loaded)
|
|
389
697
|
continue;
|
|
@@ -402,10 +710,15 @@ export class AcpDriver {
|
|
|
402
710
|
stateFor(nativeId, cwd) {
|
|
403
711
|
let state = this.sessions.get(nativeId);
|
|
404
712
|
if (!state) {
|
|
713
|
+
const saved = this.host?.state(this.id, nativeId).get("acp-settings");
|
|
405
714
|
state = {
|
|
406
715
|
cwd,
|
|
716
|
+
settings: saved ? acpAgentSettingsSchema.parse(JSON.parse(saved)) : { ...this.settings(), ...(this.discoveredDirectories.has(nativeId) ? { additionalDirectories: this.discoveredDirectories.get(nativeId) } : {}) },
|
|
407
717
|
loaded: false,
|
|
408
718
|
tracker: new AcpItemTracker(),
|
|
719
|
+
updates: new AcpUpdates(),
|
|
720
|
+
messageAliases: new Map(JSON.parse(this.host?.state(this.id, nativeId).get("acp-message-aliases") ?? "[]")),
|
|
721
|
+
deferredUserUpdates: [],
|
|
409
722
|
config: [],
|
|
410
723
|
turnActive: false,
|
|
411
724
|
inflight: 0,
|
|
@@ -419,36 +732,168 @@ export class AcpDriver {
|
|
|
419
732
|
return state;
|
|
420
733
|
}
|
|
421
734
|
emitConfig(nativeId, state) {
|
|
422
|
-
if (state.config.length === 0)
|
|
423
|
-
return;
|
|
424
735
|
this.host?.update(this.id, nativeId, {
|
|
425
736
|
sessionUpdate: "ls_config",
|
|
426
737
|
options: state.config.map(({ source: _source, ...option }) => option),
|
|
427
738
|
});
|
|
739
|
+
if (state.commands)
|
|
740
|
+
this.host?.update(this.id, nativeId, state.commands);
|
|
741
|
+
}
|
|
742
|
+
sessionCommands(state, response) {
|
|
743
|
+
const commands = response.availableCommands;
|
|
744
|
+
if (!Array.isArray(commands))
|
|
745
|
+
return;
|
|
746
|
+
const update = normalizeAcpUpdate({ sessionUpdate: "available_commands_update", availableCommands: commands });
|
|
747
|
+
if (update?.sessionUpdate === "available_commands_update")
|
|
748
|
+
state.commands = update;
|
|
749
|
+
}
|
|
750
|
+
mergeConfig(state, options) {
|
|
751
|
+
return [...options, ...state.config.filter((option) => option.source !== "configOptions" && !options.some((next) => next.id === option.id || next.category === option.category))];
|
|
428
752
|
}
|
|
429
753
|
/** Feeds an update through the item tracker and on to the host. */
|
|
430
754
|
emit(nativeId, update) {
|
|
431
755
|
const state = this.sessions.get(nativeId);
|
|
432
756
|
const tracked = state ? state.tracker.feed(update) : [{ update }];
|
|
433
757
|
for (const entry of tracked)
|
|
434
|
-
this.
|
|
758
|
+
this.publish(nativeId, entry.update, entry.itemId);
|
|
435
759
|
}
|
|
436
760
|
closeMessage(nativeId) {
|
|
437
761
|
const state = this.sessions.get(nativeId);
|
|
438
762
|
if (!state)
|
|
439
763
|
return;
|
|
440
764
|
for (const entry of state.tracker.close())
|
|
441
|
-
this.
|
|
765
|
+
this.publish(nativeId, entry.update, entry.itemId);
|
|
766
|
+
}
|
|
767
|
+
/** Child streams reuse the existing nested timeline and never overwrite the parent's settings or status. */
|
|
768
|
+
publish(nativeId, update, itemId) {
|
|
769
|
+
const parents = this.children.get(nativeId);
|
|
770
|
+
if (!parents?.size) {
|
|
771
|
+
this.host?.update(this.id, nativeId, update, itemId);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (["ls_status", "ls_config", "session_info_update", "usage_update", "available_commands_update", "current_mode_update", "ls_goal", "plan"].includes(update.sessionUpdate))
|
|
775
|
+
return;
|
|
776
|
+
const prefix = `acp:${encodeURIComponent(nativeId)}:`;
|
|
777
|
+
for (const [parent, association] of parents) {
|
|
778
|
+
const mapped = update.sessionUpdate === "user_message_chunk"
|
|
779
|
+
? { sessionUpdate: "ls_message", messageId: update.messageId ?? "prompt", role: "user", content: [update.content], append: true }
|
|
780
|
+
: { ...update };
|
|
781
|
+
if ("messageId" in mapped && mapped.messageId)
|
|
782
|
+
mapped.messageId = prefix + mapped.messageId;
|
|
783
|
+
if ("toolCallId" in mapped && mapped.toolCallId)
|
|
784
|
+
mapped.toolCallId = prefix + mapped.toolCallId;
|
|
785
|
+
if (["agent_message_chunk", "agent_thought_chunk", "ls_message", "ls_message_done", "tool_call", "tool_call_update", "ls_turn"].includes(mapped.sessionUpdate)) {
|
|
786
|
+
mapped.parentToolCallId = "parentToolCallId" in update && update.parentToolCallId ? prefix + update.parentToolCallId : this.childCallId(nativeId);
|
|
787
|
+
}
|
|
788
|
+
if (mapped.sessionUpdate === "ls_permission") {
|
|
789
|
+
mapped.title = `${association.title ?? "子代理"} · ${mapped.title}`;
|
|
790
|
+
mapped.childSessionId ??= nativeId;
|
|
791
|
+
}
|
|
792
|
+
this.onMappedUpdate(parent, mapped);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
childCallId(id) { return `acp:subagent:${encodeURIComponent(id)}`; }
|
|
796
|
+
subagentUpdate(parent, raw) {
|
|
797
|
+
const update = raw;
|
|
798
|
+
const childId = update.sessionId;
|
|
799
|
+
if (typeof childId !== "string" || childId === parent || this.isChildOf(parent, childId))
|
|
800
|
+
return;
|
|
801
|
+
const parentState = this.sessions.get(parent);
|
|
802
|
+
if (!parentState)
|
|
803
|
+
return;
|
|
804
|
+
const parents = this.children.get(childId) ?? new Map();
|
|
805
|
+
const created = !parents.has(parent);
|
|
806
|
+
const association = parents.get(parent) ?? { canCancel: false };
|
|
807
|
+
if (update.title !== undefined)
|
|
808
|
+
association.title = update.title ?? undefined;
|
|
809
|
+
if (update.description !== undefined)
|
|
810
|
+
association.description = update.description ?? undefined;
|
|
811
|
+
if (update.capabilities !== undefined)
|
|
812
|
+
association.canCancel = !!update.capabilities?.cancel;
|
|
813
|
+
if (update.state !== undefined)
|
|
814
|
+
association.state = update.state;
|
|
815
|
+
parents.set(parent, association);
|
|
816
|
+
this.children.set(childId, parents);
|
|
817
|
+
const child = this.stateFor(childId, parentState.cwd);
|
|
818
|
+
child.loaded = true;
|
|
819
|
+
child.settings = parentState.settings;
|
|
820
|
+
const status = association.state;
|
|
821
|
+
const state = status?.state === "running" ? "running" : status?.state === "requires_action" ? "paused" : status?.state === "idle"
|
|
822
|
+
? status.stopReason === "cancelled" ? "stopped" : status.stopReason === "error" ? "failed" : status.stopReason ? "completed" : "unknown" : "unknown";
|
|
823
|
+
const call = {
|
|
824
|
+
toolCallId: this.childCallId(childId), title: association.title ?? "子代理", kind: "other",
|
|
825
|
+
status: state === "failed" || state === "stopped" ? "failed" : state === "completed" ? "completed" : "in_progress",
|
|
826
|
+
detail: { type: "subagent", action: "spawn", name: association.title, task: association.description, nativeSessionId: childId, canCancel: association.canCancel, state },
|
|
827
|
+
};
|
|
828
|
+
this.onMappedUpdate(parent, { sessionUpdate: created ? "tool_call" : "tool_call_update", ...call });
|
|
829
|
+
}
|
|
830
|
+
isChildOf(child, parent, seen = new Set()) {
|
|
831
|
+
if (seen.has(child))
|
|
832
|
+
return false;
|
|
833
|
+
seen.add(child);
|
|
834
|
+
const parents = this.children.get(child);
|
|
835
|
+
return !!parents && (parents.has(parent) || [...parents.keys()].some((id) => this.isChildOf(id, parent, seen)));
|
|
836
|
+
}
|
|
837
|
+
requestOwner(nativeId, requestId) {
|
|
838
|
+
for (const [id, state] of this.sessions)
|
|
839
|
+
if ((id === nativeId || this.isChildOf(id, nativeId)) && (state.permissions.has(requestId) || state.questions.has(requestId)))
|
|
840
|
+
return { id, state };
|
|
841
|
+
return undefined;
|
|
442
842
|
}
|
|
443
843
|
onUpdate(sessionId, raw) {
|
|
444
|
-
const
|
|
445
|
-
if (
|
|
844
|
+
const record = raw;
|
|
845
|
+
if (record?.sessionUpdate === "notice") {
|
|
846
|
+
if (!this.sessions.get(sessionId)?.replaying && typeof record.title === "string" && typeof record.severity === "string") {
|
|
847
|
+
const roots = (id) => this.children.has(id) ? [...this.children.get(id).keys()].flatMap(roots) : [id];
|
|
848
|
+
for (const root of new Set(roots(sessionId)))
|
|
849
|
+
this.host?.notice?.(this.id, root, { severity: record.severity, title: record.title, description: typeof record.description === "string" ? record.description : undefined });
|
|
850
|
+
}
|
|
446
851
|
return;
|
|
852
|
+
}
|
|
447
853
|
const state = this.sessions.get(sessionId);
|
|
448
|
-
if (
|
|
449
|
-
|
|
854
|
+
if (record?.sessionUpdate === "subagent_update" && this.settings().experimental) {
|
|
855
|
+
this.subagentUpdate(sessionId, raw);
|
|
450
856
|
return;
|
|
451
857
|
}
|
|
858
|
+
const version = this.connection?.protocolVersion ?? 1;
|
|
859
|
+
if (version === 2 && state && record?.sessionUpdate === "state_update") {
|
|
860
|
+
const status = raw;
|
|
861
|
+
for (const parent of this.children.get(sessionId)?.keys() ?? [])
|
|
862
|
+
this.subagentUpdate(parent, { sessionId, state: status });
|
|
863
|
+
state.reportedState = status.state === "running" ? "running" : status.state === "requires_action" ? "waiting" : status.state === "idle" ? "idle" : "unknown";
|
|
864
|
+
if (status.state === "running" || status.state === "requires_action") {
|
|
865
|
+
if (!state.turnActive && !state.replaying)
|
|
866
|
+
this.emit(sessionId, { sessionUpdate: "ls_turn", state: "started" });
|
|
867
|
+
state.turnActive = true;
|
|
868
|
+
state.idleReason = undefined;
|
|
869
|
+
}
|
|
870
|
+
else if (status.state === "idle") {
|
|
871
|
+
if (state.replaying)
|
|
872
|
+
state.turnActive = false;
|
|
873
|
+
else if (state.turnActive) {
|
|
874
|
+
state.idleReason = toStopReason(status.stopReason);
|
|
875
|
+
if (!state.inflight)
|
|
876
|
+
this.finishTurn(sessionId, state, state.idleReason);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const usage = status.usage ? usageUpdate(status.usage) : undefined;
|
|
880
|
+
if (usage)
|
|
881
|
+
this.onMappedUpdate(sessionId, usage);
|
|
882
|
+
if (!state.replaying && !this.children.has(sessionId))
|
|
883
|
+
this.host?.update(this.id, sessionId, { sessionUpdate: "ls_status", state: this.localState(sessionId), turnActive: state.turnActive });
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (version === 2 && state && record?.sessionUpdate === "user_message" && state.inflight > 0 && !state.replaying) {
|
|
887
|
+
state.deferredUserUpdates.push(raw);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const remapped = version === 2 && raw && typeof raw === "object" && "messageId" in raw && typeof raw.messageId === "string"
|
|
891
|
+
? { ...raw, messageId: state?.messageAliases.get(raw.messageId) ?? raw.messageId } : raw;
|
|
892
|
+
for (const update of state?.updates.map(remapped, version) ?? [normalizeAcpUpdate(remapped)].filter((entry) => !!entry))
|
|
893
|
+
this.onMappedUpdate(sessionId, update);
|
|
894
|
+
}
|
|
895
|
+
onMappedUpdate(sessionId, update) {
|
|
896
|
+
const state = this.sessions.get(sessionId);
|
|
452
897
|
if (state?.turnActive && (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update"))
|
|
453
898
|
state.toolCalls.add(update.toolCallId);
|
|
454
899
|
if (update.sessionUpdate === "current_mode_update" && state) {
|
|
@@ -457,11 +902,43 @@ export class AcpDriver {
|
|
|
457
902
|
mode.current = update.currentModeId;
|
|
458
903
|
}
|
|
459
904
|
if (update.sessionUpdate === "ls_config" && state) {
|
|
460
|
-
state.config = update.options.map((option) => ({ ...option, source: "configOptions" }));
|
|
905
|
+
state.config = this.mergeConfig(state, update.options.map((option) => ({ ...option, source: "configOptions" })));
|
|
906
|
+
update = { ...update, options: state.config.map(({ source: _source, ...option }) => option) };
|
|
907
|
+
}
|
|
908
|
+
if (update.sessionUpdate === "available_commands_update" && state)
|
|
909
|
+
state.commands = update;
|
|
910
|
+
if (state?.replaying) {
|
|
911
|
+
state.replaying.push(update);
|
|
912
|
+
return;
|
|
461
913
|
}
|
|
462
914
|
this.emit(sessionId, update);
|
|
915
|
+
if (this.connection?.protocolVersion !== 2 && (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update")) {
|
|
916
|
+
for (const content of update.content ?? []) {
|
|
917
|
+
if (content.type !== "terminal")
|
|
918
|
+
continue;
|
|
919
|
+
const key = `${sessionId}\n${content.terminalId}`;
|
|
920
|
+
const calls = this.terminalCalls.get(key) ?? new Set();
|
|
921
|
+
if (!calls.has(update.toolCallId)) {
|
|
922
|
+
calls.add(update.toolCallId);
|
|
923
|
+
this.terminalCalls.set(key, calls);
|
|
924
|
+
const snapshot = this.services.output(sessionId, content.terminalId);
|
|
925
|
+
if (snapshot)
|
|
926
|
+
this.emit(sessionId, { sessionUpdate: "tool_call_update", toolCallId: update.toolCallId, replaceOutput: snapshot.output });
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
463
930
|
}
|
|
464
931
|
onRequest(method, params, id) {
|
|
932
|
+
if (method === "mcp/message") {
|
|
933
|
+
const controller = new AbortController();
|
|
934
|
+
this.serviceRequests.set(id, controller);
|
|
935
|
+
return this.mcp.request(params, controller.signal).finally(() => this.serviceRequests.delete(id));
|
|
936
|
+
}
|
|
937
|
+
if (method.startsWith("fs/") || method.startsWith("terminal/")) {
|
|
938
|
+
const controller = new AbortController();
|
|
939
|
+
this.serviceRequests.set(id, controller);
|
|
940
|
+
return this.services.request(method, params, controller.signal).finally(() => this.serviceRequests.delete(id));
|
|
941
|
+
}
|
|
465
942
|
if (questionMethod(this.id, method))
|
|
466
943
|
return this.onQuestions(method, params, id);
|
|
467
944
|
if (method !== "session/request_permission") {
|
|
@@ -474,9 +951,9 @@ export class AcpDriver {
|
|
|
474
951
|
if (!state || !mapped)
|
|
475
952
|
return { outcome: { outcome: "cancelled" } };
|
|
476
953
|
return new Promise((resolve) => {
|
|
477
|
-
state.permissions.set(requestId, resolve);
|
|
954
|
+
state.permissions.set(requestId, { rpcId: id, resolve });
|
|
478
955
|
this.closeMessage(sessionId);
|
|
479
|
-
this.
|
|
956
|
+
this.publish(sessionId, mapped.update);
|
|
480
957
|
});
|
|
481
958
|
}
|
|
482
959
|
/** Questions are routed by explicit session id, then Cursor's tool id, never a "last session". */
|
|
@@ -500,6 +977,17 @@ export class AcpDriver {
|
|
|
500
977
|
throw new RpcError(-32602, "无法确定这个问题属于哪个会话,未提交任何回答");
|
|
501
978
|
}
|
|
502
979
|
onQuestions(method, params, id) {
|
|
980
|
+
const scope = params;
|
|
981
|
+
if (method === "elicitation/create" && scope.sessionId === undefined && (typeof scope.requestId === "string" || typeof scope.requestId === "number")) {
|
|
982
|
+
if (!this.connection?.hasPendingRequest(scope.requestId))
|
|
983
|
+
throw new RpcError(-32602, "授权请求已经结束");
|
|
984
|
+
const requestId = `${this.id}-q${this.nextPermissionId++}-${String(id)}`;
|
|
985
|
+
const request = mapAcpQuestions(this.id, method, params, requestId);
|
|
986
|
+
return new Promise((resolve) => {
|
|
987
|
+
this.interactions.set(requestId, { rpcId: id, parentId: scope.requestId, request, resolve });
|
|
988
|
+
this.host?.interaction?.(this.id, request.update);
|
|
989
|
+
});
|
|
990
|
+
}
|
|
503
991
|
const sessionId = this.questionSession(params);
|
|
504
992
|
const state = this.sessions.get(sessionId);
|
|
505
993
|
const requestId = `${this.id}-q${this.nextPermissionId++}-${String(id)}`;
|
|
@@ -507,33 +995,50 @@ export class AcpDriver {
|
|
|
507
995
|
return new Promise((resolve) => {
|
|
508
996
|
state.questions.set(requestId, { rpcId: id, request, resolve });
|
|
509
997
|
this.closeMessage(sessionId);
|
|
510
|
-
this.
|
|
998
|
+
this.publish(sessionId, request.update);
|
|
511
999
|
});
|
|
512
1000
|
}
|
|
513
1001
|
cancelQuestionRequest(id) {
|
|
1002
|
+
this.serviceRequests.get(id)?.abort();
|
|
1003
|
+
for (const [requestId, pending] of this.interactions)
|
|
1004
|
+
if (pending.rpcId === id)
|
|
1005
|
+
this.resolveInteraction(requestId, "cancel");
|
|
514
1006
|
for (const [sessionId, state] of this.sessions) {
|
|
1007
|
+
for (const [requestId, pending] of state.permissions) {
|
|
1008
|
+
if (pending.rpcId !== id)
|
|
1009
|
+
continue;
|
|
1010
|
+
state.permissions.delete(requestId);
|
|
1011
|
+
pending.resolve({ outcome: { outcome: "cancelled" } });
|
|
1012
|
+
this.publish(sessionId, { sessionUpdate: "ls_permission_resolved", requestId });
|
|
1013
|
+
}
|
|
515
1014
|
for (const [requestId, pending] of state.questions) {
|
|
516
1015
|
if (pending.rpcId !== id)
|
|
517
1016
|
continue;
|
|
518
1017
|
state.questions.delete(requestId);
|
|
519
1018
|
pending.resolve(pending.request.respond("cancel"));
|
|
520
|
-
this.
|
|
1019
|
+
this.publish(sessionId, { sessionUpdate: "ls_permission_resolved", requestId });
|
|
521
1020
|
}
|
|
522
1021
|
}
|
|
523
1022
|
}
|
|
524
1023
|
async answerQuestion(nativeId, requestId, answers) {
|
|
525
|
-
const
|
|
1024
|
+
const owner = this.requestOwner(nativeId, requestId);
|
|
1025
|
+
const state = owner?.state;
|
|
526
1026
|
const pending = state?.questions.get(requestId);
|
|
527
1027
|
if (!state || !pending)
|
|
528
1028
|
throw RpcError.app("not_found", "这个问题已经不在等回答了");
|
|
529
1029
|
const response = pending.request.answer(answers);
|
|
530
1030
|
state.questions.delete(requestId);
|
|
531
1031
|
pending.resolve(response);
|
|
532
|
-
|
|
1032
|
+
const recorded = answers.map((answer) => pending.request.update.questions?.find((question) => question.id === answer.id)?.secret ? { id: answer.id, values: ["••••••"] } : answer);
|
|
1033
|
+
this.publish(owner.id, { sessionUpdate: "ls_permission_resolved", requestId, optionId: "answered", answers: recorded });
|
|
533
1034
|
}
|
|
534
1035
|
cancelPermissions(state) {
|
|
535
|
-
|
|
536
|
-
|
|
1036
|
+
const nativeId = [...this.sessions].find(([, value]) => value === state)?.[0];
|
|
1037
|
+
if (nativeId)
|
|
1038
|
+
for (const requestId of [...state.permissions.keys(), ...state.questions.keys()])
|
|
1039
|
+
this.publish(nativeId, { sessionUpdate: "ls_permission_resolved", requestId });
|
|
1040
|
+
for (const pending of state.permissions.values())
|
|
1041
|
+
pending.resolve({ outcome: { outcome: "cancelled" } });
|
|
537
1042
|
state.permissions.clear();
|
|
538
1043
|
for (const pending of state.questions.values())
|
|
539
1044
|
pending.resolve(pending.request.respond("cancel"));
|
|
@@ -549,16 +1054,28 @@ export class AcpDriver {
|
|
|
549
1054
|
}
|
|
550
1055
|
startTurn(nativeId, state, content, clientMessageId, context) {
|
|
551
1056
|
state.turnActive = true;
|
|
1057
|
+
state.reportedState = undefined;
|
|
1058
|
+
state.idleReason = undefined;
|
|
552
1059
|
this.echoUserMessage(nativeId, content, clientMessageId);
|
|
553
1060
|
this.emit(nativeId, { sessionUpdate: "ls_turn", state: "started" });
|
|
554
|
-
void this.sendPrompt(nativeId, state, withContext(toAcpPrompt(content), context));
|
|
1061
|
+
void this.sendPrompt(nativeId, state, withContext(toAcpPrompt(content), context), clientMessageId);
|
|
555
1062
|
}
|
|
556
|
-
async sendPrompt(nativeId, state, prompt) {
|
|
1063
|
+
async sendPrompt(nativeId, state, prompt, clientMessageId) {
|
|
557
1064
|
state.inflight += 1;
|
|
558
1065
|
let stopReason;
|
|
559
1066
|
try {
|
|
560
1067
|
const result = await this.rpc("session/prompt", { sessionId: nativeId, prompt }, 0);
|
|
561
|
-
|
|
1068
|
+
if (this.connection?.protocolVersion === 2) {
|
|
1069
|
+
if (typeof result.messageId !== "string")
|
|
1070
|
+
throw new Error("ACP 2 未返回已接收的消息 ID");
|
|
1071
|
+
state.messageAliases.set(result.messageId, `local-${clientMessageId}`);
|
|
1072
|
+
this.host?.state(this.id, nativeId).set("acp-message-aliases", JSON.stringify([...state.messageAliases]));
|
|
1073
|
+
}
|
|
1074
|
+
else
|
|
1075
|
+
stopReason = toStopReason(result.stopReason);
|
|
1076
|
+
const usage = result.usage ? usageUpdate(result.usage) : undefined;
|
|
1077
|
+
if (usage)
|
|
1078
|
+
this.emit(nativeId, usage);
|
|
562
1079
|
}
|
|
563
1080
|
catch (error) {
|
|
564
1081
|
stopReason = "error";
|
|
@@ -570,8 +1087,19 @@ export class AcpDriver {
|
|
|
570
1087
|
// A steered prompt settles the earlier one; the turn ends with the last.
|
|
571
1088
|
if (state.inflight > 0)
|
|
572
1089
|
return;
|
|
1090
|
+
for (const raw of state.deferredUserUpdates.splice(0))
|
|
1091
|
+
this.onUpdate(nativeId, raw);
|
|
1092
|
+
const ended = stopReason ?? state.idleReason;
|
|
1093
|
+
if (ended)
|
|
1094
|
+
this.finishTurn(nativeId, state, ended);
|
|
1095
|
+
}
|
|
1096
|
+
finishTurn(nativeId, state, stopReason) {
|
|
1097
|
+
if (!state.turnActive)
|
|
1098
|
+
return;
|
|
573
1099
|
this.closeMessage(nativeId);
|
|
574
1100
|
state.turnActive = false;
|
|
1101
|
+
state.reportedState = stopReason === "error" ? "error" : "idle";
|
|
1102
|
+
state.idleReason = undefined;
|
|
575
1103
|
state.toolCalls.clear();
|
|
576
1104
|
this.cancelPermissions(state);
|
|
577
1105
|
this.emit(nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason });
|
|
@@ -581,6 +1109,9 @@ export class AcpDriver {
|
|
|
581
1109
|
if (next && state.loaded)
|
|
582
1110
|
this.startTurn(nativeId, state, next.content, next.clientMessageId, next.context);
|
|
583
1111
|
}
|
|
1112
|
+
settings() {
|
|
1113
|
+
return this.host?.agentSettings?.(this.id) ?? this.spec.settings ?? acpAgentSettingsSchema.parse({});
|
|
1114
|
+
}
|
|
584
1115
|
/** The agent's own delete, when it has one (ACP session/delete). */
|
|
585
1116
|
async delete(nativeId) {
|
|
586
1117
|
if (!this.connection?.alive)
|
|
@@ -631,11 +1162,20 @@ export class AcpDriver {
|
|
|
631
1162
|
}
|
|
632
1163
|
const connection = this.connection;
|
|
633
1164
|
const opensSession = ["session/new", "session/load", "session/resume", "session/fork"].includes(method);
|
|
1165
|
+
const serverIds = opensSession ? (params.mcpServers ?? []).flatMap((server) => server.type === "acp" && server.serverId ? [server.serverId] : []) : [];
|
|
1166
|
+
let succeeded = false;
|
|
1167
|
+
const bindServers = (result) => {
|
|
1168
|
+
succeeded = true;
|
|
1169
|
+
const sessionId = result?.sessionId ?? params.sessionId;
|
|
1170
|
+
if (sessionId)
|
|
1171
|
+
this.mcp.bind(serverIds, sessionId);
|
|
1172
|
+
return result;
|
|
1173
|
+
};
|
|
634
1174
|
try {
|
|
635
1175
|
const result = await connection.request(method, params, timeoutMs);
|
|
636
1176
|
if (opensSession)
|
|
637
1177
|
this.signedOut = undefined;
|
|
638
|
-
return result;
|
|
1178
|
+
return bindServers(result);
|
|
639
1179
|
}
|
|
640
1180
|
catch (error) {
|
|
641
1181
|
if (!opensSession)
|
|
@@ -659,12 +1199,19 @@ export class AcpDriver {
|
|
|
659
1199
|
await this.authenticating.promise;
|
|
660
1200
|
const result = await connection.request(method, params, timeoutMs);
|
|
661
1201
|
this.signedOut = undefined;
|
|
662
|
-
return result;
|
|
1202
|
+
return bindServers(result);
|
|
663
1203
|
}
|
|
664
1204
|
catch (retryError) {
|
|
665
1205
|
throw this.signInError(retryError);
|
|
666
1206
|
}
|
|
667
1207
|
}
|
|
1208
|
+
finally {
|
|
1209
|
+
if (!succeeded && serverIds.length)
|
|
1210
|
+
await this.mcp.close(undefined, serverIds);
|
|
1211
|
+
for (const [requestId, pending] of this.interactions)
|
|
1212
|
+
if (!connection.hasPendingRequest(pending.parentId))
|
|
1213
|
+
this.resolveInteraction(requestId, "cancel");
|
|
1214
|
+
}
|
|
668
1215
|
}
|
|
669
1216
|
}
|
|
670
1217
|
//# sourceMappingURL=driver.js.map
|