@camerontaylor/paseo-plugin 0.8.0-fork.1

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.
Files changed (45) hide show
  1. package/dist/attachments.d.ts +26 -0
  2. package/dist/attachments.js +17 -0
  3. package/dist/client/buttons.d.ts +64 -0
  4. package/dist/client/buttons.js +2 -0
  5. package/dist/client/client-state.d.ts +15 -0
  6. package/dist/client/client-state.js +32 -0
  7. package/dist/client/contracts.d.ts +201 -0
  8. package/dist/client/contracts.js +2 -0
  9. package/dist/client/host.d.ts +19 -0
  10. package/dist/client/host.js +12 -0
  11. package/dist/client/index.d.ts +10 -0
  12. package/dist/client/index.js +4 -0
  13. package/dist/client/paseo-context.d.ts +9 -0
  14. package/dist/client/paseo-context.js +16 -0
  15. package/dist/client/react-native.d.ts +46 -0
  16. package/dist/client/react-native.js +2 -0
  17. package/dist/client/rpc-context.d.ts +13 -0
  18. package/dist/client/rpc-context.js +18 -0
  19. package/dist/client/runtime-context-bridge.d.ts +5 -0
  20. package/dist/client/runtime-context-bridge.js +19 -0
  21. package/dist/client/shallow.d.ts +2 -0
  22. package/dist/client/shallow.js +45 -0
  23. package/dist/client/ui.d.ts +60 -0
  24. package/dist/client/ui.js +2 -0
  25. package/dist/contracts.d.ts +90 -0
  26. package/dist/contracts.js +2 -0
  27. package/dist/index.d.ts +5 -0
  28. package/dist/index.js +4 -0
  29. package/dist/rpc.d.ts +17 -0
  30. package/dist/rpc.js +13 -0
  31. package/dist/server/acp-internal/connection.d.ts +4 -0
  32. package/dist/server/acp-internal/connection.js +1187 -0
  33. package/dist/server/acp.d.ts +112 -0
  34. package/dist/server/acp.js +21 -0
  35. package/dist/server/contracts.d.ts +16 -0
  36. package/dist/server/contracts.js +2 -0
  37. package/dist/server/index.d.ts +3 -0
  38. package/dist/server/index.js +2 -0
  39. package/dist/server/lifecycle.d.ts +92 -0
  40. package/dist/server/lifecycle.js +2 -0
  41. package/dist/server/provider.d.ts +610 -0
  42. package/dist/server/provider.js +811 -0
  43. package/dist/settings.d.ts +70 -0
  44. package/dist/settings.js +40 -0
  45. package/package.json +90 -0
