@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.
Files changed (109) hide show
  1. package/README.md +14 -2
  2. package/dist/host/src/acp-settings.d.ts +12 -0
  3. package/dist/host/src/acp-settings.js +41 -0
  4. package/dist/host/src/acp-settings.js.map +1 -0
  5. package/dist/host/src/carry.js +13 -2
  6. package/dist/host/src/carry.js.map +1 -1
  7. package/dist/host/src/drivers/acp/client-services.d.ts +29 -0
  8. package/dist/host/src/drivers/acp/client-services.js +192 -0
  9. package/dist/host/src/drivers/acp/client-services.js.map +1 -0
  10. package/dist/host/src/drivers/acp/connection.d.ts +40 -7
  11. package/dist/host/src/drivers/acp/connection.js +132 -103
  12. package/dist/host/src/drivers/acp/connection.js.map +1 -1
  13. package/dist/host/src/drivers/acp/driver.d.ts +95 -2
  14. package/dist/host/src/drivers/acp/driver.js +628 -53
  15. package/dist/host/src/drivers/acp/driver.js.map +1 -1
  16. package/dist/host/src/drivers/acp/editor.d.ts +31 -0
  17. package/dist/host/src/drivers/acp/editor.js +242 -0
  18. package/dist/host/src/drivers/acp/editor.js.map +1 -0
  19. package/dist/host/src/drivers/acp/mapper.d.ts +2 -1
  20. package/dist/host/src/drivers/acp/mapper.js +88 -35
  21. package/dist/host/src/drivers/acp/mapper.js.map +1 -1
  22. package/dist/host/src/drivers/acp/mcp.d.ts +20 -0
  23. package/dist/host/src/drivers/acp/mcp.js +139 -0
  24. package/dist/host/src/drivers/acp/mcp.js.map +1 -0
  25. package/dist/host/src/drivers/acp/questions.js +14 -0
  26. package/dist/host/src/drivers/acp/questions.js.map +1 -1
  27. package/dist/host/src/drivers/acp/updates.d.ts +10 -0
  28. package/dist/host/src/drivers/acp/updates.js +137 -0
  29. package/dist/host/src/drivers/acp/updates.js.map +1 -0
  30. package/dist/host/src/drivers/claude/activity.d.ts +1 -0
  31. package/dist/host/src/drivers/claude/activity.js +17 -3
  32. package/dist/host/src/drivers/claude/activity.js.map +1 -1
  33. package/dist/host/src/drivers/claude/children.d.ts +7 -0
  34. package/dist/host/src/drivers/claude/children.js +31 -2
  35. package/dist/host/src/drivers/claude/children.js.map +1 -1
  36. package/dist/host/src/drivers/claude/driver.d.ts +4 -1
  37. package/dist/host/src/drivers/claude/driver.js +42 -1
  38. package/dist/host/src/drivers/claude/driver.js.map +1 -1
  39. package/dist/host/src/drivers/claude/tasks.d.ts +3 -3
  40. package/dist/host/src/drivers/claude/transcript.d.ts +497 -38
  41. package/dist/host/src/drivers/codex/archives.d.ts +2 -0
  42. package/dist/host/src/drivers/codex/archives.js +18 -0
  43. package/dist/host/src/drivers/codex/archives.js.map +1 -0
  44. package/dist/host/src/drivers/codex/attention.js +2 -2
  45. package/dist/host/src/drivers/codex/attention.js.map +1 -1
  46. package/dist/host/src/drivers/codex/driver.d.ts +4 -0
  47. package/dist/host/src/drivers/codex/driver.js +51 -2
  48. package/dist/host/src/drivers/codex/driver.js.map +1 -1
  49. package/dist/host/src/drivers/registry.d.ts +6 -0
  50. package/dist/host/src/drivers/registry.js +8 -0
  51. package/dist/host/src/drivers/registry.js.map +1 -1
  52. package/dist/host/src/drivers/types.d.ts +38 -1
  53. package/dist/host/src/fs.d.ts +3 -3
  54. package/dist/host/src/host.js +21 -3
  55. package/dist/host/src/host.js.map +1 -1
  56. package/dist/host/src/hub.d.ts +250 -2
  57. package/dist/host/src/hub.js +184 -13
  58. package/dist/host/src/hub.js.map +1 -1
  59. package/dist/host/src/input.d.ts +5 -3
  60. package/dist/host/src/input.js +8 -6
  61. package/dist/host/src/input.js.map +1 -1
  62. package/dist/host/src/questions.d.ts +3 -1
  63. package/dist/host/src/questions.js +33 -7
  64. package/dist/host/src/questions.js.map +1 -1
  65. package/dist/host/src/rpc/server.js +14 -0
  66. package/dist/host/src/rpc/server.js.map +1 -1
  67. package/dist/host/src/screen.js +5 -5
  68. package/dist/host/src/screen.js.map +1 -1
  69. package/dist/host/src/slim.js +15 -4
  70. package/dist/host/src/slim.js.map +1 -1
  71. package/dist/host/src/store.d.ts +2 -0
  72. package/dist/host/src/store.js +16 -2
  73. package/dist/host/src/store.js.map +1 -1
  74. package/dist/host/src/terminals.d.ts +8 -3
  75. package/dist/host/src/terminals.js +3 -2
  76. package/dist/host/src/terminals.js.map +1 -1
  77. package/dist/host/tsconfig.tsbuildinfo +1 -1
  78. package/dist/wire/src/acp.d.ts +510 -0
  79. package/dist/wire/src/acp.js +39 -0
  80. package/dist/wire/src/acp.js.map +1 -0
  81. package/dist/wire/src/async-questions.d.ts +2 -2
  82. package/dist/wire/src/computer-preview.d.ts +2 -2
  83. package/dist/wire/src/content.d.ts +465 -0
  84. package/dist/wire/src/content.js +58 -0
  85. package/dist/wire/src/content.js.map +1 -0
  86. package/dist/wire/src/editor.d.ts +702 -0
  87. package/dist/wire/src/editor.js +23 -0
  88. package/dist/wire/src/editor.js.map +1 -0
  89. package/dist/wire/src/index.d.ts +3 -0
  90. package/dist/wire/src/index.js +3 -0
  91. package/dist/wire/src/index.js.map +1 -1
  92. package/dist/wire/src/model.d.ts +1383 -79
  93. package/dist/wire/src/model.js +20 -1
  94. package/dist/wire/src/model.js.map +1 -1
  95. package/dist/wire/src/peer.d.ts +1 -0
  96. package/dist/wire/src/peer.js +22 -6
  97. package/dist/wire/src/peer.js.map +1 -1
  98. package/dist/wire/src/questions.d.ts +3 -0
  99. package/dist/wire/src/questions.js +65 -0
  100. package/dist/wire/src/questions.js.map +1 -0
  101. package/dist/wire/src/relay.d.ts +4 -4
  102. package/dist/wire/src/rpc.d.ts +19023 -2518
  103. package/dist/wire/src/rpc.js +41 -3
  104. package/dist/wire/src/rpc.js.map +1 -1
  105. package/dist/wire/src/updates.d.ts +3848 -1114
  106. package/dist/wire/src/updates.js +42 -40
  107. package/dist/wire/src/updates.js.map +1 -1
  108. package/dist/wire/src/workflow.d.ts +22 -22
  109. 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 { RpcError, } from "@linkshell/wire";
