happy-imou-cloud 2.1.49 → 2.1.51
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/AcpBackend-CqO3D07V.mjs +2619 -0
- package/dist/AcpBackend-XPiTd6ph.cjs +2621 -0
- package/dist/{BaseReasoningProcessor-Dn9NcoHz.cjs → BaseReasoningProcessor-BD9tiwep.cjs} +1 -144
- package/dist/{BaseReasoningProcessor-CAVeOdyo.mjs → BaseReasoningProcessor-CjlayL2f.mjs} +2 -144
- package/dist/ConversationHistory-Bl2doTA-.cjs +780 -0
- package/dist/ConversationHistory-CI5bBfuA.mjs +771 -0
- package/dist/{ProviderSelectionHandler-BJJc7qOR.cjs → ProviderSelectionHandler-C7GE5QjX.cjs} +6 -6
- package/dist/{ProviderSelectionHandler-DIYidT13.mjs → ProviderSelectionHandler-uQ8jzdzr.mjs} +2 -2
- package/dist/RuntimeShell-BDt42io_.mjs +252 -0
- package/dist/RuntimeShell-D_Te12wq.cjs +258 -0
- package/dist/bootstrapManagedProviderSession-Bln-TwyB.cjs +147 -0
- package/dist/bootstrapManagedProviderSession-D2Z6YU3n.mjs +145 -0
- package/dist/claude-BKNT-2fG.cjs +1080 -0
- package/dist/claude-CnN5WCWj.mjs +1073 -0
- package/dist/codex-DLGP8WF6.mjs +577 -0
- package/dist/codex-Fv2eali8.cjs +582 -0
- package/dist/{command-VcH4hbhi.cjs → command-BWPlJyCN.cjs} +16 -8
- package/dist/{command-CzfRRhVe.mjs → command-CELwsYoG.mjs} +15 -7
- package/dist/config-CFL0Gkqt.cjs +184 -0
- package/dist/config-ChSPe7p9.mjs +174 -0
- package/dist/createDefaultRuntimeShell-BXu3vCvT.cjs +33 -0
- package/dist/createDefaultRuntimeShell-DOg6g3-G.mjs +31 -0
- package/dist/cursor-Blq1cHdr.cjs +91 -0
- package/dist/cursor-CwPNSy_A.mjs +88 -0
- package/dist/future-Dq4Ha1Dn.cjs +24 -0
- package/dist/future-xRdLl3vf.mjs +22 -0
- package/dist/{index-xa1kwZoj.cjs → index-B_JYgMUS.cjs} +189 -5352
- package/dist/{index-7Z93BoVn.mjs → index-CX-F_fuk.mjs} +177 -5331
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/installFatalProcessHandlers-0vaw9MAz.mjs +55 -0
- package/dist/installFatalProcessHandlers-CyURn5Bp.cjs +57 -0
- package/dist/launch-BoCCEd5p.mjs +63 -0
- package/dist/launch-wZA5BcvS.cjs +66 -0
- package/dist/lib.cjs +2 -3
- package/dist/lib.d.cts +20 -17
- package/dist/lib.d.mts +20 -17
- package/dist/lib.mjs +1 -2
- package/dist/resolveCommand-B3BGyBE2.mjs +189 -0
- package/dist/resolveCommand-DYMd9PNC.cjs +193 -0
- package/dist/{runClaude-zCwRhpOw.mjs → runClaude-Be0myF9k.mjs} +8 -5
- package/dist/{runClaude-BBGNmGj6.cjs → runClaude-DZJt5er7.cjs} +46 -43
- package/dist/{runCodex-BbgLVjb9.mjs → runCodex-BSnyN4m7.mjs} +226 -117
- package/dist/{runCodex-jUU6U2tZ.cjs → runCodex-DTCcGRue.cjs} +269 -160
- package/dist/runCursor-Bn1PuwJy.cjs +506 -0
- package/dist/runCursor-M6dQ6bGF.mjs +504 -0
- package/dist/{runGemini-DcwNsudA.mjs → runGemini-BNm4vYKA.mjs} +279 -5
- package/dist/{runGemini-C0NT8MHK.cjs → runGemini-Bn3lFhz6.cjs} +309 -35
- package/dist/{registerKillSessionHandler-DLDg2EES.mjs → sessionControl-1bT_7OI6.mjs} +1643 -2405
- package/dist/{registerKillSessionHandler-CfCya6si.cjs → sessionControl-flKnQrx0.cjs} +1647 -2417
- package/dist/{api-DnqaNvyV.mjs → types-B5vtxa38.mjs} +55 -5
- package/dist/{api-D7nAeZi7.cjs → types-CttABk32.cjs} +55 -4
- package/package.json +2 -2
- package/dist/types-CiliQpqS.mjs +0 -52
- package/dist/types-DVk3crez.cjs +0 -54
|
@@ -0,0 +1,2619 @@
|
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
|
+
import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { l as logger, H as HeadTailPreviewBuffer, p as packageJson, e as delay } from './types-B5vtxa38.mjs';
|
|
5
|
+
import { i as isTerminalReferenceOnlyPayload, f as formatDisplayMessage } from './RuntimeShell-BDt42io_.mjs';
|
|
6
|
+
import { D as DefaultTransport, f as resolveAcpSessionPreferences, g as buildAcpSessionConfigPresetPlan, h as resolveAcpPostPromptNoUpdatesTimeoutMs, i as resolveAcpResponseWaitTimeoutMs } from './index-CX-F_fuk.mjs';
|
|
7
|
+
import psList from 'ps-list';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 500;
|
|
10
|
+
const DEFAULT_TOOL_CALL_TIMEOUT_MS = 10 * 6e4;
|
|
11
|
+
const DEFAULT_TOOL_CALL_OUTPUT_PREVIEW_HEAD_BYTES = 4096;
|
|
12
|
+
const DEFAULT_TOOL_CALL_OUTPUT_PREVIEW_TAIL_BYTES = 12288;
|
|
13
|
+
function parseArgsFromContent(content) {
|
|
14
|
+
if (Array.isArray(content)) {
|
|
15
|
+
return { items: content };
|
|
16
|
+
}
|
|
17
|
+
if (content && typeof content === "object" && content !== null) {
|
|
18
|
+
return content;
|
|
19
|
+
}
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
function buildToolCallArgs(_toolKind, content, locations) {
|
|
23
|
+
const args = parseArgsFromContent(content);
|
|
24
|
+
if (Array.isArray(locations)) {
|
|
25
|
+
args.locations = locations;
|
|
26
|
+
}
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
29
|
+
const MAX_TOOL_OUTPUT_OVERLAP_CHARS = 4096;
|
|
30
|
+
const MAX_TRACKED_TOOL_OUTPUT_EXACT_CHARS = 16384;
|
|
31
|
+
const MAX_TRACKED_TOOL_OUTPUT_HEAD_CHARS = 2048;
|
|
32
|
+
const MAX_TRACKED_TOOL_OUTPUT_TAIL_CHARS = MAX_TOOL_OUTPUT_OVERLAP_CHARS;
|
|
33
|
+
const MAX_TRACKED_TOOL_OUTPUT_SUFFIX_CHARS = 65536;
|
|
34
|
+
function findTrailingNewlineTrimmedLength(text) {
|
|
35
|
+
let trimmedLength = text.length;
|
|
36
|
+
while (trimmedLength > 0) {
|
|
37
|
+
const charCode = text.charCodeAt(trimmedLength - 1);
|
|
38
|
+
if (charCode !== 10 && charCode !== 13) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
trimmedLength--;
|
|
42
|
+
}
|
|
43
|
+
return trimmedLength;
|
|
44
|
+
}
|
|
45
|
+
function sliceSnapshotHead(text, length) {
|
|
46
|
+
return text.slice(0, Math.min(length, MAX_TRACKED_TOOL_OUTPUT_HEAD_CHARS));
|
|
47
|
+
}
|
|
48
|
+
function sliceSnapshotTail(text, length) {
|
|
49
|
+
const tailLength = Math.min(length, MAX_TRACKED_TOOL_OUTPUT_TAIL_CHARS);
|
|
50
|
+
return text.slice(Math.max(0, length - tailLength), length);
|
|
51
|
+
}
|
|
52
|
+
function sliceSnapshotSuffixWindow(text, length) {
|
|
53
|
+
const suffixLength = Math.min(length, MAX_TRACKED_TOOL_OUTPUT_SUFFIX_CHARS);
|
|
54
|
+
return text.slice(Math.max(0, length - suffixLength), length);
|
|
55
|
+
}
|
|
56
|
+
function buildToolOutputSnapshot(text) {
|
|
57
|
+
const length = text.length;
|
|
58
|
+
const trimmedLength = findTrailingNewlineTrimmedLength(text);
|
|
59
|
+
return {
|
|
60
|
+
length,
|
|
61
|
+
exact: length <= MAX_TRACKED_TOOL_OUTPUT_EXACT_CHARS ? text : void 0,
|
|
62
|
+
head: sliceSnapshotHead(text, length),
|
|
63
|
+
tail: sliceSnapshotTail(text, length),
|
|
64
|
+
suffixWindow: sliceSnapshotSuffixWindow(text, length),
|
|
65
|
+
trimmedLength,
|
|
66
|
+
trimmedExact: trimmedLength <= MAX_TRACKED_TOOL_OUTPUT_EXACT_CHARS ? text.slice(0, trimmedLength) : void 0,
|
|
67
|
+
trimmedHead: sliceSnapshotHead(text, trimmedLength),
|
|
68
|
+
trimmedTail: sliceSnapshotTail(text, trimmedLength),
|
|
69
|
+
trimmedSuffixWindow: sliceSnapshotSuffixWindow(text, trimmedLength)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function snapshotMatchesEdges(next, snapshotLength, snapshotHead, snapshotTail) {
|
|
73
|
+
if (next.length < snapshotLength) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (snapshotHead.length > 0 && !next.startsWith(snapshotHead)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (snapshotTail.length === 0) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
const tailStart = snapshotLength - snapshotTail.length;
|
|
83
|
+
return next.slice(tailStart, tailStart + snapshotTail.length) === snapshotTail;
|
|
84
|
+
}
|
|
85
|
+
function snapshotMatchesText(snapshot, next, useTrimmedSnapshot = false) {
|
|
86
|
+
const snapshotLength = useTrimmedSnapshot ? snapshot.trimmedLength : snapshot.length;
|
|
87
|
+
if (next.length !== snapshotLength) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const snapshotExact = useTrimmedSnapshot ? snapshot.trimmedExact : snapshot.exact;
|
|
91
|
+
if (snapshotExact !== void 0) {
|
|
92
|
+
return next === snapshotExact;
|
|
93
|
+
}
|
|
94
|
+
const snapshotHead = useTrimmedSnapshot ? snapshot.trimmedHead : snapshot.head;
|
|
95
|
+
const snapshotTail = useTrimmedSnapshot ? snapshot.trimmedTail : snapshot.tail;
|
|
96
|
+
return snapshotMatchesEdges(next, snapshotLength, snapshotHead, snapshotTail);
|
|
97
|
+
}
|
|
98
|
+
function snapshotIsPrefixOfText(snapshot, next, useTrimmedSnapshot = false) {
|
|
99
|
+
const snapshotLength = useTrimmedSnapshot ? snapshot.trimmedLength : snapshot.length;
|
|
100
|
+
if (next.length < snapshotLength) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const snapshotExact = useTrimmedSnapshot ? snapshot.trimmedExact : snapshot.exact;
|
|
104
|
+
if (snapshotExact !== void 0) {
|
|
105
|
+
return next.startsWith(snapshotExact);
|
|
106
|
+
}
|
|
107
|
+
const snapshotHead = useTrimmedSnapshot ? snapshot.trimmedHead : snapshot.head;
|
|
108
|
+
const snapshotTail = useTrimmedSnapshot ? snapshot.trimmedTail : snapshot.tail;
|
|
109
|
+
return snapshotMatchesEdges(next, snapshotLength, snapshotHead, snapshotTail);
|
|
110
|
+
}
|
|
111
|
+
function snapshotEndsWithText(snapshot, next) {
|
|
112
|
+
if (next.length === 0) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (snapshot.exact !== void 0) {
|
|
116
|
+
return snapshot.exact.endsWith(next);
|
|
117
|
+
}
|
|
118
|
+
return next.length <= snapshot.suffixWindow.length && snapshot.suffixWindow.endsWith(next);
|
|
119
|
+
}
|
|
120
|
+
function findToolOutputOverlap(previous, next) {
|
|
121
|
+
const maxOverlap = Math.min(previous.length, next.length, MAX_TOOL_OUTPUT_OVERLAP_CHARS);
|
|
122
|
+
if (maxOverlap === 0) {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
const previousSuffix = previous.slice(-maxOverlap);
|
|
126
|
+
const nextPrefix = next.slice(0, maxOverlap);
|
|
127
|
+
for (let overlap = maxOverlap; overlap > 0; overlap--) {
|
|
128
|
+
if (previousSuffix.endsWith(nextPrefix.slice(0, overlap))) {
|
|
129
|
+
return overlap;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
function getToolOutputDelta(previous, next) {
|
|
135
|
+
if (!previous || previous.length === 0) {
|
|
136
|
+
return next;
|
|
137
|
+
}
|
|
138
|
+
if (snapshotMatchesText(previous, next) || snapshotEndsWithText(previous, next)) {
|
|
139
|
+
return "";
|
|
140
|
+
}
|
|
141
|
+
if (snapshotIsPrefixOfText(previous, next)) {
|
|
142
|
+
return next.slice(previous.length);
|
|
143
|
+
}
|
|
144
|
+
const overlapSource = previous.exact ?? previous.tail;
|
|
145
|
+
const overlap = findToolOutputOverlap(overlapSource, next);
|
|
146
|
+
return overlap > 0 ? next.slice(overlap) : next;
|
|
147
|
+
}
|
|
148
|
+
function shouldReplaceToolOutput(previous, next) {
|
|
149
|
+
if (!previous || previous.length === 0) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
if (snapshotIsPrefixOfText(previous, next)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (previous.trimmedLength === previous.length) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return snapshotIsPrefixOfText(previous, next, true);
|
|
159
|
+
}
|
|
160
|
+
function appendToolOutput(existing, next) {
|
|
161
|
+
const shouldReplace = shouldReplaceToolOutput(existing?.lastRawText, next);
|
|
162
|
+
const preview = shouldReplace || !existing?.preview ? new HeadTailPreviewBuffer(
|
|
163
|
+
DEFAULT_TOOL_CALL_OUTPUT_PREVIEW_HEAD_BYTES,
|
|
164
|
+
DEFAULT_TOOL_CALL_OUTPUT_PREVIEW_TAIL_BYTES
|
|
165
|
+
) : existing.preview;
|
|
166
|
+
let emittedChunk = null;
|
|
167
|
+
if (shouldReplace) {
|
|
168
|
+
preview.append(next);
|
|
169
|
+
} else {
|
|
170
|
+
const textToAppend = getToolOutputDelta(existing?.lastRawText, next);
|
|
171
|
+
if (textToAppend.length > 0) {
|
|
172
|
+
preview.append(textToAppend);
|
|
173
|
+
emittedChunk = textToAppend;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
accumulator: {
|
|
178
|
+
preview,
|
|
179
|
+
// ACP runtimes sometimes resend the entire cumulative stdout snapshot.
|
|
180
|
+
// Keep only a bounded summary for dedupe so long tool runs cannot retain
|
|
181
|
+
// every historical snapshot in memory.
|
|
182
|
+
lastRawText: buildToolOutputSnapshot(next),
|
|
183
|
+
updateCount: (existing?.updateCount ?? 0) + 1
|
|
184
|
+
},
|
|
185
|
+
emittedChunk
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function renderToolOutput(accumulator) {
|
|
189
|
+
if (!accumulator) {
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
return accumulator.preview.render("tool output");
|
|
193
|
+
}
|
|
194
|
+
function extractTerminalOutputMeta(update) {
|
|
195
|
+
if (!isRecord(update._meta)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const terminalOutput = update._meta.terminal_output;
|
|
199
|
+
if (!isRecord(terminalOutput)) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const data = typeof terminalOutput.data === "string" && terminalOutput.data.length > 0 ? terminalOutput.data : null;
|
|
203
|
+
if (!data) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const terminalId = typeof terminalOutput.terminal_id === "string" && terminalOutput.terminal_id.length > 0 ? terminalOutput.terminal_id : null;
|
|
207
|
+
const toolCallId = typeof update.toolCallId === "string" && update.toolCallId.length > 0 ? update.toolCallId : terminalId;
|
|
208
|
+
return toolCallId ? { toolCallId, data } : null;
|
|
209
|
+
}
|
|
210
|
+
function isTerminalLikeToolKind(toolKind) {
|
|
211
|
+
if (typeof toolKind !== "string" || toolKind.length === 0) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const normalized = toolKind.toLowerCase();
|
|
215
|
+
return normalized === "execute" || normalized.includes("bash") || normalized.includes("shell") || normalized.includes("terminal") || normalized.includes("command");
|
|
216
|
+
}
|
|
217
|
+
function formatToolCallTimeoutLimit(timeoutMs) {
|
|
218
|
+
if (timeoutMs < 1e3) {
|
|
219
|
+
return `${timeoutMs}ms`;
|
|
220
|
+
}
|
|
221
|
+
return `${(timeoutMs / 1e3).toFixed(Number.isInteger(timeoutMs / 1e3) ? 0 : 1)}s`;
|
|
222
|
+
}
|
|
223
|
+
function isRecord(value) {
|
|
224
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
225
|
+
}
|
|
226
|
+
function extractNestedToolContentText(value) {
|
|
227
|
+
if (isTerminalReferenceOnlyPayload(value)) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
if (typeof value === "string") {
|
|
231
|
+
return value.length > 0 ? value : null;
|
|
232
|
+
}
|
|
233
|
+
if (Array.isArray(value)) {
|
|
234
|
+
const parts = value.map((item) => extractNestedToolContentText(item)).filter((item) => Boolean(item));
|
|
235
|
+
return parts.length > 0 ? parts.join("") : null;
|
|
236
|
+
}
|
|
237
|
+
if (!isRecord(value)) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
if (typeof value.text === "string" && value.text.length > 0) {
|
|
241
|
+
return value.text;
|
|
242
|
+
}
|
|
243
|
+
if ("content" in value) {
|
|
244
|
+
return extractNestedToolContentText(value.content);
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
function hasMeaningfulContent(value) {
|
|
249
|
+
if (value === null || value === void 0) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
if (typeof value === "string") {
|
|
253
|
+
return value.length > 0;
|
|
254
|
+
}
|
|
255
|
+
if (Array.isArray(value)) {
|
|
256
|
+
return value.length > 0;
|
|
257
|
+
}
|
|
258
|
+
if (isRecord(value)) {
|
|
259
|
+
return Object.keys(value).length > 0;
|
|
260
|
+
}
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
function looksLikeToolMetadata(record) {
|
|
264
|
+
const metadataKeys = [
|
|
265
|
+
"command",
|
|
266
|
+
"cmd",
|
|
267
|
+
"script",
|
|
268
|
+
"argv",
|
|
269
|
+
"cwd",
|
|
270
|
+
"workingDirectory",
|
|
271
|
+
"description",
|
|
272
|
+
"title",
|
|
273
|
+
"parsed_cmd"
|
|
274
|
+
];
|
|
275
|
+
if (metadataKeys.some((key) => key in record)) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
const nestedKeys = ["input", "toolCall", "arguments", "content"];
|
|
279
|
+
for (const key of nestedKeys) {
|
|
280
|
+
const nested = record[key];
|
|
281
|
+
if (isRecord(nested) && looksLikeToolMetadata(nested)) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
function extractToolOutputChunk(content) {
|
|
288
|
+
if (typeof content === "string") {
|
|
289
|
+
return content.length > 0 ? content : null;
|
|
290
|
+
}
|
|
291
|
+
if (Array.isArray(content)) {
|
|
292
|
+
const parts = content.map((item) => extractToolOutputChunk(item)).filter((item) => Boolean(item));
|
|
293
|
+
return parts.length > 0 ? parts.join("") : null;
|
|
294
|
+
}
|
|
295
|
+
if (!isRecord(content)) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
if (isTerminalReferenceOnlyPayload(content)) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const outputKeys = ["stdout", "stderr", "output", "text", "message", "data", "error", "reason"];
|
|
302
|
+
const hasOutputKey = outputKeys.some((key) => key in content);
|
|
303
|
+
if (!hasOutputKey && looksLikeToolMetadata(content)) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
for (const key of outputKeys) {
|
|
307
|
+
if (!(key in content)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const value = content[key];
|
|
311
|
+
const nestedText2 = extractNestedToolContentText(value);
|
|
312
|
+
if (nestedText2 && nestedText2.length > 0) {
|
|
313
|
+
return nestedText2;
|
|
314
|
+
}
|
|
315
|
+
const formatted2 = typeof value === "string" ? value : formatDisplayMessage(value);
|
|
316
|
+
if (formatted2.length > 0) {
|
|
317
|
+
return formatted2;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const nestedText = extractNestedToolContentText(content);
|
|
321
|
+
if (nestedText && nestedText.length > 0) {
|
|
322
|
+
return nestedText;
|
|
323
|
+
}
|
|
324
|
+
const formatted = formatDisplayMessage(content);
|
|
325
|
+
return formatted.length > 0 ? formatted : null;
|
|
326
|
+
}
|
|
327
|
+
function hasVisibleToolCallProgress(content) {
|
|
328
|
+
return extractToolOutputChunk(content) !== null;
|
|
329
|
+
}
|
|
330
|
+
function mergeStreamedOutputWithResult(content, streamedOutput) {
|
|
331
|
+
if (!streamedOutput || streamedOutput.length === 0) {
|
|
332
|
+
return content;
|
|
333
|
+
}
|
|
334
|
+
if (!hasMeaningfulContent(content)) {
|
|
335
|
+
return streamedOutput;
|
|
336
|
+
}
|
|
337
|
+
if (isRecord(content)) {
|
|
338
|
+
const hasStructuredOutput = ["stdout", "stderr", "output", "text", "message", "data"].some((key) => key in content);
|
|
339
|
+
if (!hasStructuredOutput) {
|
|
340
|
+
return {
|
|
341
|
+
...content,
|
|
342
|
+
stdout: streamedOutput
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return content;
|
|
347
|
+
}
|
|
348
|
+
function buildCompletedToolResult(toolKind, content, streamedOutput) {
|
|
349
|
+
if (isTerminalLikeToolKind(toolKind) && isTerminalReferenceOnlyPayload(content)) {
|
|
350
|
+
return void 0;
|
|
351
|
+
}
|
|
352
|
+
return mergeStreamedOutputWithResult(content, streamedOutput);
|
|
353
|
+
}
|
|
354
|
+
function extractErrorDetail(content) {
|
|
355
|
+
if (!content) return void 0;
|
|
356
|
+
if (typeof content === "string") {
|
|
357
|
+
return content;
|
|
358
|
+
}
|
|
359
|
+
if (typeof content === "object" && content !== null && !Array.isArray(content)) {
|
|
360
|
+
const obj = content;
|
|
361
|
+
if (obj.error) {
|
|
362
|
+
const error = obj.error;
|
|
363
|
+
if (typeof error === "string") return error;
|
|
364
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
365
|
+
const errObj = error;
|
|
366
|
+
if (typeof errObj.message === "string") return errObj.message;
|
|
367
|
+
}
|
|
368
|
+
return JSON.stringify(error);
|
|
369
|
+
}
|
|
370
|
+
if (typeof obj.message === "string") return obj.message;
|
|
371
|
+
const status = typeof obj.status === "string" ? obj.status : void 0;
|
|
372
|
+
const reason = typeof obj.reason === "string" ? obj.reason : void 0;
|
|
373
|
+
return status || reason || JSON.stringify(obj).substring(0, 500);
|
|
374
|
+
}
|
|
375
|
+
return void 0;
|
|
376
|
+
}
|
|
377
|
+
function formatDuration(startTime) {
|
|
378
|
+
if (!startTime) return "unknown";
|
|
379
|
+
const duration = Date.now() - startTime;
|
|
380
|
+
return `${(duration / 1e3).toFixed(2)}s`;
|
|
381
|
+
}
|
|
382
|
+
function formatDurationMinutes(startTime) {
|
|
383
|
+
if (!startTime) return "unknown";
|
|
384
|
+
const duration = Date.now() - startTime;
|
|
385
|
+
return (duration / 1e3 / 60).toFixed(2);
|
|
386
|
+
}
|
|
387
|
+
function handleAgentMessageChunk(update, ctx) {
|
|
388
|
+
const content = update.content;
|
|
389
|
+
if (!content || typeof content !== "object" || !("text" in content)) {
|
|
390
|
+
return { handled: false };
|
|
391
|
+
}
|
|
392
|
+
const text = content.text;
|
|
393
|
+
if (typeof text !== "string") {
|
|
394
|
+
return { handled: false };
|
|
395
|
+
}
|
|
396
|
+
const isThinking = /^\*\*[^*]+\*\*\n/.test(text);
|
|
397
|
+
if (isThinking) {
|
|
398
|
+
ctx.emit({
|
|
399
|
+
type: "event",
|
|
400
|
+
name: "thinking",
|
|
401
|
+
payload: { text }
|
|
402
|
+
});
|
|
403
|
+
} else {
|
|
404
|
+
logger.debug(`[AcpBackend] Received message chunk (length: ${text.length}): ${text.substring(0, 50)}...`);
|
|
405
|
+
ctx.emit({
|
|
406
|
+
type: "model-output",
|
|
407
|
+
textDelta: text
|
|
408
|
+
});
|
|
409
|
+
ctx.clearIdleTimeout();
|
|
410
|
+
const idleTimeoutMs = ctx.transport.getIdleTimeout?.() ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
411
|
+
ctx.setIdleTimeout(() => {
|
|
412
|
+
if (ctx.activeToolCalls.size === 0) {
|
|
413
|
+
logger.debug("[AcpBackend] No more chunks received, emitting idle status");
|
|
414
|
+
ctx.emitIdleStatus();
|
|
415
|
+
} else {
|
|
416
|
+
logger.debug(`[AcpBackend] Delaying idle status - ${ctx.activeToolCalls.size} active tool calls`);
|
|
417
|
+
}
|
|
418
|
+
}, idleTimeoutMs);
|
|
419
|
+
}
|
|
420
|
+
return { handled: true };
|
|
421
|
+
}
|
|
422
|
+
function handleAgentThoughtChunk(update, ctx) {
|
|
423
|
+
const content = update.content;
|
|
424
|
+
if (!content || typeof content !== "object" || !("text" in content)) {
|
|
425
|
+
return { handled: false };
|
|
426
|
+
}
|
|
427
|
+
const text = content.text;
|
|
428
|
+
if (typeof text !== "string") {
|
|
429
|
+
return { handled: false };
|
|
430
|
+
}
|
|
431
|
+
if (ctx.activeToolCalls.size > 0) {
|
|
432
|
+
const activeToolCallsList = Array.from(ctx.activeToolCalls);
|
|
433
|
+
logger.debug(`[AcpBackend] \u{1F4AD} Thinking chunk received (${text.length} chars) during active tool calls: ${activeToolCallsList.join(", ")}`);
|
|
434
|
+
}
|
|
435
|
+
ctx.emit({
|
|
436
|
+
type: "event",
|
|
437
|
+
name: "thinking",
|
|
438
|
+
payload: { text }
|
|
439
|
+
});
|
|
440
|
+
return { handled: true };
|
|
441
|
+
}
|
|
442
|
+
function startToolCall(toolCallId, toolKind, update, ctx, source) {
|
|
443
|
+
const startTime = Date.now();
|
|
444
|
+
const toolKindStr = typeof toolKind === "string" ? toolKind : void 0;
|
|
445
|
+
const isInvestigation = ctx.transport.isInvestigationTool?.(toolCallId, toolKindStr) ?? false;
|
|
446
|
+
const extractedName = ctx.transport.extractToolNameFromId?.(toolCallId);
|
|
447
|
+
const realToolName = extractedName ?? (toolKindStr || "unknown");
|
|
448
|
+
ctx.toolCallIdToNameMap.set(toolCallId, realToolName);
|
|
449
|
+
ctx.activeToolCalls.add(toolCallId);
|
|
450
|
+
ctx.toolCallStartTimes.set(toolCallId, startTime);
|
|
451
|
+
logger.debug(`[AcpBackend] \u23F1\uFE0F Set startTime for ${toolCallId} at ${new Date(startTime).toISOString()} (from ${source})`);
|
|
452
|
+
logger.debug(`[AcpBackend] \u{1F527} Tool call START: ${toolCallId} (${toolKind} -> ${realToolName})${isInvestigation ? " [INVESTIGATION TOOL]" : ""}`);
|
|
453
|
+
if (isInvestigation) {
|
|
454
|
+
logger.debug(`[AcpBackend] \u{1F50D} Investigation tool detected - extended timeout (10min) will be used`);
|
|
455
|
+
}
|
|
456
|
+
const timeoutMs = ctx.transport.getToolCallTimeout?.(toolCallId, toolKindStr) ?? DEFAULT_TOOL_CALL_TIMEOUT_MS;
|
|
457
|
+
if (!ctx.toolCallTimeouts.has(toolCallId)) {
|
|
458
|
+
ctx.armToolCallTimeout({
|
|
459
|
+
toolCallId,
|
|
460
|
+
toolKind,
|
|
461
|
+
toolName: realToolName,
|
|
462
|
+
timeoutMs,
|
|
463
|
+
source
|
|
464
|
+
});
|
|
465
|
+
logger.debug(`[AcpBackend] \u23F1\uFE0F Set no-progress timeout for ${toolCallId}: ${(timeoutMs / 1e3).toFixed(0)}s${isInvestigation ? " (investigation tool)" : ""}`);
|
|
466
|
+
} else {
|
|
467
|
+
logger.debug(`[AcpBackend] Timeout already set for ${toolCallId}, skipping`);
|
|
468
|
+
}
|
|
469
|
+
ctx.clearIdleTimeout();
|
|
470
|
+
ctx.emit({ type: "status", status: "running" });
|
|
471
|
+
const args = buildToolCallArgs(toolKind, update.content, update.locations);
|
|
472
|
+
if (isInvestigation && args.objective) {
|
|
473
|
+
logger.debug(`[AcpBackend] \u{1F50D} Investigation tool objective: ${String(args.objective).substring(0, 100)}...`);
|
|
474
|
+
}
|
|
475
|
+
ctx.emit({
|
|
476
|
+
type: "tool-call",
|
|
477
|
+
toolName: toolKindStr || "unknown",
|
|
478
|
+
args,
|
|
479
|
+
callId: toolCallId
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function completeToolCall(toolCallId, toolKind, content, ctx) {
|
|
483
|
+
const startTime = ctx.toolCallStartTimes.get(toolCallId);
|
|
484
|
+
const duration = formatDuration(startTime);
|
|
485
|
+
const toolKindStr = typeof toolKind === "string" && toolKind !== "unknown" ? toolKind : ctx.toolCallIdToNameMap.get(toolCallId) || "unknown";
|
|
486
|
+
ctx.activeToolCalls.delete(toolCallId);
|
|
487
|
+
ctx.toolCallStartTimes.delete(toolCallId);
|
|
488
|
+
ctx.toolCallIdToNameMap.delete(toolCallId);
|
|
489
|
+
ctx.clearToolCallTimeout(toolCallId);
|
|
490
|
+
const streamedOutput = renderToolOutput(ctx.toolCallOutputs.get(toolCallId));
|
|
491
|
+
ctx.toolCallOutputs.delete(toolCallId);
|
|
492
|
+
logger.debug(`[AcpBackend] \u2705 Tool call COMPLETED: ${toolCallId} (${toolKindStr}) - Duration: ${duration}. Active tool calls: ${ctx.activeToolCalls.size}`);
|
|
493
|
+
ctx.emit({
|
|
494
|
+
type: "tool-result",
|
|
495
|
+
toolName: toolKindStr,
|
|
496
|
+
result: buildCompletedToolResult(toolKindStr, content, streamedOutput),
|
|
497
|
+
callId: toolCallId
|
|
498
|
+
});
|
|
499
|
+
if (ctx.activeToolCalls.size === 0) {
|
|
500
|
+
ctx.clearIdleTimeout();
|
|
501
|
+
logger.debug("[AcpBackend] All tool calls completed, emitting idle status");
|
|
502
|
+
ctx.emitIdleStatus();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function failToolCall(toolCallId, status, toolKind, content, ctx) {
|
|
506
|
+
const startTime = ctx.toolCallStartTimes.get(toolCallId);
|
|
507
|
+
const duration = startTime ? Date.now() - startTime : null;
|
|
508
|
+
const toolKindStr = typeof toolKind === "string" && toolKind !== "unknown" ? toolKind : ctx.toolCallIdToNameMap.get(toolCallId) || "unknown";
|
|
509
|
+
const isInvestigation = ctx.transport.isInvestigationTool?.(toolCallId, toolKindStr) ?? false;
|
|
510
|
+
const hadTimeout = ctx.toolCallTimeouts.has(toolCallId);
|
|
511
|
+
if (isInvestigation) {
|
|
512
|
+
const durationStr2 = formatDuration(startTime);
|
|
513
|
+
const durationMinutes = formatDurationMinutes(startTime);
|
|
514
|
+
logger.debug(`[AcpBackend] \u{1F50D} Investigation tool ${status.toUpperCase()} after ${durationMinutes} minutes (${durationStr2})`);
|
|
515
|
+
if (duration) {
|
|
516
|
+
const threeMinutes = 3 * 60 * 1e3;
|
|
517
|
+
const tolerance = 5e3;
|
|
518
|
+
if (Math.abs(duration - threeMinutes) < tolerance) {
|
|
519
|
+
logger.debug(`[AcpBackend] \u{1F50D} \u26A0\uFE0F Investigation tool failed at ~3 minutes - likely Gemini CLI timeout, not our timeout`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
logger.debug(`[AcpBackend] \u{1F50D} Investigation tool FAILED - full content:`, JSON.stringify(content, null, 2));
|
|
523
|
+
logger.debug(`[AcpBackend] \u{1F50D} Investigation tool timeout status BEFORE cleanup: ${hadTimeout ? "timeout was set" : "no timeout was set"}`);
|
|
524
|
+
logger.debug(`[AcpBackend] \u{1F50D} Investigation tool startTime status BEFORE cleanup: ${startTime ? `set at ${new Date(startTime).toISOString()}` : "not set"}`);
|
|
525
|
+
}
|
|
526
|
+
ctx.activeToolCalls.delete(toolCallId);
|
|
527
|
+
ctx.toolCallStartTimes.delete(toolCallId);
|
|
528
|
+
ctx.toolCallIdToNameMap.delete(toolCallId);
|
|
529
|
+
const clearedTimeout = ctx.clearToolCallTimeout(toolCallId);
|
|
530
|
+
if (clearedTimeout) {
|
|
531
|
+
logger.debug(`[AcpBackend] Cleared timeout for ${toolCallId} (tool call ${status})`);
|
|
532
|
+
} else {
|
|
533
|
+
logger.debug(`[AcpBackend] No timeout found for ${toolCallId} (tool call ${status}) - timeout may not have been set`);
|
|
534
|
+
}
|
|
535
|
+
const durationStr = formatDuration(startTime);
|
|
536
|
+
logger.debug(`[AcpBackend] \u274C Tool call ${status.toUpperCase()}: ${toolCallId} (${toolKindStr}) - Duration: ${durationStr}. Active tool calls: ${ctx.activeToolCalls.size}`);
|
|
537
|
+
const streamedOutput = renderToolOutput(ctx.toolCallOutputs.get(toolCallId));
|
|
538
|
+
ctx.toolCallOutputs.delete(toolCallId);
|
|
539
|
+
const errorDetail = extractErrorDetail(content);
|
|
540
|
+
if (errorDetail) {
|
|
541
|
+
logger.debug(`[AcpBackend] \u274C Tool call error details: ${errorDetail.substring(0, 500)}`);
|
|
542
|
+
} else {
|
|
543
|
+
logger.debug(`[AcpBackend] \u274C Tool call ${status} but no error details in content`);
|
|
544
|
+
}
|
|
545
|
+
ctx.emit({
|
|
546
|
+
type: "tool-result",
|
|
547
|
+
toolName: toolKindStr,
|
|
548
|
+
result: streamedOutput ? {
|
|
549
|
+
stdout: streamedOutput,
|
|
550
|
+
error: errorDetail || `Tool call ${status}`,
|
|
551
|
+
status
|
|
552
|
+
} : errorDetail ? { error: errorDetail, status } : { error: `Tool call ${status}`, status },
|
|
553
|
+
callId: toolCallId
|
|
554
|
+
});
|
|
555
|
+
if (ctx.activeToolCalls.size === 0) {
|
|
556
|
+
ctx.clearIdleTimeout();
|
|
557
|
+
logger.debug("[AcpBackend] All tool calls completed/failed, emitting idle status");
|
|
558
|
+
ctx.emitIdleStatus();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function handleToolCallUpdate(update, ctx, options) {
|
|
562
|
+
const status = update.status;
|
|
563
|
+
const toolCallId = update.toolCallId;
|
|
564
|
+
if (!toolCallId) {
|
|
565
|
+
logger.debug("[AcpBackend] Tool call update without toolCallId:", update);
|
|
566
|
+
return { handled: false };
|
|
567
|
+
}
|
|
568
|
+
const toolKind = update.kind || ctx.toolCallIdToNameMap.get(toolCallId) || "unknown";
|
|
569
|
+
let toolCallCountSincePrompt = ctx.toolCallCountSincePrompt;
|
|
570
|
+
const hasSupplementalTerminalOutput = typeof options?.supplementalOutputChunk === "string" && options.supplementalOutputChunk.length > 0;
|
|
571
|
+
const outputChunk = extractToolOutputChunk(update.content) ?? options?.supplementalOutputChunk ?? null;
|
|
572
|
+
if (status === "in_progress" || status === "pending") {
|
|
573
|
+
if (!ctx.activeToolCalls.has(toolCallId)) {
|
|
574
|
+
toolCallCountSincePrompt++;
|
|
575
|
+
startToolCall(toolCallId, toolKind, update, ctx, "tool_call_update");
|
|
576
|
+
} else {
|
|
577
|
+
logger.debug(`[AcpBackend] Tool call ${toolCallId} already tracked, status: ${status}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (outputChunk) {
|
|
581
|
+
const nextOutput = appendToolOutput(ctx.toolCallOutputs.get(toolCallId), outputChunk);
|
|
582
|
+
ctx.toolCallOutputs.set(toolCallId, nextOutput.accumulator);
|
|
583
|
+
if (ctx.activeToolCalls.has(toolCallId) && nextOutput.emittedChunk && (hasSupplementalTerminalOutput || isTerminalLikeToolKind(toolKind))) {
|
|
584
|
+
ctx.emit({
|
|
585
|
+
type: "terminal-output",
|
|
586
|
+
data: nextOutput.emittedChunk,
|
|
587
|
+
callId: toolCallId
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (status === "completed") {
|
|
592
|
+
completeToolCall(toolCallId, toolKind, update.content, ctx);
|
|
593
|
+
} else if (status === "failed" || status === "cancelled") {
|
|
594
|
+
failToolCall(toolCallId, status, toolKind, update.content, ctx);
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
handled: true,
|
|
598
|
+
toolCallCountSincePrompt,
|
|
599
|
+
hadVisibleProgress: outputChunk !== null
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function extractSupplementalToolProgress(update) {
|
|
603
|
+
const terminalOutput = extractTerminalOutputMeta(update);
|
|
604
|
+
if (!terminalOutput) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
toolCallId: terminalOutput.toolCallId,
|
|
609
|
+
outputChunk: terminalOutput.data
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function handleToolCall(update, ctx) {
|
|
613
|
+
const toolCallId = update.toolCallId;
|
|
614
|
+
const status = update.status;
|
|
615
|
+
logger.debug(`[AcpBackend] Received tool_call: toolCallId=${toolCallId}, status=${status}, kind=${update.kind}`);
|
|
616
|
+
const isInProgress = !status || status === "in_progress" || status === "pending";
|
|
617
|
+
if (!toolCallId || !isInProgress) {
|
|
618
|
+
logger.debug(`[AcpBackend] Tool call ${toolCallId} not in progress (status: ${status}), skipping`);
|
|
619
|
+
return { handled: false };
|
|
620
|
+
}
|
|
621
|
+
if (ctx.activeToolCalls.has(toolCallId)) {
|
|
622
|
+
logger.debug(`[AcpBackend] Tool call ${toolCallId} already in active set, skipping`);
|
|
623
|
+
return { handled: true };
|
|
624
|
+
}
|
|
625
|
+
startToolCall(toolCallId, update.kind, update, ctx, "tool_call");
|
|
626
|
+
return { handled: true };
|
|
627
|
+
}
|
|
628
|
+
function handleLegacyMessageChunk(update, ctx) {
|
|
629
|
+
if (!update.messageChunk) {
|
|
630
|
+
return { handled: false };
|
|
631
|
+
}
|
|
632
|
+
const chunk = update.messageChunk;
|
|
633
|
+
if (chunk.textDelta) {
|
|
634
|
+
ctx.emit({
|
|
635
|
+
type: "model-output",
|
|
636
|
+
textDelta: chunk.textDelta
|
|
637
|
+
});
|
|
638
|
+
return { handled: true };
|
|
639
|
+
}
|
|
640
|
+
return { handled: false };
|
|
641
|
+
}
|
|
642
|
+
function handlePlanUpdate(update, ctx) {
|
|
643
|
+
if (!update.plan) {
|
|
644
|
+
return { handled: false };
|
|
645
|
+
}
|
|
646
|
+
ctx.emit({
|
|
647
|
+
type: "event",
|
|
648
|
+
name: "plan",
|
|
649
|
+
payload: update.plan
|
|
650
|
+
});
|
|
651
|
+
return { handled: true };
|
|
652
|
+
}
|
|
653
|
+
function handleThinkingUpdate(update, ctx) {
|
|
654
|
+
if (!update.thinking) {
|
|
655
|
+
return { handled: false };
|
|
656
|
+
}
|
|
657
|
+
ctx.emit({
|
|
658
|
+
type: "event",
|
|
659
|
+
name: "thinking",
|
|
660
|
+
payload: update.thinking
|
|
661
|
+
});
|
|
662
|
+
return { handled: true };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function buildAcpSpawnSpec(params) {
|
|
666
|
+
return {
|
|
667
|
+
command: params.command,
|
|
668
|
+
args: (params.args ?? []).map((arg) => String(arg)),
|
|
669
|
+
options: {
|
|
670
|
+
cwd: params.cwd,
|
|
671
|
+
env: params.env,
|
|
672
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
673
|
+
windowsHide: process.platform === "win32"
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const MAX_DROPPED_LINE_PREVIEW_CHARS = 4096;
|
|
679
|
+
const DEFAULT_ACP_MAX_MULTILINE_STDOUT_BYTES = 32 * 1024;
|
|
680
|
+
function formatDroppedLinePreview(line) {
|
|
681
|
+
if (line.length <= MAX_DROPPED_LINE_PREVIEW_CHARS) {
|
|
682
|
+
return line;
|
|
683
|
+
}
|
|
684
|
+
const headChars = 2048;
|
|
685
|
+
const tailChars = 1024;
|
|
686
|
+
const omittedChars = Math.max(0, line.length - headChars - tailChars);
|
|
687
|
+
return `${line.slice(0, headChars)}
|
|
688
|
+
...[stdout line truncated ${omittedChars} chars]...
|
|
689
|
+
${line.slice(-tailChars)}`;
|
|
690
|
+
}
|
|
691
|
+
function createAcpFilteredStdoutReadable(params) {
|
|
692
|
+
const maxMultilineBytes = typeof params.maxMultilineBytes === "number" && Number.isFinite(params.maxMultilineBytes) && params.maxMultilineBytes > 0 ? Math.trunc(params.maxMultilineBytes) : DEFAULT_ACP_MAX_MULTILINE_STDOUT_BYTES;
|
|
693
|
+
const encoder = new TextEncoder();
|
|
694
|
+
return new ReadableStream({
|
|
695
|
+
async start(controller) {
|
|
696
|
+
const reader = params.readable.getReader();
|
|
697
|
+
let buffer = Buffer.alloc(0);
|
|
698
|
+
let multiline = null;
|
|
699
|
+
let discardingOversizedLine = false;
|
|
700
|
+
let controllerErrored = false;
|
|
701
|
+
const drop = (reason, line) => {
|
|
702
|
+
params.onDroppedLine?.({ reason, line: formatDroppedLinePreview(line) });
|
|
703
|
+
};
|
|
704
|
+
const enqueueLine = (line) => {
|
|
705
|
+
if (!line.trim()) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const filtered = params.transport.filterStdoutLine?.(line);
|
|
709
|
+
if (filtered === void 0) {
|
|
710
|
+
controller.enqueue(encoder.encode(`${line}
|
|
711
|
+
`));
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (filtered === null) {
|
|
715
|
+
drop("transport_filter_null", line);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
controller.enqueue(encoder.encode(`${filtered}
|
|
719
|
+
`));
|
|
720
|
+
};
|
|
721
|
+
const tryFlushMultiline = (candidate) => {
|
|
722
|
+
const trimmed = candidate.trim();
|
|
723
|
+
if (!trimmed) {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
const parsed = JSON.parse(trimmed);
|
|
731
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
enqueueLine(JSON.stringify(parsed));
|
|
735
|
+
return true;
|
|
736
|
+
} catch {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
const consumeLine = (line, lineBytes) => {
|
|
741
|
+
if (multiline) {
|
|
742
|
+
const nextBuf = multiline.buf.length > 0 ? `${multiline.buf}
|
|
743
|
+
${line}` : line;
|
|
744
|
+
const nextBytes = multiline.bytes + lineBytes + 1;
|
|
745
|
+
if (nextBytes > maxMultilineBytes) {
|
|
746
|
+
drop("multiline_overflow", multiline.buf);
|
|
747
|
+
multiline = null;
|
|
748
|
+
if (lineBytes > maxMultilineBytes) {
|
|
749
|
+
drop("multiline_overflow", line);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
enqueueLine(line);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
multiline = { buf: nextBuf, bytes: nextBytes };
|
|
756
|
+
if (tryFlushMultiline(multiline.buf)) {
|
|
757
|
+
multiline = null;
|
|
758
|
+
}
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (lineBytes > maxMultilineBytes) {
|
|
762
|
+
drop("multiline_overflow", line);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const trimmed = line.trim();
|
|
766
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
767
|
+
try {
|
|
768
|
+
const parsed = JSON.parse(trimmed);
|
|
769
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
770
|
+
enqueueLine(line);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
} catch {
|
|
774
|
+
multiline = { buf: line, bytes: lineBytes };
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
enqueueLine(line);
|
|
779
|
+
};
|
|
780
|
+
try {
|
|
781
|
+
while (true) {
|
|
782
|
+
const { value, done } = await reader.read();
|
|
783
|
+
if (done) {
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
if (!value) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
let chunkBuffer = Buffer.from(value);
|
|
790
|
+
if (discardingOversizedLine) {
|
|
791
|
+
const newlineIndex2 = chunkBuffer.indexOf(10);
|
|
792
|
+
if (newlineIndex2 === -1) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
chunkBuffer = chunkBuffer.subarray(newlineIndex2 + 1);
|
|
796
|
+
discardingOversizedLine = false;
|
|
797
|
+
}
|
|
798
|
+
if (chunkBuffer.length === 0) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
buffer = buffer.length === 0 ? chunkBuffer : Buffer.concat([buffer, chunkBuffer]);
|
|
802
|
+
let newlineIndex = buffer.indexOf(10);
|
|
803
|
+
while (newlineIndex !== -1) {
|
|
804
|
+
const rawLine = buffer.subarray(0, newlineIndex);
|
|
805
|
+
buffer = buffer.subarray(newlineIndex + 1);
|
|
806
|
+
const lineBuffer = rawLine.at(-1) === 13 ? rawLine.subarray(0, rawLine.length - 1) : rawLine;
|
|
807
|
+
const line = lineBuffer.toString("utf8");
|
|
808
|
+
consumeLine(line, lineBuffer.length);
|
|
809
|
+
newlineIndex = buffer.indexOf(10);
|
|
810
|
+
}
|
|
811
|
+
if (buffer.length > maxMultilineBytes) {
|
|
812
|
+
drop("multiline_overflow", buffer.toString("utf8"));
|
|
813
|
+
buffer = Buffer.alloc(0);
|
|
814
|
+
discardingOversizedLine = true;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (buffer.length > 0) {
|
|
818
|
+
const trailingLine = buffer.toString("utf8");
|
|
819
|
+
const pendingMultiline = multiline;
|
|
820
|
+
if (pendingMultiline) {
|
|
821
|
+
const nextBuf = pendingMultiline.buf.length > 0 ? `${pendingMultiline.buf}
|
|
822
|
+
${trailingLine}` : trailingLine;
|
|
823
|
+
if (!tryFlushMultiline(nextBuf)) {
|
|
824
|
+
drop("multiline_incomplete", nextBuf);
|
|
825
|
+
}
|
|
826
|
+
multiline = null;
|
|
827
|
+
} else if (trailingLine.trim()) {
|
|
828
|
+
drop("multiline_incomplete", trailingLine);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const remainingMultiline = multiline;
|
|
832
|
+
if (remainingMultiline) {
|
|
833
|
+
if (!tryFlushMultiline(remainingMultiline.buf)) {
|
|
834
|
+
drop("multiline_incomplete", remainingMultiline.buf);
|
|
835
|
+
}
|
|
836
|
+
multiline = null;
|
|
837
|
+
}
|
|
838
|
+
} catch (error) {
|
|
839
|
+
controllerErrored = true;
|
|
840
|
+
controller.error(error);
|
|
841
|
+
} finally {
|
|
842
|
+
reader.releaseLock();
|
|
843
|
+
try {
|
|
844
|
+
params.onDone?.();
|
|
845
|
+
} catch {
|
|
846
|
+
}
|
|
847
|
+
if (!controllerErrored) {
|
|
848
|
+
controller.close();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function isAlive(pid) {
|
|
856
|
+
try {
|
|
857
|
+
process.kill(pid, 0);
|
|
858
|
+
return true;
|
|
859
|
+
} catch {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
async function resolveDescendantPids(rootPid) {
|
|
864
|
+
const processes = await psList();
|
|
865
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
866
|
+
for (const proc of processes) {
|
|
867
|
+
if (typeof proc.pid !== "number" || typeof proc.ppid !== "number") {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
const children = childrenByParent.get(proc.ppid) ?? [];
|
|
871
|
+
children.push(proc.pid);
|
|
872
|
+
childrenByParent.set(proc.ppid, children);
|
|
873
|
+
}
|
|
874
|
+
const descendants = [];
|
|
875
|
+
const seen = /* @__PURE__ */ new Set();
|
|
876
|
+
const visit = (pid) => {
|
|
877
|
+
for (const childPid of childrenByParent.get(pid) ?? []) {
|
|
878
|
+
if (seen.has(childPid)) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
seen.add(childPid);
|
|
882
|
+
visit(childPid);
|
|
883
|
+
descendants.push(childPid);
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
visit(rootPid);
|
|
887
|
+
return descendants;
|
|
888
|
+
}
|
|
889
|
+
function bestEffortKillPid(pid, signal) {
|
|
890
|
+
try {
|
|
891
|
+
process.kill(pid, signal);
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
async function waitForAllGone(pids, timeoutMs) {
|
|
896
|
+
const start = Date.now();
|
|
897
|
+
while (Date.now() - start < timeoutMs) {
|
|
898
|
+
if (pids.every((pid) => !isAlive(pid))) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
async function killProcessTree(proc, options) {
|
|
905
|
+
const pid = proc.pid;
|
|
906
|
+
if (!pid) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const graceMs = Math.max(1, options?.graceMs ?? 1e3);
|
|
910
|
+
const descendants = await resolveDescendantPids(pid).catch(() => []);
|
|
911
|
+
const allPids = [...descendants, pid];
|
|
912
|
+
for (const targetPid of allPids) {
|
|
913
|
+
bestEffortKillPid(targetPid, "SIGTERM");
|
|
914
|
+
}
|
|
915
|
+
await waitForAllGone(allPids, graceMs);
|
|
916
|
+
const remaining = allPids.filter((targetPid) => isAlive(targetPid));
|
|
917
|
+
if (remaining.length === 0) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
for (const targetPid of remaining) {
|
|
921
|
+
bestEffortKillPid(targetPid, "SIGKILL");
|
|
922
|
+
}
|
|
923
|
+
await waitForAllGone(remaining, Math.min(250, graceMs));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const RETRY_CONFIG = {
|
|
927
|
+
/** Maximum number of retry attempts for init/newSession */
|
|
928
|
+
maxAttempts: 3,
|
|
929
|
+
/** Base delay between retries in ms */
|
|
930
|
+
baseDelayMs: 1e3,
|
|
931
|
+
/** Maximum delay between retries in ms */
|
|
932
|
+
maxDelayMs: 5e3
|
|
933
|
+
};
|
|
934
|
+
const DEFAULT_POST_PROMPT_NO_UPDATES_TIMEOUT_MS = 2 * 6e4;
|
|
935
|
+
const MAX_DROPPED_STDOUT_LINE_SAMPLES = 5;
|
|
936
|
+
const HAPPY_ACP_MAX_MULTILINE_STDOUT_BYTES_ENV = "HAPPY_ACP_MAX_MULTILINE_STDOUT_BYTES";
|
|
937
|
+
const HAPPIER_ACP_MAX_MULTILINE_STDOUT_BYTES_ENV = "HAPPIER_ACP_MAX_MULTILINE_STDOUT_BYTES";
|
|
938
|
+
function readPositiveIntegerEnv(name) {
|
|
939
|
+
const raw = typeof process.env[name] === "string" ? process.env[name].trim() : "";
|
|
940
|
+
if (!raw) {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
const value = Number(raw);
|
|
944
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
return value;
|
|
948
|
+
}
|
|
949
|
+
function resolvePostPromptNoUpdatesTimeoutMs(transport) {
|
|
950
|
+
const envValue = readPositiveIntegerEnv("HAPPY_ACP_POST_PROMPT_NO_UPDATES_TIMEOUT_MS") ?? readPositiveIntegerEnv("HAPPIER_ACP_POST_PROMPT_NO_UPDATES_TIMEOUT_MS");
|
|
951
|
+
if (envValue != null) {
|
|
952
|
+
return envValue;
|
|
953
|
+
}
|
|
954
|
+
const transportValue = transport.getPostPromptNoUpdatesTimeoutMs?.();
|
|
955
|
+
if (typeof transportValue === "number" && Number.isFinite(transportValue) && transportValue > 0) {
|
|
956
|
+
return Math.trunc(transportValue);
|
|
957
|
+
}
|
|
958
|
+
return DEFAULT_POST_PROMPT_NO_UPDATES_TIMEOUT_MS;
|
|
959
|
+
}
|
|
960
|
+
function resolveAcpMaxMultilineStdoutBytes() {
|
|
961
|
+
const envValue = readPositiveIntegerEnv(HAPPY_ACP_MAX_MULTILINE_STDOUT_BYTES_ENV) ?? readPositiveIntegerEnv(HAPPIER_ACP_MAX_MULTILINE_STDOUT_BYTES_ENV);
|
|
962
|
+
return envValue ?? DEFAULT_ACP_MAX_MULTILINE_STDOUT_BYTES;
|
|
963
|
+
}
|
|
964
|
+
function formatAcpStdoutByteLimit(bytes) {
|
|
965
|
+
if (bytes >= 1024 * 1024) {
|
|
966
|
+
const value = bytes / (1024 * 1024);
|
|
967
|
+
return `${Number.isInteger(value) ? value : value.toFixed(1)} MiB`;
|
|
968
|
+
}
|
|
969
|
+
if (bytes >= 1024) {
|
|
970
|
+
const value = bytes / 1024;
|
|
971
|
+
return `${Number.isInteger(value) ? value : value.toFixed(1)} KiB`;
|
|
972
|
+
}
|
|
973
|
+
return `${bytes} bytes`;
|
|
974
|
+
}
|
|
975
|
+
function buildMultilineOverflowStatusDetail(maxBytes) {
|
|
976
|
+
const formattedLimit = formatAcpStdoutByteLimit(maxBytes);
|
|
977
|
+
return `Suppressed oversized agent output (>${formattedLimit} / ${maxBytes} bytes per ACP stdout record). Full output stays hidden by default. If you need larger payloads, set ${HAPPY_ACP_MAX_MULTILINE_STDOUT_BYTES_ENV}.`;
|
|
978
|
+
}
|
|
979
|
+
function getSessionUpdates(params) {
|
|
980
|
+
const notification = params;
|
|
981
|
+
const updates = Array.isArray(notification.updates) ? notification.updates.filter((update) => Boolean(update && typeof update === "object")) : [];
|
|
982
|
+
if (updates.length > 0) {
|
|
983
|
+
return updates;
|
|
984
|
+
}
|
|
985
|
+
if (notification.update && typeof notification.update === "object") {
|
|
986
|
+
return [notification.update];
|
|
987
|
+
}
|
|
988
|
+
return [];
|
|
989
|
+
}
|
|
990
|
+
function asNonNegativeFiniteNumber(value) {
|
|
991
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
992
|
+
}
|
|
993
|
+
function shouldRefreshProgressForToolCallUpdate(update, wasToolCallActive, isToolCallActive, hadVisibleProgress = false) {
|
|
994
|
+
if (!wasToolCallActive && isToolCallActive) {
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
if (update.status === "completed" || update.status === "failed" || update.status === "cancelled") {
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
return hadVisibleProgress || hasVisibleToolCallProgress(update.content);
|
|
1001
|
+
}
|
|
1002
|
+
function extractUsageTokens(record) {
|
|
1003
|
+
const used = asNonNegativeFiniteNumber(record.used);
|
|
1004
|
+
const size = asNonNegativeFiniteNumber(record.size);
|
|
1005
|
+
if (used != null || size != null) {
|
|
1006
|
+
const tokens2 = {
|
|
1007
|
+
total: used ?? 0
|
|
1008
|
+
};
|
|
1009
|
+
if (used != null) tokens2.used = used;
|
|
1010
|
+
if (size != null) tokens2.size = size;
|
|
1011
|
+
return tokens2;
|
|
1012
|
+
}
|
|
1013
|
+
const input = asNonNegativeFiniteNumber(record.input_tokens) ?? asNonNegativeFiniteNumber(record.inputTokens) ?? asNonNegativeFiniteNumber(record.prompt_tokens) ?? asNonNegativeFiniteNumber(record.promptTokens);
|
|
1014
|
+
const output = asNonNegativeFiniteNumber(record.output_tokens) ?? asNonNegativeFiniteNumber(record.outputTokens) ?? asNonNegativeFiniteNumber(record.completion_tokens) ?? asNonNegativeFiniteNumber(record.completionTokens);
|
|
1015
|
+
const cacheRead = asNonNegativeFiniteNumber(record.cache_read_input_tokens) ?? asNonNegativeFiniteNumber(record.cacheReadInputTokens) ?? asNonNegativeFiniteNumber(record.cache_read_tokens) ?? asNonNegativeFiniteNumber(record.cacheReadTokens) ?? asNonNegativeFiniteNumber(record.cached_read_tokens) ?? asNonNegativeFiniteNumber(record.cachedReadTokens);
|
|
1016
|
+
const cacheCreation = asNonNegativeFiniteNumber(record.cache_creation_input_tokens) ?? asNonNegativeFiniteNumber(record.cacheCreationInputTokens) ?? asNonNegativeFiniteNumber(record.cache_creation_tokens) ?? asNonNegativeFiniteNumber(record.cacheCreationTokens) ?? asNonNegativeFiniteNumber(record.cached_write_tokens) ?? asNonNegativeFiniteNumber(record.cachedWriteTokens);
|
|
1017
|
+
const thought = asNonNegativeFiniteNumber(record.thought_tokens) ?? asNonNegativeFiniteNumber(record.thoughtTokens);
|
|
1018
|
+
const totalFromPayload = asNonNegativeFiniteNumber(record.total_tokens) ?? asNonNegativeFiniteNumber(record.totalTokens);
|
|
1019
|
+
const anyPresent = totalFromPayload != null || input != null || output != null || cacheRead != null || cacheCreation != null || thought != null;
|
|
1020
|
+
if (!anyPresent) {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
const total = totalFromPayload ?? (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheCreation ?? 0) + (thought ?? 0);
|
|
1024
|
+
const tokens = { total };
|
|
1025
|
+
if (input != null) tokens.input = input;
|
|
1026
|
+
if (output != null) tokens.output = output;
|
|
1027
|
+
if (cacheRead != null) tokens.cache_read = cacheRead;
|
|
1028
|
+
if (cacheCreation != null) tokens.cache_creation = cacheCreation;
|
|
1029
|
+
if (thought != null) tokens.thought = thought;
|
|
1030
|
+
return tokens;
|
|
1031
|
+
}
|
|
1032
|
+
function isApprovalOption(option) {
|
|
1033
|
+
if (option.optionId === "proceed_once" || option.optionId === "proceed_always" || option.optionId === "cancel") {
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
const searchable = `${option.name ?? ""} ${option.label ?? ""}`.toLowerCase();
|
|
1037
|
+
return searchable.includes("once") || searchable.includes("always") || searchable.includes("cancel");
|
|
1038
|
+
}
|
|
1039
|
+
function isSelectionPermissionRequest(params) {
|
|
1040
|
+
if (Array.isArray(params.codex_command) && params.codex_command.length > 0) {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
if (params.requestedSchema?.properties?.optionId) {
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
const options = Array.isArray(params.options) ? params.options : [];
|
|
1047
|
+
if (options.length === 0) {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
return !options.every((option) => isApprovalOption(option));
|
|
1051
|
+
}
|
|
1052
|
+
function appendDroppedStdoutLineSummary(summary, entry) {
|
|
1053
|
+
summary.count += 1;
|
|
1054
|
+
if (summary.samples.length < MAX_DROPPED_STDOUT_LINE_SAMPLES) {
|
|
1055
|
+
summary.samples.push(entry);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function nodeToWebStreams(stdin, stdout) {
|
|
1059
|
+
const isIgnorableStdinPipeError = (error) => {
|
|
1060
|
+
const code = typeof error === "object" && error !== null ? error.code : void 0;
|
|
1061
|
+
return code === "ERR_STREAM_DESTROYED" || code === "EPIPE";
|
|
1062
|
+
};
|
|
1063
|
+
stdin.on("error", (err) => {
|
|
1064
|
+
if (isIgnorableStdinPipeError(err)) {
|
|
1065
|
+
logger.debug("[AcpBackend] Ignoring stdin pipe error:", err);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
logger.debug("[AcpBackend] Stdin error:", err);
|
|
1069
|
+
});
|
|
1070
|
+
const writable = new WritableStream({
|
|
1071
|
+
write(chunk) {
|
|
1072
|
+
return new Promise((resolve, reject) => {
|
|
1073
|
+
if (stdin.destroyed) {
|
|
1074
|
+
resolve();
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const ok = stdin.write(chunk, (err) => {
|
|
1078
|
+
if (err) {
|
|
1079
|
+
if (isIgnorableStdinPipeError(err)) {
|
|
1080
|
+
resolve();
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
logger.debug(`[AcpBackend] Error writing to stdin:`, err);
|
|
1084
|
+
reject(err);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
resolve();
|
|
1088
|
+
});
|
|
1089
|
+
if (!ok) {
|
|
1090
|
+
stdin.once("drain", resolve);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
},
|
|
1094
|
+
close() {
|
|
1095
|
+
return new Promise((resolve) => {
|
|
1096
|
+
if (stdin.destroyed) {
|
|
1097
|
+
resolve();
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
let settled = false;
|
|
1101
|
+
const settle = () => {
|
|
1102
|
+
if (settled) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
settled = true;
|
|
1106
|
+
stdin.off("close", settle);
|
|
1107
|
+
stdin.off("finish", settle);
|
|
1108
|
+
stdin.off("error", handleError);
|
|
1109
|
+
resolve();
|
|
1110
|
+
};
|
|
1111
|
+
const handleError = (err) => {
|
|
1112
|
+
if (!isIgnorableStdinPipeError(err)) {
|
|
1113
|
+
logger.debug("[AcpBackend] Error ending stdin:", err);
|
|
1114
|
+
}
|
|
1115
|
+
settle();
|
|
1116
|
+
};
|
|
1117
|
+
stdin.once("close", settle);
|
|
1118
|
+
stdin.once("finish", settle);
|
|
1119
|
+
stdin.once("error", handleError);
|
|
1120
|
+
try {
|
|
1121
|
+
stdin.end(() => settle());
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
if (!isIgnorableStdinPipeError(error)) {
|
|
1124
|
+
logger.debug("[AcpBackend] Error closing stdin:", error);
|
|
1125
|
+
}
|
|
1126
|
+
settle();
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
},
|
|
1130
|
+
abort(reason) {
|
|
1131
|
+
stdin.destroy(reason instanceof Error ? reason : new Error(String(reason)));
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
const readable = new ReadableStream({
|
|
1135
|
+
start(controller) {
|
|
1136
|
+
stdout.on("data", (chunk) => {
|
|
1137
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
1138
|
+
});
|
|
1139
|
+
stdout.on("end", () => {
|
|
1140
|
+
controller.close();
|
|
1141
|
+
});
|
|
1142
|
+
stdout.on("error", (err) => {
|
|
1143
|
+
logger.debug(`[AcpBackend] Stdout error:`, err);
|
|
1144
|
+
controller.error(err);
|
|
1145
|
+
});
|
|
1146
|
+
},
|
|
1147
|
+
cancel() {
|
|
1148
|
+
stdout.destroy();
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
return { writable, readable };
|
|
1152
|
+
}
|
|
1153
|
+
async function withRetry(operation, options) {
|
|
1154
|
+
let lastError = null;
|
|
1155
|
+
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
|
|
1156
|
+
try {
|
|
1157
|
+
return await operation();
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
lastError = normalizeAcpError(error);
|
|
1160
|
+
const retryable = options.shouldRetry?.(lastError) ?? true;
|
|
1161
|
+
if (!retryable || attempt >= options.maxAttempts) {
|
|
1162
|
+
throw lastError;
|
|
1163
|
+
}
|
|
1164
|
+
const delayMs = Math.min(
|
|
1165
|
+
options.baseDelayMs * Math.pow(2, attempt - 1),
|
|
1166
|
+
options.maxDelayMs
|
|
1167
|
+
);
|
|
1168
|
+
logger.debug(`[AcpBackend] ${options.operationName} failed (attempt ${attempt}/${options.maxAttempts}): ${lastError.message}. Retrying in ${delayMs}ms...`);
|
|
1169
|
+
options.onRetry?.(attempt, lastError);
|
|
1170
|
+
await delay(delayMs);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
throw lastError;
|
|
1174
|
+
}
|
|
1175
|
+
function formatAcpErrorMessage(error) {
|
|
1176
|
+
if (error instanceof Error && error.message && error.message !== "[object Object]") {
|
|
1177
|
+
return error.message;
|
|
1178
|
+
}
|
|
1179
|
+
if (typeof error === "object" && error !== null) {
|
|
1180
|
+
const record = error;
|
|
1181
|
+
const message = formatDisplayMessage(error).trim();
|
|
1182
|
+
const code = record.code;
|
|
1183
|
+
const status = record.status;
|
|
1184
|
+
const prefix = [
|
|
1185
|
+
code !== void 0 && code !== null ? `[code=${String(code)}]` : "",
|
|
1186
|
+
status !== void 0 && status !== null ? `[status=${String(status)}]` : ""
|
|
1187
|
+
].filter(Boolean).join(" ");
|
|
1188
|
+
if (message.length > 0) {
|
|
1189
|
+
return prefix ? `${prefix} ${message}` : message;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const fallback = formatDisplayMessage(error).trim();
|
|
1193
|
+
return fallback || "Unknown ACP error";
|
|
1194
|
+
}
|
|
1195
|
+
function normalizeAcpError(error) {
|
|
1196
|
+
if (error instanceof Error && error.message && error.message !== "[object Object]") {
|
|
1197
|
+
return error;
|
|
1198
|
+
}
|
|
1199
|
+
const normalized = new Error(formatAcpErrorMessage(error));
|
|
1200
|
+
normalized.name = error instanceof Error ? error.name : "Error";
|
|
1201
|
+
if (error && typeof error === "object") {
|
|
1202
|
+
const { message: _message, ...rest } = error;
|
|
1203
|
+
Object.assign(normalized, rest);
|
|
1204
|
+
}
|
|
1205
|
+
return normalized;
|
|
1206
|
+
}
|
|
1207
|
+
function getStatusErrorDetail(message) {
|
|
1208
|
+
if (message.type !== "status" || message.status !== "error") {
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
const detail = typeof message.detail === "string" ? message.detail.trim() : "";
|
|
1212
|
+
return detail || "Unknown ACP transport error";
|
|
1213
|
+
}
|
|
1214
|
+
function looksLikeDroppedStdoutError(text) {
|
|
1215
|
+
const lower = text.toLowerCase();
|
|
1216
|
+
return lower.startsWith("error") || lower.includes("error:") || lower.includes("exception") || lower.includes("traceback") || lower.includes("invalid request") || lower.includes("invalid_request") || lower.includes("unauthorized") || lower.includes("forbidden") || lower.includes("permission denied") || /\b(4\d\d|5\d\d)\b/.test(lower) && (lower.includes("http") || lower.includes("status") || lower.includes("request"));
|
|
1217
|
+
}
|
|
1218
|
+
function createAcpAbortError(message) {
|
|
1219
|
+
const error = new Error(message);
|
|
1220
|
+
error.name = "AbortError";
|
|
1221
|
+
return error;
|
|
1222
|
+
}
|
|
1223
|
+
function enrichAcpError(error, stderrExcerpt) {
|
|
1224
|
+
const normalized = normalizeAcpError(error);
|
|
1225
|
+
if (!stderrExcerpt.trim()) {
|
|
1226
|
+
return normalized;
|
|
1227
|
+
}
|
|
1228
|
+
const existingStderr = normalized.stderr;
|
|
1229
|
+
if (typeof existingStderr !== "string" || existingStderr.trim().length === 0) {
|
|
1230
|
+
Object.assign(normalized, { stderr: stderrExcerpt });
|
|
1231
|
+
}
|
|
1232
|
+
return normalized;
|
|
1233
|
+
}
|
|
1234
|
+
class AcpProcessStartupError extends Error {
|
|
1235
|
+
constructor(message, exitCode, signal, stderr) {
|
|
1236
|
+
super(message);
|
|
1237
|
+
this.exitCode = exitCode;
|
|
1238
|
+
this.signal = signal;
|
|
1239
|
+
this.stderr = stderr;
|
|
1240
|
+
this.name = "AcpProcessStartupError";
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
function createProcessStartupError(agentName, operationName, code, signal, stderrExcerpt) {
|
|
1244
|
+
const normalizedStderrExcerpt = stderrExcerpt?.trim() || void 0;
|
|
1245
|
+
if (code !== null) {
|
|
1246
|
+
return new AcpProcessStartupError(
|
|
1247
|
+
`${agentName} exited with code ${code} during ${operationName}`,
|
|
1248
|
+
code,
|
|
1249
|
+
signal,
|
|
1250
|
+
normalizedStderrExcerpt
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
if (signal) {
|
|
1254
|
+
return new AcpProcessStartupError(
|
|
1255
|
+
`${agentName} exited due to signal ${signal} during ${operationName}`,
|
|
1256
|
+
code,
|
|
1257
|
+
signal,
|
|
1258
|
+
normalizedStderrExcerpt
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
return new AcpProcessStartupError(
|
|
1262
|
+
`${agentName} exited unexpectedly during ${operationName}`,
|
|
1263
|
+
code,
|
|
1264
|
+
signal,
|
|
1265
|
+
normalizedStderrExcerpt
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
function isAcpConnectionClosedError(error) {
|
|
1269
|
+
return error.message.trim() === "ACP connection closed";
|
|
1270
|
+
}
|
|
1271
|
+
function createCurrentProcessExitError(childProcess, options) {
|
|
1272
|
+
if (childProcess.exitCode !== null || childProcess.signalCode !== null) {
|
|
1273
|
+
return createProcessStartupError(
|
|
1274
|
+
options.agentName,
|
|
1275
|
+
options.operationName,
|
|
1276
|
+
childProcess.exitCode,
|
|
1277
|
+
childProcess.signalCode ?? null,
|
|
1278
|
+
options.getStderrExcerpt?.()
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
return null;
|
|
1282
|
+
}
|
|
1283
|
+
async function raceWithProcessExit(childProcess, operationFactory, options) {
|
|
1284
|
+
if (childProcess.exitCode !== null || childProcess.signalCode !== null) {
|
|
1285
|
+
throw createProcessStartupError(
|
|
1286
|
+
options.agentName,
|
|
1287
|
+
options.operationName,
|
|
1288
|
+
childProcess.exitCode,
|
|
1289
|
+
childProcess.signalCode ?? null,
|
|
1290
|
+
options.getStderrExcerpt?.()
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
return await new Promise((resolve, reject) => {
|
|
1294
|
+
let settled = false;
|
|
1295
|
+
let closedConnectionFallbackTimeout = null;
|
|
1296
|
+
const cleanup = () => {
|
|
1297
|
+
if (closedConnectionFallbackTimeout) {
|
|
1298
|
+
clearTimeout(closedConnectionFallbackTimeout);
|
|
1299
|
+
closedConnectionFallbackTimeout = null;
|
|
1300
|
+
}
|
|
1301
|
+
childProcess.off("error", handleError);
|
|
1302
|
+
childProcess.off("exit", handleExit);
|
|
1303
|
+
};
|
|
1304
|
+
const settleResolve = (value) => {
|
|
1305
|
+
if (settled) {
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
settled = true;
|
|
1309
|
+
cleanup();
|
|
1310
|
+
resolve(value);
|
|
1311
|
+
};
|
|
1312
|
+
const rejectWithNormalizedError = (error) => {
|
|
1313
|
+
if (settled) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
settled = true;
|
|
1317
|
+
cleanup();
|
|
1318
|
+
reject(error);
|
|
1319
|
+
};
|
|
1320
|
+
const settleReject = (error) => {
|
|
1321
|
+
const normalizedError = normalizeAcpError(error);
|
|
1322
|
+
const currentProcessExitError = createCurrentProcessExitError(childProcess, options);
|
|
1323
|
+
if (currentProcessExitError && isAcpConnectionClosedError(normalizedError)) {
|
|
1324
|
+
rejectWithNormalizedError(currentProcessExitError);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (!settled && !currentProcessExitError && !closedConnectionFallbackTimeout && isAcpConnectionClosedError(normalizedError)) {
|
|
1328
|
+
closedConnectionFallbackTimeout = setTimeout(() => {
|
|
1329
|
+
closedConnectionFallbackTimeout = null;
|
|
1330
|
+
rejectWithNormalizedError(normalizedError);
|
|
1331
|
+
}, 50);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
rejectWithNormalizedError(normalizedError);
|
|
1335
|
+
};
|
|
1336
|
+
const handleError = (error) => {
|
|
1337
|
+
settleReject(error);
|
|
1338
|
+
};
|
|
1339
|
+
const handleExit = (code, signal) => {
|
|
1340
|
+
settleReject(
|
|
1341
|
+
createProcessStartupError(
|
|
1342
|
+
options.agentName,
|
|
1343
|
+
options.operationName,
|
|
1344
|
+
code,
|
|
1345
|
+
signal,
|
|
1346
|
+
options.getStderrExcerpt?.()
|
|
1347
|
+
)
|
|
1348
|
+
);
|
|
1349
|
+
};
|
|
1350
|
+
childProcess.once("error", handleError);
|
|
1351
|
+
childProcess.once("exit", handleExit);
|
|
1352
|
+
let operation;
|
|
1353
|
+
try {
|
|
1354
|
+
operation = operationFactory();
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
settleReject(error);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
operation.then(settleResolve, settleReject);
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
class AcpBackend {
|
|
1363
|
+
constructor(options) {
|
|
1364
|
+
this.options = options;
|
|
1365
|
+
this.transport = options.transportHandler ?? new DefaultTransport(options.agentName);
|
|
1366
|
+
this.sessionPreferences = resolveAcpSessionPreferences({
|
|
1367
|
+
agentName: options.agentName,
|
|
1368
|
+
env: options.env
|
|
1369
|
+
});
|
|
1370
|
+
this.acpMaxMultilineStdoutBytes = resolveAcpMaxMultilineStdoutBytes();
|
|
1371
|
+
}
|
|
1372
|
+
listeners = [];
|
|
1373
|
+
process = null;
|
|
1374
|
+
connection = null;
|
|
1375
|
+
acpSessionId = null;
|
|
1376
|
+
disposed = false;
|
|
1377
|
+
/** Track active tool calls to prevent duplicate events */
|
|
1378
|
+
activeToolCalls = /* @__PURE__ */ new Set();
|
|
1379
|
+
toolCallTimeouts = /* @__PURE__ */ new Map();
|
|
1380
|
+
toolCallTimeoutSpecs = /* @__PURE__ */ new Map();
|
|
1381
|
+
/** Track tool call start times for performance monitoring */
|
|
1382
|
+
toolCallStartTimes = /* @__PURE__ */ new Map();
|
|
1383
|
+
/** Track streamed tool output between ACP updates and final completion */
|
|
1384
|
+
toolCallOutputs = /* @__PURE__ */ new Map();
|
|
1385
|
+
/** Pending permission requests that need response */
|
|
1386
|
+
pendingPermissions = /* @__PURE__ */ new Map();
|
|
1387
|
+
/** Map from permission request ID to real tool call ID for tracking */
|
|
1388
|
+
permissionToToolCallMap = /* @__PURE__ */ new Map();
|
|
1389
|
+
/** Map from real tool call ID to tool name for auto-approval */
|
|
1390
|
+
toolCallIdToNameMap = /* @__PURE__ */ new Map();
|
|
1391
|
+
/** Track tool calls count since last prompt (to identify first tool call) */
|
|
1392
|
+
toolCallCountSincePrompt = 0;
|
|
1393
|
+
/** Timeout for emitting 'idle' status after last message chunk */
|
|
1394
|
+
idleTimeout = null;
|
|
1395
|
+
/** Promise resolver for waitForResponseComplete */
|
|
1396
|
+
idleResolver = null;
|
|
1397
|
+
/** Promise rejecter for waitForResponseComplete */
|
|
1398
|
+
idleRejecter = null;
|
|
1399
|
+
/** Completion signal captured before waitForResponseComplete is attached */
|
|
1400
|
+
responseCompletionOutcome = null;
|
|
1401
|
+
/** Whether the current prompt is still waiting for completion */
|
|
1402
|
+
waitingForResponse = false;
|
|
1403
|
+
/** First fatal prompt-level error observed for the current turn */
|
|
1404
|
+
responseCompletionError = null;
|
|
1405
|
+
/** Resettable no-progress timeout while waiting for a turn to finish */
|
|
1406
|
+
responseWaitTimeout = null;
|
|
1407
|
+
/** Current inactivity threshold used by waitForResponseComplete */
|
|
1408
|
+
responseWaitTimeoutMs = null;
|
|
1409
|
+
/** Timestamp of the last meaningful response progress for the current turn */
|
|
1410
|
+
responseLastProgressAt = null;
|
|
1411
|
+
/** Fallback completion when prompt returns but the agent emits no session updates */
|
|
1412
|
+
postPromptCompletionIdleTimeout = null;
|
|
1413
|
+
/** Whether at least one session/update arrived after the current prompt */
|
|
1414
|
+
sawSessionUpdateSincePrompt = false;
|
|
1415
|
+
/** Transport handler for agent-specific behavior */
|
|
1416
|
+
transport;
|
|
1417
|
+
sessionPreferences;
|
|
1418
|
+
sessionConfigOptions = null;
|
|
1419
|
+
agentCapabilities = null;
|
|
1420
|
+
/** Keep a short rolling stderr buffer so startup failures can surface the real cause. */
|
|
1421
|
+
recentStderrLines = [];
|
|
1422
|
+
acpMaxMultilineStdoutBytes;
|
|
1423
|
+
oversizedStdoutNoticeEmittedForCurrentTurn = false;
|
|
1424
|
+
resourceCleanupDone = false;
|
|
1425
|
+
getProviderSessionId() {
|
|
1426
|
+
return this.acpSessionId;
|
|
1427
|
+
}
|
|
1428
|
+
recordRecentStderr(text) {
|
|
1429
|
+
const normalized = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1430
|
+
if (normalized.length === 0) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
this.recentStderrLines.push(...normalized);
|
|
1434
|
+
if (this.recentStderrLines.length > 12) {
|
|
1435
|
+
this.recentStderrLines = this.recentStderrLines.slice(-12);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
getRecentStderrExcerpt() {
|
|
1439
|
+
return this.recentStderrLines.slice(-6).join("\n");
|
|
1440
|
+
}
|
|
1441
|
+
getRecentStderrSummaryLine() {
|
|
1442
|
+
for (const line of this.recentStderrLines.slice(-6).reverse()) {
|
|
1443
|
+
if (/^note:\s+run with\b/i.test(line)) {
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
return line;
|
|
1447
|
+
}
|
|
1448
|
+
return this.recentStderrLines.at(-1);
|
|
1449
|
+
}
|
|
1450
|
+
async cleanupOwnedResources() {
|
|
1451
|
+
if (this.resourceCleanupDone) {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
this.resourceCleanupDone = true;
|
|
1455
|
+
try {
|
|
1456
|
+
await this.options.resourceCleanup?.();
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
logger.debug("[AcpBackend] Failed to clean up backend-owned resources:", error);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
updateSessionConfigOptions(configOptions) {
|
|
1462
|
+
this.sessionConfigOptions = Array.isArray(configOptions) ? configOptions : null;
|
|
1463
|
+
}
|
|
1464
|
+
supportsLoadSession() {
|
|
1465
|
+
const capabilities = this.agentCapabilities;
|
|
1466
|
+
return capabilities?.loadSession === true;
|
|
1467
|
+
}
|
|
1468
|
+
supportsResumeSession() {
|
|
1469
|
+
const capabilities = this.agentCapabilities;
|
|
1470
|
+
return capabilities?.sessionCapabilities?.resume != null;
|
|
1471
|
+
}
|
|
1472
|
+
syncSessionConfigOptionsFromUpdate(update) {
|
|
1473
|
+
const configOptions = update.configOptions;
|
|
1474
|
+
if (!Array.isArray(configOptions)) {
|
|
1475
|
+
return false;
|
|
1476
|
+
}
|
|
1477
|
+
this.updateSessionConfigOptions(configOptions);
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1480
|
+
async applySessionConfigPresets() {
|
|
1481
|
+
if (!this.connection || !this.acpSessionId) {
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const presetPlan = buildAcpSessionConfigPresetPlan({
|
|
1485
|
+
sessionId: this.acpSessionId,
|
|
1486
|
+
configOptions: this.sessionConfigOptions,
|
|
1487
|
+
preferences: this.sessionPreferences
|
|
1488
|
+
});
|
|
1489
|
+
if (presetPlan.length === 0) {
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
logger.debug(
|
|
1493
|
+
`[AcpBackend] Applying ${presetPlan.length} ACP session config preset(s) for ${this.sessionPreferences.agentName}`
|
|
1494
|
+
);
|
|
1495
|
+
for (const preset of presetPlan) {
|
|
1496
|
+
try {
|
|
1497
|
+
logger.debug(
|
|
1498
|
+
`[AcpBackend] Setting ACP session config ${preset.optionId} from ${String(preset.currentValue)} to ${String(preset.targetValue)}`
|
|
1499
|
+
);
|
|
1500
|
+
const response = await this.connection.setSessionConfigOption(preset.request);
|
|
1501
|
+
this.updateSessionConfigOptions(response.configOptions);
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
logger.warn(
|
|
1504
|
+
`[AcpBackend] Failed to apply ACP session config preset ${preset.optionId}:`,
|
|
1505
|
+
error
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
getPostPromptNoUpdatesTimeoutMs() {
|
|
1511
|
+
const explicitTimeout = readPositiveIntegerEnv("HAPPY_ACP_POST_PROMPT_NO_UPDATES_TIMEOUT_MS") ?? readPositiveIntegerEnv("HAPPIER_ACP_POST_PROMPT_NO_UPDATES_TIMEOUT_MS");
|
|
1512
|
+
if (explicitTimeout != null) {
|
|
1513
|
+
return explicitTimeout;
|
|
1514
|
+
}
|
|
1515
|
+
if (this.sessionPreferences.timeoutSource === "env") {
|
|
1516
|
+
return resolveAcpPostPromptNoUpdatesTimeoutMs(
|
|
1517
|
+
DEFAULT_POST_PROMPT_NO_UPDATES_TIMEOUT_MS,
|
|
1518
|
+
this.sessionPreferences.timeoutProfile
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
return resolvePostPromptNoUpdatesTimeoutMs(this.transport);
|
|
1522
|
+
}
|
|
1523
|
+
getResponseWaitTimeoutMs(timeoutMs) {
|
|
1524
|
+
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
1525
|
+
return Math.trunc(timeoutMs);
|
|
1526
|
+
}
|
|
1527
|
+
return readPositiveIntegerEnv("HAPPY_ACP_RESPONSE_WAIT_TIMEOUT_MS") ?? readPositiveIntegerEnv("HAPPIER_ACP_RESPONSE_WAIT_TIMEOUT_MS") ?? resolveAcpResponseWaitTimeoutMs(this.sessionPreferences.timeoutProfile);
|
|
1528
|
+
}
|
|
1529
|
+
createPromptCompletionWatchdog(timeoutMs) {
|
|
1530
|
+
let timeoutHandle = null;
|
|
1531
|
+
let cancelled = false;
|
|
1532
|
+
const promise = new Promise((resolve, reject) => {
|
|
1533
|
+
const schedule = () => {
|
|
1534
|
+
if (cancelled) {
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
if (this.responseCompletionError) {
|
|
1538
|
+
reject(this.responseCompletionError);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
if (!this.waitingForResponse) {
|
|
1542
|
+
resolve();
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
const lastProgressAt = this.responseLastProgressAt ?? Date.now();
|
|
1546
|
+
const elapsedMs = Math.max(0, Date.now() - lastProgressAt);
|
|
1547
|
+
const remainingMs = Math.max(1, timeoutMs - elapsedMs);
|
|
1548
|
+
timeoutHandle = setTimeout(() => {
|
|
1549
|
+
timeoutHandle = null;
|
|
1550
|
+
if (cancelled) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (this.responseCompletionError) {
|
|
1554
|
+
reject(this.responseCompletionError);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
if (!this.waitingForResponse) {
|
|
1558
|
+
resolve();
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
const latestProgressAt = this.responseLastProgressAt ?? Date.now();
|
|
1562
|
+
const latestElapsedMs = Math.max(0, Date.now() - latestProgressAt);
|
|
1563
|
+
if (latestElapsedMs < timeoutMs) {
|
|
1564
|
+
schedule();
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const timeoutLabel = formatToolCallTimeoutLimit(timeoutMs);
|
|
1568
|
+
const error = new Error(`Timeout waiting for Codex ACP prompt to complete after ${timeoutLabel} without response progress`);
|
|
1569
|
+
this.failPendingResponseWait(error);
|
|
1570
|
+
reject(error);
|
|
1571
|
+
}, remainingMs);
|
|
1572
|
+
};
|
|
1573
|
+
schedule();
|
|
1574
|
+
});
|
|
1575
|
+
return {
|
|
1576
|
+
promise,
|
|
1577
|
+
cancel: () => {
|
|
1578
|
+
cancelled = true;
|
|
1579
|
+
if (timeoutHandle) {
|
|
1580
|
+
clearTimeout(timeoutHandle);
|
|
1581
|
+
timeoutHandle = null;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
clearIdleTimeoutState() {
|
|
1587
|
+
if (this.idleTimeout) {
|
|
1588
|
+
clearTimeout(this.idleTimeout);
|
|
1589
|
+
this.idleTimeout = null;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
clearPostPromptCompletionIdleTimeout() {
|
|
1593
|
+
if (this.postPromptCompletionIdleTimeout) {
|
|
1594
|
+
clearTimeout(this.postPromptCompletionIdleTimeout);
|
|
1595
|
+
this.postPromptCompletionIdleTimeout = null;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
clearResponseWaitTimeout() {
|
|
1599
|
+
if (this.responseWaitTimeout) {
|
|
1600
|
+
clearTimeout(this.responseWaitTimeout);
|
|
1601
|
+
this.responseWaitTimeout = null;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
resetResponseWaitTracking() {
|
|
1605
|
+
this.clearResponseWaitTimeout();
|
|
1606
|
+
this.responseWaitTimeoutMs = null;
|
|
1607
|
+
this.responseLastProgressAt = null;
|
|
1608
|
+
}
|
|
1609
|
+
armResponseWaitTimeout(timeoutMs) {
|
|
1610
|
+
this.responseWaitTimeoutMs = timeoutMs;
|
|
1611
|
+
this.clearResponseWaitTimeout();
|
|
1612
|
+
const lastProgressAt = this.responseLastProgressAt ?? Date.now();
|
|
1613
|
+
const elapsedMs = Math.max(0, Date.now() - lastProgressAt);
|
|
1614
|
+
const remainingMs = Math.max(1, timeoutMs - elapsedMs);
|
|
1615
|
+
this.responseWaitTimeout = setTimeout(() => {
|
|
1616
|
+
this.responseWaitTimeout = null;
|
|
1617
|
+
this.responseWaitTimeoutMs = null;
|
|
1618
|
+
this.failPendingResponseWait(new Error("Timeout waiting for response to complete"));
|
|
1619
|
+
}, remainingMs);
|
|
1620
|
+
}
|
|
1621
|
+
scheduleToolCallTimeout(toolCallId) {
|
|
1622
|
+
const spec = this.toolCallTimeoutSpecs.get(toolCallId);
|
|
1623
|
+
if (!spec) {
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const existing = this.toolCallTimeouts.get(toolCallId);
|
|
1627
|
+
if (existing) {
|
|
1628
|
+
clearTimeout(existing);
|
|
1629
|
+
}
|
|
1630
|
+
const timeout = setTimeout(() => {
|
|
1631
|
+
const duration = formatDuration(this.toolCallStartTimes.get(toolCallId));
|
|
1632
|
+
const timeoutLabel = formatToolCallTimeoutLimit(spec.timeoutMs);
|
|
1633
|
+
const timeoutDetail = `Tool call ${spec.toolName} timed out after ${timeoutLabel}`;
|
|
1634
|
+
logger.debug(`[AcpBackend] \u23F1\uFE0F Tool call TIMEOUT (from ${spec.source}): ${toolCallId} (${spec.toolKind}) after ${timeoutLabel} without progress - Duration: ${duration}, failing current turn`);
|
|
1635
|
+
this.activeToolCalls.delete(toolCallId);
|
|
1636
|
+
this.toolCallStartTimes.delete(toolCallId);
|
|
1637
|
+
this.clearToolCallTimeout(toolCallId);
|
|
1638
|
+
this.clearIdleTimeoutState();
|
|
1639
|
+
const streamedOutput = renderToolOutput(this.toolCallOutputs.get(toolCallId));
|
|
1640
|
+
this.toolCallOutputs.delete(toolCallId);
|
|
1641
|
+
this.emit({
|
|
1642
|
+
type: "tool-result",
|
|
1643
|
+
toolName: spec.toolName,
|
|
1644
|
+
result: streamedOutput ? {
|
|
1645
|
+
stdout: streamedOutput,
|
|
1646
|
+
error: timeoutDetail,
|
|
1647
|
+
status: "failed",
|
|
1648
|
+
timedOut: true
|
|
1649
|
+
} : {
|
|
1650
|
+
error: timeoutDetail,
|
|
1651
|
+
status: "failed",
|
|
1652
|
+
timedOut: true
|
|
1653
|
+
},
|
|
1654
|
+
callId: toolCallId
|
|
1655
|
+
});
|
|
1656
|
+
this.emit({
|
|
1657
|
+
type: "status",
|
|
1658
|
+
status: "error",
|
|
1659
|
+
detail: timeoutDetail
|
|
1660
|
+
});
|
|
1661
|
+
this.failPendingResponseWait(new Error(timeoutDetail));
|
|
1662
|
+
}, spec.timeoutMs);
|
|
1663
|
+
this.toolCallTimeouts.set(toolCallId, timeout);
|
|
1664
|
+
}
|
|
1665
|
+
armToolCallTimeout(spec) {
|
|
1666
|
+
this.toolCallTimeoutSpecs.set(spec.toolCallId, spec);
|
|
1667
|
+
this.scheduleToolCallTimeout(spec.toolCallId);
|
|
1668
|
+
}
|
|
1669
|
+
clearToolCallTimeout(toolCallId) {
|
|
1670
|
+
const timeout = this.toolCallTimeouts.get(toolCallId);
|
|
1671
|
+
const hadTimeout = Boolean(timeout);
|
|
1672
|
+
if (timeout) {
|
|
1673
|
+
clearTimeout(timeout);
|
|
1674
|
+
this.toolCallTimeouts.delete(toolCallId);
|
|
1675
|
+
}
|
|
1676
|
+
this.toolCallTimeoutSpecs.delete(toolCallId);
|
|
1677
|
+
return hadTimeout;
|
|
1678
|
+
}
|
|
1679
|
+
finalizeDanglingToolCallsAfterTaskComplete() {
|
|
1680
|
+
if (this.activeToolCalls.size === 0) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
const danglingToolCallIds = Array.from(this.activeToolCalls);
|
|
1684
|
+
logger.debug(
|
|
1685
|
+
`[AcpBackend] task_complete arrived with ${danglingToolCallIds.length} active tool call(s); synthesizing completion from buffered output`,
|
|
1686
|
+
danglingToolCallIds
|
|
1687
|
+
);
|
|
1688
|
+
for (const toolCallId of danglingToolCallIds) {
|
|
1689
|
+
const toolName = this.toolCallIdToNameMap.get(toolCallId) || "unknown";
|
|
1690
|
+
const streamedOutput = renderToolOutput(this.toolCallOutputs.get(toolCallId));
|
|
1691
|
+
this.activeToolCalls.delete(toolCallId);
|
|
1692
|
+
this.toolCallStartTimes.delete(toolCallId);
|
|
1693
|
+
this.toolCallIdToNameMap.delete(toolCallId);
|
|
1694
|
+
this.toolCallOutputs.delete(toolCallId);
|
|
1695
|
+
this.clearToolCallTimeout(toolCallId);
|
|
1696
|
+
this.emit({
|
|
1697
|
+
type: "tool-result",
|
|
1698
|
+
toolName,
|
|
1699
|
+
result: streamedOutput,
|
|
1700
|
+
callId: toolCallId
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
refreshActiveToolCallTimeouts() {
|
|
1705
|
+
for (const toolCallId of this.activeToolCalls) {
|
|
1706
|
+
if (this.toolCallTimeoutSpecs.has(toolCallId)) {
|
|
1707
|
+
this.scheduleToolCallTimeout(toolCallId);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
markResponseProgress(opts = {}) {
|
|
1712
|
+
this.responseLastProgressAt = Date.now();
|
|
1713
|
+
if (opts.refreshToolTimeouts !== false) {
|
|
1714
|
+
this.refreshActiveToolCallTimeouts();
|
|
1715
|
+
}
|
|
1716
|
+
if (this.waitingForResponse && this.responseWaitTimeoutMs != null) {
|
|
1717
|
+
this.armResponseWaitTimeout(this.responseWaitTimeoutMs);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
clearToolCallTracking() {
|
|
1721
|
+
this.activeToolCalls.clear();
|
|
1722
|
+
for (const timeout of this.toolCallTimeouts.values()) {
|
|
1723
|
+
clearTimeout(timeout);
|
|
1724
|
+
}
|
|
1725
|
+
this.toolCallTimeouts.clear();
|
|
1726
|
+
this.toolCallTimeoutSpecs.clear();
|
|
1727
|
+
this.toolCallStartTimes.clear();
|
|
1728
|
+
this.toolCallIdToNameMap.clear();
|
|
1729
|
+
this.toolCallOutputs.clear();
|
|
1730
|
+
this.toolCallCountSincePrompt = 0;
|
|
1731
|
+
}
|
|
1732
|
+
resetResponseTrackingForNewPrompt() {
|
|
1733
|
+
this.responseCompletionOutcome = null;
|
|
1734
|
+
this.responseCompletionError = null;
|
|
1735
|
+
this.sawSessionUpdateSincePrompt = false;
|
|
1736
|
+
this.oversizedStdoutNoticeEmittedForCurrentTurn = false;
|
|
1737
|
+
this.resetResponseWaitTracking();
|
|
1738
|
+
this.clearIdleTimeoutState();
|
|
1739
|
+
this.clearPostPromptCompletionIdleTimeout();
|
|
1740
|
+
this.clearToolCallTracking();
|
|
1741
|
+
}
|
|
1742
|
+
failPendingResponseWait(error) {
|
|
1743
|
+
if (this.responseCompletionError) {
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
this.responseCompletionError = error;
|
|
1747
|
+
this.responseCompletionOutcome = null;
|
|
1748
|
+
this.waitingForResponse = false;
|
|
1749
|
+
this.resetResponseWaitTracking();
|
|
1750
|
+
this.clearPostPromptCompletionIdleTimeout();
|
|
1751
|
+
if (this.idleRejecter) {
|
|
1752
|
+
this.idleRejecter(error);
|
|
1753
|
+
}
|
|
1754
|
+
this.idleResolver = null;
|
|
1755
|
+
this.idleRejecter = null;
|
|
1756
|
+
}
|
|
1757
|
+
settleResponseWaiter(outcome) {
|
|
1758
|
+
const hasActiveWaiter = Boolean(this.idleResolver || this.idleRejecter);
|
|
1759
|
+
this.resetResponseWaitTracking();
|
|
1760
|
+
if (!this.waitingForResponse && !hasActiveWaiter) {
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (!hasActiveWaiter) {
|
|
1764
|
+
this.waitingForResponse = false;
|
|
1765
|
+
this.responseCompletionOutcome = outcome;
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
if (outcome.kind === "resolved") {
|
|
1769
|
+
this.idleResolver?.();
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
this.idleRejecter?.(outcome.error);
|
|
1773
|
+
}
|
|
1774
|
+
onMessage(handler) {
|
|
1775
|
+
this.listeners.push(handler);
|
|
1776
|
+
}
|
|
1777
|
+
offMessage(handler) {
|
|
1778
|
+
const index = this.listeners.indexOf(handler);
|
|
1779
|
+
if (index !== -1) {
|
|
1780
|
+
this.listeners.splice(index, 1);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
emit(msg) {
|
|
1784
|
+
if (this.disposed) return;
|
|
1785
|
+
for (const listener of this.listeners) {
|
|
1786
|
+
try {
|
|
1787
|
+
listener(msg);
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
logger.warn("[AcpBackend] Error in message handler:", error);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
handleDroppedStdoutLine(entry) {
|
|
1794
|
+
if (entry.reason === "multiline_overflow") {
|
|
1795
|
+
if (!this.waitingForResponse || this.responseCompletionError || this.oversizedStdoutNoticeEmittedForCurrentTurn) {
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
this.oversizedStdoutNoticeEmittedForCurrentTurn = true;
|
|
1799
|
+
this.emit({
|
|
1800
|
+
type: "status",
|
|
1801
|
+
status: "running",
|
|
1802
|
+
detail: buildMultilineOverflowStatusDetail(this.acpMaxMultilineStdoutBytes)
|
|
1803
|
+
});
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (entry.reason !== "transport_filter_null" || !this.waitingForResponse || this.responseCompletionError) {
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
const raw = entry.line.trim();
|
|
1810
|
+
if (!raw) {
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
const context = {
|
|
1814
|
+
activeToolCalls: this.activeToolCalls,
|
|
1815
|
+
hasActiveInvestigation: this.transport.isInvestigationTool ? Array.from(this.activeToolCalls).some((id) => this.transport.isInvestigationTool(id)) : false
|
|
1816
|
+
};
|
|
1817
|
+
const transportResult = this.transport.handleStderr?.(entry.line, context);
|
|
1818
|
+
const transportMessage = transportResult?.message ?? null;
|
|
1819
|
+
if (transportMessage) {
|
|
1820
|
+
this.emit(transportMessage);
|
|
1821
|
+
const errorDetail = getStatusErrorDetail(transportMessage);
|
|
1822
|
+
if (errorDetail) {
|
|
1823
|
+
this.failPendingResponseWait(new Error(errorDetail));
|
|
1824
|
+
}
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (!looksLikeDroppedStdoutError(raw)) {
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
this.emit({
|
|
1831
|
+
type: "status",
|
|
1832
|
+
status: "error",
|
|
1833
|
+
detail: raw
|
|
1834
|
+
});
|
|
1835
|
+
this.failPendingResponseWait(new Error(raw));
|
|
1836
|
+
}
|
|
1837
|
+
async startSession(initialPrompt) {
|
|
1838
|
+
if (this.disposed) {
|
|
1839
|
+
throw new Error("Backend has been disposed");
|
|
1840
|
+
}
|
|
1841
|
+
const sessionId = randomUUID();
|
|
1842
|
+
this.emit({ type: "status", status: "starting" });
|
|
1843
|
+
try {
|
|
1844
|
+
logger.debug(`[AcpBackend] Starting session: ${sessionId}`);
|
|
1845
|
+
this.recentStderrLines = [];
|
|
1846
|
+
const spawnSpec = buildAcpSpawnSpec({
|
|
1847
|
+
command: this.options.command,
|
|
1848
|
+
args: this.options.args ?? [],
|
|
1849
|
+
cwd: this.options.cwd,
|
|
1850
|
+
env: { ...process.env, ...this.options.env }
|
|
1851
|
+
});
|
|
1852
|
+
this.process = spawn(spawnSpec.command, spawnSpec.args, spawnSpec.options);
|
|
1853
|
+
if (this.process.stderr) {
|
|
1854
|
+
}
|
|
1855
|
+
if (!this.process.stdin || !this.process.stdout || !this.process.stderr) {
|
|
1856
|
+
throw new Error("Failed to create stdio pipes");
|
|
1857
|
+
}
|
|
1858
|
+
this.process.stderr.on("data", (data) => {
|
|
1859
|
+
const text = data.toString();
|
|
1860
|
+
if (!text.trim()) return;
|
|
1861
|
+
this.recordRecentStderr(text);
|
|
1862
|
+
const hasActiveInvestigation = this.transport.isInvestigationTool ? Array.from(this.activeToolCalls).some((id) => this.transport.isInvestigationTool(id)) : false;
|
|
1863
|
+
const context = {
|
|
1864
|
+
activeToolCalls: this.activeToolCalls,
|
|
1865
|
+
hasActiveInvestigation
|
|
1866
|
+
};
|
|
1867
|
+
if (hasActiveInvestigation) {
|
|
1868
|
+
logger.debug(`[AcpBackend] \u{1F50D} Agent stderr (during investigation): ${text.trim()}`);
|
|
1869
|
+
} else {
|
|
1870
|
+
logger.debug(`[AcpBackend] Agent stderr: ${text.trim()}`);
|
|
1871
|
+
}
|
|
1872
|
+
if (this.transport.handleStderr) {
|
|
1873
|
+
const result = this.transport.handleStderr(text, context);
|
|
1874
|
+
if (result.message) {
|
|
1875
|
+
this.emit(result.message);
|
|
1876
|
+
const errorDetail = getStatusErrorDetail(result.message);
|
|
1877
|
+
if (errorDetail && this.waitingForResponse) {
|
|
1878
|
+
this.failPendingResponseWait(new Error(errorDetail));
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
});
|
|
1883
|
+
this.process.on("error", (err) => {
|
|
1884
|
+
logger.debug(`[AcpBackend] Process error:`, err);
|
|
1885
|
+
if (this.waitingForResponse) {
|
|
1886
|
+
this.failPendingResponseWait(err);
|
|
1887
|
+
}
|
|
1888
|
+
this.emit({ type: "status", status: "error", detail: err.message });
|
|
1889
|
+
});
|
|
1890
|
+
this.process.on("exit", (code, signal) => {
|
|
1891
|
+
logger.debug(
|
|
1892
|
+
`[AcpBackend] Process exited with code ${code}, signal ${signal}, disposed=${this.disposed}, waitingForResponse=${this.waitingForResponse}`
|
|
1893
|
+
);
|
|
1894
|
+
const recentStderrExcerpt = this.getRecentStderrExcerpt();
|
|
1895
|
+
const recentStderrSummaryLine = this.getRecentStderrSummaryLine();
|
|
1896
|
+
if (recentStderrExcerpt) {
|
|
1897
|
+
logger.debug(`[AcpBackend] Recent stderr before exit:
|
|
1898
|
+
${recentStderrExcerpt}`);
|
|
1899
|
+
}
|
|
1900
|
+
if (this.disposed) {
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
if (this.waitingForResponse) {
|
|
1904
|
+
const detail = code !== null ? `ACP process exited with code ${code}${signal ? ` (${signal})` : ""}` : signal ? `ACP process exited with signal ${signal}` : "ACP process exited unexpectedly";
|
|
1905
|
+
this.failPendingResponseWait(new Error(detail));
|
|
1906
|
+
}
|
|
1907
|
+
if (code !== 0 && code !== null) {
|
|
1908
|
+
const detail = recentStderrSummaryLine ? `Exit code: ${code}
|
|
1909
|
+
Recent stderr: ${recentStderrSummaryLine}` : `Exit code: ${code}`;
|
|
1910
|
+
this.emit({ type: "status", status: "stopped", detail });
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
if (signal) {
|
|
1914
|
+
const detail = recentStderrSummaryLine ? `Signal: ${signal}
|
|
1915
|
+
Recent stderr: ${recentStderrSummaryLine}` : `Signal: ${signal}`;
|
|
1916
|
+
this.emit({ type: "status", status: "stopped", detail });
|
|
1917
|
+
}
|
|
1918
|
+
});
|
|
1919
|
+
const streams = nodeToWebStreams(
|
|
1920
|
+
this.process.stdin,
|
|
1921
|
+
this.process.stdout
|
|
1922
|
+
);
|
|
1923
|
+
const writable = streams.writable;
|
|
1924
|
+
const readable = streams.readable;
|
|
1925
|
+
const droppedStdoutSummary = {
|
|
1926
|
+
count: 0,
|
|
1927
|
+
samples: []
|
|
1928
|
+
};
|
|
1929
|
+
const filteredReadable = createAcpFilteredStdoutReadable({
|
|
1930
|
+
readable,
|
|
1931
|
+
transport: this.transport,
|
|
1932
|
+
onDroppedLine: (entry) => {
|
|
1933
|
+
appendDroppedStdoutLineSummary(droppedStdoutSummary, entry);
|
|
1934
|
+
this.handleDroppedStdoutLine(entry);
|
|
1935
|
+
},
|
|
1936
|
+
onDone: () => {
|
|
1937
|
+
if (droppedStdoutSummary.count > 0) {
|
|
1938
|
+
logger.debug(
|
|
1939
|
+
`[AcpBackend] Filtered out ${droppedStdoutSummary.count} stdout lines from ${this.transport.agentName}`,
|
|
1940
|
+
droppedStdoutSummary.samples
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
},
|
|
1944
|
+
maxMultilineBytes: this.acpMaxMultilineStdoutBytes
|
|
1945
|
+
});
|
|
1946
|
+
const stream = ndJsonStream(writable, filteredReadable);
|
|
1947
|
+
const client = {
|
|
1948
|
+
sessionUpdate: async (params) => {
|
|
1949
|
+
this.handleSessionUpdate(params);
|
|
1950
|
+
},
|
|
1951
|
+
requestPermission: async (params) => {
|
|
1952
|
+
const extendedParams = params;
|
|
1953
|
+
const toolCall = extendedParams.toolCall;
|
|
1954
|
+
let toolName = toolCall?.kind || toolCall?.toolName || extendedParams.kind || "Unknown tool";
|
|
1955
|
+
const toolCallId = toolCall?.id || randomUUID();
|
|
1956
|
+
const permissionId = toolCallId;
|
|
1957
|
+
let input = {};
|
|
1958
|
+
if (toolCall) {
|
|
1959
|
+
input = toolCall.input || toolCall.arguments || toolCall.content || {};
|
|
1960
|
+
} else {
|
|
1961
|
+
input = extendedParams.input || extendedParams.arguments || extendedParams.content || {};
|
|
1962
|
+
}
|
|
1963
|
+
const context = {
|
|
1964
|
+
toolCallCountSincePrompt: this.toolCallCountSincePrompt
|
|
1965
|
+
};
|
|
1966
|
+
toolName = this.transport.determineToolName?.(toolName, toolCallId, input, context) ?? toolName;
|
|
1967
|
+
if (toolName !== (toolCall?.kind || toolCall?.toolName || extendedParams.kind || "Unknown tool")) {
|
|
1968
|
+
logger.debug(`[AcpBackend] Detected tool name: ${toolName} from toolCallId: ${toolCallId}`);
|
|
1969
|
+
}
|
|
1970
|
+
this.toolCallCountSincePrompt++;
|
|
1971
|
+
const options = extendedParams.options || [];
|
|
1972
|
+
const isSelectionRequest = isSelectionPermissionRequest(extendedParams);
|
|
1973
|
+
logger.debug(`[AcpBackend] Permission request: tool=${toolName}, toolCallId=${toolCallId}, input=`, JSON.stringify(input));
|
|
1974
|
+
logger.debug(`[AcpBackend] Permission request params structure:`, JSON.stringify({
|
|
1975
|
+
hasToolCall: !!toolCall,
|
|
1976
|
+
toolCallKind: toolCall?.kind,
|
|
1977
|
+
toolCallId: toolCall?.id,
|
|
1978
|
+
paramsKind: extendedParams.kind,
|
|
1979
|
+
paramsKeys: Object.keys(params)
|
|
1980
|
+
}, null, 2));
|
|
1981
|
+
if (isSelectionRequest) {
|
|
1982
|
+
const selectionOptions = options.reduce((acc, option) => {
|
|
1983
|
+
if (!option.optionId) {
|
|
1984
|
+
return acc;
|
|
1985
|
+
}
|
|
1986
|
+
const displayOption = option;
|
|
1987
|
+
acc.push({
|
|
1988
|
+
optionId: option.optionId,
|
|
1989
|
+
label: displayOption.label || option.name || option.optionId,
|
|
1990
|
+
description: displayOption.description || option.name || option.optionId
|
|
1991
|
+
});
|
|
1992
|
+
return acc;
|
|
1993
|
+
}, []);
|
|
1994
|
+
if (selectionOptions.length === 0) {
|
|
1995
|
+
logger.debug("[AcpBackend] Selection request has no valid options, cancelling");
|
|
1996
|
+
return { outcome: { outcome: "selected", optionId: "cancel" } };
|
|
1997
|
+
}
|
|
1998
|
+
if (!this.options.selectionHandler) {
|
|
1999
|
+
logger.debug("[AcpBackend] No selection handler configured, cancelling selection request");
|
|
2000
|
+
return { outcome: { outcome: "selected", optionId: "cancel" } };
|
|
2001
|
+
}
|
|
2002
|
+
try {
|
|
2003
|
+
const selectionMessage = typeof extendedParams.message === "string" && extendedParams.message.trim().length > 0 ? extendedParams.message : formatDisplayMessage(input).trim() || toolName;
|
|
2004
|
+
const requestId = extendedParams.codex_event_id || extendedParams.codex_elicitation || toolCallId;
|
|
2005
|
+
const response = await this.options.selectionHandler.handleSelection({
|
|
2006
|
+
id: requestId,
|
|
2007
|
+
message: selectionMessage,
|
|
2008
|
+
options: selectionOptions,
|
|
2009
|
+
defaultOptionId: extendedParams.requestedSchema?.properties?.optionId?.default
|
|
2010
|
+
});
|
|
2011
|
+
return { outcome: { outcome: "selected", optionId: response.optionId } };
|
|
2012
|
+
} catch (error) {
|
|
2013
|
+
logger.debug("[AcpBackend] Error in selection handler:", error);
|
|
2014
|
+
return { outcome: { outcome: "selected", optionId: "cancel" } };
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
this.emit({
|
|
2018
|
+
type: "permission-request",
|
|
2019
|
+
id: permissionId,
|
|
2020
|
+
reason: toolName,
|
|
2021
|
+
payload: {
|
|
2022
|
+
...params,
|
|
2023
|
+
permissionId,
|
|
2024
|
+
toolCallId,
|
|
2025
|
+
toolName,
|
|
2026
|
+
input,
|
|
2027
|
+
options: options.map((opt) => ({
|
|
2028
|
+
id: opt.optionId,
|
|
2029
|
+
name: opt.name,
|
|
2030
|
+
kind: opt.kind
|
|
2031
|
+
}))
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
if (this.options.permissionHandler) {
|
|
2035
|
+
try {
|
|
2036
|
+
const result = await this.options.permissionHandler.handleToolCall(
|
|
2037
|
+
toolCallId,
|
|
2038
|
+
toolName,
|
|
2039
|
+
input
|
|
2040
|
+
);
|
|
2041
|
+
let optionId = "cancel";
|
|
2042
|
+
if (result.decision === "approved" || result.decision === "approved_for_session") {
|
|
2043
|
+
const proceedOnceOption2 = options.find(
|
|
2044
|
+
(opt) => opt.optionId === "proceed_once" || opt.name?.toLowerCase().includes("once")
|
|
2045
|
+
);
|
|
2046
|
+
const proceedAlwaysOption = options.find(
|
|
2047
|
+
(opt) => opt.optionId === "proceed_always" || opt.name?.toLowerCase().includes("always")
|
|
2048
|
+
);
|
|
2049
|
+
if (result.decision === "approved_for_session" && proceedAlwaysOption) {
|
|
2050
|
+
optionId = proceedAlwaysOption.optionId || "proceed_always";
|
|
2051
|
+
} else if (proceedOnceOption2) {
|
|
2052
|
+
optionId = proceedOnceOption2.optionId || "proceed_once";
|
|
2053
|
+
} else if (options.length > 0) {
|
|
2054
|
+
optionId = options[0].optionId || "proceed_once";
|
|
2055
|
+
}
|
|
2056
|
+
this.emit({
|
|
2057
|
+
type: "tool-result",
|
|
2058
|
+
toolName,
|
|
2059
|
+
result: { status: "approved", decision: result.decision },
|
|
2060
|
+
callId: permissionId
|
|
2061
|
+
});
|
|
2062
|
+
} else {
|
|
2063
|
+
const cancelOption = options.find(
|
|
2064
|
+
(opt) => opt.optionId === "cancel" || opt.name?.toLowerCase().includes("cancel")
|
|
2065
|
+
);
|
|
2066
|
+
if (cancelOption) {
|
|
2067
|
+
optionId = cancelOption.optionId || "cancel";
|
|
2068
|
+
}
|
|
2069
|
+
this.emit({
|
|
2070
|
+
type: "tool-result",
|
|
2071
|
+
toolName,
|
|
2072
|
+
result: { status: "denied", decision: result.decision },
|
|
2073
|
+
callId: permissionId
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
return { outcome: { outcome: "selected", optionId } };
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
logger.debug("[AcpBackend] Error in permission handler:", error);
|
|
2079
|
+
return { outcome: { outcome: "selected", optionId: "cancel" } };
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
const proceedOnceOption = options.find(
|
|
2083
|
+
(opt) => opt.optionId === "proceed_once" || typeof opt.name === "string" && opt.name.toLowerCase().includes("once")
|
|
2084
|
+
);
|
|
2085
|
+
const defaultOptionId = proceedOnceOption?.optionId || (options.length > 0 && options[0].optionId ? options[0].optionId : "proceed_once");
|
|
2086
|
+
return { outcome: { outcome: "selected", optionId: defaultOptionId } };
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
this.connection = new ClientSideConnection(
|
|
2090
|
+
(agent) => client,
|
|
2091
|
+
stream
|
|
2092
|
+
);
|
|
2093
|
+
const initRequest = {
|
|
2094
|
+
protocolVersion: 1,
|
|
2095
|
+
clientCapabilities: {
|
|
2096
|
+
_meta: {
|
|
2097
|
+
terminal_output: true
|
|
2098
|
+
},
|
|
2099
|
+
fs: {
|
|
2100
|
+
readTextFile: false,
|
|
2101
|
+
writeTextFile: false
|
|
2102
|
+
}
|
|
2103
|
+
},
|
|
2104
|
+
clientInfo: {
|
|
2105
|
+
name: "happy-cli",
|
|
2106
|
+
version: packageJson.version
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
const initTimeout = this.transport.getInitTimeout();
|
|
2110
|
+
const initDelayMs = this.transport.getInitDelayMs?.() ?? 0;
|
|
2111
|
+
if (initDelayMs > 0) {
|
|
2112
|
+
logger.debug(`[AcpBackend] Waiting ${initDelayMs}ms before initialize (${this.transport.agentName})...`);
|
|
2113
|
+
await delay(initDelayMs);
|
|
2114
|
+
}
|
|
2115
|
+
logger.debug(`[AcpBackend] Initializing connection (timeout: ${initTimeout}ms)...`);
|
|
2116
|
+
const initializeResponse = await withRetry(
|
|
2117
|
+
async () => {
|
|
2118
|
+
let timeoutHandle = null;
|
|
2119
|
+
try {
|
|
2120
|
+
const result = await raceWithProcessExit(
|
|
2121
|
+
this.process,
|
|
2122
|
+
() => Promise.race([
|
|
2123
|
+
this.connection.initialize(initRequest).then((res) => {
|
|
2124
|
+
if (timeoutHandle) {
|
|
2125
|
+
clearTimeout(timeoutHandle);
|
|
2126
|
+
timeoutHandle = null;
|
|
2127
|
+
}
|
|
2128
|
+
return res;
|
|
2129
|
+
}),
|
|
2130
|
+
new Promise((_, reject) => {
|
|
2131
|
+
timeoutHandle = setTimeout(() => {
|
|
2132
|
+
reject(new Error(`Initialize timeout after ${initTimeout}ms - ${this.transport.agentName} did not respond`));
|
|
2133
|
+
}, initTimeout);
|
|
2134
|
+
})
|
|
2135
|
+
]),
|
|
2136
|
+
{
|
|
2137
|
+
agentName: this.transport.agentName,
|
|
2138
|
+
operationName: "initialize",
|
|
2139
|
+
getStderrExcerpt: () => this.getRecentStderrExcerpt()
|
|
2140
|
+
}
|
|
2141
|
+
);
|
|
2142
|
+
return result;
|
|
2143
|
+
} finally {
|
|
2144
|
+
if (timeoutHandle) {
|
|
2145
|
+
clearTimeout(timeoutHandle);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
},
|
|
2149
|
+
{
|
|
2150
|
+
operationName: "Initialize",
|
|
2151
|
+
maxAttempts: RETRY_CONFIG.maxAttempts,
|
|
2152
|
+
baseDelayMs: RETRY_CONFIG.baseDelayMs,
|
|
2153
|
+
maxDelayMs: RETRY_CONFIG.maxDelayMs,
|
|
2154
|
+
shouldRetry: (error) => !(error instanceof AcpProcessStartupError)
|
|
2155
|
+
}
|
|
2156
|
+
);
|
|
2157
|
+
this.agentCapabilities = initializeResponse.agentCapabilities ?? null;
|
|
2158
|
+
logger.debug(`[AcpBackend] Initialize completed`);
|
|
2159
|
+
const mcpServers = this.options.mcpServers ? Object.entries(this.options.mcpServers).map(([name, config]) => ({
|
|
2160
|
+
name,
|
|
2161
|
+
command: config.command,
|
|
2162
|
+
args: config.args || [],
|
|
2163
|
+
env: config.env ? Object.entries(config.env).map(([envName, envValue]) => ({ name: envName, value: envValue })) : []
|
|
2164
|
+
})) : [];
|
|
2165
|
+
const newSessionRequest = {
|
|
2166
|
+
cwd: this.options.cwd,
|
|
2167
|
+
mcpServers
|
|
2168
|
+
};
|
|
2169
|
+
const requestedResumeSessionId = typeof this.options.resumeSessionId === "string" && this.options.resumeSessionId.trim() ? this.options.resumeSessionId.trim() : null;
|
|
2170
|
+
const sessionOperation = requestedResumeSessionId && this.supportsResumeSession() ? "resume" : requestedResumeSessionId && this.supportsLoadSession() ? "load" : "new";
|
|
2171
|
+
const sessionRequest = sessionOperation === "resume" ? {
|
|
2172
|
+
cwd: this.options.cwd,
|
|
2173
|
+
mcpServers,
|
|
2174
|
+
sessionId: requestedResumeSessionId
|
|
2175
|
+
} : sessionOperation === "load" ? {
|
|
2176
|
+
cwd: this.options.cwd,
|
|
2177
|
+
mcpServers,
|
|
2178
|
+
sessionId: requestedResumeSessionId
|
|
2179
|
+
} : newSessionRequest;
|
|
2180
|
+
if (requestedResumeSessionId && sessionOperation === "new") {
|
|
2181
|
+
throw new Error("ACP agent does not support session resume/load; refusing to fork the Codex conversation");
|
|
2182
|
+
}
|
|
2183
|
+
logger.debug(`[AcpBackend] ${sessionOperation === "new" ? "Creating new" : `${sessionOperation === "resume" ? "Resuming" : "Loading"} existing`} session...`);
|
|
2184
|
+
const sessionResponse = await withRetry(
|
|
2185
|
+
async () => {
|
|
2186
|
+
let timeoutHandle = null;
|
|
2187
|
+
try {
|
|
2188
|
+
const resumeSession = this.connection.resumeSession ?? this.connection.unstable_resumeSession;
|
|
2189
|
+
const result = await raceWithProcessExit(
|
|
2190
|
+
this.process,
|
|
2191
|
+
() => Promise.race([
|
|
2192
|
+
(sessionOperation === "resume" ? resumeSession ? resumeSession(sessionRequest).then((response) => ({
|
|
2193
|
+
...response,
|
|
2194
|
+
sessionId: requestedResumeSessionId
|
|
2195
|
+
})) : Promise.reject(new Error("ACP agent advertised session resume, but this SDK connection does not expose resumeSession.")) : sessionOperation === "load" ? this.connection.loadSession(sessionRequest) : this.connection.newSession(sessionRequest)).then((res) => {
|
|
2196
|
+
if (timeoutHandle) {
|
|
2197
|
+
clearTimeout(timeoutHandle);
|
|
2198
|
+
timeoutHandle = null;
|
|
2199
|
+
}
|
|
2200
|
+
return res;
|
|
2201
|
+
}),
|
|
2202
|
+
new Promise((_, reject) => {
|
|
2203
|
+
timeoutHandle = setTimeout(() => {
|
|
2204
|
+
reject(new Error(`New session timeout after ${initTimeout}ms - ${this.transport.agentName} did not respond`));
|
|
2205
|
+
}, initTimeout);
|
|
2206
|
+
})
|
|
2207
|
+
]),
|
|
2208
|
+
{
|
|
2209
|
+
agentName: this.transport.agentName,
|
|
2210
|
+
operationName: "new session",
|
|
2211
|
+
getStderrExcerpt: () => this.getRecentStderrExcerpt()
|
|
2212
|
+
}
|
|
2213
|
+
);
|
|
2214
|
+
return result;
|
|
2215
|
+
} finally {
|
|
2216
|
+
if (timeoutHandle) {
|
|
2217
|
+
clearTimeout(timeoutHandle);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
operationName: sessionOperation === "resume" ? "ResumeSession" : sessionOperation === "load" ? "LoadSession" : "NewSession",
|
|
2223
|
+
maxAttempts: RETRY_CONFIG.maxAttempts,
|
|
2224
|
+
baseDelayMs: RETRY_CONFIG.baseDelayMs,
|
|
2225
|
+
maxDelayMs: RETRY_CONFIG.maxDelayMs,
|
|
2226
|
+
shouldRetry: (error) => !(error instanceof AcpProcessStartupError)
|
|
2227
|
+
}
|
|
2228
|
+
);
|
|
2229
|
+
this.acpSessionId = requestedResumeSessionId ?? sessionResponse.sessionId;
|
|
2230
|
+
this.updateSessionConfigOptions(sessionResponse.configOptions);
|
|
2231
|
+
logger.debug(`[AcpBackend] Session created: ${this.acpSessionId}`);
|
|
2232
|
+
await this.applySessionConfigPresets();
|
|
2233
|
+
this.emitIdleStatus();
|
|
2234
|
+
if (initialPrompt) {
|
|
2235
|
+
this.sendPrompt(sessionId, initialPrompt).catch((error) => {
|
|
2236
|
+
logger.debug("[AcpBackend] Error sending initial prompt:", error);
|
|
2237
|
+
this.emit({ type: "status", status: "error", detail: formatAcpErrorMessage(error) });
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
return { sessionId };
|
|
2241
|
+
} catch (error) {
|
|
2242
|
+
const enrichedError = enrichAcpError(error, this.getRecentStderrExcerpt());
|
|
2243
|
+
if (this.process) {
|
|
2244
|
+
try {
|
|
2245
|
+
await killProcessTree(this.process, { graceMs: 250 });
|
|
2246
|
+
} catch {
|
|
2247
|
+
} finally {
|
|
2248
|
+
this.process = null;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
this.connection = null;
|
|
2252
|
+
this.acpSessionId = null;
|
|
2253
|
+
await this.cleanupOwnedResources();
|
|
2254
|
+
logger.debug("[AcpBackend] Error starting session:", enrichedError);
|
|
2255
|
+
this.emit({
|
|
2256
|
+
type: "status",
|
|
2257
|
+
status: "error",
|
|
2258
|
+
detail: formatAcpErrorMessage(enrichedError)
|
|
2259
|
+
});
|
|
2260
|
+
throw enrichedError;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Create handler context for session update processing
|
|
2265
|
+
*/
|
|
2266
|
+
createHandlerContext() {
|
|
2267
|
+
return {
|
|
2268
|
+
transport: this.transport,
|
|
2269
|
+
activeToolCalls: this.activeToolCalls,
|
|
2270
|
+
toolCallStartTimes: this.toolCallStartTimes,
|
|
2271
|
+
toolCallTimeouts: this.toolCallTimeouts,
|
|
2272
|
+
toolCallIdToNameMap: this.toolCallIdToNameMap,
|
|
2273
|
+
toolCallOutputs: this.toolCallOutputs,
|
|
2274
|
+
idleTimeout: this.idleTimeout,
|
|
2275
|
+
toolCallCountSincePrompt: this.toolCallCountSincePrompt,
|
|
2276
|
+
emit: (msg) => this.emit(msg),
|
|
2277
|
+
emitIdleStatus: () => this.emitIdleStatus(),
|
|
2278
|
+
failPendingResponseWait: (error) => this.failPendingResponseWait(error),
|
|
2279
|
+
armToolCallTimeout: (spec) => this.armToolCallTimeout(spec),
|
|
2280
|
+
clearToolCallTimeout: (toolCallId) => this.clearToolCallTimeout(toolCallId),
|
|
2281
|
+
clearIdleTimeout: () => {
|
|
2282
|
+
if (this.idleTimeout) {
|
|
2283
|
+
clearTimeout(this.idleTimeout);
|
|
2284
|
+
this.idleTimeout = null;
|
|
2285
|
+
}
|
|
2286
|
+
},
|
|
2287
|
+
setIdleTimeout: (callback, ms) => {
|
|
2288
|
+
this.idleTimeout = setTimeout(() => {
|
|
2289
|
+
callback();
|
|
2290
|
+
this.idleTimeout = null;
|
|
2291
|
+
}, ms);
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
emitUsageTelemetry(payload, source) {
|
|
2296
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
2297
|
+
return false;
|
|
2298
|
+
}
|
|
2299
|
+
const tokens = extractUsageTokens(payload);
|
|
2300
|
+
if (!tokens) {
|
|
2301
|
+
return false;
|
|
2302
|
+
}
|
|
2303
|
+
this.emit({
|
|
2304
|
+
type: "token-count",
|
|
2305
|
+
key: source,
|
|
2306
|
+
tokens,
|
|
2307
|
+
source
|
|
2308
|
+
});
|
|
2309
|
+
return true;
|
|
2310
|
+
}
|
|
2311
|
+
handleSessionUpdate(params) {
|
|
2312
|
+
const updates = getSessionUpdates(params);
|
|
2313
|
+
if (updates.length === 0) {
|
|
2314
|
+
logger.debug("[AcpBackend] Received session update without update field:", params);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
this.sawSessionUpdateSincePrompt = true;
|
|
2318
|
+
this.clearPostPromptCompletionIdleTimeout();
|
|
2319
|
+
for (const update of updates) {
|
|
2320
|
+
const sessionUpdateType = update.sessionUpdate;
|
|
2321
|
+
if (sessionUpdateType !== "agent_message_chunk" && sessionUpdateType !== "tool_call_update") {
|
|
2322
|
+
logger.debug(`[AcpBackend] Received session update: ${sessionUpdateType}`, JSON.stringify({
|
|
2323
|
+
sessionUpdate: sessionUpdateType,
|
|
2324
|
+
toolCallId: update.toolCallId,
|
|
2325
|
+
status: update.status,
|
|
2326
|
+
kind: update.kind,
|
|
2327
|
+
hasContent: !!update.content,
|
|
2328
|
+
hasLocations: !!update.locations
|
|
2329
|
+
}, null, 2));
|
|
2330
|
+
}
|
|
2331
|
+
const ctx = this.createHandlerContext();
|
|
2332
|
+
const toolCallId = update.toolCallId;
|
|
2333
|
+
const wasToolCallActive = typeof toolCallId === "string" && this.activeToolCalls.has(toolCallId);
|
|
2334
|
+
const supplementalToolProgress = extractSupplementalToolProgress(update);
|
|
2335
|
+
if (sessionUpdateType === "agent_message_chunk") {
|
|
2336
|
+
if (handleAgentMessageChunk(update, ctx).handled) {
|
|
2337
|
+
this.markResponseProgress();
|
|
2338
|
+
}
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
if (sessionUpdateType === "tool_call_update") {
|
|
2342
|
+
const result = handleToolCallUpdate(update, ctx, {
|
|
2343
|
+
supplementalOutputChunk: supplementalToolProgress && supplementalToolProgress.toolCallId === toolCallId ? supplementalToolProgress.outputChunk : null
|
|
2344
|
+
});
|
|
2345
|
+
if (result.toolCallCountSincePrompt !== void 0) {
|
|
2346
|
+
this.toolCallCountSincePrompt = result.toolCallCountSincePrompt;
|
|
2347
|
+
}
|
|
2348
|
+
const isToolCallActive = typeof toolCallId === "string" && this.activeToolCalls.has(toolCallId);
|
|
2349
|
+
if (shouldRefreshProgressForToolCallUpdate(
|
|
2350
|
+
update,
|
|
2351
|
+
wasToolCallActive,
|
|
2352
|
+
isToolCallActive,
|
|
2353
|
+
result.hadVisibleProgress
|
|
2354
|
+
)) {
|
|
2355
|
+
this.markResponseProgress();
|
|
2356
|
+
}
|
|
2357
|
+
continue;
|
|
2358
|
+
}
|
|
2359
|
+
if (sessionUpdateType === "agent_thought_chunk") {
|
|
2360
|
+
if (handleAgentThoughtChunk(update, ctx).handled) {
|
|
2361
|
+
this.markResponseProgress();
|
|
2362
|
+
}
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
if (sessionUpdateType === "tool_call") {
|
|
2366
|
+
const result = handleToolCall(update, ctx);
|
|
2367
|
+
const isToolCallActive = typeof toolCallId === "string" && this.activeToolCalls.has(toolCallId);
|
|
2368
|
+
if (result.handled && !wasToolCallActive && isToolCallActive) {
|
|
2369
|
+
this.markResponseProgress();
|
|
2370
|
+
}
|
|
2371
|
+
continue;
|
|
2372
|
+
}
|
|
2373
|
+
if (sessionUpdateType === "usage_update") {
|
|
2374
|
+
this.emitUsageTelemetry(update, "acp-usage-update");
|
|
2375
|
+
continue;
|
|
2376
|
+
}
|
|
2377
|
+
if (sessionUpdateType === "config_option_update") {
|
|
2378
|
+
if (this.syncSessionConfigOptionsFromUpdate(update)) {
|
|
2379
|
+
this.markResponseProgress({ refreshToolTimeouts: false });
|
|
2380
|
+
}
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
if (sessionUpdateType === "task_complete") {
|
|
2384
|
+
this.emitUsageTelemetry(update.usage, "acp-session-usage");
|
|
2385
|
+
this.finalizeDanglingToolCallsAfterTaskComplete();
|
|
2386
|
+
ctx.clearIdleTimeout();
|
|
2387
|
+
logger.debug("[AcpBackend] task_complete received, emitting idle status");
|
|
2388
|
+
this.emitIdleStatus();
|
|
2389
|
+
continue;
|
|
2390
|
+
}
|
|
2391
|
+
const handledLegacy = handleLegacyMessageChunk(update, ctx).handled;
|
|
2392
|
+
const handledPlan = handlePlanUpdate(update, ctx).handled;
|
|
2393
|
+
const handledThinking = handleThinkingUpdate(update, ctx).handled;
|
|
2394
|
+
const handledUsage = this.emitUsageTelemetry(update.usage, "acp-session-usage");
|
|
2395
|
+
if (handledLegacy || handledPlan || handledThinking) {
|
|
2396
|
+
this.markResponseProgress();
|
|
2397
|
+
}
|
|
2398
|
+
const updateTypeStr = sessionUpdateType;
|
|
2399
|
+
const handledTypes = [
|
|
2400
|
+
"agent_message_chunk",
|
|
2401
|
+
"tool_call_update",
|
|
2402
|
+
"agent_thought_chunk",
|
|
2403
|
+
"tool_call",
|
|
2404
|
+
"usage_update",
|
|
2405
|
+
"config_option_update",
|
|
2406
|
+
"task_complete"
|
|
2407
|
+
];
|
|
2408
|
+
if (updateTypeStr && !handledTypes.includes(updateTypeStr) && !handledLegacy && !handledPlan && !handledThinking && !handledUsage) {
|
|
2409
|
+
logger.debug(`[AcpBackend] Unhandled session update type: ${updateTypeStr}`, JSON.stringify(update, null, 2));
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
async sendPrompt(sessionId, prompt) {
|
|
2414
|
+
this.toolCallCountSincePrompt = 0;
|
|
2415
|
+
if (this.disposed) {
|
|
2416
|
+
throw new Error("Backend has been disposed");
|
|
2417
|
+
}
|
|
2418
|
+
if (!this.connection || !this.acpSessionId) {
|
|
2419
|
+
throw new Error("Session not started");
|
|
2420
|
+
}
|
|
2421
|
+
this.resetResponseTrackingForNewPrompt();
|
|
2422
|
+
this.emit({ type: "status", status: "running" });
|
|
2423
|
+
this.waitingForResponse = true;
|
|
2424
|
+
this.markResponseProgress({ refreshToolTimeouts: false });
|
|
2425
|
+
try {
|
|
2426
|
+
logger.debug(`[AcpBackend] Sending prompt (length: ${prompt.length}): ${prompt.substring(0, 100)}...`);
|
|
2427
|
+
logger.debug(`[AcpBackend] Full prompt: ${prompt}`);
|
|
2428
|
+
const contentBlock = {
|
|
2429
|
+
type: "text",
|
|
2430
|
+
text: prompt
|
|
2431
|
+
};
|
|
2432
|
+
const promptRequest = {
|
|
2433
|
+
sessionId: this.acpSessionId,
|
|
2434
|
+
prompt: [contentBlock]
|
|
2435
|
+
};
|
|
2436
|
+
logger.debug(`[AcpBackend] Prompt request:`, JSON.stringify(promptRequest, null, 2));
|
|
2437
|
+
const promptWatchdog = this.createPromptCompletionWatchdog(this.getResponseWaitTimeoutMs());
|
|
2438
|
+
try {
|
|
2439
|
+
await raceWithProcessExit(
|
|
2440
|
+
this.process,
|
|
2441
|
+
() => Promise.race([
|
|
2442
|
+
this.connection.prompt(promptRequest),
|
|
2443
|
+
promptWatchdog.promise
|
|
2444
|
+
]),
|
|
2445
|
+
{
|
|
2446
|
+
agentName: this.transport.agentName,
|
|
2447
|
+
operationName: "prompt",
|
|
2448
|
+
getStderrExcerpt: () => this.getRecentStderrExcerpt()
|
|
2449
|
+
}
|
|
2450
|
+
);
|
|
2451
|
+
} finally {
|
|
2452
|
+
promptWatchdog.cancel();
|
|
2453
|
+
}
|
|
2454
|
+
logger.debug("[AcpBackend] Prompt request sent to ACP connection");
|
|
2455
|
+
if (this.waitingForResponse && this.activeToolCalls.size === 0 && this.sawSessionUpdateSincePrompt === false) {
|
|
2456
|
+
const noUpdatesTimeoutMs = this.getPostPromptNoUpdatesTimeoutMs();
|
|
2457
|
+
this.postPromptCompletionIdleTimeout = setTimeout(() => {
|
|
2458
|
+
this.postPromptCompletionIdleTimeout = null;
|
|
2459
|
+
if (this.responseCompletionError || !this.waitingForResponse) {
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
if (this.sawSessionUpdateSincePrompt || this.activeToolCalls.size > 0) {
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
const exitCode = this.process?.exitCode;
|
|
2466
|
+
if (typeof exitCode === "number" && Number.isFinite(exitCode) && exitCode !== 0) {
|
|
2467
|
+
this.failPendingResponseWait(new Error(`Exit code: ${exitCode}`));
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
const signalCode = this.process?.signalCode;
|
|
2471
|
+
if (typeof signalCode === "string" && signalCode.trim().length > 0) {
|
|
2472
|
+
this.failPendingResponseWait(new Error(`Signal: ${signalCode}`));
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
this.emitIdleStatus();
|
|
2476
|
+
}, Math.max(100, noUpdatesTimeoutMs));
|
|
2477
|
+
}
|
|
2478
|
+
} catch (error) {
|
|
2479
|
+
logger.debug("[AcpBackend] Error sending prompt:", error);
|
|
2480
|
+
let errorDetail;
|
|
2481
|
+
if (error instanceof Error) {
|
|
2482
|
+
errorDetail = error.message;
|
|
2483
|
+
} else if (typeof error === "object" && error !== null) {
|
|
2484
|
+
errorDetail = formatAcpErrorMessage(error);
|
|
2485
|
+
} else {
|
|
2486
|
+
errorDetail = formatAcpErrorMessage(error);
|
|
2487
|
+
}
|
|
2488
|
+
this.emit({
|
|
2489
|
+
type: "status",
|
|
2490
|
+
status: "error",
|
|
2491
|
+
detail: errorDetail
|
|
2492
|
+
});
|
|
2493
|
+
this.failPendingResponseWait(error instanceof Error ? error : normalizeAcpError(error));
|
|
2494
|
+
throw error;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* Wait for the response to complete (idle status after all chunks received)
|
|
2499
|
+
* Call this after sendPrompt to wait for Gemini to finish responding
|
|
2500
|
+
*/
|
|
2501
|
+
async waitForResponseComplete(timeoutMs) {
|
|
2502
|
+
if (this.responseCompletionError) {
|
|
2503
|
+
throw this.responseCompletionError;
|
|
2504
|
+
}
|
|
2505
|
+
const pendingOutcome = this.responseCompletionOutcome;
|
|
2506
|
+
if (pendingOutcome) {
|
|
2507
|
+
this.responseCompletionOutcome = null;
|
|
2508
|
+
if (pendingOutcome.kind === "rejected") {
|
|
2509
|
+
throw pendingOutcome.error;
|
|
2510
|
+
}
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
if (!this.waitingForResponse) {
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
const effectiveTimeoutMs = this.getResponseWaitTimeoutMs(timeoutMs);
|
|
2517
|
+
return new Promise((resolve, reject) => {
|
|
2518
|
+
this.idleResolver = () => {
|
|
2519
|
+
this.idleResolver = null;
|
|
2520
|
+
this.idleRejecter = null;
|
|
2521
|
+
this.waitingForResponse = false;
|
|
2522
|
+
resolve();
|
|
2523
|
+
};
|
|
2524
|
+
this.idleRejecter = (error) => {
|
|
2525
|
+
this.idleResolver = null;
|
|
2526
|
+
this.idleRejecter = null;
|
|
2527
|
+
this.waitingForResponse = false;
|
|
2528
|
+
reject(error);
|
|
2529
|
+
};
|
|
2530
|
+
this.armResponseWaitTimeout(effectiveTimeoutMs);
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
/**
|
|
2534
|
+
* Helper to emit idle status and resolve any waiting promises
|
|
2535
|
+
*/
|
|
2536
|
+
emitIdleStatus() {
|
|
2537
|
+
this.clearPostPromptCompletionIdleTimeout();
|
|
2538
|
+
this.emit({ type: "status", status: "idle" });
|
|
2539
|
+
this.settleResponseWaiter({ kind: "resolved" });
|
|
2540
|
+
}
|
|
2541
|
+
async cancel(sessionId) {
|
|
2542
|
+
const cancelError = createAcpAbortError("Cancelled by user");
|
|
2543
|
+
this.clearIdleTimeoutState();
|
|
2544
|
+
this.clearPostPromptCompletionIdleTimeout();
|
|
2545
|
+
this.clearToolCallTracking();
|
|
2546
|
+
this.settleResponseWaiter({ kind: "rejected", error: cancelError });
|
|
2547
|
+
this.emit({ type: "status", status: "stopped", detail: "Cancelled by user" });
|
|
2548
|
+
if (!this.connection || !this.acpSessionId) {
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
try {
|
|
2552
|
+
await this.connection.cancel({ sessionId: this.acpSessionId });
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
logger.debug("[AcpBackend] Error cancelling:", error);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* Emit permission response event for UI/logging purposes.
|
|
2559
|
+
*
|
|
2560
|
+
* **IMPORTANT:** For ACP backends, this method does NOT send the actual permission
|
|
2561
|
+
* response to the agent. The ACP protocol requires synchronous permission handling,
|
|
2562
|
+
* which is done inside the `requestPermission` RPC handler via `this.options.permissionHandler`.
|
|
2563
|
+
*
|
|
2564
|
+
* This method only emits a `permission-response` event for:
|
|
2565
|
+
* - UI updates (e.g., closing permission dialogs)
|
|
2566
|
+
* - Logging and debugging
|
|
2567
|
+
* - Other parts of the CLI that need to react to permission decisions
|
|
2568
|
+
*
|
|
2569
|
+
* @param requestId - The ID of the permission request
|
|
2570
|
+
* @param approved - Whether the permission was granted
|
|
2571
|
+
*/
|
|
2572
|
+
async respondToPermission(requestId, approved) {
|
|
2573
|
+
logger.debug(`[AcpBackend] Permission response event (UI only): ${requestId} = ${approved}`);
|
|
2574
|
+
this.emit({ type: "permission-response", id: requestId, approved });
|
|
2575
|
+
}
|
|
2576
|
+
async dispose() {
|
|
2577
|
+
if (this.disposed) return;
|
|
2578
|
+
logger.debug("[AcpBackend] Disposing backend");
|
|
2579
|
+
this.disposed = true;
|
|
2580
|
+
this.settleResponseWaiter({
|
|
2581
|
+
kind: "rejected",
|
|
2582
|
+
error: createAcpAbortError("ACP backend disposed")
|
|
2583
|
+
});
|
|
2584
|
+
const processStillRunning = this.process ? this.process.exitCode === null && this.process.signalCode === null : false;
|
|
2585
|
+
if (this.connection && this.acpSessionId && processStillRunning) {
|
|
2586
|
+
try {
|
|
2587
|
+
await Promise.race([
|
|
2588
|
+
this.connection.cancel({ sessionId: this.acpSessionId }),
|
|
2589
|
+
new Promise((resolve) => setTimeout(resolve, 2e3))
|
|
2590
|
+
// 2s timeout for graceful shutdown
|
|
2591
|
+
]);
|
|
2592
|
+
} catch (error) {
|
|
2593
|
+
logger.debug("[AcpBackend] Error during graceful shutdown:", error);
|
|
2594
|
+
}
|
|
2595
|
+
} else if (this.connection && this.acpSessionId && this.process && !processStillRunning) {
|
|
2596
|
+
logger.debug("[AcpBackend] Skipping graceful shutdown because ACP process already exited");
|
|
2597
|
+
}
|
|
2598
|
+
if (this.process) {
|
|
2599
|
+
try {
|
|
2600
|
+
await killProcessTree(this.process, { graceMs: 1e3 });
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
logger.debug("[AcpBackend] Failed to kill ACP process tree:", error);
|
|
2603
|
+
} finally {
|
|
2604
|
+
this.process = null;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
this.clearIdleTimeoutState();
|
|
2608
|
+
this.clearPostPromptCompletionIdleTimeout();
|
|
2609
|
+
this.listeners = [];
|
|
2610
|
+
this.connection = null;
|
|
2611
|
+
this.acpSessionId = null;
|
|
2612
|
+
this.sessionConfigOptions = null;
|
|
2613
|
+
this.clearToolCallTracking();
|
|
2614
|
+
this.pendingPermissions.clear();
|
|
2615
|
+
await this.cleanupOwnedResources();
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
export { AcpBackend as A };
|