@agentclientprotocol/claude-agent-acp 0.85.2-preview.9 → 0.86.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 (50) hide show
  1. package/dist/acp-agent.d.ts +56 -21
  2. package/dist/acp-agent.d.ts.map +1 -1
  3. package/dist/acp-agent.js +161 -34
  4. package/dist/clear-context-coordinator.d.ts +1 -0
  5. package/dist/clear-context-coordinator.d.ts.map +1 -1
  6. package/dist/clear-context-coordinator.js +2 -1
  7. package/dist/diff.d.ts +45 -8
  8. package/dist/diff.d.ts.map +1 -1
  9. package/dist/diff.js +111 -30
  10. package/dist/permissions/presentation.d.ts +17 -2
  11. package/dist/permissions/presentation.d.ts.map +1 -1
  12. package/dist/permissions/presentation.js +4 -0
  13. package/dist/session-config-ids.d.ts +1 -0
  14. package/dist/session-config-ids.d.ts.map +1 -1
  15. package/dist/session-config-ids.js +1 -0
  16. package/dist/tool-calls/client-capabilities.d.ts +20 -2
  17. package/dist/tool-calls/client-capabilities.d.ts.map +1 -1
  18. package/dist/tool-calls/client-capabilities.js +14 -4
  19. package/dist/tool-calls/facts.d.ts +8 -1
  20. package/dist/tool-calls/facts.d.ts.map +1 -1
  21. package/dist/tool-calls/renderer.d.ts +5 -1
  22. package/dist/tool-calls/renderer.d.ts.map +1 -1
  23. package/dist/tool-calls/renderer.js +14 -3
  24. package/dist/tool-calls/reporters/bash.d.ts.map +1 -1
  25. package/dist/tool-calls/reporters/bash.js +32 -5
  26. package/dist/tool-calls/reporters/file-edit.d.ts +1 -1
  27. package/dist/tool-calls/reporters/file-edit.d.ts.map +1 -1
  28. package/dist/tool-calls/reporters/file-edit.js +20 -14
  29. package/dist/v2/agent.d.ts +2 -0
  30. package/dist/v2/agent.d.ts.map +1 -1
  31. package/dist/v2/agent.js +109 -14
  32. package/dist/v2/initialize.d.ts +2 -5
  33. package/dist/v2/initialize.d.ts.map +1 -1
  34. package/dist/v2/initialize.js +42 -11
  35. package/dist/v2/permission.d.ts +25 -0
  36. package/dist/v2/permission.d.ts.map +1 -0
  37. package/dist/v2/permission.js +38 -0
  38. package/dist/v2/session-update.d.ts +6 -0
  39. package/dist/v2/session-update.d.ts.map +1 -1
  40. package/dist/v2/session-update.js +19 -0
  41. package/dist/v2/session.d.ts +29 -4
  42. package/dist/v2/session.d.ts.map +1 -1
  43. package/dist/v2/session.js +41 -6
  44. package/dist/v2/terminal.d.ts +42 -0
  45. package/dist/v2/terminal.d.ts.map +1 -0
  46. package/dist/v2/terminal.js +95 -0
  47. package/dist/v2/tool-call.d.ts +38 -0
  48. package/dist/v2/tool-call.d.ts.map +1 -0
  49. package/dist/v2/tool-call.js +49 -0
  50. package/package.json +1 -1
@@ -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
@@ -3,14 +3,39 @@
3
3
  * `ClaudeAcpAgent`: v2 requests become the v1 requests that the agent serves,
4
4
  * and its v1 responses become v2 responses.
5
5
  */
6
- import { type AvailableCommand, type NewSessionRequest, type NewSessionResponse, type ResumeSessionRequest, type ResumeSessionResponse, type SessionConfigOption, type SetSessionConfigOptionRequest } from "@agentclientprotocol/sdk";
6
+ import { type AvailableCommand, type ForkSessionRequest, type LoadSessionRequest, type NewSessionRequest, type NewSessionResponse, type ResumeSessionRequest, type ResumeSessionResponse, type SessionConfigOption, type SetSessionConfigOptionRequest } from "@agentclientprotocol/sdk";
7
7
  import * as v2 from "@agentclientprotocol/sdk/experimental/v2";
8
8
  export declare function v1NewSessionRequest(params: v2.NewSessionRequest): NewSessionRequest;
9
9
  /**
10
- * `replayFrom` is rejected until the v2 surface can translate the replayed
11
- * history, which needs the message and tool call updates.
10
+ * The v1 request that a v2 `session/resume` means.
11
+ *
12
+ * v2 restores a session with `session/resume` and replays its history only
13
+ * when `replayFrom` asks for it. v1 has two methods: `session/resume`, which
14
+ * never replays, and `session/load`, which replays the whole history before it
15
+ * responds. `replayFrom: { type: "start" }` is the second; a cursor that
16
+ * starts elsewhere is rejected.
12
17
  */
13
- export declare function v1ResumeSessionRequest(params: v2.ResumeSessionRequest): ResumeSessionRequest;
18
+ export declare function v1RestoreSessionRequest(params: v2.ResumeSessionRequest): {
19
+ method: "resume";
20
+ request: ResumeSessionRequest;
21
+ } | {
22
+ method: "load";
23
+ request: LoadSessionRequest;
24
+ };
25
+ /**
26
+ * The v1 requests that a v2 `session/fork` means.
27
+ *
28
+ * The agent's v1 fork copies the transcript into a new session and returns
29
+ * only its id; a v1 client then resumes or loads it. A v2 fork returns the new
30
+ * session with its config options, like `session/new`, so the client can
31
+ * prompt it. The v2 surface therefore forks, then resumes the fork without
32
+ * replay, with the request's `cwd`, `additionalDirectories`, and
33
+ * `mcpServers`. The request's `_meta` goes to the fork alone.
34
+ */
35
+ export declare function v1ForkSessionRequests(params: v2.ForkSessionRequest): {
36
+ fork: ForkSessionRequest;
37
+ resume: (sessionId: string) => ResumeSessionRequest;
38
+ };
14
39
  /** v2 drops `modes`: the session mode is the `mode` config option, which the agent always lists. */
