ycodium-acp-executor 0.2.2488

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 (98) hide show
  1. package/README.md +127 -0
  2. package/dist/adapter-support.d.ts +26 -0
  3. package/dist/adapter-support.d.ts.map +1 -0
  4. package/dist/adapter-support.js +79 -0
  5. package/dist/adapter-support.js.map +1 -0
  6. package/dist/approvals.d.ts +39 -0
  7. package/dist/approvals.d.ts.map +1 -0
  8. package/dist/approvals.js +69 -0
  9. package/dist/approvals.js.map +1 -0
  10. package/dist/config-option-wiring.d.ts +71 -0
  11. package/dist/config-option-wiring.d.ts.map +1 -0
  12. package/dist/config-option-wiring.js +71 -0
  13. package/dist/config-option-wiring.js.map +1 -0
  14. package/dist/config-options.d.ts +75 -0
  15. package/dist/config-options.d.ts.map +1 -0
  16. package/dist/config-options.js +171 -0
  17. package/dist/config-options.js.map +1 -0
  18. package/dist/connection.d.ts +34 -0
  19. package/dist/connection.d.ts.map +1 -0
  20. package/dist/connection.js +374 -0
  21. package/dist/connection.js.map +1 -0
  22. package/dist/errors.d.ts +21 -0
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +41 -0
  25. package/dist/errors.js.map +1 -0
  26. package/dist/event-stream.d.ts +15 -0
  27. package/dist/event-stream.d.ts.map +1 -0
  28. package/dist/event-stream.js +81 -0
  29. package/dist/event-stream.js.map +1 -0
  30. package/dist/events.d.ts +69 -0
  31. package/dist/events.d.ts.map +1 -0
  32. package/dist/events.js +205 -0
  33. package/dist/events.js.map +1 -0
  34. package/dist/executor.d.ts +84 -0
  35. package/dist/executor.d.ts.map +1 -0
  36. package/dist/executor.js +1179 -0
  37. package/dist/executor.js.map +1 -0
  38. package/dist/handshake.d.ts +24 -0
  39. package/dist/handshake.d.ts.map +1 -0
  40. package/dist/handshake.js +26 -0
  41. package/dist/handshake.js.map +1 -0
  42. package/dist/index.d.ts +23 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +15 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/jsonrpc.d.ts +12 -0
  47. package/dist/jsonrpc.d.ts.map +1 -0
  48. package/dist/jsonrpc.js +42 -0
  49. package/dist/jsonrpc.js.map +1 -0
  50. package/dist/line-buffer.d.ts +14 -0
  51. package/dist/line-buffer.d.ts.map +1 -0
  52. package/dist/line-buffer.js +51 -0
  53. package/dist/line-buffer.js.map +1 -0
  54. package/dist/mutex.d.ts +13 -0
  55. package/dist/mutex.d.ts.map +1 -0
  56. package/dist/mutex.js +44 -0
  57. package/dist/mutex.js.map +1 -0
  58. package/dist/probe.d.ts +74 -0
  59. package/dist/probe.d.ts.map +1 -0
  60. package/dist/probe.js +121 -0
  61. package/dist/probe.js.map +1 -0
  62. package/dist/protocol.d.ts +8 -0
  63. package/dist/protocol.d.ts.map +1 -0
  64. package/dist/protocol.js +6 -0
  65. package/dist/protocol.js.map +1 -0
  66. package/dist/runtime-mode.d.ts +49 -0
  67. package/dist/runtime-mode.d.ts.map +1 -0
  68. package/dist/runtime-mode.js +123 -0
  69. package/dist/runtime-mode.js.map +1 -0
  70. package/dist/runtime-model.d.ts +157 -0
  71. package/dist/runtime-model.d.ts.map +1 -0
  72. package/dist/runtime-model.js +601 -0
  73. package/dist/runtime-model.js.map +1 -0
  74. package/dist/session-update.d.ts +42 -0
  75. package/dist/session-update.d.ts.map +1 -0
  76. package/dist/session-update.js +63 -0
  77. package/dist/session-update.js.map +1 -0
  78. package/dist/tool-presentation.d.ts +17 -0
  79. package/dist/tool-presentation.d.ts.map +1 -0
  80. package/dist/tool-presentation.js +209 -0
  81. package/dist/tool-presentation.js.map +1 -0
  82. package/dist/turn-usage.d.ts +18 -0
  83. package/dist/turn-usage.d.ts.map +1 -0
  84. package/dist/turn-usage.js +59 -0
  85. package/dist/turn-usage.js.map +1 -0
  86. package/dist/types.d.ts +116 -0
  87. package/dist/types.d.ts.map +1 -0
  88. package/dist/types.js +6 -0
  89. package/dist/types.js.map +1 -0
  90. package/dist/util.d.ts +3 -0
  91. package/dist/util.d.ts.map +1 -0
  92. package/dist/util.js +11 -0
  93. package/dist/util.js.map +1 -0
  94. package/dist/vendor-hooks.d.ts +112 -0
  95. package/dist/vendor-hooks.d.ts.map +1 -0
  96. package/dist/vendor-hooks.js +4 -0
  97. package/dist/vendor-hooks.js.map +1 -0
  98. package/package.json +39 -0
