@moxt-ai/mobius 0.0.2 → 0.0.4
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/acp/activity.d.ts +5 -2
- package/dist/acp/activity.d.ts.map +1 -1
- package/dist/acp/activity.js +89 -5
- package/dist/acp/activity.js.map +1 -1
- package/dist/acp/create-client.d.ts +8 -2
- package/dist/acp/create-client.d.ts.map +1 -1
- package/dist/acp/create-client.js +10 -2
- package/dist/acp/create-client.js.map +1 -1
- package/dist/acp/run-prompt.d.ts.map +1 -1
- package/dist/acp/run-prompt.js +4 -0
- package/dist/acp/run-prompt.js.map +1 -1
- package/dist/acp/session-manager.d.ts +100 -4
- package/dist/acp/session-manager.d.ts.map +1 -1
- package/dist/acp/session-manager.js +1239 -138
- package/dist/acp/session-manager.js.map +1 -1
- package/dist/acp/session-store.d.ts +37 -0
- package/dist/acp/session-store.d.ts.map +1 -0
- package/dist/acp/session-store.js +158 -0
- package/dist/acp/session-store.js.map +1 -0
- package/dist/authorization/workspace-directory.d.ts +17 -0
- package/dist/authorization/workspace-directory.d.ts.map +1 -0
- package/dist/authorization/workspace-directory.js +164 -0
- package/dist/authorization/workspace-directory.js.map +1 -0
- package/dist/cli.js +22429 -697
- package/dist/cli.js.map +4 -4
- package/dist/index.js +22435 -677
- package/dist/index.js.map +4 -4
- package/dist/relay-connection/liveness.d.ts +14 -0
- package/dist/relay-connection/liveness.d.ts.map +1 -0
- package/dist/relay-connection/liveness.js +102 -0
- package/dist/relay-connection/liveness.js.map +1 -0
- package/dist/relay-connection/timer.d.ts +8 -0
- package/dist/relay-connection/timer.d.ts.map +1 -0
- package/dist/relay-connection/timer.js +8 -0
- package/dist/relay-connection/timer.js.map +1 -0
- package/dist/run-daemon.d.ts +2 -1
- package/dist/run-daemon.d.ts.map +1 -1
- package/dist/run-daemon.js +352 -20
- package/dist/run-daemon.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,44 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { once } from "node:events";
|
|
3
3
|
import { methods, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
|
|
4
|
-
import {
|
|
4
|
+
import { AGENT_CONTENT_KIND, MOBIUS_PROTOCOL_VERSION, SESSION_COMMANDS_UPDATED_KIND, SESSION_CONFIG_UPDATED_KIND, SESSION_ELICITATION_REQUESTED_KIND, SESSION_ELICITATION_RESOLVED_KIND, SESSION_INFO_UPDATED_KIND, SESSION_PERMISSION_REQUESTED_KIND, SESSION_PERMISSION_RESOLVED_KIND, SESSION_USAGE_UPDATED_KIND, } from "@moxt-ai/mobius-protocol";
|
|
5
|
+
import { AcpActivityTracker, isProgressMessage, normalizeContentBlock, } from "./activity.js";
|
|
5
6
|
import { createAcpClient } from "./create-client.js";
|
|
7
|
+
import { AcpStoredSession, FoundAcpStoredSession, MemoryAcpSessionStore, } from "./session-store.js";
|
|
6
8
|
const PROCESS_STOP_TIMEOUT_MILLISECONDS = 1_000;
|
|
9
|
+
const SESSION_CONTROL_COMMAND_ID = "session-control";
|
|
10
|
+
export class NullAcpSessionObserver {
|
|
11
|
+
onCommands = () => Promise.resolve();
|
|
12
|
+
onConfig = () => Promise.resolve();
|
|
13
|
+
onContent = () => Promise.resolve();
|
|
14
|
+
onElicitation = () => Promise.resolve();
|
|
15
|
+
onElicitationResolved = () => Promise.resolve();
|
|
16
|
+
onInfo = () => Promise.resolve();
|
|
17
|
+
onPermission = () => Promise.resolve();
|
|
18
|
+
onPermissionResolved = () => Promise.resolve();
|
|
19
|
+
onUsage = () => Promise.resolve();
|
|
20
|
+
}
|
|
21
|
+
export class AcpPreparedSession {
|
|
22
|
+
agentName;
|
|
23
|
+
agentVersion;
|
|
24
|
+
capabilities;
|
|
25
|
+
commands;
|
|
26
|
+
configOptions;
|
|
27
|
+
restored;
|
|
28
|
+
constructor(agentName, agentVersion, capabilities, commands, configOptions, restored) {
|
|
29
|
+
this.agentName = agentName;
|
|
30
|
+
this.agentVersion = agentVersion;
|
|
31
|
+
this.capabilities = capabilities;
|
|
32
|
+
this.commands = commands;
|
|
33
|
+
this.configOptions = configOptions;
|
|
34
|
+
this.restored = restored;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
7
37
|
export class AcpExecutionError extends Error {
|
|
8
|
-
code
|
|
9
|
-
constructor(message, options = {}) {
|
|
38
|
+
code;
|
|
39
|
+
constructor(message, options = {}, code = "acp_execution_failed") {
|
|
10
40
|
super(message, options);
|
|
41
|
+
this.code = code;
|
|
11
42
|
this.name = "AcpExecutionError";
|
|
12
43
|
}
|
|
13
44
|
}
|
|
@@ -46,15 +77,40 @@ function readableWebStream(input) {
|
|
|
46
77
|
},
|
|
47
78
|
});
|
|
48
79
|
}
|
|
49
|
-
function
|
|
80
|
+
export function createWritableAcpStream(output) {
|
|
81
|
+
let outputFailure;
|
|
82
|
+
const pendingWriteFailures = new Set();
|
|
83
|
+
const fail = (error) => {
|
|
84
|
+
outputFailure = error;
|
|
85
|
+
for (const reject of pendingWriteFailures) {
|
|
86
|
+
reject(error);
|
|
87
|
+
}
|
|
88
|
+
pendingWriteFailures.clear();
|
|
89
|
+
};
|
|
90
|
+
output.on("error", fail);
|
|
91
|
+
output.once("close", () => {
|
|
92
|
+
if (outputFailure === undefined) {
|
|
93
|
+
fail(new Error("ACP process input closed"));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
50
96
|
return new WritableStream({
|
|
51
97
|
write(chunk) {
|
|
98
|
+
if (outputFailure !== undefined) {
|
|
99
|
+
return Promise.reject(outputFailure);
|
|
100
|
+
}
|
|
52
101
|
return new Promise((resolve, reject) => {
|
|
102
|
+
pendingWriteFailures.add(reject);
|
|
53
103
|
output.write(chunk, (error) => {
|
|
104
|
+
pendingWriteFailures.delete(reject);
|
|
54
105
|
if (error) {
|
|
106
|
+
outputFailure = error;
|
|
55
107
|
reject(error);
|
|
56
108
|
return;
|
|
57
109
|
}
|
|
110
|
+
if (outputFailure !== undefined) {
|
|
111
|
+
reject(outputFailure);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
58
114
|
resolve();
|
|
59
115
|
});
|
|
60
116
|
});
|
|
@@ -75,176 +131,1199 @@ async function stopAgentProcess(child) {
|
|
|
75
131
|
catch {
|
|
76
132
|
if (child.exitCode === null) {
|
|
77
133
|
child.kill("SIGKILL");
|
|
78
|
-
|
|
134
|
+
try {
|
|
135
|
+
await once(child, "exit", {
|
|
136
|
+
signal: AbortSignal.timeout(PROCESS_STOP_TIMEOUT_MILLISECONDS),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Teardown must not wait forever for a process the OS did not reap.
|
|
141
|
+
}
|
|
79
142
|
}
|
|
80
143
|
}
|
|
81
144
|
}
|
|
82
|
-
function
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
145
|
+
function truncate(value, maximumLength) {
|
|
146
|
+
return value.length <= maximumLength
|
|
147
|
+
? value
|
|
148
|
+
: `${value.slice(0, Math.max(0, maximumLength - 1))}…`;
|
|
149
|
+
}
|
|
150
|
+
function isRecord(value) {
|
|
151
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
152
|
+
}
|
|
153
|
+
function readString(value, fallback = "") {
|
|
154
|
+
return typeof value === "string" ? value : fallback;
|
|
155
|
+
}
|
|
156
|
+
function readBoolean(value, fallback = false) {
|
|
157
|
+
return typeof value === "boolean" ? value : fallback;
|
|
158
|
+
}
|
|
159
|
+
function arraysEqual(left, right) {
|
|
160
|
+
return (left.length === right.length &&
|
|
161
|
+
left.every((value, index) => value === right[index]));
|
|
162
|
+
}
|
|
163
|
+
function normalizeConfigValue(option, groupId = "", groupName = "") {
|
|
164
|
+
return {
|
|
165
|
+
description: truncate(readString(option["description"]), 500),
|
|
166
|
+
groupId: truncate(groupId, 128),
|
|
167
|
+
groupName: truncate(groupName, 255),
|
|
168
|
+
name: truncate(readString(option["name"], readString(option["value"])), 255),
|
|
169
|
+
value: truncate(readString(option["value"]), 500),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function normalizeConfigValues(value) {
|
|
173
|
+
if (!Array.isArray(value)) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const normalized = [];
|
|
177
|
+
for (const entry of value) {
|
|
178
|
+
if (!isRecord(entry)) {
|
|
179
|
+
continue;
|
|
90
180
|
}
|
|
91
|
-
|
|
181
|
+
const groupedOptions = entry["options"];
|
|
182
|
+
if (Array.isArray(groupedOptions)) {
|
|
183
|
+
const groupId = readString(entry["group"]);
|
|
184
|
+
const groupName = readString(entry["name"]);
|
|
185
|
+
for (const groupedOption of groupedOptions) {
|
|
186
|
+
if (isRecord(groupedOption)) {
|
|
187
|
+
normalized.push(normalizeConfigValue(groupedOption, groupId, groupName));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
normalized.push(normalizeConfigValue(entry));
|
|
193
|
+
}
|
|
194
|
+
return normalized.filter((entry) => entry.value.length > 0).slice(0, 500);
|
|
195
|
+
}
|
|
196
|
+
function normalizeConfigOptions(value, modes) {
|
|
197
|
+
const normalized = [];
|
|
198
|
+
if (Array.isArray(value)) {
|
|
199
|
+
for (const option of value) {
|
|
200
|
+
if (!isRecord(option)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const id = truncate(readString(option["id"]), 128);
|
|
204
|
+
const name = truncate(readString(option["name"], id), 255);
|
|
205
|
+
if (id.length === 0 || name.length === 0) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (option["type"] === "boolean") {
|
|
209
|
+
normalized.push({
|
|
210
|
+
booleanValue: readBoolean(option["currentValue"]),
|
|
211
|
+
category: truncate(readString(option["category"]), 128),
|
|
212
|
+
currentValue: "",
|
|
213
|
+
description: truncate(readString(option["description"]), 500),
|
|
214
|
+
id,
|
|
215
|
+
name,
|
|
216
|
+
type: "boolean",
|
|
217
|
+
values: [],
|
|
218
|
+
});
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (option["type"] === "select") {
|
|
222
|
+
const values = normalizeConfigValues(option["options"]);
|
|
223
|
+
const currentValue = truncate(readString(option["currentValue"]), 500);
|
|
224
|
+
if (currentValue.length > 0 &&
|
|
225
|
+
values.some((entry) => entry.value === currentValue)) {
|
|
226
|
+
normalized.push({
|
|
227
|
+
booleanValue: false,
|
|
228
|
+
category: truncate(readString(option["category"]), 128),
|
|
229
|
+
currentValue,
|
|
230
|
+
description: truncate(readString(option["description"]), 500),
|
|
231
|
+
id,
|
|
232
|
+
name,
|
|
233
|
+
type: "select",
|
|
234
|
+
values,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (isRecord(modes)) {
|
|
241
|
+
const values = [];
|
|
242
|
+
const availableModes = modes["availableModes"];
|
|
243
|
+
if (Array.isArray(availableModes)) {
|
|
244
|
+
for (const mode of availableModes) {
|
|
245
|
+
if (!isRecord(mode)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const id = truncate(readString(mode["id"]), 500);
|
|
249
|
+
if (id.length === 0) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
values.push({
|
|
253
|
+
description: truncate(readString(mode["description"]), 500),
|
|
254
|
+
groupId: "",
|
|
255
|
+
groupName: "",
|
|
256
|
+
name: truncate(readString(mode["name"], id), 255),
|
|
257
|
+
value: id,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const currentValue = truncate(readString(modes["currentModeId"]), 500);
|
|
262
|
+
if (currentValue.length > 0 &&
|
|
263
|
+
values.some((entry) => entry.value === currentValue) &&
|
|
264
|
+
!normalized.some((entry) => entry.category === "mode")) {
|
|
265
|
+
normalized.unshift({
|
|
266
|
+
booleanValue: false,
|
|
267
|
+
category: "mode",
|
|
268
|
+
currentValue,
|
|
269
|
+
description: "Agent session mode",
|
|
270
|
+
id: "mode",
|
|
271
|
+
name: "Mode",
|
|
272
|
+
type: "select",
|
|
273
|
+
values,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return normalized.slice(0, 100);
|
|
278
|
+
}
|
|
279
|
+
function replaceConfigOptionsKeepingMode(current, value) {
|
|
280
|
+
const next = normalizeConfigOptions(value, undefined);
|
|
281
|
+
if (next.some((option) => option.category === "mode")) {
|
|
282
|
+
return next;
|
|
283
|
+
}
|
|
284
|
+
return [
|
|
285
|
+
...current.filter((option) => option.category === "mode"),
|
|
286
|
+
...next,
|
|
287
|
+
].slice(0, 100);
|
|
288
|
+
}
|
|
289
|
+
function normalizeCapabilities(capabilities) {
|
|
290
|
+
const session = capabilities.sessionCapabilities;
|
|
291
|
+
const prompt = capabilities.promptCapabilities;
|
|
292
|
+
return {
|
|
293
|
+
additionalDirectories: session?.additionalDirectories != null,
|
|
294
|
+
audioPrompt: prompt?.audio === true,
|
|
295
|
+
closeSession: session?.close != null,
|
|
296
|
+
deleteSession: session?.delete != null,
|
|
297
|
+
embeddedContextPrompt: prompt?.embeddedContext === true,
|
|
298
|
+
imagePrompt: prompt?.image === true,
|
|
299
|
+
listSessions: session?.list != null,
|
|
300
|
+
loadSession: capabilities.loadSession === true,
|
|
301
|
+
resumeSession: session?.resume != null,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function elicitationFieldType(value) {
|
|
305
|
+
if (value === "boolean") {
|
|
306
|
+
return "boolean";
|
|
307
|
+
}
|
|
308
|
+
if (value === "integer") {
|
|
309
|
+
return "integer";
|
|
310
|
+
}
|
|
311
|
+
if (value === "number") {
|
|
312
|
+
return "number";
|
|
313
|
+
}
|
|
314
|
+
if (value === "array") {
|
|
315
|
+
return "multiselect";
|
|
316
|
+
}
|
|
317
|
+
return "string";
|
|
318
|
+
}
|
|
319
|
+
function normalizeElicitationOptions(schema) {
|
|
320
|
+
const normalized = [];
|
|
321
|
+
const oneOf = schema["oneOf"];
|
|
322
|
+
if (Array.isArray(oneOf)) {
|
|
323
|
+
for (const option of oneOf) {
|
|
324
|
+
if (isRecord(option) && typeof option["const"] === "string") {
|
|
325
|
+
normalized.push({
|
|
326
|
+
description: truncate(readString(option["description"]), 500),
|
|
327
|
+
label: truncate(readString(option["title"], option["const"]), 255),
|
|
328
|
+
value: truncate(option["const"], 500),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return normalized.slice(0, 200);
|
|
333
|
+
}
|
|
334
|
+
const enumValues = schema["enum"];
|
|
335
|
+
if (Array.isArray(enumValues)) {
|
|
336
|
+
for (const entry of enumValues) {
|
|
337
|
+
if (typeof entry === "string") {
|
|
338
|
+
normalized.push({ description: "", label: entry, value: entry });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return normalized.slice(0, 200);
|
|
342
|
+
}
|
|
343
|
+
const items = schema["items"];
|
|
344
|
+
if (!isRecord(items)) {
|
|
345
|
+
return normalized;
|
|
346
|
+
}
|
|
347
|
+
const anyOf = items["anyOf"];
|
|
348
|
+
if (Array.isArray(anyOf)) {
|
|
349
|
+
return normalizeElicitationOptions({ oneOf: anyOf });
|
|
350
|
+
}
|
|
351
|
+
return normalizeElicitationOptions({ enum: items["enum"] });
|
|
352
|
+
}
|
|
353
|
+
function numericString(value) {
|
|
354
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
355
|
+
? value.toString()
|
|
356
|
+
: "";
|
|
357
|
+
}
|
|
358
|
+
function normalizeElicitationFields(requestedSchema) {
|
|
359
|
+
if (!isRecord(requestedSchema) || !isRecord(requestedSchema["properties"])) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
const properties = requestedSchema["properties"];
|
|
363
|
+
const requiredValues = requestedSchema["required"];
|
|
364
|
+
const required = new Set(Array.isArray(requiredValues)
|
|
365
|
+
? requiredValues.filter((entry) => typeof entry === "string")
|
|
366
|
+
: []);
|
|
367
|
+
const normalized = [];
|
|
368
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
369
|
+
if (!isRecord(property)) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const type = elicitationFieldType(property["type"]);
|
|
373
|
+
const options = normalizeElicitationOptions(property);
|
|
374
|
+
const normalizedType = type === "string" && options.length > 0 ? "select" : type;
|
|
375
|
+
const defaultValue = property["default"];
|
|
376
|
+
normalized.push({
|
|
377
|
+
defaultBooleanValue: typeof defaultValue === "boolean" ? defaultValue : false,
|
|
378
|
+
defaultNumberValue: typeof defaultValue === "number" && Number.isFinite(defaultValue)
|
|
379
|
+
? defaultValue
|
|
380
|
+
: 0,
|
|
381
|
+
defaultStringValue: typeof defaultValue === "string"
|
|
382
|
+
? truncate(defaultValue, 10_000)
|
|
383
|
+
: Array.isArray(defaultValue)
|
|
384
|
+
? truncate(JSON.stringify(defaultValue), 10_000)
|
|
385
|
+
: "",
|
|
386
|
+
description: truncate(readString(property["description"]), 500),
|
|
387
|
+
format: truncate(readString(property["format"]), 64),
|
|
388
|
+
hasDefaultValue: defaultValue !== undefined && defaultValue !== null,
|
|
389
|
+
maximum: numericString(property["maximum"]),
|
|
390
|
+
maximumLength: numericString(property["maxLength"] ?? property["maxItems"]),
|
|
391
|
+
minimum: numericString(property["minimum"]),
|
|
392
|
+
minimumLength: numericString(property["minLength"] ?? property["minItems"]),
|
|
393
|
+
name: truncate(name, 128),
|
|
394
|
+
options,
|
|
395
|
+
pattern: truncate(readString(property["pattern"]), 500),
|
|
396
|
+
required: required.has(name),
|
|
397
|
+
title: truncate(readString(property["title"], name), 255),
|
|
398
|
+
type: normalizedType,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return normalized.slice(0, 100);
|
|
402
|
+
}
|
|
403
|
+
function validateStringValue(name, value, schema) {
|
|
404
|
+
const minimum = schema["minLength"];
|
|
405
|
+
const maximum = schema["maxLength"];
|
|
406
|
+
if (typeof minimum === "number" && value.length < minimum) {
|
|
407
|
+
throw new AcpExecutionError(`Elicitation field ${name} is shorter than allowed`, {}, "invalid_elicitation_response");
|
|
408
|
+
}
|
|
409
|
+
if (typeof maximum === "number" && value.length > maximum) {
|
|
410
|
+
throw new AcpExecutionError(`Elicitation field ${name} is longer than allowed`, {}, "invalid_elicitation_response");
|
|
411
|
+
}
|
|
412
|
+
const pattern = schema["pattern"];
|
|
413
|
+
if (typeof pattern === "string" && !new RegExp(pattern, "u").test(value)) {
|
|
414
|
+
throw new AcpExecutionError(`Elicitation field ${name} does not match the required format`, {}, "invalid_elicitation_response");
|
|
415
|
+
}
|
|
416
|
+
const allowed = normalizeElicitationOptions(schema);
|
|
417
|
+
if (allowed.length > 0 && !allowed.some((entry) => entry.value === value)) {
|
|
418
|
+
throw new AcpExecutionError(`Elicitation field ${name} is not an allowed value`, {}, "invalid_elicitation_response");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function validateNumericValue(name, value, schema, integer) {
|
|
422
|
+
if (!Number.isFinite(value) || (integer && !Number.isInteger(value))) {
|
|
423
|
+
throw new AcpExecutionError(`Elicitation field ${name} is not a valid number`, {}, "invalid_elicitation_response");
|
|
424
|
+
}
|
|
425
|
+
const minimum = schema["minimum"];
|
|
426
|
+
const maximum = schema["maximum"];
|
|
427
|
+
if ((typeof minimum === "number" && value < minimum) ||
|
|
428
|
+
(typeof maximum === "number" && value > maximum)) {
|
|
429
|
+
throw new AcpExecutionError(`Elicitation field ${name} is outside the allowed range`, {}, "invalid_elicitation_response");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function validateElicitationContent(encoded, requestedSchema) {
|
|
433
|
+
let decoded;
|
|
434
|
+
try {
|
|
435
|
+
decoded = JSON.parse(encoded);
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
throw new AcpExecutionError("Elicitation response is not valid JSON", { cause: error }, "invalid_elicitation_response");
|
|
439
|
+
}
|
|
440
|
+
if (!isRecord(decoded) ||
|
|
441
|
+
!isRecord(requestedSchema) ||
|
|
442
|
+
!isRecord(requestedSchema["properties"])) {
|
|
443
|
+
throw new AcpExecutionError("Elicitation response does not match the requested form", {}, "invalid_elicitation_response");
|
|
444
|
+
}
|
|
445
|
+
const properties = requestedSchema["properties"];
|
|
446
|
+
const requiredValue = requestedSchema["required"];
|
|
447
|
+
const required = new Set(Array.isArray(requiredValue)
|
|
448
|
+
? requiredValue.filter((entry) => typeof entry === "string")
|
|
449
|
+
: []);
|
|
450
|
+
for (const name of required) {
|
|
451
|
+
if (!(name in decoded)) {
|
|
452
|
+
throw new AcpExecutionError(`Elicitation field ${name} is required`, {}, "invalid_elicitation_response");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const validated = {};
|
|
456
|
+
for (const [name, value] of Object.entries(decoded)) {
|
|
457
|
+
const property = properties[name];
|
|
458
|
+
if (!isRecord(property)) {
|
|
459
|
+
throw new AcpExecutionError(`Elicitation field ${name} was not requested`, {}, "invalid_elicitation_response");
|
|
460
|
+
}
|
|
461
|
+
if (property["type"] === "string" && typeof value === "string") {
|
|
462
|
+
validateStringValue(name, value, property);
|
|
463
|
+
validated[name] = value;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (property["type"] === "number" && typeof value === "number") {
|
|
467
|
+
validateNumericValue(name, value, property, false);
|
|
468
|
+
validated[name] = value;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (property["type"] === "integer" && typeof value === "number") {
|
|
472
|
+
validateNumericValue(name, value, property, true);
|
|
473
|
+
validated[name] = value;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (property["type"] === "boolean" && typeof value === "boolean") {
|
|
477
|
+
validated[name] = value;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (property["type"] === "array" &&
|
|
481
|
+
Array.isArray(value) &&
|
|
482
|
+
value.every((entry) => typeof entry === "string")) {
|
|
483
|
+
const minimum = property["minItems"];
|
|
484
|
+
const maximum = property["maxItems"];
|
|
485
|
+
if ((typeof minimum === "number" && value.length < minimum) ||
|
|
486
|
+
(typeof maximum === "number" && value.length > maximum)) {
|
|
487
|
+
throw new AcpExecutionError(`Elicitation field ${name} has an invalid selection count`, {}, "invalid_elicitation_response");
|
|
488
|
+
}
|
|
489
|
+
const allowed = normalizeElicitationOptions(property);
|
|
490
|
+
if (allowed.length > 0 &&
|
|
491
|
+
value.some((entry) => !allowed.some((option) => option.value === entry))) {
|
|
492
|
+
throw new AcpExecutionError(`Elicitation field ${name} contains an invalid selection`, {}, "invalid_elicitation_response");
|
|
493
|
+
}
|
|
494
|
+
validated[name] = value;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
throw new AcpExecutionError(`Elicitation field ${name} has the wrong type`, {}, "invalid_elicitation_response");
|
|
498
|
+
}
|
|
499
|
+
return validated;
|
|
500
|
+
}
|
|
501
|
+
class AcpTurnState {
|
|
502
|
+
}
|
|
503
|
+
class IdleAcpTurn extends AcpTurnState {
|
|
504
|
+
commandId = SESSION_CONTROL_COMMAND_ID;
|
|
505
|
+
running = false;
|
|
506
|
+
activities = new AcpActivityTracker(() => Promise.resolve());
|
|
507
|
+
onText = () => Promise.resolve();
|
|
508
|
+
}
|
|
509
|
+
class RunningAcpTurn extends AcpTurnState {
|
|
510
|
+
commandId;
|
|
511
|
+
running = true;
|
|
512
|
+
activities;
|
|
513
|
+
onText;
|
|
514
|
+
constructor(commandId, onActivity, onText) {
|
|
515
|
+
super();
|
|
516
|
+
this.commandId = commandId;
|
|
517
|
+
this.activities = new AcpActivityTracker(onActivity);
|
|
518
|
+
this.onText = onText;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
class PendingPermission {
|
|
522
|
+
completion = Promise.withResolvers();
|
|
523
|
+
interactionId;
|
|
524
|
+
optionIds;
|
|
525
|
+
#settled = false;
|
|
526
|
+
constructor(interactionId, optionIds) {
|
|
527
|
+
this.interactionId = interactionId;
|
|
528
|
+
this.optionIds = optionIds;
|
|
529
|
+
}
|
|
530
|
+
cancel = () => {
|
|
531
|
+
if (this.#settled) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
this.#settled = true;
|
|
535
|
+
this.completion.resolve({ outcome: { outcome: "cancelled" } });
|
|
536
|
+
return true;
|
|
537
|
+
};
|
|
538
|
+
select = (optionId) => {
|
|
539
|
+
if (this.#settled) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
this.#settled = true;
|
|
543
|
+
this.completion.resolve({
|
|
544
|
+
outcome: { optionId, outcome: "selected" },
|
|
545
|
+
});
|
|
546
|
+
return true;
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
class PendingElicitation {
|
|
550
|
+
completion = Promise.withResolvers();
|
|
551
|
+
elicitationId;
|
|
552
|
+
interactionId;
|
|
553
|
+
requestedSchema;
|
|
554
|
+
#settled = false;
|
|
555
|
+
constructor(interactionId, elicitationId, requestedSchema) {
|
|
556
|
+
this.elicitationId = elicitationId;
|
|
557
|
+
this.interactionId = interactionId;
|
|
558
|
+
this.requestedSchema = requestedSchema;
|
|
559
|
+
}
|
|
560
|
+
accept = (content) => {
|
|
561
|
+
if (this.#settled) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
this.#settled = true;
|
|
565
|
+
this.completion.resolve({ action: "accept", content });
|
|
566
|
+
return true;
|
|
567
|
+
};
|
|
568
|
+
cancel = () => {
|
|
569
|
+
if (this.#settled) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
this.#settled = true;
|
|
573
|
+
this.completion.resolve({ action: "cancel" });
|
|
574
|
+
return true;
|
|
575
|
+
};
|
|
576
|
+
decline = () => {
|
|
577
|
+
if (this.#settled) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
this.#settled = true;
|
|
581
|
+
this.completion.resolve({ action: "decline" });
|
|
582
|
+
return true;
|
|
583
|
+
};
|
|
92
584
|
}
|
|
93
585
|
class AcpProcessSession {
|
|
586
|
+
#additionalDirectories;
|
|
587
|
+
#acceptedUrlElicitations = new Map();
|
|
94
588
|
#agentId;
|
|
95
589
|
#child;
|
|
96
590
|
#connection;
|
|
591
|
+
#cwd;
|
|
592
|
+
#getObserver;
|
|
593
|
+
#mobiusSessionId;
|
|
594
|
+
#pendingElicitations = new Map();
|
|
595
|
+
#pendingPermissions = new Map();
|
|
97
596
|
#processExit;
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
597
|
+
#store;
|
|
598
|
+
#agentName = "";
|
|
599
|
+
#agentVersion = "";
|
|
600
|
+
#capabilities = {
|
|
601
|
+
additionalDirectories: false,
|
|
602
|
+
audioPrompt: false,
|
|
603
|
+
closeSession: false,
|
|
604
|
+
deleteSession: false,
|
|
605
|
+
embeddedContextPrompt: false,
|
|
606
|
+
imagePrompt: false,
|
|
607
|
+
listSessions: false,
|
|
608
|
+
loadSession: false,
|
|
609
|
+
resumeSession: false,
|
|
610
|
+
};
|
|
611
|
+
#commands = [];
|
|
612
|
+
#configOptions = [];
|
|
613
|
+
#nativeSessionId = "";
|
|
614
|
+
#prompted = false;
|
|
615
|
+
#replaying = false;
|
|
616
|
+
#restored = false;
|
|
617
|
+
#sessionTitle = "";
|
|
618
|
+
#sessionUpdatedAt = "";
|
|
619
|
+
#turn = new IdleAcpTurn();
|
|
620
|
+
#updateTail = Promise.resolve();
|
|
621
|
+
constructor(agent, cwd, additionalDirectories, mobiusSessionId, getObserver, store) {
|
|
622
|
+
this.#additionalDirectories = [...additionalDirectories];
|
|
623
|
+
this.#agentId = agent.id;
|
|
624
|
+
this.#cwd = cwd;
|
|
625
|
+
this.#getObserver = getObserver;
|
|
626
|
+
this.#mobiusSessionId = mobiusSessionId;
|
|
627
|
+
this.#store = store;
|
|
628
|
+
this.#child = spawn(agent.command, [...agent.arguments], {
|
|
110
629
|
cwd,
|
|
111
630
|
stdio: ["pipe", "pipe", "pipe"],
|
|
112
631
|
});
|
|
113
|
-
child.
|
|
114
|
-
|
|
632
|
+
const stdin = this.#child.stdin;
|
|
633
|
+
const stdout = this.#child.stdout;
|
|
634
|
+
const stderr = this.#child.stderr;
|
|
635
|
+
if (stdin === null || stdout === null || stderr === null) {
|
|
636
|
+
this.#child.kill("SIGKILL");
|
|
637
|
+
throw new AcpExecutionError("ACP process streams are unavailable");
|
|
638
|
+
}
|
|
639
|
+
stderr.setEncoding("utf8");
|
|
640
|
+
stderr.on("data", (chunk) => {
|
|
115
641
|
process.stderr.write(`[${agent.id}-acp] ${chunk}`);
|
|
116
642
|
});
|
|
117
|
-
|
|
118
|
-
child.once("error", resolve);
|
|
119
|
-
child.once("exit", resolve);
|
|
643
|
+
this.#processExit = new Promise((resolve) => {
|
|
644
|
+
this.#child.once("error", resolve);
|
|
645
|
+
this.#child.once("exit", resolve);
|
|
120
646
|
});
|
|
121
|
-
const stream = ndJsonStream(
|
|
122
|
-
|
|
647
|
+
const stream = ndJsonStream(createWritableAcpStream(stdin), readableWebStream(stdout));
|
|
648
|
+
this.#connection = createAcpClient(this).connect(stream);
|
|
649
|
+
}
|
|
650
|
+
static start = async (options, getObserver, store) => {
|
|
651
|
+
options.signal.throwIfAborted();
|
|
652
|
+
const runtime = new AcpProcessSession(options.agent, options.cwd, options.additionalDirectories, options.sessionId, getObserver, store);
|
|
123
653
|
try {
|
|
124
|
-
await
|
|
125
|
-
|
|
126
|
-
clientCapabilities: {},
|
|
127
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
128
|
-
}, { cancellationSignal: signal }),
|
|
129
|
-
processExit.then(() => {
|
|
130
|
-
throw new AcpExecutionError("ACP agent exited during initialization");
|
|
131
|
-
}),
|
|
132
|
-
rejectWhenAborted(signal),
|
|
133
|
-
]);
|
|
134
|
-
const session = await Promise.race([
|
|
135
|
-
connection.agent
|
|
136
|
-
.buildSession(cwd)
|
|
137
|
-
.start({ cancellationSignal: signal }),
|
|
138
|
-
processExit.then(() => {
|
|
139
|
-
throw new AcpExecutionError("ACP agent exited while starting a session");
|
|
140
|
-
}),
|
|
141
|
-
rejectWhenAborted(signal),
|
|
142
|
-
]);
|
|
143
|
-
return new AcpProcessSession(agent.id, child, connection, session, processExit);
|
|
654
|
+
await runtime.initialize(options.signal);
|
|
655
|
+
return runtime;
|
|
144
656
|
}
|
|
145
657
|
catch (error) {
|
|
146
|
-
|
|
147
|
-
|
|
658
|
+
await runtime.closeProcess();
|
|
659
|
+
if (error instanceof AcpExecutionError) {
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
148
662
|
throw new AcpExecutionError("ACP agent initialization failed", {
|
|
149
663
|
cause: error,
|
|
150
664
|
});
|
|
151
665
|
}
|
|
152
|
-
}
|
|
153
|
-
belongsTo = (agentId) => this.#agentId === agentId
|
|
666
|
+
};
|
|
667
|
+
belongsTo = (agentId, cwd, additionalDirectories) => this.#agentId === agentId &&
|
|
668
|
+
this.#cwd === cwd &&
|
|
669
|
+
arraysEqual(this.#additionalDirectories, additionalDirectories);
|
|
670
|
+
canRebind = () => !this.#prompted && !this.#turn.running;
|
|
671
|
+
prepared = () => new AcpPreparedSession(this.#agentName, this.#agentVersion, this.#capabilities, this.#commands, this.#configOptions, this.#restored);
|
|
672
|
+
initialize = async (signal) => {
|
|
673
|
+
const initialized = await this.race(this.#connection.agent.request(methods.agent.initialize, {
|
|
674
|
+
clientCapabilities: {
|
|
675
|
+
elicitation: { form: {}, url: {} },
|
|
676
|
+
plan: {},
|
|
677
|
+
session: { configOptions: { boolean: {} } },
|
|
678
|
+
},
|
|
679
|
+
clientInfo: { name: "mobius", version: "0.0.2" },
|
|
680
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
681
|
+
}, { cancellationSignal: signal }), "ACP agent exited during initialization", signal);
|
|
682
|
+
if (initialized.protocolVersion !== PROTOCOL_VERSION) {
|
|
683
|
+
throw new AcpExecutionError("ACP agent selected an unsupported protocol version", {}, "unsupported_acp_protocol");
|
|
684
|
+
}
|
|
685
|
+
const capabilities = initialized.agentCapabilities ?? {};
|
|
686
|
+
this.#capabilities = normalizeCapabilities(capabilities);
|
|
687
|
+
this.#agentName = truncate(initialized.agentInfo?.title ??
|
|
688
|
+
initialized.agentInfo?.name ??
|
|
689
|
+
this.#agentId, 255);
|
|
690
|
+
this.#agentVersion = truncate(initialized.agentInfo?.version ?? "", 128);
|
|
691
|
+
const stored = this.#store.read(this.#mobiusSessionId);
|
|
692
|
+
let lifecycleResponse;
|
|
693
|
+
if (stored instanceof FoundAcpStoredSession &&
|
|
694
|
+
stored.session.agentId === this.#agentId &&
|
|
695
|
+
stored.session.cwd === this.#cwd &&
|
|
696
|
+
arraysEqual(stored.session.additionalDirectories, this.#additionalDirectories)) {
|
|
697
|
+
this.#nativeSessionId = stored.session.nativeSessionId;
|
|
698
|
+
this.#prompted = stored.session.prompted;
|
|
699
|
+
this.#restored = true;
|
|
700
|
+
if (this.#capabilities.resumeSession) {
|
|
701
|
+
lifecycleResponse = await this.race(this.#connection.agent.request(methods.agent.session.resume, {
|
|
702
|
+
additionalDirectories: [...this.#additionalDirectories],
|
|
703
|
+
cwd: this.#cwd,
|
|
704
|
+
mcpServers: [],
|
|
705
|
+
sessionId: this.#nativeSessionId,
|
|
706
|
+
}, { cancellationSignal: signal }), "ACP agent exited while resuming a session", signal);
|
|
707
|
+
}
|
|
708
|
+
else if (this.#capabilities.loadSession) {
|
|
709
|
+
this.#replaying = true;
|
|
710
|
+
try {
|
|
711
|
+
lifecycleResponse = await this.race(this.#connection.agent.request(methods.agent.session.load, {
|
|
712
|
+
additionalDirectories: [...this.#additionalDirectories],
|
|
713
|
+
cwd: this.#cwd,
|
|
714
|
+
mcpServers: [],
|
|
715
|
+
sessionId: this.#nativeSessionId,
|
|
716
|
+
}, { cancellationSignal: signal }), "ACP agent exited while loading a session", signal);
|
|
717
|
+
}
|
|
718
|
+
finally {
|
|
719
|
+
this.#replaying = false;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
throw new AcpExecutionError("The ACP agent cannot restore its persisted session", {}, "session_restore_unsupported");
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
if (stored instanceof FoundAcpStoredSession) {
|
|
728
|
+
if (stored.session.prompted) {
|
|
729
|
+
throw new AcpExecutionError("The agent or workspace cannot change after a conversation has started", {}, "session_identity_mismatch");
|
|
730
|
+
}
|
|
731
|
+
this.#store.remove(this.#mobiusSessionId);
|
|
732
|
+
}
|
|
733
|
+
const created = await this.race(this.#connection.agent.request(methods.agent.session.new, {
|
|
734
|
+
additionalDirectories: [...this.#additionalDirectories],
|
|
735
|
+
cwd: this.#cwd,
|
|
736
|
+
mcpServers: [],
|
|
737
|
+
}, { cancellationSignal: signal }), "ACP agent exited while starting a session", signal);
|
|
738
|
+
this.#nativeSessionId = created.sessionId;
|
|
739
|
+
lifecycleResponse = created;
|
|
740
|
+
this.#store.write(new AcpStoredSession(this.#mobiusSessionId, this.#agentId, this.#nativeSessionId, this.#cwd, this.#additionalDirectories, false));
|
|
741
|
+
}
|
|
742
|
+
const lifecycleRecord = isRecord(lifecycleResponse)
|
|
743
|
+
? lifecycleResponse
|
|
744
|
+
: {};
|
|
745
|
+
this.#configOptions = normalizeConfigOptions(lifecycleRecord["configOptions"], lifecycleRecord["modes"]);
|
|
746
|
+
await this.#updateTail;
|
|
747
|
+
signal.throwIfAborted();
|
|
748
|
+
};
|
|
749
|
+
configure = async (options) => {
|
|
750
|
+
const existing = this.#configOptions.find((option) => option.id === options.configId);
|
|
751
|
+
if (existing === undefined || existing.type !== options.valueType) {
|
|
752
|
+
throw new AcpExecutionError("The requested ACP configuration option is unavailable", {}, "invalid_session_configuration");
|
|
753
|
+
}
|
|
754
|
+
if (existing.type === "select") {
|
|
755
|
+
if (!existing.values.some((entry) => entry.value === options.value)) {
|
|
756
|
+
throw new AcpExecutionError("The requested ACP configuration value is unavailable", {}, "invalid_session_configuration");
|
|
757
|
+
}
|
|
758
|
+
if (existing.category === "mode") {
|
|
759
|
+
await this.race(this.#connection.agent.request(methods.agent.session.setMode, { modeId: options.value, sessionId: this.#nativeSessionId }, { cancellationSignal: options.signal }), "ACP agent exited while setting the session mode", options.signal);
|
|
760
|
+
this.#configOptions = this.#configOptions.map((option) => option.id === existing.id
|
|
761
|
+
? { ...option, currentValue: options.value }
|
|
762
|
+
: option);
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
const response = await this.race(this.#connection.agent.request(methods.agent.session.setConfigOption, {
|
|
766
|
+
configId: existing.id,
|
|
767
|
+
sessionId: this.#nativeSessionId,
|
|
768
|
+
value: options.value,
|
|
769
|
+
}, { cancellationSignal: options.signal }), "ACP agent exited while changing session configuration", options.signal);
|
|
770
|
+
this.#configOptions = replaceConfigOptionsKeepingMode(this.#configOptions, response.configOptions);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
const response = await this.race(this.#connection.agent.request(methods.agent.session.setConfigOption, {
|
|
775
|
+
configId: existing.id,
|
|
776
|
+
sessionId: this.#nativeSessionId,
|
|
777
|
+
type: "boolean",
|
|
778
|
+
value: options.booleanValue,
|
|
779
|
+
}, { cancellationSignal: options.signal }), "ACP agent exited while changing session configuration", options.signal);
|
|
780
|
+
this.#configOptions = replaceConfigOptionsKeepingMode(this.#configOptions, response.configOptions);
|
|
781
|
+
}
|
|
782
|
+
return this.#configOptions;
|
|
783
|
+
};
|
|
784
|
+
prompt = async (options) => {
|
|
785
|
+
if (this.#turn.running) {
|
|
786
|
+
throw new AcpExecutionError("Another prompt is already running in this conversation", {}, "session_busy");
|
|
787
|
+
}
|
|
788
|
+
if (options.images.length > 0 && !this.#capabilities.imagePrompt) {
|
|
789
|
+
throw new AcpExecutionError("The selected agent does not accept image prompts", {}, "unsupported_prompt_content");
|
|
790
|
+
}
|
|
791
|
+
if (options.resources.some((resource) => resource.type === "embedded") &&
|
|
792
|
+
!this.#capabilities.embeddedContextPrompt) {
|
|
793
|
+
throw new AcpExecutionError("The selected agent does not accept embedded context", {}, "unsupported_prompt_content");
|
|
794
|
+
}
|
|
795
|
+
const prompt = [];
|
|
796
|
+
if (options.prompt.length > 0) {
|
|
797
|
+
prompt.push({ text: options.prompt, type: "text" });
|
|
798
|
+
}
|
|
799
|
+
for (const image of options.images) {
|
|
800
|
+
prompt.push({
|
|
801
|
+
_meta: { mobius: { name: image.name } },
|
|
802
|
+
data: image.data,
|
|
803
|
+
mimeType: image.mimeType,
|
|
804
|
+
type: "image",
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
for (const resource of options.resources) {
|
|
808
|
+
if (resource.type === "embedded") {
|
|
809
|
+
prompt.push({
|
|
810
|
+
resource: {
|
|
811
|
+
mimeType: resource.mimeType,
|
|
812
|
+
text: resource.text,
|
|
813
|
+
uri: resource.uri,
|
|
814
|
+
},
|
|
815
|
+
type: "resource",
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
prompt.push({
|
|
820
|
+
mimeType: resource.mimeType,
|
|
821
|
+
name: resource.name,
|
|
822
|
+
type: "resource_link",
|
|
823
|
+
uri: resource.uri,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (prompt.length === 0) {
|
|
828
|
+
throw new AcpExecutionError("A prompt must contain text or an attachment", {}, "empty_prompt");
|
|
829
|
+
}
|
|
830
|
+
this.#store.write(new AcpStoredSession(this.#mobiusSessionId, this.#agentId, this.#nativeSessionId, this.#cwd, this.#additionalDirectories, true));
|
|
831
|
+
this.#prompted = true;
|
|
832
|
+
const running = new RunningAcpTurn(options.commandId, options.onActivity, options.onText);
|
|
833
|
+
this.#turn = running;
|
|
834
|
+
try {
|
|
835
|
+
const response = await this.race(this.#connection.agent.request(methods.agent.session.prompt, { prompt, sessionId: this.#nativeSessionId }, { cancellationSignal: options.signal }), "ACP agent exited during a prompt", options.signal);
|
|
836
|
+
await this.#updateTail;
|
|
837
|
+
await running.activities.finish();
|
|
838
|
+
return { stopReason: response.stopReason };
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
await running.activities.finish();
|
|
842
|
+
if (error instanceof AcpExecutionError) {
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
throw new AcpExecutionError("ACP agent execution failed", {
|
|
846
|
+
cause: error,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
finally {
|
|
850
|
+
this.#turn = new IdleAcpTurn();
|
|
851
|
+
}
|
|
852
|
+
};
|
|
154
853
|
cancel = async () => {
|
|
155
|
-
|
|
854
|
+
await this.cancelPendingInteractions();
|
|
855
|
+
if (!this.#turn.running) {
|
|
156
856
|
return;
|
|
157
857
|
}
|
|
158
858
|
await this.#connection.agent.notify(methods.agent.session.cancel, {
|
|
159
|
-
sessionId: this.#
|
|
859
|
+
sessionId: this.#nativeSessionId,
|
|
160
860
|
});
|
|
161
861
|
};
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
862
|
+
respondPermission = async (options) => {
|
|
863
|
+
const pending = this.#pendingPermissions.get(options.interactionId);
|
|
864
|
+
if (pending === undefined) {
|
|
865
|
+
throw new AcpExecutionError("The permission request is no longer active", {}, "permission_request_not_found");
|
|
165
866
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
if (update.sessionUpdate === "tool_call") {
|
|
199
|
-
await activities.recordToolCall(update);
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
if (update.sessionUpdate === "tool_call_update") {
|
|
203
|
-
await activities.recordToolCallUpdate(update);
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
if (update.sessionUpdate === "plan") {
|
|
207
|
-
await activities.recordPlan(update);
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
if (update.sessionUpdate === "plan_update") {
|
|
211
|
-
await activities.recordPlanUpdate(update);
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
if (update.sessionUpdate === "plan_removed") {
|
|
215
|
-
await activities.removePlan(update);
|
|
867
|
+
if (options.outcome === "selected" &&
|
|
868
|
+
!pending.optionIds.has(options.optionId)) {
|
|
869
|
+
throw new AcpExecutionError("The selected permission option is unavailable", {}, "invalid_permission_response");
|
|
870
|
+
}
|
|
871
|
+
const resolved = options.outcome === "selected"
|
|
872
|
+
? pending.select(options.optionId)
|
|
873
|
+
: pending.cancel();
|
|
874
|
+
if (!resolved) {
|
|
875
|
+
throw new AcpExecutionError("The permission request was already resolved", {}, "permission_request_not_found");
|
|
876
|
+
}
|
|
877
|
+
this.#pendingPermissions.delete(options.interactionId);
|
|
878
|
+
await this.#getObserver().onPermissionResolved({
|
|
879
|
+
interactionId: options.interactionId,
|
|
880
|
+
kind: SESSION_PERMISSION_RESOLVED_KIND,
|
|
881
|
+
optionId: options.outcome === "selected" ? options.optionId : "",
|
|
882
|
+
outcome: options.outcome,
|
|
883
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
884
|
+
sessionId: this.#mobiusSessionId,
|
|
885
|
+
});
|
|
886
|
+
};
|
|
887
|
+
respondElicitation = async (options) => {
|
|
888
|
+
const pending = this.#pendingElicitations.get(options.interactionId);
|
|
889
|
+
if (pending === undefined) {
|
|
890
|
+
throw new AcpExecutionError("The elicitation request is no longer active", {}, "elicitation_request_not_found");
|
|
891
|
+
}
|
|
892
|
+
let resolved = false;
|
|
893
|
+
if (options.action === "accept") {
|
|
894
|
+
if (pending.elicitationId.length > 0) {
|
|
895
|
+
resolved = pending.accept({});
|
|
896
|
+
if (resolved) {
|
|
897
|
+
this.#acceptedUrlElicitations.set(pending.elicitationId, pending.interactionId);
|
|
216
898
|
}
|
|
217
899
|
}
|
|
900
|
+
else {
|
|
901
|
+
resolved = pending.accept(validateElicitationContent(options.content, pending.requestedSchema));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
else if (options.action === "decline") {
|
|
905
|
+
resolved = pending.decline();
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
resolved = pending.cancel();
|
|
909
|
+
}
|
|
910
|
+
if (!resolved) {
|
|
911
|
+
throw new AcpExecutionError("The elicitation request was already resolved", {}, "elicitation_request_not_found");
|
|
912
|
+
}
|
|
913
|
+
this.#pendingElicitations.delete(options.interactionId);
|
|
914
|
+
await this.#getObserver().onElicitationResolved({
|
|
915
|
+
action: options.action,
|
|
916
|
+
interactionId: options.interactionId,
|
|
917
|
+
kind: SESSION_ELICITATION_RESOLVED_KIND,
|
|
918
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
919
|
+
sessionId: this.#mobiusSessionId,
|
|
920
|
+
});
|
|
921
|
+
};
|
|
922
|
+
onPermission = async (request, signal) => {
|
|
923
|
+
this.assertNativeSession(request.sessionId);
|
|
924
|
+
const interactionId = `permission-${crypto.randomUUID()}`;
|
|
925
|
+
const options = request.options
|
|
926
|
+
.slice(0, 20)
|
|
927
|
+
.map((option) => ({
|
|
928
|
+
kind: option.kind,
|
|
929
|
+
name: truncate(option.name, 255),
|
|
930
|
+
optionId: truncate(option.optionId, 255),
|
|
931
|
+
}));
|
|
932
|
+
if (options.length === 0) {
|
|
933
|
+
throw new AcpExecutionError("ACP permission request did not provide any choices", {}, "invalid_agent_permission_request");
|
|
934
|
+
}
|
|
935
|
+
const pending = new PendingPermission(interactionId, new Set(options.map((option) => option.optionId)));
|
|
936
|
+
this.#pendingPermissions.set(interactionId, pending);
|
|
937
|
+
const abort = () => {
|
|
938
|
+
void this.cancelPermission(interactionId);
|
|
218
939
|
};
|
|
940
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
219
941
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
942
|
+
await this.#getObserver().onPermission({
|
|
943
|
+
commandId: this.#turn.commandId,
|
|
944
|
+
interactionId,
|
|
945
|
+
kind: SESSION_PERMISSION_REQUESTED_KIND,
|
|
946
|
+
options,
|
|
947
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
948
|
+
sessionId: this.#mobiusSessionId,
|
|
949
|
+
title: truncate(request.toolCall.title ?? "Permission required", 500),
|
|
950
|
+
toolCallId: truncate(request.toolCall.toolCallId, 255),
|
|
951
|
+
toolKind: truncate(request.toolCall.kind ?? "other", 64),
|
|
952
|
+
});
|
|
953
|
+
return await pending.completion.promise;
|
|
227
954
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
955
|
+
finally {
|
|
956
|
+
signal.removeEventListener("abort", abort);
|
|
957
|
+
this.#pendingPermissions.delete(interactionId);
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
onElicitation = async (request, signal) => {
|
|
961
|
+
const sessionId = Reflect.get(request, "sessionId");
|
|
962
|
+
if (typeof sessionId !== "string") {
|
|
963
|
+
throw new AcpExecutionError("Mobius only accepts session-scoped ACP elicitations", {}, "unsupported_elicitation_scope");
|
|
964
|
+
}
|
|
965
|
+
this.assertNativeSession(sessionId);
|
|
966
|
+
if (request.mode !== "form" && request.mode !== "url") {
|
|
967
|
+
throw new AcpExecutionError("The ACP agent requested an unsupported elicitation mode", {}, "unsupported_elicitation_mode");
|
|
968
|
+
}
|
|
969
|
+
const interactionId = `elicitation-${crypto.randomUUID()}`;
|
|
970
|
+
const schemaValue = Reflect.get(request, "requestedSchema");
|
|
971
|
+
const requestedSchema = request.mode === "form" ? schemaValue : {};
|
|
972
|
+
const elicitationIdValue = Reflect.get(request, "elicitationId");
|
|
973
|
+
const elicitationId = request.mode === "url" && typeof elicitationIdValue === "string"
|
|
974
|
+
? truncate(elicitationIdValue, 255)
|
|
975
|
+
: "";
|
|
976
|
+
const pending = new PendingElicitation(interactionId, elicitationId, requestedSchema);
|
|
977
|
+
this.#pendingElicitations.set(interactionId, pending);
|
|
978
|
+
const abort = () => {
|
|
979
|
+
void this.cancelElicitation(interactionId);
|
|
980
|
+
};
|
|
981
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
982
|
+
try {
|
|
983
|
+
await this.#getObserver().onElicitation({
|
|
984
|
+
accepted: false,
|
|
985
|
+
commandId: this.#turn.commandId,
|
|
986
|
+
elicitationId,
|
|
987
|
+
fields: normalizeElicitationFields(requestedSchema),
|
|
988
|
+
interactionId,
|
|
989
|
+
kind: SESSION_ELICITATION_REQUESTED_KIND,
|
|
990
|
+
message: truncate(request.message, 2_000),
|
|
991
|
+
mode: request.mode,
|
|
992
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
993
|
+
sessionId: this.#mobiusSessionId,
|
|
994
|
+
url: request.mode === "url" &&
|
|
995
|
+
typeof Reflect.get(request, "url") === "string"
|
|
996
|
+
? truncate(readString(Reflect.get(request, "url")), 4_096)
|
|
997
|
+
: "",
|
|
232
998
|
});
|
|
999
|
+
return await pending.completion.promise;
|
|
233
1000
|
}
|
|
234
1001
|
finally {
|
|
235
|
-
|
|
1002
|
+
signal.removeEventListener("abort", abort);
|
|
1003
|
+
this.#pendingElicitations.delete(interactionId);
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
onElicitationComplete = async (notification) => {
|
|
1007
|
+
const interactionId = this.#acceptedUrlElicitations.get(notification.elicitationId);
|
|
1008
|
+
if (interactionId === undefined) {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
this.#acceptedUrlElicitations.delete(notification.elicitationId);
|
|
1012
|
+
await this.#getObserver().onElicitationResolved({
|
|
1013
|
+
action: "complete",
|
|
1014
|
+
interactionId,
|
|
1015
|
+
kind: SESSION_ELICITATION_RESOLVED_KIND,
|
|
1016
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
1017
|
+
sessionId: this.#mobiusSessionId,
|
|
1018
|
+
});
|
|
1019
|
+
};
|
|
1020
|
+
onUpdate = (notification) => {
|
|
1021
|
+
this.#updateTail = this.#updateTail.then(async () => await this.processUpdate(notification));
|
|
1022
|
+
return this.#updateTail;
|
|
1023
|
+
};
|
|
1024
|
+
processUpdate = async (notification) => {
|
|
1025
|
+
this.assertNativeSession(notification.sessionId);
|
|
1026
|
+
const update = notification.update;
|
|
1027
|
+
if (this.#replaying &&
|
|
1028
|
+
(update.sessionUpdate === "user_message_chunk" ||
|
|
1029
|
+
update.sessionUpdate === "agent_message_chunk" ||
|
|
1030
|
+
update.sessionUpdate === "agent_thought_chunk" ||
|
|
1031
|
+
update.sessionUpdate === "tool_call" ||
|
|
1032
|
+
update.sessionUpdate === "tool_call_update" ||
|
|
1033
|
+
update.sessionUpdate === "plan" ||
|
|
1034
|
+
update.sessionUpdate === "plan_update" ||
|
|
1035
|
+
update.sessionUpdate === "plan_removed")) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (update.sessionUpdate === "agent_message_chunk") {
|
|
1039
|
+
if (update.content.type === "text") {
|
|
1040
|
+
if (isProgressMessage(update)) {
|
|
1041
|
+
await this.#turn.activities.recordNarrative("progress", update.messageId ?? "current", update.content.text);
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
await this.#turn.activities.finishNarrative();
|
|
1045
|
+
await this.#turn.onText(update.content.text);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
await this.#turn.activities.finishNarrative();
|
|
1050
|
+
await this.#getObserver().onContent({
|
|
1051
|
+
commandId: this.#turn.commandId,
|
|
1052
|
+
content: normalizeContentBlock(update.content),
|
|
1053
|
+
contentId: `content-${crypto.randomUUID()}`,
|
|
1054
|
+
kind: AGENT_CONTENT_KIND,
|
|
1055
|
+
messageId: truncate(update.messageId ?? "", 255),
|
|
1056
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
1057
|
+
sessionId: this.#mobiusSessionId,
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (update.sessionUpdate === "agent_thought_chunk") {
|
|
1063
|
+
if (update.content.type === "text") {
|
|
1064
|
+
await this.#turn.activities.recordNarrative("reasoning", update.messageId ?? "current", update.content.text);
|
|
1065
|
+
}
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (update.sessionUpdate === "tool_call") {
|
|
1069
|
+
await this.#turn.activities.recordToolCall(update);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (update.sessionUpdate === "tool_call_update") {
|
|
1073
|
+
await this.#turn.activities.recordToolCallUpdate(update);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
if (update.sessionUpdate === "plan") {
|
|
1077
|
+
await this.#turn.activities.recordPlan(update);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (update.sessionUpdate === "plan_update") {
|
|
1081
|
+
await this.#turn.activities.recordPlanUpdate(update);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (update.sessionUpdate === "plan_removed") {
|
|
1085
|
+
await this.#turn.activities.removePlan(update);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (update.sessionUpdate === "available_commands_update") {
|
|
1089
|
+
const commands = update.availableCommands
|
|
1090
|
+
.slice(0, 500)
|
|
1091
|
+
.map((command) => ({
|
|
1092
|
+
description: truncate(command.description, 500),
|
|
1093
|
+
inputHint: truncate(command.input?.hint ?? "", 255),
|
|
1094
|
+
name: truncate(command.name, 128),
|
|
1095
|
+
}))
|
|
1096
|
+
.filter((command) => command.name.length > 0);
|
|
1097
|
+
this.#commands = commands;
|
|
1098
|
+
await this.#getObserver().onCommands({
|
|
1099
|
+
commands,
|
|
1100
|
+
kind: SESSION_COMMANDS_UPDATED_KIND,
|
|
1101
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
1102
|
+
sessionId: this.#mobiusSessionId,
|
|
1103
|
+
});
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (update.sessionUpdate === "config_option_update") {
|
|
1107
|
+
this.#configOptions = replaceConfigOptionsKeepingMode(this.#configOptions, update.configOptions);
|
|
1108
|
+
await this.emitConfigUpdate();
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (update.sessionUpdate === "current_mode_update") {
|
|
1112
|
+
this.#configOptions = this.#configOptions.map((option) => option.category === "mode"
|
|
1113
|
+
? { ...option, currentValue: update.currentModeId }
|
|
1114
|
+
: option);
|
|
1115
|
+
await this.emitConfigUpdate();
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (update.sessionUpdate === "session_info_update") {
|
|
1119
|
+
if (Object.hasOwn(update, "title")) {
|
|
1120
|
+
this.#sessionTitle = truncate(update.title ?? "", 500);
|
|
1121
|
+
}
|
|
1122
|
+
if (Object.hasOwn(update, "updatedAt")) {
|
|
1123
|
+
this.#sessionUpdatedAt = truncate(update.updatedAt ?? "", 128);
|
|
1124
|
+
}
|
|
1125
|
+
await this.#getObserver().onInfo({
|
|
1126
|
+
kind: SESSION_INFO_UPDATED_KIND,
|
|
1127
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
1128
|
+
sessionId: this.#mobiusSessionId,
|
|
1129
|
+
title: this.#sessionTitle,
|
|
1130
|
+
updatedAt: this.#sessionUpdatedAt,
|
|
1131
|
+
});
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (update.sessionUpdate === "usage_update") {
|
|
1135
|
+
await this.#getObserver().onUsage({
|
|
1136
|
+
costAmount: update.cost?.amount ?? 0,
|
|
1137
|
+
costCurrency: truncate(update.cost?.currency ?? "", 16),
|
|
1138
|
+
hasCost: update.cost !== undefined && update.cost !== null,
|
|
1139
|
+
kind: SESSION_USAGE_UPDATED_KIND,
|
|
1140
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
1141
|
+
sessionId: this.#mobiusSessionId,
|
|
1142
|
+
size: Math.max(0, Math.trunc(update.size)),
|
|
1143
|
+
used: Math.max(0, Math.trunc(update.used)),
|
|
1144
|
+
});
|
|
236
1145
|
}
|
|
237
1146
|
};
|
|
238
1147
|
close = async () => {
|
|
239
|
-
this
|
|
1148
|
+
await this.cancelPendingInteractions();
|
|
1149
|
+
if (this.#capabilities.closeSession && this.#nativeSessionId.length > 0) {
|
|
1150
|
+
try {
|
|
1151
|
+
await this.#connection.agent.request(methods.agent.session.close, { sessionId: this.#nativeSessionId }, {
|
|
1152
|
+
cancellationSignal: AbortSignal.timeout(PROCESS_STOP_TIMEOUT_MILLISECONDS),
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
catch {
|
|
1156
|
+
// Process teardown below remains authoritative.
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
await this.closeProcess();
|
|
1160
|
+
};
|
|
1161
|
+
closeProcess = async () => {
|
|
240
1162
|
this.#connection.close();
|
|
241
1163
|
await stopAgentProcess(this.#child);
|
|
242
1164
|
};
|
|
1165
|
+
race = async (operation, exitMessage, signal) => {
|
|
1166
|
+
signal.throwIfAborted();
|
|
1167
|
+
let removeAbortListener = () => { };
|
|
1168
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
1169
|
+
const rejectAbort = () => {
|
|
1170
|
+
reject(signal.reason ?? new Error("ACP request was cancelled"));
|
|
1171
|
+
};
|
|
1172
|
+
signal.addEventListener("abort", rejectAbort, { once: true });
|
|
1173
|
+
removeAbortListener = () => {
|
|
1174
|
+
signal.removeEventListener("abort", rejectAbort);
|
|
1175
|
+
};
|
|
1176
|
+
});
|
|
1177
|
+
try {
|
|
1178
|
+
return await Promise.race([
|
|
1179
|
+
operation,
|
|
1180
|
+
this.#processExit.then(() => {
|
|
1181
|
+
throw new AcpExecutionError(exitMessage);
|
|
1182
|
+
}),
|
|
1183
|
+
aborted,
|
|
1184
|
+
]);
|
|
1185
|
+
}
|
|
1186
|
+
finally {
|
|
1187
|
+
removeAbortListener();
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
assertNativeSession = (sessionId) => {
|
|
1191
|
+
if (sessionId !== this.#nativeSessionId) {
|
|
1192
|
+
throw new AcpExecutionError("ACP agent referenced an unknown session", {}, "invalid_agent_session");
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
emitConfigUpdate = async () => {
|
|
1196
|
+
await this.#getObserver().onConfig({
|
|
1197
|
+
configOptions: this.#configOptions,
|
|
1198
|
+
kind: SESSION_CONFIG_UPDATED_KIND,
|
|
1199
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
1200
|
+
requestId: `agent-update-${crypto.randomUUID()}`,
|
|
1201
|
+
sessionId: this.#mobiusSessionId,
|
|
1202
|
+
});
|
|
1203
|
+
};
|
|
1204
|
+
cancelPermission = async (interactionId) => {
|
|
1205
|
+
const pending = this.#pendingPermissions.get(interactionId);
|
|
1206
|
+
if (pending === undefined || !pending.cancel()) {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
this.#pendingPermissions.delete(interactionId);
|
|
1210
|
+
await this.#getObserver().onPermissionResolved({
|
|
1211
|
+
interactionId,
|
|
1212
|
+
kind: SESSION_PERMISSION_RESOLVED_KIND,
|
|
1213
|
+
optionId: "",
|
|
1214
|
+
outcome: "cancelled",
|
|
1215
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
1216
|
+
sessionId: this.#mobiusSessionId,
|
|
1217
|
+
});
|
|
1218
|
+
};
|
|
1219
|
+
cancelElicitation = async (interactionId) => {
|
|
1220
|
+
const pending = this.#pendingElicitations.get(interactionId);
|
|
1221
|
+
if (pending === undefined || !pending.cancel()) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
this.#pendingElicitations.delete(interactionId);
|
|
1225
|
+
await this.#getObserver().onElicitationResolved({
|
|
1226
|
+
action: "cancel",
|
|
1227
|
+
interactionId,
|
|
1228
|
+
kind: SESSION_ELICITATION_RESOLVED_KIND,
|
|
1229
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
1230
|
+
sessionId: this.#mobiusSessionId,
|
|
1231
|
+
});
|
|
1232
|
+
};
|
|
1233
|
+
cancelPendingInteractions = async () => {
|
|
1234
|
+
const permissionIds = [...this.#pendingPermissions.keys()];
|
|
1235
|
+
const elicitationIds = [...this.#pendingElicitations.keys()];
|
|
1236
|
+
const acceptedElicitationIds = [...this.#acceptedUrlElicitations.values()];
|
|
1237
|
+
this.#acceptedUrlElicitations.clear();
|
|
1238
|
+
for (const interactionId of permissionIds) {
|
|
1239
|
+
await this.cancelPermission(interactionId);
|
|
1240
|
+
}
|
|
1241
|
+
for (const interactionId of elicitationIds) {
|
|
1242
|
+
await this.cancelElicitation(interactionId);
|
|
1243
|
+
}
|
|
1244
|
+
for (const interactionId of acceptedElicitationIds) {
|
|
1245
|
+
await this.#getObserver().onElicitationResolved({
|
|
1246
|
+
action: "cancel",
|
|
1247
|
+
interactionId,
|
|
1248
|
+
kind: SESSION_ELICITATION_RESOLVED_KIND,
|
|
1249
|
+
protocolVersion: MOBIUS_PROTOCOL_VERSION,
|
|
1250
|
+
sessionId: this.#mobiusSessionId,
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
243
1254
|
}
|
|
244
1255
|
export class AcpSessionManager {
|
|
245
1256
|
#activeSessionIds = new Set();
|
|
246
1257
|
#cancellationRequests = new Set();
|
|
1258
|
+
#preparing = new Map();
|
|
247
1259
|
#sessions = new Map();
|
|
1260
|
+
#store;
|
|
1261
|
+
#observer;
|
|
1262
|
+
constructor(observer = new NullAcpSessionObserver(), store = new MemoryAcpSessionStore()) {
|
|
1263
|
+
this.#observer = observer;
|
|
1264
|
+
this.#store = store;
|
|
1265
|
+
}
|
|
1266
|
+
setObserver = (observer) => {
|
|
1267
|
+
this.#observer = observer;
|
|
1268
|
+
};
|
|
1269
|
+
async #startPreparation(options) {
|
|
1270
|
+
const operation = (async () => {
|
|
1271
|
+
try {
|
|
1272
|
+
const runtime = await AcpProcessSession.start(options, () => this.#observer, this.#store);
|
|
1273
|
+
this.#sessions.set(options.sessionId, runtime);
|
|
1274
|
+
return runtime;
|
|
1275
|
+
}
|
|
1276
|
+
finally {
|
|
1277
|
+
this.#preparing.delete(options.sessionId);
|
|
1278
|
+
}
|
|
1279
|
+
})();
|
|
1280
|
+
this.#preparing.set(options.sessionId, operation);
|
|
1281
|
+
return await operation;
|
|
1282
|
+
}
|
|
1283
|
+
async #replacePreparation(existing, options) {
|
|
1284
|
+
const operation = (async () => {
|
|
1285
|
+
try {
|
|
1286
|
+
this.#sessions.delete(options.sessionId);
|
|
1287
|
+
await existing.close();
|
|
1288
|
+
this.#store.remove(options.sessionId);
|
|
1289
|
+
const runtime = await AcpProcessSession.start(options, () => this.#observer, this.#store);
|
|
1290
|
+
this.#sessions.set(options.sessionId, runtime);
|
|
1291
|
+
return runtime;
|
|
1292
|
+
}
|
|
1293
|
+
finally {
|
|
1294
|
+
this.#preparing.delete(options.sessionId);
|
|
1295
|
+
}
|
|
1296
|
+
})();
|
|
1297
|
+
this.#preparing.set(options.sessionId, operation);
|
|
1298
|
+
return await operation;
|
|
1299
|
+
}
|
|
1300
|
+
prepareSession = async (options) => {
|
|
1301
|
+
const pending = this.#preparing.get(options.sessionId);
|
|
1302
|
+
if (pending !== undefined) {
|
|
1303
|
+
const runtime = await pending;
|
|
1304
|
+
if (runtime.belongsTo(options.agent.id, options.cwd, options.additionalDirectories)) {
|
|
1305
|
+
return runtime.prepared();
|
|
1306
|
+
}
|
|
1307
|
+
return await this.prepareSession(options);
|
|
1308
|
+
}
|
|
1309
|
+
const existing = this.#sessions.get(options.sessionId);
|
|
1310
|
+
if (existing !== undefined) {
|
|
1311
|
+
if (existing.belongsTo(options.agent.id, options.cwd, options.additionalDirectories)) {
|
|
1312
|
+
return existing.prepared();
|
|
1313
|
+
}
|
|
1314
|
+
if (!existing.canRebind()) {
|
|
1315
|
+
throw new AcpExecutionError("The agent or workspace cannot change after a conversation has started", {}, "session_identity_mismatch");
|
|
1316
|
+
}
|
|
1317
|
+
const runtime = await this.#replacePreparation(existing, options);
|
|
1318
|
+
return runtime.prepared();
|
|
1319
|
+
}
|
|
1320
|
+
const runtime = await this.#startPreparation(options);
|
|
1321
|
+
return runtime.prepared();
|
|
1322
|
+
};
|
|
1323
|
+
configureSession = async (options) => {
|
|
1324
|
+
const runtime = await this.ensureSession(options);
|
|
1325
|
+
return await runtime.configure(options);
|
|
1326
|
+
};
|
|
248
1327
|
cancelPrompt = async (sessionId) => {
|
|
249
1328
|
this.#cancellationRequests.add(sessionId);
|
|
250
1329
|
const runtime = this.#sessions.get(sessionId);
|
|
@@ -254,31 +1333,24 @@ export class AcpSessionManager {
|
|
|
254
1333
|
};
|
|
255
1334
|
runPrompt = async (options) => {
|
|
256
1335
|
if (this.#activeSessionIds.has(options.sessionId)) {
|
|
257
|
-
throw new AcpExecutionError("Another prompt is already running in this conversation");
|
|
1336
|
+
throw new AcpExecutionError("Another prompt is already running in this conversation", {}, "session_busy");
|
|
258
1337
|
}
|
|
259
1338
|
this.#activeSessionIds.add(options.sessionId);
|
|
260
|
-
let runtime = this.#sessions.get(options.sessionId);
|
|
261
|
-
if (runtime !== undefined && !runtime.belongsTo(options.agent.id)) {
|
|
262
|
-
this.#activeSessionIds.delete(options.sessionId);
|
|
263
|
-
throw new AcpExecutionError("The agent cannot change after a conversation has started");
|
|
264
|
-
}
|
|
265
1339
|
try {
|
|
266
1340
|
if (this.#cancellationRequests.has(options.sessionId)) {
|
|
267
1341
|
return { stopReason: "cancelled" };
|
|
268
1342
|
}
|
|
269
|
-
|
|
270
|
-
runtime = await AcpProcessSession.start(options.agent, options.cwd, options.signal);
|
|
271
|
-
this.#sessions.set(options.sessionId, runtime);
|
|
272
|
-
}
|
|
1343
|
+
const runtime = await this.ensureSession(options);
|
|
273
1344
|
if (this.#cancellationRequests.has(options.sessionId)) {
|
|
274
1345
|
return { stopReason: "cancelled" };
|
|
275
1346
|
}
|
|
276
|
-
return await runtime.prompt(options
|
|
1347
|
+
return await runtime.prompt(options);
|
|
277
1348
|
}
|
|
278
1349
|
catch (error) {
|
|
279
|
-
|
|
1350
|
+
const failedRuntime = this.#sessions.get(options.sessionId);
|
|
1351
|
+
if (failedRuntime !== undefined) {
|
|
280
1352
|
this.#sessions.delete(options.sessionId);
|
|
281
|
-
await
|
|
1353
|
+
await failedRuntime.close();
|
|
282
1354
|
}
|
|
283
1355
|
if (error instanceof AcpExecutionError) {
|
|
284
1356
|
throw error;
|
|
@@ -292,10 +1364,39 @@ export class AcpSessionManager {
|
|
|
292
1364
|
this.#cancellationRequests.delete(options.sessionId);
|
|
293
1365
|
}
|
|
294
1366
|
};
|
|
1367
|
+
respondPermission = async (options) => {
|
|
1368
|
+
const runtime = this.#sessions.get(options.sessionId);
|
|
1369
|
+
if (runtime === undefined) {
|
|
1370
|
+
throw new AcpExecutionError("The permission request session is unavailable", {}, "session_not_found");
|
|
1371
|
+
}
|
|
1372
|
+
await runtime.respondPermission(options);
|
|
1373
|
+
};
|
|
1374
|
+
respondElicitation = async (options) => {
|
|
1375
|
+
const runtime = this.#sessions.get(options.sessionId);
|
|
1376
|
+
if (runtime === undefined) {
|
|
1377
|
+
throw new AcpExecutionError("The elicitation request session is unavailable", {}, "session_not_found");
|
|
1378
|
+
}
|
|
1379
|
+
await runtime.respondElicitation(options);
|
|
1380
|
+
};
|
|
295
1381
|
close = async () => {
|
|
296
1382
|
const sessions = [...this.#sessions.values()];
|
|
297
1383
|
this.#sessions.clear();
|
|
298
1384
|
await Promise.all(sessions.map(async (session) => await session.close()));
|
|
299
1385
|
};
|
|
1386
|
+
ensureSession = async (options) => {
|
|
1387
|
+
const existing = this.#sessions.get(options.sessionId);
|
|
1388
|
+
if (existing !== undefined) {
|
|
1389
|
+
if (!existing.belongsTo(options.agent.id, options.cwd, options.additionalDirectories)) {
|
|
1390
|
+
throw new AcpExecutionError("The agent or workspace cannot change after a conversation has started", {}, "session_identity_mismatch");
|
|
1391
|
+
}
|
|
1392
|
+
return existing;
|
|
1393
|
+
}
|
|
1394
|
+
await this.prepareSession(options);
|
|
1395
|
+
const started = this.#sessions.get(options.sessionId);
|
|
1396
|
+
if (started === undefined) {
|
|
1397
|
+
throw new AcpExecutionError("ACP session did not start");
|
|
1398
|
+
}
|
|
1399
|
+
return started;
|
|
1400
|
+
};
|
|
300
1401
|
}
|
|
301
1402
|
//# sourceMappingURL=session-manager.js.map
|