@agentclientprotocol/claude-agent-acp 0.70.0 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +10 -9
  2. package/dist/acp-agent.d.ts +136 -49
  3. package/dist/acp-agent.d.ts.map +1 -1
  4. package/dist/acp-agent.js +1304 -724
  5. package/dist/acp-subagents.d.ts +82 -0
  6. package/dist/acp-subagents.d.ts.map +1 -0
  7. package/dist/acp-subagents.js +13 -0
  8. package/dist/air-extension.d.ts +23 -0
  9. package/dist/air-extension.d.ts.map +1 -0
  10. package/dist/air-extension.js +63 -0
  11. package/dist/async-tasks.d.ts +109 -0
  12. package/dist/async-tasks.d.ts.map +1 -0
  13. package/dist/async-tasks.js +613 -0
  14. package/dist/clear-context-coordinator.d.ts +60 -0
  15. package/dist/clear-context-coordinator.d.ts.map +1 -0
  16. package/dist/clear-context-coordinator.js +80 -0
  17. package/dist/exit-plan.d.ts +42 -0
  18. package/dist/exit-plan.d.ts.map +1 -0
  19. package/dist/exit-plan.js +154 -0
  20. package/dist/file-change-audit.d.ts.map +1 -1
  21. package/dist/file-change-audit.js +4 -33
  22. package/dist/fork-session.d.ts +12 -0
  23. package/dist/fork-session.d.ts.map +1 -0
  24. package/dist/fork-session.js +41 -0
  25. package/dist/native-subagents.d.ts +71 -0
  26. package/dist/native-subagents.d.ts.map +1 -0
  27. package/dist/native-subagents.js +422 -0
  28. package/dist/permissions/effects.d.ts +19 -0
  29. package/dist/permissions/effects.d.ts.map +1 -0
  30. package/dist/permissions/effects.js +166 -0
  31. package/dist/permissions/modes.d.ts +8 -0
  32. package/dist/permissions/modes.d.ts.map +1 -0
  33. package/dist/permissions/modes.js +41 -0
  34. package/dist/permissions/normalization.d.ts +6 -0
  35. package/dist/permissions/normalization.d.ts.map +1 -0
  36. package/dist/permissions/normalization.js +100 -0
  37. package/dist/permissions/options/filesystem.d.ts +9 -0
  38. package/dist/permissions/options/filesystem.d.ts.map +1 -0
  39. package/dist/permissions/options/filesystem.js +124 -0
  40. package/dist/permissions/options/shared.d.ts +39 -0
  41. package/dist/permissions/options/shared.d.ts.map +1 -0
  42. package/dist/permissions/options/shared.js +64 -0
  43. package/dist/permissions/options/shell.d.ts +5 -0
  44. package/dist/permissions/options/shell.d.ts.map +1 -0
  45. package/dist/permissions/options/shell.js +100 -0
  46. package/dist/permissions/options/tools.d.ts +11 -0
  47. package/dist/permissions/options/tools.d.ts.map +1 -0
  48. package/dist/permissions/options/tools.js +135 -0
  49. package/dist/permissions/options.d.ts +5 -0
  50. package/dist/permissions/options.d.ts.map +1 -0
  51. package/dist/permissions/options.js +60 -0
  52. package/dist/permissions/presentation.d.ts +15 -0
  53. package/dist/permissions/presentation.d.ts.map +1 -0
  54. package/dist/permissions/presentation.js +83 -0
  55. package/dist/permissions/response.d.ts +11 -0
  56. package/dist/permissions/response.d.ts.map +1 -0
  57. package/dist/permissions/response.js +19 -0
  58. package/dist/session-config-ids.d.ts +5 -0
  59. package/dist/session-config-ids.d.ts.map +1 -0
  60. package/dist/session-config-ids.js +4 -0
  61. package/dist/session-failure-extension.d.ts +2 -24
  62. package/dist/session-failure-extension.d.ts.map +1 -1
  63. package/dist/session-failure-extension.js +13 -40
  64. package/dist/session-mode.d.ts +66 -0
  65. package/dist/session-mode.d.ts.map +1 -0
  66. package/dist/session-mode.js +234 -0
  67. package/dist/session-titles.d.ts +77 -0
  68. package/dist/session-titles.d.ts.map +1 -0
  69. package/dist/session-titles.js +199 -0
  70. package/dist/tool-result-meta.d.ts +8 -0
  71. package/dist/tool-result-meta.d.ts.map +1 -0
  72. package/dist/tool-result-meta.js +19 -0
  73. package/dist/tools.d.ts +6 -1
  74. package/dist/tools.d.ts.map +1 -1
  75. package/dist/tools.js +144 -20
  76. package/package.json +12 -12
