@agentknight/sdk 0.9.35
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/LICENSE +201 -0
- package/dist/chunks/acp-adapter-4N2QCTAL.js +936 -0
- package/dist/chunks/chunk-AHJSR7OY.js +24 -0
- package/dist/chunks/chunk-ATMWDEPF.js +1017 -0
- package/dist/chunks/chunk-C7XDS2KG.js +24 -0
- package/dist/chunks/chunk-JFHUAIRY.js +2659 -0
- package/dist/chunks/chunk-MVUOCZFL.js +47 -0
- package/dist/chunks/chunk-SJHJER3B.js +340 -0
- package/dist/chunks/chunk-W3PG2W3C.js +139 -0
- package/dist/chunks/chunk-ZKAN2FO7.js +2654 -0
- package/dist/chunks/copilot-adapter-CLIDIK2S.js +5179 -0
- package/dist/chunks/managed-agent-adapter-HP5VE3AH.js +1252 -0
- package/dist/chunks/pi-adapter-GSDVA7EG.js +1177 -0
- package/dist/index.d.ts +1962 -0
- package/dist/index.js +9187 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.js +13 -0
- package/package.json +65 -0
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
import {
|
|
2
|
+
latestPreviewItems
|
|
3
|
+
} from "./chunk-MVUOCZFL.js";
|
|
4
|
+
import {
|
|
5
|
+
UnsupportedAgentOperationError
|
|
6
|
+
} from "./chunk-SJHJER3B.js";
|
|
7
|
+
import {
|
|
8
|
+
ConversationItemSchema,
|
|
9
|
+
PromptImageAttachmentSchema,
|
|
10
|
+
SESSION_TIMELINE_PAGE_SIZE,
|
|
11
|
+
SessionSummarySchema
|
|
12
|
+
} from "./chunk-ZKAN2FO7.js";
|
|
13
|
+
|
|
14
|
+
// src/acp-adapter.ts
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
17
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
18
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
19
|
+
import { Readable, Writable } from "node:stream";
|
|
20
|
+
import {
|
|
21
|
+
client,
|
|
22
|
+
ndJsonStream,
|
|
23
|
+
PROTOCOL_VERSION,
|
|
24
|
+
RequestError
|
|
25
|
+
} from "@agentclientprotocol/sdk";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
var ACP_DEFAULT_MODEL_ID = "acp:agent-default";
|
|
28
|
+
var STREAM_CHECKPOINT_MS = 1e3;
|
|
29
|
+
var SelectValueSchema = z.object({
|
|
30
|
+
value: z.string().trim().min(1).max(256),
|
|
31
|
+
name: z.string().trim().min(1).max(260)
|
|
32
|
+
});
|
|
33
|
+
var CapabilitiesSchema = z.object({
|
|
34
|
+
loadSession: z.boolean().optional(),
|
|
35
|
+
promptCapabilities: z.object({ image: z.boolean().optional() }).optional(),
|
|
36
|
+
sessionCapabilities: z.object({
|
|
37
|
+
list: z.object({}).nullable().optional(),
|
|
38
|
+
resume: z.object({}).nullable().optional(),
|
|
39
|
+
close: z.object({}).nullable().optional(),
|
|
40
|
+
delete: z.object({}).nullable().optional()
|
|
41
|
+
}).optional()
|
|
42
|
+
});
|
|
43
|
+
var ConfigOptionSchema = z.intersection(
|
|
44
|
+
z.object({ id: z.string().min(1), name: z.string(), category: z.string().nullable().optional() }),
|
|
45
|
+
z.discriminatedUnion("type", [
|
|
46
|
+
z.object({
|
|
47
|
+
type: z.literal("select"),
|
|
48
|
+
currentValue: z.string(),
|
|
49
|
+
options: z.union([
|
|
50
|
+
z.array(SelectValueSchema),
|
|
51
|
+
z.array(z.object({ group: z.string(), name: z.string(), options: z.array(SelectValueSchema) }))
|
|
52
|
+
])
|
|
53
|
+
}),
|
|
54
|
+
z.object({ type: z.literal("boolean"), currentValue: z.boolean() })
|
|
55
|
+
])
|
|
56
|
+
);
|
|
57
|
+
var ConfigStateSchema = z.object({
|
|
58
|
+
configOptions: z.array(ConfigOptionSchema).nullable().optional()
|
|
59
|
+
});
|
|
60
|
+
var StoredSessionSchema = z.object({
|
|
61
|
+
version: z.literal(1),
|
|
62
|
+
summary: SessionSummarySchema,
|
|
63
|
+
configOptions: z.array(ConfigOptionSchema),
|
|
64
|
+
items: z.array(ConversationItemSchema),
|
|
65
|
+
pendingPromptIds: z.array(z.string().min(1)).default([])
|
|
66
|
+
});
|
|
67
|
+
function hash(value) {
|
|
68
|
+
return createHash("sha256").update(value).digest("hex");
|
|
69
|
+
}
|
|
70
|
+
function errorMessage(error) {
|
|
71
|
+
return error instanceof Error ? error.message : String(error);
|
|
72
|
+
}
|
|
73
|
+
async function bounded(promise, timeout, operation) {
|
|
74
|
+
let timer;
|
|
75
|
+
try {
|
|
76
|
+
return await Promise.race([
|
|
77
|
+
promise,
|
|
78
|
+
new Promise((_, reject) => {
|
|
79
|
+
timer = setTimeout(() => reject(new Error(`ACP ${operation} timed out`)), timeout);
|
|
80
|
+
})
|
|
81
|
+
]);
|
|
82
|
+
} finally {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
var AcpCodingAgentAdapter = class {
|
|
87
|
+
agentId = "acp";
|
|
88
|
+
sdkVersion = "1.4.0";
|
|
89
|
+
agentModes = ["interactive"];
|
|
90
|
+
supportsReasoningEffort = false;
|
|
91
|
+
supportsSessionListUpdates = false;
|
|
92
|
+
supportsAssistantPreview = true;
|
|
93
|
+
supportsFileAttachments = false;
|
|
94
|
+
#options;
|
|
95
|
+
#directory;
|
|
96
|
+
#sessions = /* @__PURE__ */ new Map();
|
|
97
|
+
#permissions = /* @__PURE__ */ new Map();
|
|
98
|
+
#callbacks;
|
|
99
|
+
#child;
|
|
100
|
+
#connection;
|
|
101
|
+
#capabilities = {};
|
|
102
|
+
#started = false;
|
|
103
|
+
#stopping = false;
|
|
104
|
+
#startPromise;
|
|
105
|
+
#stopPromise;
|
|
106
|
+
#exit;
|
|
107
|
+
#fatalError;
|
|
108
|
+
#stderr = "";
|
|
109
|
+
constructor(options) {
|
|
110
|
+
if (!options.command.trim() || options.command.includes("\0")) {
|
|
111
|
+
throw new Error("ACP requires an installed executable");
|
|
112
|
+
}
|
|
113
|
+
for (const timeout of [
|
|
114
|
+
options.requestTimeoutMs,
|
|
115
|
+
options.promptTimeoutMs,
|
|
116
|
+
options.cancelTimeoutMs,
|
|
117
|
+
options.permissionTimeoutMs
|
|
118
|
+
]) {
|
|
119
|
+
if (timeout !== void 0 && (!Number.isSafeInteger(timeout) || timeout < 1)) {
|
|
120
|
+
throw new RangeError("ACP timeouts must be positive integers");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
this.#options = { ...options, args: [...options.args ?? []] };
|
|
124
|
+
this.#directory = join(
|
|
125
|
+
resolve(options.dataDirectory),
|
|
126
|
+
"acp",
|
|
127
|
+
hash(JSON.stringify([options.command, options.args ?? [], resolve(options.cwd ?? process.cwd())]))
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
get supportsSessionDelete() {
|
|
131
|
+
return this.#capabilities.sessionCapabilities?.delete != null;
|
|
132
|
+
}
|
|
133
|
+
async canUpgrade() {
|
|
134
|
+
return this.#started && !this.#stopping && this.#fatalError === void 0 && this.#permissions.size === 0 && [...this.#sessions.values()].every((session) => session.active === void 0 && session.queue.length === 0 && session.loading === void 0 && !session.writeInProgress && session.persistenceError === void 0 && !session.items.some((item) => item.kind === "activity-group" && item.status === "running"));
|
|
135
|
+
}
|
|
136
|
+
setSessionCallbacks(callbacks) {
|
|
137
|
+
this.#callbacks = callbacks;
|
|
138
|
+
}
|
|
139
|
+
start() {
|
|
140
|
+
if (this.#stopping) return Promise.reject(new Error("Create a new ACP adapter after stopping"));
|
|
141
|
+
if (this.#started) return Promise.resolve();
|
|
142
|
+
if (this.#startPromise) return this.#startPromise;
|
|
143
|
+
this.#startPromise = this.#start();
|
|
144
|
+
return this.#startPromise;
|
|
145
|
+
}
|
|
146
|
+
async #start() {
|
|
147
|
+
try {
|
|
148
|
+
await mkdir(this.#directory, { recursive: true, mode: 448 });
|
|
149
|
+
for (const file of await readdir(this.#directory)) {
|
|
150
|
+
if (!/^[a-f0-9]{64}\.json$/.test(file)) continue;
|
|
151
|
+
const stored = StoredSessionSchema.parse(JSON.parse(await readFile(join(this.#directory, file), "utf8")));
|
|
152
|
+
if (file !== `${hash(stored.summary.sessionId)}.json` || !isAbsolute(stored.summary.workingDirectory ?? "")) {
|
|
153
|
+
throw new Error("Invalid ACP session index");
|
|
154
|
+
}
|
|
155
|
+
const session = this.#newState(stored.summary, stored.configOptions);
|
|
156
|
+
session.items = stored.items;
|
|
157
|
+
this.#sessions.set(stored.summary.sessionId, session);
|
|
158
|
+
for (const id of stored.pendingPromptIds) {
|
|
159
|
+
this.#finalize(session, { id, prompt: [], cancelled: true }, false);
|
|
160
|
+
this.#notice(session, id, "warning", "Interrupted", "Agent process stopped before the prompt completed");
|
|
161
|
+
}
|
|
162
|
+
if (stored.pendingPromptIds.length) await this.#save(session);
|
|
163
|
+
}
|
|
164
|
+
if (this.#stopping) throw new Error("ACP startup was stopped");
|
|
165
|
+
const child = spawn(this.#options.command, [...this.#options.args ?? []], {
|
|
166
|
+
cwd: this.#options.cwd ?? process.cwd(),
|
|
167
|
+
env: this.#options.env ?? process.env,
|
|
168
|
+
shell: false,
|
|
169
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
170
|
+
windowsHide: true
|
|
171
|
+
});
|
|
172
|
+
this.#child = child;
|
|
173
|
+
this.#exit = new Promise((resolveExit) => {
|
|
174
|
+
child.once("close", () => resolveExit());
|
|
175
|
+
});
|
|
176
|
+
child.on("error", (error) => this.#fail(error));
|
|
177
|
+
child.stdin.on("error", (error) => this.#fail(error));
|
|
178
|
+
child.stdout.on("error", (error) => this.#fail(error));
|
|
179
|
+
child.stderr.on("error", (error) => this.#fail(error));
|
|
180
|
+
child.once("exit", (code, signal) => {
|
|
181
|
+
if (!this.#stopping) this.#fail(new Error(`ACP process exited (${signal ?? code})`));
|
|
182
|
+
});
|
|
183
|
+
child.stderr.setEncoding("utf8");
|
|
184
|
+
child.stderr.on("data", (chunk) => {
|
|
185
|
+
this.#stderr = (this.#stderr + chunk).slice(-8192);
|
|
186
|
+
this.#options.onDiagnostic?.(chunk.slice(-8192));
|
|
187
|
+
});
|
|
188
|
+
const connection = client({ name: "AgentKnight" }).onRequest("session/request_permission", ({ params, signal }) => this.#permission(params, signal)).onNotification("session/update", ({ params }) => this.#update(params)).connect(ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout)));
|
|
189
|
+
this.#connection = connection;
|
|
190
|
+
connection.signal.addEventListener("abort", () => {
|
|
191
|
+
if (!this.#stopping) this.#fail(new Error("ACP connection closed"));
|
|
192
|
+
}, { once: true });
|
|
193
|
+
const response = await this.#request("initialize", {
|
|
194
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
195
|
+
clientInfo: { name: "agentknight", version: this.sdkVersion },
|
|
196
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false }
|
|
197
|
+
});
|
|
198
|
+
if (!response || response.protocolVersion !== PROTOCOL_VERSION) {
|
|
199
|
+
throw new Error("ACP agent returned an unsupported protocol version");
|
|
200
|
+
}
|
|
201
|
+
this.#capabilities = CapabilitiesSchema.parse(response.agentCapabilities ?? {});
|
|
202
|
+
this.#started = true;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
this.#fail(error);
|
|
205
|
+
await this.stop();
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async #request(method, params) {
|
|
210
|
+
if (!this.#connection || this.#fatalError) throw this.#fatalError ?? new Error("ACP is not connected");
|
|
211
|
+
try {
|
|
212
|
+
return await bounded(
|
|
213
|
+
this.#connection.agent.request(method, params),
|
|
214
|
+
this.#options.requestTimeoutMs ?? 3e4,
|
|
215
|
+
method
|
|
216
|
+
);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error instanceof RequestError && error.code === -32e3) {
|
|
219
|
+
throw new Error("ACP authentication required; authenticate the configured bridge outside AgentKnight", { cause: error });
|
|
220
|
+
}
|
|
221
|
+
if (error instanceof Error && error.message.endsWith("timed out")) this.#fail(error);
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
#ready() {
|
|
226
|
+
if (!this.#started || this.#stopping || this.#fatalError) {
|
|
227
|
+
throw this.#fatalError ?? new Error("ACP adapter is not running");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
#known(sessionId) {
|
|
231
|
+
this.#ready();
|
|
232
|
+
const session = this.#sessions.get(sessionId);
|
|
233
|
+
if (!session || session.deleted) throw new Error(`Unknown ACP session: ${sessionId}`);
|
|
234
|
+
if (session.persistenceError) throw session.persistenceError;
|
|
235
|
+
return session;
|
|
236
|
+
}
|
|
237
|
+
#newState(summary, configOptions = []) {
|
|
238
|
+
return {
|
|
239
|
+
summary,
|
|
240
|
+
configOptions,
|
|
241
|
+
items: [],
|
|
242
|
+
loaded: false,
|
|
243
|
+
replaying: false,
|
|
244
|
+
queue: [],
|
|
245
|
+
epoch: 0,
|
|
246
|
+
lock: Promise.resolve(),
|
|
247
|
+
saving: Promise.resolve(),
|
|
248
|
+
commands: [],
|
|
249
|
+
writeInProgress: false,
|
|
250
|
+
changeVersion: 0,
|
|
251
|
+
requestedVersion: 0,
|
|
252
|
+
persistedVersion: 0,
|
|
253
|
+
deleted: false
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
#save(session) {
|
|
257
|
+
session.changeVersion++;
|
|
258
|
+
this.#clearCheckpoint(session);
|
|
259
|
+
return this.#flush(session);
|
|
260
|
+
}
|
|
261
|
+
#clearCheckpoint(session) {
|
|
262
|
+
clearTimeout(session.checkpointTimer);
|
|
263
|
+
delete session.checkpointTimer;
|
|
264
|
+
}
|
|
265
|
+
#checkpoint(session) {
|
|
266
|
+
session.changeVersion++;
|
|
267
|
+
if (session.checkpointTimer || this.#stopping || session.deleted || session.persistenceError) return;
|
|
268
|
+
session.checkpointTimer = setTimeout(() => {
|
|
269
|
+
delete session.checkpointTimer;
|
|
270
|
+
void this.#flush(session);
|
|
271
|
+
}, STREAM_CHECKPOINT_MS);
|
|
272
|
+
session.checkpointTimer.unref();
|
|
273
|
+
}
|
|
274
|
+
#flush(session) {
|
|
275
|
+
if (session.persistenceError || session.deleted) return session.saving;
|
|
276
|
+
session.requestedVersion = session.changeVersion;
|
|
277
|
+
if (session.writeInProgress) return session.saving;
|
|
278
|
+
session.writeInProgress = true;
|
|
279
|
+
const destination = join(this.#directory, `${hash(session.summary.sessionId)}.json`);
|
|
280
|
+
session.saving = (async () => {
|
|
281
|
+
try {
|
|
282
|
+
while (session.persistedVersion < session.requestedVersion) {
|
|
283
|
+
const version = session.changeVersion;
|
|
284
|
+
const snapshot = JSON.stringify({
|
|
285
|
+
version: 1,
|
|
286
|
+
summary: session.summary,
|
|
287
|
+
configOptions: session.configOptions,
|
|
288
|
+
items: session.items,
|
|
289
|
+
pendingPromptIds: [...session.active ? [session.active.id] : [], ...session.queue.map((job) => job.id)]
|
|
290
|
+
});
|
|
291
|
+
const staging = `${destination}.${randomUUID()}.pending`;
|
|
292
|
+
try {
|
|
293
|
+
await writeFile(staging, snapshot, { mode: 384, flag: "wx" });
|
|
294
|
+
await rename(staging, destination);
|
|
295
|
+
session.persistedVersion = version;
|
|
296
|
+
} finally {
|
|
297
|
+
await rm(staging, { force: true });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
session.persistenceError = error;
|
|
302
|
+
throw error;
|
|
303
|
+
} finally {
|
|
304
|
+
session.writeInProgress = false;
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
void session.saving.catch((error) => {
|
|
308
|
+
this.#callbacks?.onSyncError(session.summary.sessionId, error);
|
|
309
|
+
this.#fail(error);
|
|
310
|
+
});
|
|
311
|
+
return session.saving;
|
|
312
|
+
}
|
|
313
|
+
async listModels() {
|
|
314
|
+
this.#ready();
|
|
315
|
+
const models = /* @__PURE__ */ new Map([
|
|
316
|
+
[ACP_DEFAULT_MODEL_ID, { id: ACP_DEFAULT_MODEL_ID, name: "Agent default", isDefault: true }]
|
|
317
|
+
]);
|
|
318
|
+
for (const session of this.#sessions.values()) {
|
|
319
|
+
if (!session.loaded) continue;
|
|
320
|
+
const selector = this.#modelSelector(session);
|
|
321
|
+
if (!selector) continue;
|
|
322
|
+
for (const value of selector.options.flatMap((entry) => "group" in entry ? entry.options : [entry])) {
|
|
323
|
+
models.set(value.value, { id: value.value, name: value.name, isDefault: false });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return [...models.values()];
|
|
327
|
+
}
|
|
328
|
+
#modelSelector(session) {
|
|
329
|
+
return session.configOptions.find(
|
|
330
|
+
(option) => option.type === "select" && option.category === "model"
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
#configuration(session) {
|
|
334
|
+
return {
|
|
335
|
+
modelId: this.#modelSelector(session)?.currentValue ?? ACP_DEFAULT_MODEL_ID,
|
|
336
|
+
agentMode: "interactive",
|
|
337
|
+
reasoningEffort: "default"
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
#validateConfiguration(options) {
|
|
341
|
+
if (options.agentMode !== void 0 && options.agentMode !== "interactive") {
|
|
342
|
+
throw new UnsupportedAgentOperationError("acp", `agent mode ${options.agentMode}`);
|
|
343
|
+
}
|
|
344
|
+
if (options.reasoningEffort !== void 0 && options.reasoningEffort !== "default") {
|
|
345
|
+
throw new UnsupportedAgentOperationError("acp", "reasoning effort");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async createSession(options) {
|
|
349
|
+
this.#ready();
|
|
350
|
+
this.#validateConfiguration(options);
|
|
351
|
+
if (!isAbsolute(options.workingDirectory)) throw new Error("ACP working directory must be absolute");
|
|
352
|
+
if (options.modelId !== ACP_DEFAULT_MODEL_ID && !(await this.listModels()).some((model) => model.id === options.modelId)) {
|
|
353
|
+
throw new Error("ACP model has not been advertised by an agent session");
|
|
354
|
+
}
|
|
355
|
+
const response = await this.#request("session/new", { cwd: options.workingDirectory, mcpServers: [] });
|
|
356
|
+
const parsed = ConfigStateSchema.extend({ sessionId: z.string().min(1).max(256) }).parse(response);
|
|
357
|
+
if (this.#sessions.has(parsed.sessionId)) throw new Error("ACP agent reused an existing session ID");
|
|
358
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
359
|
+
const summary = SessionSummarySchema.parse({
|
|
360
|
+
sessionId: parsed.sessionId,
|
|
361
|
+
userId: options.userId,
|
|
362
|
+
machineId: options.machineId,
|
|
363
|
+
workingDirectory: options.workingDirectory,
|
|
364
|
+
isRemote: false,
|
|
365
|
+
createdAt: now,
|
|
366
|
+
modifiedAt: now
|
|
367
|
+
});
|
|
368
|
+
const session = this.#newState(summary, parsed.configOptions ?? []);
|
|
369
|
+
session.loaded = true;
|
|
370
|
+
this.#sessions.set(summary.sessionId, session);
|
|
371
|
+
await this.#save(session);
|
|
372
|
+
if (options.modelId !== ACP_DEFAULT_MODEL_ID) {
|
|
373
|
+
await this.configureSession(summary.sessionId, { modelId: options.modelId });
|
|
374
|
+
}
|
|
375
|
+
this.#callbacks?.onSessionInventoryChanged(summary.sessionId);
|
|
376
|
+
return { ...summary };
|
|
377
|
+
}
|
|
378
|
+
async #load(session) {
|
|
379
|
+
if (session.loaded) return;
|
|
380
|
+
if (session.loading) return session.loading;
|
|
381
|
+
session.loading = (async () => {
|
|
382
|
+
const params = { sessionId: session.summary.sessionId, cwd: session.summary.workingDirectory, mcpServers: [] };
|
|
383
|
+
let response;
|
|
384
|
+
if (this.#capabilities.sessionCapabilities?.resume != null) {
|
|
385
|
+
response = await this.#request("session/resume", params);
|
|
386
|
+
} else if (this.#capabilities.loadSession === true) {
|
|
387
|
+
session.replaying = true;
|
|
388
|
+
try {
|
|
389
|
+
response = await this.#request("session/load", params);
|
|
390
|
+
} finally {
|
|
391
|
+
session.replaying = false;
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
throw new UnsupportedAgentOperationError("acp", "resuming persisted sessions");
|
|
395
|
+
}
|
|
396
|
+
const parsed = ConfigStateSchema.parse(response ?? {});
|
|
397
|
+
session.configOptions = parsed.configOptions ?? [];
|
|
398
|
+
session.loaded = true;
|
|
399
|
+
await this.#save(session);
|
|
400
|
+
})();
|
|
401
|
+
try {
|
|
402
|
+
await session.loading;
|
|
403
|
+
} finally {
|
|
404
|
+
delete session.loading;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async listSessions(userId, machineId) {
|
|
408
|
+
this.#ready();
|
|
409
|
+
if (this.#capabilities.sessionCapabilities?.list != null) {
|
|
410
|
+
let cursor;
|
|
411
|
+
const seen = /* @__PURE__ */ new Set();
|
|
412
|
+
do {
|
|
413
|
+
const response = await this.#request("session/list", cursor === void 0 ? {} : { cursor });
|
|
414
|
+
if (!Array.isArray(response?.sessions)) throw new Error("Malformed ACP session list");
|
|
415
|
+
for (const entry of response.sessions) {
|
|
416
|
+
const session = this.#sessions.get(entry.sessionId);
|
|
417
|
+
if (!session || session.summary.userId !== userId || session.summary.machineId !== machineId) continue;
|
|
418
|
+
this.#mergeSessionInfo(session, entry);
|
|
419
|
+
await this.#save(session);
|
|
420
|
+
}
|
|
421
|
+
cursor = response.nextCursor ?? void 0;
|
|
422
|
+
if (cursor !== void 0 && (seen.has(cursor) || seen.size >= 1e3)) {
|
|
423
|
+
throw new Error("ACP session list pagination did not terminate");
|
|
424
|
+
}
|
|
425
|
+
if (cursor !== void 0) seen.add(cursor);
|
|
426
|
+
} while (cursor !== void 0);
|
|
427
|
+
}
|
|
428
|
+
return [...this.#sessions.values()].filter((session) => session.summary.userId === userId && session.summary.machineId === machineId).map((session) => ({ ...session.summary })).sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
|
|
429
|
+
}
|
|
430
|
+
async getSessionConfiguration(sessionId) {
|
|
431
|
+
const session = this.#known(sessionId);
|
|
432
|
+
await this.#load(session);
|
|
433
|
+
return this.#configuration(session);
|
|
434
|
+
}
|
|
435
|
+
async configureSession(sessionId, options) {
|
|
436
|
+
if (options.configOption !== void 0) {
|
|
437
|
+
throw new UnsupportedAgentOperationError("acp", `configuration option ${options.configOption.id}`);
|
|
438
|
+
}
|
|
439
|
+
const session = this.#known(sessionId);
|
|
440
|
+
this.#validateConfiguration(options);
|
|
441
|
+
return this.#locked(session, async () => {
|
|
442
|
+
if (session.active || session.queue.length) throw new Error("ACP session is busy");
|
|
443
|
+
await this.#load(session);
|
|
444
|
+
if (options.modelId !== void 0 && options.modelId !== this.#configuration(session).modelId) {
|
|
445
|
+
const selector = this.#modelSelector(session);
|
|
446
|
+
if (options.modelId === ACP_DEFAULT_MODEL_ID) {
|
|
447
|
+
throw new UnsupportedAgentOperationError("acp", "resetting a session to an unadvertised default model");
|
|
448
|
+
}
|
|
449
|
+
if (!selector) throw new UnsupportedAgentOperationError("acp", "model selection");
|
|
450
|
+
const values = selector.options.flatMap((entry) => "group" in entry ? entry.options : [entry]);
|
|
451
|
+
if (!values.some((entry) => entry.value === options.modelId)) {
|
|
452
|
+
throw new Error("Model is not advertised for this ACP session");
|
|
453
|
+
}
|
|
454
|
+
const response = await this.#request("session/set_config_option", {
|
|
455
|
+
sessionId,
|
|
456
|
+
configId: selector.id,
|
|
457
|
+
value: options.modelId
|
|
458
|
+
});
|
|
459
|
+
session.configOptions = ConfigStateSchema.parse(response).configOptions ?? [];
|
|
460
|
+
if (this.#modelSelector(session)?.currentValue !== options.modelId) {
|
|
461
|
+
throw new Error("ACP agent did not confirm the requested model");
|
|
462
|
+
}
|
|
463
|
+
await this.#save(session);
|
|
464
|
+
}
|
|
465
|
+
return this.#configuration(session);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
async getSessionEnvironment(sessionId) {
|
|
469
|
+
const session = this.#known(sessionId);
|
|
470
|
+
await this.#load(session);
|
|
471
|
+
return { tools: [], skills: [], slashCommands: [...session.commands] };
|
|
472
|
+
}
|
|
473
|
+
async getSessionRemoteStatus(sessionId) {
|
|
474
|
+
this.#known(sessionId);
|
|
475
|
+
throw new UnsupportedAgentOperationError("acp", "vendor remote sessions");
|
|
476
|
+
}
|
|
477
|
+
async setSessionRemote(sessionId, _enabled) {
|
|
478
|
+
return this.getSessionRemoteStatus(sessionId);
|
|
479
|
+
}
|
|
480
|
+
async deleteSession(sessionId) {
|
|
481
|
+
const session = this.#known(sessionId);
|
|
482
|
+
if (!this.supportsSessionDelete) throw new UnsupportedAgentOperationError("acp", "session deletion");
|
|
483
|
+
await this.abortSession(sessionId);
|
|
484
|
+
await this.#locked(session, async () => {
|
|
485
|
+
await this.#request("session/delete", { sessionId });
|
|
486
|
+
session.deleted = true;
|
|
487
|
+
this.#clearCheckpoint(session);
|
|
488
|
+
await session.saving;
|
|
489
|
+
await rm(join(this.#directory, `${hash(sessionId)}.json`));
|
|
490
|
+
this.#sessions.delete(sessionId);
|
|
491
|
+
this.#callbacks?.onSessionInventoryChanged(sessionId);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
async watchSession(sessionId, watched) {
|
|
495
|
+
const session = this.#known(sessionId);
|
|
496
|
+
if (watched) await this.#load(session);
|
|
497
|
+
}
|
|
498
|
+
async readSessionPlan(sessionId) {
|
|
499
|
+
const session = this.#known(sessionId);
|
|
500
|
+
const plan = [...session.items].reverse().find((item) => item.kind === "plan-update");
|
|
501
|
+
if (plan) return structuredClone(plan);
|
|
502
|
+
return ConversationItemSchema.parse({
|
|
503
|
+
...this.#base(hash(`${sessionId}:empty-plan`), sessionId),
|
|
504
|
+
timestamp: session.summary.createdAt,
|
|
505
|
+
kind: "plan-update",
|
|
506
|
+
actor: "assistant",
|
|
507
|
+
operation: "deleted",
|
|
508
|
+
content: "",
|
|
509
|
+
todos: []
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
async readSessionTimelinePage(sessionId, options = {}) {
|
|
513
|
+
const session = this.#known(sessionId);
|
|
514
|
+
const pageSize = options.pageSize ?? SESSION_TIMELINE_PAGE_SIZE;
|
|
515
|
+
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > SESSION_TIMELINE_PAGE_SIZE) {
|
|
516
|
+
throw new RangeError("Invalid ACP timeline page size");
|
|
517
|
+
}
|
|
518
|
+
let end = session.items.length;
|
|
519
|
+
let cursorStatus = "ok";
|
|
520
|
+
if (options.cursor !== void 0) {
|
|
521
|
+
const boundary = session.items.findIndex((item) => `${hash(sessionId)}:${item.id}` === options.cursor);
|
|
522
|
+
if (boundary < 0) cursorStatus = "expired";
|
|
523
|
+
else end = boundary;
|
|
524
|
+
}
|
|
525
|
+
const start = Math.max(0, end - pageSize);
|
|
526
|
+
return {
|
|
527
|
+
items: options.metadataOnly ? options.assistantPreview === true ? structuredClone(latestPreviewItems(session.items)) : [] : structuredClone(session.items.slice(start, end)),
|
|
528
|
+
cursor: `${hash(sessionId)}:${session.items[start]?.id ?? "empty"}`,
|
|
529
|
+
hasMore: options.metadataOnly && options.assistantPreview ? false : start > 0,
|
|
530
|
+
cursorStatus
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
#locked(session, operation) {
|
|
534
|
+
const result = session.lock.then(() => {
|
|
535
|
+
if (session.deleted) throw new Error(`Unknown ACP session: ${session.summary.sessionId}`);
|
|
536
|
+
return operation();
|
|
537
|
+
});
|
|
538
|
+
session.lock = result.then(() => void 0, () => void 0);
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
async sendPrompt(sessionId, prompt, mode, attachments = [], _persistInteractions = true, fileAttachments = []) {
|
|
542
|
+
const session = this.#known(sessionId);
|
|
543
|
+
const epoch = session.epoch;
|
|
544
|
+
if (fileAttachments.length) throw new UnsupportedAgentOperationError("acp", "file attachments");
|
|
545
|
+
if (attachments.length && this.#capabilities.promptCapabilities?.image !== true) {
|
|
546
|
+
throw new UnsupportedAgentOperationError("acp", "image prompts");
|
|
547
|
+
}
|
|
548
|
+
for (const image of attachments) PromptImageAttachmentSchema.parse(image);
|
|
549
|
+
if (!prompt.trim() && !attachments.length) throw new Error("ACP prompt is empty");
|
|
550
|
+
if (prompt.length > 1e5) throw new Error("ACP prompt is too long");
|
|
551
|
+
return this.#locked(session, async () => {
|
|
552
|
+
await this.#load(session);
|
|
553
|
+
this.#ready();
|
|
554
|
+
if (session.epoch !== epoch) throw new Error("ACP prompt was cancelled before acceptance");
|
|
555
|
+
if (mode === "immediate" && (session.active || session.queue.length)) {
|
|
556
|
+
throw new Error("ACP session is busy; enqueue the prompt or abort first");
|
|
557
|
+
}
|
|
558
|
+
const id = randomUUID();
|
|
559
|
+
const job = {
|
|
560
|
+
id,
|
|
561
|
+
cancelled: false,
|
|
562
|
+
prompt: [
|
|
563
|
+
{ type: "text", text: prompt },
|
|
564
|
+
...attachments.map((image) => ({ type: "image", data: image.data, mimeType: image.mimeType }))
|
|
565
|
+
]
|
|
566
|
+
};
|
|
567
|
+
const queued = Boolean(session.active || session.queue.length);
|
|
568
|
+
this.#upsert(session, {
|
|
569
|
+
...this.#base(id, id),
|
|
570
|
+
kind: "human-message",
|
|
571
|
+
actor: "human",
|
|
572
|
+
content: prompt,
|
|
573
|
+
attachments: attachments.map((image) => ({ kind: "image", ...image }))
|
|
574
|
+
});
|
|
575
|
+
session.queue.push(job);
|
|
576
|
+
await this.#save(session);
|
|
577
|
+
if (session.epoch !== epoch) throw new Error("ACP prompt was cancelled before delivery");
|
|
578
|
+
this.#pump(session);
|
|
579
|
+
return { messageId: id, deliveryState: queued ? "queued" : "processing" };
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
#pump(session) {
|
|
583
|
+
if (session.active || this.#stopping || this.#fatalError) return;
|
|
584
|
+
const job = session.queue.shift();
|
|
585
|
+
if (!job) return;
|
|
586
|
+
session.active = job;
|
|
587
|
+
delete session.streamMessage;
|
|
588
|
+
job.done = Promise.resolve().then(() => this.#runPrompt(session, job));
|
|
589
|
+
this.#callbacks?.onSessionActivity?.(session.summary.sessionId, "working", (/* @__PURE__ */ new Date()).toISOString());
|
|
590
|
+
void job.done.catch((error) => this.#fail(error));
|
|
591
|
+
}
|
|
592
|
+
async #runPrompt(session, job) {
|
|
593
|
+
try {
|
|
594
|
+
if (job.cancelled || this.#stopping || this.#fatalError) {
|
|
595
|
+
throw new Error("ACP prompt cancelled before transmission");
|
|
596
|
+
}
|
|
597
|
+
const response = await bounded(this.#connection.agent.request("session/prompt", {
|
|
598
|
+
sessionId: session.summary.sessionId,
|
|
599
|
+
prompt: job.prompt
|
|
600
|
+
}), this.#options.promptTimeoutMs ?? 30 * 6e4, "prompt");
|
|
601
|
+
if (!response || typeof response.stopReason !== "string") throw new Error("Malformed ACP prompt response");
|
|
602
|
+
const reason = job.cancelled ? "cancelled" : response.stopReason;
|
|
603
|
+
this.#finalize(session, job, reason === "end_turn");
|
|
604
|
+
if (reason !== "end_turn") {
|
|
605
|
+
this.#notice(
|
|
606
|
+
session,
|
|
607
|
+
job.id,
|
|
608
|
+
reason === "cancelled" ? "warning" : "error",
|
|
609
|
+
reason === "cancelled" ? "Cancelled" : "Agent stopped",
|
|
610
|
+
reason
|
|
611
|
+
);
|
|
612
|
+
if (reason !== "cancelled") {
|
|
613
|
+
this.#callbacks?.onSyncError(session.summary.sessionId, new Error(`ACP prompt stopped: ${reason}`));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} catch (error) {
|
|
617
|
+
this.#finalize(session, job, false);
|
|
618
|
+
this.#notice(
|
|
619
|
+
session,
|
|
620
|
+
job.id,
|
|
621
|
+
job.cancelled ? "warning" : "error",
|
|
622
|
+
job.cancelled ? "Cancelled" : "Prompt failed",
|
|
623
|
+
errorMessage(error)
|
|
624
|
+
);
|
|
625
|
+
this.#callbacks?.onSyncError(session.summary.sessionId, error);
|
|
626
|
+
if (error instanceof Error && error.message.endsWith("timed out")) this.#fail(error);
|
|
627
|
+
} finally {
|
|
628
|
+
this.#cancelPermissions(session.summary.sessionId);
|
|
629
|
+
delete session.active;
|
|
630
|
+
delete session.streamMessage;
|
|
631
|
+
await this.#save(session);
|
|
632
|
+
this.#callbacks?.onSessionActivity?.(session.summary.sessionId, "idle", (/* @__PURE__ */ new Date()).toISOString());
|
|
633
|
+
this.#callbacks?.onSessionInventoryChanged(session.summary.sessionId);
|
|
634
|
+
this.#pump(session);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
#base(id, turnId) {
|
|
638
|
+
return { id, turnId, revision: 1, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sourceEventIds: [id] };
|
|
639
|
+
}
|
|
640
|
+
#upsert(session, item) {
|
|
641
|
+
const index = session.items.findIndex((old) => old.id === item.id);
|
|
642
|
+
const previous = session.items[index];
|
|
643
|
+
const next = ConversationItemSchema.parse({
|
|
644
|
+
...item,
|
|
645
|
+
revision: (previous?.revision ?? 0) + 1,
|
|
646
|
+
timestamp: previous?.timestamp ?? item.timestamp
|
|
647
|
+
});
|
|
648
|
+
if (index < 0) session.items.push(next);
|
|
649
|
+
else session.items[index] = next;
|
|
650
|
+
session.summary.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
651
|
+
this.#callbacks?.onConversationItem(session.summary.sessionId, structuredClone(next));
|
|
652
|
+
}
|
|
653
|
+
#notice(session, turnId, severity, title, content) {
|
|
654
|
+
this.#upsert(session, {
|
|
655
|
+
...this.#base(randomUUID(), turnId),
|
|
656
|
+
kind: "notice",
|
|
657
|
+
actor: "system",
|
|
658
|
+
severity,
|
|
659
|
+
title,
|
|
660
|
+
content: content.slice(0, 4096)
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
#finalize(session, job, success) {
|
|
664
|
+
for (const item of [...session.items]) {
|
|
665
|
+
if (item.turnId !== job.id) continue;
|
|
666
|
+
if (item.kind === "assistant-message" && !item.final) this.#upsert(session, { ...item, final: true });
|
|
667
|
+
if (item.kind === "activity-group" && item.status === "running") {
|
|
668
|
+
this.#upsert(session, {
|
|
669
|
+
...item,
|
|
670
|
+
status: "failed",
|
|
671
|
+
steps: item.steps.map((step) => step.status === "running" ? { ...step, status: "failed" } : step)
|
|
672
|
+
});
|
|
673
|
+
if (success) this.#callbacks?.onSyncError(session.summary.sessionId, new Error("ACP tool did not report completion"));
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async #update(notification) {
|
|
678
|
+
const session = this.#sessions.get(notification.sessionId);
|
|
679
|
+
if (!session || session.deleted) {
|
|
680
|
+
this.#options.onDiagnostic?.("Ignored update for unknown ACP session");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
const update = notification.update;
|
|
685
|
+
if (update.sessionUpdate === "config_option_update") {
|
|
686
|
+
session.configOptions = ConfigStateSchema.parse(update).configOptions ?? [];
|
|
687
|
+
} else if (update.sessionUpdate === "available_commands_update") {
|
|
688
|
+
session.commands = update.availableCommands.slice(0, 512).map((command) => ({
|
|
689
|
+
name: command.name.slice(0, 260),
|
|
690
|
+
kind: "client",
|
|
691
|
+
...command.description ? { description: command.description.slice(0, 4096) } : {}
|
|
692
|
+
}));
|
|
693
|
+
} else if (update.sessionUpdate === "session_info_update") {
|
|
694
|
+
if (this.#mergeSessionInfo(session, update)) {
|
|
695
|
+
this.#callbacks?.onSessionInventoryChanged(notification.sessionId);
|
|
696
|
+
}
|
|
697
|
+
} else if (!session.replaying) {
|
|
698
|
+
this.#project(session, update);
|
|
699
|
+
}
|
|
700
|
+
if (!session.replaying) this.#checkpoint(session);
|
|
701
|
+
} catch (error) {
|
|
702
|
+
this.#callbacks?.onSyncError(notification.sessionId, error);
|
|
703
|
+
this.#fail(error);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
#mergeSessionInfo(session, update) {
|
|
707
|
+
const beforeTitle = session.summary.summary;
|
|
708
|
+
const beforeTime = session.summary.modifiedAt;
|
|
709
|
+
if (update.title === null) delete session.summary.summary;
|
|
710
|
+
else if (update.title !== void 0) session.summary.summary = update.title.slice(0, 4096);
|
|
711
|
+
if (update.updatedAt === null) session.summary.modifiedAt = session.summary.createdAt;
|
|
712
|
+
else if (update.updatedAt !== void 0 && Number.isFinite(Date.parse(update.updatedAt))) {
|
|
713
|
+
session.summary.modifiedAt = new Date(update.updatedAt).toISOString();
|
|
714
|
+
}
|
|
715
|
+
return beforeTitle !== session.summary.summary || beforeTime !== session.summary.modifiedAt;
|
|
716
|
+
}
|
|
717
|
+
#project(session, update) {
|
|
718
|
+
const turnId = session.active?.id;
|
|
719
|
+
if (!turnId) return;
|
|
720
|
+
if (update.sessionUpdate === "agent_message_chunk") {
|
|
721
|
+
if (update.content.type !== "text") {
|
|
722
|
+
this.#notice(session, turnId, "warning", "Unsupported agent content", update.content.type);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const key = update.messageId ?? "stream";
|
|
726
|
+
if (session.streamMessage?.key !== key) {
|
|
727
|
+
session.streamMessage = { key, id: hash(`${turnId}:message:${update.messageId ?? randomUUID()}`) };
|
|
728
|
+
}
|
|
729
|
+
const id = session.streamMessage.id;
|
|
730
|
+
const previous = session.items.find((item) => item.id === id);
|
|
731
|
+
const content = (previous?.kind === "assistant-message" ? previous.content : "") + update.content.text;
|
|
732
|
+
this.#upsert(session, {
|
|
733
|
+
...this.#base(id, turnId),
|
|
734
|
+
kind: "assistant-message",
|
|
735
|
+
actor: "assistant",
|
|
736
|
+
content,
|
|
737
|
+
final: false
|
|
738
|
+
});
|
|
739
|
+
} else if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") {
|
|
740
|
+
if (session.streamMessage?.key === "stream") delete session.streamMessage;
|
|
741
|
+
const id = hash(`${session.summary.sessionId}:tool:${update.toolCallId}`);
|
|
742
|
+
const previous = session.items.find((item) => item.id === id);
|
|
743
|
+
const step = previous?.kind === "activity-group" ? previous.steps[0] : void 0;
|
|
744
|
+
const status = update.status === "completed" ? "completed" : update.status === "failed" ? "failed" : update.status == null && step ? step.status : "running";
|
|
745
|
+
const base = this.#base(id, previous?.turnId ?? turnId);
|
|
746
|
+
this.#upsert(session, {
|
|
747
|
+
...base,
|
|
748
|
+
kind: "activity-group",
|
|
749
|
+
actor: "assistant",
|
|
750
|
+
status,
|
|
751
|
+
steps: [{
|
|
752
|
+
id,
|
|
753
|
+
kind: "tool",
|
|
754
|
+
name: (update.title ?? step?.name ?? "Tool").slice(0, 256) || "Tool",
|
|
755
|
+
status,
|
|
756
|
+
timestamp: step?.timestamp ?? previous?.timestamp ?? base.timestamp
|
|
757
|
+
}]
|
|
758
|
+
});
|
|
759
|
+
} else if (update.sessionUpdate === "plan") {
|
|
760
|
+
const id = hash(`${session.summary.sessionId}:plan`);
|
|
761
|
+
const previous = session.items.find((item) => item.id === id);
|
|
762
|
+
this.#upsert(session, {
|
|
763
|
+
...this.#base(id, turnId),
|
|
764
|
+
kind: "plan-update",
|
|
765
|
+
actor: "assistant",
|
|
766
|
+
operation: previous ? "updated" : "created",
|
|
767
|
+
content: "",
|
|
768
|
+
todos: update.entries.map((entry, index) => ({
|
|
769
|
+
id: hash(`${id}:${index}`),
|
|
770
|
+
title: entry.content.slice(0, 1024),
|
|
771
|
+
status: entry.status,
|
|
772
|
+
dependsOn: []
|
|
773
|
+
}))
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
#permission(request, signal) {
|
|
778
|
+
const session = this.#sessions.get(request.sessionId);
|
|
779
|
+
if (!session?.active || session.active.cancelled || this.#stopping || signal.aborted) {
|
|
780
|
+
return Promise.resolve({ outcome: { outcome: "cancelled" } });
|
|
781
|
+
}
|
|
782
|
+
const id = randomUUID();
|
|
783
|
+
return new Promise((resolvePermission) => {
|
|
784
|
+
const cancel = () => finish({ outcome: { outcome: "cancelled" } });
|
|
785
|
+
const timer = setTimeout(() => {
|
|
786
|
+
this.#callbacks?.onSyncError(request.sessionId, new Error("ACP permission request timed out"));
|
|
787
|
+
cancel();
|
|
788
|
+
}, this.#options.permissionTimeoutMs ?? 5 * 6e4);
|
|
789
|
+
const finish = (response) => {
|
|
790
|
+
clearTimeout(timer);
|
|
791
|
+
signal.removeEventListener("abort", cancel);
|
|
792
|
+
this.#permissions.delete(id);
|
|
793
|
+
resolvePermission(response);
|
|
794
|
+
};
|
|
795
|
+
this.#permissions.set(id, { sessionId: request.sessionId, request, finish });
|
|
796
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
797
|
+
if (!this.#callbacks) cancel();
|
|
798
|
+
else this.#callbacks.onPermissionRequest(request.sessionId, id, this.#permissionPayload(request));
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
#permissionPayload(request) {
|
|
802
|
+
return {
|
|
803
|
+
kind: request.toolCall.kind ?? "other",
|
|
804
|
+
title: request.toolCall.title ?? "Tool",
|
|
805
|
+
toolCall: request.toolCall,
|
|
806
|
+
options: request.options
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
respondPermission(permissionRequestId, decision, _feedback) {
|
|
810
|
+
const pending = this.#permissions.get(permissionRequestId);
|
|
811
|
+
if (!pending) return false;
|
|
812
|
+
const kind = decision === "approve-once" ? "allow_once" : "reject_once";
|
|
813
|
+
const option = pending.request.options.find((entry) => entry.kind === kind);
|
|
814
|
+
pending.finish({ outcome: option ? { outcome: "selected", optionId: option.optionId } : { outcome: "cancelled" } });
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
817
|
+
async respondInteraction(_interactionRequestId, _response) {
|
|
818
|
+
throw new UnsupportedAgentOperationError("acp", "elicitations");
|
|
819
|
+
}
|
|
820
|
+
replayPendingInteractions() {
|
|
821
|
+
for (const [id, pending] of this.#permissions) {
|
|
822
|
+
this.#callbacks?.onPermissionRequest(pending.sessionId, id, this.#permissionPayload(pending.request));
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
#cancelPermissions(sessionId) {
|
|
826
|
+
for (const pending of [...this.#permissions.values()]) {
|
|
827
|
+
if (sessionId === void 0 || pending.sessionId === sessionId) {
|
|
828
|
+
pending.finish({ outcome: { outcome: "cancelled" } });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async abortSession(sessionId) {
|
|
833
|
+
const session = this.#known(sessionId);
|
|
834
|
+
session.epoch++;
|
|
835
|
+
for (const job of session.queue.splice(0)) {
|
|
836
|
+
this.#notice(session, job.id, "warning", "Cancelled", "Queued prompt cancelled");
|
|
837
|
+
}
|
|
838
|
+
const active = session.active;
|
|
839
|
+
if (active) active.cancelled = true;
|
|
840
|
+
this.#cancelPermissions(sessionId);
|
|
841
|
+
if (active) {
|
|
842
|
+
try {
|
|
843
|
+
await bounded((async () => {
|
|
844
|
+
await this.#connection.agent.notify("session/cancel", { sessionId });
|
|
845
|
+
await active.done;
|
|
846
|
+
})(), this.#options.cancelTimeoutMs ?? 5e3, "cancellation");
|
|
847
|
+
} catch (error) {
|
|
848
|
+
this.#fail(error);
|
|
849
|
+
await this.stop();
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
await this.#save(session);
|
|
854
|
+
}
|
|
855
|
+
#fail(error) {
|
|
856
|
+
if (this.#fatalError || this.#stopping) return;
|
|
857
|
+
this.#fatalError = error instanceof Error ? error : new Error(String(error));
|
|
858
|
+
this.#options.onDiagnostic?.(`ACP failure: ${this.#fatalError.message}${this.#stderr ? `
|
|
859
|
+
${this.#stderr}` : ""}`);
|
|
860
|
+
for (const session of this.#sessions.values()) {
|
|
861
|
+
this.#clearCheckpoint(session);
|
|
862
|
+
session.epoch++;
|
|
863
|
+
for (const job of session.queue.splice(0)) {
|
|
864
|
+
this.#notice(session, job.id, "error", "Prompt failed", this.#fatalError.message);
|
|
865
|
+
}
|
|
866
|
+
this.#callbacks?.onSyncError(session.summary.sessionId, this.#fatalError);
|
|
867
|
+
}
|
|
868
|
+
this.#cancelPermissions();
|
|
869
|
+
this.#connection?.close(this.#fatalError);
|
|
870
|
+
this.#child?.kill("SIGTERM");
|
|
871
|
+
void this.stop().catch((stopError) => {
|
|
872
|
+
this.#options.onDiagnostic?.(`ACP cleanup failed: ${errorMessage(stopError)}`);
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
stop() {
|
|
876
|
+
if (!this.#stopPromise) this.#stopPromise = this.#stop();
|
|
877
|
+
return this.#stopPromise;
|
|
878
|
+
}
|
|
879
|
+
async #stop() {
|
|
880
|
+
this.#stopping = true;
|
|
881
|
+
this.#started = false;
|
|
882
|
+
this.#cancelPermissions();
|
|
883
|
+
const active = [];
|
|
884
|
+
for (const session of this.#sessions.values()) {
|
|
885
|
+
this.#clearCheckpoint(session);
|
|
886
|
+
session.epoch++;
|
|
887
|
+
for (const job of session.queue.splice(0)) {
|
|
888
|
+
this.#notice(session, job.id, "warning", "Cancelled", "Agent stopped");
|
|
889
|
+
}
|
|
890
|
+
if (session.active) {
|
|
891
|
+
if (!this.#fatalError) session.active.cancelled = true;
|
|
892
|
+
if (session.active.done) active.push(session.active.done);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (this.#connection && !this.#fatalError) {
|
|
896
|
+
try {
|
|
897
|
+
await bounded((async () => {
|
|
898
|
+
await Promise.all([...this.#sessions.values()].filter((session) => session.active).map((session) => this.#connection.agent.notify("session/cancel", {
|
|
899
|
+
sessionId: session.summary.sessionId
|
|
900
|
+
})));
|
|
901
|
+
await Promise.all(active);
|
|
902
|
+
if (this.#capabilities.sessionCapabilities?.close != null) {
|
|
903
|
+
await Promise.all([...this.#sessions.values()].filter((session) => session.loaded).map((session) => this.#connection.agent.request("session/close", {
|
|
904
|
+
sessionId: session.summary.sessionId
|
|
905
|
+
})));
|
|
906
|
+
}
|
|
907
|
+
})(), this.#options.cancelTimeoutMs ?? 5e3, "graceful shutdown");
|
|
908
|
+
} catch (error) {
|
|
909
|
+
this.#options.onDiagnostic?.(errorMessage(error));
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
this.#connection?.close(new Error("ACP adapter stopped"));
|
|
913
|
+
const child = this.#child;
|
|
914
|
+
if (child && child.exitCode === null && child.signalCode === null) {
|
|
915
|
+
child.kill("SIGTERM");
|
|
916
|
+
try {
|
|
917
|
+
await bounded(this.#exit, this.#options.cancelTimeoutMs ?? 5e3, "process exit");
|
|
918
|
+
} catch (error) {
|
|
919
|
+
this.#options.onDiagnostic?.(errorMessage(error));
|
|
920
|
+
child.kill("SIGKILL");
|
|
921
|
+
await bounded(this.#exit, this.#options.cancelTimeoutMs ?? 5e3, "process termination");
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
child?.stdin.destroy();
|
|
925
|
+
child?.stdout.destroy();
|
|
926
|
+
child?.stderr.destroy();
|
|
927
|
+
const completed = await Promise.allSettled(active);
|
|
928
|
+
const saved = await Promise.allSettled([...this.#sessions.values()].filter((session) => !session.deleted).map((session) => this.#save(session)));
|
|
929
|
+
const failures = [...completed, ...saved].filter((result) => result.status === "rejected");
|
|
930
|
+
if (failures.length) throw failures[0].reason;
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
export {
|
|
934
|
+
ACP_DEFAULT_MODEL_ID,
|
|
935
|
+
AcpCodingAgentAdapter
|
|
936
|
+
};
|