@wrongstack/acp 0.274.0 → 0.275.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.
package/dist/client.js CHANGED
@@ -2,6 +2,7 @@ import { expectDefined, writeErr } from '@wrongstack/core';
2
2
  import * as fsp from 'fs/promises';
3
3
  import * as path from 'path';
4
4
  import { spawn } from 'child_process';
5
+ import '@wrongstack/core/coordination';
5
6
 
6
7
  // src/agent/stdio-transport.ts
7
8
  var ClientTransport = class {
@@ -20,9 +21,10 @@ var ClientTransport = class {
20
21
  }
21
22
  async start() {
22
23
  if (this.child) return;
23
- const [{ spawn: spawn2 }, { buildChildEnv }] = await Promise.all([
24
+ const [{ spawn: spawn2 }, { buildChildEnv }, os] = await Promise.all([
24
25
  import('child_process'),
25
- import('@wrongstack/core')
26
+ import('@wrongstack/core'),
27
+ import('os')
26
28
  ]);
27
29
  return new Promise((resolve3, reject) => {
28
30
  const timeout = setTimeout(() => {
@@ -30,10 +32,12 @@ var ClientTransport = class {
30
32
  new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
31
33
  );
32
34
  }, this.opts.handshakeTimeoutMs);
35
+ const isPkgLauncher = this.opts.command === "npx" || this.opts.command === "uvx";
36
+ const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
33
37
  try {
34
38
  this.child = spawn2(this.opts.command, this.opts.args ?? [], {
35
39
  env: { ...buildChildEnv(), ...this.opts.env },
36
- cwd: this.opts.cwd,
40
+ cwd: spawnCwd,
37
41
  stdio: ["pipe", "pipe", "pipe"],
38
42
  windowsHide: true,
39
43
  // On Windows, most ACP-supporting tools (claude, gemini, codex,
@@ -52,17 +56,39 @@ var ClientTransport = class {
52
56
  }
53
57
  const child = this.child;
54
58
  child.stdout.setEncoding("utf8");
59
+ let settled = false;
60
+ const onSpawnFailure = (err) => {
61
+ if (settled) {
62
+ this.closed = true;
63
+ return;
64
+ }
65
+ settled = true;
66
+ clearTimeout(timeout);
67
+ reject(err);
68
+ };
69
+ child.on("error", onSpawnFailure);
70
+ child.stdout.on("error", onSpawnFailure);
71
+ if (this.opts.skipHandshakeMarker) {
72
+ child.stdout.on("data", (c) => this.onChildData(c));
73
+ child.stderr.on("data", (c) => this.onChildError(c));
74
+ child.on("close", (code) => this.onChildClose(code));
75
+ child.once("spawn", () => {
76
+ if (settled) return;
77
+ settled = true;
78
+ clearTimeout(timeout);
79
+ resolve3();
80
+ });
81
+ return;
82
+ }
55
83
  const onReady = () => {
84
+ if (settled) return;
85
+ settled = true;
56
86
  child.stdout.on("data", (c) => this.onChildData(c));
57
87
  child.stderr.on("data", (c) => this.onChildError(c));
58
88
  child.on("close", (code) => this.onChildClose(code));
59
89
  clearTimeout(timeout);
60
90
  resolve3();
61
91
  };
62
- if (this.opts.skipHandshakeMarker) {
63
- onReady();
64
- return;
65
- }
66
92
  const waitForMarker = (chunk) => {
67
93
  this.buffer += chunk;
68
94
  const idx = this.buffer.indexOf("[wstack-acp]\n");
@@ -73,14 +99,6 @@ var ClientTransport = class {
73
99
  }
74
100
  };
75
101
  child.stdout.on("data", waitForMarker);
76
- child.stdout.on("error", (err) => {
77
- clearTimeout(timeout);
78
- reject(err);
79
- });
80
- child.on("error", (err) => {
81
- clearTimeout(timeout);
82
- reject(err);
83
- });
84
102
  });
85
103
  }
86
104
  send(msg) {
@@ -153,6 +171,115 @@ var ClientTransport = class {
153
171
  }
154
172
  }
155
173
  };
174
+
175
+ // src/client/websocket-transport.ts
176
+ var WebSocketClientTransport = class {
177
+ ws = null;
178
+ handlers = /* @__PURE__ */ new Set();
179
+ closed = false;
180
+ opts;
181
+ constructor(opts) {
182
+ this.opts = opts;
183
+ }
184
+ start() {
185
+ const WS = globalThis.WebSocket;
186
+ if (!WS) {
187
+ return Promise.reject(
188
+ new Error(
189
+ "global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
190
+ )
191
+ );
192
+ }
193
+ const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
194
+ return new Promise((resolve3, reject) => {
195
+ let settled = false;
196
+ const ws = new WS(this.opts.url, this.opts.protocols);
197
+ this.ws = ws;
198
+ const timer = setTimeout(() => {
199
+ if (settled) return;
200
+ settled = true;
201
+ try {
202
+ ws.close();
203
+ } catch {
204
+ }
205
+ reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
206
+ }, timeoutMs);
207
+ ws.addEventListener("open", () => {
208
+ if (settled) return;
209
+ settled = true;
210
+ clearTimeout(timer);
211
+ resolve3();
212
+ });
213
+ ws.addEventListener("error", (ev) => {
214
+ if (settled) {
215
+ this.closed = true;
216
+ return;
217
+ }
218
+ settled = true;
219
+ clearTimeout(timer);
220
+ const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
221
+ reject(new Error(message));
222
+ });
223
+ ws.addEventListener("close", () => {
224
+ this.closed = true;
225
+ });
226
+ ws.addEventListener("message", (ev) => {
227
+ this.onData(ev.data);
228
+ });
229
+ });
230
+ }
231
+ send(msg) {
232
+ if (this.closed || !this.ws) {
233
+ return Promise.reject(new Error("WebSocket transport is not open"));
234
+ }
235
+ try {
236
+ this.ws.send(JSON.stringify(msg));
237
+ return Promise.resolve();
238
+ } catch (err) {
239
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
240
+ }
241
+ }
242
+ onMessage(handler) {
243
+ this.handlers.add(handler);
244
+ return () => this.handlers.delete(handler);
245
+ }
246
+ stop() {
247
+ this.closed = true;
248
+ if (this.ws) {
249
+ try {
250
+ this.ws.close();
251
+ } catch {
252
+ }
253
+ this.ws = null;
254
+ }
255
+ }
256
+ onData(data) {
257
+ const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
258
+ if (!text.trim()) return;
259
+ let msg;
260
+ try {
261
+ msg = JSON.parse(text);
262
+ } catch {
263
+ for (const line of text.split("\n")) {
264
+ if (!line.trim()) continue;
265
+ try {
266
+ this.dispatch(JSON.parse(line));
267
+ } catch {
268
+ }
269
+ }
270
+ return;
271
+ }
272
+ this.dispatch(msg);
273
+ }
274
+ dispatch(msg) {
275
+ for (const handler of [...this.handlers]) {
276
+ try {
277
+ handler(msg);
278
+ } catch {
279
+ }
280
+ }
281
+ }
282
+ };
156
283
  var DEFAULT_OPTIONS = {
157
284
  asyncTools: true,
158
285
  pollIntervalMs: 500,
@@ -195,6 +322,7 @@ var ToolTranslator = class {
195
322
  */
196
323
  async callTool(transport, name, args, callId = crypto.randomUUID()) {
197
324
  await transport.send({
325
+ jsonrpc: "2.0",
198
326
  method: "tools/call",
199
327
  id: callId,
200
328
  params: { name, arguments: args }
@@ -318,12 +446,11 @@ function randomHex(bytes) {
318
446
  }
319
447
 
320
448
  // src/client/permission.ts
321
- var defaultPermissionPolicy = async (req) => {
322
- if (req.signal.aborted) return { outcome: "cancelled" };
323
- const ranked = [...req.options].sort((a, b) => {
449
+ function pickAllow(options) {
450
+ const ranked = [...options].sort((a, b) => {
324
451
  const score = (k) => {
325
- if (k === "allow_always") return 0;
326
- if (k === "allow_once") return 1;
452
+ if (k === "allow_once") return 0;
453
+ if (k === "allow_always") return 1;
327
454
  if (k === "reject_once") return 2;
328
455
  return 3;
329
456
  };
@@ -334,7 +461,33 @@ var defaultPermissionPolicy = async (req) => {
334
461
  return { outcome: "cancelled" };
335
462
  }
336
463
  return { outcome: "selected", optionId: chosen.optionId };
464
+ }
465
+ function pickReject(options) {
466
+ const reject = options.find(
467
+ (o) => o.kind === "reject_once" || o.kind === "reject_always"
468
+ );
469
+ return reject ? { outcome: "selected", optionId: reject.optionId } : { outcome: "cancelled" };
470
+ }
471
+ var READ_ONLY_KINDS = /* @__PURE__ */ new Set(["read", "search", "fetch", "think"]);
472
+ var defaultPermissionPolicy = async (req) => {
473
+ if (req.signal.aborted) return { outcome: "cancelled" };
474
+ return pickAllow(req.options);
337
475
  };
476
+ var readOnlyPermissionPolicy = async (req) => {
477
+ if (req.signal.aborted) return { outcome: "cancelled" };
478
+ const kind = req.toolCall.kind;
479
+ if (kind && READ_ONLY_KINDS.has(kind)) {
480
+ return pickAllow(req.options);
481
+ }
482
+ return pickReject(req.options);
483
+ };
484
+ function makePermissionPolicy(decide) {
485
+ return async (req) => {
486
+ if (req.signal.aborted) return { outcome: "cancelled" };
487
+ const allow = await decide(req);
488
+ return allow ? pickAllow(req.options) : pickReject(req.options);
489
+ };
490
+ }
338
491
  var TerminalServer = class {
339
492
  terminals = /* @__PURE__ */ new Map();
340
493
  projectRoot;
@@ -530,6 +683,12 @@ var ACPSession = class _ACPSession {
530
683
  nextId = 1;
531
684
  /** True after close() has been called. */
532
685
  closed = false;
686
+ // Agent-provided info from the initialize handshake
687
+ agentCapabilities = {};
688
+ agentInfo = null;
689
+ authMethods = [];
690
+ /** Protocol version negotiated with the agent during initialize. */
691
+ negotiatedVersion = ACP_PROTOCOL_VERSION;
533
692
  constructor(opts, transport) {
534
693
  this.opts = opts;
535
694
  this.transport = transport;
@@ -551,6 +710,36 @@ var ACPSession = class _ACPSession {
551
710
  this.terminalServer = new TerminalServer(termOpts);
552
711
  this.permissionPolicy = opts.permissionPolicy ?? defaultPermissionPolicy;
553
712
  }
713
+ // ──────────────────────────────────────────────────────────────────────
714
+ // Public accessors
715
+ // ──────────────────────────────────────────────────────────────────────
716
+ /** Agent capabilities advertised during initialize. */
717
+ getCapabilities() {
718
+ return { ...this.agentCapabilities };
719
+ }
720
+ /** Authentication methods advertised by the agent. */
721
+ getAuthMethods() {
722
+ return [...this.authMethods];
723
+ }
724
+ /** Agent info (name, title, version) from initialize. */
725
+ getAgentInfo() {
726
+ return this.agentInfo;
727
+ }
728
+ /** Whether the agent requires authentication (has auth methods). */
729
+ requiresAuth() {
730
+ return this.authMethods.length > 0;
731
+ }
732
+ /** Current session id, if one exists. */
733
+ getSessionId() {
734
+ return this.sessionId;
735
+ }
736
+ /** Protocol version negotiated during initialize. */
737
+ getNegotiatedVersion() {
738
+ return this.negotiatedVersion;
739
+ }
740
+ // ──────────────────────────────────────────────────────────────────────
741
+ // Lifecycle — start
742
+ // ──────────────────────────────────────────────────────────────────────
554
743
  /**
555
744
  * Spawn the child, run the initialize handshake, install the
556
745
  * message dispatch, and return a ready session.
@@ -560,20 +749,37 @@ var ACPSession = class _ACPSession {
560
749
  command: opts.command,
561
750
  args: opts.args ? [...opts.args] : [],
562
751
  handshakeTimeoutMs: 3e4,
563
- // ACPSession is the v1 CLIENT side: it speaks to external agents
564
- // (Claude Code, Gemini CLI, …) that do NOT emit a `[wstack-acp]\n`
565
- // startup marker. The transport should treat the child as ready
566
- // as soon as the process is spawned and stdout is flowing.
567
752
  skipHandshakeMarker: true
568
753
  };
569
754
  if (opts.env !== void 0) transportOpts.env = opts.env;
570
755
  if (opts.cwd !== void 0) transportOpts.cwd = opts.cwd;
571
756
  const transport = new ClientTransport(transportOpts);
757
+ return _ACPSession.attach(opts, transport, `failed to spawn ${opts.command}`);
758
+ }
759
+ /**
760
+ * Connect to a REMOTE ACP agent over a WebSocket instead of spawning a
761
+ * local subprocess. `opts.command` is ignored for the wire (a label is
762
+ * still useful for `role`); everything else (projectRoot sandbox for
763
+ * fs/terminal, timeouts, permission policy, MCP servers) applies the same.
764
+ */
765
+ static async connectWebSocket(wsOpts, opts) {
766
+ const transport = new WebSocketClientTransport(wsOpts);
767
+ return _ACPSession.attach(opts, transport, `failed to connect to ${wsOpts.url}`);
768
+ }
769
+ /**
770
+ * Connect using a caller-supplied transport. Lets advanced callers plug
771
+ * in their own wire (SDK streams, in-process pipes, test doubles).
772
+ */
773
+ static async connect(transport, opts) {
774
+ return _ACPSession.attach(opts, transport, "failed to connect transport");
775
+ }
776
+ /** Shared connect path: start the transport, install dispatch, handshake. */
777
+ static async attach(opts, transport, spawnErrLabel) {
572
778
  try {
573
779
  await transport.start();
574
780
  } catch (err) {
575
781
  const msg = err instanceof Error ? err.message : String(err);
576
- throw new ACPSessionError("spawn_failed", `failed to spawn ${opts.command}: ${msg}`, err);
782
+ throw new ACPSessionError("spawn_failed", `${spawnErrLabel}: ${msg}`, err);
577
783
  }
578
784
  const session = new _ACPSession(opts, transport);
579
785
  transport.onMessage((msg) => session.handleMessage(msg));
@@ -588,14 +794,16 @@ var ACPSession = class _ACPSession {
588
794
  }
589
795
  return session;
590
796
  }
797
+ // ──────────────────────────────────────────────────────────────────────
798
+ // Initialization
799
+ // ──────────────────────────────────────────────────────────────────────
591
800
  async initialize() {
592
801
  const id = this.allocId();
593
802
  const result = await this.sendRequest(id, "initialize", {
594
803
  protocolVersion: ACP_PROTOCOL_VERSION,
595
804
  clientCapabilities: {
596
805
  fs: { readTextFile: true, writeTextFile: true },
597
- terminal: true,
598
- promptCapabilities: { image: false, audio: false, embeddedContext: true }
806
+ terminal: true
599
807
  },
600
808
  clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.263.0" }
601
809
  });
@@ -606,53 +814,344 @@ var ACPSession = class _ACPSession {
606
814
  throw new ACPSessionError("protocol_error", "initialize returned no protocolVersion");
607
815
  }
608
816
  const r = result;
609
- if (r.protocolVersion !== ACP_PROTOCOL_VERSION) {
817
+ if (r.protocolVersion > ACP_PROTOCOL_VERSION) {
818
+ throw new ACPSessionError(
819
+ "unsupported_capability",
820
+ `agent requires protocolVersion=${r.protocolVersion}, client supports up to ${ACP_PROTOCOL_VERSION}`
821
+ );
822
+ }
823
+ this.negotiatedVersion = r.protocolVersion;
824
+ this.agentCapabilities = r.agentCapabilities ?? {};
825
+ this.agentInfo = r.agentInfo ?? null;
826
+ this.authMethods = r.authMethods ?? [];
827
+ this.state = "ready";
828
+ }
829
+ // ──────────────────────────────────────────────────────────────────────
830
+ // Authentication
831
+ // ──────────────────────────────────────────────────────────────────────
832
+ /**
833
+ * Authenticate with the agent using one of the advertised auth methods.
834
+ * Call this AFTER start() and BEFORE any session/new call.
835
+ *
836
+ * Throws ACPSessionError('auth_failed') if the agent rejects the
837
+ * authentication or if the methodId is not in the advertised list.
838
+ */
839
+ async authenticate(methodId) {
840
+ if (this.state === "closed") {
841
+ throw new ACPSessionError("closed", "session is closed");
842
+ }
843
+ if (this.state !== "ready") {
844
+ throw new ACPSessionError(
845
+ "protocol_error",
846
+ `authenticate called in state=${this.state} (expected 'ready')`
847
+ );
848
+ }
849
+ if (!this.authMethods.some((m) => m.id === methodId)) {
850
+ throw new ACPSessionError(
851
+ "auth_failed",
852
+ `auth method "${methodId}" not in advertised methods: ${this.authMethods.map((m) => m.id).join(", ")}`
853
+ );
854
+ }
855
+ const id = this.allocId();
856
+ const result = await this.sendRequest(id, "authenticate", { methodId });
857
+ if (isJsonRpcError(result)) {
858
+ throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
859
+ }
860
+ this.state = "authenticated";
861
+ }
862
+ /**
863
+ * Log out from the current authenticated session.
864
+ * Only callable if the agent advertises `auth.logout` capability.
865
+ */
866
+ async logout() {
867
+ if (this.state === "closed") {
868
+ throw new ACPSessionError("closed", "session is closed");
869
+ }
870
+ if (!this.agentCapabilities.auth?.logout) {
610
871
  throw new ACPSessionError(
611
872
  "unsupported_capability",
612
- `agent speaks protocolVersion=${r.protocolVersion}, client speaks ${ACP_PROTOCOL_VERSION}`
873
+ "agent does not support logout (auth.logout capability not advertised)"
613
874
  );
614
875
  }
876
+ const id = this.allocId();
877
+ const result = await this.sendRequest(id, "logout", {});
878
+ if (isJsonRpcError(result)) {
879
+ throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
880
+ }
615
881
  this.state = "ready";
616
882
  }
883
+ // ──────────────────────────────────────────────────────────────────────
884
+ // Session management
885
+ // ──────────────────────────────────────────────────────────────────────
886
+ /**
887
+ * Load an existing session. The agent replays the conversation history
888
+ * via session/update notifications before responding.
889
+ *
890
+ * Only works if the agent advertises `loadSession` capability.
891
+ *
892
+ * @param sessionId - The session to load
893
+ * @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
894
+ * @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
895
+ */
896
+ async loadSession(sessionId, mcpServers, cwd) {
897
+ if (this.closed) {
898
+ throw new ACPSessionError("closed", "session is closed");
899
+ }
900
+ if (!this.agentCapabilities.loadSession) {
901
+ throw new ACPSessionError(
902
+ "unsupported_capability",
903
+ "agent does not support session/load (loadSession capability not advertised)"
904
+ );
905
+ }
906
+ if (this.sessionId) {
907
+ await this.closeSession();
908
+ }
909
+ this.resetScratch();
910
+ const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
911
+ const id = this.allocId();
912
+ const result = await this.sendRequest(id, "session/load", {
913
+ sessionId,
914
+ cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
915
+ mcpServers: servers
916
+ });
917
+ if (isJsonRpcError(result)) {
918
+ throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
919
+ }
920
+ this.sessionId = sessionId;
921
+ }
922
+ /**
923
+ * Resume an existing session without replaying history.
924
+ *
925
+ * Only works if the agent advertises `sessionCapabilities.resume`.
926
+ *
927
+ * @param sessionId - The session to resume
928
+ * @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
929
+ * @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
930
+ */
931
+ async resumeSession(sessionId, mcpServers, cwd) {
932
+ if (this.closed) {
933
+ throw new ACPSessionError("closed", "session is closed");
934
+ }
935
+ if (!this.agentCapabilities.sessionCapabilities?.resume) {
936
+ throw new ACPSessionError(
937
+ "unsupported_capability",
938
+ "agent does not support session/resume (sessionCapabilities.resume not advertised)"
939
+ );
940
+ }
941
+ if (this.sessionId) {
942
+ await this.closeSession();
943
+ }
944
+ const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
945
+ const id = this.allocId();
946
+ const result = await this.sendRequest(id, "session/resume", {
947
+ sessionId,
948
+ cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
949
+ mcpServers: servers
950
+ });
951
+ if (isJsonRpcError(result)) {
952
+ throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
953
+ }
954
+ this.sessionId = sessionId;
955
+ }
956
+ /**
957
+ * List existing sessions known to the agent.
958
+ *
959
+ * Only works if the agent advertises `sessionCapabilities.list`.
960
+ */
961
+ async listSessions(cursor, cwd) {
962
+ if (this.closed) {
963
+ throw new ACPSessionError("closed", "session is closed");
964
+ }
965
+ if (!this.agentCapabilities.sessionCapabilities?.list) {
966
+ throw new ACPSessionError(
967
+ "unsupported_capability",
968
+ "agent does not support session/list (sessionCapabilities.list not advertised)"
969
+ );
970
+ }
971
+ const id = this.allocId();
972
+ const params = {};
973
+ if (cursor !== void 0) params.cursor = cursor;
974
+ if (cwd !== void 0) params.cwd = cwd;
975
+ const result = await this.sendRequest(id, "session/list", params);
976
+ if (isJsonRpcError(result)) {
977
+ throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
978
+ }
979
+ const r = result;
980
+ return {
981
+ sessions: r.sessions ?? [],
982
+ nextCursor: r.nextCursor
983
+ };
984
+ }
985
+ /**
986
+ * Delete a session from the agent's session list.
987
+ *
988
+ * Only works if the agent advertises `sessionCapabilities.delete`.
989
+ */
990
+ async deleteSession(sessionId) {
991
+ if (this.closed) {
992
+ throw new ACPSessionError("closed", "session is closed");
993
+ }
994
+ if (!this.agentCapabilities.sessionCapabilities?.delete) {
995
+ throw new ACPSessionError(
996
+ "unsupported_capability",
997
+ "agent does not support session/delete (sessionCapabilities.delete not advertised)"
998
+ );
999
+ }
1000
+ const id = this.allocId();
1001
+ const result = await this.sendRequest(id, "session/delete", { sessionId });
1002
+ if (isJsonRpcError(result)) {
1003
+ throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
1004
+ }
1005
+ if (this.sessionId === sessionId) {
1006
+ this.sessionId = null;
1007
+ }
1008
+ }
1009
+ /**
1010
+ * Fork a session — create a new session from an existing one.
1011
+ */
1012
+ async forkSession(sourceSessionId, cwd, mcpServers) {
1013
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
1014
+ const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
1015
+ const id = this.allocId();
1016
+ const result = await this.sendRequest(id, "session/fork", {
1017
+ sessionId: sourceSessionId,
1018
+ cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
1019
+ ...servers.length > 0 ? { mcpServers: servers } : {}
1020
+ });
1021
+ if (isJsonRpcError(result)) {
1022
+ throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
1023
+ }
1024
+ const newId = result.sessionId;
1025
+ if (typeof newId !== "string" || !newId) {
1026
+ throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
1027
+ }
1028
+ return newId;
1029
+ }
1030
+ /**
1031
+ * Set the active mode for a session.
1032
+ */
1033
+ async setMode(sessionId, modeId) {
1034
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
1035
+ const id = this.allocId();
1036
+ const result = await this.sendRequest(id, "session/set_mode", { sessionId, modeId });
1037
+ if (isJsonRpcError(result)) {
1038
+ throw new ACPSessionError("prompt_failed", `session/set_mode failed: ${result.message}`, result);
1039
+ }
1040
+ }
1041
+ /**
1042
+ * Set a configuration option for a session.
1043
+ */
1044
+ async setConfigOption(sessionId, configId, value) {
1045
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
1046
+ const id = this.allocId();
1047
+ const result = await this.sendRequest(id, "session/set_config_option", {
1048
+ sessionId,
1049
+ configId,
1050
+ value
1051
+ });
1052
+ if (isJsonRpcError(result)) {
1053
+ throw new ACPSessionError("prompt_failed", `session/set_config_option failed: ${result.message}`, result);
1054
+ }
1055
+ }
1056
+ /**
1057
+ * List available providers and the current provider.
1058
+ */
1059
+ async listProviders() {
1060
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
1061
+ const id = this.allocId();
1062
+ const result = await this.sendRequest(id, "providers/list", {});
1063
+ if (isJsonRpcError(result)) {
1064
+ throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
1065
+ }
1066
+ const r = result;
1067
+ return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
1068
+ }
1069
+ /**
1070
+ * Send an MCP message to the agent for routing.
1071
+ */
1072
+ async mcpMessage(connectionId, message) {
1073
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
1074
+ const id = this.allocId();
1075
+ const result = await this.sendRequest(id, "mcp/message", { connectionId, message });
1076
+ if (isJsonRpcError(result)) {
1077
+ throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
1078
+ }
1079
+ return result;
1080
+ }
1081
+ /**
1082
+ * Set the active provider for the agent.
1083
+ */
1084
+ async setProvider(providerId, config) {
1085
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
1086
+ const id = this.allocId();
1087
+ const result = await this.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
1088
+ if (isJsonRpcError(result)) {
1089
+ throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
1090
+ }
1091
+ }
1092
+ /**
1093
+ * Disable the current provider.
1094
+ */
1095
+ async disableProvider() {
1096
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
1097
+ const id = this.allocId();
1098
+ const result = await this.sendRequest(id, "providers/disable", {});
1099
+ if (isJsonRpcError(result)) {
1100
+ throw new ACPSessionError("prompt_failed", `providers/disable failed: ${result.message}`, result);
1101
+ }
1102
+ }
1103
+ // ──────────────────────────────────────────────────────────────────────
1104
+ // Prompt
1105
+ // ──────────────────────────────────────────────────────────────────────
617
1106
  /**
618
1107
  * Run one prompt turn. Creates a session if needed, sends the
619
1108
  * prompt, streams session/update notifications, and resolves with
620
1109
  * the agent's response.
621
1110
  *
1111
+ * @param blocks - Content blocks to send. Use `textContent()` for plain
1112
+ * text, or include ImageContent/AudioContent if the agent's
1113
+ * `promptCapabilities` allow it.
1114
+ * @param signal - AbortSignal for cancellation.
1115
+ *
622
1116
  * Cancellation: if `signal` aborts mid-prompt, we send
623
1117
  * `session/cancel` (a notification per spec) and keep accepting
624
1118
  * updates until the agent returns with `stopReason: 'cancelled'`.
625
1119
  * The result is the same shape as a normal turn, with
626
1120
  * `stopReason === 'cancelled'`.
627
1121
  */
628
- async prompt(text, signal) {
1122
+ async prompt(blocks, signal, onProgress) {
629
1123
  if (this.closed) {
630
1124
  throw new ACPSessionError("closed", "session is closed");
631
1125
  }
632
- if (this.state !== "ready" && this.state !== "done") {
1126
+ if (this.state !== "ready" && this.state !== "authenticated" && this.state !== "done") {
633
1127
  throw new ACPSessionError("protocol_error", `prompt called in state=${this.state}`);
634
1128
  }
635
1129
  if (signal.aborted) {
636
- return { text: "", stopReason: "cancelled", hasText: false };
1130
+ return emptyRunResult("cancelled");
637
1131
  }
638
1132
  if (!this.sessionId) {
639
1133
  await this.createSession();
640
1134
  }
641
1135
  this.resetScratch();
1136
+ this.progressHandler = onProgress ?? null;
642
1137
  const promptId = this.allocId();
643
1138
  const turnPromise = this.sendRequest(
644
1139
  promptId,
645
1140
  "session/prompt",
646
1141
  {
647
1142
  sessionId: this.sessionId,
648
- prompt: [textContent(text)]
1143
+ prompt: blocks
649
1144
  },
650
1145
  this.timeoutMs
651
1146
  );
652
1147
  let cancelled = false;
653
1148
  const onAbort = () => {
654
1149
  cancelled = true;
655
- this.transport.send({ method: "session/cancel", params: { sessionId: this.sessionId } }).catch(() => {
1150
+ this.transport.send({
1151
+ jsonrpc: "2.0",
1152
+ method: "session/cancel",
1153
+ params: { sessionId: this.sessionId }
1154
+ }).catch(() => {
656
1155
  });
657
1156
  };
658
1157
  signal.addEventListener("abort", onAbort, { once: true });
@@ -670,6 +1169,7 @@ var ACPSession = class _ACPSession {
670
1169
  throw new ACPSessionError("prompt_failed", `session/prompt failed: ${msg}`, err);
671
1170
  } finally {
672
1171
  signal.removeEventListener("abort", onAbort);
1172
+ this.progressHandler = null;
673
1173
  }
674
1174
  this.state = "done";
675
1175
  if (isJsonRpcError(response)) {
@@ -682,14 +1182,18 @@ var ACPSession = class _ACPSession {
682
1182
  stopReason,
683
1183
  hasText: finalText.length > 0,
684
1184
  usage: this.scratch.usage,
685
- plan: this.scratch.plan
1185
+ plan: this.scratch.plan,
1186
+ toolCalls: [...this.scratch.toolCalls.values()],
1187
+ diffs: this.scratch.diffs,
1188
+ thoughts: this.scratch.thoughts
686
1189
  };
687
1190
  }
688
1191
  async createSession() {
1192
+ const servers = this.filterMcpServers(this.opts.mcpServers);
689
1193
  const id = this.allocId();
690
1194
  const result = await this.sendRequest(id, "session/new", {
691
1195
  cwd: this.opts.cwd ?? this.opts.projectRoot,
692
- mcpServers: []
1196
+ mcpServers: servers
693
1197
  });
694
1198
  if (isJsonRpcError(result)) {
695
1199
  throw new ACPSessionError(
@@ -708,12 +1212,40 @@ var ACPSession = class _ACPSession {
708
1212
  }
709
1213
  this.sessionId = sessionId;
710
1214
  }
1215
+ /**
1216
+ * Close the current session gracefully (if the agent supports it).
1217
+ *
1218
+ * Sends `session/close` JSON-RPC request, then clears the local
1219
+ * session id. Best-effort — errors are swallowed so the caller can
1220
+ * always proceed to transport teardown.
1221
+ */
1222
+ async closeSession() {
1223
+ if (!this.sessionId) return;
1224
+ const sid = this.sessionId;
1225
+ this.sessionId = null;
1226
+ if (this.agentCapabilities.sessionCapabilities?.close) {
1227
+ const id = this.allocId();
1228
+ try {
1229
+ await this.sendRequest(id, "session/close", { sessionId: sid }, 1e4);
1230
+ } catch {
1231
+ }
1232
+ }
1233
+ }
1234
+ // ──────────────────────────────────────────────────────────────────────
1235
+ // Lifecycle — close
1236
+ // ──────────────────────────────────────────────────────────────────────
711
1237
  /** Tear down the session and kill the child process. */
712
1238
  async close() {
713
1239
  if (this.closed) return;
714
1240
  this.closed = true;
715
1241
  this.state = "closed";
716
1242
  this.terminalServer.releaseAll();
1243
+ if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
1244
+ try {
1245
+ await this.closeSession();
1246
+ } catch {
1247
+ }
1248
+ }
717
1249
  for (const [, p] of this.pending) {
718
1250
  clearTimeout(p.timeoutHandle);
719
1251
  p.reject(new ACPSessionError("closed", "session was closed"));
@@ -724,6 +1256,24 @@ var ACPSession = class _ACPSession {
724
1256
  } catch {
725
1257
  }
726
1258
  }
1259
+ // ──────────────────────────────────────────────────────────────────────
1260
+ // Helpers
1261
+ // ──────────────────────────────────────────────────────────────────────
1262
+ /**
1263
+ * Filter MCP servers according to agent capabilities.
1264
+ * - Stdio servers are always included.
1265
+ * - HTTP servers are only included if agent supports mcpCapabilities.http.
1266
+ * - SSE servers are only included if agent supports mcpCapabilities.sse.
1267
+ */
1268
+ filterMcpServers(servers) {
1269
+ if (!servers || servers.length === 0) return [];
1270
+ const mcpCaps = this.agentCapabilities.mcpCapabilities ?? {};
1271
+ return servers.filter((s) => {
1272
+ if ("type" in s && s.type === "http") return mcpCaps.http === true;
1273
+ if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
1274
+ return true;
1275
+ });
1276
+ }
727
1277
  // ────────────────────────────────────────────────────────────────────
728
1278
  // Wire layer
729
1279
  // ────────────────────────────────────────────────────────────────────
@@ -757,6 +1307,28 @@ var ACPSession = class _ACPSession {
757
1307
  });
758
1308
  });
759
1309
  }
1310
+ /**
1311
+ * Send a JSON-RPC 2.0 success response to an agent-initiated request.
1312
+ *
1313
+ * Per JSON-RPC 2.0 (and the official ACP SDK's message router) a Response
1314
+ * object MUST carry `jsonrpc: "2.0"` and MUST NOT carry a `method` field —
1315
+ * the SDK classifies any object with a `method` key as a Request and drops
1316
+ * it as a response, so an agent's `fs/*`, `terminal/*`, or
1317
+ * `session/request_permission` callback would hang forever. The legacy
1318
+ * `ACPMessage` type predates v1 (requires `method`, lacks `jsonrpc`), so we
1319
+ * build the correct wire object and cast at the boundary.
1320
+ */
1321
+ sendResult(id, result) {
1322
+ return this.transport.send({ jsonrpc: "2.0", id, result });
1323
+ }
1324
+ /** Send a JSON-RPC 2.0 error response (no `method` field, per spec). */
1325
+ sendErrorResponse(id, code, message) {
1326
+ return this.transport.send({
1327
+ jsonrpc: "2.0",
1328
+ id,
1329
+ error: { code, message }
1330
+ });
1331
+ }
760
1332
  handleMessage(msg) {
761
1333
  if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
762
1334
  const pending = this.pending.get(msg.id);
@@ -786,6 +1358,23 @@ var ACPSession = class _ACPSession {
786
1358
  void this.handleTerminalRequest(msg);
787
1359
  return;
788
1360
  }
1361
+ if (msg.method === "mcp/connect" || msg.method === "mcp/message" || msg.method === "mcp/disconnect") {
1362
+ if (msg.id !== void 0) {
1363
+ this.sendResult(msg.id, {}).catch(() => {
1364
+ });
1365
+ }
1366
+ return;
1367
+ }
1368
+ if (msg.method === "elicitation/create" || msg.method === "elicitation/complete") {
1369
+ if (msg.id !== void 0) {
1370
+ this.sendResult(msg.id, {}).catch(() => {
1371
+ });
1372
+ }
1373
+ return;
1374
+ }
1375
+ if (msg.method === "$/cancel_request") {
1376
+ return;
1377
+ }
789
1378
  if (msg.method) {
790
1379
  console.warn(`[acp-session] unhandled method: ${msg.method}`);
791
1380
  }
@@ -794,31 +1383,44 @@ var ACPSession = class _ACPSession {
794
1383
  const update = msg.params?.update;
795
1384
  if (typeof update !== "object" || update === null) return;
796
1385
  const u = update;
1386
+ this.emitProgress({ type: "raw", update: u });
797
1387
  switch (u.sessionUpdate) {
798
1388
  case "agent_message_chunk": {
799
1389
  const text = extractText(u.content);
800
- if (text) this.accumulatedText(text);
1390
+ if (text) {
1391
+ this.scratch.text += text;
1392
+ this.emitProgress({ type: "message", text });
1393
+ }
801
1394
  return;
802
1395
  }
803
- case "thought_chunk":
1396
+ case "thought_chunk": {
1397
+ const text = extractText(u.content);
1398
+ if (text) {
1399
+ this.scratch.thoughts += text;
1400
+ this.emitProgress({ type: "thought", text });
1401
+ }
804
1402
  return;
1403
+ }
805
1404
  case "tool_call":
806
- case "tool_call_update":
1405
+ case "tool_call_update": {
1406
+ this.captureToolCall(u, u.sessionUpdate === "tool_call");
807
1407
  return;
1408
+ }
808
1409
  case "plan":
809
1410
  if (Array.isArray(u.entries)) {
810
- this.accumulatedPlan(u.entries);
1411
+ this.scratch.plan = u.entries;
1412
+ this.emitProgress({ type: "plan", entries: u.entries });
811
1413
  }
812
1414
  return;
813
1415
  case "usage_update":
814
1416
  if (typeof u.used === "number" && typeof u.size === "number") {
815
- this.accumulatedUsage({
1417
+ const usage = {
816
1418
  used: u.used,
817
1419
  size: u.size,
818
- ...typeof u.cost === "object" && u.cost !== null ? {
819
- cost: u.cost
820
- } : {}
821
- });
1420
+ ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
1421
+ };
1422
+ this.scratch.usage = usage;
1423
+ this.emitProgress({ type: "usage", usage });
822
1424
  }
823
1425
  return;
824
1426
  case "available_commands_update":
@@ -826,27 +1428,62 @@ var ACPSession = class _ACPSession {
826
1428
  case "config_option_update":
827
1429
  case "session_info_update":
828
1430
  case "user_message_chunk":
1431
+ case "next_edit_suggestions":
1432
+ case "elicitation":
829
1433
  return;
830
1434
  default:
831
- console.warn(`[acp-session] unhandled sessionUpdate: ${u.sessionUpdate}`);
832
1435
  return;
833
1436
  }
834
1437
  }
835
- // Per-prompt scratch state. Reset at the start of each prompt() and
836
- // read at the end to assemble the ACPSessionRunResult. The stream
837
- // pump writes to it via the three `accumulated*` helpers below.
838
- scratch = { text: "" };
839
- accumulatedText(chunk) {
840
- this.scratch.text += chunk;
841
- }
842
- accumulatedPlan(entries) {
843
- this.scratch.plan = entries;
1438
+ /**
1439
+ * Fold a `tool_call` / `tool_call_update` notification into the scratch
1440
+ * tool-call map (deduped by toolCallId), extract any `diff` content into
1441
+ * the diffs list, and emit live progress.
1442
+ */
1443
+ captureToolCall(u, isNew) {
1444
+ const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
1445
+ if (!toolCallId) return;
1446
+ const prev = this.scratch.toolCalls.get(toolCallId);
1447
+ const record = {
1448
+ toolCallId,
1449
+ title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
1450
+ kind: typeof u.kind === "string" ? u.kind : prev?.kind,
1451
+ status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
1452
+ rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
1453
+ rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
1454
+ };
1455
+ this.scratch.toolCalls.set(toolCallId, record);
1456
+ if (Array.isArray(u.content)) {
1457
+ for (const c of u.content) {
1458
+ if (c && typeof c === "object" && c.type === "diff") {
1459
+ const diff = {
1460
+ path: c.path,
1461
+ oldText: c.oldText,
1462
+ newText: c.newText
1463
+ };
1464
+ this.scratch.diffs.push(diff);
1465
+ this.emitProgress({ type: "diff", diff });
1466
+ }
1467
+ }
1468
+ }
1469
+ this.emitProgress({
1470
+ type: isNew ? "tool_call" : "tool_call_update",
1471
+ toolCall: record
1472
+ });
844
1473
  }
845
- accumulatedUsage(u) {
846
- this.scratch.usage = u;
1474
+ emitProgress(event) {
1475
+ if (!this.progressHandler) return;
1476
+ try {
1477
+ this.progressHandler(event);
1478
+ } catch {
1479
+ }
847
1480
  }
1481
+ /** Live progress handler installed for the duration of a `prompt()` turn. */
1482
+ progressHandler = null;
1483
+ // Per-prompt scratch state
1484
+ scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
848
1485
  resetScratch() {
849
- this.scratch = { text: "" };
1486
+ this.scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
850
1487
  }
851
1488
  async handlePermissionRequest(msg) {
852
1489
  const id = msg.id;
@@ -855,11 +1492,7 @@ var ACPSession = class _ACPSession {
855
1492
  const toolCall = params?.toolCall;
856
1493
  const options = Array.isArray(params?.options) ? params.options : [];
857
1494
  if (!toolCall) {
858
- await this.transport.send({
859
- id,
860
- method: "session/request_permission",
861
- error: { code: -32602, message: "toolCall is required" }
862
- });
1495
+ await this.sendErrorResponse(id, -32602, "toolCall is required");
863
1496
  return;
864
1497
  }
865
1498
  const policyAbort = new AbortController();
@@ -868,22 +1501,14 @@ var ACPSession = class _ACPSession {
868
1501
  options,
869
1502
  signal: policyAbort.signal
870
1503
  });
871
- await this.transport.send({
872
- id,
873
- method: "session/request_permission",
874
- result: { outcome }
875
- });
1504
+ await this.sendResult(id, { outcome });
876
1505
  }
877
1506
  async handleFsRequest(msg) {
878
1507
  const id = msg.id;
879
1508
  if (id === void 0) return;
880
1509
  const params = msg.params;
881
1510
  if (!params?.path) {
882
- await this.transport.send({
883
- id,
884
- method: msg.method,
885
- error: { code: -32602, message: "path is required" }
886
- });
1511
+ await this.sendErrorResponse(id, -32602, "path is required");
887
1512
  return;
888
1513
  }
889
1514
  try {
@@ -892,19 +1517,19 @@ var ACPSession = class _ACPSession {
892
1517
  sessionId: params.sessionId ?? "",
893
1518
  path: params.path
894
1519
  });
895
- await this.transport.send({ id, method: msg.method, result });
1520
+ await this.sendResult(id, result);
896
1521
  } else {
897
1522
  await this.fileServer.writeTextFile({
898
1523
  sessionId: params.sessionId ?? "",
899
1524
  path: params.path,
900
1525
  content: params.content ?? ""
901
1526
  });
902
- await this.transport.send({ id, method: msg.method, result: {} });
1527
+ await this.sendResult(id, {});
903
1528
  }
904
1529
  } catch (err) {
905
1530
  const code = err instanceof FsError ? -32602 : -32603;
906
1531
  const message = err instanceof Error ? err.message : String(err);
907
- await this.transport.send({ id, method: msg.method, error: { code, message } });
1532
+ await this.sendErrorResponse(id, code, message);
908
1533
  }
909
1534
  }
910
1535
  async handleTerminalRequest(msg) {
@@ -929,61 +1554,73 @@ var ACPSession = class _ACPSession {
929
1554
  createOpts.outputByteLimit = params.outputByteLimit;
930
1555
  }
931
1556
  const result = this.terminalServer.create(createOpts);
932
- await this.transport.send({ id, method: msg.method, result });
1557
+ await this.sendResult(id, result);
933
1558
  return;
934
1559
  }
935
1560
  case "terminal/output": {
936
1561
  const terminalId = String(params.terminalId ?? "");
937
1562
  const out = this.terminalServer.output(terminalId);
938
- await this.transport.send({ id, method: msg.method, result: out });
1563
+ await this.sendResult(id, out);
939
1564
  return;
940
1565
  }
941
1566
  case "terminal/wait_for_exit": {
942
1567
  const terminalId = String(params.terminalId ?? "");
943
1568
  const exit = await this.terminalServer.waitForExit(terminalId);
944
- await this.transport.send({ id, method: msg.method, result: exit });
1569
+ await this.sendResult(id, exit);
945
1570
  return;
946
1571
  }
947
1572
  case "terminal/kill": {
948
1573
  const terminalId = String(params.terminalId ?? "");
949
1574
  this.terminalServer.kill(terminalId);
950
- await this.transport.send({ id, method: msg.method, result: {} });
1575
+ await this.sendResult(id, {});
951
1576
  return;
952
1577
  }
953
1578
  case "terminal/release": {
954
1579
  const terminalId = String(params.terminalId ?? "");
955
1580
  this.terminalServer.release(terminalId);
956
- await this.transport.send({ id, method: msg.method, result: {} });
1581
+ await this.sendResult(id, {});
957
1582
  return;
958
1583
  }
959
1584
  default:
960
- await this.transport.send({
961
- id,
962
- method: msg.method,
963
- error: { code: -32601, message: `unknown method: ${msg.method}` }
964
- });
1585
+ await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
965
1586
  }
966
1587
  } catch (err) {
967
1588
  const message = err instanceof Error ? err.message : String(err);
968
- await this.transport.send({
969
- id,
970
- method: msg.method,
971
- error: { code: -32603, message }
972
- });
1589
+ await this.sendErrorResponse(id, -32603, message);
973
1590
  }
974
1591
  }
975
1592
  };
976
1593
  function textContent(text) {
977
1594
  return { type: "text", text };
978
1595
  }
1596
+ function imageContent(mimeType, data) {
1597
+ return { type: "image", mimeType, data };
1598
+ }
1599
+ function audioContent(mimeType, data) {
1600
+ return { type: "audio", mimeType, data };
1601
+ }
979
1602
  function extractText(block) {
980
1603
  if (typeof block !== "object" || block === null) return "";
981
1604
  const b = block;
982
1605
  if (b.type === "text" && typeof b.text === "string") return b.text;
1606
+ if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
1607
+ return b.resource.text;
1608
+ }
983
1609
  return "";
984
1610
  }
985
-
986
- // src/integration/acp-subagent-runner.ts
1611
+ function isRecord(v) {
1612
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1613
+ }
1614
+ function emptyRunResult(stopReason) {
1615
+ return {
1616
+ text: "",
1617
+ stopReason,
1618
+ hasText: false,
1619
+ toolCalls: [],
1620
+ diffs: [],
1621
+ thoughts: ""
1622
+ };
1623
+ }
987
1624
  async function makeACPSubagentRunner(options) {
988
1625
  const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
989
1626
  const wrappedRunner = async (task, ctx) => {
@@ -998,42 +1635,69 @@ async function makeACPSubagentRunner(options) {
998
1635
  async function makeACPSubagentRunnerWithStop(options) {
999
1636
  const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
1000
1637
  const timeoutMs = options.timeoutMs ?? 5 * 6e4;
1638
+ const persistent = options.persistent === true;
1639
+ let shared = null;
1640
+ const startSession = async () => {
1641
+ return ACPSession.start({
1642
+ command: options.command,
1643
+ ...options.args !== void 0 ? { args: options.args } : {},
1644
+ ...options.env !== void 0 ? { env: options.env } : {},
1645
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1646
+ projectRoot,
1647
+ timeoutMs,
1648
+ role: options.role,
1649
+ ...options.permissionPolicy !== void 0 ? { permissionPolicy: options.permissionPolicy } : {},
1650
+ ...options.mcpServers !== void 0 ? { mcpServers: options.mcpServers } : {}
1651
+ });
1652
+ };
1001
1653
  const runner = async (task, ctx) => {
1002
- let session = null;
1654
+ let session;
1655
+ const reuse = persistent && shared !== null;
1003
1656
  try {
1004
- session = await ACPSession.start({
1005
- command: options.command,
1006
- ...options.args !== void 0 ? { args: options.args } : {},
1007
- ...options.env !== void 0 ? { env: options.env } : {},
1008
- ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1009
- projectRoot,
1010
- timeoutMs,
1011
- role: options.role
1012
- });
1657
+ session = reuse ? shared : await startSession();
1658
+ if (persistent) shared = session;
1013
1659
  } catch (err) {
1014
1660
  throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
1015
1661
  }
1662
+ const onProgress = (event) => {
1663
+ try {
1664
+ ctx.budget.markActivity();
1665
+ } catch {
1666
+ }
1667
+ options.onProgress?.(event);
1668
+ };
1016
1669
  try {
1017
- const result = await session.prompt(task.description, ctx.signal);
1670
+ const result = await session.prompt(
1671
+ [textContent(task.description)],
1672
+ ctx.signal,
1673
+ onProgress
1674
+ );
1018
1675
  return {
1019
1676
  result: result.text,
1020
1677
  iterations: 1,
1021
- toolCalls: 0
1678
+ toolCalls: result.toolCalls.length
1022
1679
  };
1023
1680
  } catch (err) {
1024
- if (err instanceof ACPSessionError && err.kind === "aborted") {
1025
- throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
1026
- }
1027
1681
  throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
1028
1682
  } finally {
1683
+ if (!persistent) {
1684
+ try {
1685
+ await session.close();
1686
+ } catch {
1687
+ }
1688
+ }
1689
+ }
1690
+ };
1691
+ const stop = async () => {
1692
+ if (shared) {
1693
+ const s = shared;
1694
+ shared = null;
1029
1695
  try {
1030
- await session.close();
1696
+ await s.close();
1031
1697
  } catch {
1032
1698
  }
1033
1699
  }
1034
1700
  };
1035
- const stop = () => {
1036
- };
1037
1701
  return { runner, stop };
1038
1702
  }
1039
1703
  function acpErrorToSubagentError(err, subagentId) {
@@ -1072,6 +1736,9 @@ function mapACPKind(acpKind) {
1072
1736
  return "bridge_failed";
1073
1737
  case "prompt_failed":
1074
1738
  return "tool_failed";
1739
+ case "auth_failed":
1740
+ case "logout_failed":
1741
+ return "bridge_failed";
1075
1742
  case "aborted":
1076
1743
  return "aborted_by_parent";
1077
1744
  case "closed":
@@ -1092,6 +1759,6 @@ function isRetryable(kind) {
1092
1759
  }
1093
1760
  }
1094
1761
 
1095
- export { ClientTransport, ToolTranslator, makeACPSubagentRunner };
1762
+ export { ACPSession, ACPSessionError, ClientTransport, ToolTranslator, WebSocketClientTransport, audioContent, defaultPermissionPolicy, imageContent, makeACPSubagentRunner, makePermissionPolicy, readOnlyPermissionPolicy, textContent };
1096
1763
  //# sourceMappingURL=client.js.map
1097
1764
  //# sourceMappingURL=client.js.map