@linkshell/host 0.4.18 → 0.4.20
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 +14 -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 +40 -7
- package/dist/host/src/drivers/acp/connection.js +132 -103
- package/dist/host/src/drivers/acp/connection.js.map +1 -1
- package/dist/host/src/drivers/acp/driver.d.ts +95 -2
- package/dist/host/src/drivers/acp/driver.js +628 -53
- 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.d.ts +4 -1
- package/dist/host/src/drivers/claude/driver.js +42 -1
- 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/archives.d.ts +2 -0
- package/dist/host/src/drivers/codex/archives.js +18 -0
- package/dist/host/src/drivers/codex/archives.js.map +1 -0
- package/dist/host/src/drivers/codex/attention.js +2 -2
- package/dist/host/src/drivers/codex/attention.js.map +1 -1
- package/dist/host/src/drivers/codex/driver.d.ts +4 -0
- package/dist/host/src/drivers/codex/driver.js +51 -2
- 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 +38 -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 +250 -2
- package/dist/host/src/hub.js +184 -13
- 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.js +5 -5
- 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.d.ts +2 -0
- package/dist/host/src/store.js +16 -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,7 +1,15 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import {
|
|
4
|
-
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";
|
|
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";
|
|
7
15
|
const execFileAsync = promisify(execFile);
|
|
@@ -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,8 +318,13 @@ 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
|
-
found.push({
|
|
324
|
+
found.push({
|
|
325
|
+
nativeId: info.sessionId, cwd: info.cwd, title: info.title ?? undefined, createdAt: updatedAt, updatedAt,
|
|
326
|
+
state: await this.discoveredState(info.sessionId, info.cwd),
|
|
327
|
+
});
|
|
111
328
|
if (found.length >= limit)
|
|
112
329
|
return found;
|
|
113
330
|
}
|
|
@@ -115,6 +332,19 @@ export class AcpDriver {
|
|
|
115
332
|
} while (cursor);
|
|
116
333
|
return found;
|
|
117
334
|
}
|
|
335
|
+
/** ACP v1 does not include live state in session/list; only this connection's turns are known. */
|
|
336
|
+
async discoveredState(nativeId, _cwd) {
|
|
337
|
+
return this.localState(nativeId);
|
|
338
|
+
}
|
|
339
|
+
localState(nativeId) {
|
|
340
|
+
const state = this.sessions.get(nativeId);
|
|
341
|
+
if (state?.permissions.size || state?.questions.size)
|
|
342
|
+
return "waiting";
|
|
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");
|
|
347
|
+
}
|
|
118
348
|
/** ACP's session/fork, for agents that offer it: the whole conversation, in `cwd`. */
|
|
119
349
|
async fork(nativeId, options) {
|
|
120
350
|
if (!this.connection?.alive)
|
|
@@ -123,7 +353,9 @@ export class AcpDriver {
|
|
|
123
353
|
throw RpcError.app("not_supported", `${this.label} 不支持从会话分叉`);
|
|
124
354
|
if (options.upTo)
|
|
125
355
|
throw RpcError.app("not_supported", `${this.label} 只能分叉整个会话`);
|
|
126
|
-
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;
|
|
127
359
|
const now = Date.now();
|
|
128
360
|
return { nativeId: response.sessionId, cwd: options.cwd, createdAt: now, updatedAt: now };
|
|
129
361
|
}
|
|
@@ -135,12 +367,13 @@ export class AcpDriver {
|
|
|
135
367
|
return this.id === "claude" ? { _meta: { claudeCode: { emitRawSDKMessages: [{ type: "active_goal" }] } } } : {};
|
|
136
368
|
}
|
|
137
369
|
async createSession(options) {
|
|
370
|
+
await this.ensureStarted();
|
|
371
|
+
if (this.connection?.capabilities.sessions === false)
|
|
372
|
+
throw RpcError.app("not_supported", "这个 ACP Agent 不支持会话,可在文件中使用编辑建议");
|
|
138
373
|
let response;
|
|
139
374
|
try {
|
|
140
375
|
response = await this.rpc("session/new", {
|
|
141
|
-
|
|
142
|
-
mcpServers: [],
|
|
143
|
-
...this.sessionMeta(),
|
|
376
|
+
...await this.sessionParameters(options.cwd),
|
|
144
377
|
});
|
|
145
378
|
}
|
|
146
379
|
catch (error) {
|
|
@@ -150,31 +383,44 @@ export class AcpDriver {
|
|
|
150
383
|
const state = this.stateFor(response.sessionId, options.cwd);
|
|
151
384
|
state.loaded = true;
|
|
152
385
|
state.config = toConfigOptions(response);
|
|
386
|
+
this.sessionCommands(state, response);
|
|
153
387
|
if (options.model)
|
|
154
388
|
await this.setConfig(response.sessionId, "model", options.model).catch(() => { });
|
|
155
389
|
const now = Date.now();
|
|
156
390
|
return { nativeId: response.sessionId, cwd: options.cwd, createdAt: now, updatedAt: now, state: "idle" };
|
|
157
391
|
}
|
|
158
392
|
async attach(nativeId, context) {
|
|
393
|
+
// Lazy agents have no capabilities until initialize has completed.
|
|
394
|
+
await this.ensureStarted();
|
|
395
|
+
if (!this.connection?.alive)
|
|
396
|
+
throw RpcError.app("agent_unavailable", this.current.problem ?? `${this.label} 尚未启动`);
|
|
159
397
|
const state = this.stateFor(nativeId, context.cwd);
|
|
160
398
|
if (state.loaded) {
|
|
161
399
|
this.emitConfig(nativeId, state);
|
|
400
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: this.localState(nativeId), turnActive: state.turnActive });
|
|
162
401
|
return [];
|
|
163
402
|
}
|
|
164
403
|
const capabilities = this.connection?.capabilities;
|
|
165
404
|
let history = [];
|
|
166
|
-
|
|
405
|
+
state.config = [];
|
|
406
|
+
state.commands = undefined;
|
|
407
|
+
if (capabilities?.loadSession || this.connection.protocolVersion === 2) {
|
|
167
408
|
state.replaying = [];
|
|
409
|
+
state.updates = new AcpUpdates();
|
|
168
410
|
try {
|
|
169
|
-
const response = await this.rpc("session/load", {
|
|
411
|
+
const response = await this.rpc(this.connection.protocolVersion === 2 ? "session/resume" : "session/load", {
|
|
170
412
|
sessionId: nativeId,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
...this.sessionMeta(),
|
|
413
|
+
...await this.sessionParameters(context.cwd, state.settings),
|
|
414
|
+
...(this.connection.protocolVersion === 2 ? { replayFrom: { type: "start" } } : {}),
|
|
174
415
|
});
|
|
175
|
-
|
|
416
|
+
if (response.configOptions !== undefined || response.modes !== undefined || response.models !== undefined)
|
|
417
|
+
state.config = this.mergeConfig(state, toConfigOptions(response));
|
|
418
|
+
this.sessionCommands(state, response);
|
|
176
419
|
state.tracker = new AcpItemTracker();
|
|
177
|
-
|
|
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 });
|
|
178
424
|
}
|
|
179
425
|
finally {
|
|
180
426
|
state.replaying = undefined;
|
|
@@ -183,17 +429,24 @@ export class AcpDriver {
|
|
|
183
429
|
else if (capabilities?.sessionCapabilities?.resume) {
|
|
184
430
|
const response = await this.rpc("session/resume", {
|
|
185
431
|
sessionId: nativeId,
|
|
186
|
-
|
|
187
|
-
mcpServers: [],
|
|
188
|
-
...this.sessionMeta(),
|
|
432
|
+
...await this.sessionParameters(context.cwd, state.settings),
|
|
189
433
|
});
|
|
190
434
|
state.config = toConfigOptions(response);
|
|
435
|
+
this.sessionCommands(state, response);
|
|
436
|
+
history.push({
|
|
437
|
+
itemId: "linkshell:acp-history-unavailable",
|
|
438
|
+
updates: [{
|
|
439
|
+
sessionUpdate: "ls_notice", level: "info", title: "这个 AI 暂不提供以往消息",
|
|
440
|
+
detail: "会话已恢复;这里只显示 LinkShell 已缓存的内容和之后的新消息。",
|
|
441
|
+
}],
|
|
442
|
+
});
|
|
191
443
|
}
|
|
192
444
|
else {
|
|
193
|
-
throw RpcError.app("not_supported", `${this.label}
|
|
445
|
+
throw RpcError.app("not_supported", `${this.label} 当前版本不支持恢复以往会话;已缓存的消息仍可查看。`);
|
|
194
446
|
}
|
|
195
447
|
state.loaded = true;
|
|
196
448
|
this.emitConfig(nativeId, state);
|
|
449
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: this.localState(nativeId), turnActive: state.turnActive });
|
|
197
450
|
return history;
|
|
198
451
|
}
|
|
199
452
|
async detach(nativeId) {
|
|
@@ -202,6 +455,11 @@ export class AcpDriver {
|
|
|
202
455
|
return;
|
|
203
456
|
state.loaded = false;
|
|
204
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);
|
|
205
463
|
if (this.connection?.capabilities.sessionCapabilities?.close) {
|
|
206
464
|
await this.rpc("session/close", { sessionId: nativeId }).catch(() => { });
|
|
207
465
|
}
|
|
@@ -210,13 +468,19 @@ export class AcpDriver {
|
|
|
210
468
|
const state = this.sessions.get(nativeId);
|
|
211
469
|
if (!state?.loaded)
|
|
212
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
|
+
}
|
|
213
477
|
const prompt = toAcpPrompt(content);
|
|
214
478
|
if (prompt.length === 0)
|
|
215
479
|
throw RpcError.app("invalid_params", "nothing to send");
|
|
216
480
|
if (state.turnActive) {
|
|
217
481
|
if (this.capabilities.steer) {
|
|
218
482
|
this.echoUserMessage(nativeId, content, clientMessageId);
|
|
219
|
-
void this.sendPrompt(nativeId, state, withContext(prompt, context));
|
|
483
|
+
void this.sendPrompt(nativeId, state, withContext(prompt, context), clientMessageId);
|
|
220
484
|
return "steered";
|
|
221
485
|
}
|
|
222
486
|
state.queue.push({ content, clientMessageId, context });
|
|
@@ -236,6 +500,12 @@ export class AcpDriver {
|
|
|
236
500
|
this.cancelPermissions(state);
|
|
237
501
|
this.connection?.notify("session/cancel", { sessionId: nativeId });
|
|
238
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
|
+
}
|
|
239
509
|
/** Stops the running turn only: the first queued message starts as soon as it has ended. */
|
|
240
510
|
async sendQueuedNow(nativeId) {
|
|
241
511
|
const state = this.sessions.get(nativeId);
|
|
@@ -245,20 +515,21 @@ export class AcpDriver {
|
|
|
245
515
|
this.connection?.notify("session/cancel", { sessionId: nativeId });
|
|
246
516
|
}
|
|
247
517
|
async respondPermission(nativeId, requestId, optionId) {
|
|
248
|
-
const
|
|
518
|
+
const owner = this.requestOwner(nativeId, requestId);
|
|
519
|
+
const question = owner?.state.questions.get(requestId);
|
|
249
520
|
if (question) {
|
|
250
521
|
// Not answering: skip (the agent goes on without an answer) or stop.
|
|
251
|
-
|
|
522
|
+
owner.state.questions.delete(requestId);
|
|
252
523
|
question.resolve(question.request.respond(optionId));
|
|
253
|
-
this.
|
|
524
|
+
this.publish(owner.id, { sessionUpdate: "ls_permission_resolved", requestId, optionId });
|
|
254
525
|
return;
|
|
255
526
|
}
|
|
256
|
-
const
|
|
257
|
-
if (!
|
|
527
|
+
const pending = owner?.state.permissions.get(requestId);
|
|
528
|
+
if (!pending)
|
|
258
529
|
throw RpcError.app("not_found", "this permission request is no longer pending");
|
|
259
|
-
|
|
260
|
-
resolve({ outcome: { outcome: "selected", optionId } });
|
|
261
|
-
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 });
|
|
262
533
|
}
|
|
263
534
|
async setConfig(nativeId, optionId, value) {
|
|
264
535
|
const state = this.sessions.get(nativeId);
|
|
@@ -275,13 +546,24 @@ export class AcpDriver {
|
|
|
275
546
|
await this.rpc("session/set_model", { sessionId: nativeId, modelId: value });
|
|
276
547
|
}
|
|
277
548
|
else {
|
|
278
|
-
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
|
+
}
|
|
279
557
|
}
|
|
280
558
|
option.current = value;
|
|
281
559
|
this.emitConfig(nativeId, state);
|
|
282
560
|
}
|
|
283
561
|
// ── internals ──────────────────────────────────────────────────────
|
|
284
562
|
async detectVersion() {
|
|
563
|
+
if (this.spec.transport === "http" || this.spec.transport === "websocket")
|
|
564
|
+
return "remote";
|
|
565
|
+
if (this.spec.transport === "stdio")
|
|
566
|
+
return "";
|
|
285
567
|
const probe = this.spec.version ?? { command: this.spec.command, args: ["--version"] };
|
|
286
568
|
try {
|
|
287
569
|
const { stdout, stderr } = await execFileAsync(probe.command, probe.args, { env: this.env, timeout: 15_000 });
|
|
@@ -296,22 +578,63 @@ export class AcpDriver {
|
|
|
296
578
|
booting;
|
|
297
579
|
/** Starts the agent process once; concurrent callers share the attempt. */
|
|
298
580
|
ensureStarted() {
|
|
299
|
-
|
|
581
|
+
const refresh = this.credentialsChanged && ![...this.sessions.values()].some((state) => state.turnActive);
|
|
582
|
+
if (this.connection?.alive && !refresh)
|
|
300
583
|
return Promise.resolve();
|
|
301
|
-
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(() => {
|
|
302
596
|
this.booting = undefined;
|
|
303
597
|
});
|
|
304
598
|
return this.booting;
|
|
305
599
|
}
|
|
306
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
|
+
}
|
|
307
609
|
const connection = new AcpConnection({
|
|
308
610
|
command: this.spec.command,
|
|
309
611
|
args: this.spec.args,
|
|
310
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,
|
|
311
616
|
clientVersion: this.options.hostVersion,
|
|
312
617
|
onUpdate: (sessionId, update) => this.onUpdate(sessionId, update),
|
|
313
618
|
onRequest: (method, params, id) => this.onRequest(method, params, id),
|
|
314
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
|
+
},
|
|
315
638
|
onExit: (reason) => this.onExit(reason),
|
|
316
639
|
});
|
|
317
640
|
this.connection = connection;
|
|
@@ -325,18 +648,22 @@ export class AcpDriver {
|
|
|
325
648
|
...this.capabilities,
|
|
326
649
|
steer: steering || queueing,
|
|
327
650
|
images: caps.promptCapabilities?.image === true,
|
|
651
|
+
audio: caps.promptCapabilities?.audio === true,
|
|
652
|
+
embeddedContext: caps.promptCapabilities?.embeddedContext === true,
|
|
328
653
|
fork: Boolean(caps.sessionCapabilities?.fork),
|
|
329
654
|
};
|
|
330
655
|
this.current = { installed: true, version: this.current.version };
|
|
331
656
|
this.restartDelay = 1000;
|
|
332
657
|
}
|
|
333
658
|
catch (error) {
|
|
659
|
+
await connection.stop();
|
|
334
660
|
this.current = {
|
|
335
661
|
installed: true,
|
|
336
662
|
version: this.current.version,
|
|
337
663
|
problem: `${this.label} failed to start: ${error instanceof Error ? error.message : String(error)}`,
|
|
338
664
|
};
|
|
339
|
-
|
|
665
|
+
if (!(error instanceof AcpProtocolError))
|
|
666
|
+
this.scheduleRestart();
|
|
340
667
|
}
|
|
341
668
|
}
|
|
342
669
|
scheduleRestart() {
|
|
@@ -356,6 +683,15 @@ export class AcpDriver {
|
|
|
356
683
|
if (this.stopped)
|
|
357
684
|
return;
|
|
358
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");
|
|
359
695
|
for (const [nativeId, state] of this.sessions) {
|
|
360
696
|
if (!state.loaded)
|
|
361
697
|
continue;
|
|
@@ -374,10 +710,15 @@ export class AcpDriver {
|
|
|
374
710
|
stateFor(nativeId, cwd) {
|
|
375
711
|
let state = this.sessions.get(nativeId);
|
|
376
712
|
if (!state) {
|
|
713
|
+
const saved = this.host?.state(this.id, nativeId).get("acp-settings");
|
|
377
714
|
state = {
|
|
378
715
|
cwd,
|
|
716
|
+
settings: saved ? acpAgentSettingsSchema.parse(JSON.parse(saved)) : { ...this.settings(), ...(this.discoveredDirectories.has(nativeId) ? { additionalDirectories: this.discoveredDirectories.get(nativeId) } : {}) },
|
|
379
717
|
loaded: false,
|
|
380
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: [],
|
|
381
722
|
config: [],
|
|
382
723
|
turnActive: false,
|
|
383
724
|
inflight: 0,
|
|
@@ -391,36 +732,168 @@ export class AcpDriver {
|
|
|
391
732
|
return state;
|
|
392
733
|
}
|
|
393
734
|
emitConfig(nativeId, state) {
|
|
394
|
-
if (state.config.length === 0)
|
|
395
|
-
return;
|
|
396
735
|
this.host?.update(this.id, nativeId, {
|
|
397
736
|
sessionUpdate: "ls_config",
|
|
398
737
|
options: state.config.map(({ source: _source, ...option }) => option),
|
|
399
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))];
|
|
400
752
|
}
|
|
401
753
|
/** Feeds an update through the item tracker and on to the host. */
|
|
402
754
|
emit(nativeId, update) {
|
|
403
755
|
const state = this.sessions.get(nativeId);
|
|
404
756
|
const tracked = state ? state.tracker.feed(update) : [{ update }];
|
|
405
757
|
for (const entry of tracked)
|
|
406
|
-
this.
|
|
758
|
+
this.publish(nativeId, entry.update, entry.itemId);
|
|
407
759
|
}
|
|
408
760
|
closeMessage(nativeId) {
|
|
409
761
|
const state = this.sessions.get(nativeId);
|
|
410
762
|
if (!state)
|
|
411
763
|
return;
|
|
412
764
|
for (const entry of state.tracker.close())
|
|
413
|
-
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;
|
|
414
842
|
}
|
|
415
843
|
onUpdate(sessionId, raw) {
|
|
416
|
-
const
|
|
417
|
-
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
|
+
}
|
|
418
851
|
return;
|
|
852
|
+
}
|
|
419
853
|
const state = this.sessions.get(sessionId);
|
|
420
|
-
if (
|
|
421
|
-
|
|
854
|
+
if (record?.sessionUpdate === "subagent_update" && this.settings().experimental) {
|
|
855
|
+
this.subagentUpdate(sessionId, raw);
|
|
856
|
+
return;
|
|
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 });
|
|
422
884
|
return;
|
|
423
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);
|
|
424
897
|
if (state?.turnActive && (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update"))
|
|
425
898
|
state.toolCalls.add(update.toolCallId);
|
|
426
899
|
if (update.sessionUpdate === "current_mode_update" && state) {
|
|
@@ -429,11 +902,43 @@ export class AcpDriver {
|
|
|
429
902
|
mode.current = update.currentModeId;
|
|
430
903
|
}
|
|
431
904
|
if (update.sessionUpdate === "ls_config" && state) {
|
|
432
|
-
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;
|
|
433
913
|
}
|
|
434
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
|
+
}
|
|
435
930
|
}
|
|
436
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
|
+
}
|
|
437
942
|
if (questionMethod(this.id, method))
|
|
438
943
|
return this.onQuestions(method, params, id);
|
|
439
944
|
if (method !== "session/request_permission") {
|
|
@@ -446,9 +951,9 @@ export class AcpDriver {
|
|
|
446
951
|
if (!state || !mapped)
|
|
447
952
|
return { outcome: { outcome: "cancelled" } };
|
|
448
953
|
return new Promise((resolve) => {
|
|
449
|
-
state.permissions.set(requestId, resolve);
|
|
954
|
+
state.permissions.set(requestId, { rpcId: id, resolve });
|
|
450
955
|
this.closeMessage(sessionId);
|
|
451
|
-
this.
|
|
956
|
+
this.publish(sessionId, mapped.update);
|
|
452
957
|
});
|
|
453
958
|
}
|
|
454
959
|
/** Questions are routed by explicit session id, then Cursor's tool id, never a "last session". */
|
|
@@ -472,6 +977,17 @@ export class AcpDriver {
|
|
|
472
977
|
throw new RpcError(-32602, "无法确定这个问题属于哪个会话,未提交任何回答");
|
|
473
978
|
}
|
|
474
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
|
+
}
|
|
475
991
|
const sessionId = this.questionSession(params);
|
|
476
992
|
const state = this.sessions.get(sessionId);
|
|
477
993
|
const requestId = `${this.id}-q${this.nextPermissionId++}-${String(id)}`;
|
|
@@ -479,33 +995,50 @@ export class AcpDriver {
|
|
|
479
995
|
return new Promise((resolve) => {
|
|
480
996
|
state.questions.set(requestId, { rpcId: id, request, resolve });
|
|
481
997
|
this.closeMessage(sessionId);
|
|
482
|
-
this.
|
|
998
|
+
this.publish(sessionId, request.update);
|
|
483
999
|
});
|
|
484
1000
|
}
|
|
485
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");
|
|
486
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
|
+
}
|
|
487
1014
|
for (const [requestId, pending] of state.questions) {
|
|
488
1015
|
if (pending.rpcId !== id)
|
|
489
1016
|
continue;
|
|
490
1017
|
state.questions.delete(requestId);
|
|
491
1018
|
pending.resolve(pending.request.respond("cancel"));
|
|
492
|
-
this.
|
|
1019
|
+
this.publish(sessionId, { sessionUpdate: "ls_permission_resolved", requestId });
|
|
493
1020
|
}
|
|
494
1021
|
}
|
|
495
1022
|
}
|
|
496
1023
|
async answerQuestion(nativeId, requestId, answers) {
|
|
497
|
-
const
|
|
1024
|
+
const owner = this.requestOwner(nativeId, requestId);
|
|
1025
|
+
const state = owner?.state;
|
|
498
1026
|
const pending = state?.questions.get(requestId);
|
|
499
1027
|
if (!state || !pending)
|
|
500
1028
|
throw RpcError.app("not_found", "这个问题已经不在等回答了");
|
|
501
1029
|
const response = pending.request.answer(answers);
|
|
502
1030
|
state.questions.delete(requestId);
|
|
503
1031
|
pending.resolve(response);
|
|
504
|
-
|
|
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 });
|
|
505
1034
|
}
|
|
506
1035
|
cancelPermissions(state) {
|
|
507
|
-
|
|
508
|
-
|
|
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" } });
|
|
509
1042
|
state.permissions.clear();
|
|
510
1043
|
for (const pending of state.questions.values())
|
|
511
1044
|
pending.resolve(pending.request.respond("cancel"));
|
|
@@ -521,16 +1054,28 @@ export class AcpDriver {
|
|
|
521
1054
|
}
|
|
522
1055
|
startTurn(nativeId, state, content, clientMessageId, context) {
|
|
523
1056
|
state.turnActive = true;
|
|
1057
|
+
state.reportedState = undefined;
|
|
1058
|
+
state.idleReason = undefined;
|
|
524
1059
|
this.echoUserMessage(nativeId, content, clientMessageId);
|
|
525
1060
|
this.emit(nativeId, { sessionUpdate: "ls_turn", state: "started" });
|
|
526
|
-
void this.sendPrompt(nativeId, state, withContext(toAcpPrompt(content), context));
|
|
1061
|
+
void this.sendPrompt(nativeId, state, withContext(toAcpPrompt(content), context), clientMessageId);
|
|
527
1062
|
}
|
|
528
|
-
async sendPrompt(nativeId, state, prompt) {
|
|
1063
|
+
async sendPrompt(nativeId, state, prompt, clientMessageId) {
|
|
529
1064
|
state.inflight += 1;
|
|
530
1065
|
let stopReason;
|
|
531
1066
|
try {
|
|
532
1067
|
const result = await this.rpc("session/prompt", { sessionId: nativeId, prompt }, 0);
|
|
533
|
-
|
|
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);
|
|
534
1079
|
}
|
|
535
1080
|
catch (error) {
|
|
536
1081
|
stopReason = "error";
|
|
@@ -542,8 +1087,19 @@ export class AcpDriver {
|
|
|
542
1087
|
// A steered prompt settles the earlier one; the turn ends with the last.
|
|
543
1088
|
if (state.inflight > 0)
|
|
544
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;
|
|
545
1099
|
this.closeMessage(nativeId);
|
|
546
1100
|
state.turnActive = false;
|
|
1101
|
+
state.reportedState = stopReason === "error" ? "error" : "idle";
|
|
1102
|
+
state.idleReason = undefined;
|
|
547
1103
|
state.toolCalls.clear();
|
|
548
1104
|
this.cancelPermissions(state);
|
|
549
1105
|
this.emit(nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason });
|
|
@@ -553,6 +1109,9 @@ export class AcpDriver {
|
|
|
553
1109
|
if (next && state.loaded)
|
|
554
1110
|
this.startTurn(nativeId, state, next.content, next.clientMessageId, next.context);
|
|
555
1111
|
}
|
|
1112
|
+
settings() {
|
|
1113
|
+
return this.host?.agentSettings?.(this.id) ?? this.spec.settings ?? acpAgentSettingsSchema.parse({});
|
|
1114
|
+
}
|
|
556
1115
|
/** The agent's own delete, when it has one (ACP session/delete). */
|
|
557
1116
|
async delete(nativeId) {
|
|
558
1117
|
if (!this.connection?.alive)
|
|
@@ -603,11 +1162,20 @@ export class AcpDriver {
|
|
|
603
1162
|
}
|
|
604
1163
|
const connection = this.connection;
|
|
605
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
|
+
};
|
|
606
1174
|
try {
|
|
607
1175
|
const result = await connection.request(method, params, timeoutMs);
|
|
608
1176
|
if (opensSession)
|
|
609
1177
|
this.signedOut = undefined;
|
|
610
|
-
return result;
|
|
1178
|
+
return bindServers(result);
|
|
611
1179
|
}
|
|
612
1180
|
catch (error) {
|
|
613
1181
|
if (!opensSession)
|
|
@@ -631,12 +1199,19 @@ export class AcpDriver {
|
|
|
631
1199
|
await this.authenticating.promise;
|
|
632
1200
|
const result = await connection.request(method, params, timeoutMs);
|
|
633
1201
|
this.signedOut = undefined;
|
|
634
|
-
return result;
|
|
1202
|
+
return bindServers(result);
|
|
635
1203
|
}
|
|
636
1204
|
catch (retryError) {
|
|
637
1205
|
throw this.signInError(retryError);
|
|
638
1206
|
}
|
|
639
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
|
+
}
|
|
640
1215
|
}
|
|
641
1216
|
}
|
|
642
1217
|
//# sourceMappingURL=driver.js.map
|