@wrongstack/acp 0.306.3 → 0.307.0
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/agent/protocol-handler.d.ts +3 -49
- package/dist/agent/protocol-session-management.d.ts +36 -0
- package/dist/agent/protocol-session-ops.d.ts +59 -0
- package/dist/agent.js +430 -517
- package/dist/client/acp-session-ops.d.ts +33 -0
- package/dist/client/acp-session.d.ts +1 -98
- package/dist/client.js +242 -300
- package/dist/index.js +677 -822
- package/dist/wrongstack-acp-agent.js +430 -517
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// src/agent/protocol-handler.ts
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import * as fsp from "node:fs/promises";
|
|
4
|
-
import * as path from "node:path";
|
|
5
3
|
|
|
6
4
|
// src/types/acp-v1.ts
|
|
7
5
|
var ACP_PROTOCOL_VERSION = 1;
|
|
@@ -32,7 +30,9 @@ function toWire(msg) {
|
|
|
32
30
|
}
|
|
33
31
|
var WRONGSTACK_VERSION = ACP_PACKAGE_VERSION;
|
|
34
32
|
|
|
35
|
-
// src/agent/protocol-
|
|
33
|
+
// src/agent/protocol-session-ops.ts
|
|
34
|
+
import * as fsp from "node:fs/promises";
|
|
35
|
+
import * as path from "node:path";
|
|
36
36
|
var WRONGSTACK_AUTH_METHODS = [
|
|
37
37
|
{
|
|
38
38
|
id: "wrongstack-auth",
|
|
@@ -51,6 +51,384 @@ var DEFAULT_MODES = [
|
|
|
51
51
|
description: "Default agent mode for code-generation tasks."
|
|
52
52
|
}
|
|
53
53
|
];
|
|
54
|
+
async function resolveSessionCwd(requested) {
|
|
55
|
+
if (!path.isAbsolute(requested)) return null;
|
|
56
|
+
const resolved = path.resolve(requested);
|
|
57
|
+
try {
|
|
58
|
+
const stat3 = await fsp.stat(resolved);
|
|
59
|
+
return stat3.isDirectory() ? resolved : null;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function errorToJsonRpc(err) {
|
|
65
|
+
if (err && typeof err === "object") {
|
|
66
|
+
const e = err;
|
|
67
|
+
if (typeof e.code === "number" && typeof e.message === "string") {
|
|
68
|
+
const result = {
|
|
69
|
+
code: e.code,
|
|
70
|
+
message: e.message
|
|
71
|
+
};
|
|
72
|
+
if (e.data !== void 0) result.data = e.data;
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
77
|
+
return { code: -32603, message };
|
|
78
|
+
}
|
|
79
|
+
function createRunTurnApi(sessionId, clientCapabilities, request) {
|
|
80
|
+
return {
|
|
81
|
+
clientCapabilities,
|
|
82
|
+
requestPermission: async (req) => {
|
|
83
|
+
const res = await request("session/request_permission", {
|
|
84
|
+
sessionId,
|
|
85
|
+
toolCall: req.toolCall,
|
|
86
|
+
options: req.options
|
|
87
|
+
});
|
|
88
|
+
const outcome = res?.outcome;
|
|
89
|
+
return outcome ?? { outcome: "cancelled" };
|
|
90
|
+
},
|
|
91
|
+
readTextFile: async (params) => {
|
|
92
|
+
const res = await request("fs/read_text_file", { sessionId, ...params });
|
|
93
|
+
return String(res?.content ?? "");
|
|
94
|
+
},
|
|
95
|
+
writeTextFile: async (params) => {
|
|
96
|
+
await request("fs/write_text_file", { sessionId, ...params });
|
|
97
|
+
},
|
|
98
|
+
runTerminal: async ({ command, args, cwd }) => {
|
|
99
|
+
const created = await request("terminal/create", {
|
|
100
|
+
sessionId,
|
|
101
|
+
command,
|
|
102
|
+
...args ? { args } : {},
|
|
103
|
+
...cwd ? { cwd } : {}
|
|
104
|
+
});
|
|
105
|
+
const terminalId = created?.terminalId;
|
|
106
|
+
if (!terminalId) return { output: "", exitCode: null };
|
|
107
|
+
try {
|
|
108
|
+
const exit = await request("terminal/wait_for_exit", {
|
|
109
|
+
sessionId,
|
|
110
|
+
terminalId
|
|
111
|
+
});
|
|
112
|
+
const out = await request("terminal/output", { sessionId, terminalId });
|
|
113
|
+
return {
|
|
114
|
+
output: String(out?.output ?? ""),
|
|
115
|
+
exitCode: typeof exit?.exitCode === "number" ? exit.exitCode : null
|
|
116
|
+
};
|
|
117
|
+
} finally {
|
|
118
|
+
try {
|
|
119
|
+
await request("terminal/release", { sessionId, terminalId });
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function buildInitializeResult(agentName, modes, configOptions) {
|
|
127
|
+
return {
|
|
128
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
129
|
+
agentCapabilities: {
|
|
130
|
+
loadSession: true,
|
|
131
|
+
promptCapabilities: {
|
|
132
|
+
image: true,
|
|
133
|
+
audio: false,
|
|
134
|
+
embeddedContext: true
|
|
135
|
+
},
|
|
136
|
+
mcpCapabilities: {
|
|
137
|
+
http: false,
|
|
138
|
+
sse: false
|
|
139
|
+
},
|
|
140
|
+
sessionCapabilities: {
|
|
141
|
+
close: {},
|
|
142
|
+
list: {},
|
|
143
|
+
delete: {},
|
|
144
|
+
resume: {},
|
|
145
|
+
fork: {}
|
|
146
|
+
},
|
|
147
|
+
auth: {
|
|
148
|
+
logout: {}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
agentInfo: {
|
|
152
|
+
name: agentName,
|
|
153
|
+
title: "WrongStack",
|
|
154
|
+
version: WRONGSTACK_VERSION
|
|
155
|
+
},
|
|
156
|
+
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
157
|
+
modes,
|
|
158
|
+
configOptions
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/agent/protocol-session-management.ts
|
|
163
|
+
async function handleSessionNewOp(ctx, id, params) {
|
|
164
|
+
if (ctx.sessions.size >= ctx.maxSessions) {
|
|
165
|
+
await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
const p = params ?? {};
|
|
169
|
+
let cwd = ctx.defaultCwd;
|
|
170
|
+
if (typeof p.cwd === "string") {
|
|
171
|
+
const resolved = await resolveSessionCwd(p.cwd);
|
|
172
|
+
if (resolved === null) {
|
|
173
|
+
await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
cwd = resolved;
|
|
177
|
+
}
|
|
178
|
+
const sessionId = `sess_${ctx.allocId()}`;
|
|
179
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
180
|
+
const state = {
|
|
181
|
+
id: sessionId,
|
|
182
|
+
cwd,
|
|
183
|
+
abort: new AbortController(),
|
|
184
|
+
modeId: DEFAULT_MODE_ID,
|
|
185
|
+
createdAt: now,
|
|
186
|
+
updatedAt: now
|
|
187
|
+
};
|
|
188
|
+
ctx.sessions.set(sessionId, state);
|
|
189
|
+
ctx.onSessionNew(state);
|
|
190
|
+
await ctx.persist(state);
|
|
191
|
+
await ctx.sendNotification({
|
|
192
|
+
sessionId,
|
|
193
|
+
update: {
|
|
194
|
+
sessionUpdate: "current_mode_update",
|
|
195
|
+
modeId: ctx.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
if (ctx.configOptions.length > 0) {
|
|
199
|
+
await ctx.sendNotification({
|
|
200
|
+
sessionId,
|
|
201
|
+
update: {
|
|
202
|
+
sessionUpdate: "config_option_update",
|
|
203
|
+
configOptions: [...ctx.configOptions]
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
await ctx.sendResult(id, {
|
|
208
|
+
sessionId,
|
|
209
|
+
modes: ctx.modes,
|
|
210
|
+
configOptions: ctx.configOptions
|
|
211
|
+
});
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
async function handleSessionLoadOp(ctx, id, params) {
|
|
215
|
+
const p = params ?? {};
|
|
216
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
217
|
+
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
218
|
+
const existing = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
219
|
+
if (!existing && sessionId && ctx.store) {
|
|
220
|
+
const persisted = await ctx.store.load(sessionId);
|
|
221
|
+
if (persisted) {
|
|
222
|
+
if (ctx.sessions.size >= ctx.maxSessions) {
|
|
223
|
+
await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
if (loadCwd !== void 0 && await resolveSessionCwd(loadCwd) === null) {
|
|
227
|
+
await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
const candidateCwd = persisted.cwd ?? loadCwd ?? ctx.defaultCwd;
|
|
231
|
+
const restoredCwd = await resolveSessionCwd(candidateCwd) ?? ctx.defaultCwd;
|
|
232
|
+
const restored = {
|
|
233
|
+
id: sessionId,
|
|
234
|
+
cwd: restoredCwd,
|
|
235
|
+
abort: new AbortController(),
|
|
236
|
+
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
237
|
+
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
238
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
239
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
240
|
+
};
|
|
241
|
+
ctx.sessions.set(sessionId, restored);
|
|
242
|
+
ctx.seedFor?.(sessionId, persisted.history ?? []);
|
|
243
|
+
for (const update of persisted.history ?? []) {
|
|
244
|
+
await ctx.sendNotification({ sessionId, update });
|
|
245
|
+
}
|
|
246
|
+
await ctx.sendNotification({
|
|
247
|
+
sessionId,
|
|
248
|
+
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
249
|
+
});
|
|
250
|
+
await ctx.sendResult(id, {
|
|
251
|
+
initialMode: { currentModeId: restored.modeId, availableModes: ctx.modes }
|
|
252
|
+
});
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (existing) {
|
|
257
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
258
|
+
const replay = ctx.replayFor?.(sessionId);
|
|
259
|
+
if (replay) {
|
|
260
|
+
for (const update of replay) {
|
|
261
|
+
await ctx.sendNotification({ sessionId, update });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
await ctx.sendNotification({
|
|
265
|
+
sessionId,
|
|
266
|
+
update: {
|
|
267
|
+
sessionUpdate: "session_info_update",
|
|
268
|
+
updatedAt: existing.updatedAt
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
await ctx.sendNotification({
|
|
272
|
+
sessionId,
|
|
273
|
+
update: {
|
|
274
|
+
sessionUpdate: "current_mode_update",
|
|
275
|
+
modeId: existing.modeId
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
await ctx.sendResult(id, {
|
|
279
|
+
initialMode: {
|
|
280
|
+
currentModeId: existing.modeId,
|
|
281
|
+
availableModes: ctx.modes
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
await ctx.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
async function handleSessionForkOp(ctx, id, params) {
|
|
290
|
+
const p = params ?? {};
|
|
291
|
+
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
292
|
+
const source = sourceId ? ctx.sessions.get(sourceId) : void 0;
|
|
293
|
+
if (!sourceId || !source) {
|
|
294
|
+
await ctx.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (ctx.sessions.size >= ctx.maxSessions) {
|
|
298
|
+
await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
let forkCwd = source.cwd;
|
|
302
|
+
if (typeof p.cwd === "string") {
|
|
303
|
+
const resolved = await resolveSessionCwd(p.cwd);
|
|
304
|
+
if (resolved === null) {
|
|
305
|
+
await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
forkCwd = resolved;
|
|
309
|
+
}
|
|
310
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
311
|
+
const sessionId = `sess_${ctx.allocId()}`;
|
|
312
|
+
const forked = {
|
|
313
|
+
id: sessionId,
|
|
314
|
+
cwd: forkCwd,
|
|
315
|
+
abort: new AbortController(),
|
|
316
|
+
modeId: source.modeId,
|
|
317
|
+
createdAt: now,
|
|
318
|
+
updatedAt: now,
|
|
319
|
+
...source.title !== void 0 ? { title: source.title } : {}
|
|
320
|
+
};
|
|
321
|
+
const history = (ctx.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
322
|
+
sessionUpdate: update.sessionUpdate,
|
|
323
|
+
content: structuredClone(update.content)
|
|
324
|
+
}));
|
|
325
|
+
ctx.sessions.set(sessionId, forked);
|
|
326
|
+
ctx.seedFor?.(sessionId, history);
|
|
327
|
+
ctx.onSessionNew(forked);
|
|
328
|
+
await ctx.persist(forked, history);
|
|
329
|
+
await ctx.sendNotification({
|
|
330
|
+
sessionId,
|
|
331
|
+
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
332
|
+
});
|
|
333
|
+
await ctx.sendResult(id, {
|
|
334
|
+
sessionId,
|
|
335
|
+
modes: ctx.modes,
|
|
336
|
+
configOptions: ctx.configOptions
|
|
337
|
+
});
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
async function handleSessionPromptOp(ctx, id, params) {
|
|
341
|
+
const p = params ?? {};
|
|
342
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
343
|
+
if (!sessionId || !ctx.sessions.has(sessionId)) {
|
|
344
|
+
await ctx.sendError(id, -32e3, "unknown or missing sessionId");
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
if (!Array.isArray(p.prompt)) {
|
|
348
|
+
await ctx.sendError(id, -32602, "prompt must be an array of content blocks");
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
const session = ctx.sessions.get(sessionId);
|
|
352
|
+
if (session.abort.signal.aborted) {
|
|
353
|
+
session.abort = new AbortController();
|
|
354
|
+
}
|
|
355
|
+
const turnSignal = new AbortController();
|
|
356
|
+
const onCancel = () => turnSignal.abort();
|
|
357
|
+
session.abort.signal.addEventListener("abort", onCancel, { once: true });
|
|
358
|
+
const api = createRunTurnApi(
|
|
359
|
+
sessionId,
|
|
360
|
+
ctx.clientCapabilities ?? {},
|
|
361
|
+
(method, req) => ctx.request(method, req)
|
|
362
|
+
);
|
|
363
|
+
let result;
|
|
364
|
+
const pendingNotifications = [];
|
|
365
|
+
const emit = (update) => {
|
|
366
|
+
const notifPromise = ctx.sendNotification({ sessionId, update });
|
|
367
|
+
pendingNotifications.push(notifPromise.catch(() => {
|
|
368
|
+
}));
|
|
369
|
+
};
|
|
370
|
+
try {
|
|
371
|
+
result = await ctx.runTurn(
|
|
372
|
+
{ sessionId, prompt: p.prompt, signal: turnSignal.signal },
|
|
373
|
+
emit,
|
|
374
|
+
api
|
|
375
|
+
);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
session.abort.signal.removeEventListener("abort", onCancel);
|
|
378
|
+
const { code, message, data } = errorToJsonRpc(err);
|
|
379
|
+
await ctx.sendError(id, code, message, data);
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
await Promise.all(pendingNotifications);
|
|
383
|
+
session.abort.signal.removeEventListener("abort", onCancel);
|
|
384
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
385
|
+
await ctx.persist(session);
|
|
386
|
+
await ctx.sendResult(id, { stopReason: result.stopReason });
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
async function handleSetModeOp(ctx, id, params) {
|
|
390
|
+
const p = params ?? {};
|
|
391
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
392
|
+
const modeId = typeof p.modeId === "string" ? p.modeId : null;
|
|
393
|
+
const session = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
394
|
+
if (!session || !modeId || !ctx.modes.some((m) => m.id === modeId)) {
|
|
395
|
+
await ctx.sendError(id, -32602, "invalid sessionId or modeId");
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
session.modeId = modeId;
|
|
399
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
400
|
+
await ctx.sendNotification({
|
|
401
|
+
sessionId,
|
|
402
|
+
update: { sessionUpdate: "current_mode_update", modeId }
|
|
403
|
+
});
|
|
404
|
+
await ctx.sendResult(id, {});
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
async function handleSetConfigOptionOp(ctx, id, params) {
|
|
408
|
+
const p = params ?? {};
|
|
409
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
410
|
+
const optionId = typeof p.configId === "string" ? p.configId : null;
|
|
411
|
+
const value = typeof p.value === "string" ? p.value : null;
|
|
412
|
+
const session = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
413
|
+
const option = optionId ? ctx.configOptions.find((o) => o.id === optionId) : void 0;
|
|
414
|
+
if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
|
|
415
|
+
await ctx.sendError(id, -32602, "invalid sessionId, configId, or value");
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
option.currentValue = value;
|
|
419
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
420
|
+
await ctx.sendNotification({
|
|
421
|
+
sessionId,
|
|
422
|
+
update: {
|
|
423
|
+
sessionUpdate: "config_option_update",
|
|
424
|
+
configOptions: [...ctx.configOptions]
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
await ctx.sendResult(id, { configOptions: [...ctx.configOptions] });
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/agent/protocol-handler.ts
|
|
54
432
|
var ACPProtocolHandler = class {
|
|
55
433
|
transport;
|
|
56
434
|
defaultCwd;
|
|
@@ -159,6 +537,27 @@ var ACPProtocolHandler = class {
|
|
|
159
537
|
} catch {
|
|
160
538
|
}
|
|
161
539
|
}
|
|
540
|
+
sessionContext() {
|
|
541
|
+
return {
|
|
542
|
+
sessions: this.sessions,
|
|
543
|
+
maxSessions: this.maxSessions,
|
|
544
|
+
defaultCwd: this.defaultCwd,
|
|
545
|
+
modes: this.modes,
|
|
546
|
+
configOptions: this.configOptions,
|
|
547
|
+
store: this.store,
|
|
548
|
+
replayFor: this.replayFor,
|
|
549
|
+
seedFor: this.seedFor,
|
|
550
|
+
onSessionNew: this.onSessionNew,
|
|
551
|
+
allocId: () => this.allocId(),
|
|
552
|
+
persist: (state, history) => this.persist(state, history),
|
|
553
|
+
sendNotification: (params) => this.sendNotification(params),
|
|
554
|
+
sendError: (id, code, message, data) => this.sendError(id, code, message, data),
|
|
555
|
+
sendResult: (id, result) => this.sendResult(id, result),
|
|
556
|
+
request: (method, params, timeoutMs) => this.request(method, params, timeoutMs),
|
|
557
|
+
runTurn: this.runTurn,
|
|
558
|
+
clientCapabilities: this.clientCapabilities
|
|
559
|
+
};
|
|
560
|
+
}
|
|
162
561
|
// ────────────────────────────────────────────────────────────────────
|
|
163
562
|
// Requests
|
|
164
563
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -176,9 +575,9 @@ var ACPProtocolHandler = class {
|
|
|
176
575
|
case "logout":
|
|
177
576
|
return await this.handleLogout(id, params);
|
|
178
577
|
case "session/new":
|
|
179
|
-
return await this.
|
|
578
|
+
return await handleSessionNewOp(this.sessionContext(), id, params);
|
|
180
579
|
case "session/load":
|
|
181
|
-
return await this.
|
|
580
|
+
return await handleSessionLoadOp(this.sessionContext(), id, params);
|
|
182
581
|
case "session/resume":
|
|
183
582
|
return await this.handleSessionResume(id, params);
|
|
184
583
|
case "session/close":
|
|
@@ -186,15 +585,15 @@ var ACPProtocolHandler = class {
|
|
|
186
585
|
case "session/delete":
|
|
187
586
|
return await this.handleSessionDelete(id, params);
|
|
188
587
|
case "session/prompt":
|
|
189
|
-
return await this.
|
|
588
|
+
return await handleSessionPromptOp(this.sessionContext(), id, params);
|
|
190
589
|
case "session/set_mode":
|
|
191
|
-
return await this.
|
|
590
|
+
return await handleSetModeOp(this.sessionContext(), id, params);
|
|
192
591
|
case "session/set_config_option":
|
|
193
|
-
return await this.
|
|
592
|
+
return await handleSetConfigOptionOp(this.sessionContext(), id, params);
|
|
194
593
|
case "session/list":
|
|
195
594
|
return await this.handleSessionList(id);
|
|
196
595
|
case "session/fork":
|
|
197
|
-
return await this.
|
|
596
|
+
return await handleSessionForkOp(this.sessionContext(), id, params);
|
|
198
597
|
case "providers/list":
|
|
199
598
|
return await this.handleProvidersList(id, params);
|
|
200
599
|
case "providers/set":
|
|
@@ -219,232 +618,32 @@ var ACPProtocolHandler = class {
|
|
|
219
618
|
this.clientCapabilities = p.clientCapabilities;
|
|
220
619
|
}
|
|
221
620
|
this.initialized = true;
|
|
222
|
-
await this.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
id,
|
|
226
|
-
result: {
|
|
227
|
-
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
228
|
-
agentCapabilities: {
|
|
229
|
-
loadSession: true,
|
|
230
|
-
promptCapabilities: {
|
|
231
|
-
// We route ACP image blocks into the core agent's multimodal
|
|
232
|
-
// input (server-agent-turn.promptToAgentInput); whether the
|
|
233
|
-
// model can see them is the configured provider's concern.
|
|
234
|
-
image: true,
|
|
235
|
-
audio: false,
|
|
236
|
-
embeddedContext: true
|
|
237
|
-
},
|
|
238
|
-
mcpCapabilities: {
|
|
239
|
-
http: false,
|
|
240
|
-
sse: false
|
|
241
|
-
},
|
|
242
|
-
sessionCapabilities: {
|
|
243
|
-
close: {},
|
|
244
|
-
list: {},
|
|
245
|
-
delete: {},
|
|
246
|
-
resume: {},
|
|
247
|
-
fork: {}
|
|
248
|
-
},
|
|
249
|
-
auth: {
|
|
250
|
-
logout: {}
|
|
251
|
-
}
|
|
252
|
-
},
|
|
253
|
-
agentInfo: {
|
|
254
|
-
name: this.agentName,
|
|
255
|
-
title: "WrongStack",
|
|
256
|
-
version: WRONGSTACK_VERSION
|
|
257
|
-
},
|
|
258
|
-
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
259
|
-
modes: this.modes,
|
|
260
|
-
configOptions: this.configOptions
|
|
261
|
-
}
|
|
262
|
-
})
|
|
621
|
+
await this.sendResult(
|
|
622
|
+
id,
|
|
623
|
+
buildInitializeResult(this.agentName, this.modes, this.configOptions)
|
|
263
624
|
);
|
|
264
625
|
return false;
|
|
265
626
|
}
|
|
266
627
|
async handleAuthenticate(id, _params) {
|
|
267
|
-
await this.
|
|
268
|
-
toWire({
|
|
269
|
-
jsonrpc: "2.0",
|
|
270
|
-
id,
|
|
271
|
-
result: { outcome: "unauthenticated" }
|
|
272
|
-
})
|
|
273
|
-
);
|
|
628
|
+
await this.sendResult(id, { outcome: "unauthenticated" });
|
|
274
629
|
return false;
|
|
275
630
|
}
|
|
276
631
|
async handleLogout(id, _params) {
|
|
277
|
-
await this.
|
|
278
|
-
toWire({
|
|
279
|
-
jsonrpc: "2.0",
|
|
280
|
-
id,
|
|
281
|
-
result: {}
|
|
282
|
-
})
|
|
283
|
-
);
|
|
632
|
+
await this.sendResult(id, {});
|
|
284
633
|
return false;
|
|
285
634
|
}
|
|
286
|
-
async
|
|
287
|
-
if (this.sessions.size >= this.maxSessions) {
|
|
288
|
-
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
291
|
-
const p = params ?? {};
|
|
292
|
-
let cwd = this.defaultCwd;
|
|
293
|
-
if (typeof p.cwd === "string") {
|
|
294
|
-
const resolved = await this.resolveSessionCwd(p.cwd);
|
|
295
|
-
if (resolved === null) {
|
|
296
|
-
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
cwd = resolved;
|
|
300
|
-
}
|
|
301
|
-
const sessionId = `sess_${this.allocId()}`;
|
|
302
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
303
|
-
const state = {
|
|
304
|
-
id: sessionId,
|
|
305
|
-
cwd,
|
|
306
|
-
abort: new AbortController(),
|
|
307
|
-
modeId: DEFAULT_MODE_ID,
|
|
308
|
-
createdAt: now,
|
|
309
|
-
updatedAt: now
|
|
310
|
-
};
|
|
311
|
-
this.sessions.set(sessionId, state);
|
|
312
|
-
this.onSessionNew(state);
|
|
313
|
-
await this.persist(state);
|
|
314
|
-
await this.sendNotification({
|
|
315
|
-
sessionId,
|
|
316
|
-
update: {
|
|
317
|
-
sessionUpdate: "current_mode_update",
|
|
318
|
-
modeId: this.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
if (this.configOptions.length > 0) {
|
|
322
|
-
await this.sendNotification({
|
|
323
|
-
sessionId,
|
|
324
|
-
update: {
|
|
325
|
-
sessionUpdate: "config_option_update",
|
|
326
|
-
configOptions: [...this.configOptions]
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
await this.transport.send(
|
|
331
|
-
toWire({
|
|
332
|
-
jsonrpc: "2.0",
|
|
333
|
-
id,
|
|
334
|
-
result: {
|
|
335
|
-
sessionId,
|
|
336
|
-
modes: this.modes,
|
|
337
|
-
configOptions: this.configOptions
|
|
338
|
-
}
|
|
339
|
-
})
|
|
340
|
-
);
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
async handleSessionLoad(id, params) {
|
|
635
|
+
async handleSessionResume(id, params) {
|
|
344
636
|
const p = params ?? {};
|
|
345
637
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
346
|
-
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
347
638
|
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
348
|
-
if (!existing && sessionId && this.store) {
|
|
349
|
-
const persisted = await this.store.load(sessionId);
|
|
350
|
-
if (persisted) {
|
|
351
|
-
if (this.sessions.size >= this.maxSessions) {
|
|
352
|
-
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
355
|
-
if (loadCwd !== void 0 && await this.resolveSessionCwd(loadCwd) === null) {
|
|
356
|
-
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
const candidateCwd = persisted.cwd ?? loadCwd ?? this.defaultCwd;
|
|
360
|
-
const restoredCwd = await this.resolveSessionCwd(candidateCwd) ?? this.defaultCwd;
|
|
361
|
-
const restored = {
|
|
362
|
-
id: sessionId,
|
|
363
|
-
cwd: restoredCwd,
|
|
364
|
-
abort: new AbortController(),
|
|
365
|
-
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
366
|
-
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
367
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
368
|
-
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
369
|
-
};
|
|
370
|
-
this.sessions.set(sessionId, restored);
|
|
371
|
-
this.seedFor?.(sessionId, persisted.history ?? []);
|
|
372
|
-
for (const update of persisted.history ?? []) {
|
|
373
|
-
await this.sendNotification({ sessionId, update });
|
|
374
|
-
}
|
|
375
|
-
await this.sendNotification({
|
|
376
|
-
sessionId,
|
|
377
|
-
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
378
|
-
});
|
|
379
|
-
await this.transport.send(
|
|
380
|
-
toWire({
|
|
381
|
-
jsonrpc: "2.0",
|
|
382
|
-
id,
|
|
383
|
-
result: {
|
|
384
|
-
initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
|
|
385
|
-
}
|
|
386
|
-
})
|
|
387
|
-
);
|
|
388
|
-
return false;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
639
|
if (existing) {
|
|
392
640
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
await this.sendNotification({
|
|
400
|
-
sessionId,
|
|
401
|
-
update: {
|
|
402
|
-
sessionUpdate: "session_info_update",
|
|
403
|
-
updatedAt: existing.updatedAt
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
await this.sendNotification({
|
|
407
|
-
sessionId,
|
|
408
|
-
update: {
|
|
409
|
-
sessionUpdate: "current_mode_update",
|
|
410
|
-
modeId: existing.modeId
|
|
641
|
+
await this.sendResult(id, {
|
|
642
|
+
initialMode: {
|
|
643
|
+
currentModeId: existing.modeId,
|
|
644
|
+
availableModes: this.modes
|
|
411
645
|
}
|
|
412
646
|
});
|
|
413
|
-
await this.transport.send(
|
|
414
|
-
toWire({
|
|
415
|
-
jsonrpc: "2.0",
|
|
416
|
-
id,
|
|
417
|
-
result: {
|
|
418
|
-
initialMode: {
|
|
419
|
-
currentModeId: existing.modeId,
|
|
420
|
-
availableModes: this.modes
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
})
|
|
424
|
-
);
|
|
425
|
-
return false;
|
|
426
|
-
}
|
|
427
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
async handleSessionResume(id, params) {
|
|
431
|
-
const p = params ?? {};
|
|
432
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
433
|
-
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
434
|
-
if (existing) {
|
|
435
|
-
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
436
|
-
await this.transport.send(
|
|
437
|
-
toWire({
|
|
438
|
-
jsonrpc: "2.0",
|
|
439
|
-
id,
|
|
440
|
-
result: {
|
|
441
|
-
initialMode: {
|
|
442
|
-
currentModeId: existing.modeId,
|
|
443
|
-
availableModes: this.modes
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
})
|
|
447
|
-
);
|
|
448
647
|
return false;
|
|
449
648
|
}
|
|
450
649
|
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
@@ -461,13 +660,7 @@ var ACPProtocolHandler = class {
|
|
|
461
660
|
session.abort.abort();
|
|
462
661
|
this.sessions.delete(sessionId);
|
|
463
662
|
this.disposeSession(sessionId);
|
|
464
|
-
await this.
|
|
465
|
-
toWire({
|
|
466
|
-
jsonrpc: "2.0",
|
|
467
|
-
id,
|
|
468
|
-
result: {}
|
|
469
|
-
})
|
|
470
|
-
);
|
|
663
|
+
await this.sendResult(id, {});
|
|
471
664
|
return false;
|
|
472
665
|
}
|
|
473
666
|
async handleSessionDelete(id, params) {
|
|
@@ -478,252 +671,37 @@ var ACPProtocolHandler = class {
|
|
|
478
671
|
return false;
|
|
479
672
|
}
|
|
480
673
|
if (!this.sessions.has(sessionId)) {
|
|
481
|
-
await this.
|
|
482
|
-
toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } })
|
|
483
|
-
);
|
|
674
|
+
await this.sendResult(id, { configOptions: [...this.configOptions] });
|
|
484
675
|
return false;
|
|
485
676
|
}
|
|
486
677
|
const session = this.sessions.get(sessionId);
|
|
487
678
|
session.abort.abort();
|
|
488
679
|
this.sessions.delete(sessionId);
|
|
489
680
|
this.disposeSession(sessionId);
|
|
490
|
-
await this.
|
|
491
|
-
toWire({
|
|
492
|
-
jsonrpc: "2.0",
|
|
493
|
-
id,
|
|
494
|
-
result: {}
|
|
495
|
-
})
|
|
496
|
-
);
|
|
497
|
-
return false;
|
|
498
|
-
}
|
|
499
|
-
async handleSessionFork(id, params) {
|
|
500
|
-
const p = params ?? {};
|
|
501
|
-
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
502
|
-
const source = sourceId ? this.sessions.get(sourceId) : void 0;
|
|
503
|
-
if (!sourceId || !source) {
|
|
504
|
-
await this.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
505
|
-
return false;
|
|
506
|
-
}
|
|
507
|
-
if (this.sessions.size >= this.maxSessions) {
|
|
508
|
-
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
509
|
-
return false;
|
|
510
|
-
}
|
|
511
|
-
let forkCwd = source.cwd;
|
|
512
|
-
if (typeof p.cwd === "string") {
|
|
513
|
-
const resolved = await this.resolveSessionCwd(p.cwd);
|
|
514
|
-
if (resolved === null) {
|
|
515
|
-
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
516
|
-
return false;
|
|
517
|
-
}
|
|
518
|
-
forkCwd = resolved;
|
|
519
|
-
}
|
|
520
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
521
|
-
const sessionId = `sess_${this.allocId()}`;
|
|
522
|
-
const forked = {
|
|
523
|
-
id: sessionId,
|
|
524
|
-
cwd: forkCwd,
|
|
525
|
-
abort: new AbortController(),
|
|
526
|
-
modeId: source.modeId,
|
|
527
|
-
createdAt: now,
|
|
528
|
-
updatedAt: now,
|
|
529
|
-
...source.title !== void 0 ? { title: source.title } : {}
|
|
530
|
-
};
|
|
531
|
-
const history = (this.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
532
|
-
sessionUpdate: update.sessionUpdate,
|
|
533
|
-
content: structuredClone(update.content)
|
|
534
|
-
}));
|
|
535
|
-
this.sessions.set(sessionId, forked);
|
|
536
|
-
this.seedFor?.(sessionId, history);
|
|
537
|
-
this.onSessionNew(forked);
|
|
538
|
-
await this.persist(forked, history);
|
|
539
|
-
await this.sendNotification({
|
|
540
|
-
sessionId,
|
|
541
|
-
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
542
|
-
});
|
|
543
|
-
await this.transport.send(
|
|
544
|
-
toWire({
|
|
545
|
-
jsonrpc: "2.0",
|
|
546
|
-
id,
|
|
547
|
-
result: {
|
|
548
|
-
sessionId,
|
|
549
|
-
modes: this.modes,
|
|
550
|
-
configOptions: this.configOptions
|
|
551
|
-
}
|
|
552
|
-
})
|
|
553
|
-
);
|
|
681
|
+
await this.sendResult(id, {});
|
|
554
682
|
return false;
|
|
555
683
|
}
|
|
556
684
|
async handleProvidersList(id, _params) {
|
|
557
|
-
await this.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
result: {
|
|
562
|
-
providers: [],
|
|
563
|
-
currentProviderId: null
|
|
564
|
-
}
|
|
565
|
-
})
|
|
566
|
-
);
|
|
685
|
+
await this.sendResult(id, {
|
|
686
|
+
providers: [],
|
|
687
|
+
currentProviderId: null
|
|
688
|
+
});
|
|
567
689
|
return false;
|
|
568
690
|
}
|
|
569
691
|
async handleProvidersSet(id, _params) {
|
|
570
692
|
await this.sendError(
|
|
571
693
|
id,
|
|
572
|
-
-32e3,
|
|
573
|
-
"provider configuration not available through ACP; use wstack auth"
|
|
574
|
-
);
|
|
575
|
-
return false;
|
|
576
|
-
}
|
|
577
|
-
async handleProvidersDisable(id, _params) {
|
|
578
|
-
await this.transport.send(
|
|
579
|
-
toWire({
|
|
580
|
-
jsonrpc: "2.0",
|
|
581
|
-
id,
|
|
582
|
-
result: {}
|
|
583
|
-
})
|
|
584
|
-
);
|
|
585
|
-
return false;
|
|
586
|
-
}
|
|
587
|
-
async handleMcpMessage(id, _params) {
|
|
588
|
-
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
589
|
-
return false;
|
|
590
|
-
}
|
|
591
|
-
async handleSessionPrompt(id, params) {
|
|
592
|
-
const p = params ?? {};
|
|
593
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
594
|
-
if (!sessionId || !this.sessions.has(sessionId)) {
|
|
595
|
-
await this.sendError(id, -32e3, "unknown or missing sessionId");
|
|
596
|
-
return false;
|
|
597
|
-
}
|
|
598
|
-
if (!Array.isArray(p.prompt)) {
|
|
599
|
-
await this.sendError(id, -32602, "prompt must be an array of content blocks");
|
|
600
|
-
return false;
|
|
601
|
-
}
|
|
602
|
-
const session = this.sessions.get(sessionId);
|
|
603
|
-
if (session.abort.signal.aborted) {
|
|
604
|
-
session.abort = new AbortController();
|
|
605
|
-
}
|
|
606
|
-
const turnSignal = new AbortController();
|
|
607
|
-
const onCancel = () => turnSignal.abort();
|
|
608
|
-
session.abort.signal.addEventListener("abort", onCancel, { once: true });
|
|
609
|
-
const api = {
|
|
610
|
-
clientCapabilities: this.clientCapabilities,
|
|
611
|
-
requestPermission: async (req) => {
|
|
612
|
-
const res = await this.request("session/request_permission", {
|
|
613
|
-
sessionId,
|
|
614
|
-
toolCall: req.toolCall,
|
|
615
|
-
options: req.options
|
|
616
|
-
});
|
|
617
|
-
const outcome = res?.outcome;
|
|
618
|
-
return outcome ?? { outcome: "cancelled" };
|
|
619
|
-
},
|
|
620
|
-
readTextFile: async (params2) => {
|
|
621
|
-
const res = await this.request("fs/read_text_file", { sessionId, ...params2 });
|
|
622
|
-
return String(res?.content ?? "");
|
|
623
|
-
},
|
|
624
|
-
writeTextFile: async (params2) => {
|
|
625
|
-
await this.request("fs/write_text_file", { sessionId, ...params2 });
|
|
626
|
-
},
|
|
627
|
-
runTerminal: async ({ command, args, cwd }) => {
|
|
628
|
-
const created = await this.request("terminal/create", {
|
|
629
|
-
sessionId,
|
|
630
|
-
command,
|
|
631
|
-
...args ? { args } : {},
|
|
632
|
-
...cwd ? { cwd } : {}
|
|
633
|
-
});
|
|
634
|
-
const terminalId = created?.terminalId;
|
|
635
|
-
if (!terminalId) return { output: "", exitCode: null };
|
|
636
|
-
try {
|
|
637
|
-
const exit = await this.request("terminal/wait_for_exit", {
|
|
638
|
-
sessionId,
|
|
639
|
-
terminalId
|
|
640
|
-
});
|
|
641
|
-
const out = await this.request("terminal/output", { sessionId, terminalId });
|
|
642
|
-
return {
|
|
643
|
-
output: String(out?.output ?? ""),
|
|
644
|
-
exitCode: typeof exit?.exitCode === "number" ? exit.exitCode : null
|
|
645
|
-
};
|
|
646
|
-
} finally {
|
|
647
|
-
try {
|
|
648
|
-
await this.request("terminal/release", { sessionId, terminalId });
|
|
649
|
-
} catch {
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
};
|
|
654
|
-
let result;
|
|
655
|
-
const pendingNotifications = [];
|
|
656
|
-
const emit = (update) => {
|
|
657
|
-
const p2 = this.sendNotification({ sessionId, update });
|
|
658
|
-
pendingNotifications.push(p2.catch(() => {
|
|
659
|
-
}));
|
|
660
|
-
};
|
|
661
|
-
try {
|
|
662
|
-
result = await this.runTurn(
|
|
663
|
-
{ sessionId, prompt: p.prompt, signal: turnSignal.signal },
|
|
664
|
-
emit,
|
|
665
|
-
api
|
|
666
|
-
);
|
|
667
|
-
} catch (err) {
|
|
668
|
-
session.abort.signal.removeEventListener("abort", onCancel);
|
|
669
|
-
const { code, message, data } = errorToJsonRpc(err);
|
|
670
|
-
await this.sendError(id, code, message, data);
|
|
671
|
-
return false;
|
|
672
|
-
}
|
|
673
|
-
await Promise.all(pendingNotifications);
|
|
674
|
-
session.abort.signal.removeEventListener("abort", onCancel);
|
|
675
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
676
|
-
await this.persist(session);
|
|
677
|
-
await this.transport.send(
|
|
678
|
-
toWire({
|
|
679
|
-
jsonrpc: "2.0",
|
|
680
|
-
id,
|
|
681
|
-
result: { stopReason: result.stopReason }
|
|
682
|
-
})
|
|
694
|
+
-32e3,
|
|
695
|
+
"provider configuration not available through ACP; use wstack auth"
|
|
683
696
|
);
|
|
684
697
|
return false;
|
|
685
698
|
}
|
|
686
|
-
async
|
|
687
|
-
|
|
688
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
689
|
-
const modeId = typeof p.modeId === "string" ? p.modeId : null;
|
|
690
|
-
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
691
|
-
if (!session || !modeId || !this.modes.some((m) => m.id === modeId)) {
|
|
692
|
-
await this.sendError(id, -32602, "invalid sessionId or modeId");
|
|
693
|
-
return false;
|
|
694
|
-
}
|
|
695
|
-
session.modeId = modeId;
|
|
696
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
697
|
-
await this.sendNotification({
|
|
698
|
-
sessionId,
|
|
699
|
-
update: { sessionUpdate: "current_mode_update", modeId }
|
|
700
|
-
});
|
|
701
|
-
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: {} }));
|
|
699
|
+
async handleProvidersDisable(id, _params) {
|
|
700
|
+
await this.sendResult(id, {});
|
|
702
701
|
return false;
|
|
703
702
|
}
|
|
704
|
-
async
|
|
705
|
-
|
|
706
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
707
|
-
const optionId = typeof p.configId === "string" ? p.configId : null;
|
|
708
|
-
const value = typeof p.value === "string" ? p.value : null;
|
|
709
|
-
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
710
|
-
const option = optionId ? this.configOptions.find((o) => o.id === optionId) : void 0;
|
|
711
|
-
if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
|
|
712
|
-
await this.sendError(id, -32602, "invalid sessionId, configId, or value");
|
|
713
|
-
return false;
|
|
714
|
-
}
|
|
715
|
-
option.currentValue = value;
|
|
716
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
717
|
-
await this.sendNotification({
|
|
718
|
-
sessionId,
|
|
719
|
-
update: {
|
|
720
|
-
sessionUpdate: "config_option_update",
|
|
721
|
-
configOptions: [...this.configOptions]
|
|
722
|
-
}
|
|
723
|
-
});
|
|
724
|
-
await this.transport.send(
|
|
725
|
-
toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } })
|
|
726
|
-
);
|
|
703
|
+
async handleMcpMessage(id, _params) {
|
|
704
|
+
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
727
705
|
return false;
|
|
728
706
|
}
|
|
729
707
|
async handleSessionList(id) {
|
|
@@ -736,13 +714,7 @@ var ACPProtocolHandler = class {
|
|
|
736
714
|
if (s.title !== void 0) out.title = s.title;
|
|
737
715
|
return out;
|
|
738
716
|
});
|
|
739
|
-
await this.
|
|
740
|
-
toWire({
|
|
741
|
-
jsonrpc: "2.0",
|
|
742
|
-
id,
|
|
743
|
-
result: { sessions }
|
|
744
|
-
})
|
|
745
|
-
);
|
|
717
|
+
await this.sendResult(id, { sessions });
|
|
746
718
|
return false;
|
|
747
719
|
}
|
|
748
720
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -775,7 +747,9 @@ var ACPProtocolHandler = class {
|
|
|
775
747
|
async sendNotification(params) {
|
|
776
748
|
await this.transport.send(toWire({ jsonrpc: "2.0", method: "session/update", params }));
|
|
777
749
|
}
|
|
778
|
-
|
|
750
|
+
async sendResult(id, result) {
|
|
751
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result }));
|
|
752
|
+
}
|
|
779
753
|
async persist(state, history = void 0) {
|
|
780
754
|
if (!this.store) return;
|
|
781
755
|
try {
|
|
@@ -788,71 +762,10 @@ var ACPProtocolHandler = class {
|
|
|
788
762
|
if (data !== void 0) error.data = data;
|
|
789
763
|
await this.transport.send(toWire({ jsonrpc: "2.0", id, error }));
|
|
790
764
|
}
|
|
791
|
-
/**
|
|
792
|
-
* Allocate a session id (WS-015).
|
|
793
|
-
*
|
|
794
|
-
* This was `this.nextId++`, so ids were `sess_1`, `sess_2`, … — and the
|
|
795
|
-
* handler has no per-connection ownership: any caller that names a session
|
|
796
|
-
* id can `session/load`, `session/prompt`, `session/cancel` or
|
|
797
|
-
* `session/delete` it. Over stdio that is academic (one client per process),
|
|
798
|
-
* but the agent also serves over HTTP, where a guessable id is the whole
|
|
799
|
-
* authorization story for any local process or page that reaches the port.
|
|
800
|
-
*
|
|
801
|
-
* Random ids do not create ownership — they remove the trivial enumeration
|
|
802
|
-
* that made its absence exploitable. Real per-connection ownership is the
|
|
803
|
-
* larger fix and is noted in the WS-015 test file.
|
|
804
|
-
*
|
|
805
|
-
* The counter is retained: it keeps ids ordered for debugging and guarantees
|
|
806
|
-
* uniqueness within a process even in the (impossible) event of a UUID
|
|
807
|
-
* collision. The random half is what makes the id unguessable.
|
|
808
|
-
*/
|
|
809
|
-
/**
|
|
810
|
-
* Resolve a client-supplied `cwd` for a session, or `null` when it is not
|
|
811
|
-
* usable (WS-015).
|
|
812
|
-
*
|
|
813
|
-
* `session/new`, `session/load` and `session/fork` all took `params.cwd`
|
|
814
|
-
* with a single `typeof === 'string'` check and nothing else. That value is
|
|
815
|
-
* the working directory the agent then reads, writes and executes in.
|
|
816
|
-
*
|
|
817
|
-
* SCOPE, deliberately stated: this does NOT confine the session to a root.
|
|
818
|
-
* In ACP the client IS the editor and legitimately names its own workspace —
|
|
819
|
-
* Zed and JetBrains pass the project root — so a fixed boundary here would
|
|
820
|
-
* break the integration this package exists for. What it enforces is that
|
|
821
|
-
* the directory is absolute and actually exists as a directory: a relative
|
|
822
|
-
* or missing `cwd` is a bug or an attack under either reading, and silently
|
|
823
|
-
* running the agent somewhere other than where the client asked is worse
|
|
824
|
-
* than refusing. Confinement, if wanted, belongs in an operator-set option
|
|
825
|
-
* on top of this, not in place of it.
|
|
826
|
-
*/
|
|
827
|
-
async resolveSessionCwd(requested) {
|
|
828
|
-
if (!path.isAbsolute(requested)) return null;
|
|
829
|
-
const resolved = path.resolve(requested);
|
|
830
|
-
try {
|
|
831
|
-
const stat3 = await fsp.stat(resolved);
|
|
832
|
-
return stat3.isDirectory() ? resolved : null;
|
|
833
|
-
} catch {
|
|
834
|
-
return null;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
765
|
allocId() {
|
|
838
766
|
return `${this.nextId++}_${randomUUID().replaceAll("-", "")}`;
|
|
839
767
|
}
|
|
840
768
|
};
|
|
841
|
-
function errorToJsonRpc(err) {
|
|
842
|
-
if (err && typeof err === "object") {
|
|
843
|
-
const e = err;
|
|
844
|
-
if (typeof e.code === "number" && typeof e.message === "string") {
|
|
845
|
-
const result = {
|
|
846
|
-
code: e.code,
|
|
847
|
-
message: e.message
|
|
848
|
-
};
|
|
849
|
-
if (e.data !== void 0) result.data = e.data;
|
|
850
|
-
return result;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
854
|
-
return { code: -32603, message };
|
|
855
|
-
}
|
|
856
769
|
|
|
857
770
|
// src/agent/stdio-transport.ts
|
|
858
771
|
import { expectDefined, writeErr } from "@wrongstack/core/utils";
|
|
@@ -2757,6 +2670,220 @@ function isBestEffortAckMethod(method) {
|
|
|
2757
2670
|
return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
|
|
2758
2671
|
}
|
|
2759
2672
|
|
|
2673
|
+
// src/client/acp-session-ops.ts
|
|
2674
|
+
function filterMcpServers(agentCapabilities, servers) {
|
|
2675
|
+
if (!servers || servers.length === 0) return [];
|
|
2676
|
+
const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
|
|
2677
|
+
return servers.filter((s) => {
|
|
2678
|
+
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
2679
|
+
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
2680
|
+
return true;
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
|
|
2684
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2685
|
+
if (!ctx.agentCapabilities.loadSession) {
|
|
2686
|
+
throw new ACPSessionError(
|
|
2687
|
+
"unsupported_capability",
|
|
2688
|
+
"agent does not support session/load (loadSession capability not advertised)"
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
if (ctx.sessionId) {
|
|
2692
|
+
await ctx.closeSession();
|
|
2693
|
+
}
|
|
2694
|
+
ctx.resetScratch();
|
|
2695
|
+
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
2696
|
+
const id = ctx.allocId();
|
|
2697
|
+
const result = await ctx.sendRequest(id, "session/load", {
|
|
2698
|
+
sessionId,
|
|
2699
|
+
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
2700
|
+
mcpServers: servers
|
|
2701
|
+
});
|
|
2702
|
+
if (isJsonRpcError(result)) {
|
|
2703
|
+
throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
|
|
2704
|
+
}
|
|
2705
|
+
ctx.setSessionId(sessionId);
|
|
2706
|
+
}
|
|
2707
|
+
async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
|
|
2708
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2709
|
+
if (!ctx.agentCapabilities.sessionCapabilities?.resume) {
|
|
2710
|
+
throw new ACPSessionError(
|
|
2711
|
+
"unsupported_capability",
|
|
2712
|
+
"agent does not support session/resume (sessionCapabilities.resume not advertised)"
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2715
|
+
if (ctx.sessionId) {
|
|
2716
|
+
await ctx.closeSession();
|
|
2717
|
+
}
|
|
2718
|
+
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
2719
|
+
const id = ctx.allocId();
|
|
2720
|
+
const result = await ctx.sendRequest(id, "session/resume", {
|
|
2721
|
+
sessionId,
|
|
2722
|
+
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
2723
|
+
mcpServers: servers
|
|
2724
|
+
});
|
|
2725
|
+
if (isJsonRpcError(result)) {
|
|
2726
|
+
throw new ACPSessionError(
|
|
2727
|
+
"prompt_failed",
|
|
2728
|
+
`session/resume failed: ${result.message}`,
|
|
2729
|
+
result
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
ctx.setSessionId(sessionId);
|
|
2733
|
+
}
|
|
2734
|
+
async function executeListSessions(ctx, cursor, cwd) {
|
|
2735
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2736
|
+
if (!ctx.agentCapabilities.sessionCapabilities?.list) {
|
|
2737
|
+
throw new ACPSessionError(
|
|
2738
|
+
"unsupported_capability",
|
|
2739
|
+
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
2740
|
+
);
|
|
2741
|
+
}
|
|
2742
|
+
const id = ctx.allocId();
|
|
2743
|
+
const params = {};
|
|
2744
|
+
if (cursor !== void 0) params.cursor = cursor;
|
|
2745
|
+
if (cwd !== void 0) params.cwd = cwd;
|
|
2746
|
+
const result = await ctx.sendRequest(id, "session/list", params);
|
|
2747
|
+
if (isJsonRpcError(result)) {
|
|
2748
|
+
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
2749
|
+
}
|
|
2750
|
+
const r = result;
|
|
2751
|
+
return {
|
|
2752
|
+
sessions: r.sessions ?? [],
|
|
2753
|
+
nextCursor: r.nextCursor
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
async function executeDeleteSession(ctx, sessionId) {
|
|
2757
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2758
|
+
if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
|
|
2759
|
+
throw new ACPSessionError(
|
|
2760
|
+
"unsupported_capability",
|
|
2761
|
+
"agent does not support session/delete (sessionCapabilities.delete not advertised)"
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
const id = ctx.allocId();
|
|
2765
|
+
const result = await ctx.sendRequest(id, "session/delete", { sessionId });
|
|
2766
|
+
if (isJsonRpcError(result)) {
|
|
2767
|
+
throw new ACPSessionError(
|
|
2768
|
+
"prompt_failed",
|
|
2769
|
+
`session/delete failed: ${result.message}`,
|
|
2770
|
+
result
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
if (ctx.sessionId === sessionId) {
|
|
2774
|
+
ctx.setSessionId(null);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
async function executeForkSession(ctx, sourceSessionId, cwd, mcpServers) {
|
|
2778
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2779
|
+
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
2780
|
+
const id = ctx.allocId();
|
|
2781
|
+
const result = await ctx.sendRequest(id, "session/fork", {
|
|
2782
|
+
sessionId: sourceSessionId,
|
|
2783
|
+
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
2784
|
+
...servers.length > 0 ? { mcpServers: servers } : {}
|
|
2785
|
+
});
|
|
2786
|
+
if (isJsonRpcError(result)) {
|
|
2787
|
+
throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
|
|
2788
|
+
}
|
|
2789
|
+
const newId = result.sessionId;
|
|
2790
|
+
if (typeof newId !== "string" || !newId) {
|
|
2791
|
+
throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
|
|
2792
|
+
}
|
|
2793
|
+
return newId;
|
|
2794
|
+
}
|
|
2795
|
+
async function executeSetMode(ctx, sessionId, modeId) {
|
|
2796
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2797
|
+
const id = ctx.allocId();
|
|
2798
|
+
const result = await ctx.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
2799
|
+
if (isJsonRpcError(result)) {
|
|
2800
|
+
throw new ACPSessionError(
|
|
2801
|
+
"prompt_failed",
|
|
2802
|
+
`session/set_mode failed: ${result.message}`,
|
|
2803
|
+
result
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
async function executeSetConfigOption(ctx, sessionId, configId, value) {
|
|
2808
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2809
|
+
const id = ctx.allocId();
|
|
2810
|
+
const result = await ctx.sendRequest(id, "session/set_config_option", {
|
|
2811
|
+
sessionId,
|
|
2812
|
+
configId,
|
|
2813
|
+
value
|
|
2814
|
+
});
|
|
2815
|
+
if (isJsonRpcError(result)) {
|
|
2816
|
+
throw new ACPSessionError(
|
|
2817
|
+
"prompt_failed",
|
|
2818
|
+
`session/set_config_option failed: ${result.message}`,
|
|
2819
|
+
result
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
async function executeListProviders(ctx) {
|
|
2824
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2825
|
+
const id = ctx.allocId();
|
|
2826
|
+
const result = await ctx.sendRequest(id, "providers/list", {});
|
|
2827
|
+
if (isJsonRpcError(result)) {
|
|
2828
|
+
throw new ACPSessionError(
|
|
2829
|
+
"prompt_failed",
|
|
2830
|
+
`providers/list failed: ${result.message}`,
|
|
2831
|
+
result
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
const r = result;
|
|
2835
|
+
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
2836
|
+
}
|
|
2837
|
+
async function executeSetProvider(ctx, providerId, config) {
|
|
2838
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2839
|
+
const id = ctx.allocId();
|
|
2840
|
+
const result = await ctx.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
|
|
2841
|
+
if (isJsonRpcError(result)) {
|
|
2842
|
+
throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
async function executeDisableProvider(ctx) {
|
|
2846
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2847
|
+
const id = ctx.allocId();
|
|
2848
|
+
const result = await ctx.sendRequest(id, "providers/disable", {});
|
|
2849
|
+
if (isJsonRpcError(result)) {
|
|
2850
|
+
throw new ACPSessionError(
|
|
2851
|
+
"prompt_failed",
|
|
2852
|
+
`providers/disable failed: ${result.message}`,
|
|
2853
|
+
result
|
|
2854
|
+
);
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
async function executeMcpMessage(ctx, connectionId, message) {
|
|
2858
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2859
|
+
const id = ctx.allocId();
|
|
2860
|
+
const result = await ctx.sendRequest(id, "mcp/message", { connectionId, message });
|
|
2861
|
+
if (isJsonRpcError(result)) {
|
|
2862
|
+
throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
|
|
2863
|
+
}
|
|
2864
|
+
return result;
|
|
2865
|
+
}
|
|
2866
|
+
async function executeCreateSession(ctx) {
|
|
2867
|
+
const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
|
|
2868
|
+
const id = ctx.allocId();
|
|
2869
|
+
const result = await ctx.sendRequest(id, "session/new", {
|
|
2870
|
+
cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
2871
|
+
mcpServers: servers
|
|
2872
|
+
});
|
|
2873
|
+
if (isJsonRpcError(result)) {
|
|
2874
|
+
throw new ACPSessionError(
|
|
2875
|
+
"session_create_failed",
|
|
2876
|
+
`session/new failed: ${result.message}`,
|
|
2877
|
+
result
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
const sessionId = result.sessionId;
|
|
2881
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
2882
|
+
throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
|
|
2883
|
+
}
|
|
2884
|
+
return sessionId;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2760
2887
|
// src/client/acp-session.ts
|
|
2761
2888
|
var ACPSession = class _ACPSession {
|
|
2762
2889
|
transport;
|
|
@@ -2945,9 +3072,6 @@ var ACPSession = class _ACPSession {
|
|
|
2945
3072
|
/**
|
|
2946
3073
|
* Authenticate with the agent using one of the advertised auth methods.
|
|
2947
3074
|
* Call this AFTER start() and BEFORE any session/new call.
|
|
2948
|
-
*
|
|
2949
|
-
* Throws ACPSessionError('auth_failed') if the agent rejects the
|
|
2950
|
-
* authentication or if the methodId is not in the advertised list.
|
|
2951
3075
|
*/
|
|
2952
3076
|
async authenticate(methodId) {
|
|
2953
3077
|
if (this.state === "closed") {
|
|
@@ -2994,268 +3118,59 @@ var ACPSession = class _ACPSession {
|
|
|
2994
3118
|
this.state = "ready";
|
|
2995
3119
|
}
|
|
2996
3120
|
// ──────────────────────────────────────────────────────────────────────
|
|
2997
|
-
// Session management
|
|
3121
|
+
// Session management delegation
|
|
2998
3122
|
// ──────────────────────────────────────────────────────────────────────
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3123
|
+
opContext() {
|
|
3124
|
+
return {
|
|
3125
|
+
closed: this.closed,
|
|
3126
|
+
sessionId: this.sessionId,
|
|
3127
|
+
agentCapabilities: this.agentCapabilities,
|
|
3128
|
+
opts: this.opts,
|
|
3129
|
+
allocId: () => this.allocId(),
|
|
3130
|
+
sendRequest: (id, method, params, timeoutMs) => this.sendRequest(id, method, params, timeoutMs),
|
|
3131
|
+
setSessionId: (id) => {
|
|
3132
|
+
this.sessionId = id;
|
|
3133
|
+
},
|
|
3134
|
+
resetScratch: () => this.resetScratch(),
|
|
3135
|
+
closeSession: () => this.closeSession()
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3009
3138
|
async loadSession(sessionId, mcpServers, cwd) {
|
|
3010
|
-
|
|
3011
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
3012
|
-
}
|
|
3013
|
-
if (!this.agentCapabilities.loadSession) {
|
|
3014
|
-
throw new ACPSessionError(
|
|
3015
|
-
"unsupported_capability",
|
|
3016
|
-
"agent does not support session/load (loadSession capability not advertised)"
|
|
3017
|
-
);
|
|
3018
|
-
}
|
|
3019
|
-
if (this.sessionId) {
|
|
3020
|
-
await this.closeSession();
|
|
3021
|
-
}
|
|
3022
|
-
this.resetScratch();
|
|
3023
|
-
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
3024
|
-
const id = this.allocId();
|
|
3025
|
-
const result = await this.sendRequest(id, "session/load", {
|
|
3026
|
-
sessionId,
|
|
3027
|
-
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
3028
|
-
mcpServers: servers
|
|
3029
|
-
});
|
|
3030
|
-
if (isJsonRpcError(result)) {
|
|
3031
|
-
throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
|
|
3032
|
-
}
|
|
3033
|
-
this.sessionId = sessionId;
|
|
3139
|
+
return executeLoadSession(this.opContext(), sessionId, mcpServers, cwd);
|
|
3034
3140
|
}
|
|
3035
|
-
/**
|
|
3036
|
-
* Resume an existing session without replaying history.
|
|
3037
|
-
*
|
|
3038
|
-
* Only works if the agent advertises `sessionCapabilities.resume`.
|
|
3039
|
-
*
|
|
3040
|
-
* @param sessionId - The session to resume
|
|
3041
|
-
* @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
|
|
3042
|
-
* @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
|
|
3043
|
-
*/
|
|
3044
3141
|
async resumeSession(sessionId, mcpServers, cwd) {
|
|
3045
|
-
|
|
3046
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
3047
|
-
}
|
|
3048
|
-
if (!this.agentCapabilities.sessionCapabilities?.resume) {
|
|
3049
|
-
throw new ACPSessionError(
|
|
3050
|
-
"unsupported_capability",
|
|
3051
|
-
"agent does not support session/resume (sessionCapabilities.resume not advertised)"
|
|
3052
|
-
);
|
|
3053
|
-
}
|
|
3054
|
-
if (this.sessionId) {
|
|
3055
|
-
await this.closeSession();
|
|
3056
|
-
}
|
|
3057
|
-
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
3058
|
-
const id = this.allocId();
|
|
3059
|
-
const result = await this.sendRequest(id, "session/resume", {
|
|
3060
|
-
sessionId,
|
|
3061
|
-
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
3062
|
-
mcpServers: servers
|
|
3063
|
-
});
|
|
3064
|
-
if (isJsonRpcError(result)) {
|
|
3065
|
-
throw new ACPSessionError(
|
|
3066
|
-
"prompt_failed",
|
|
3067
|
-
`session/resume failed: ${result.message}`,
|
|
3068
|
-
result
|
|
3069
|
-
);
|
|
3070
|
-
}
|
|
3071
|
-
this.sessionId = sessionId;
|
|
3142
|
+
return executeResumeSession(this.opContext(), sessionId, mcpServers, cwd);
|
|
3072
3143
|
}
|
|
3073
|
-
/**
|
|
3074
|
-
* List existing sessions known to the agent.
|
|
3075
|
-
*
|
|
3076
|
-
* Only works if the agent advertises `sessionCapabilities.list`.
|
|
3077
|
-
*/
|
|
3078
3144
|
async listSessions(cursor, cwd) {
|
|
3079
|
-
|
|
3080
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
3081
|
-
}
|
|
3082
|
-
if (!this.agentCapabilities.sessionCapabilities?.list) {
|
|
3083
|
-
throw new ACPSessionError(
|
|
3084
|
-
"unsupported_capability",
|
|
3085
|
-
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
3086
|
-
);
|
|
3087
|
-
}
|
|
3088
|
-
const id = this.allocId();
|
|
3089
|
-
const params = {};
|
|
3090
|
-
if (cursor !== void 0) params.cursor = cursor;
|
|
3091
|
-
if (cwd !== void 0) params.cwd = cwd;
|
|
3092
|
-
const result = await this.sendRequest(id, "session/list", params);
|
|
3093
|
-
if (isJsonRpcError(result)) {
|
|
3094
|
-
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
3095
|
-
}
|
|
3096
|
-
const r = result;
|
|
3097
|
-
return {
|
|
3098
|
-
sessions: r.sessions ?? [],
|
|
3099
|
-
nextCursor: r.nextCursor
|
|
3100
|
-
};
|
|
3145
|
+
return executeListSessions(this.opContext(), cursor, cwd);
|
|
3101
3146
|
}
|
|
3102
|
-
/**
|
|
3103
|
-
* Delete a session from the agent's session list.
|
|
3104
|
-
*
|
|
3105
|
-
* Only works if the agent advertises `sessionCapabilities.delete`.
|
|
3106
|
-
*/
|
|
3107
3147
|
async deleteSession(sessionId) {
|
|
3108
|
-
|
|
3109
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
3110
|
-
}
|
|
3111
|
-
if (!this.agentCapabilities.sessionCapabilities?.delete) {
|
|
3112
|
-
throw new ACPSessionError(
|
|
3113
|
-
"unsupported_capability",
|
|
3114
|
-
"agent does not support session/delete (sessionCapabilities.delete not advertised)"
|
|
3115
|
-
);
|
|
3116
|
-
}
|
|
3117
|
-
const id = this.allocId();
|
|
3118
|
-
const result = await this.sendRequest(id, "session/delete", { sessionId });
|
|
3119
|
-
if (isJsonRpcError(result)) {
|
|
3120
|
-
throw new ACPSessionError(
|
|
3121
|
-
"prompt_failed",
|
|
3122
|
-
`session/delete failed: ${result.message}`,
|
|
3123
|
-
result
|
|
3124
|
-
);
|
|
3125
|
-
}
|
|
3126
|
-
if (this.sessionId === sessionId) {
|
|
3127
|
-
this.sessionId = null;
|
|
3128
|
-
}
|
|
3148
|
+
return executeDeleteSession(this.opContext(), sessionId);
|
|
3129
3149
|
}
|
|
3130
|
-
/**
|
|
3131
|
-
* Fork a session — create a new session from an existing one.
|
|
3132
|
-
*/
|
|
3133
3150
|
async forkSession(sourceSessionId, cwd, mcpServers) {
|
|
3134
|
-
|
|
3135
|
-
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
3136
|
-
const id = this.allocId();
|
|
3137
|
-
const result = await this.sendRequest(id, "session/fork", {
|
|
3138
|
-
sessionId: sourceSessionId,
|
|
3139
|
-
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
3140
|
-
...servers.length > 0 ? { mcpServers: servers } : {}
|
|
3141
|
-
});
|
|
3142
|
-
if (isJsonRpcError(result)) {
|
|
3143
|
-
throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
|
|
3144
|
-
}
|
|
3145
|
-
const newId = result.sessionId;
|
|
3146
|
-
if (typeof newId !== "string" || !newId) {
|
|
3147
|
-
throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
|
|
3148
|
-
}
|
|
3149
|
-
return newId;
|
|
3151
|
+
return executeForkSession(this.opContext(), sourceSessionId, cwd, mcpServers);
|
|
3150
3152
|
}
|
|
3151
|
-
/**
|
|
3152
|
-
* Set the active mode for a session.
|
|
3153
|
-
*/
|
|
3154
3153
|
async setMode(sessionId, modeId) {
|
|
3155
|
-
|
|
3156
|
-
const id = this.allocId();
|
|
3157
|
-
const result = await this.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
3158
|
-
if (isJsonRpcError(result)) {
|
|
3159
|
-
throw new ACPSessionError(
|
|
3160
|
-
"prompt_failed",
|
|
3161
|
-
`session/set_mode failed: ${result.message}`,
|
|
3162
|
-
result
|
|
3163
|
-
);
|
|
3164
|
-
}
|
|
3154
|
+
return executeSetMode(this.opContext(), sessionId, modeId);
|
|
3165
3155
|
}
|
|
3166
|
-
/**
|
|
3167
|
-
* Set a configuration option for a session.
|
|
3168
|
-
*/
|
|
3169
3156
|
async setConfigOption(sessionId, configId, value) {
|
|
3170
|
-
|
|
3171
|
-
const id = this.allocId();
|
|
3172
|
-
const result = await this.sendRequest(id, "session/set_config_option", {
|
|
3173
|
-
sessionId,
|
|
3174
|
-
configId,
|
|
3175
|
-
value
|
|
3176
|
-
});
|
|
3177
|
-
if (isJsonRpcError(result)) {
|
|
3178
|
-
throw new ACPSessionError(
|
|
3179
|
-
"prompt_failed",
|
|
3180
|
-
`session/set_config_option failed: ${result.message}`,
|
|
3181
|
-
result
|
|
3182
|
-
);
|
|
3183
|
-
}
|
|
3157
|
+
return executeSetConfigOption(this.opContext(), sessionId, configId, value);
|
|
3184
3158
|
}
|
|
3185
|
-
/**
|
|
3186
|
-
* List available providers and the current provider.
|
|
3187
|
-
*/
|
|
3188
3159
|
async listProviders() {
|
|
3189
|
-
|
|
3190
|
-
const id = this.allocId();
|
|
3191
|
-
const result = await this.sendRequest(id, "providers/list", {});
|
|
3192
|
-
if (isJsonRpcError(result)) {
|
|
3193
|
-
throw new ACPSessionError(
|
|
3194
|
-
"prompt_failed",
|
|
3195
|
-
`providers/list failed: ${result.message}`,
|
|
3196
|
-
result
|
|
3197
|
-
);
|
|
3198
|
-
}
|
|
3199
|
-
const r = result;
|
|
3200
|
-
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
3160
|
+
return executeListProviders(this.opContext());
|
|
3201
3161
|
}
|
|
3202
|
-
/**
|
|
3203
|
-
* Send an MCP message to the agent for routing.
|
|
3204
|
-
*/
|
|
3205
3162
|
async mcpMessage(connectionId, message) {
|
|
3206
|
-
|
|
3207
|
-
const id = this.allocId();
|
|
3208
|
-
const result = await this.sendRequest(id, "mcp/message", { connectionId, message });
|
|
3209
|
-
if (isJsonRpcError(result)) {
|
|
3210
|
-
throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
|
|
3211
|
-
}
|
|
3212
|
-
return result;
|
|
3163
|
+
return executeMcpMessage(this.opContext(), connectionId, message);
|
|
3213
3164
|
}
|
|
3214
|
-
/**
|
|
3215
|
-
* Set the active provider for the agent.
|
|
3216
|
-
*/
|
|
3217
3165
|
async setProvider(providerId, config) {
|
|
3218
|
-
|
|
3219
|
-
const id = this.allocId();
|
|
3220
|
-
const result = await this.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
|
|
3221
|
-
if (isJsonRpcError(result)) {
|
|
3222
|
-
throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
|
|
3223
|
-
}
|
|
3166
|
+
return executeSetProvider(this.opContext(), providerId, config);
|
|
3224
3167
|
}
|
|
3225
|
-
/**
|
|
3226
|
-
* Disable the current provider.
|
|
3227
|
-
*/
|
|
3228
3168
|
async disableProvider() {
|
|
3229
|
-
|
|
3230
|
-
const id = this.allocId();
|
|
3231
|
-
const result = await this.sendRequest(id, "providers/disable", {});
|
|
3232
|
-
if (isJsonRpcError(result)) {
|
|
3233
|
-
throw new ACPSessionError(
|
|
3234
|
-
"prompt_failed",
|
|
3235
|
-
`providers/disable failed: ${result.message}`,
|
|
3236
|
-
result
|
|
3237
|
-
);
|
|
3238
|
-
}
|
|
3169
|
+
return executeDisableProvider(this.opContext());
|
|
3239
3170
|
}
|
|
3240
3171
|
// ──────────────────────────────────────────────────────────────────────
|
|
3241
3172
|
// Prompt
|
|
3242
3173
|
// ──────────────────────────────────────────────────────────────────────
|
|
3243
|
-
/**
|
|
3244
|
-
* Run one prompt turn. Creates a session if needed, sends the
|
|
3245
|
-
* prompt, streams session/update notifications, and resolves with
|
|
3246
|
-
* the agent's response.
|
|
3247
|
-
*
|
|
3248
|
-
* @param blocks - Content blocks to send. Use `textContent()` for plain
|
|
3249
|
-
* text, or include ImageContent/AudioContent if the agent's
|
|
3250
|
-
* `promptCapabilities` allow it.
|
|
3251
|
-
* @param signal - AbortSignal for cancellation.
|
|
3252
|
-
*
|
|
3253
|
-
* Cancellation: if `signal` aborts mid-prompt, we send
|
|
3254
|
-
* `session/cancel` (a notification per spec) and keep accepting
|
|
3255
|
-
* updates until the agent returns with `stopReason: 'cancelled'`.
|
|
3256
|
-
* The result is the same shape as a normal turn, with
|
|
3257
|
-
* `stopReason === 'cancelled'`.
|
|
3258
|
-
*/
|
|
3259
3174
|
async prompt(blocks, signal, onProgress) {
|
|
3260
3175
|
if (this.closed) {
|
|
3261
3176
|
throw new ACPSessionError("closed", "session is closed");
|
|
@@ -3267,7 +3182,7 @@ var ACPSession = class _ACPSession {
|
|
|
3267
3182
|
return emptyRunResult("cancelled");
|
|
3268
3183
|
}
|
|
3269
3184
|
if (!this.sessionId) {
|
|
3270
|
-
await this.
|
|
3185
|
+
this.sessionId = await executeCreateSession(this.opContext());
|
|
3271
3186
|
}
|
|
3272
3187
|
this.resetScratch();
|
|
3273
3188
|
this.progressHandler = onProgress ?? null;
|
|
@@ -3329,33 +3244,6 @@ var ACPSession = class _ACPSession {
|
|
|
3329
3244
|
thoughts: this.scratch.thoughts
|
|
3330
3245
|
};
|
|
3331
3246
|
}
|
|
3332
|
-
async createSession() {
|
|
3333
|
-
const servers = this.filterMcpServers(this.opts.mcpServers);
|
|
3334
|
-
const id = this.allocId();
|
|
3335
|
-
const result = await this.sendRequest(id, "session/new", {
|
|
3336
|
-
cwd: this.opts.cwd ?? this.opts.projectRoot,
|
|
3337
|
-
mcpServers: servers
|
|
3338
|
-
});
|
|
3339
|
-
if (isJsonRpcError(result)) {
|
|
3340
|
-
throw new ACPSessionError(
|
|
3341
|
-
"session_create_failed",
|
|
3342
|
-
`session/new failed: ${result.message}`,
|
|
3343
|
-
result
|
|
3344
|
-
);
|
|
3345
|
-
}
|
|
3346
|
-
const sessionId = result.sessionId;
|
|
3347
|
-
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
3348
|
-
throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
|
|
3349
|
-
}
|
|
3350
|
-
this.sessionId = sessionId;
|
|
3351
|
-
}
|
|
3352
|
-
/**
|
|
3353
|
-
* Close the current session gracefully (if the agent supports it).
|
|
3354
|
-
*
|
|
3355
|
-
* Sends `session/close` JSON-RPC request, then clears the local
|
|
3356
|
-
* session id. Best-effort — errors are swallowed so the caller can
|
|
3357
|
-
* always proceed to transport teardown.
|
|
3358
|
-
*/
|
|
3359
3247
|
async closeSession() {
|
|
3360
3248
|
if (!this.sessionId) return;
|
|
3361
3249
|
const sid = this.sessionId;
|
|
@@ -3371,7 +3259,6 @@ var ACPSession = class _ACPSession {
|
|
|
3371
3259
|
// ──────────────────────────────────────────────────────────────────────
|
|
3372
3260
|
// Lifecycle — close
|
|
3373
3261
|
// ──────────────────────────────────────────────────────────────────────
|
|
3374
|
-
/** Tear down the session and kill the child process. */
|
|
3375
3262
|
async close() {
|
|
3376
3263
|
if (this.closed) return;
|
|
3377
3264
|
this.closed = true;
|
|
@@ -3398,24 +3285,6 @@ var ACPSession = class _ACPSession {
|
|
|
3398
3285
|
} catch {
|
|
3399
3286
|
}
|
|
3400
3287
|
}
|
|
3401
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
3402
|
-
// Helpers
|
|
3403
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
3404
|
-
/**
|
|
3405
|
-
* Filter MCP servers according to agent capabilities.
|
|
3406
|
-
* - Stdio servers are always included.
|
|
3407
|
-
* - HTTP servers are only included if agent supports mcpCapabilities.http.
|
|
3408
|
-
* - SSE servers are only included if agent supports mcpCapabilities.sse.
|
|
3409
|
-
*/
|
|
3410
|
-
filterMcpServers(servers) {
|
|
3411
|
-
if (!servers || servers.length === 0) return [];
|
|
3412
|
-
const mcpCaps = this.agentCapabilities.mcpCapabilities ?? {};
|
|
3413
|
-
return servers.filter((s) => {
|
|
3414
|
-
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
3415
|
-
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
3416
|
-
return true;
|
|
3417
|
-
});
|
|
3418
|
-
}
|
|
3419
3288
|
// ────────────────────────────────────────────────────────────────────
|
|
3420
3289
|
// Wire layer
|
|
3421
3290
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -3446,21 +3315,9 @@ var ACPSession = class _ACPSession {
|
|
|
3446
3315
|
});
|
|
3447
3316
|
});
|
|
3448
3317
|
}
|
|
3449
|
-
/**
|
|
3450
|
-
* Send a JSON-RPC 2.0 success response to an agent-initiated request.
|
|
3451
|
-
*
|
|
3452
|
-
* Per JSON-RPC 2.0 (and the official ACP SDK's message router) a Response
|
|
3453
|
-
* object MUST carry `jsonrpc: "2.0"` and MUST NOT carry a `method` field —
|
|
3454
|
-
* the SDK classifies any object with a `method` key as a Request and drops
|
|
3455
|
-
* it as a response, so an agent's `fs/*`, `terminal/*`, or
|
|
3456
|
-
* `session/request_permission` callback would hang forever. The legacy
|
|
3457
|
-
* `ACPMessage` type predates v1 (requires `method`, lacks `jsonrpc`), so we
|
|
3458
|
-
* build the correct wire object and cast at the boundary.
|
|
3459
|
-
*/
|
|
3460
3318
|
sendResult(id, result) {
|
|
3461
3319
|
return this.transport.send({ jsonrpc: "2.0", id, result });
|
|
3462
3320
|
}
|
|
3463
|
-
/** Send a JSON-RPC 2.0 error response (no `method` field, per spec). */
|
|
3464
3321
|
sendErrorResponse(id, code, message) {
|
|
3465
3322
|
return this.transport.send({
|
|
3466
3323
|
jsonrpc: "2.0",
|
|
@@ -3556,9 +3413,7 @@ var ACPSession = class _ACPSession {
|
|
|
3556
3413
|
} catch {
|
|
3557
3414
|
}
|
|
3558
3415
|
}
|
|
3559
|
-
/** Live progress handler installed for the duration of a `prompt()` turn. */
|
|
3560
3416
|
progressHandler = null;
|
|
3561
|
-
// Per-prompt scratch state
|
|
3562
3417
|
scratch = createSessionScratch();
|
|
3563
3418
|
resetScratch() {
|
|
3564
3419
|
this.scratch = createSessionScratch();
|