@@ -0,0 +1,1187 @@
1
+ import { spawn } from "node:child_process";
2
+ import { Readable, Writable } from "node:stream";
3
+ import { ClientSideConnection, CLIENT_METHODS, PROTOCOL_VERSION, ndJsonStream, } from "@agentclientprotocol/sdk";
4
+ import { z } from "zod";
5
+ import { PROVIDER_PROTOCOL_VERSION, ProviderEventSchema, ProviderInputSchema, negotiateProviderCapabilities, requireProviderCapabilities, } from "../provider.js";
6
+ const ADAPTER_CAPABILITIES = [
7
+ "prompt.message",
8
+ "prompt.command",
9
+ "session.configure",
10
+ "permission",
11
+ ];
12
+ export async function createAcpProviderConnection(options, request) {
13
+ if (!request.versions.includes(PROVIDER_PROTOCOL_VERSION)) {
14
+ throw new Error(`ACP adapter requires provider protocol ${PROVIDER_PROTOCOL_VERSION}`);
15
+ }
16
+ const probe = await AcpRuntime.start({
17
+ options,
18
+ boundarySessionId: "capability-probe",
19
+ env: {},
20
+ emit: () => undefined,
21
+ });
22
+ const supportedCapabilities = [...ADAPTER_CAPABILITIES];
23
+ if (probe.agentCapabilities.promptCapabilities?.image) {
24
+ supportedCapabilities.push("prompt.image");
25
+ }
26
+ if (probe.agentCapabilities.sessionCapabilities?.list) {
27
+ supportedCapabilities.push("session.list");
28
+ }
29
+ if (probe.agentCapabilities.loadSession) {
30
+ supportedCapabilities.push("session.persistence");
31
+ }
32
+ await probe.close();
33
+ const capabilities = negotiateProviderCapabilities(request.capabilities, supportedCapabilities);
34
+ const listeners = new Set();
35
+ const sessions = new Map();
36
+ const inFlight = new Set();
37
+ let closed = false;
38
+ let closePromise = null;
39
+ const emit = (event) => {
40
+ if (closed)
41
+ return;
42
+ const parsed = ProviderEventSchema.parse(event);
43
+ for (const listener of listeners)
44
+ listener(parsed);
45
+ };
46
+ const state = {
47
+ options,
48
+ capabilities,
49
+ sessions,
50
+ emit,
51
+ isClosed: () => closed,
52
+ };
53
+ return {
54
+ version: PROVIDER_PROTOCOL_VERSION,
55
+ capabilities,
56
+ async send(input) {
57
+ if (closed)
58
+ throw new Error("ACP provider connection is closed");
59
+ input = ProviderInputSchema.parse(input);
60
+ validateAdmission(input, { capabilities, sessions });
61
+ const operation = Promise.resolve().then(async () => {
62
+ if (closed)
63
+ return undefined;
64
+ await dispatch(input, state);
65
+ return undefined;
66
+ });
67
+ const settled = operation.catch((error) => {
68
+ emitOperationFailure(input, emit, error);
69
+ });
70
+ inFlight.add(settled);
71
+ void settled.finally(() => inFlight.delete(settled));
72
+ },
73
+ onEvent(listener) {
74
+ listeners.add(listener);
75
+ return () => listeners.delete(listener);
76
+ },
77
+ async close() {
78
+ if (closePromise)
79
+ return closePromise;
80
+ closed = true;
81
+ closePromise = (async () => {
82
+ await Promise.all(inFlight);
83
+ const active = [...sessions.values()];
84
+ sessions.clear();
85
+ await Promise.all(active.map(({ runtime }) => runtime.closeSession()));
86
+ listeners.clear();
87
+ })();
88
+ return closePromise;
89
+ },
90
+ };
91
+ }
92
+ function validateAdmission(input, state) {
93
+ if (input.type === "session.open") {
94
+ if (state.sessions.has(input.sessionId))
95
+ throw new Error(`Session already exists: ${input.sessionId}`);
96
+ requireProviderCapabilities(state.capabilities, input);
97
+ return;
98
+ }
99
+ if (!("sessionId" in input)) {
100
+ requireProviderCapabilities(state.capabilities, input);
101
+ return;
102
+ }
103
+ const session = requireSession(state, input.sessionId);
104
+ requireProviderCapabilities(session.capabilities, input);
105
+ }
106
+ function emitOperationFailure(input, emit, error) {
107
+ const failure = { message: describeError(error) };
108
+ if (input.type === "session.prompt") {
109
+ emit({
110
+ type: "session.prompt_result",
111
+ sessionId: input.sessionId,
112
+ clientMessageId: input.prompt.clientMessageId,
113
+ result: { type: "failed", error: failure },
114
+ });
115
+ return;
116
+ }
117
+ if ("requestId" in input) {
118
+ emit({ type: "request.failed", requestId: input.requestId, error: failure });
119
+ return;
120
+ }
121
+ if ("sessionId" in input) {
122
+ emit({ type: "session.runtime_failed", sessionId: input.sessionId, error: failure });
123
+ }
124
+ }
125
+ async function dispatch(input, state) {
126
+ switch (input.type) {
127
+ case "catalog":
128
+ await discover(input, state);
129
+ return;
130
+ case "sessions":
131
+ await listSessions(input, state);
132
+ return;
133
+ case "session.open":
134
+ await openSession(input, state);
135
+ return;
136
+ case "session.prompt":
137
+ await requireSession(state, input.sessionId).runtime.prompt(input);
138
+ return;
139
+ case "session.interrupt":
140
+ await requireSession(state, input.sessionId).runtime.interrupt();
141
+ state.emit({ type: "request.completed", requestId: input.requestId });
142
+ return;
143
+ case "session.permission":
144
+ requireSession(state, input.sessionId).runtime.respondToPermission(input.permissionId, input.response);
145
+ return;
146
+ case "session.configure": {
147
+ const session = requireSession(state, input.sessionId);
148
+ await mutateSession(session, () => session.runtime.configure(input.changes));
149
+ state.emit({ type: "request.completed", requestId: input.requestId });
150
+ return;
151
+ }
152
+ case "session.close": {
153
+ const session = requireSession(state, input.sessionId);
154
+ state.sessions.delete(input.sessionId);
155
+ await session.runtime.closeSession();
156
+ state.emit({ type: "session.closed", sessionId: input.sessionId });
157
+ state.emit({ type: "request.completed", requestId: input.requestId });
158
+ return;
159
+ }
160
+ case "session.archive":
161
+ case "session.unarchive":
162
+ case "session.revert":
163
+ state.emit({
164
+ type: "request.failed",
165
+ requestId: input.requestId,
166
+ error: { message: `${input.type} is not supported by this ACP provider` },
167
+ });
168
+ }
169
+ }
170
+ async function discover(input, state) {
171
+ const runtime = await AcpRuntime.start({
172
+ options: state.options,
173
+ boundarySessionId: "catalog",
174
+ env: {},
175
+ emit: state.emit,
176
+ });
177
+ try {
178
+ let catalog = await runtime.discover(input.cwd);
179
+ const config = runtime.configAccess();
180
+ for (const transformer of state.options.transformers ?? []) {
181
+ if (transformer.discover) {
182
+ catalog = await transformer.discover(catalog, { sessionId: "catalog", config });
183
+ }
184
+ }
185
+ state.emit({ type: "catalog", requestId: input.requestId, catalog });
186
+ }
187
+ finally {
188
+ await runtime.closeSession();
189
+ }
190
+ }
191
+ async function listSessions(input, state) {
192
+ const runtime = await AcpRuntime.start({
193
+ options: state.options,
194
+ boundarySessionId: "sessions",
195
+ env: {},
196
+ emit: state.emit,
197
+ });
198
+ try {
199
+ const response = await runtime.listSessions(input.cwd);
200
+ const selected = input.limit ? response.sessions.slice(0, input.limit) : response.sessions;
201
+ state.emit({
202
+ type: "sessions",
203
+ requestId: input.requestId,
204
+ sessions: selected.map((session) => ({
205
+ persistence: nativePersistence(session.sessionId),
206
+ cwd: session.cwd,
207
+ title: session.title ?? undefined,
208
+ updatedAt: session.updatedAt ?? undefined,
209
+ })),
210
+ });
211
+ }
212
+ finally {
213
+ await runtime.close();
214
+ }
215
+ }
216
+ async function openSession(input, state) {
217
+ if (state.sessions.has(input.sessionId))
218
+ throw new Error(`Session already exists: ${input.sessionId}`);
219
+ const runtime = await AcpRuntime.start({
220
+ options: state.options,
221
+ boundarySessionId: input.sessionId,
222
+ env: input.config.env,
223
+ emit: state.emit,
224
+ });
225
+ try {
226
+ const capabilities = await runtime.open(input, state.capabilities);
227
+ if (state.isClosed()) {
228
+ await runtime.closeSession();
229
+ return;
230
+ }
231
+ state.sessions.set(input.sessionId, {
232
+ runtime,
233
+ capabilities,
234
+ mutationLane: Promise.resolve(),
235
+ });
236
+ }
237
+ catch (error) {
238
+ await runtime.close();
239
+ throw error;
240
+ }
241
+ }
242
+ async function mutateSession(session, operation) {
243
+ const result = session.mutationLane.then(operation);
244
+ session.mutationLane = result.catch(() => undefined);
245
+ return result;
246
+ }
247
+ function requireSession(state, sessionId) {
248
+ const session = state.sessions.get(sessionId);
249
+ if (!session)
250
+ throw new Error(`Unknown session: ${sessionId}`);
251
+ return session;
252
+ }
253
+ class AcpRuntime {
254
+ static async start(options) {
255
+ let child = null;
256
+ let spawnFailure = null;
257
+ let stream;
258
+ let closeConnector = async () => { };
259
+ if (options.options.command) {
260
+ const [executable, ...args] = options.options.command;
261
+ child = spawn(executable, args, {
262
+ env: { ...process.env, ...options.env },
263
+ stdio: ["pipe", "pipe", "pipe"],
264
+ });
265
+ spawnFailure = new Promise((_resolve, reject) => child.once("error", reject));
266
+ child.stderr.on("data", () => undefined);
267
+ const output = Writable.toWeb(child.stdin);
268
+ const input = Readable.toWeb(child.stdout);
269
+ stream = ndJsonStream(output, input);
270
+ }
271
+ else {
272
+ const owned = ownConnectorStream(await options.options.connector());
273
+ stream = owned.stream;
274
+ closeConnector = owned.close;
275
+ }
276
+ const runtime = new AcpRuntime(stream, child, closeConnector, options);
277
+ try {
278
+ const initialize = withTimeout(runtime.call(runtime.connection.initialize({
279
+ protocolVersion: PROTOCOL_VERSION,
280
+ clientCapabilities: {},
281
+ clientInfo: { name: "paseo", version: "1" },
282
+ })), options.options.acpOptions?.startupTimeoutMs ?? 10000, `ACP provider ${options.options.id} did not initialize`);
283
+ const initialized = spawnFailure
284
+ ? await Promise.race([initialize, spawnFailure])
285
+ : await initialize;
286
+ runtime.agentCapabilities = initialized.agentCapabilities ?? {};
287
+ return runtime;
288
+ }
289
+ catch (error) {
290
+ await runtime.close();
291
+ throw error;
292
+ }
293
+ }
294
+ constructor(stream, child, closeConnector, options) {
295
+ this.options = options;
296
+ this.agentCapabilities = {};
297
+ this.nativeSessionId = "";
298
+ this.messages = new Map();
299
+ this.toolCalls = new Map();
300
+ this.pendingCompactions = new Set();
301
+ this.permissions = new Map();
302
+ this.configOptions = [];
303
+ this.modes = null;
304
+ this.commandWaiter = null;
305
+ this.messageSequence = 0;
306
+ this.closing = false;
307
+ this.processFailed = false;
308
+ this.configTransaction = false;
309
+ this.stagedTransformerConfig = null;
310
+ this.notificationLane = Promise.resolve();
311
+ this.promptLane = Promise.resolve();
312
+ this.activePrompt = null;
313
+ this.child = child;
314
+ this.closeConnector = closeConnector;
315
+ this.emit = options.emit;
316
+ this.transformers = options.options.transformers ?? [];
317
+ const client = {
318
+ requestPermission: (request) => this.requestPermission(request),
319
+ sessionUpdate: (notification) => this.enqueueNotification(() => this.sessionUpdate(notification)),
320
+ };
321
+ this.connection = new ClientSideConnection(() => client, routeVendorNotifications(stream, (method, params) => this.vendorNotification(method, params)));
322
+ if (!child)
323
+ void this.connection.closed.then(() => this.handleUnexpectedTransportClose());
324
+ child?.on("error", (error) => {
325
+ this.processFailed = true;
326
+ this.settlePendingWork();
327
+ if (this.closing || !this.nativeSessionId)
328
+ return;
329
+ this.emit({
330
+ type: "session.runtime_failed",
331
+ sessionId: this.options.boundarySessionId,
332
+ error: { message: describeError(error) },
333
+ });
334
+ });
335
+ child?.once("close", (code, signal) => {
336
+ if (this.closing || this.processFailed || !this.nativeSessionId)
337
+ return;
338
+ this.processFailed = true;
339
+ this.settlePendingWork();
340
+ this.emit({
341
+ type: "session.runtime_failed",
342
+ sessionId: this.options.boundarySessionId,
343
+ error: { message: `ACP process exited (${signal ?? code ?? "unknown"})` },
344
+ });
345
+ });
346
+ }
347
+ async open(input, connectionCapabilities) {
348
+ const mcpServers = toAcpMcpServers(input.config);
349
+ const nativeSessionId = readNativeSessionId(input.persistence);
350
+ const metadata = {
351
+ _paseo: {
352
+ systemPrompt: input.config.systemPrompt,
353
+ providerOptions: input.config.providerOptions,
354
+ toolPolicy: input.config.toolPolicy,
355
+ persist: input.config.persist,
356
+ },
357
+ };
358
+ let response;
359
+ if (nativeSessionId) {
360
+ response = await this.call(this.connection.loadSession({
361
+ sessionId: nativeSessionId,
362
+ cwd: input.config.cwd,
363
+ mcpServers,
364
+ _meta: metadata,
365
+ }));
366
+ this.nativeSessionId = nativeSessionId;
367
+ }
368
+ else {
369
+ const newSession = await this.call(this.connection.newSession({
370
+ cwd: input.config.cwd,
371
+ mcpServers,
372
+ _meta: metadata,
373
+ }));
374
+ response = newSession;
375
+ this.nativeSessionId = newSession.sessionId;
376
+ }
377
+ this.modes = response.modes;
378
+ this.configOptions = response.configOptions ?? [];
379
+ await this.applyInitialConfig(input.config);
380
+ const sessionCapabilities = connectionCapabilities.filter((capability) => capability !== "session.configure" ||
381
+ this.modes !== null ||
382
+ this.configOptions.length > 0 ||
383
+ this.transformers.some((transformer) => transformer.configure !== undefined));
384
+ this.emit({
385
+ type: "session.opened",
386
+ requestId: input.requestId,
387
+ sessionId: input.sessionId,
388
+ capabilities: sessionCapabilities,
389
+ restoration: "core",
390
+ persistence: nativePersistence(this.nativeSessionId),
391
+ title: input.config.title,
392
+ cwd: input.config.cwd,
393
+ });
394
+ this.emitConfig();
395
+ if (this.options.options.acpOptions?.waitForInitialCommands) {
396
+ await this.waitForInitialCommands();
397
+ }
398
+ this.emit({ type: "session.ready", requestId: input.requestId, sessionId: input.sessionId });
399
+ return sessionCapabilities;
400
+ }
401
+ async discover(cwd = process.cwd()) {
402
+ const response = await this.call(this.connection.newSession({ cwd, mcpServers: [] }));
403
+ this.nativeSessionId = response.sessionId;
404
+ this.modes = response.modes;
405
+ this.configOptions = response.configOptions ?? [];
406
+ return toProviderCatalog(response.modes, response.configOptions ?? []);
407
+ }
408
+ prompt(input) {
409
+ const admission = this.promptLane.then(() => this.admitPrompt(input));
410
+ this.promptLane = admission.catch(() => undefined);
411
+ return admission;
412
+ }
413
+ async admitPrompt(input) {
414
+ const active = this.activePrompt;
415
+ if (active) {
416
+ await this.interrupt();
417
+ await active.settled;
418
+ }
419
+ const prompt = toAcpPrompt(input.prompt);
420
+ const turnId = `acp:${input.prompt.clientMessageId}`;
421
+ this.emit({
422
+ type: "timeline.item",
423
+ sessionId: this.options.boundarySessionId,
424
+ item: {
425
+ type: "user_message",
426
+ id: input.prompt.clientMessageId,
427
+ text: prompt
428
+ .filter((part) => part.type === "text")
429
+ .map((part) => part.text)
430
+ .join("\n"),
431
+ clientMessageId: input.prompt.clientMessageId,
432
+ },
433
+ });
434
+ this.emit({
435
+ type: "session.prompt_result",
436
+ sessionId: this.options.boundarySessionId,
437
+ clientMessageId: input.prompt.clientMessageId,
438
+ result: { type: "turn", turnId },
439
+ });
440
+ this.emit({
441
+ type: "session.turn",
442
+ sessionId: this.options.boundarySessionId,
443
+ turnId,
444
+ state: "started",
445
+ });
446
+ const settled = this.call(this.connection.prompt({ sessionId: this.nativeSessionId, prompt })).then((response) => {
447
+ const state = response.stopReason === "cancelled" ? "canceled" : "completed";
448
+ this.terminalizeTransientItems(state);
449
+ this.emit({
450
+ type: "session.turn",
451
+ sessionId: this.options.boundarySessionId,
452
+ turnId,
453
+ state,
454
+ });
455
+ return undefined;
456
+ }, (error) => {
457
+ this.terminalizeTransientItems("failed");
458
+ this.emit({
459
+ type: "session.turn",
460
+ sessionId: this.options.boundarySessionId,
461
+ turnId,
462
+ state: "failed",
463
+ error: { message: describeError(error) },
464
+ });
465
+ return undefined;
466
+ });
467
+ this.activePrompt = { turnId, settled };
468
+ void settled.finally(() => {
469
+ if (this.activePrompt?.turnId === turnId)
470
+ this.activePrompt = null;
471
+ });
472
+ }
473
+ interrupt() {
474
+ return this.call(this.connection.cancel({ sessionId: this.nativeSessionId }));
475
+ }
476
+ listSessions(cwd) {
477
+ return this.call(this.connection.listSessions({ cwd }));
478
+ }
479
+ async drainNotifications() {
480
+ await new Promise((resolve) => setTimeout(resolve, 0));
481
+ await this.notificationLane;
482
+ }
483
+ respondToPermission(permissionId, response) {
484
+ const pending = this.permissions.get(permissionId);
485
+ if (!pending)
486
+ throw new Error(`Unknown ACP permission: ${permissionId}`);
487
+ const selected = selectPermissionOption(pending.request.options, response);
488
+ if (response.selectedActionId !== undefined && !selected) {
489
+ throw new Error(`ACP permission action '${response.selectedActionId}' does not exist or does not match '${response.behavior}' behavior`);
490
+ }
491
+ this.permissions.delete(permissionId);
492
+ pending.resolve({
493
+ outcome: selected
494
+ ? { outcome: "selected", optionId: selected.optionId }
495
+ : { outcome: "cancelled" },
496
+ });
497
+ this.emit({
498
+ type: "session.permission_resolved",
499
+ sessionId: this.options.boundarySessionId,
500
+ permissionId,
501
+ });
502
+ }
503
+ async configure(changes) {
504
+ const ordered = [];
505
+ if (changes.model !== undefined)
506
+ ordered.push({ target: "model", value: changes.model });
507
+ if (changes.mode !== undefined)
508
+ ordered.push({ target: "mode", value: changes.mode });
509
+ if (changes.thinkingOption !== undefined) {
510
+ ordered.push({ target: "thinking", value: changes.thinkingOption });
511
+ }
512
+ for (const [id, value] of Object.entries(changes.settings ?? {})) {
513
+ ordered.push({ target: "setting", id, value });
514
+ }
515
+ const previousModes = this.modes ? structuredClone(this.modes) : null;
516
+ const previousOptions = structuredClone(this.configOptions);
517
+ this.configTransaction = true;
518
+ this.stagedTransformerConfig = null;
519
+ let committedConfig = null;
520
+ try {
521
+ for (const change of ordered)
522
+ await this.applyConfigChange(change);
523
+ committedConfig =
524
+ this.stagedTransformerConfig ?? toProviderConfigState(this.modes, this.configOptions);
525
+ }
526
+ catch (error) {
527
+ try {
528
+ await this.restoreConfig(previousModes, previousOptions);
529
+ }
530
+ catch (rollbackError) {
531
+ const failure = new AggregateError([error, rollbackError], `ACP configuration failed and rollback could not restore the previous state`);
532
+ this.emit({
533
+ type: "session.runtime_failed",
534
+ sessionId: this.options.boundarySessionId,
535
+ error: { message: describeError(failure) },
536
+ });
537
+ await this.close();
538
+ throw failure;
539
+ }
540
+ this.modes = previousModes;
541
+ this.configOptions = previousOptions;
542
+ this.stagedTransformerConfig = null;
543
+ throw error;
544
+ }
545
+ finally {
546
+ this.configTransaction = false;
547
+ }
548
+ this.stagedTransformerConfig = null;
549
+ this.emitConfigSnapshot(committedConfig);
550
+ }
551
+ async closeSession() {
552
+ if (this.nativeSessionId) {
553
+ await withTimeout(this.call(this.connection.closeSession({ sessionId: this.nativeSessionId })), 1000, `ACP session ${this.nativeSessionId} did not close`).catch(() => undefined);
554
+ }
555
+ await this.close();
556
+ }
557
+ async close() {
558
+ if (this.closing)
559
+ return;
560
+ this.closing = true;
561
+ this.settlePendingWork();
562
+ await this.closeConnector();
563
+ const child = this.child;
564
+ if (!child || this.processFailed)
565
+ return;
566
+ if (child.exitCode !== null || child.signalCode !== null)
567
+ return;
568
+ const closed = new Promise((resolve) => child.once("close", () => resolve()));
569
+ child.kill("SIGTERM");
570
+ if (await settlesWithin(closed, 1000))
571
+ return;
572
+ child.kill("SIGKILL");
573
+ if (!(await settlesWithin(closed, 1000))) {
574
+ throw new Error(`ACP provider ${this.options.options.id} did not terminate after SIGKILL`);
575
+ }
576
+ }
577
+ async applyInitialConfig(config) {
578
+ if (config.model)
579
+ await this.applyConfigChange({ target: "model", value: config.model });
580
+ if (config.mode)
581
+ await this.applyConfigChange({ target: "mode", value: config.mode });
582
+ if (config.thinkingOption) {
583
+ await this.applyConfigChange({ target: "thinking", value: config.thinkingOption });
584
+ }
585
+ for (const [id, value] of Object.entries(config.settings)) {
586
+ await this.applyConfigChange({ target: "setting", id, value });
587
+ }
588
+ }
589
+ async applyConfigChange(change) {
590
+ const access = this.configAccess();
591
+ for (const transformer of this.transformers) {
592
+ if ((await transformer.configure?.(change, {
593
+ sessionId: this.options.boundarySessionId,
594
+ config: access,
595
+ })) === "handled") {
596
+ return;
597
+ }
598
+ }
599
+ if (change.target === "mode") {
600
+ if (!change.value || !this.modes) {
601
+ throw new Error("ACP session does not expose mode configuration");
602
+ }
603
+ await this.call(this.connection.setSessionMode({
604
+ sessionId: this.nativeSessionId,
605
+ modeId: change.value,
606
+ }));
607
+ this.modes = { ...this.modes, currentModeId: change.value };
608
+ return;
609
+ }
610
+ let category = null;
611
+ if (change.target === "model")
612
+ category = "model";
613
+ if (change.target === "thinking")
614
+ category = "thought_level";
615
+ const configId = change.target === "setting"
616
+ ? change.id
617
+ : this.configOptions.find((option) => option.category === category)?.id;
618
+ if (!configId) {
619
+ throw new Error(`ACP session does not expose ${change.target} configuration`);
620
+ }
621
+ if (change.value === null) {
622
+ throw new Error(`ACP configuration ${configId} cannot be cleared`);
623
+ }
624
+ const response = await this.call(this.connection.setSessionConfigOption({
625
+ sessionId: this.nativeSessionId,
626
+ configId,
627
+ ...(typeof change.value === "boolean"
628
+ ? { type: "boolean", value: change.value }
629
+ : { value: String(change.value) }),
630
+ }));
631
+ this.configOptions = response.configOptions;
632
+ }
633
+ configAccess() {
634
+ return {
635
+ read: async () => configValues(this.configOptions),
636
+ set: async (id, value) => {
637
+ if (typeof value !== "string" && typeof value !== "boolean") {
638
+ throw new Error(`ACP configuration ${id} accepts only string or boolean values`);
639
+ }
640
+ const response = await this.call(this.connection.setSessionConfigOption({
641
+ sessionId: this.nativeSessionId,
642
+ configId: id,
643
+ ...(typeof value === "boolean" ? { type: "boolean", value } : { value }),
644
+ }));
645
+ this.configOptions = response.configOptions;
646
+ },
647
+ };
648
+ }
649
+ emitConfig() {
650
+ if (this.configTransaction)
651
+ return;
652
+ this.emitConfigSnapshot(toProviderConfigState(this.modes, this.configOptions));
653
+ }
654
+ emitConfigSnapshot(config) {
655
+ this.emit({
656
+ type: "session.config",
657
+ sessionId: this.options.boundarySessionId,
658
+ config,
659
+ });
660
+ }
661
+ async restoreConfig(modes, options) {
662
+ if (modes?.currentModeId && this.modes?.currentModeId !== modes.currentModeId) {
663
+ await this.call(this.connection.setSessionMode({
664
+ sessionId: this.nativeSessionId,
665
+ modeId: modes.currentModeId,
666
+ }));
667
+ }
668
+ const currentValues = configValues(this.configOptions);
669
+ for (const option of options) {
670
+ const value = option.currentValue;
671
+ if (currentValues[option.id] === value)
672
+ continue;
673
+ await this.call(this.connection.setSessionConfigOption({
674
+ sessionId: this.nativeSessionId,
675
+ configId: option.id,
676
+ ...(typeof value === "boolean" ? { type: "boolean", value } : { value }),
677
+ }));
678
+ }
679
+ }
680
+ requestPermission(request) {
681
+ const permissionId = `permission:${request.toolCall.toolCallId}`;
682
+ return new Promise((resolve) => {
683
+ this.permissions.set(permissionId, { request, resolve });
684
+ this.emit({
685
+ type: "session.permission",
686
+ sessionId: this.options.boundarySessionId,
687
+ request: {
688
+ id: permissionId,
689
+ name: request.toolCall.name ?? request.toolCall.title ?? "Tool",
690
+ kind: "tool",
691
+ title: request.toolCall.title ?? undefined,
692
+ input: jsonRecord(request.toolCall.rawInput),
693
+ actions: request.options.map((option) => ({
694
+ id: option.optionId,
695
+ label: option.name,
696
+ behavior: option.kind.startsWith("allow") ? "allow" : "deny",
697
+ })),
698
+ },
699
+ });
700
+ });
701
+ }
702
+ sessionUpdate(notification) {
703
+ if (notification.sessionId !== this.nativeSessionId)
704
+ return;
705
+ this.reduceUpdate(notification.update);
706
+ }
707
+ reduceUpdate(update) {
708
+ if (update.sessionUpdate === "agent_message_chunk" ||
709
+ update.sessionUpdate === "agent_thought_chunk" ||
710
+ update.sessionUpdate === "user_message_chunk") {
711
+ if (update.sessionUpdate === "user_message_chunk")
712
+ return;
713
+ if (update.content.type !== "text")
714
+ return;
715
+ const fallbackId = `${update.sessionUpdate}:${++this.messageSequence}`;
716
+ const id = update.messageId ?? fallbackId;
717
+ const text = `${this.messages.get(id) ?? ""}${update.content.text}`;
718
+ this.messages.set(id, text);
719
+ this.emit({
720
+ type: "timeline.item",
721
+ sessionId: this.options.boundarySessionId,
722
+ item: {
723
+ type: update.sessionUpdate === "agent_message_chunk" ? "assistant_message" : "reasoning",
724
+ id,
725
+ text,
726
+ ...(update.sessionUpdate === "agent_message_chunk"
727
+ ? { messageId: update.messageId ?? undefined }
728
+ : {}),
729
+ },
730
+ });
731
+ return;
732
+ }
733
+ if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") {
734
+ this.reduceToolCall(update);
735
+ return;
736
+ }
737
+ this.reduceStateUpdate(update);
738
+ }
739
+ reduceStateUpdate(update) {
740
+ if (update.sessionUpdate === "plan") {
741
+ this.emit({
742
+ type: "timeline.item",
743
+ sessionId: this.options.boundarySessionId,
744
+ item: {
745
+ type: "todo",
746
+ id: "acp-plan",
747
+ items: update.entries.map((entry, index) => ({
748
+ id: String(index),
749
+ text: entry.content,
750
+ completed: entry.status === "completed",
751
+ status: entry.status,
752
+ })),
753
+ },
754
+ });
755
+ return;
756
+ }
757
+ if (update.sessionUpdate === "available_commands_update") {
758
+ const commands = update.availableCommands.map((command) => ({
759
+ name: command.name,
760
+ description: command.description,
761
+ argumentHint: command.input?.hint,
762
+ }));
763
+ this.emit({ type: "session.commands", sessionId: this.options.boundarySessionId, commands });
764
+ this.commandWaiter?.();
765
+ this.commandWaiter = null;
766
+ return;
767
+ }
768
+ if (update.sessionUpdate === "current_mode_update") {
769
+ if (this.modes)
770
+ this.modes = { ...this.modes, currentModeId: update.currentModeId };
771
+ this.emitConfig();
772
+ return;
773
+ }
774
+ if (update.sessionUpdate === "config_option_update") {
775
+ this.configOptions = update.configOptions;
776
+ this.emitConfig();
777
+ return;
778
+ }
779
+ if (update.sessionUpdate === "usage_update") {
780
+ this.emit({
781
+ type: "session.usage",
782
+ sessionId: this.options.boundarySessionId,
783
+ usage: {
784
+ contextWindowUsedTokens: update.used,
785
+ contextWindowMaxTokens: update.size,
786
+ totalCostUsd: update.cost?.currency === "USD" ? update.cost.amount : undefined,
787
+ },
788
+ });
789
+ return;
790
+ }
791
+ if (update.sessionUpdate === "compaction_update") {
792
+ if (update.status === "completed")
793
+ this.pendingCompactions.delete(update.compactionId);
794
+ else
795
+ this.pendingCompactions.add(update.compactionId);
796
+ this.emit({
797
+ type: "timeline.item",
798
+ sessionId: this.options.boundarySessionId,
799
+ item: {
800
+ type: "compaction",
801
+ id: update.compactionId,
802
+ status: update.status === "completed" ? "completed" : "loading",
803
+ },
804
+ });
805
+ }
806
+ }
807
+ terminalizeTransientItems(state) {
808
+ for (const [id, snapshot] of this.toolCalls) {
809
+ if (snapshot.status !== "pending" && snapshot.status !== "in_progress")
810
+ continue;
811
+ const completed = {
812
+ ...snapshot,
813
+ status: state === "completed" ? "completed" : "failed",
814
+ };
815
+ this.toolCalls.set(id, completed);
816
+ this.emit({
817
+ type: "timeline.item",
818
+ sessionId: this.options.boundarySessionId,
819
+ item: toolTimelineItem(completed),
820
+ });
821
+ }
822
+ for (const id of this.pendingCompactions) {
823
+ this.emit({
824
+ type: "timeline.item",
825
+ sessionId: this.options.boundarySessionId,
826
+ item: { type: "compaction", id, status: "completed" },
827
+ });
828
+ }
829
+ this.pendingCompactions.clear();
830
+ }
831
+ reduceToolCall(update) {
832
+ const current = this.toolCalls.get(update.toolCallId);
833
+ let snapshot = mergeToolCallSnapshot(update, current);
834
+ const context = { sessionId: this.options.boundarySessionId };
835
+ for (const transformer of this.transformers)
836
+ snapshot = transformer.toolCall?.(snapshot, context) ?? snapshot;
837
+ this.toolCalls.set(snapshot.id, snapshot);
838
+ this.emit({
839
+ type: "timeline.item",
840
+ sessionId: this.options.boundarySessionId,
841
+ item: toolTimelineItem(snapshot),
842
+ });
843
+ }
844
+ vendorNotification(method, params) {
845
+ const notification = { method, params: jsonValue(params) };
846
+ const context = { sessionId: this.options.boundarySessionId };
847
+ for (const transformer of this.transformers) {
848
+ const transformed = transformer.notification?.(notification, context);
849
+ if (!transformed)
850
+ continue;
851
+ const updates = Array.isArray(transformed) ? transformed : [transformed];
852
+ for (const update of updates)
853
+ this.emitVendorUpdate(update);
854
+ }
855
+ }
856
+ emitVendorUpdate(update) {
857
+ if (update.type === "commands") {
858
+ this.emit({
859
+ type: "session.commands",
860
+ sessionId: this.options.boundarySessionId,
861
+ commands: [...update.commands],
862
+ });
863
+ }
864
+ else if (update.type === "config") {
865
+ if (this.configTransaction)
866
+ this.stagedTransformerConfig = update.config;
867
+ else
868
+ this.emitConfigSnapshot(update.config);
869
+ }
870
+ else if (update.type === "timeline") {
871
+ this.emit({
872
+ type: "timeline.item",
873
+ sessionId: this.options.boundarySessionId,
874
+ item: update.item,
875
+ });
876
+ }
877
+ else {
878
+ this.emit({
879
+ type: "session.notice",
880
+ sessionId: this.options.boundarySessionId,
881
+ notice: update.notice,
882
+ });
883
+ }
884
+ }
885
+ call(operation) {
886
+ if (this.connection.signal.aborted) {
887
+ return Promise.reject(new Error("ACP transport is closed"));
888
+ }
889
+ return Promise.race([
890
+ operation,
891
+ this.connection.closed.then(() => {
892
+ throw new Error("ACP transport closed unexpectedly");
893
+ }),
894
+ ]);
895
+ }
896
+ enqueueNotification(operation) {
897
+ const result = this.notificationLane.then(operation);
898
+ this.notificationLane = result.catch(() => undefined);
899
+ return result;
900
+ }
901
+ handleUnexpectedTransportClose() {
902
+ if (this.closing || this.processFailed)
903
+ return;
904
+ this.processFailed = true;
905
+ this.settlePendingWork();
906
+ if (!this.nativeSessionId)
907
+ return;
908
+ this.emit({
909
+ type: "session.runtime_failed",
910
+ sessionId: this.options.boundarySessionId,
911
+ error: { message: "ACP transport closed unexpectedly" },
912
+ });
913
+ }
914
+ settlePendingWork() {
915
+ for (const permission of this.permissions.values()) {
916
+ permission.resolve({ outcome: { outcome: "cancelled" } });
917
+ }
918
+ this.permissions.clear();
919
+ this.commandWaiter?.();
920
+ this.commandWaiter = null;
921
+ }
922
+ waitForInitialCommands() {
923
+ const timeoutMs = this.options.options.acpOptions?.initialCommandsTimeoutMs ?? 1000;
924
+ const commands = new Promise((resolve) => {
925
+ this.commandWaiter = resolve;
926
+ });
927
+ const timeout = new Promise((resolve) => {
928
+ setTimeout(resolve, timeoutMs);
929
+ });
930
+ return Promise.race([commands, timeout]).finally(() => {
931
+ this.commandWaiter = null;
932
+ });
933
+ }
934
+ }
935
+ function routeVendorNotifications(stream, receive) {
936
+ const clientMethods = new Set(Object.values(CLIENT_METHODS));
937
+ return {
938
+ writable: stream.writable,
939
+ readable: stream.readable.pipeThrough(new TransformStream({
940
+ transform(message, controller) {
941
+ if ("method" in message && !("id" in message) && !clientMethods.has(message.method)) {
942
+ // The SDK dispatches extensions asynchronously and can resolve a later response first.
943
+ // Run synchronous vendor transforms in wire order, inside the configuration transaction.
944
+ try {
945
+ receive(message.method, message.params);
946
+ }
947
+ catch (error) {
948
+ console.error("Error handling ACP vendor notification", error);
949
+ }
950
+ }
951
+ else
952
+ controller.enqueue(message);
953
+ },
954
+ })),
955
+ };
956
+ }
957
+ function selectPermissionOption(options, response) {
958
+ if (response.selectedActionId !== undefined) {
959
+ return (options.find((option) => option.optionId === response.selectedActionId &&
960
+ permissionOptionBehavior(option) === response.behavior) ?? null);
961
+ }
962
+ return options.find((option) => permissionOptionBehavior(option) === response.behavior) ?? null;
963
+ }
964
+ function permissionOptionBehavior(option) {
965
+ return option.kind.startsWith("allow") ? "allow" : "deny";
966
+ }
967
+ function ownConnectorStream(source) {
968
+ const reader = source.readable.getReader();
969
+ const writer = source.writable.getWriter();
970
+ let closed = false;
971
+ const stream = {
972
+ readable: new ReadableStream({
973
+ async pull(controller) {
974
+ try {
975
+ const next = await reader.read();
976
+ if (next.done)
977
+ controller.close();
978
+ else
979
+ controller.enqueue(next.value);
980
+ }
981
+ catch (error) {
982
+ controller.error(error);
983
+ }
984
+ },
985
+ cancel: (reason) => reader.cancel(reason),
986
+ }),
987
+ writable: new WritableStream({
988
+ write: (message) => writer.write(message),
989
+ close: () => writer.close(),
990
+ abort: (reason) => writer.abort(reason),
991
+ }),
992
+ };
993
+ return {
994
+ stream,
995
+ async close() {
996
+ if (closed)
997
+ return;
998
+ closed = true;
999
+ await Promise.allSettled([
1000
+ reader.cancel(new Error("Paseo closed the ACP connector")),
1001
+ writer.close(),
1002
+ ]);
1003
+ },
1004
+ };
1005
+ }
1006
+ function mergeToolCallSnapshot(update, current) {
1007
+ return {
1008
+ id: update.toolCallId,
1009
+ name: firstText(update.name, current?.name),
1010
+ title: firstText(update.title, current?.title, update.name) ?? "Tool call",
1011
+ kind: firstText(update.kind, current?.kind),
1012
+ status: firstDefined(update.status, current?.status, "pending") ?? "pending",
1013
+ input: jsonValue(firstDefined(update.rawInput, current?.input, null)),
1014
+ output: jsonValue(firstDefined(update.rawOutput, current?.output, null)),
1015
+ locations: update.locations?.map((location) => location.path) ?? current?.locations ?? [],
1016
+ };
1017
+ }
1018
+ function firstDefined(...values) {
1019
+ return values.find((value) => value !== undefined);
1020
+ }
1021
+ function firstText(...values) {
1022
+ return values.find((value) => typeof value === "string");
1023
+ }
1024
+ function toAcpMcpServers(config) {
1025
+ return Object.entries(config.mcpServers).map(([name, server]) => {
1026
+ if (server.type === "stdio") {
1027
+ return {
1028
+ name,
1029
+ command: server.command,
1030
+ args: server.args ?? [],
1031
+ env: Object.entries(server.env ?? {}).map(([key, value]) => ({ name: key, value })),
1032
+ };
1033
+ }
1034
+ return {
1035
+ type: server.type,
1036
+ name,
1037
+ url: server.url,
1038
+ headers: Object.entries(server.headers ?? {}).map(([key, value]) => ({ name: key, value })),
1039
+ };
1040
+ });
1041
+ }
1042
+ function toAcpPrompt(prompt) {
1043
+ if (prompt.input.type === "command") {
1044
+ const suffix = prompt.input.arguments ? ` ${prompt.input.arguments}` : "";
1045
+ return [{ type: "text", text: `/${prompt.input.name}${suffix}` }];
1046
+ }
1047
+ return prompt.input.content.map((content) => {
1048
+ if (content.type === "text" || content.type === "image")
1049
+ return content;
1050
+ return { type: "text", text: JSON.stringify(content) };
1051
+ });
1052
+ }
1053
+ function nativePersistence(sessionId) {
1054
+ return { version: 1, data: { sessionId } };
1055
+ }
1056
+ function readNativeSessionId(persistence) {
1057
+ if (!persistence ||
1058
+ typeof persistence.data !== "object" ||
1059
+ persistence.data === null ||
1060
+ Array.isArray(persistence.data))
1061
+ return null;
1062
+ return typeof persistence.data.sessionId === "string" ? persistence.data.sessionId : null;
1063
+ }
1064
+ function toProviderConfigState(modes, options) {
1065
+ const model = options.find((option) => option.category === "model");
1066
+ const thinking = options.find((option) => option.category === "thought_level");
1067
+ return {
1068
+ model: selectedValue(model),
1069
+ mode: modes?.currentModeId,
1070
+ thinkingOption: selectedValue(thinking),
1071
+ models: selectOptions(model).map((option) => ({ id: option.value, label: option.label })),
1072
+ modes: modes?.availableModes.map((mode) => ({
1073
+ id: mode.id,
1074
+ label: mode.name,
1075
+ description: mode.description ?? undefined,
1076
+ })) ?? [],
1077
+ thinkingOptions: selectOptions(thinking).map((option) => ({
1078
+ id: option.value,
1079
+ label: option.label,
1080
+ })),
1081
+ settings: options
1082
+ .filter((option) => option.category !== "model" && option.category !== "thought_level")
1083
+ .map(toProviderSetting),
1084
+ };
1085
+ }
1086
+ function toProviderCatalog(modes, options) {
1087
+ const state = toProviderConfigState(modes, options);
1088
+ return {
1089
+ models: state.models,
1090
+ modes: state.modes,
1091
+ thinkingOptions: state.thinkingOptions,
1092
+ defaultModel: state.model,
1093
+ defaultMode: state.mode,
1094
+ defaultThinkingOption: state.thinkingOption,
1095
+ };
1096
+ }
1097
+ function toProviderSetting(option) {
1098
+ if (option.type === "boolean") {
1099
+ return {
1100
+ type: "toggle",
1101
+ id: option.id,
1102
+ label: option.name,
1103
+ description: option.description ?? undefined,
1104
+ value: option.currentValue,
1105
+ };
1106
+ }
1107
+ return {
1108
+ type: "select",
1109
+ id: option.id,
1110
+ label: option.name,
1111
+ description: option.description ?? undefined,
1112
+ value: option.currentValue,
1113
+ options: selectOptions(option),
1114
+ };
1115
+ }
1116
+ function selectOptions(option) {
1117
+ if (!option || option.type === "boolean")
1118
+ return [];
1119
+ return option.options.flatMap((candidate) => "group" in candidate
1120
+ ? candidate.options.map((nested) => ({ label: nested.name, value: nested.value }))
1121
+ : [{ label: candidate.name, value: candidate.value }]);
1122
+ }
1123
+ function selectedValue(option) {
1124
+ return option && option.type !== "boolean" ? option.currentValue : undefined;
1125
+ }
1126
+ function configValues(options) {
1127
+ return Object.fromEntries(options.map((option) => [option.id, option.currentValue]));
1128
+ }
1129
+ function toolTimelineItem(snapshot) {
1130
+ const input = jsonRecord(snapshot.input);
1131
+ const isEdit = snapshot.kind === "edit" || snapshot.name?.toLowerCase().includes("edit") === true;
1132
+ const detail = isEdit
1133
+ ? {
1134
+ type: "edit",
1135
+ filePath: typeof input.filePath === "string" ? input.filePath : (snapshot.locations[0] ?? ""),
1136
+ oldString: typeof input.oldString === "string" ? input.oldString : undefined,
1137
+ newString: typeof input.newString === "string" ? input.newString : undefined,
1138
+ unifiedDiff: typeof input.unifiedDiff === "string" ? input.unifiedDiff : undefined,
1139
+ }
1140
+ : { type: "unknown", input: snapshot.input, output: snapshot.output };
1141
+ const status = snapshot.status === "in_progress" || snapshot.status === "pending"
1142
+ ? "running"
1143
+ : snapshot.status;
1144
+ const base = {
1145
+ type: "tool_call",
1146
+ id: snapshot.id,
1147
+ callId: snapshot.id,
1148
+ name: snapshot.name ?? snapshot.title,
1149
+ detail,
1150
+ };
1151
+ if (status === "failed")
1152
+ return { ...base, status, error: snapshot.output };
1153
+ return { ...base, status, error: null };
1154
+ }
1155
+ function jsonValue(value) {
1156
+ return z.json().parse(value);
1157
+ }
1158
+ function jsonRecord(value) {
1159
+ const parsed = jsonValue(value);
1160
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
1161
+ return {};
1162
+ return parsed;
1163
+ }
1164
+ function describeError(error) {
1165
+ return error instanceof Error ? error.message : String(error);
1166
+ }
1167
+ function withTimeout(promise, timeoutMs, message) {
1168
+ return new Promise((resolve, reject) => {
1169
+ const timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
1170
+ promise.then((value) => {
1171
+ clearTimeout(timeout);
1172
+ resolve(value);
1173
+ return undefined;
1174
+ }, (error) => {
1175
+ clearTimeout(timeout);
1176
+ reject(error);
1177
+ return undefined;
1178
+ });
1179
+ });
1180
+ }
1181
+ async function settlesWithin(promise, timeoutMs) {
1182
+ return Promise.race([
1183
+ promise.then(() => true),
1184
+ new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs)),
1185
+ ]);
1186
+ }
1187
+ //# sourceMappingURL=connection.js.map