@agentclientprotocol/claude-agent-acp 0.95.1-preview.1 → 0.95.1-preview.3

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,8 +1,9 @@
1
- import { HANDBACK_TOOL, handbackMessage, isNativeSubagentControlTool, launchReply, linkedSessionMessages, nativeSubagentState, rfdEndState, rfdLabels, } from "./native-subagents.js";
1
+ import { childModelOption, HANDBACK_TOOL, handbackMessage, isNativeSubagentControlTool, launchReply, linkedSessionMessages, nativeSubagentState, responseModelEntry, rfdEndState, rfdLabels, } from "./native-subagents.js";
2
2
  import { noticeUpdate } from "./session-notices.js";
3
3
  export class RfdSubagentReplay {
4
4
  history;
5
5
  publish;
6
+ models;
6
7
  /** The children by the id of the tool use that launched each. */
7
8
  children = new Map();
8
9
  byAgentId = new Map();
@@ -13,9 +14,10 @@ export class RfdSubagentReplay {
13
14
  withoutHistory = 0;
14
15
  /** The SubagentHandback calls, with their report and whether their result failed. */
15
16
  handbacks = new Map();
16
- constructor(history, publish) {
17
+ constructor(history, publish, models) {
17
18
  this.history = history;
18
19
  this.publish = publish;
20
+ this.models = models;
19
21
  }
20
22
  /**
21
23
  * Registers the launches, the launch results, and the SendMessage calls in
@@ -41,7 +43,8 @@ export class RfdSubagentReplay {
41
43
  to: input.to,
42
44
  message: input.message,
43
45
  senderSessionId: ownerSessionId,
44
- delivered: false,
46
+ shownAsToolCall: false,
47
+ outgoingSent: false,
45
48
  });
46
49
  }
47
50
  }
@@ -63,8 +66,12 @@ export class RfdSubagentReplay {
63
66
  childOfLaunch(toolUseId) {
64
67
  return this.children.get(toolUseId);
65
68
  }
66
- /** Announces `child` on its parent, as live: running, with the parent's labels. */
67
- async announce(child) {
69
+ /**
70
+ * Announces `child` on its parent, as live: running, with the parent's
71
+ * labels. Then it sends `firstModel`, the model of the child's first
72
+ * response, when the transcript has one.
73
+ */
74
+ async announce(child, firstModel) {
68
75
  if (child.announced)
69
76
  return;
70
77
  const labels = rfdLabels(child.record.name ?? child.input.name, child.record.agentType ?? child.input.subagent_type, child.record.description ?? child.input.description);
@@ -79,6 +86,30 @@ export class RfdSubagentReplay {
79
86
  },
80
87
  });
81
88
  child.announced = true;
89
+ await this.responseModel(child, firstModel);
90
+ }
91
+ /**
92
+ * Takes the model of a replayed response of the child `owner`. A child
93
+ * whose model changes gets its `model` option again.
94
+ */
95
+ async responseModel(owner, model) {
96
+ const entry = responseModelEntry(model, this.models);
97
+ if (!entry || !owner.announced)
98
+ return;
99
+ owner.responseModel = model;
100
+ await this.sendModel(owner, entry);
101
+ }
102
+ async sendModel(child, model) {
103
+ if (child.model?.value === model.value && child.model.name === model.name)
104
+ return;
105
+ child.model = model;
106
+ await this.publish({
107
+ sessionId: child.sessionId,
108
+ update: {
109
+ sessionUpdate: "config_option_update",
110
+ configOptions: [childModelOption(model)],
111
+ },
112
+ });
82
113
  }
83
114
  /**
84
115
  * The rest of a child's user message to replay, or `null` when nothing is
@@ -119,15 +150,30 @@ export class RfdSubagentReplay {
119
150
  /**
120
151
  * Takes a replayed update of a tool call, from the transcript of `owner`
121
152
  * when it is a child's. Returns whether the client is not shown it: an
122
- * update of the launch of a child that the replay shows, or of a successful
153
+ * update of the launch of a child that the replay shows, of a SendMessage
154
+ * call whose message the replay already delivered, or of a successful
123
155
  * SubagentHandback call of `owner`. A foreground launch's completed result
124
- * goes out as the child's reply, and a hand-back as the child's message to
125
- * its parent.
156
+ * goes out as the child's reply, a delivered SendMessage as the sender's
157
+ * outgoing message, and a hand-back as the child's message to its parent.
126
158
  */
127
159
  async toolCallUpdate(update, owner) {
128
160
  if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
129
161
  return false;
130
162
  }
163
+ const sendMessage = this.sendMessages.find((call) => call.toolUseId === update.toolCallId);
164
+ if (sendMessage) {
165
+ if (sendMessage.recipientSessionId === undefined) {
166
+ sendMessage.shownAsToolCall = true;
167
+ return false;
168
+ }
169
+ if (!sendMessage.outgoingSent) {
170
+ sendMessage.outgoingSent = true;
171
+ const [outgoing] = linkedSessionMessages(sendMessage.toolUseId, sendMessage.senderSessionId, sendMessage.recipientSessionId, sendMessage.message);
172
+ if (outgoing)
173
+ await this.publish(outgoing);
174
+ }
175
+ return true;
176
+ }
131
177
  const handback = owner ? this.handbacks.get(update.toolCallId) : undefined;
132
178
  if (owner && handback && !handback.failed) {
133
179
  if (!handback.sent) {
@@ -205,21 +251,37 @@ export class RfdSubagentReplay {
205
251
  * can be cancelled, as live. Any other child cannot be running, because the
206
252
  * process that ran it is gone, so its replayed end is its current state.
207
253
  */
208
- currentStates(live) {
254
+ currentStates(live, liveConfiguration) {
209
255
  return [...this.children.values()]
210
256
  .filter((child) => child.announced)
211
- .map((child) => {
257
+ .flatMap((child) => {
212
258
  const liveState = live(child.sessionId);
213
259
  const state = liveState === undefined || liveState === "idle" ? endState(child) : { state: liveState };
214
- return {
215
- sessionId: child.parentSessionId,
216
- update: {
217
- sessionUpdate: "subagent_update",
218
- sessionId: child.sessionId,
219
- ...(liveState !== undefined ? { capabilities: { cancel: {} } } : {}),
220
- state,
260
+ // The session's model list may be known only now, after the load response.
261
+ const entry = responseModelEntry(child.responseModel, this.models);
262
+ const replayed = child.model ? JSON.stringify([childModelOption(child.model)]) : undefined;
263
+ const current = liveConfiguration?.(child.sessionId) ?? (entry ? [childModelOption(entry)] : undefined);
264
+ // A configuration that the replay already sent does not go again.
265
+ const configOptions = current && JSON.stringify(current) !== replayed ? current : undefined;
266
+ return [
267
+ {
268
+ sessionId: child.parentSessionId,
269
+ update: {
270
+ sessionUpdate: "subagent_update",
271
+ sessionId: child.sessionId,
272
+ ...(liveState !== undefined ? { capabilities: { cancel: {} } } : {}),
273
+ state,
274
+ },
221
275
  },
222
- };
276
+ ...(configOptions
277
+ ? [
278
+ {
279
+ sessionId: child.sessionId,
280
+ update: { sessionUpdate: "config_option_update", configOptions },
281
+ },
282
+ ]
283
+ : []),
284
+ ];
223
285
  });
224
286
  }
225
287
  registerLaunch(toolUseId, input, parentSessionId) {
@@ -256,21 +318,26 @@ export class RfdSubagentReplay {
256
318
  * first undelivered SendMessage to the child whose message the delivered
257
319
  * text contains. With that call, the message has the call's id and text and
258
320
  * its sender, as live; without it, its transcript id and the delivered text.
321
+ * Unless the replay already showed the call as a tool call, the message
322
+ * replaces it, so this view names the sender's view, which goes out at the
323
+ * call (see {@link toolCallUpdate}).
259
324
  */
260
325
  async sendDelivery(child, message, text) {
261
326
  const names = [child.sessionId, child.record.name, child.input.name];
262
- const call = this.sendMessages.find((candidate) => !candidate.delivered &&
327
+ const call = this.sendMessages.find((candidate) => candidate.recipientSessionId === undefined &&
263
328
  names.includes(candidate.to) &&
264
329
  text.includes(candidate.message.trim()));
265
330
  if (call) {
266
- call.delivered = true;
267
- await this.sendMessage(child, call.toolUseId, call.message, call.senderSessionId);
331
+ call.recipientSessionId = child.sessionId;
332
+ await this.sendMessage(child, call.toolUseId, call.message, call.senderSessionId, {
333
+ linked: !call.shownAsToolCall,
334
+ });
268
335
  }
269
336
  else {
270
337
  await this.sendMessage(child, message.uuid, text, undefined);
271
338
  }
272
339
  }
273
- async sendMessage(child, messageId, text, sender) {
340
+ async sendMessage(child, messageId, text, sender, { linked = false } = {}) {
274
341
  await this.publish({
275
342
  sessionId: child.sessionId,
276
343
  update: {
@@ -278,11 +345,23 @@ export class RfdSubagentReplay {
278
345
  messageId,
279
346
  ...(sender ? { senderSessionId: sender } : {}),
280
347
  recipientSessionId: child.sessionId,
348
+ ...(linked ? { senderMessageId: messageId } : {}),
281
349
  content: [{ type: "text", text }],
282
350
  },
283
351
  });
284
352
  }
285
353
  }
354
+ /** The model of the first response in `messages`, a subagent's transcript. */
355
+ export function firstResponseModel(messages) {
356
+ for (const message of messages) {
357
+ if (message.type !== "assistant")
358
+ continue;
359
+ const model = message.message?.model;
360
+ if (typeof model === "string" && model && model !== "<synthetic>")
361
+ return model;
362
+ }
363
+ return undefined;
364
+ }
286
365
  /**
287
366
  * The end of a foreground launch from its tool result: an error result that
288
367
  * names a stop is a cancellation, any other error a failure.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.95.1-preview.1",
6
+ "version": "0.95.1-preview.3",
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",