@wrongstack/acp 0.295.0 → 0.296.2

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 (49) hide show
  1. package/dist/agent/protocol-handler.d.ts +9 -0
  2. package/dist/agent/protocol-handler.d.ts.map +1 -1
  3. package/dist/agent/server-agent-turn.d.ts +83 -1
  4. package/dist/agent/server-agent-turn.d.ts.map +1 -1
  5. package/dist/agent/session-store.d.ts.map +1 -1
  6. package/dist/agent/stdio-transport.d.ts +18 -0
  7. package/dist/agent/stdio-transport.d.ts.map +1 -1
  8. package/dist/agent/wrongstack-acp-agent.d.ts +35 -0
  9. package/dist/agent/wrongstack-acp-agent.d.ts.map +1 -1
  10. package/dist/agent.js +30 -23
  11. package/dist/agent.js.map +3 -3
  12. package/dist/client/acp-message-routing.d.ts +2 -0
  13. package/dist/client/acp-message-routing.d.ts.map +1 -0
  14. package/dist/client/acp-session-callbacks.d.ts +12 -0
  15. package/dist/client/acp-session-callbacks.d.ts.map +1 -0
  16. package/dist/client/acp-session-content.d.ts +24 -0
  17. package/dist/client/acp-session-content.d.ts.map +1 -0
  18. package/dist/client/acp-session-errors.d.ts +13 -0
  19. package/dist/client/acp-session-errors.d.ts.map +1 -0
  20. package/dist/client/acp-session-types.d.ts +81 -0
  21. package/dist/client/acp-session-types.d.ts.map +1 -0
  22. package/dist/client/acp-session-updates.d.ts +18 -0
  23. package/dist/client/acp-session-updates.d.ts.map +1 -0
  24. package/dist/client/acp-session.d.ts +6 -171
  25. package/dist/client/acp-session.d.ts.map +1 -1
  26. package/dist/client/file-server.d.ts +19 -0
  27. package/dist/client/file-server.d.ts.map +1 -1
  28. package/dist/client/terminal-server.d.ts.map +1 -1
  29. package/dist/client/trust-boundary-permission.d.ts +16 -1
  30. package/dist/client/trust-boundary-permission.d.ts.map +1 -1
  31. package/dist/client/websocket-transport.d.ts.map +1 -1
  32. package/dist/client.js +358 -348
  33. package/dist/client.js.map +3 -3
  34. package/dist/index.js +409 -395
  35. package/dist/index.js.map +4 -4
  36. package/dist/integration/acp-bench.d.ts +32 -0
  37. package/dist/integration/acp-bench.d.ts.map +1 -1
  38. package/dist/integration/acp-subagent-runner.d.ts.map +1 -1
  39. package/dist/integration/ensemble-runner.d.ts +22 -1
  40. package/dist/integration/ensemble-runner.d.ts.map +1 -1
  41. package/dist/registry/acp-registry-fetch.d.ts +1 -1
  42. package/dist/registry/acp-registry-fetch.d.ts.map +1 -1
  43. package/dist/registry/ensemble-registry.d.ts +22 -0
  44. package/dist/registry/ensemble-registry.d.ts.map +1 -1
  45. package/dist/version.d.ts +8 -0
  46. package/dist/version.d.ts.map +1 -1
  47. package/dist/wrongstack-acp-agent.js +26 -20
  48. package/dist/wrongstack-acp-agent.js.map +2 -2
  49. package/package.json +3 -3
package/dist/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/agent/stdio-transport.ts
2
2
  import { expectDefined, writeErr } from "@wrongstack/core/utils";
3
+ import { treeKill } from "@wrongstack/core/utils/tree-kill";
3
4
 
4
5
  // src/win32-cmd.ts