@@ -0,0 +1,1179 @@
1
+ // @effect-diagnostics nodeBuiltinImport:off
2
+ // @effect-diagnostics globalDate:off
3
+ // @effect-diagnostics globalTimers:off
4
+ import * as NodeCrypto from "node:crypto";
5
+ import { CLIENT_METHODS, RequestError, createAcpConnection, } from "./protocol.js";
6
+ import { acpApprovalOptions, acpPromptSettlementBelongsToContext, isAcpRequestPermissionParams, selectAcpPermissionOptionId, selectAutoApprovedAcpPermissionOption, } from "./adapter-support.js";
7
+ import { approvalOutcomeForDecision, createApprovalGate, } from "./approvals.js";
8
+ import { UnboundedEventStream } from "./event-stream.js";
9
+ import { AcpExecutorCancelTimeoutError, AcpExecutorDisposedError, AcpExecutorError, AcpExecutorNotStartedError, AcpExecutorRequestError, } from "./errors.js";
10
+ import { fileChangesFromAcpPermission, makeAcpAssistantItemEvent, makeAcpContentDeltaEvent, makeAcpPlanUpdatedEvent, makeAcpRequestOpenedEvent, makeAcpRequestResolvedEvent, makeAcpToolCallEvent, } from "./events.js";
11
+ import { findAcpEffortConfig } from "./config-options.js";
12
+ import { acpModeConfigId } from "./runtime-mode.js";
13
+ import { acpInitializePayload } from "./handshake.js";
14
+ import { unhandledConfigCategories } from "./config-option-wiring.js";
15
+ import { Mutex } from "./mutex.js";
16
+ import { collectSessionConfigOptionValues, collectSessionModelIds, decideToolCallUpdateEmission, extractModelConfigId, filterMcpServersByAgentCapabilities, findSessionConfigOption, matchAdvertisedModelValue, mergeToolCallState, parseAvailableCommandsUpdate, parsePermissionRequest, parseSessionModeState, parseSessionUpdateEvent, } from "./runtime-model.js";
17
+ import { isRecord } from "./util.js";
18
+ import { defaultVendorHooks, } from "./vendor-hooks.js";
19
+ const DEFAULT_CANCEL_GRACE_MS = 5_000;
20
+ /** 原生选择题只能回传一个已提供的选项 ID,不能把任意文本当作答案。 */
21
+ const userInputAnswerOptionId = (question, answers) => {
22
+ const answer = answers[question.id];
23
+ const selected = typeof answer === "string"
24
+ ? answer
25
+ : Array.isArray(answer) && answer.length === 1 && typeof answer[0] === "string"
26
+ ? answer[0]
27
+ : undefined;
28
+ return selected !== undefined && question.options.some((option) => option.id === selected)
29
+ ? selected
30
+ : undefined;
31
+ };
32
+ function defaultNowIso() {
33
+ return new Date().toISOString();
34
+ }
35
+ function defaultRandomId() {
36
+ return NodeCrypto.randomUUID();
37
+ }
38
+ function normalizeModelId(value) {
39
+ return value?.trim() || undefined;
40
+ }
41
+ function modelSelectionStateFromSetup(setup) {
42
+ const sessionModels = isRecord(setup.models) ? setup.models : undefined;
43
+ const modelConfigId = extractModelConfigId(setup);
44
+ // `models` 优先;只有配置项时走它。两样都没报仍选 set_model,发送守卫会保持静默。
45
+ const strategy = sessionModels === undefined && modelConfigId !== undefined
46
+ ? "config-option"
47
+ : "session/set_model";
48
+ const modelConfigOption = modelConfigId === undefined
49
+ ? undefined
50
+ : findSessionConfigOption(setup.configOptions, modelConfigId);
51
+ const configModel = modelConfigOption?.currentValue;
52
+ const currentModelId = strategy === "config-option"
53
+ ? typeof configModel === "string"
54
+ ? normalizeModelId(configModel)
55
+ : undefined
56
+ : typeof sessionModels?.["currentModelId"] === "string"
57
+ ? normalizeModelId(sessionModels["currentModelId"])
58
+ : undefined;
59
+ return {
60
+ strategy,
61
+ sessionModelsAdvertised: sessionModels !== undefined,
62
+ modelConfigId,
63
+ currentModelId,
64
+ advertisedModelValues: strategy === "config-option"
65
+ ? modelConfigOption === undefined
66
+ ? []
67
+ : collectSessionConfigOptionValues(modelConfigOption)
68
+ : collectSessionModelIds(sessionModels),
69
+ };
70
+ }
71
+ export function createAcpExecutor(options) {
72
+ const hooks = { ...defaultVendorHooks, ...options.hooks };
73
+ const cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
74
+ const nowIso = options.nowIso ?? defaultNowIso;
75
+ const randomId = options.randomId ?? defaultRandomId;
76
+ const eventStream = new UnboundedEventStream();
77
+ const threadMutex = new Mutex();
78
+ const promptMutex = new Mutex();
79
+ let generation = 0;
80
+ let disposed = false;
81
+ let status = "idle";
82
+ let sessionId;
83
+ let modelSelectionState = {
84
+ strategy: "session/set_model",
85
+ sessionModelsAdvertised: false,
86
+ modelConfigId: undefined,
87
+ currentModelId: undefined,
88
+ advertisedModelValues: [],
89
+ };
90
+ /** 上一个对不上目录的请求型号:同一个 id 每轮都来,日志只记一次。 */
91
+ let lastUnmatchedModelId;
92
+ let initializeResult;
93
+ let startResult;
94
+ let startInFlight;
95
+ let activeTurnId;
96
+ let promptsInFlight = 0;
97
+ const interruptedTurnIds = new Set();
98
+ const approvals = createApprovalGate({
99
+ singleResolver: false,
100
+ });
101
+ // 结构化用户输入(厂商钩子把 permission 认成提问/计划时)的挂起表,
102
+ // 与 approvals 同理:答复 / 取消 / 断连三者竞争只完成一次。
103
+ const pendingUserInputs = new Map();
104
+ /** 这一轮挂着的「提前收口」等待。同一时刻至多一条:prompt 之间有 promptMutex。 */
105
+ let pendingSettlement;
106
+ /**
107
+ * 已经结算过的 promptKey。通知与 RPC 应答都可能带同一个 key 再来一次,
108
+ * 不记住就会把下一轮的等待错误地收掉。有界:只留最近这些。
109
+ */
110
+ const settledPromptKeys = [];
111
+ const SETTLED_PROMPT_KEY_LIMIT = 32;
112
+ const rememberSettledPromptKey = (key) => {
113
+ if (settledPromptKeys.includes(key))
114
+ return;
115
+ settledPromptKeys.push(key);
116
+ if (settledPromptKeys.length > SETTLED_PROMPT_KEY_LIMIT)
117
+ settledPromptKeys.shift();
118
+ };
119
+ const toolCalls = new Map();
120
+ let assistantSegment = { nextSegmentIndex: 0 };
121
+ let modeState;
122
+ let availableCommands = [];
123
+ const assistantItemRuntimeId = randomId();
124
+ let promptWaiters = [];
125
+ let abortLocalPrompt;
126
+ let connectionClosed = false;
127
+ const stamp = () => ({
128
+ eventId: randomId(),
129
+ createdAt: nowIso(),
130
+ });
131
+ // 迟到的结果先验身份:dispose 之后、或已经换代的那次 start/prompt 完成时,
132
+ // 不许回写状态。`Promise.race` 输掉的那一方还在跑,这是唯一的拦截点。
133
+ const isLive = (expectedGeneration) => !disposed && generation === expectedGeneration;
134
+ const publish = (event) => {
135
+ if (disposed) {
136
+ return;
137
+ }
138
+ eventStream.publish(options.originBindingGeneration === undefined
139
+ ? event
140
+ : { ...event, originBindingGeneration: options.originBindingGeneration });
141
+ };
142
+ const emit = (event) => {
143
+ publish(event);
144
+ };
145
+ const failIfDisposed = () => {
146
+ if (disposed) {
147
+ throw new AcpExecutorDisposedError();
148
+ }
149
+ };
150
+ const closeActiveAssistantSegment = (turnId) => {
151
+ if (!assistantSegment.activeItemId) {
152
+ return;
153
+ }
154
+ const itemId = assistantSegment.activeItemId;
155
+ assistantSegment = { nextSegmentIndex: assistantSegment.nextSegmentIndex };
156
+ emit(makeAcpAssistantItemEvent({
157
+ stamp: stamp(),
158
+ provider: options.provider,
159
+ threadId: options.threadId,
160
+ turnId,
161
+ itemId,
162
+ lifecycle: "item.completed",
163
+ }));
164
+ };
165
+ const ensureActiveAssistantSegment = (acpSessionId, turnId) => {
166
+ if (assistantSegment.activeItemId) {
167
+ return assistantSegment.activeItemId;
168
+ }
169
+ const itemId = `assistant:${acpSessionId}:runtime:${assistantItemRuntimeId}:segment:${assistantSegment.nextSegmentIndex}`;
170
+ assistantSegment = {
171
+ nextSegmentIndex: assistantSegment.nextSegmentIndex + 1,
172
+ activeItemId: itemId,
173
+ };
174
+ emit(makeAcpAssistantItemEvent({
175
+ stamp: stamp(),
176
+ provider: options.provider,
177
+ threadId: options.threadId,
178
+ turnId,
179
+ itemId,
180
+ lifecycle: "item.started",
181
+ }));
182
+ return itemId;
183
+ };
184
+ const handleSessionUpdate = (params) => {
185
+ const parsed = parseSessionUpdateEvent(params);
186
+ if (parsed.modeId && modeState) {
187
+ modeState = {
188
+ ...modeState,
189
+ currentModeId: parsed.modeId,
190
+ };
191
+ }
192
+ const commands = parseAvailableCommandsUpdate(params);
193
+ if (commands !== undefined) {
194
+ availableCommands = commands;
195
+ }
196
+ for (const event of parsed.events) {
197
+ if (event._tag === "ToolCallUpdated") {
198
+ closeActiveAssistantSegment(activeTurnId);
199
+ const tracked = toolCalls.get(event.toolCall.toolCallId);
200
+ const previous = tracked?.state;
201
+ const nextToolCall = mergeToolCallState(previous, event.toolCall);
202
+ const decision = decideToolCallUpdateEmission({
203
+ previous,
204
+ next: nextToolCall,
205
+ lastEmittedDetailLength: tracked?.lastEmittedDetailLength,
206
+ skippedSinceEmit: tracked?.skippedSinceEmit ?? 0,
207
+ });
208
+ if (nextToolCall.status === "completed" || nextToolCall.status === "failed") {
209
+ toolCalls.delete(nextToolCall.toolCallId);
210
+ }
211
+ else {
212
+ toolCalls.set(nextToolCall.toolCallId, {
213
+ state: nextToolCall,
214
+ lastEmittedDetailLength: decision.emit
215
+ ? nextToolCall.detail?.length
216
+ : tracked?.lastEmittedDetailLength,
217
+ skippedSinceEmit: decision.skippedSinceEmit,
218
+ });
219
+ }
220
+ if (!decision.emit) {
221
+ continue;
222
+ }
223
+ emit(makeAcpToolCallEvent({
224
+ stamp: stamp(),
225
+ provider: options.provider,
226
+ threadId: options.threadId,
227
+ turnId: activeTurnId,
228
+ toolCall: nextToolCall,
229
+ rawPayload: event.rawPayload,
230
+ }));
231
+ continue;
232
+ }
233
+ if (event._tag === "ContentDelta") {
234
+ if (event.streamKind === "reasoning_text") {
235
+ // 思考不得开 assistant 段:item.started 会把思考段收口,字会进正式回答。
236
+ if (event.text.trim().length === 0) {
237
+ continue;
238
+ }
239
+ emit(makeAcpContentDeltaEvent({
240
+ stamp: stamp(),
241
+ provider: options.provider,
242
+ threadId: options.threadId,
243
+ turnId: activeTurnId,
244
+ text: event.text,
245
+ streamKind: "reasoning_text",
246
+ rawPayload: event.rawPayload,
247
+ }));
248
+ continue;
249
+ }
250
+ if (event.text.trim().length === 0 && !assistantSegment.activeItemId) {
251
+ continue;
252
+ }
253
+ const itemId = sessionId
254
+ ? ensureActiveAssistantSegment(sessionId, activeTurnId)
255
+ : undefined;
256
+ emit(makeAcpContentDeltaEvent({
257
+ stamp: stamp(),
258
+ provider: options.provider,
259
+ threadId: options.threadId,
260
+ turnId: activeTurnId,
261
+ ...(itemId ? { itemId } : {}),
262
+ text: event.text,
263
+ streamKind: "assistant_text",
264
+ rawPayload: event.rawPayload,
265
+ }));
266
+ continue;
267
+ }
268
+ if (event._tag === "PlanUpdated") {
269
+ emit(makeAcpPlanUpdatedEvent({
270
+ stamp: stamp(),
271
+ provider: options.provider,
272
+ threadId: options.threadId,
273
+ turnId: activeTurnId,
274
+ payload: event.payload,
275
+ source: "acp.jsonrpc",
276
+ method: "session/update",
277
+ rawPayload: event.rawPayload,
278
+ }));
279
+ }
280
+ }
281
+ };
282
+ const emitResolved = (pending, decision, reason) => {
283
+ emit(makeAcpRequestResolvedEvent({
284
+ stamp: stamp(),
285
+ provider: options.provider,
286
+ threadId: options.threadId,
287
+ turnId: pending.turnId,
288
+ requestId: pending.requestId,
289
+ permissionRequest: pending.permissionRequest,
290
+ decision,
291
+ ...(reason !== undefined ? { reason } : {}),
292
+ }));
293
+ };
294
+ const settleApproval = (pending, outcome, decision, reason) => {
295
+ if (!approvals.settle(pending, outcome)) {
296
+ return false;
297
+ }
298
+ emitResolved(pending, decision, reason);
299
+ return true;
300
+ };
301
+ const settleAllApprovals = (decision) => {
302
+ for (const entry of approvals.settleAll({ outcome: "cancelled" })) {
303
+ emitResolved(entry, decision);
304
+ }
305
+ };
306
+ const emitUserInputResolved = (pending, answers) => {
307
+ emit({
308
+ type: "user-input.resolved",
309
+ ...stamp(),
310
+ provider: options.provider,
311
+ threadId: options.threadId,
312
+ ...(pending.turnId !== undefined ? { turnId: pending.turnId } : {}),
313
+ requestId: pending.requestId,
314
+ payload: { answers },
315
+ });
316
+ };
317
+ /** 挂着的提前收口等待。四个清理点(流断 / 连接关 / 取消 / dispose)都要过它。 */
318
+ const abortPendingSettlement = (reason) => {
319
+ const waiting = pendingSettlement;
320
+ if (waiting === undefined)
321
+ return;
322
+ pendingSettlement = undefined;
323
+ waiting.settle({ error: new AcpExecutorRequestError("session/prompt", reason) });
324
+ };
325
+ const settleAllUserInputs = () => {
326
+ for (const pending of pendingUserInputs.values()) {
327
+ if (pending.settled)
328
+ continue;
329
+ pending.settled = true;
330
+ pending.resolve({ kind: "cancelled" });
331
+ }
332
+ pendingUserInputs.clear();
333
+ };
334
+ /**
335
+ * 一条通知能不能收掉这一轮。返回 true 表示它是收口信号,已消费。
336
+ *
337
+ * 四条语义都在这里,不外放给钩子——外放的话每家会各写一遍,而其中第三条有事故记录:
338
+ * 1. 带 key 按 key 配,不带 key 按 sessionId 配当前这一轮(Grok 的 promptId 是可选的)
339
+ * 2. 已结算过的 key 丢弃,防迟到的重复通知收掉下一轮
340
+ * 3. `error` 走失败通道,**绝不能归成正常结束**——Grok 额度耗尽正是发它,
341
+ * 归成成功的话用户看到一条空白回复(`cba38ce0d`)
342
+ * 4. 赢了之后输掉的那条 RPC 不会有人再等,交给 dispose/close 时的 rejectPending 收
343
+ */
344
+ const settleFromNotification = (method, params) => {
345
+ const hook = hooks.promptSettlement;
346
+ if (hook === undefined || pendingSettlement === undefined)
347
+ return false;
348
+ const signal = hook.fromNotification(method, params);
349
+ if (signal === undefined)
350
+ return false;
351
+ const waiting = pendingSettlement;
352
+ if (signal.sessionId !== waiting.sessionId)
353
+ return false;
354
+ if (signal.promptKey !== undefined && signal.promptKey !== waiting.promptKey) {
355
+ // 不是这一轮的:可能是上一轮迟到的重复通知,消费掉但不结算。
356
+ return settledPromptKeys.includes(signal.promptKey);
357
+ }
358
+ pendingSettlement = undefined;
359
+ rememberSettledPromptKey(waiting.promptKey);
360
+ if (signal.outcome === "error") {
361
+ waiting.settle({
362
+ error: new AcpExecutorRequestError("session/prompt", signal.detail ?? "prompt failed without an error detail"),
363
+ });
364
+ }
365
+ else {
366
+ waiting.settle({
367
+ response: { stopReason: signal.stopReason ?? "end_turn" },
368
+ });
369
+ }
370
+ return true;
371
+ };
372
+ /**
373
+ * 把一组问题交给用户,等回答。权限那条(Kimi 借 permission 传提问)与厂商扩展那条
374
+ * (Grok 的 `_x.ai/ask_user_question`)共用这一份等待表——两边各自登记会让
375
+ * `settleAllUserInputs` 漏掉一半,CLI 崩溃时那一半永远不 settle。
376
+ */
377
+ const askUserInput = async (input) => {
378
+ // dispose 之后迟到的请求不得再登记:清理已经跑完,塞进去就再也 settle 不了。
379
+ failIfDisposed();
380
+ const requestId = randomId();
381
+ const { promise: settlement, resolve: settle } = Promise.withResolvers();
382
+ const pending = {
383
+ requestId,
384
+ questions: input.questions,
385
+ turnId: activeTurnId,
386
+ settled: false,
387
+ resolve: settle,
388
+ };
389
+ pendingUserInputs.set(requestId, pending);
390
+ emit({
391
+ type: "user-input.requested",
392
+ ...stamp(),
393
+ provider: options.provider,
394
+ threadId: options.threadId,
395
+ ...(activeTurnId !== undefined ? { turnId: activeTurnId } : {}),
396
+ requestId,
397
+ payload: { questions: input.questions },
398
+ raw: {
399
+ source: "acp.jsonrpc",
400
+ method: input.method,
401
+ payload: input.payload,
402
+ },
403
+ });
404
+ const resolution = await settlement;
405
+ pendingUserInputs.delete(requestId);
406
+ emitUserInputResolved(pending, resolution.kind === "answered" ? resolution.answers : {});
407
+ return resolution;
408
+ };
409
+ /** 每次扩展请求现造一份,把真实方法名与原始载荷带进 `raw`,排障时看得出是谁问的。 */
410
+ const makeExtensionContext = (method, params) => ({
411
+ askUserInput: (questions) => askUserInput({ questions, method, payload: params }),
412
+ });
413
+ /** 厂商钩子认出的提问/计划走 `user-input.*`,返回 undefined 表示没接管。 */
414
+ const handlePermissionAsUserInput = async (params) => {
415
+ if (hooks.permissionUserInput === undefined)
416
+ return undefined;
417
+ let question;
418
+ try {
419
+ question = hooks.permissionUserInput(params);
420
+ }
421
+ catch (error) {
422
+ // 认出来却形状不全的交互绝不能降格成批准/拒绝两档——回协议错误。
423
+ throw RequestError.invalidParams({
424
+ detail: error instanceof Error ? error.message : String(error),
425
+ });
426
+ }
427
+ if (question === undefined)
428
+ return undefined;
429
+ const resolution = await askUserInput({
430
+ questions: [question],
431
+ method: CLIENT_METHODS.session_request_permission,
432
+ payload: params,
433
+ });
434
+ const optionId = resolution.kind === "answered"
435
+ ? userInputAnswerOptionId(question, resolution.answers)
436
+ : undefined;
437
+ return {
438
+ outcome: optionId !== undefined ? { outcome: "selected", optionId } : { outcome: "cancelled" },
439
+ };
440
+ };
441
+ const handlePermissionRequest = async (params) => {
442
+ if (!isAcpRequestPermissionParams(params)) {
443
+ throw RequestError.invalidParams({ detail: "Malformed session/request_permission" });
444
+ }
445
+ const userInputOutcome = await handlePermissionAsUserInput(params);
446
+ if (userInputOutcome !== undefined)
447
+ return userInputOutcome;
448
+ const permissionRequest = parsePermissionRequest(params);
449
+ if (options.autoApprove?.(permissionRequest) === true) {
450
+ // 选项表认不出唯一的「允许」项就照常弹给用户,不替它猜。
451
+ const optionId = selectAutoApprovedAcpPermissionOption(params);
452
+ if (optionId !== undefined) {
453
+ return { outcome: { outcome: "selected", optionId } };
454
+ }
455
+ }
456
+ const requestId = randomId();
457
+ const outcome = await new Promise((resolve) => {
458
+ const pending = {
459
+ requestId,
460
+ request: params,
461
+ permissionRequest,
462
+ turnId: activeTurnId,
463
+ settled: false,
464
+ resolve,
465
+ };
466
+ approvals.add(pending);
467
+ emit(makeAcpRequestOpenedEvent({
468
+ stamp: stamp(),
469
+ provider: options.provider,
470
+ threadId: options.threadId,
471
+ turnId: activeTurnId,
472
+ requestId,
473
+ permissionRequest,
474
+ detail: permissionRequest.detail ?? "Approve this tool call?",
475
+ args: permissionRequest.toolCall?.data["rawInput"] ?? params.toolCall.rawInput,
476
+ options: acpApprovalOptions(params),
477
+ fileChanges: fileChangesFromAcpPermission(permissionRequest),
478
+ source: "acp.jsonrpc",
479
+ method: "session/request_permission",
480
+ rawPayload: params,
481
+ }));
482
+ });
483
+ return { outcome };
484
+ };
485
+ const connection = createAcpConnection({
486
+ readable: options.readable,
487
+ writable: options.writable,
488
+ onInboundFailure: (error) => {
489
+ options.log?.warn?.("acp connection closed: an inbound request could not be answered", {
490
+ threadId: options.threadId,
491
+ error: error instanceof Error ? error.message : String(error),
492
+ });
493
+ },
494
+ onNotification: (notification) => {
495
+ if (notification.method === "session/update") {
496
+ handleSessionUpdate(notification.params);
497
+ return;
498
+ }
499
+ if (settleFromNotification(notification.method, notification.params))
500
+ return;
501
+ void hooks.onExtensionNotification?.(notification.method, notification.params);
502
+ },
503
+ onRequest: async (method, params) => {
504
+ if (method === CLIENT_METHODS.session_request_permission) {
505
+ return handlePermissionRequest(params);
506
+ }
507
+ if (hooks.onExtensionRequest) {
508
+ // undefined = 这家不认这个方法。当成合法结果回去的话,只认自己那两个方法的
509
+ // 钩子会让其它未知方法静默收到 null——agent 以为调成功了。
510
+ const handled = await hooks.onExtensionRequest(method, params, makeExtensionContext(method, params));
511
+ if (handled !== undefined)
512
+ return handled;
513
+ }
514
+ throw RequestError.methodNotFound(method);
515
+ },
516
+ });
517
+ void connection.closed.then(() => {
518
+ connectionClosed = true;
519
+ if (disposed) {
520
+ return;
521
+ }
522
+ settleAllApprovals("cancel");
523
+ settleAllUserInputs();
524
+ abortPendingSettlement("connection closed");
525
+ promptWaiters.splice(0).forEach((wake) => wake());
526
+ }, () => {
527
+ connectionClosed = true;
528
+ });
529
+ // 入流一死就当场结算挂着的审批/用户输入/轮次等待,不能等 connection.closed:
530
+ // 协议的 shutdown 排在入站队列后面,而队列正卡在等这些 handler settle——
531
+ // 等 closed 才动手就是死锁。这里先结算,队列疏通后剩余缓冲帧照常投递。
532
+ const onInputDead = () => {
533
+ if (disposed) {
534
+ return;
535
+ }
536
+ settleAllApprovals("cancel");
537
+ settleAllUserInputs();
538
+ // 提前收口的等待不在这里中止:清掉审批 / 用户输入之后入站队列就通了,协议层的
539
+ // shutdown 随即 reject 那条 prompt RPC,竞速的另一边自己会赢(实测两版都 reject,
540
+ // 只是理由从 "input stream ended" 变成流上的那个 error)。
541
+ promptWaiters.splice(0).forEach((wake) => wake());
542
+ };
543
+ options.readable.once("end", onInputDead);
544
+ options.readable.once("error", onInputDead);
545
+ const applyRequestedModel = async (input) => {
546
+ const requestedModelId = normalizeModelId(input.requestedModelId);
547
+ if (requestedModelId === undefined ||
548
+ requestedModelId === input.state.currentModelId ||
549
+ (input.state.strategy === "session/set_model" && !input.state.sessionModelsAdvertised) ||
550
+ (input.state.strategy === "config-option" && input.state.modelConfigId === undefined)) {
551
+ return input.state;
552
+ }
553
+ const value = matchAdvertisedModelValue(requestedModelId, input.state.advertisedModelValues);
554
+ if (value === undefined) {
555
+ // 对不上就保留手自己的型号,只记一次日志(带上手的目录);没有别处替它改写上游请求。
556
+ if (requestedModelId !== lastUnmatchedModelId) {
557
+ lastUnmatchedModelId = requestedModelId;
558
+ options.log?.info("requested model is not in the agent catalog; keeping its own model", {
559
+ requestedModelId,
560
+ currentModelId: input.state.currentModelId,
561
+ advertisedModelValues: input.state.advertisedModelValues,
562
+ });
563
+ }
564
+ return input.state;
565
+ }
566
+ if (value === input.state.currentModelId) {
567
+ return input.state;
568
+ }
569
+ if (input.state.strategy === "config-option") {
570
+ await connection.request("session/set_config_option", {
571
+ sessionId: input.acpSessionId,
572
+ configId: input.state.modelConfigId,
573
+ value,
574
+ });
575
+ }
576
+ else {
577
+ await connection.request("session/set_model", {
578
+ sessionId: input.acpSessionId,
579
+ modelId: value,
580
+ });
581
+ }
582
+ if (!isLive(input.expectedGeneration)) {
583
+ throw new AcpExecutorDisposedError();
584
+ }
585
+ return { ...input.state, currentModelId: value };
586
+ };
587
+ /**
588
+ * 把用户选的思考档位发给这只手。
589
+ *
590
+ * 三种情况不发:没选、这只手不报力度档、选的值不在它报的那几档里。最后一种尤其重要——
591
+ * 各家的档位值不是同一套(dsh 有 `off`,claude-agent-acp 有 `default`),发一个它不认的
592
+ * 值要么被拒、要么被当成未知档,不如保留它自己的默认。
593
+ *
594
+ * 发失败只记一笔,不让会话起不来:档位是锦上添花,为它把整条会话废掉不值。
595
+ */
596
+ const applyRequestedEffort = async (input) => {
597
+ const requested = input.requestedEffort?.trim();
598
+ if (!requested)
599
+ return;
600
+ const configOptions = typeof input.setup === "object" && input.setup !== null
601
+ ? input.setup.configOptions
602
+ : undefined;
603
+ const effort = findAcpEffortConfig(configOptions);
604
+ if (effort === undefined)
605
+ return;
606
+ const matched = effort.choices.find((choice) => choice.value === requested);
607
+ if (matched === undefined) {
608
+ options.log?.info("requested effort is not in the agent catalog; keeping its own", {
609
+ requestedEffort: requested,
610
+ advertised: effort.choices.map((choice) => choice.value),
611
+ });
612
+ return;
613
+ }
614
+ if (matched.isDefault === true)
615
+ return;
616
+ if (!isLive(input.expectedGeneration))
617
+ return;
618
+ try {
619
+ await connection.request("session/set_config_option", {
620
+ sessionId: input.acpSessionId,
621
+ configId: effort.configId,
622
+ value: requested,
623
+ });
624
+ }
625
+ catch (error) {
626
+ options.log?.info("setting the effort level failed; the session keeps its own", {
627
+ requestedEffort: requested,
628
+ configId: effort.configId,
629
+ error: error instanceof Error ? error.message : String(error),
630
+ });
631
+ }
632
+ };
633
+ const startOnce = async (expectedGeneration) => {
634
+ // 载荷与探测路径共用一份(`handshake.ts`):agent 按客户端能力决定播报什么,
635
+ // 两边各写一份就会问出两个不同的问题。
636
+ const initializePayload = acpInitializePayload(options.clientInfo, options.clientCapabilities);
637
+ const initialized = await connection.request("initialize", initializePayload);
638
+ if (!isLive(expectedGeneration)) {
639
+ throw new AcpExecutorDisposedError();
640
+ }
641
+ initializeResult = initialized;
642
+ const authMethodId = typeof options.authMethodId === "function"
643
+ ? options.authMethodId(initialized)
644
+ : options.authMethodId;
645
+ if (authMethodId !== undefined) {
646
+ await connection.request("authenticate", { methodId: authMethodId });
647
+ if (!isLive(expectedGeneration)) {
648
+ throw new AcpExecutorDisposedError();
649
+ }
650
+ }
651
+ const mcpServers = filterMcpServersByAgentCapabilities(options.mcpServers ?? [], initialized.agentCapabilities);
652
+ let setup;
653
+ let nextSessionId;
654
+ if (options.resumeSessionId && options.resumeMethod === "session/resume") {
655
+ setup = await connection.request("session/resume", {
656
+ sessionId: options.resumeSessionId,
657
+ cwd: options.cwd,
658
+ mcpServers,
659
+ });
660
+ nextSessionId = options.resumeSessionId;
661
+ }
662
+ else if (options.resumeSessionId) {
663
+ setup = await connection.request("session/load", {
664
+ sessionId: options.resumeSessionId,
665
+ cwd: options.cwd,
666
+ mcpServers,
667
+ });
668
+ nextSessionId = options.resumeSessionId;
669
+ }
670
+ else {
671
+ const created = await connection.request("session/new", {
672
+ cwd: options.cwd,
673
+ mcpServers,
674
+ });
675
+ nextSessionId = created.sessionId;
676
+ setup = created;
677
+ }
678
+ // 报了一类从没登记过的选项,说一声。在此之前只挑 `model` 与力度档,其余整个丢掉、
679
+ // 零痕迹——界面上少一格,日志里一个字都没有(2026-09-19 力度档就是这么漏的)。
680
+ const unhandledCategories = unhandledConfigCategories(setup.configOptions);
681
+ if (unhandledCategories.length > 0) {
682
+ options.log?.info("acp session reported config option categories we do not handle", {
683
+ sessionId: nextSessionId,
684
+ categories: unhandledCategories,
685
+ });
686
+ }
687
+ const nextModelSelectionState = await applyRequestedModel({
688
+ state: modelSelectionStateFromSetup(setup),
689
+ acpSessionId: nextSessionId,
690
+ requestedModelId: options.requestedModelId,
691
+ expectedGeneration,
692
+ });
693
+ await applyRequestedEffort({
694
+ setup,
695
+ acpSessionId: nextSessionId,
696
+ requestedEffort: options.requestedEffort,
697
+ expectedGeneration,
698
+ });
699
+ if (options.beforeApplyResult) {
700
+ await options.beforeApplyResult();
701
+ }
702
+ if (!isLive(expectedGeneration)) {
703
+ throw new AcpExecutorDisposedError();
704
+ }
705
+ sessionId = nextSessionId;
706
+ modelSelectionState = nextModelSelectionState;
707
+ modeState = parseSessionModeState(setup);
708
+ const result = {
709
+ sessionId: nextSessionId,
710
+ generation: expectedGeneration,
711
+ initializeResult: initialized,
712
+ sessionSetupResult: setup,
713
+ modelConfigId: extractModelConfigId(setup),
714
+ modelId: nextModelSelectionState.currentModelId,
715
+ };
716
+ startResult = result;
717
+ status = "ready";
718
+ emit({
719
+ type: "session.started",
720
+ ...stamp(),
721
+ provider: options.provider,
722
+ ...(options.providerInstanceId ? { providerInstanceId: options.providerInstanceId } : {}),
723
+ threadId: options.threadId,
724
+ payload: {
725
+ sessionId: nextSessionId,
726
+ cwd: options.cwd,
727
+ },
728
+ });
729
+ return result;
730
+ };
731
+ const start = async () => {
732
+ failIfDisposed();
733
+ if (startResult) {
734
+ return startResult;
735
+ }
736
+ if (startInFlight) {
737
+ return startInFlight;
738
+ }
739
+ generation += 1;
740
+ const expectedGeneration = generation;
741
+ status = "starting";
742
+ startInFlight = startOnce(expectedGeneration).then((result) => {
743
+ startInFlight = undefined;
744
+ return result;
745
+ }, (error) => {
746
+ startInFlight = undefined;
747
+ if (isLive(expectedGeneration) && !startResult) {
748
+ status = "idle";
749
+ generation = expectedGeneration;
750
+ }
751
+ throw error;
752
+ });
753
+ return startInFlight;
754
+ };
755
+ const requireStarted = () => {
756
+ failIfDisposed();
757
+ if (!sessionId || !startResult) {
758
+ throw new AcpExecutorNotStartedError();
759
+ }
760
+ return { sessionId, generation: startResult.generation };
761
+ };
762
+ const notifyPromptSettled = () => {
763
+ const waiters = promptWaiters.splice(0);
764
+ for (const waiter of waiters) {
765
+ waiter();
766
+ }
767
+ };
768
+ const waitForPromptIdle = async (timeoutMs) => {
769
+ if (!promptMutex.isLocked) {
770
+ return true;
771
+ }
772
+ return await new Promise((resolve) => {
773
+ const timer = setTimeout(() => {
774
+ promptWaiters = promptWaiters.filter((waiter) => waiter !== onSettle);
775
+ resolve(false);
776
+ }, timeoutMs);
777
+ const onSettle = () => {
778
+ clearTimeout(timer);
779
+ resolve(true);
780
+ };
781
+ promptWaiters.push(onSettle);
782
+ });
783
+ };
784
+ const settleTurn = (input) => {
785
+ if (!isLive(input.expectedGeneration)) {
786
+ return;
787
+ }
788
+ if (!acpPromptSettlementBelongsToContext({
789
+ liveAcpSessionId: sessionId ?? "",
790
+ expectedAcpSessionId: input.expectedSessionId,
791
+ liveActiveTurnId: activeTurnId,
792
+ liveSessionActiveTurnId: activeTurnId,
793
+ turnId: input.turnId,
794
+ })) {
795
+ return;
796
+ }
797
+ closeActiveAssistantSegment(input.turnId);
798
+ promptsInFlight = Math.max(0, promptsInFlight - 1);
799
+ if (promptsInFlight === 0 && activeTurnId === input.turnId) {
800
+ activeTurnId = undefined;
801
+ if (status !== "disposed") {
802
+ status = "ready";
803
+ }
804
+ emit({
805
+ type: "turn.completed",
806
+ ...stamp(),
807
+ provider: options.provider,
808
+ threadId: options.threadId,
809
+ turnId: input.turnId,
810
+ payload: {
811
+ state: input.state,
812
+ stopReason: input.stopReason,
813
+ },
814
+ });
815
+ interruptedTurnIds.delete(input.turnId);
816
+ }
817
+ };
818
+ const prompt = async (input) => {
819
+ failIfDisposed();
820
+ const started = requireStarted();
821
+ let turnId;
822
+ // thread 锁只罩短临界区(开轮、结算),不罩整个 prompt RPC。把整轮锁进去的话,
823
+ // 取消与审批答复会排在这把锁后面——「停止」和「允许」就永远到不了。
824
+ await threadMutex.acquire();
825
+ try {
826
+ failIfDisposed();
827
+ turnId = input.turnId ?? (promptsInFlight > 0 ? activeTurnId : undefined) ?? randomId();
828
+ promptsInFlight += 1;
829
+ activeTurnId = turnId;
830
+ status = "running";
831
+ if (promptsInFlight === 1) {
832
+ emit({
833
+ type: "turn.started",
834
+ ...stamp(),
835
+ provider: options.provider,
836
+ threadId: options.threadId,
837
+ turnId,
838
+ payload: {},
839
+ });
840
+ }
841
+ }
842
+ finally {
843
+ threadMutex.release();
844
+ }
845
+ const promptParts = input.prompt ??
846
+ (input.text !== undefined
847
+ ? [{ type: "text", text: input.text }]
848
+ : [{ type: "text", text: "" }]);
849
+ await promptMutex.acquire();
850
+ let response;
851
+ try {
852
+ failIfDisposed();
853
+ if (interruptedTurnIds.has(turnId)) {
854
+ settleTurn({
855
+ expectedGeneration: started.generation,
856
+ expectedSessionId: started.sessionId,
857
+ turnId,
858
+ stopReason: "cancelled",
859
+ state: "cancelled",
860
+ });
861
+ return {
862
+ threadId: options.threadId,
863
+ turnId,
864
+ stopReason: "cancelled",
865
+ };
866
+ }
867
+ modelSelectionState = await applyRequestedModel({
868
+ state: modelSelectionState,
869
+ acpSessionId: started.sessionId,
870
+ requestedModelId: input.requestedModelId,
871
+ expectedGeneration: started.generation,
872
+ });
873
+ closeActiveAssistantSegment(turnId);
874
+ const settlementHooks = hooks.promptSettlement;
875
+ // 有提前收口钩子时给这一轮一个关联 id,通知才认得回来。
876
+ const promptKey = settlementHooks === undefined ? undefined : randomId();
877
+ const promptRpc = connection.request("session/prompt", {
878
+ sessionId: started.sessionId,
879
+ prompt: promptParts,
880
+ ...(settlementHooks !== undefined && promptKey !== undefined
881
+ ? settlementHooks.requestFields(promptKey)
882
+ : {}),
883
+ });
884
+ // 老实等对端应答。取消走 `session/cancel` 通知让 agent 自己收尾,不在本地
885
+ // 假装 resolve —— 那会让「调用方不再等」被误当成「远端已经停了」。
886
+ //
887
+ // 唯一的例外是提前收口钩子:Grok 1.0.24 起发完 `prompt_complete` 就不再回这条
888
+ // 应答,只等它这一轮永远收不了口。那不是本地假装 resolve——收口信号是对端发的。
889
+ try {
890
+ if (settlementHooks === undefined || promptKey === undefined) {
891
+ response = await promptRpc;
892
+ }
893
+ else {
894
+ const { promise: early, resolve: settleEarly } = Promise.withResolvers();
895
+ pendingSettlement = { promptKey, sessionId: started.sessionId, settle: settleEarly };
896
+ try {
897
+ const outcome = await Promise.race([
898
+ promptRpc.then((value) => {
899
+ const key = settlementHooks.responseKey?.(value);
900
+ if (key !== undefined)
901
+ rememberSettledPromptKey(key);
902
+ return { response: value };
903
+ }),
904
+ early,
905
+ ]);
906
+ if (outcome.error !== undefined)
907
+ throw outcome.error;
908
+ response = outcome.response;
909
+ }
910
+ finally {
911
+ pendingSettlement = undefined;
912
+ // 输掉的那条 RPC 没人再等。协议层没有单请求取消,它挂在 pending 表里
913
+ // 直到连接关闭——一轮一条,所以这里吞掉它的 rejection,不留未处理拒绝。
914
+ void promptRpc.catch(() => undefined);
915
+ }
916
+ }
917
+ }
918
+ catch (error) {
919
+ if (disposed) {
920
+ throw new AcpExecutorDisposedError();
921
+ }
922
+ throw error;
923
+ }
924
+ // 应答唤醒调用方不代表同批通知已经派发:一次 stdio read 可能同时带回应答与
925
+ // 排在它后面的 `session/update`。不等排空就结算会漏事件。
926
+ await connection.awaitInboundIdle();
927
+ if (options.beforeApplyResult) {
928
+ await options.beforeApplyResult();
929
+ }
930
+ }
931
+ finally {
932
+ promptMutex.release();
933
+ notifyPromptSettled();
934
+ }
935
+ if (!isLive(started.generation)) {
936
+ throw new AcpExecutorDisposedError();
937
+ }
938
+ if (turnId === undefined) {
939
+ throw new AcpExecutorError("internal", "ACP prompt settled without a turn id");
940
+ }
941
+ const settledTurnId = turnId;
942
+ // 上下文占用在这家 CLI 自己写的记录里(厂商钩子);读不到、读坏都不挡
943
+ // 轮次收尾——少一条事件,界面不画圈,而不是画一个编出来的数。
944
+ let contextWindow;
945
+ if (hooks.readContextWindow !== undefined) {
946
+ try {
947
+ contextWindow =
948
+ (await hooks.readContextWindow({
949
+ sessionId: started.sessionId,
950
+ promptResponse: response,
951
+ })) ?? undefined;
952
+ }
953
+ catch {
954
+ contextWindow = undefined;
955
+ }
956
+ }
957
+ const finishPrompt = () => {
958
+ if (interruptedTurnIds.has(settledTurnId)) {
959
+ settleTurn({
960
+ expectedGeneration: started.generation,
961
+ expectedSessionId: started.sessionId,
962
+ turnId: settledTurnId,
963
+ stopReason: "cancelled",
964
+ state: "cancelled",
965
+ });
966
+ return {
967
+ threadId: options.threadId,
968
+ turnId: settledTurnId,
969
+ stopReason: "cancelled",
970
+ };
971
+ }
972
+ const stopReason = hooks.completedStopReason?.(response) ?? response.stopReason;
973
+ if (contextWindow !== undefined) {
974
+ emit({
975
+ type: "thread.token-usage.updated",
976
+ ...stamp(),
977
+ provider: options.provider,
978
+ threadId: options.threadId,
979
+ turnId: settledTurnId,
980
+ payload: {
981
+ usage: {
982
+ usedTokens: contextWindow.usedTokens,
983
+ ...(contextWindow.maxTokens === undefined
984
+ ? {}
985
+ : { maxTokens: contextWindow.maxTokens }),
986
+ },
987
+ },
988
+ });
989
+ }
990
+ settleTurn({
991
+ expectedGeneration: started.generation,
992
+ expectedSessionId: started.sessionId,
993
+ turnId: settledTurnId,
994
+ stopReason,
995
+ state: response.stopReason === "cancelled" ? "cancelled" : "completed",
996
+ });
997
+ return {
998
+ threadId: options.threadId,
999
+ turnId: settledTurnId,
1000
+ stopReason,
1001
+ ...(response.usage !== undefined && response.usage !== null
1002
+ ? { usage: response.usage }
1003
+ : {}),
1004
+ };
1005
+ };
1006
+ if (threadMutex.tryAcquire()) {
1007
+ try {
1008
+ return finishPrompt();
1009
+ }
1010
+ finally {
1011
+ threadMutex.release();
1012
+ }
1013
+ }
1014
+ await threadMutex.acquire();
1015
+ try {
1016
+ return finishPrompt();
1017
+ }
1018
+ finally {
1019
+ threadMutex.release();
1020
+ }
1021
+ };
1022
+ const cancel = async (turnId) => {
1023
+ failIfDisposed();
1024
+ const started = requireStarted();
1025
+ const targetTurnId = turnId ?? activeTurnId;
1026
+ if (targetTurnId !== undefined) {
1027
+ interruptedTurnIds.add(targetTurnId);
1028
+ }
1029
+ settleAllApprovals("cancel");
1030
+ settleAllUserInputs();
1031
+ abortPendingSettlement("turn cancelled");
1032
+ const runCancel = async () => {
1033
+ if (sessionId) {
1034
+ await connection.notify("session/cancel", { sessionId });
1035
+ }
1036
+ const settled = await waitForPromptIdle(cancelGraceMs);
1037
+ if (!settled) {
1038
+ throw new AcpExecutorCancelTimeoutError();
1039
+ }
1040
+ if (targetTurnId !== undefined && isLive(started.generation)) {
1041
+ if (activeTurnId === targetTurnId || interruptedTurnIds.has(targetTurnId)) {
1042
+ if (status === "running") {
1043
+ closeActiveAssistantSegment(targetTurnId);
1044
+ promptsInFlight = 0;
1045
+ activeTurnId = undefined;
1046
+ status = "ready";
1047
+ emit({
1048
+ type: "turn.completed",
1049
+ ...stamp(),
1050
+ provider: options.provider,
1051
+ threadId: options.threadId,
1052
+ turnId: targetTurnId,
1053
+ payload: {
1054
+ state: "cancelled",
1055
+ stopReason: "cancelled",
1056
+ },
1057
+ });
1058
+ interruptedTurnIds.delete(targetTurnId);
1059
+ }
1060
+ }
1061
+ }
1062
+ };
1063
+ // 取消不走 thread 锁。今天 thread 锁只罩开轮与结算两段短临界区,prompt RPC 在锁外,
1064
+ // 所以排进去也不会被 RPC 堵住——但那是「锁范围恰好正确」带来的,不是设计保证。
1065
+ // 让取消独立于这把锁,是为了以后有人加长临界区时,Stop 仍然到得了。
1066
+ await runCancel();
1067
+ };
1068
+ const respondToRequest = async (requestId, decision, reason) => {
1069
+ failIfDisposed();
1070
+ const apply = () => {
1071
+ const pending = approvals.get(requestId);
1072
+ if (!pending || pending.requestId !== requestId) {
1073
+ throw new AcpExecutorRequestError("session/request_permission", `Unknown pending approval request: ${requestId}`);
1074
+ }
1075
+ if (pending.settled) {
1076
+ return;
1077
+ }
1078
+ const selectedOptionId = decision === "cancel" ? undefined : selectAcpPermissionOptionId(pending.request, decision);
1079
+ if (decision !== "cancel" && selectedOptionId === undefined) {
1080
+ throw new AcpExecutorRequestError("session/request_permission", `Unsupported approval decision for this request: ${decision}`);
1081
+ }
1082
+ settleApproval(pending, approvalOutcomeForDecision(decision, selectedOptionId), decision, reason);
1083
+ };
1084
+ // 审批答复同样不走 thread 锁,理由同 `cancel`。
1085
+ apply();
1086
+ };
1087
+ const setNativeMode = async (modeId) => {
1088
+ failIfDisposed();
1089
+ const started = startResult;
1090
+ if (started === undefined) {
1091
+ throw new AcpExecutorRequestError("session/set_mode", "session has not started yet");
1092
+ }
1093
+ // 报了 configOptions 那一项就走它,否则走 session/set_mode——两种报法各家挑一种,
1094
+ // 发错通道会被回 `-32601 Method not found`(dsh 实测)。
1095
+ const configId = acpModeConfigId(started.sessionSetupResult);
1096
+ if (configId !== undefined) {
1097
+ await connection.request("session/set_config_option", {
1098
+ sessionId: started.sessionId,
1099
+ configId,
1100
+ value: modeId,
1101
+ });
1102
+ return;
1103
+ }
1104
+ await connection.request("session/set_mode", {
1105
+ sessionId: started.sessionId,
1106
+ modeId,
1107
+ });
1108
+ };
1109
+ const respondToUserInput = async (requestId, answers) => {
1110
+ failIfDisposed();
1111
+ const pending = pendingUserInputs.get(requestId);
1112
+ if (pending === undefined) {
1113
+ throw new AcpExecutorRequestError("session/request_permission", `Unknown pending user-input request: ${requestId}`);
1114
+ }
1115
+ if (pending.settled) {
1116
+ return;
1117
+ }
1118
+ pending.settled = true;
1119
+ pendingUserInputs.delete(requestId);
1120
+ pending.resolve({
1121
+ kind: "answered",
1122
+ answers: isRecord(answers) ? answers : {},
1123
+ });
1124
+ };
1125
+ const dispose = async () => {
1126
+ if (disposed) {
1127
+ return;
1128
+ }
1129
+ settleAllApprovals("cancel");
1130
+ settleAllUserInputs();
1131
+ abortPendingSettlement("executor disposed");
1132
+ notifyPromptSettled();
1133
+ abortLocalPrompt?.();
1134
+ emit({
1135
+ type: "session.exited",
1136
+ ...stamp(),
1137
+ provider: options.provider,
1138
+ threadId: options.threadId,
1139
+ payload: { reason: "disposed" },
1140
+ });
1141
+ disposed = true;
1142
+ generation += 1;
1143
+ status = "disposed";
1144
+ eventStream.close();
1145
+ options.readable.off("end", onInputDead);
1146
+ options.readable.off("error", onInputDead);
1147
+ options.readable.pause();
1148
+ if (!connectionClosed) {
1149
+ await connection.awaitInboundIdle().catch(() => undefined);
1150
+ await connection.close().catch(() => undefined);
1151
+ }
1152
+ };
1153
+ return {
1154
+ get generation() {
1155
+ return generation;
1156
+ },
1157
+ get sessionId() {
1158
+ return sessionId;
1159
+ },
1160
+ get modelId() {
1161
+ return modelSelectionState.currentModelId;
1162
+ },
1163
+ get status() {
1164
+ return status;
1165
+ },
1166
+ events: eventStream.subscribe(),
1167
+ start,
1168
+ prompt,
1169
+ cancel,
1170
+ respondToRequest,
1171
+ respondToUserInput,
1172
+ setNativeMode,
1173
+ dispose,
1174
+ };
1175
+ }
1176
+ export function isAcpExecutorError(error) {
1177
+ return error instanceof AcpExecutorError;
1178
+ }
1179
+ //# sourceMappingURL=executor.js.map