4
- import { AcpConnection } from "./connection.js";
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({ nativeId: info.sessionId, cwd: info.cwd, title: info.title ?? undefined, createdAt: updatedAt, updatedAt });
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 response = await this.rpc("session/fork", { sessionId: nativeId, cwd: options.cwd, mcpServers: [], ...this.sessionMeta() });
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
- cwd: options.cwd,
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
- if (capabilities?.loadSession) {
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
- cwd: context.cwd,
172
- mcpServers: [],
173
- ...this.sessionMeta(),
413
+ ...await this.sessionParameters(context.cwd, state.settings),
414
+ ...(this.connection.protocolVersion === 2 ? { replayFrom: { type: "start" } } : {}),
174
415
  });
175
- state.config = toConfigOptions(response);
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
- history = toHistory(state.replaying, state.tracker);
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
- cwd: context.cwd,
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} can't reopen earlier sessions`);
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 question = this.sessions.get(nativeId)?.questions.get(requestId);
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
- this.sessions.get(nativeId)?.questions.delete(requestId);
522
+ owner.state.questions.delete(requestId);
252
523
  question.resolve(question.request.respond(optionId));
253
- this.host?.update(this.id, nativeId, { sessionUpdate: "ls_permission_resolved", requestId, optionId });
524
+ this.publish(owner.id, { sessionUpdate: "ls_permission_resolved", requestId, optionId });
254
525
  return;
255
526
  }
256
- const resolve = this.sessions.get(nativeId)?.permissions.get(requestId);
257
- if (!resolve)
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
- this.sessions.get(nativeId)?.permissions.delete(requestId);
260
- resolve({ outcome: { outcome: "selected", optionId } });
261
- this.host?.update(this.id, nativeId, { sessionUpdate: "ls_permission_resolved", requestId, optionId });
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", { sessionId: nativeId, configId: optionId, value });
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
- if (this.connection?.alive)
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 ??= this.boot().finally(() => {
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
- this.scheduleRestart();
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.host?.update(this.id, nativeId, entry.update, entry.itemId);
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.host?.update(this.id, nativeId, entry.update, entry.itemId);
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 update = normalizeAcpUpdate(raw);
417
- if (!update)
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 (state?.replaying) {
421
- state.replaying.push(update);
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.host?.update(this.id, sessionId, mapped.update);
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.host?.update(this.id, sessionId, request.update);
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.host?.update(this.id, sessionId, { sessionUpdate: "ls_permission_resolved", requestId });
1019
+ this.publish(sessionId, { sessionUpdate: "ls_permission_resolved", requestId });
493
1020
  }
494
1021
  }
495
1022
  }
496
1023
  async answerQuestion(nativeId, requestId, answers) {
497
- const state = this.sessions.get(nativeId);
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
- this.host?.update(this.id, nativeId, { sessionUpdate: "ls_permission_resolved", requestId, optionId: "answered", answers });
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
- for (const resolve of state.permissions.values())
508
- resolve({ outcome: { outcome: "cancelled" } });
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
- stopReason = toStopReason(result.stopReason);
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