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.
- package/dist/executor/lifecycle.d.ts +15 -0
- package/dist/executor/lifecycle.d.ts.map +1 -0
- package/dist/executor/lifecycle.js +150 -0
- package/dist/executor/lifecycle.js.map +1 -0
- package/dist/executor/prompt.d.ts +11 -0
- package/dist/executor/prompt.d.ts.map +1 -0
- package/dist/executor/prompt.js +283 -0
- package/dist/executor/prompt.js.map +1 -0
- package/dist/executor/requests.d.ts +13 -0
- package/dist/executor/requests.d.ts.map +1 -0
- package/dist/executor/requests.js +215 -0
- package/dist/executor/requests.js.map +1 -0
- package/dist/executor/runtime.d.ts +14 -0
- package/dist/executor/runtime.d.ts.map +1 -0
- package/dist/executor/runtime.js +103 -0
- package/dist/executor/runtime.js.map +1 -0
- package/dist/executor/session.d.ts +19 -0
- package/dist/executor/session.d.ts.map +1 -0
- package/dist/executor/session.js +225 -0
- package/dist/executor/session.js.map +1 -0
- package/dist/executor/settlement.d.ts +8 -0
- package/dist/executor/settlement.d.ts.map +1 -0
- package/dist/executor/settlement.js +70 -0
- package/dist/executor/settlement.js.map +1 -0
- package/dist/executor/support.d.ts +88 -0
- package/dist/executor/support.d.ts.map +1 -0
- package/dist/executor/support.js +13 -0
- package/dist/executor/support.js.map +1 -0
- package/dist/executor/updates.d.ts +8 -0
- package/dist/executor/updates.d.ts.map +1 -0
- package/dist/executor/updates.js +245 -0
- package/dist/executor/updates.js.map +1 -0
- package/dist/executor.d.ts +1 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +144 -1247
- package/dist/executor.js.map +1 -1
- 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 {
|
|
6
|
-
import {
|
|
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 {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
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 {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { defaultVendorHooks
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
}
|