ycodium-acp-executor 0.2.3080 → 0.2.3088

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