@agentclientprotocol/claude-agent-acp 0.85.2-preview.9 → 0.86.1-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/acp-agent.d.ts +67 -21
  2. package/dist/acp-agent.d.ts.map +1 -1
  3. package/dist/acp-agent.js +264 -68
  4. package/dist/async-tasks.d.ts +52 -3
  5. package/dist/async-tasks.d.ts.map +1 -1
  6. package/dist/async-tasks.js +185 -42
  7. package/dist/clear-context-coordinator.d.ts +1 -0
  8. package/dist/clear-context-coordinator.d.ts.map +1 -1
  9. package/dist/clear-context-coordinator.js +2 -1
  10. package/dist/diff.d.ts +45 -8
  11. package/dist/diff.d.ts.map +1 -1
  12. package/dist/diff.js +111 -30
  13. package/dist/native-subagents.d.ts +7 -4
  14. package/dist/native-subagents.d.ts.map +1 -1
  15. package/dist/native-subagents.js +22 -5
  16. package/dist/permissions/presentation.d.ts +17 -2
  17. package/dist/permissions/presentation.d.ts.map +1 -1
  18. package/dist/permissions/presentation.js +4 -0
  19. package/dist/session-config-ids.d.ts +1 -0
  20. package/dist/session-config-ids.d.ts.map +1 -1
  21. package/dist/session-config-ids.js +1 -0
  22. package/dist/tool-calls/background.d.ts +5 -2
  23. package/dist/tool-calls/background.d.ts.map +1 -1
  24. package/dist/tool-calls/background.js +6 -11
  25. package/dist/tool-calls/client-capabilities.d.ts +20 -2
  26. package/dist/tool-calls/client-capabilities.d.ts.map +1 -1
  27. package/dist/tool-calls/client-capabilities.js +14 -4
  28. package/dist/tool-calls/facts.d.ts +8 -1
  29. package/dist/tool-calls/facts.d.ts.map +1 -1
  30. package/dist/tool-calls/renderer.d.ts +5 -1
  31. package/dist/tool-calls/renderer.d.ts.map +1 -1
  32. package/dist/tool-calls/renderer.js +14 -3
  33. package/dist/tool-calls/reporters/bash.d.ts.map +1 -1
  34. package/dist/tool-calls/reporters/bash.js +32 -5
  35. package/dist/tool-calls/reporters/file-edit.d.ts +1 -1
  36. package/dist/tool-calls/reporters/file-edit.d.ts.map +1 -1
  37. package/dist/tool-calls/reporters/file-edit.js +20 -14
  38. package/dist/v2/agent.d.ts +2 -0
  39. package/dist/v2/agent.d.ts.map +1 -1
  40. package/dist/v2/agent.js +109 -14
  41. package/dist/v2/initialize.d.ts +2 -5
  42. package/dist/v2/initialize.d.ts.map +1 -1
  43. package/dist/v2/initialize.js +42 -11
  44. package/dist/v2/permission.d.ts +25 -0
  45. package/dist/v2/permission.d.ts.map +1 -0
  46. package/dist/v2/permission.js +38 -0
  47. package/dist/v2/session-update.d.ts +6 -0
  48. package/dist/v2/session-update.d.ts.map +1 -1
  49. package/dist/v2/session-update.js +19 -0
  50. package/dist/v2/session.d.ts +29 -4
  51. package/dist/v2/session.d.ts.map +1 -1
  52. package/dist/v2/session.js +41 -6
  53. package/dist/v2/terminal.d.ts +42 -0
  54. package/dist/v2/terminal.d.ts.map +1 -0
  55. package/dist/v2/terminal.js +95 -0
  56. package/dist/v2/tool-call.d.ts +38 -0
  57. package/dist/v2/tool-call.d.ts.map +1 -0
  58. package/dist/v2/tool-call.js +49 -0
  59. package/package.json +1 -1