15
40
  export declare function v2NewSessionResponse(response: NewSessionResponse): v2.NewSessionResponse;
16
41
  export declare function v2ResumeSessionResponse(response: ResumeSessionResponse): v2.ResumeSessionResponse;
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/v2/session.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAEL,KAAK,gBAAgB,EAErB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EAGxB,KAAK,6BAA6B,EACnC,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAE/D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,EAAE,CAAC,iBAAiB,GAAG,iBAAiB,CAInF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,EAAE,CAAC,oBAAoB,GAAG,oBAAoB,CAS5F;AAED,oGAAoG;AACpG,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,EAAE,CAAC,kBAAkB,CAExF;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,EAAE,CAAC,qBAAqB,CAKjG;AAED,uEAAuE;AACvE,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,EAAE,CAAC,6BAA6B,GACvC,6BAA6B,CAgB/B;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE,CAAC,mBAAmB,EAAE,CAQxF;AAWD,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC,gBAAgB,EAAE,CAKvF"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/v2/session.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EAEvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EAGxB,KAAK,6BAA6B,EACnC,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAE/D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,EAAE,CAAC,iBAAiB,GAAG,iBAAiB,CAInF;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,EAAE,CAAC,oBAAoB,GAE7B;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,oBAAoB,CAAA;CAAE,GACnD;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,kBAAkB,CAAA;CAAE,CAkBlD;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,EAAE,CAAC,kBAAkB,GAAG;IACpE,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,oBAAoB,CAAC;CACrD,CAUA;AAED,oGAAoG;AACpG,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,EAAE,CAAC,kBAAkB,CAExF;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,EAAE,CAAC,qBAAqB,CAKjG;AAED,uEAAuE;AACvE,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,EAAE,CAAC,6BAA6B,GACvC,6BAA6B,CAgB/B;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE,CAAC,mBAAmB,EAAE,CAQxF;AAWD,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC,gBAAgB,EAAE,CAKvF"}
@@ -11,15 +11,50 @@ export function v1NewSessionRequest(params) {
11
11
  return { ...request, mcpServers: (mcpServers ?? []).map(v1McpServer) };
12
12
  }
13
13
  /**
14
- * `replayFrom` is rejected until the v2 surface can translate the replayed
15
- * history, which needs the message and tool call updates.
14
+ * The v1 request that a v2 `session/resume` means.
15
+ *
16
+ * v2 restores a session with `session/resume` and replays its history only
17
+ * when `replayFrom` asks for it. v1 has two methods: `session/resume`, which
18
+ * never replays, and `session/load`, which replays the whole history before it
19
+ * responds. `replayFrom: { type: "start" }` is the second; a cursor that
20
+ * starts elsewhere is rejected.
16
21
  */
17
- export function v1ResumeSessionRequest(params) {
22
+ export function v1RestoreSessionRequest(params) {
18
23
  const { replayFrom, mcpServers, ...request } = params;
19
- if (replayFrom != null) {
20
- throw RequestError.invalidParams({ replayFrom }, "The ACP v2 surface does not replay session history yet");
24
+ if (replayFrom == null) {
25
+ return {
26
+ method: "resume",
27
+ request: { ...request, ...(mcpServers ? { mcpServers: mcpServers.map(v1McpServer) } : {}) },
28
+ };
21
29
  }
22
- return { ...request, ...(mcpServers ? { mcpServers: mcpServers.map(v1McpServer) } : {}) };
30
+ if (replayFrom.type !== "start") {
31
+ throw RequestError.invalidParams({ replayFrom }, `Replaying from a ${replayFrom.type} cursor is not supported`);
32
+ }
33
+ return {
34
+ method: "load",
35
+ request: { ...request, mcpServers: (mcpServers ?? []).map(v1McpServer) },
36
+ };
37
+ }
38
+ /**
39
+ * The v1 requests that a v2 `session/fork` means.
40
+ *
41
+ * The agent's v1 fork copies the transcript into a new session and returns
42
+ * only its id; a v1 client then resumes or loads it. A v2 fork returns the new
43
+ * session with its config options, like `session/new`, so the client can
44
+ * prompt it. The v2 surface therefore forks, then resumes the fork without
45
+ * replay, with the request's `cwd`, `additionalDirectories`, and
46
+ * `mcpServers`. The request's `_meta` goes to the fork alone.
47
+ */
48
+ export function v1ForkSessionRequests(params) {
49
+ const { mcpServers, _meta, ...request } = params;
50
+ const session = {
51
+ ...request,
52
+ ...(mcpServers ? { mcpServers: mcpServers.map(v1McpServer) } : {}),
53
+ };
54
+ return {
55
+ fork: { ...session, ...(_meta != null ? { _meta } : {}) },
56
+ resume: (sessionId) => ({ ...session, sessionId }),
57
+ };
23
58
  }
24
59
  /** v2 drops `modes`: the session mode is the `mode` config option, which the agent always lists. */
25
60
  export function v2NewSessionResponse(response) {