package/dist/tools.js CHANGED
@@ -211,15 +211,8 @@ export function toolInfoFromToolUse(toolUse, supportsTerminalOutput = false, cwd
211
211
  }
212
212
  case "WebSearch": {
213
213
  const input = toolUse.input;
214
- let label = input?.query ? `"${input.query}"` : "Web search";
215
- if (input?.allowed_domains && input.allowed_domains.length > 0) {
216
- label += ` (allowed: ${input.allowed_domains.join(", ")})`;
217
- }
218
- if (input?.blocked_domains && input.blocked_domains.length > 0) {
219
- label += ` (blocked: ${input.blocked_domains.join(", ")})`;
220
- }
221
214
  return {
222
- title: label,
215
+ title: input?.query ? `Search "${input.query}"` : "Web search",
223
216
  kind: "fetch",
224
217
  content: [],
225
218
  };
@@ -284,7 +277,7 @@ export function toolInfoFromToolUse(toolUse, supportsTerminalOutput = false, cwd
284
277
  case "ExitPlanMode": {
285
278
  const planInput = toolUse.input;
286
279
  return {
287
- title: "Ready to code?",
280
+ title: "Approve Plan",
288
281
  kind: "switch_mode",
289
282
  content: planInput?.plan
290
283
  ? [{ type: "content", content: { type: "text", text: planInput.plan } }]
@@ -420,6 +413,45 @@ function stripAgentTrailerFromContent(content) {
420
413
  }
421
414
  return content;
422
415
  }
416
+ /** Leading model-directed note the CLI prepends to a subagent's report when
417
+ * the agent stopped at its maxTurns limit (CLI 2.1.246+); the result still
418
+ * ships as `status: "completed"`. Two body variants follow this prefix, and
419
+ * the trailing "Send the agent a message (SendMessage) …" sentence is
420
+ * omitted for some agent types — anchor only the stable prefix so a format
421
+ * change makes the replacement stop matching rather than mangle a report. */
422
+ const PARTIAL_OUTPUT_NOTE = /^NOTE: this agent stopped at its \d+-turn limit before finishing\./;
423
+ /** Client-facing replacement: the partial-output fact matters to the user,
424
+ * but the SendMessage continuation instruction is model-directed and
425
+ * meaningless over ACP. */
426
+ const PARTIAL_OUTPUT_LABEL = "[Agent stopped at its turn limit — the output below is partial]";
427
+ /** Replace a leading partial-output note paragraph with the concise
428
+ * client-facing label, leaving the report that follows intact. */
429
+ function replacePartialNoteInText(text) {
430
+ if (!PARTIAL_OUTPUT_NOTE.test(text))
431
+ return text;
432
+ const paragraphEnd = text.indexOf("\n\n");
433
+ const report = paragraphEnd === -1 ? "" : text.slice(paragraphEnd + 2).trimStart();
434
+ return report ? `${PARTIAL_OUTPUT_LABEL}\n\n${report}` : PARTIAL_OUTPUT_LABEL;
435
+ }
436
+ /** Apply {@link replacePartialNoteInText} to an Agent/Task result `content`.
437
+ * In the structured AgentOutput lane the note is its own leading text block;
438
+ * in the raw lane it is the first paragraph of the text — both reduce to
439
+ * transforming the first text block (or the plain string). */
440
+ function replacePartialOutputNote(content) {
441
+ if (typeof content === "string") {
442
+ return replacePartialNoteInText(content);
443
+ }
444
+ if (Array.isArray(content) && content.length > 0) {
445
+ const [first, ...rest] = content;
446
+ if (first !== null &&
447
+ typeof first === "object" &&
448
+ first.type === "text" &&
449
+ typeof first.text === "string") {
450
+ return [{ ...first, text: replacePartialNoteInText(first.text) }, ...rest];
451
+ }
452
+ }
453
+ return content;
454
+ }
423
455
  export function toolUpdateFromToolResult(toolResult, toolUse, supportsTerminalOutput = false, toolUseResult) {
424
456
  if ("is_error" in toolResult &&
425
457
  toolResult.is_error &&
@@ -656,7 +688,11 @@ export function toolUpdateFromToolResult(toolResult, toolUse, supportsTerminalOu
656
688
  // A completed subagent can end with zero text blocks; an empty
657
689
  // structured render would beat the raw fallback for no benefit.
658
690
  structured.content.length > 0) {
659
- return toAcpContentUpdate(structured.content, "is_error" in toolResult ? toolResult.is_error : false);
691
+ return toAcpContentUpdate(
692
+ // A maxTurns-stopped subagent still completes, with a model-directed
693
+ // partial-output note prepended to its report — swap it for a
694
+ // client-facing label (see PARTIAL_OUTPUT_NOTE).
695
+ replacePartialOutputNote(structured.content), "is_error" in toolResult ? toolResult.is_error : false);
660
696
  }
661
697
  // No structured report to render from (replayed sessions —
662
698
  // getSessionMessages doesn't expose the transcript's toolUseResult —
@@ -665,7 +701,10 @@ export function toolUpdateFromToolResult(toolResult, toolUse, supportsTerminalOu
665
701
  // tail-anchored strip is the only cleanup available; if the trailer
666
702
  // format changes it simply stops matching and the full raw text
667
703
  // renders, no worse than before.
668
- return toAcpContentUpdate(stripAgentTrailerFromContent(toolResult.content), "is_error" in toolResult ? toolResult.is_error : false);
704
+ return toAcpContentUpdate(
705
+ // Head and tail cleanups are independent: the partial-output note
706
+ // leads the raw text the same way it leads the structured content.
707
+ replacePartialOutputNote(stripAgentTrailerFromContent(toolResult.content)), "is_error" in toolResult ? toolResult.is_error : false);
669
708
  }
670
709
  case "Skill": {
671
710
  return {};
@@ -718,6 +757,15 @@ export function toolUpdateFromToolResult(toolResult, toolUse, supportsTerminalOu
718
757
  function formatWebSearchHit(hit) {
719
758
  return `${hit.title} (${hit.url})`;
720
759
  }
760
+ /** Human-readable size for the document placeholder ("312 B", "2.4 KB",
761
+ * "1.3 MB"). */
762
+ function formatByteSize(bytes) {
763
+ if (bytes < 1024)
764
+ return `${bytes} B`;
765
+ if (bytes < 1024 * 1024)
766
+ return `${(bytes / 1024).toFixed(1)} KB`;
767
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
768
+ }
721
769
  function toAcpContentUpdate(content, isError = false) {
722
770
  if (Array.isArray(content) && content.length > 0) {
723
771
  return {
@@ -775,6 +823,28 @@ function toAcpContentBlock(content, isError) {
775
823
  return wrapText(content.source.type === "url"
776
824
  ? `[image: ${content.source.url}]`
777
825
  : "[image: file reference]");
826
+ case "document": {
827
+ // A PDF Read delivers its raw `document` block inside the tool_result
828
+ // content (SDK 0.3.243 moved it here from a separate follow-up user
829
+ // message; the MultiRead documents lane always lived here). ACP has no
830
+ // document block and the base64 payload can be megabytes — render a
831
+ // compact placeholder, never the data, matching the CLI's own compact
832
+ // "Read PDF (size)" rendering.
833
+ const title = typeof content.title === "string" && content.title.length > 0 ? ` "${content.title}"` : "";
834
+ const source = content.source;
835
+ switch (source.type) {
836
+ case "url":
837
+ return wrapText(`[document${title}: ${source.url}]`);
838
+ case "base64":
839
+ case "text":
840
+ // base64 inflates the byte count by 4/3; plain text is 1:1.
841
+ return wrapText(`[document${title}: ${source.media_type}, ${formatByteSize(source.type === "base64"
842
+ ? Math.floor((source.data.length * 3) / 4)
843
+ : source.data.length)}]`);
844
+ default:
845
+ return wrapText(`[document${title}]`);
846
+ }
847
+ }
778
848
  case "tool_reference":
779
849
  return wrapText(`Tool: ${content.tool_name}`);
780
850
  case "tool_search_tool_search_result":
@@ -1040,6 +1110,32 @@ export function toolUpdateFromDiffToolResponse(toolResponse) {
1040
1110
  });
1041
1111
  }
1042
1112
  }
1113
+ // A Write `update` can arrive with an empty structuredPatch — nothing
1114
+ // changed, the diff timed out, or the previous content was too large to
1115
+ // diff (originalFile null; SDK 0.3.252 documents the lane). Returning `{}`
1116
+ // would leave Write's optimistic tool_use-time content standing, and that
1117
+ // was built with `oldText: null` — "creation" semantics — so an overwrite
1118
+ // of a large existing file would render as creating it. Emit a truthful
1119
+ // replacement instead. Gated on `type` so Edit (whose output carries no
1120
+ // `type` and whose optimistic old/new diff is already truthful) keeps the
1121
+ // empty-return behavior.
1122
+ if (content.length === 0 && response.type === "update" && typeof response.content === "string") {
1123
+ locations.push({ path: response.filePath });
1124
+ content.push(typeof response.originalFile === "string"
1125
+ ? {
1126
+ type: "diff",
1127
+ path: response.filePath,
1128
+ oldText: response.originalFile,
1129
+ newText: response.content,
1130
+ }
1131
+ : {
1132
+ type: "content",
1133
+ content: {
1134
+ type: "text",
1135
+ text: `Updated \`${response.filePath}\` (previous content too large to diff)`,
1136
+ },
1137
+ });
1138
+ }
1043
1139
  const result = {};
1044
1140
  if (content.length > 0)
1045
1141
  result.content = content;
@@ -1047,14 +1143,38 @@ export function toolUpdateFromDiffToolResponse(toolResponse) {
1047
1143
  result.locations = locations;
1048
1144
  return result;
1049
1145
  }
1050
- /* A global variable to store callbacks that should be executed when receiving hooks from Claude Code */
1051
- const toolUseCallbacks = {};
1146
+ /* Callbacks are keyed globally because the SDK hook is process-wide, but each
1147
+ * entry retains its owning ACP session so cancellation/teardown can release it. */
1148
+ const toolUseCallbacks = new Map();
1052
1149
  /* Setup callbacks that will be called when receiving hooks from Claude Code */
1053
- export const registerHookCallback = (toolUseID, { onPostToolUseHook, }) => {
1054
- toolUseCallbacks[toolUseID] = {
1150
+ export const registerHookCallback = (toolUseID, { onPostToolUseHook, }, ownerId) => {
1151
+ unregisterHookCallback(toolUseID);
1152
+ toolUseCallbacks.set(toolUseID, {
1153
+ ownerId,
1055
1154
  onPostToolUseHook,
1056
- };
1155
+ });
1057
1156
  };
1157
+ export function unregisterHookCallback(toolUseID) {
1158
+ const callback = toolUseCallbacks.get(toolUseID);
1159
+ if (callback?.cleanupTimer)
1160
+ clearTimeout(callback.cleanupTimer);
1161
+ toolUseCallbacks.delete(toolUseID);
1162
+ }
1163
+ /** PostToolUse normally follows tool_result, so keep the callback for a short
1164
+ * grace period while still bounding retention when the hook never arrives. */
1165
+ export function completeHookCallback(toolUseID) {
1166
+ const callback = toolUseCallbacks.get(toolUseID);
1167
+ if (!callback || callback.cleanupTimer)
1168
+ return;
1169
+ callback.cleanupTimer = setTimeout(() => unregisterHookCallback(toolUseID), 30_000);
1170
+ callback.cleanupTimer.unref?.();
1171
+ }
1172
+ export function clearHookCallbacks(ownerId) {
1173
+ for (const [toolUseID, callback] of toolUseCallbacks) {
1174
+ if (callback.ownerId === ownerId)
1175
+ unregisterHookCallback(toolUseID);
1176
+ }
1177
+ }
1058
1178
  /* A callback for Claude Code that is called when receiving a PostToolUse hook */
1059
1179
  export const createPostToolUseHook = (options) => async (input, toolUseID) => {
1060
1180
  if (input.hook_event_name === "PostToolUse") {
@@ -1063,11 +1183,15 @@ export const createPostToolUseHook = (options) => async (input, toolUseID) => {
1063
1183
  await options.onEnterPlanMode();
1064
1184
  }
1065
1185
  if (toolUseID) {
1066
- const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook;
1067
- if (onPostToolUseHook) {
1068
- await onPostToolUseHook(toolUseID, input.tool_input, input.tool_response);
1186
+ const onPostToolUseHook = toolUseCallbacks.get(toolUseID)?.onPostToolUseHook;
1187
+ try {
1188
+ if (onPostToolUseHook) {
1189
+ await onPostToolUseHook(toolUseID, input.tool_input, input.tool_response);
1190
+ }
1191
+ }
1192
+ finally {
1193
+ unregisterHookCallback(toolUseID);
1069
1194
  }
1070
- delete toolUseCallbacks[toolUseID]; // Cleanup after execution
1071
1195
  }
1072
1196
  }
1073
1197
  return { continue: true };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.70.0",
6
+ "version": "0.72.0",
7
7
  "description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
8
8
  "main": "dist/lib.js",
9
9
  "types": "dist/lib.d.ts",
@@ -65,23 +65,23 @@
65
65
  "node": ">=22"
66
66
  },
67
67
  "dependencies": {
68
- "@agentclientprotocol/sdk": "1.3.0",
69
- "@anthropic-ai/claude-agent-sdk": "0.3.232",
70
- "zod": "^3.25.0 || ^4.0.0"
68
+ "@agentclientprotocol/sdk": "1.4.0",
69
+ "@anthropic-ai/claude-agent-sdk": "0.3.252",
70
+ "zod": "^4.0.0"
71
71
  },
72
72
  "devDependencies": {
73
- "@anthropic-ai/sdk": "0.117.1",
73
+ "@anthropic-ai/sdk": "0.122.0",
74
74
  "@eslint/js": "10.0.1",
75
- "@tsconfig/node22": "22.0.5",
76
- "@types/node": "26.2.0",
77
- "@typescript-eslint/eslint-plugin": "8.67.0",
78
- "@typescript-eslint/parser": "8.67.0",
79
- "eslint": "10.8.1",
75
+ "@tsconfig/node22": "22.0.6",
76
+ "@types/node": "26.4.0",
77
+ "@typescript-eslint/eslint-plugin": "8.69.0",
78
+ "@typescript-eslint/parser": "8.69.0",
79
+ "eslint": "10.9.1",
80
80
  "eslint-config-prettier": "10.1.8",
81
- "globals": "17.11.0",
81
+ "globals": "17.12.0",
82
82
  "prettier": "3.9.6",
83
83
  "ts-node": "10.9.2",
84
84
  "typescript": "6.0.3",
85
- "vitest": "4.1.10"
85
+ "vitest": "4.1.11"
86
86
  }
87
87
  }