@agentclientprotocol/claude-agent-acp 0.85.2-preview.12 → 0.85.2-preview.14

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.
@@ -1 +1 @@
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;AAgB9E;;;;;;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;AAgB9E;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GACvC,EAAE,CAAC,QAAQ,CAyCb"}
package/dist/v2/agent.js CHANGED
@@ -3,7 +3,7 @@ import { ClaudeAcpAgent } from "../acp-agent.js";
3
3
  import { v1InitializeRequest, v2InitializeResponse } from "./initialize.js";
4
4
  import { v1PermissionResponse, v2PermissionRequest } from "./permission.js";
5
5
  import { v2Prompt } from "./prompt.js";
6
- import { v1NewSessionRequest, v1ResumeSessionRequest, v1SetSessionConfigOptionRequest, v2ConfigOptions, v2NewSessionResponse, v2ResumeSessionResponse, } from "./session.js";
6
+ import { v1NewSessionRequest, v1RestoreSessionRequest, v1SetSessionConfigOptionRequest, v2ConfigOptions, v2NewSessionResponse, v2ResumeSessionResponse, } from "./session.js";
7
7
  import { v2SessionUpdate } from "./session-update.js";
8
8
  /**
9
9
  * The ACP v2 surface, for one connection.
@@ -27,7 +27,12 @@ export function v2AgentApp(logger, onAgent) {
27
27
  .onRequest(v2.methods.agent.auth.logout, ({ params }) => agent.logout(params))
28
28
  .onRequest(v2.methods.agent.session.new, async ({ params }) => v2NewSessionResponse(await agent.newSession(v1NewSessionRequest(params))))
29
29
  .onRequest(v2.methods.agent.session.list, ({ params }) => agent.listSessions(params))
30
- .onRequest(v2.methods.agent.session.resume, async ({ params }) => v2ResumeSessionResponse(await agent.resumeSession(v1ResumeSessionRequest(params))))
30
+ .onRequest(v2.methods.agent.session.resume, async ({ params }) => {
31
+ const restore = v1RestoreSessionRequest(params);
32
+ return v2ResumeSessionResponse(restore.method === "resume"
33
+ ? await agent.resumeSession(restore.request)
34
+ : await client.replaying(params.sessionId, () => agent.loadSession(restore.request)));
35
+ })
31
36
  .onRequest(v2.methods.agent.session.close, ({ params }) => agent.closeSession(params))
32
37
  .onRequest(v2.methods.agent.session.delete, ({ params }) => agent.deleteSession(params))
33
38
  .onRequest(v2.methods.agent.session.setConfigOption, async ({ params }) => {
@@ -46,10 +51,32 @@ export function v2AgentApp(logger, onAgent) {
46
51
  class V2ClientConnection {
47
52
  ctx;
48
53
  logger;
54
+ /**
55
+ * The sessions whose history replays, each with the messages that the
56
+ * replay has cleared so far.
57
+ */
58
+ replays = new Map();
49
59
  constructor(ctx, logger) {
50
60
  this.ctx = ctx;
51
61
  this.logger = logger;
52
62
  }