5
6
  var WIN32_CMD_META = /[&|<>"\r\n\0]/;
@@ -29,7 +30,7 @@ function quoteWin32CmdArg(arg) {
29
30
  var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
30
31
  var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
31
32
  function positiveLimit(value, fallback) {
32
- return Number.isFinite(value) && (value ?? 0) > 0 ? Math.floor(value) : fallback;
33
+ return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
33
34
  }
34
35
  var ClientTransport = class {
35
36
  child = null;
@@ -47,10 +48,7 @@ var ClientTransport = class {
47
48
  ...options
48
49
  };
49
50
  this.maxFrameChars = positiveLimit(options.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
50
- this.maxQueuedMessages = positiveLimit(
51
- options.maxQueuedMessages,
52
- DEFAULT_MAX_QUEUED_MESSAGES
53
- );
51
+ this.maxQueuedMessages = positiveLimit(options.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
54
52
  }
55
53
  async start() {
56
54
  if (this.child) return;
@@ -69,13 +67,13 @@ var ClientTransport = class {
69
67
  const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
70
68
  try {
71
69
  const childArgs = this.opts.args ?? [];
72
- const shim = process.platform === "win32" ? buildWin32CmdShimInvocation(this.opts.command, childArgs) : null;
73
- this.child = spawn2(shim?.command ?? this.opts.command, shim?.args ?? childArgs, {
70
+ const invocation = spawnInvocation(this.opts.command, childArgs, process.platform);
71
+ this.child = spawn2(invocation.command, invocation.args, {
74
72
  env: { ...buildChildEnv2(), ...this.opts.env },
75
73
  cwd: spawnCwd,
76
74
  stdio: ["pipe", "pipe", "pipe"],
77
75
  windowsHide: true,
78
- ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
76
+ ...verbatimOptions(invocation)
79
77
  });
80
78
  } catch (err) {
81
79
  clearTimeout(timeout);
@@ -119,6 +117,9 @@ var ClientTransport = class {
119
117
  };
120
118
  const waitForMarker = (chunk) => {
121
119
  this.buffer += chunk;
120
+ if (this.buffer.length > this.maxFrameChars) {
121
+ this.buffer = this.buffer.slice(-this.maxFrameChars);
122
+ }
122
123
  const idx = this.buffer.indexOf("[wstack-acp]\n");
123
124
  if (idx !== -1) {
124
125
  this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
@@ -140,7 +141,8 @@ var ClientTransport = class {
140
141
  });
141
142
  }
142
143
  read() {
143
- if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
144
+ if (this.messageQueue.length > 0)
145
+ return Promise.resolve(expectDefined(this.messageQueue.shift()));
144
146
  if (this.closed) return Promise.resolve(null);
145
147
  return new Promise((resolve3) => {
146
148
  this.resolveRead = resolve3;
@@ -159,10 +161,7 @@ var ClientTransport = class {
159
161
  this.handlers.clear();
160
162
  const child = this.child;
161
163
  if (!child) return;
162
- try {
163
- child.kill();
164
- } catch {
165
- }
164
+ treeKill(child);
166
165
  this.child = null;
167
166
  }
168
167
  onChildData(chunk) {
@@ -226,15 +225,63 @@ var ClientTransport = class {
226
225
  }
227
226
  }
228
227
  };
228
+ function spawnInvocation(command, args, platform) {
229
+ if (platform !== "win32") return { command, args };
230
+ return buildWin32CmdShimInvocation(command, args);
231
+ }
232
+ function verbatimOptions(invocation) {
233
+ return invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: invocation.windowsVerbatimArguments } : {};
234
+ }
229
235
 
230
236
  // src/types/acp-v1.ts
231
237
  var ACP_PROTOCOL_VERSION = 1;
232
238
 
239
+ // src/client/acp-session-content.ts
240
+ function textContent(text) {
241
+ return { type: "text", text };
242
+ }
243
+ function imageContent(mimeType, data) {
244
+ return { type: "image", mimeType, data };
245
+ }
246
+ function audioContent(mimeType, data) {
247
+ return { type: "audio", mimeType, data };
248
+ }
249
+ function extractText(block) {
250
+ if (typeof block !== "object" || block === null) return "";
251
+ const b = block;
252
+ if (b.type === "text" && typeof b.text === "string") return b.text;
253
+ if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
254
+ return b.resource.text;
255
+ }
256
+ return "";
257
+ }
258
+ function isRecord(v) {
259
+ return typeof v === "object" && v !== null && !Array.isArray(v);
260
+ }
261
+ function emptyRunResult(stopReason) {
262
+ return {
263
+ text: "",
264
+ stopReason,
265
+ hasText: false,
266
+ toolCalls: [],
267
+ diffs: [],
268
+ thoughts: ""
269
+ };
270
+ }
271
+
233
272
  // src/client/file-server.ts
234
273
  import { randomBytes } from "node:crypto";
235
274
  import { realpathSync } from "node:fs";
236
275
  import * as fsp from "node:fs/promises";
237
276
  import * as path from "node:path";
277
+ var DEFAULT_FILE_OPERATIONS = {
278
+ stat: fsp.stat,
279
+ readFile: fsp.readFile,
280
+ writeFile: fsp.writeFile,
281
+ realpath: fsp.realpath,
282
+ rename: fsp.rename,
283
+ unlink: fsp.unlink
284
+ };
238
285
  var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
239
286
  var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
240
287
  var FsError = class extends Error {
@@ -253,12 +300,14 @@ var FileServer = class {
253
300
  timeoutMs;
254
301
  maxReadBytes;
255
302
  maxWriteBytes;
303
+ operations;
256
304
  constructor(opts) {
257
305
  this.root = path.resolve(opts.projectRoot);
258
306
  this.realRoot = safeRealpathSync(this.root);
259
307
  this.timeoutMs = opts.timeoutMs ?? 3e4;
260
308
  this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
261
309
  this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
310
+ this.operations = opts.operations ?? DEFAULT_FILE_OPERATIONS;
262
311
  }
263
312
  /** Read a text file. Returns the content as a string. */
264
313
  async readTextFile(params) {
@@ -266,7 +315,7 @@ var FileServer = class {
266
315
  const controller = new AbortController();
267
316
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
268
317
  try {
269
- const stat2 = await fsp.stat(safe).catch((err) => {
318
+ const stat2 = await this.operations.stat(safe).catch((err) => {
270
319
  throw mapFsError(err, safe);
271
320
  });
272
321
  if (stat2.size > this.maxReadBytes) {
@@ -276,7 +325,7 @@ var FileServer = class {
276
325
  `file is ${stat2.size} bytes, max read is ${this.maxReadBytes} bytes`
277
326
  );
278
327
  }
279
- const content = await fsp.readFile(safe, {
328
+ const content = await this.operations.readFile(safe, {
280
329
  encoding: "utf8",
281
330
  signal: controller.signal
282
331
  });
@@ -306,20 +355,20 @@ var FileServer = class {
306
355
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
307
356
  const tmp = `${safe}.${randomBytes(6).toString("hex")}.tmp`;
308
357
  try {
309
- await fsp.writeFile(tmp, params.content, {
358
+ await this.operations.writeFile(tmp, params.content, {
310
359
  encoding: "utf8",
311
360
  signal: controller.signal
312
361
  });
313
362
  await this.assertRealInside(tmp);
314
363
  await this.assertRealInside(path.dirname(safe));
315
- await fsp.rename(tmp, safe);
364
+ await this.operations.rename(tmp, safe);
316
365
  } catch (err) {
317
366
  if (err instanceof FsError) {
318
- await fsp.unlink(tmp).catch(() => void 0);
367
+ await this.operations.unlink(tmp).catch(() => void 0);
319
368
  throw err;
320
369
  }
321
370
  try {
322
- await fsp.unlink(tmp);
371
+ await this.operations.unlink(tmp);
323
372
  } catch {
324
373
  }
325
374
  if (controller.signal.aborted) {
@@ -363,12 +412,14 @@ var FileServer = class {
363
412
  for (; ; ) {
364
413
  let real;
365
414
  try {
366
- real = await fsp.realpath(probe);
415
+ real = await this.operations.realpath(probe);
367
416
  } catch (err) {
368
417
  const code = err.code;
369
418
  if (code === "ENOENT") {
370
419
  const parent = path.dirname(probe);
371
- if (parent === probe) return;
420
+ if (parent === probe) {
421
+ throw new FsError("ENOENT", resolvedPath, `no existing ancestor: ${resolvedPath}`);
422
+ }
372
423
  probe = parent;
373
424
  continue;
374
425
  }
@@ -466,7 +517,6 @@ var TerminalServer = class {
466
517
  this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
467
518
  this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
468
519
  this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
469
- if (this.maxTerminals < 1) throw new RangeError("maxTerminals must be at least 1");
470
520
  this.abortSignal = opts.signal;
471
521
  if (opts.signal) {
472
522
  opts.signal.addEventListener("abort", this.abortHandler, { once: true });
@@ -797,7 +847,6 @@ var WebSocketClientTransport = class {
797
847
  const ws = new WS(this.opts.url, this.opts.protocols);
798
848
  this.ws = ws;
799
849
  const timer = setTimeout(() => {
800
- if (settled) return;
801
850
  settled = true;
802
851
  try {
803
852
  ws.close();
@@ -835,7 +884,7 @@ var WebSocketClientTransport = class {
835
884
  }
836
885
  try {
837
886
  const serialized = JSON.stringify(msg);
838
- const buffered = Number.isFinite(this.ws.bufferedAmount) ? this.ws.bufferedAmount ?? 0 : 0;
887
+ const buffered = Number.isFinite(this.ws.bufferedAmount) ? this.ws.bufferedAmount : 0;
839
888
  if (buffered + Buffer.byteLength(serialized, "utf8") > this.maxBufferedBytes) {
840
889
  this.stop();
841
890
  return Promise.reject(new Error("WebSocket transport send buffer limit exceeded"));
@@ -892,10 +941,10 @@ var WebSocketClientTransport = class {
892
941
  }
893
942
  };
894
943
  function finitePositiveLimit(value, fallback) {
895
- return Number.isFinite(value) && (value ?? 0) > 0 ? Math.floor(value) : fallback;
944
+ return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
896
945
  }
897
946
 
898
- // src/client/acp-session.ts
947
+ // src/client/acp-session-errors.ts
899
948
  var ACPSessionError = class extends Error {
900
949
  kind;
901
950
  cause;
@@ -909,6 +958,264 @@ var ACPSessionError = class extends Error {
909
958
  function isJsonRpcError(v) {
910
959
  return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
911
960
  }
961
+
962
+ // src/client/acp-session-updates.ts
963
+ function createSessionScratch() {
964
+ return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
965
+ }
966
+ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
967
+ const update = msg.params?.update;
968
+ if (typeof update !== "object" || update === null) return;
969
+ const u = update;
970
+ emitProgress({ type: "raw", update: u });
971
+ switch (u.sessionUpdate) {
972
+ case "agent_message_chunk": {
973
+ const text = extractText(u.content);
974
+ if (text) {
975
+ scratch.text += text;
976
+ emitProgress({ type: "message", text });
977
+ }
978
+ return;
979
+ }
980
+ case "thought_chunk": {
981
+ const text = extractText(u.content);
982
+ if (text) {
983
+ scratch.thoughts += text;
984
+ emitProgress({ type: "thought", text });
985
+ }
986
+ return;
987
+ }
988
+ case "tool_call":
989
+ case "tool_call_update":
990
+ captureToolCall(u, u.sessionUpdate === "tool_call", scratch, emitProgress);
991
+ return;
992
+ case "plan":
993
+ if (Array.isArray(u.entries)) {
994
+ scratch.plan = u.entries;
995
+ emitProgress({ type: "plan", entries: u.entries });
996
+ }
997
+ return;
998
+ case "usage_update":
999
+ if (typeof u.used === "number" && typeof u.size === "number") {
1000
+ const usage = {
1001
+ used: u.used,
1002
+ size: u.size,
1003
+ ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
1004
+ };
1005
+ scratch.usage = usage;
1006
+ emitProgress({ type: "usage", usage });
1007
+ }
1008
+ return;
1009
+ case "available_commands_update":
1010
+ case "current_mode_update":
1011
+ case "config_option_update":
1012
+ case "session_info_update":
1013
+ case "user_message_chunk":
1014
+ case "next_edit_suggestions":
1015
+ case "elicitation":
1016
+ return;
1017
+ default:
1018
+ return;
1019
+ }
1020
+ }
1021
+ function captureToolCall(u, isNew, scratch, emitProgress) {
1022
+ const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
1023
+ if (!toolCallId) return;
1024
+ const prev = scratch.toolCalls.get(toolCallId);
1025
+ const record = {
1026
+ toolCallId,
1027
+ title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
1028
+ kind: typeof u.kind === "string" ? u.kind : prev?.kind,
1029
+ status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
1030
+ rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
1031
+ rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
1032
+ };
1033
+ scratch.toolCalls.set(toolCallId, record);
1034
+ if (Array.isArray(u.content)) {
1035
+ for (const c of u.content) {
1036
+ if (c && typeof c === "object" && c.type === "diff") {
1037
+ const diff = {
1038
+ path: c.path,
1039
+ oldText: c.oldText,
1040
+ newText: c.newText
1041
+ };
1042
+ scratch.diffs.push(diff);
1043
+ emitProgress({ type: "diff", diff });
1044
+ }
1045
+ }
1046
+ }
1047
+ emitProgress({
1048
+ type: isNew ? "tool_call" : "tool_call_update",
1049
+ toolCall: record
1050
+ });
1051
+ }
1052
+
1053
+ // src/client/acp-session-callbacks.ts
1054
+ async function handleAcpPermissionRequest(msg, permissionPolicy, sender) {
1055
+ const id = msg.id;
1056
+ if (id === void 0) return;
1057
+ const params = msg.params;
1058
+ const toolCall = params?.toolCall;
1059
+ const options = Array.isArray(params?.options) ? params.options : [];
1060
+ if (!toolCall) {
1061
+ await sender.sendErrorResponse(id, -32602, "toolCall is required");
1062
+ return;
1063
+ }
1064
+ const policyAbort = new AbortController();
1065
+ try {
1066
+ const outcome = await permissionPolicy({
1067
+ toolCall,
1068
+ options,
1069
+ signal: policyAbort.signal
1070
+ });
1071
+ await sender.sendResult(id, { outcome });
1072
+ } catch (err) {
1073
+ const message = err instanceof Error ? err.message : String(err);
1074
+ await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
1075
+ }
1076
+ }
1077
+ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender) {
1078
+ const id = msg.id;
1079
+ if (id === void 0) return;
1080
+ const params = msg.params;
1081
+ if (!params?.path) {
1082
+ await sender.sendErrorResponse(id, -32602, "path is required");
1083
+ return;
1084
+ }
1085
+ if (msg.method === "fs/write_text_file") {
1086
+ const allowed = await authorizeAcpCallback(permissionPolicy, {
1087
+ toolCallId: `acp-fs-write-${id}`,
1088
+ title: `Write file: ${params.path}`,
1089
+ kind: "edit",
1090
+ rawInput: { path: params.path, sessionId: params.sessionId }
1091
+ });
1092
+ if (!allowed) {
1093
+ await sender.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
1094
+ return;
1095
+ }
1096
+ }
1097
+ try {
1098
+ if (msg.method === "fs/read_text_file") {
1099
+ const result = await fileServer.readTextFile({
1100
+ sessionId: params.sessionId ?? "",
1101
+ path: params.path
1102
+ });
1103
+ await sender.sendResult(id, result);
1104
+ } else {
1105
+ await fileServer.writeTextFile({
1106
+ sessionId: params.sessionId ?? "",
1107
+ path: params.path,
1108
+ content: params.content ?? ""
1109
+ });
1110
+ await sender.sendResult(id, {});
1111
+ }
1112
+ } catch (err) {
1113
+ const code = err instanceof FsError ? -32602 : -32603;
1114
+ const message = err instanceof Error ? err.message : String(err);
1115
+ await sender.sendErrorResponse(id, code, message);
1116
+ }
1117
+ }
1118
+ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender) {
1119
+ const id = msg.id;
1120
+ if (id === void 0) return;
1121
+ const params = msg.params ?? {};
1122
+ try {
1123
+ switch (msg.method) {
1124
+ case "terminal/create": {
1125
+ const allowed = await authorizeAcpCallback(permissionPolicy, {
1126
+ toolCallId: `acp-terminal-create-${id}`,
1127
+ title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
1128
+ kind: "execute",
1129
+ rawInput: {
1130
+ command: params.command,
1131
+ args: params.args,
1132
+ cwd: params.cwd,
1133
+ sessionId: params.sessionId
1134
+ }
1135
+ });
1136
+ if (!allowed) {
1137
+ await sender.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
1138
+ return;
1139
+ }
1140
+ const createOpts = {
1141
+ sessionId: String(params.sessionId ?? ""),
1142
+ command: String(params.command ?? ""),
1143
+ args: Array.isArray(params.args) ? params.args : []
1144
+ };
1145
+ if (Array.isArray(params.env)) {
1146
+ createOpts.env = params.env;
1147
+ }
1148
+ if (typeof params.cwd === "string") {
1149
+ createOpts.cwd = params.cwd;
1150
+ }
1151
+ if (typeof params.outputByteLimit === "number") {
1152
+ createOpts.outputByteLimit = params.outputByteLimit;
1153
+ }
1154
+ const result = terminalServer.create(createOpts);
1155
+ await sender.sendResult(id, result);
1156
+ return;
1157
+ }
1158
+ case "terminal/output": {
1159
+ const terminalId = String(params.terminalId ?? "");
1160
+ const out = terminalServer.output(terminalId);
1161
+ await sender.sendResult(id, out);
1162
+ return;
1163
+ }
1164
+ case "terminal/wait_for_exit": {
1165
+ const terminalId = String(params.terminalId ?? "");
1166
+ const exit = await terminalServer.waitForExit(terminalId);
1167
+ await sender.sendResult(id, exit);
1168
+ return;
1169
+ }
1170
+ case "terminal/kill": {
1171
+ const terminalId = String(params.terminalId ?? "");
1172
+ terminalServer.kill(terminalId);
1173
+ await sender.sendResult(id, {});
1174
+ return;
1175
+ }
1176
+ case "terminal/release": {
1177
+ const terminalId = String(params.terminalId ?? "");
1178
+ terminalServer.release(terminalId);
1179
+ await sender.sendResult(id, {});
1180
+ return;
1181
+ }
1182
+ default:
1183
+ await sender.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
1184
+ }
1185
+ } catch (err) {
1186
+ const message = err instanceof Error ? err.message : String(err);
1187
+ await sender.sendErrorResponse(id, -32603, message);
1188
+ }
1189
+ }
1190
+ async function authorizeAcpCallback(permissionPolicy, partial) {
1191
+ try {
1192
+ const outcome = await permissionPolicy({
1193
+ toolCall: {
1194
+ sessionUpdate: "tool_call_update",
1195
+ toolCallId: partial.toolCallId,
1196
+ title: partial.title,
1197
+ kind: partial.kind,
1198
+ status: "pending",
1199
+ ...partial.rawInput ? { rawInput: partial.rawInput } : {}
1200
+ },
1201
+ options: [
1202
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
1203
+ { optionId: "reject", name: "Reject", kind: "reject_once" }
1204
+ ],
1205
+ signal: new AbortController().signal
1206
+ });
1207
+ return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
1208
+ } catch {
1209
+ return false;
1210
+ }
1211
+ }
1212
+
1213
+ // src/client/acp-message-routing.ts
1214
+ function isBestEffortAckMethod(method) {
1215
+ return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
1216
+ }
1217
+
1218
+ // src/client/acp-session.ts
912
1219
  var ACPSession = class _ACPSession {
913
1220
  transport;
914
1221
  fileServer;
@@ -1602,6 +1909,12 @@ var ACPSession = class _ACPSession {
1602
1909
  error: { code, message }
1603
1910
  });
1604
1911
  }
1912
+ responseSender() {
1913
+ return {
1914
+ sendResult: (id, result) => this.sendResult(id, result),
1915
+ sendErrorResponse: (id, code, message) => this.sendErrorResponse(id, code, message)
1916
+ };
1917
+ }
1605
1918
  handleMessage(msg) {
1606
1919
  if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
1607
1920
  const pending = this.pending.get(msg.id);
@@ -1616,29 +1929,32 @@ var ACPSession = class _ACPSession {
1616
1929
  return;
1617
1930
  }
1618
1931
  if (msg.method === "session/update") {
1619
- this.handleUpdate(msg);
1932
+ handleAcpSessionUpdate(msg, this.scratch, (event) => this.emitProgress(event));
1620
1933
  return;
1621
1934
  }
1622
1935
  if (msg.method === "session/request_permission") {
1623
- void this.handlePermissionRequest(msg);
1936
+ void handleAcpPermissionRequest(msg, this.permissionPolicy, this.responseSender());
1624
1937
  return;
1625
1938
  }
1626
1939
  if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
1627
- void this.handleFsRequest(msg);
1940
+ void handleAcpFsRequest(
1941
+ msg,
1942
+ this.fileServer,
1943
+ this.permissionPolicy,
1944
+ this.responseSender()
1945
+ );
1628
1946
  return;
1629
1947
  }
1630
1948
  if (msg.method?.startsWith("terminal/")) {
1631
- void this.handleTerminalRequest(msg);
1632
- return;
1633
- }
1634
- if (msg.method === "mcp/connect" || msg.method === "mcp/message" || msg.method === "mcp/disconnect") {
1635
- if (msg.id !== void 0) {
1636
- this.sendResult(msg.id, {}).catch(() => {
1637
- });
1638
- }
1949
+ void handleAcpTerminalRequest(
1950
+ msg,
1951
+ this.terminalServer,
1952
+ this.permissionPolicy,
1953
+ this.responseSender()
1954
+ );
1639
1955
  return;
1640
1956
  }
1641
- if (msg.method === "elicitation/create" || msg.method === "elicitation/complete") {
1957
+ if (isBestEffortAckMethod(msg.method)) {
1642
1958
  if (msg.id !== void 0) {
1643
1959
  this.sendResult(msg.id, {}).catch(() => {
1644
1960
  });
@@ -1659,98 +1975,6 @@ var ACPSession = class _ACPSession {
1659
1975
  );
1660
1976
  }
1661
1977
  }
1662
- handleUpdate(msg) {
1663
- const update = msg.params?.update;
1664
- if (typeof update !== "object" || update === null) return;
1665
- const u = update;
1666
- this.emitProgress({ type: "raw", update: u });
1667
- switch (u.sessionUpdate) {
1668
- case "agent_message_chunk": {
1669
- const text = extractText(u.content);
1670
- if (text) {
1671
- this.scratch.text += text;
1672
- this.emitProgress({ type: "message", text });
1673
- }
1674
- return;
1675
- }
1676
- case "thought_chunk": {
1677
- const text = extractText(u.content);
1678
- if (text) {
1679
- this.scratch.thoughts += text;
1680
- this.emitProgress({ type: "thought", text });
1681
- }
1682
- return;
1683
- }
1684
- case "tool_call":
1685
- case "tool_call_update": {
1686
- this.captureToolCall(u, u.sessionUpdate === "tool_call");
1687
- return;
1688
- }
1689
- case "plan":
1690
- if (Array.isArray(u.entries)) {
1691
- this.scratch.plan = u.entries;
1692
- this.emitProgress({ type: "plan", entries: u.entries });
1693
- }
1694
- return;
1695
- case "usage_update":
1696
- if (typeof u.used === "number" && typeof u.size === "number") {
1697
- const usage = {
1698
- used: u.used,
1699
- size: u.size,
1700
- ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
1701
- };
1702
- this.scratch.usage = usage;
1703
- this.emitProgress({ type: "usage", usage });
1704
- }
1705
- return;
1706
- case "available_commands_update":
1707
- case "current_mode_update":
1708
- case "config_option_update":
1709
- case "session_info_update":
1710
- case "user_message_chunk":
1711
- case "next_edit_suggestions":
1712
- case "elicitation":
1713
- return;
1714
- default:
1715
- return;
1716
- }
1717
- }
1718
- /**
1719
- * Fold a `tool_call` / `tool_call_update` notification into the scratch
1720
- * tool-call map (deduped by toolCallId), extract any `diff` content into
1721
- * the diffs list, and emit live progress.
1722
- */
1723
- captureToolCall(u, isNew) {
1724
- const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
1725
- if (!toolCallId) return;
1726
- const prev = this.scratch.toolCalls.get(toolCallId);
1727
- const record = {
1728
- toolCallId,
1729
- title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
1730
- kind: typeof u.kind === "string" ? u.kind : prev?.kind,
1731
- status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
1732
- rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
1733
- rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
1734
- };
1735
- this.scratch.toolCalls.set(toolCallId, record);
1736
- if (Array.isArray(u.content)) {
1737
- for (const c of u.content) {
1738
- if (c && typeof c === "object" && c.type === "diff") {
1739
- const diff = {
1740
- path: c.path,
1741
- oldText: c.oldText,
1742
- newText: c.newText
1743
- };
1744
- this.scratch.diffs.push(diff);
1745
- this.emitProgress({ type: "diff", diff });
1746
- }
1747
- }
1748
- }
1749
- this.emitProgress({
1750
- type: isNew ? "tool_call" : "tool_call_update",
1751
- toolCall: record
1752
- });
1753
- }
1754
1978
  emitProgress(event) {
1755
1979
  if (!this.progressHandler) return;
1756
1980
  try {
@@ -1761,217 +1985,11 @@ var ACPSession = class _ACPSession {
1761
1985
  /** Live progress handler installed for the duration of a `prompt()` turn. */
1762
1986
  progressHandler = null;
1763
1987
  // Per-prompt scratch state
1764
- scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
1988
+ scratch = createSessionScratch();
1765
1989
  resetScratch() {
1766
- this.scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
1767
- }
1768
- async handlePermissionRequest(msg) {
1769
- const id = msg.id;
1770
- if (id === void 0) return;
1771
- const params = msg.params;
1772
- const toolCall = params?.toolCall;
1773
- const options = Array.isArray(params?.options) ? params.options : [];
1774
- if (!toolCall) {
1775
- await this.sendErrorResponse(id, -32602, "toolCall is required");
1776
- return;
1777
- }
1778
- const policyAbort = new AbortController();
1779
- try {
1780
- const outcome = await this.permissionPolicy({
1781
- toolCall,
1782
- options,
1783
- signal: policyAbort.signal
1784
- });
1785
- await this.sendResult(id, { outcome });
1786
- } catch (err) {
1787
- const message = err instanceof Error ? err.message : String(err);
1788
- await this.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
1789
- }
1790
- }
1791
- /**
1792
- * Enforce authorization at privileged callback sinks (fs/write,
1793
- * terminal/create). Unlike `handlePermissionRequest` which responds to
1794
- * agent-initiated `session/request_permission` messages, this method is
1795
- * called by the handler BEFORE dispatching to FileServer/TerminalServer,
1796
- * closing the gap where the agent simply skips the voluntary permission
1797
- * request and sends the privileged callback directly.
1798
- *
1799
- * Uses the session's permission policy. The default
1800
- * (`readOnlyPermissionPolicy`) auto-approves only side-effect-free tool
1801
- * calls (read/search/fetch/think) and rejects everything else — this is
1802
- * the safe-by-default posture. For trusted local agents (CLI `acp spawn`,
1803
- * Director fan-out), inject `defaultPermissionPolicy` to grant
1804
- * write/execute access.
1805
- *
1806
- * Returns true if the callback is authorized, false if denied.
1807
- */
1808
- async authorizeCallback(partial) {
1809
- try {
1810
- const outcome = await this.permissionPolicy({
1811
- toolCall: {
1812
- sessionUpdate: "tool_call_update",
1813
- toolCallId: partial.toolCallId,
1814
- title: partial.title,
1815
- kind: partial.kind,
1816
- status: "pending",
1817
- ...partial.rawInput ? { rawInput: partial.rawInput } : {}
1818
- },
1819
- options: [
1820
- { optionId: "allow", name: "Allow", kind: "allow_once" },
1821
- { optionId: "reject", name: "Reject", kind: "reject_once" }
1822
- ],
1823
- signal: new AbortController().signal
1824
- });
1825
- return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
1826
- } catch {
1827
- return false;
1828
- }
1829
- }
1830
- async handleFsRequest(msg) {
1831
- const id = msg.id;
1832
- if (id === void 0) return;
1833
- const params = msg.params;
1834
- if (!params?.path) {
1835
- await this.sendErrorResponse(id, -32602, "path is required");
1836
- return;
1837
- }
1838
- if (msg.method === "fs/write_text_file") {
1839
- const allowed = await this.authorizeCallback({
1840
- toolCallId: `acp-fs-write-${id}`,
1841
- title: `Write file: ${params.path}`,
1842
- kind: "edit",
1843
- rawInput: { path: params.path, sessionId: params.sessionId }
1844
- });
1845
- if (!allowed) {
1846
- await this.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
1847
- return;
1848
- }
1849
- }
1850
- try {
1851
- if (msg.method === "fs/read_text_file") {
1852
- const result = await this.fileServer.readTextFile({
1853
- sessionId: params.sessionId ?? "",
1854
- path: params.path
1855
- });
1856
- await this.sendResult(id, result);
1857
- } else {
1858
- await this.fileServer.writeTextFile({
1859
- sessionId: params.sessionId ?? "",
1860
- path: params.path,
1861
- content: params.content ?? ""
1862
- });
1863
- await this.sendResult(id, {});
1864
- }
1865
- } catch (err) {
1866
- const code = err instanceof FsError ? -32602 : -32603;
1867
- const message = err instanceof Error ? err.message : String(err);
1868
- await this.sendErrorResponse(id, code, message);
1869
- }
1870
- }
1871
- async handleTerminalRequest(msg) {
1872
- const id = msg.id;
1873
- if (id === void 0) return;
1874
- const params = msg.params ?? {};
1875
- try {
1876
- switch (msg.method) {
1877
- case "terminal/create": {
1878
- const allowed = await this.authorizeCallback({
1879
- toolCallId: `acp-terminal-create-${id}`,
1880
- title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
1881
- kind: "execute",
1882
- rawInput: {
1883
- command: params.command,
1884
- args: params.args,
1885
- cwd: params.cwd,
1886
- sessionId: params.sessionId
1887
- }
1888
- });
1889
- if (!allowed) {
1890
- await this.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
1891
- return;
1892
- }
1893
- const createOpts = {
1894
- sessionId: String(params.sessionId ?? ""),
1895
- command: String(params.command ?? ""),
1896
- args: Array.isArray(params.args) ? params.args : []
1897
- };
1898
- if (Array.isArray(params.env)) {
1899
- createOpts.env = params.env;
1900
- }
1901
- if (typeof params.cwd === "string") {
1902
- createOpts.cwd = params.cwd;
1903
- }
1904
- if (typeof params.outputByteLimit === "number") {
1905
- createOpts.outputByteLimit = params.outputByteLimit;
1906
- }
1907
- const result = this.terminalServer.create(createOpts);
1908
- await this.sendResult(id, result);
1909
- return;
1910
- }
1911
- case "terminal/output": {
1912
- const terminalId = String(params.terminalId ?? "");
1913
- const out = this.terminalServer.output(terminalId);
1914
- await this.sendResult(id, out);
1915
- return;
1916
- }
1917
- case "terminal/wait_for_exit": {
1918
- const terminalId = String(params.terminalId ?? "");
1919
- const exit = await this.terminalServer.waitForExit(terminalId);
1920
- await this.sendResult(id, exit);
1921
- return;
1922
- }
1923
- case "terminal/kill": {
1924
- const terminalId = String(params.terminalId ?? "");
1925
- this.terminalServer.kill(terminalId);
1926
- await this.sendResult(id, {});
1927
- return;
1928
- }
1929
- case "terminal/release": {
1930
- const terminalId = String(params.terminalId ?? "");
1931
- this.terminalServer.release(terminalId);
1932
- await this.sendResult(id, {});
1933
- return;
1934
- }
1935
- default:
1936
- await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
1937
- }
1938
- } catch (err) {
1939
- const message = err instanceof Error ? err.message : String(err);
1940
- await this.sendErrorResponse(id, -32603, message);
1941
- }
1990
+ this.scratch = createSessionScratch();
1942
1991
  }
1943
1992
  };
1944
- function textContent(text) {
1945
- return { type: "text", text };
1946
- }
1947
- function imageContent(mimeType, data) {
1948
- return { type: "image", mimeType, data };
1949
- }
1950
- function audioContent(mimeType, data) {
1951
- return { type: "audio", mimeType, data };
1952
- }
1953
- function extractText(block) {
1954
- if (typeof block !== "object" || block === null) return "";
1955
- const b = block;
1956
- if (b.type === "text" && typeof b.text === "string") return b.text;
1957
- if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
1958
- return b.resource.text;
1959
- }
1960
- return "";
1961
- }
1962
- function isRecord(v) {
1963
- return typeof v === "object" && v !== null && !Array.isArray(v);
1964
- }
1965
- function emptyRunResult(stopReason) {
1966
- return {
1967
- text: "",
1968
- stopReason,
1969
- hasText: false,
1970
- toolCalls: [],
1971
- diffs: [],
1972
- thoughts: ""
1973
- };
1974
- }
1975
1993
 
1976
1994
  // src/integration/acp-subagent-runner.ts
1977
1995
  async function makeACPSubagentRunner(options) {
@@ -2100,16 +2118,8 @@ function mapACPKind(acpKind) {
2100
2118
  }
2101
2119
  }
2102
2120
  function isRetryable(kind) {
2103
- switch (kind) {
2104
- case "provider_5xx":
2105
- case "provider_rate_limit":
2106
- case "provider_timeout":
2107
- case "tool_threw":
2108
- case "budget_timeout":
2109
- return true;
2110
- default:
2111
- return false;
2112
- }
2121
+ void kind;
2122
+ return false;
2113
2123
  }
2114
2124
 
2115
2125
  // src/client/tool-translator.ts