@@ -118,8 +118,9 @@ export class AcpToolCallRenderer {
118
118
  permissionToolCall(toolUse, options = {}) {
119
119
  const facts = this.facts(toolUse, options.cwd);
120
120
  if (!this.capabilities.air.client) {
121
- // The upstream shape: the whole tool call again.
122
- const content = this.toolUseContent(toolUse.id, facts);
121
+ // The upstream shape: the whole tool call again. A v2 client also gets
122
+ // the exact preview patch, which the tool call does not show.
123
+ const content = options.previewContent ?? this.toolUseContent(toolUse.id, facts);
123
124
  const locations = [...(facts.locations ?? []), ...(options.extraLocations ?? [])];
124
125
  return {
125
126
  toolCallId: toolUse.id,
@@ -191,7 +192,17 @@ export class AcpToolCallRenderer {
191
192
  ...(this.capabilities.terminalOutputDelta
192
193
  ? { terminal_output_delta: output }
193
194
  : { terminal_output: output }),
194
- terminal_exit: { terminal_id: terminalId, exit_code: command.exitCode, signal: null },
195
+ terminal_exit: {
196
+ terminal_id: terminalId,
197
+ // AIR keeps the numbers it always got: 1 for a failed or
198
+ // interrupted command, else the exit code, or 0 when unknown.
199
+ exit_code: this.capabilities.air.client
200
+ ? result.is_error === true || command.interrupted
201
+ ? 1
202
+ : (command.exitCode ?? 0)
203
+ : (command.exitCode ?? null),
204
+ signal: null,
205
+ },
195
206
  },
196
207
  };
197
208
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../../src/tool-calls/reporters/bash.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAElG,gFAAgF;AAChF,qBAAa,YAAa,YAAW,YAAY;IAC/C,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY;IAUrC,iFAAiF;IACjF,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,eAAe,GAAG,SAAS;IAIpE,UAAU,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,iBAAiB,GAAG,eAAe;CAuEvE"}
1
+ {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../../src/tool-calls/reporters/bash.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAElG,gFAAgF;AAChF,qBAAa,YAAa,YAAW,YAAY;IAC/C,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY;IAUrC,iFAAiF;IACjF,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,eAAe,GAAG,SAAS;IAIpE,UAAU,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,iBAAiB,GAAG,eAAe;CAsFvE"}
@@ -30,7 +30,11 @@ export class BashReporter {
30
30
  // 4. Array content: text blocks for stdout, or image blocks when the
31
31
  // command produces an image.
32
32
  let output = "";
33
- let exitCode = isError ? 1 : 0;
33
+ // Claude Code accepts a non-zero code as a success only with a
34
+ // `returnCodeInterpretation` (grep's "No matches found"), so a success
35
+ // without one exited with 0.
36
+ let exitCode = isError ? undefined : 0;
37
+ let interrupted = false;
34
38
  const structuredBash = structuredResult(structured);
35
39
  if (structuredBash &&
36
40
  typeof structuredBash.stdout === "string" &&
@@ -38,12 +42,15 @@ export class BashReporter {
38
42
  !structuredBash.isImage &&
39
43
  structuredBash.backgroundTaskId === undefined) {
40
44
  output = [structuredBash.stdout, structuredBash.stderr].filter(Boolean).join("\n");
41
- // The CLI appends its abort marker only to the model-facing text, and an
42
- // aborted command is not a success.
45
+ // The CLI appends its abort marker only to the model-facing text. The
46
+ // result does not carry the code of the aborted command.
43
47
  if (structuredBash.interrupted) {
44
48
  output = [output, "[Command was aborted before completion]"].filter(Boolean).join("\n");
45
- exitCode = 1;
49
+ exitCode = undefined;
50
+ interrupted = true;
46
51
  }
52
+ if (structuredBash.returnCodeInterpretation !== undefined)
53
+ exitCode = undefined;
47
54
  // Structured stdout is clipped when the full output was persisted to
48
55
  // disk. Without this note the clip is silent.
49
56
  if (typeof structuredBash.persistedOutputPath === "string") {
@@ -78,6 +85,26 @@ export class BashReporter {
78
85
  }
79
86
  output = content.map((c) => c.text).join("\n");
80
87
  }
81
- return { command: { output, exitCode } };
88
+ if (isError && exitCode === undefined)
89
+ exitCode = failureExitCode(output);
90
+ // A backgrounded command is still running: the result only announces it.
91
+ if (structuredBash?.backgroundTaskId !== undefined)
92
+ exitCode = undefined;
93
+ return {
94
+ command: {
95
+ output,
96
+ ...(exitCode !== undefined ? { exitCode } : {}),
97
+ ...(interrupted ? { interrupted } : {}),
98
+ },
99
+ };
82
100
  }
83
101
  }
102
+ /**
103
+ * The exit code that the text of a failed command names. Claude Code starts the
104
+ * text of a command that exited with a failing code with `Exit code N`. Other
105
+ * failures, such as a denial or a command that could not start, name none.
106
+ */
107
+ function failureExitCode(text) {
108
+ const match = /^Exit code (\d+)(?:\n|$)/.exec(text);
109
+ return match ? Number(match[1]) : undefined;
110
+ }
@@ -22,7 +22,7 @@ type NormalizedWriteInput = Partial<FileWriteInput> & {
22
22
  export declare function normalizeWriteInput(input: unknown): NormalizedWriteInput | undefined;
23
23
  /** Edit: the diff holds the old and the new text. */
24
24
  export declare class EditReporter implements ToolReporter {
25
- toolUse(input: unknown, { cwd }: ToolUseContext): ToolUseFacts;
25
+ toolUse(input: unknown, { cwd, capabilities }: ToolUseContext): ToolUseFacts;
26
26
  toolResult(): ToolResultFacts;
27
27
  hookResult(toolResponse: unknown, context: ToolUseContext): Promise<ToolResultFacts>;
28
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"file-edit.d.ts","sourceRoot":"","sources":["../../../src/tool-calls/reporters/file-edit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,cAAc,EAEf,MAAM,6CAA6C,CAAC;AAIrD,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,YAAY,EACb,MAAM,aAAa,CAAC;AAErB;;;GAGG;AACH,qBAAa,aAAc,YAAW,YAAY;IAChD,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,cAAc,GAAG,YAAY;IAiCpF,UAAU,IAAI,eAAe;IAI7B,UAAU,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;CAGrF;AAED,yFAAyF;AACzF,KAAK,oBAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAI9E;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,GAAG,SAAS,CAYpF;AAED,qDAAqD;AACrD,qBAAa,YAAa,YAAW,YAAY;IAC/C,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE,cAAc,GAAG,YAAY;IAyB9D,UAAU,IAAI,eAAe;IAI7B,UAAU,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;CAGrF;AAwCD;;;;;;;GAOG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,EAAE,cAAc,GAAG,YAAY;IAsB5E;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,eAAe;CAMxD"}
1
+ {"version":3,"file":"file-edit.d.ts","sourceRoot":"","sources":["../../../src/tool-calls/reporters/file-edit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,cAAc,EAEf,MAAM,6CAA6C,CAAC;AAQrD,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,YAAY,EACb,MAAM,aAAa,CAAC;AAErB;;;GAGG;AACH,qBAAa,aAAc,YAAW,YAAY;IAChD,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,cAAc,GAAG,YAAY;IAoCpF,UAAU,IAAI,eAAe;IAI7B,UAAU,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;CAGrF;AAED,yFAAyF;AACzF,KAAK,oBAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAI9E;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,GAAG,SAAS,CAYpF;AAED,qDAAqD;AACrD,qBAAa,YAAa,YAAW,YAAY;IAC/C,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,EAAE,cAAc,GAAG,YAAY;IA0B5E,UAAU,IAAI,eAAe;IAI7B,UAAU,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;CAGrF;AAyCD;;;;;;;GAOG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,EAAE,cAAc,GAAG,YAAY;IAsB5E;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,eAAe;CAMxD"}
@@ -1,4 +1,4 @@
1
- import { patchUpdateFromDiffToolResponse, toolUpdateFromDiffToolResponse } from "../../diff.js";
1
+ import { patchUpdateFromDiffToolResponse, toolUpdateFromDiffToolResponse, v2UpdateFromDiffToolResponse, } from "../../diff.js";
2
2
  import { markdownEscape, resultText, textContent, toDisplayPath } from "../content.js";
3
3
  /**
4
4
  * Write: the diff holds the file text. The PostToolUse hook sends the final
@@ -14,12 +14,15 @@ export class WriteReporter {
14
14
  locations: write?.file_path ? [{ path: write.file_path }] : [],
15
15
  };
16
16
  if (write?.file_path) {
17
- // A negotiated client gets the exact patch from the approval preview or
18
- // from the PostToolUse hook. The input does not tell whether the file
19
- // exists, so the live tool call shows no diff: a diff without the old
20
- // text would claim a creation. The adapter reads the file only for the
21
- // preview. A replay has no preview and no hook, so it keeps the diff.
22
- if (!capabilities.diffPatch || replay) {
17
+ // A negotiated client and a v2 client get the exact patch from the
18
+ // approval preview or from the PostToolUse hook. The input does not tell
19
+ // whether the file exists, so the live tool call shows no diff: a diff
20
+ // without the old text would claim a creation. The adapter reads the
21
+ // file only for the preview. A replay has no preview and no hook, so it
22
+ // keeps the diff, except for a v2 client: a v2 diff names the operation,
23
+ // which the input does not tell.
24
+ const inputDiff = capabilities.v2 ? false : !capabilities.diffPatch || replay;
25
+ if (inputDiff) {
23
26
  facts.change = [
24
27
  {
25
28
  type: "diff",
@@ -68,7 +71,7 @@ export function normalizeWriteInput(input) {
68
71
  }
69
72
  /** Edit: the diff holds the old and the new text. */
70
73
  export class EditReporter {
71
- toolUse(input, { cwd }) {
74
+ toolUse(input, { cwd, capabilities }) {
72
75
  const edit = input;
73
76
  const displayPath = edit?.file_path ? toDisplayPath(edit.file_path, cwd) : undefined;
74
77
  const facts = {
@@ -76,10 +79,11 @@ export class EditReporter {
76
79
  kind: "edit",
77
80
  locations: edit?.file_path ? [{ path: edit.file_path }] : [],
78
81
  };
79
- if (edit?.file_path && (edit.old_string || edit.new_string)) {
80
- // The standard diff, also for a client that negotiated patches: the
81
- // input holds a snippet, not the file, so a patch would need line
82
- // numbers that the adapter does not know here.
82
+ // The standard diff, also for a client that negotiated patches: the input
83
+ // holds a snippet, not the file, so a patch would need line numbers that
84
+ // the adapter does not know here. A v2 diff cannot hold a snippet, so a v2
85
+ // client gets the patch from the PostToolUse hook alone.
86
+ if (edit?.file_path && (edit.old_string || edit.new_string) && !capabilities.v2) {
83
87
  facts.change = [
84
88
  {
85
89
  type: "diff",
@@ -104,12 +108,14 @@ export class EditReporter {
104
108
  * PostToolUse `tool_response`. For Write it replaces the optimistic creation
105
109
  * diff with the real diff of an updated file. A negotiated client gets an
106
110
  * exact git patch built from the written file, or the standard diff when none
107
- * can be built.
111
+ * can be built. A v2 client gets a v2 diff.
108
112
  */
109
113
  async function finalChange(toolResponse, { capabilities }) {
114
+ if (capabilities.v2)
115
+ return v2UpdateFromDiffToolResponse(toolResponse);
110
116
  if (!capabilities.diffPatch)
111
117
  return toolUpdateFromDiffToolResponse(toolResponse);
112
- const patch = await patchUpdateFromDiffToolResponse(toolResponse);
118
+ const patch = await patchUpdateFromDiffToolResponse(toolResponse, "air");
113
119
  if (patch)
114
120
  return patch;
115
121
  const created = createdFileDiff(toolResponse);
@@ -6,6 +6,8 @@ import { ClaudeAcpAgent, type Logger } from "../acp-agent.js";
6
6
  * As in `v1AgentApp`, the agent of the connection is created when the
7
7
  * connection opens, before the connection processes any inbound message.
8
8
  * `onAgent` receives it for the owner of the process (shutdown).
9
+ *
10
+ * The provider methods pass through: their types are the same in v1 and v2.
9
11
  */
10
12
  export declare function v2AgentApp(logger: Logger | undefined, onAgent: (agent: ClaudeAcpAgent) => void): v2.AgentApp;
11
13
  //# sourceMappingURL=agent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/v2/agent.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAkB,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAc9E;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GACvC,EAAE,CAAC,QAAQ,CAoCb"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/v2/agent.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAkB,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAkB9E;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GACvC,EAAE,CAAC,QAAQ,CA4Db"}
package/dist/v2/agent.js CHANGED
@@ -1,15 +1,19 @@
1
1
  import * as v2 from "@agentclientprotocol/sdk/experimental/v2";
2
2
  import { ClaudeAcpAgent } from "../acp-agent.js";
3
3
  import { v1InitializeRequest, v2InitializeResponse } from "./initialize.js";
4
+ import { v1PermissionResponse, v2PermissionRequest } from "./permission.js";
4
5
  import { v2Prompt } from "./prompt.js";
5
- import { v1NewSessionRequest, v1ResumeSessionRequest, v1SetSessionConfigOptionRequest, v2ConfigOptions, v2NewSessionResponse, v2ResumeSessionResponse, } from "./session.js";
6
+ import { v1ForkSessionRequests, v1NewSessionRequest, v1RestoreSessionRequest, v1SetSessionConfigOptionRequest, v2ConfigOptions, v2NewSessionResponse, v2ResumeSessionResponse, } from "./session.js";
6
7
  import { v2SessionUpdate } from "./session-update.js";
8
+ import { V2Terminals } from "./terminal.js";
7
9
  /**
8
10
  * The ACP v2 surface, for one connection.
9
11
  *
10
12
  * As in `v1AgentApp`, the agent of the connection is created when the
11
13
  * connection opens, before the connection processes any inbound message.
12
14
  * `onAgent` receives it for the owner of the process (shutdown).
15
+ *
16
+ * The provider methods pass through: their types are the same in v1 and v2.
13
17
  */
14
18
  export function v2AgentApp(logger, onAgent) {
15
19
  let agent;
@@ -18,16 +22,36 @@ export function v2AgentApp(logger, onAgent) {
18
22
  .agent({ name: "claude-code-acp" })
19
23
  .onConnect((connection) => {
20
24
  client = new V2ClientConnection(connection.client, logger ?? console);
21
- agent = new ClaudeAcpAgent(client, logger);
25
+ agent = new ClaudeAcpAgent(client, logger, { v2: true });
22
26
  onAgent(agent);
23
27
  })
24
28
  .onRequest(v2.methods.agent.initialize, async ({ params }) => v2InitializeResponse(await agent.initialize(v1InitializeRequest(params))))
25
29
  .onRequest(v2.methods.agent.auth.login, ({ params }) => agent.authenticate(params))
26
30
  .onRequest(v2.methods.agent.auth.logout, ({ params }) => agent.logout(params))
31
+ .onRequest(v2.methods.agent.providers.list, ({ params }) => agent.unstable_listProviders(params))
32
+ .onRequest(v2.methods.agent.providers.set, ({ params }) => agent.unstable_setProvider(params))
33
+ .onRequest(v2.methods.agent.providers.disable, ({ params }) => agent.unstable_disableProvider(params))
27
34
  .onRequest(v2.methods.agent.session.new, async ({ params }) => v2NewSessionResponse(await agent.newSession(v1NewSessionRequest(params))))
28
35
  .onRequest(v2.methods.agent.session.list, ({ params }) => agent.listSessions(params))
29
- .onRequest(v2.methods.agent.session.resume, async ({ params }) => v2ResumeSessionResponse(await agent.resumeSession(v1ResumeSessionRequest(params))))
30
- .onRequest(v2.methods.agent.session.close, ({ params }) => agent.closeSession(params))
36
+ .onRequest(v2.methods.agent.session.resume, async ({ params }) => {
37
+ const restore = v1RestoreSessionRequest(params);
38
+ return v2ResumeSessionResponse(restore.method === "resume"
39
+ ? await agent.resumeSession(restore.request)
40
+ : await client.replaying(params.sessionId, () => agent.loadSession(restore.request)));
41
+ })
42
+ .onRequest(v2.methods.agent.session.fork, async ({ params }) => {
43
+ const requests = v1ForkSessionRequests(params);
44
+ const { sessionId } = await agent.unstable_forkSession(requests.fork);
45
+ return {
46
+ sessionId,
47
+ ...v2ResumeSessionResponse(await agent.resumeSession(requests.resume(sessionId))),
48
+ };
49
+ })
50
+ .onRequest(v2.methods.agent.session.close, async ({ params }) => {
51
+ const response = await agent.closeSession(params);
52
+ client.terminals.forget(params.sessionId);
53
+ return response;
54
+ })
31
55
  .onRequest(v2.methods.agent.session.delete, ({ params }) => agent.deleteSession(params))
32
56
  .onRequest(v2.methods.agent.session.setConfigOption, async ({ params }) => {
33
57
  const { configOptions, ...response } = await agent.setSessionConfigOption(v1SetSessionConfigOptionRequest(params));
@@ -41,16 +65,38 @@ export function v2AgentApp(logger, onAgent) {
41
65
  /**
42
66
  * The {@link AcpClient} of an ACP v2 connection: it sends the v1 messages of
43
67
  * the agent as v2 messages.
44
- *
45
- * Permission requests still reject until the v2 surface translates them.
46
68
  */
47
69
  class V2ClientConnection {
48
70
  ctx;
49
71
  logger;
72
+ /**
73
+ * The sessions whose history replays, each with the messages that the
74
+ * replay has cleared so far.
75
+ */
76
+ replays = new Map();
77
+ /** The display terminals of the commands that the agent runs. */
78
+ terminals = new V2Terminals();
50
79
  constructor(ctx, logger) {
51
80
  this.ctx = ctx;
52
81
  this.logger = logger;
53
82
  }
83
+ /**
84
+ * Runs `restore`, which replays the history of `sessionId`.
85
+ *
86
+ * The agent replays messages as chunks. A v2 client may still hold a
87
+ * replayed message, and chunks append, so before the first replayed chunk of
88
+ * each message the client gets the message with no content, which clears it,
89
+ * as v2 requires of a message replayed as chunks.
90
+ */
91
+ async replaying(sessionId, restore) {
92
+ this.replays.set(sessionId, new Set());
93
+ try {
94
+ return await restore();
95
+ }
96
+ finally {
97
+ this.replays.delete(sessionId);
98
+ }
99
+ }
54
100
  /**
55
101
  * Sends a v2 session update, and logs rather than rejects when that fails.
56
102
  * It and {@link sessionUpdate} hand updates to the connection synchronously,
@@ -72,16 +118,48 @@ class V2ClientConnection {
72
118
  return this.ctx.notify(method, params);
73
119
  }
74
120
  async sessionUpdate({ update, ...notification }) {
75
- const v2Update = v2SessionUpdate(update);
76
- if (v2Update) {
77
- await this.ctx.notify(v2.methods.client.session.update, {
78
- ...notification,
79
- update: v2Update,
80
- });
121
+ const updates = this.v2Updates(notification.sessionId, update).flatMap((v2Update) => {
122
+ const clear = this.replayClear(notification.sessionId, v2Update);
123
+ return clear ? [clear, v2Update] : [v2Update];
124
+ });
125
+ // Every update goes to the connection before any is awaited, so an update
126
+ // that another call sends meanwhile cannot come between them.
127
+ await Promise.all(updates.map((v2Update) => this.ctx.notify(v2.methods.client.session.update, { ...notification, update: v2Update })));
128
+ }
129
+ /**
130
+ * The v2 updates of a v1 update: for a tool call report, the updates of its
131
+ * terminal first, then the report itself (see {@link V2Terminals}).
132
+ */
133
+ v2Updates(sessionId, update) {
134
+ if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
135
+ const v2Update = v2SessionUpdate(update);
136
+ return v2Update ? [v2Update] : [];
81
137
  }
138
+ const { terminal, report } = this.terminals.split(sessionId, update);
139
+ const reportUpdate = report && v2SessionUpdate(report);
140
+ return [...terminal, ...(reportUpdate ? [reportUpdate] : [])];
82
141
  }
83
- requestPermission() {
84
- return Promise.reject(new Error("The ACP v2 surface does not serve session/request_permission yet"));
142
+ /**
143
+ * The update that clears the message of a replayed chunk, when it is the
144
+ * first replayed chunk of that message (see {@link replaying}).
145
+ */
146
+ replayClear(sessionId, update) {
147
+ const cleared = this.replays.get(sessionId);
148
+ if (!cleared)
149
+ return undefined;
150
+ const kind = MESSAGE_OF_CHUNK.get(update.sessionUpdate);
151
+ if (!kind)
152
+ return undefined;
153
+ const { messageId } = update;
154
+ const key = `${kind} ${messageId}`;
155
+ if (cleared.has(key))
156
+ return undefined;
157
+ cleared.add(key);
158
+ return emptyMessage(kind, messageId);
159
+ }
160
+ async requestPermission(params, signal) {
161
+ const response = await this.ctx.request(v2.methods.client.session.requestPermission, v2PermissionRequest(params), { cancellationSignal: signal });
162
+ return v1PermissionResponse(response);
85
163
  }
86
164
  // Elicitation is the same in v1 and v2.
87
165
  createElicitation(params, signal) {
@@ -103,6 +181,23 @@ class V2ClientConnection {
103
181
  return Promise.reject(new Error("ACP v2 has no client file system"));
104
182
  }
105
183
  }
184
+ /** The message update of each message chunk. */
185
+ const MESSAGE_OF_CHUNK = new Map([
186
+ ["user_message_chunk", "user_message"],
187
+ ["agent_message_chunk", "agent_message"],
188
+ ["agent_thought_chunk", "agent_thought"],
189
+ ]);
190
+ function emptyMessage(kind, messageId) {
191
+ const message = { messageId, content: [] };
192
+ switch (kind) {
193
+ case "user_message":
194
+ return { ...message, sessionUpdate: kind };
195
+ case "agent_message":
196
+ return { ...message, sessionUpdate: kind };
197
+ default:
198
+ return { ...message, sessionUpdate: kind };
199
+ }
200
+ }
106
201
  function isExtensionMethod(method) {
107
202
  return method.startsWith("_");
108
203
  }
@@ -14,11 +14,8 @@ export declare function v1InitializeRequest(request: v2.InitializeRequest): Init
14
14
  /**
15
15
  * The v2 `initialize` response for the v1 response of the agent.
16
16
  *
17
- * It advertises no `session` capability yet. A v2 agent that advertises
18
- * `session` must serve the whole session baseline (`session/new`, `list`,
19
- * `resume`, `close`, `prompt`, `cancel`, and `update`), and the v2 surface of
20
- * the adapter does not serve it yet. Nor does it advertise the unstable
21
- * `providers` methods, which the v2 surface does not serve yet either.
17
+ * It does not advertise what the v2 surface does not serve yet: the
18
+ * extensions of {@link UNSERVED_EXTENSION_KEYS}.
22
19
  */
23
20
  export declare function v2InitializeResponse(response: InitializeResponse): v2.InitializeResponse;
24
21
  //# sourceMappingURL=initialize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"initialize.d.ts","sourceRoot":"","sources":["../../src/v2/initialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAE/D,0DAA0D;AAC1D,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,EAAE,CAAC,iBAAiB,GAAG,iBAAiB,CAwBpF;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,EAAE,CAAC,kBAAkB,CAcxF"}
1
+ {"version":3,"file":"initialize.d.ts","sourceRoot":"","sources":["../../src/v2/initialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAE/D,0DAA0D;AAC1D,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,EAAE,CAAC,iBAAiB,GAAG,iBAAiB,CA0BpF;AASD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,EAAE,CAAC,kBAAkB,CAuBxF"}
@@ -20,10 +20,12 @@ export function v1InitializeRequest(request) {
20
20
  // reads, writes, and runs commands itself.
21
21
  fs: { readTextFile: false, writeTextFile: false },
22
22
  terminal: false,
23
- // Every v2 client handles boolean config options and notices; v1 asks
24
- // for a marker. With notices, the agent reports advisories as notices
25
- // rather than as transcript messages.
26
- session: { configOptions: { boolean: {} }, notices: {} },
23
+ // Every v2 client handles boolean config options, notices, and
24
+ // compaction updates; v1 asks for a marker. With them, the agent reports
25
+ // advisories as notices rather than as transcript messages, and a
26
+ // compaction as a compaction entity rather than as a "Compact
27
+ // conversation" tool call, also on replay.
28
+ session: { configOptions: { boolean: {} }, notices: {}, compaction: {} },
27
29
  // v1 marks terminal auth support with a boolean, v2 with an object.
28
30
  auth: {
29
31
  terminal: capabilities?.auth?.terminal != null,
@@ -35,30 +37,59 @@ export function v1InitializeRequest(request) {
35
37
  ...(request._meta != null ? { _meta: request._meta } : {}),
36
38
  };
37
39
  }
40
+ /**
41
+ * The top-level `_meta` keys of the agent's v1 `initialize` response that
42
+ * advertise an extension the v2 surface does not serve yet: `steering`, for
43
+ * `_session/steering`.
44
+ */
45
+ const UNSERVED_EXTENSION_KEYS = ["steering"];
38
46
  /**
39
47
  * The v2 `initialize` response for the v1 response of the agent.
40
48
  *
41
- * It advertises no `session` capability yet. A v2 agent that advertises
42
- * `session` must serve the whole session baseline (`session/new`, `list`,
43
- * `resume`, `close`, `prompt`, `cancel`, and `update`), and the v2 surface of
44
- * the adapter does not serve it yet. Nor does it advertise the unstable
45
- * `providers` methods, which the v2 surface does not serve yet either.
49
+ * It does not advertise what the v2 surface does not serve yet: the
50
+ * extensions of {@link UNSERVED_EXTENSION_KEYS}.
46
51
  */
47
52
  export function v2InitializeResponse(response) {
48
53
  if (!response.agentInfo) {
49
54
  throw new Error("ACP v2 requires agentInfo, and the agent reported none");
50
55
  }
51
56
  const meta = response.agentCapabilities?._meta;
57
+ const topMeta = Object.fromEntries(Object.entries(response._meta ?? {}).filter(([key]) => !UNSERVED_EXTENSION_KEYS.includes(key)));
52
58
  return {
53
59
  protocolVersion: v2.PROTOCOL_VERSION,
54
60
  info: response.agentInfo,
55
61
  // v1 `auth.logout` has no v2 counterpart: a v2 agent that advertises auth
56
62
  // methods must serve `auth/logout`.
57
- capabilities: meta != null ? { _meta: meta } : {},
63
+ capabilities: {
64
+ session: V2_SESSION_CAPABILITIES,
65
+ // `providers/list`, `set`, and `disable`, which the agent serves for
66
+ // every client.
67
+ providers: {},
68
+ ...(meta != null ? { _meta: meta } : {}),
69
+ },
58
70
  authMethods: (response.authMethods ?? []).map(v2AuthMethod),
59
- ...(response._meta != null ? { _meta: response._meta } : {}),
71
+ ...(Object.keys(topMeta).length > 0 ? { _meta: topMeta } : {}),
60
72
  };
61
73
  }
74
+ /**
75
+ * The session capabilities of the v2 surface, which serves each through a v1
76
+ * method of the agent.
77
+ *
78
+ * - The session baseline: `session/new`, `list`, `resume` (with replay),
79
+ * `close`, `prompt`, `cancel`, and `update`.
80
+ * - `session/delete`, `session/fork` (which returns a session the client can
81
+ * prompt, see `v1ForkSessionRequests`), and `additionalDirectories`.
82
+ * - The prompt content and MCP transports that the agent's v1 `initialize`
83
+ * lists: images, embedded context, and HTTP servers. Stdio servers are in the
84
+ * v1 baseline; v2 has no SSE transport.
85
+ */
86
+ const V2_SESSION_CAPABILITIES = {
87
+ prompt: { image: {}, embeddedContext: {} },
88
+ mcp: { stdio: {}, http: {} },
89
+ delete: {},
90
+ fork: {},
91
+ additionalDirectories: {},
92
+ };
62
93
  /** v2 names the id of a method `methodId`, requires `type`, and lists `env` as name and value pairs. */
63
94
  function v2AuthMethod(method) {
64
95
  if ("type" in method && method.type === "terminal") {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The v2 form of the permission requests that `ClaudeAcpAgent` sends, and the
3
+ * v1 form of their answers.
4
+ */
5
+ import type { RequestPermissionResponse } from "@agentclientprotocol/sdk";
6
+ import type * as v2 from "@agentclientprotocol/sdk/experimental/v2";
7
+ import type { AcpPermissionRequest } from "../permissions/presentation.js";
8
+ /**
9
+ * The v2 form of a permission request of the agent.
10
+ *
11
+ * v1 asks about a tool call, whose title the prompt shows. v2 gives the prompt
12
+ * its own `title` and `description`, which the agent sets for a v2 client
13
+ * (`AcpPermissionRequest`), and asks about a `subject`: here always the tool
14
+ * call, as the same patch that a `tool_call_update` carries.
15
+ */
16
+ export declare function v2PermissionRequest(request: AcpPermissionRequest): v2.RequestPermissionRequest;
17
+ /**
18
+ * The v1 form of the answer to a permission request.
19
+ *
20
+ * v2 adds outcomes that v1 does not know, which the draft says are no
21
+ * approval. They become `cancelled`, which the agent reads as an aborted tool
22
+ * use, as it does a cancelled prompt.
23
+ */
24
+ export declare function v1PermissionResponse(response: v2.RequestPermissionResponse): RequestPermissionResponse;
25
+ //# sourceMappingURL=permission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../../src/v2/permission.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,KAAK,KAAK,EAAE,MAAM,0CAA0C,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAG3E;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,EAAE,CAAC,wBAAwB,CAY9F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,EAAE,CAAC,yBAAyB,GACrC,yBAAyB,CAS3B"}
@@ -0,0 +1,38 @@
1
+ import { v2ToolCallPatch } from "./tool-call.js";
2
+ /**
3
+ * The v2 form of a permission request of the agent.
4
+ *
5
+ * v1 asks about a tool call, whose title the prompt shows. v2 gives the prompt
6
+ * its own `title` and `description`, which the agent sets for a v2 client
7
+ * (`AcpPermissionRequest`), and asks about a `subject`: here always the tool
8
+ * call, as the same patch that a `tool_call_update` carries.
9
+ */
10
+ export function v2PermissionRequest(request) {
11
+ if (request.title === undefined) {
12
+ throw new Error("An ACP v2 permission request needs a title");
13
+ }
14
+ return {
15
+ sessionId: request.sessionId,
16
+ title: request.title,
17
+ ...(request.description ? { description: request.description } : {}),
18
+ subject: { type: "tool_call", toolCall: v2ToolCallPatch(request.toolCall) },
19
+ options: request.options,
20
+ ...(request._meta != null ? { _meta: request._meta } : {}),
21
+ };
22
+ }
23
+ /**
24
+ * The v1 form of the answer to a permission request.
25
+ *
26
+ * v2 adds outcomes that v1 does not know, which the draft says are no
27
+ * approval. They become `cancelled`, which the agent reads as an aborted tool
28
+ * use, as it does a cancelled prompt.
29
+ */
30
+ export function v1PermissionResponse(response) {
31
+ const outcome = response.outcome;
32
+ return {
33
+ outcome: outcome.outcome === "selected"
34
+ ? outcome
35
+ : { outcome: "cancelled" },
36
+ ...(response._meta != null ? { _meta: response._meta } : {}),
37
+ };
38
+ }
@@ -4,6 +4,12 @@
4
4
  */
5
5
  import * as v2 from "@agentclientprotocol/sdk/experimental/v2";
6
6
  import type { AcpSessionNotification } from "../acp-subagents.js";
7
+ /**
8
+ * The id of the plan of a session. v1 has one plan per session, which every
9
+ * plan update replaces whole, from TodoWrite and from the Task tools alike.
10
+ * v2 tracks plans by id, so that plan gets one.
11
+ */
12
+ export declare const SESSION_PLAN_ID = "plan";
7
13
  /**
8
14
  * The v2 form of a v1 session update, or `undefined` when v2 has no
9
15
  * counterpart and the update tells a v2 client nothing new.
@@ -1 +1 @@
1
- {"version":3,"file":"session-update.d.ts","sourceRoot":"","sources":["../../src/v2/session-update.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAC/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAGlE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,sBAAsB,CAAC,QAAQ,CAAC,GACvC,EAAE,CAAC,aAAa,GAAG,SAAS,CA8B9B"}
1
+ {"version":3,"file":"session-update.d.ts","sourceRoot":"","sources":["../../src/v2/session-update.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAC/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAIlE;;;;GAIG;AACH,eAAO,MAAM,eAAe,SAAS,CAAC;AAEtC;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,sBAAsB,CAAC,QAAQ,CAAC,GACvC,EAAE,CAAC,aAAa,GAAG,SAAS,CA0C9B"}
@@ -1,4 +1,11 @@
1
1
  import { v2AvailableCommands, v2ConfigOptions } from "./session.js";
2
+ import { v2ToolCallUpdate } from "./tool-call.js";
3
+ /**
4
+ * The id of the plan of a session. v1 has one plan per session, which every
5
+ * plan update replaces whole, from TodoWrite and from the Task tools alike.
6
+ * v2 tracks plans by id, so that plan gets one.
7
+ */
8
+ export const SESSION_PLAN_ID = "plan";
2
9
  /**
3
10
  * The v2 form of a v1 session update, or `undefined` when v2 has no
4
11
  * counterpart and the update tells a v2 client nothing new.
@@ -15,6 +22,8 @@ export function v2SessionUpdate(update) {
15
22
  case "session_info_update":
16
23
  case "usage_update":
17
24
  case "notice":
25
+ case "compaction_update":
26
+ case "compaction_summary_chunk":
18
27
  return update;
19
28
  case "agent_message_chunk":
20
29
  case "agent_thought_chunk":
@@ -26,6 +35,16 @@ export function v2SessionUpdate(update) {
26
35
  }
27
36
  return { ...chunk, messageId };
28
37
  }
38
+ case "tool_call":
39
+ case "tool_call_update":
40
+ return v2ToolCallUpdate(update);
41
+ case "plan":
42
+ // A v2 plan of items is replaced whole as well.
43
+ return {
44
+ sessionUpdate: "plan_update",
45
+ plan: { type: "items", planId: SESSION_PLAN_ID, entries: update.entries },
46
+ ...(update._meta != null ? { _meta: update._meta } : {}),
47
+ };
29
48
  case "current_mode_update":
30
49
  // v2 has no modes: the mode is the `mode` config option. Every mode
31
50
  // change also reaches the client as a `config_option_update` or in the