63
+ /**
64
+ * Runs `restore`, which replays the history of `sessionId`.
65
+ *
66
+ * The agent replays messages as chunks. A v2 client may still hold a
67
+ * replayed message, and chunks append, so before the first replayed chunk of
68
+ * each message the client gets the message with no content, which clears it,
69
+ * as v2 requires of a message replayed as chunks.
70
+ */
71
+ async replaying(sessionId, restore) {
72
+ this.replays.set(sessionId, new Set());
73
+ try {
74
+ return await restore();
75
+ }
76
+ finally {
77
+ this.replays.delete(sessionId);
78
+ }
79
+ }
53
80
  /**
54
81
  * Sends a v2 session update, and logs rather than rejects when that fails.
55
82
  * It and {@link sessionUpdate} hand updates to the connection synchronously,
@@ -72,12 +99,35 @@ class V2ClientConnection {
72
99
  }
73
100
  async sessionUpdate({ update, ...notification }) {
74
101
  const v2Update = v2SessionUpdate(update);
75
- if (v2Update) {
76
- await this.ctx.notify(v2.methods.client.session.update, {
77
- ...notification,
78
- update: v2Update,
79
- });
80
- }
102
+ if (!v2Update)
103
+ return;
104
+ const clear = this.replayClear(notification.sessionId, v2Update);
105
+ // Both go to the connection before either is awaited, so a chunk that
106
+ // another call sends meanwhile cannot come between them.
107
+ await Promise.all([
108
+ ...(clear
109
+ ? [this.ctx.notify(v2.methods.client.session.update, { ...notification, update: clear })]
110
+ : []),
111
+ this.ctx.notify(v2.methods.client.session.update, { ...notification, update: v2Update }),
112
+ ]);
113
+ }
114
+ /**
115
+ * The update that clears the message of a replayed chunk, when it is the
116
+ * first replayed chunk of that message (see {@link replaying}).
117
+ */
118
+ replayClear(sessionId, update) {
119
+ const cleared = this.replays.get(sessionId);
120
+ if (!cleared)
121
+ return undefined;
122
+ const kind = MESSAGE_OF_CHUNK.get(update.sessionUpdate);
123
+ if (!kind)
124
+ return undefined;
125
+ const { messageId } = update;
126
+ const key = `${kind} ${messageId}`;
127
+ if (cleared.has(key))
128
+ return undefined;
129
+ cleared.add(key);
130
+ return emptyMessage(kind, messageId);
81
131
  }
82
132
  async requestPermission(params, signal) {
83
133
  const response = await this.ctx.request(v2.methods.client.session.requestPermission, v2PermissionRequest(params), { cancellationSignal: signal });
@@ -103,6 +153,23 @@ class V2ClientConnection {
103
153
  return Promise.reject(new Error("ACP v2 has no client file system"));
104
154
  }
105
155
  }
156
+ /** The message update of each message chunk. */
157
+ const MESSAGE_OF_CHUNK = new Map([
158
+ ["user_message_chunk", "user_message"],
159
+ ["agent_message_chunk", "agent_message"],
160
+ ["agent_thought_chunk", "agent_thought"],
161
+ ]);
162
+ function emptyMessage(kind, messageId) {
163
+ const message = { messageId, content: [] };
164
+ switch (kind) {
165
+ case "user_message":
166
+ return { ...message, sessionUpdate: kind };
167
+ case "agent_message":
168
+ return { ...message, sessionUpdate: kind };
169
+ default:
170
+ return { ...message, sessionUpdate: kind };
171
+ }
172
+ }
106
173
  function isExtensionMethod(method) {
107
174
  return method.startsWith("_");
108
175
  }
@@ -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;AAIlE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,sBAAsB,CAAC,QAAQ,CAAC,GACvC,EAAE,CAAC,aAAa,GAAG,SAAS,CAiC9B"}
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,CAwC9B"}
@@ -1,5 +1,11 @@
1
1
  import { v2AvailableCommands, v2ConfigOptions } from "./session.js";
2
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";
3
9
  /**
4
10
  * The v2 form of a v1 session update, or `undefined` when v2 has no
5
11
  * counterpart and the update tells a v2 client nothing new.
@@ -30,6 +36,13 @@ export function v2SessionUpdate(update) {
30
36
  case "tool_call":
31
37
  case "tool_call_update":
32
38
  return v2ToolCallUpdate(update);
39
+ case "plan":
40
+ // A v2 plan of items is replaced whole as well.
41
+ return {
42
+ sessionUpdate: "plan_update",
43
+ plan: { type: "items", planId: SESSION_PLAN_ID, entries: update.entries },
44
+ ...(update._meta != null ? { _meta: update._meta } : {}),
45
+ };
33
46
  case "current_mode_update":
34
47
  // v2 has no modes: the mode is the `mode` config option. Every mode
35
48
  // change also reaches the client as a `config_option_update` or in the
@@ -3,14 +3,25 @@
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 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
+ };
14
25
  /** v2 drops `modes`: the session mode is the `mode` config option, which the agent always lists. */
15
26
  export declare function v2NewSessionResponse(response: NewSessionResponse): v2.NewSessionResponse;
16
27
  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,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,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,29 @@ 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
+ };
23
37
  }
24
38
  /** v2 drops `modes`: the session mode is the `mode` config option, which the agent always lists. */
25
39
  export function v2NewSessionResponse(response) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.85.2-preview.12",
6
+ "version": "0.85.2-preview.14",
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",