@agentclientprotocol/claude-agent-acp 0.91.1-preview.3 → 0.91.1-preview.5

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.
@@ -15,6 +15,7 @@ export class NativeSubagentRuntime {
15
15
  publish;
16
16
  logger;
17
17
  lineage;
18
+ launchedAgent;
18
19
  enabled;
19
20
  /** The form of the child sessions; see {@link SubagentForm}. */
20
21
  form;
@@ -31,23 +32,37 @@ export class NativeSubagentRuntime {
31
32
  */
32
33
  childByToolCall = new Map();
33
34
  taskFinishPromises = new Map();
34
- /** The RFD form: the session that made each SendMessage call. */
35
+ /** The session that made each SendMessage call. */
35
36
  senderByToolUse = new Map();
36
37
  generationByTaskId = new Map();
37
38
  pending = new Map();
38
39
  pendingCount = 0;
40
+ /** The AIR form: agents being restored from the history, by agent id. */
41
+ agentRestores = new Map();
42
+ /** The AIR form: launch ids being resolved to an agent, by launch id. */
43
+ launchRestores = new Map();
44
+ /** The AIR form: launch ids that the history has no restorable agent for. */
45
+ unresolvedLaunches = new Set();
46
+ /** The AIR form: agents that the history could not place, so it is read once. */
47
+ unrestorableAgents = new Set();
48
+ /** The latest generation number given to a restored agent. */
49
+ lastRestoredGeneration = 0;
50
+ /** Bumped when the restore state is dropped; a restore that began before it is void. */
51
+ restoreEpoch = 0;
39
52
  /** The RFD form: how many times the parent was cancelled (see {@link awaitingUser}). */
40
53
  parentCancels = 0;
41
54
  /**
42
55
  * `form` is undefined for a client without native subagent sessions. The RFD
43
- * form restores an agent from before a load or resume with `lineage`.
56
+ * form restores an agent from before a load or resume with `lineage`. The AIR
57
+ * form does too, and `launchedAgent` finds the agent that a launch call made.
44
58
  */
45
- constructor(form, rootSessionId, session, publish, logger, lineage) {
59
+ constructor(form, rootSessionId, session, publish, logger, lineage, launchedAgent) {
46
60
  this.rootSessionId = rootSessionId;
47
61
  this.session = session;
48
62
  this.publish = publish;
49
63
  this.logger = logger;
50
64
  this.lineage = lineage;
65
+ this.launchedAgent = launchedAgent;
51
66
  this.form = form;
52
67
  this.enabled = form !== undefined;
53
68
  this.children = session.nativeSubagentsByTaskId ??= new Map();
@@ -61,7 +76,7 @@ export class NativeSubagentRuntime {
61
76
  }
62
77
  async route(notification, deliver, forcedSessionId) {
63
78
  const routed = await this.routeUpdate(notification, deliver, forcedSessionId);
64
- if (routed && this.form === "rfd")
79
+ if (routed)
65
80
  this.rememberSendMessage(routed);
66
81
  return routed;
67
82
  }
@@ -137,7 +152,13 @@ export class NativeSubagentRuntime {
137
152
  if (this.enabled && claudeMeta?.parentToolUseId) {
138
153
  const child = this.childByParentToolUse.get(claudeMeta.parentToolUseId);
139
154
  if (!child || !child.announced) {
140
- this.buffer(claudeMeta.parentToolUseId, notification);
155
+ const launchId = claudeMeta.parentToolUseId;
156
+ this.buffer(launchId, notification);
157
+ // An id that this process never saw is the launch call of an agent
158
+ // from before a load or resume; a new Agent call is known by now.
159
+ if (!child && this.form === "air" && this.isUnseenLaunch(launchId)) {
160
+ await this.restoreLaunch(launchId, deliver);
161
+ }
141
162
  return null;
142
163
  }
143
164
  return this.toChild(child, notification, toolCallId);
@@ -157,6 +178,22 @@ export class NativeSubagentRuntime {
157
178
  await this.resumeChild(known, task.taskId, promptText(task.prompt), task.toolUseId ?? undefined, deliver);
158
179
  return;
159
180
  }
181
+ // The SDK starts an agent from before a restart under the id of the
182
+ // SendMessage call that resumed it, as in the RFD form (see `restore`).
183
+ // Its output comes under the id of the call that launched it.
184
+ if (this.form === "air" &&
185
+ task.subagentType &&
186
+ task.toolUseId &&
187
+ this.senderByToolUse.has(task.toolUseId) &&
188
+ !this.children.has(task.taskId)) {
189
+ await this.restoreAgent(task.taskId, deliver, promptText(task.prompt));
190
+ const restored = this.children.get(task.taskId);
191
+ if (restored) {
192
+ this.taskByToolUse.set(task.toolUseId, task.taskId);
193
+ this.childByParentToolUse.set(task.toolUseId, restored);
194
+ return;
195
+ }
196
+ }
160
197
  if (!task.subagentType) {
161
198
  if (task.toolUseId) {
162
199
  this.takePending(task.toolUseId);
@@ -193,13 +230,19 @@ export class NativeSubagentRuntime {
193
230
  * In the RFD form, the running patch only reports that the child runs: its
194
231
  * prompt is a guess (the latest SendMessage to the agent), and the
195
232
  * `task_started` or the SendMessage result carries the delegation.
233
+ *
234
+ * In the AIR form, an agent that the runtime does not know is restored from
235
+ * the session's history (see {@link restoreAgent}).
196
236
  */
197
237
  async taskResumed(taskId, deliver, prompt) {
198
238
  if (!this.enabled)
199
239
  return;
200
240
  const previous = this.children.get(taskId);
201
- if (!previous)
241
+ if (!previous) {
242
+ if (this.form === "air")
243
+ await this.restoreAgent(taskId, deliver, prompt);
202
244
  return;
245
+ }
203
246
  const finishing = this.taskFinishPromises.get(taskId) ?? previous.terminalPromise;
204
247
  if (finishing)
205
248
  await finishing.catch(() => { });
@@ -350,6 +393,7 @@ export class NativeSubagentRuntime {
350
393
  this.identityByToolUse.clear();
351
394
  this.controlByToolUse.clear();
352
395
  this.parentByToolUse.clear();
396
+ this.forgetRestores();
353
397
  }
354
398
  if (errors.length === 1)
355
399
  throw errors[0];
@@ -496,6 +540,105 @@ export class NativeSubagentRuntime {
496
540
  }
497
541
  return this.children.get(agentId);
498
542
  }
543
+ /**
544
+ * The AIR form: whether no call of this process made `launchId`. A new Agent
545
+ * call has a control update before its `task_started`, and a launch that was
546
+ * restored or looked up is registered or remembered.
547
+ */
548
+ isUnseenLaunch(launchId) {
549
+ return (!!this.launchedAgent && !this.unresolvedLaunches.has(launchId) && this.isUnseenCall(launchId));
550
+ }
551
+ /** Whether this runtime has no record of the tool call `toolUseId`. */
552
+ isUnseenCall(toolUseId) {
553
+ return (!this.taskByToolUse.has(toolUseId) &&
554
+ !this.childByParentToolUse.has(toolUseId) &&
555
+ !this.controlByToolUse.has(toolUseId) &&
556
+ !this.parentByToolUse.has(toolUseId) &&
557
+ !this.identityByToolUse.has(toolUseId));
558
+ }
559
+ /**
560
+ * The AIR form: restores the agent that the call `launchId` launched, once.
561
+ * Concurrent callers wait for the same lookup. Without an agent, the updates
562
+ * buffered under `launchId` stay buffered and `launchId` is not looked up again.
563
+ */
564
+ restoreLaunch(launchId, deliver) {
565
+ const existing = this.launchRestores.get(launchId);
566
+ if (existing)
567
+ return existing;
568
+ const epoch = this.restoreEpoch;
569
+ const restoring = (async () => {
570
+ const agentId = await this.launchedAgent?.(launchId).catch((error) => {
571
+ this.logger.log(`Session ${this.rootSessionId}: failed to read the history of launch ${launchId}: ${error}`);
572
+ return undefined;
573
+ });
574
+ if (epoch !== this.restoreEpoch)
575
+ return;
576
+ if (agentId)
577
+ await this.restoreAgent(agentId, deliver);
578
+ if (epoch !== this.restoreEpoch)
579
+ return;
580
+ const child = this.childByParentToolUse.get(launchId);
581
+ if (!child) {
582
+ this.unresolvedLaunches.add(launchId);
583
+ return;
584
+ }
585
+ // Updates that arrived while the generation was announced.
586
+ if (child.announced)
587
+ for (const pending of this.takePending(launchId))
588
+ await deliver(pending);
589
+ })().finally(() => {
590
+ if (this.launchRestores.get(launchId) === restoring)
591
+ this.launchRestores.delete(launchId);
592
+ });
593
+ this.launchRestores.set(launchId, restoring);
594
+ return restoring;
595
+ }
596
+ /**
597
+ * The AIR form: opens a generation for the agent `agentId` that the runtime
598
+ * does not know, because it ran before the session was loaded or resumed.
599
+ * The session's subagent history (see {@link SubagentLineage}) gives the
600
+ * call that launched it: Claude Code sends the resumed run's messages under
601
+ * that call's id, so it is the generation's parent tool use, and the updates
602
+ * buffered under it are delivered. The parent is the agent that made the call
603
+ * if it is live, else the root. An agent that the history cannot place is
604
+ * not restored. Concurrent signals for one agent open one generation.
605
+ */
606
+ restoreAgent(agentId, deliver, prompt) {
607
+ if (!this.lineage || this.children.has(agentId) || this.unrestorableAgents.has(agentId)) {
608
+ return Promise.resolve();
609
+ }
610
+ const existing = this.agentRestores.get(agentId);
611
+ if (existing)
612
+ return existing;
613
+ const lineage = this.lineage;
614
+ const epoch = this.restoreEpoch;
615
+ const restoring = (async () => {
616
+ const entries = await lineage(agentId).catch((error) => {
617
+ this.logger.log(`Session ${this.rootSessionId}: failed to read the history of subagent ${agentId}: ${error}`);
618
+ return [];
619
+ });
620
+ if (epoch !== this.restoreEpoch || this.children.has(agentId))
621
+ return;
622
+ const entry = entries.at(-1);
623
+ if (!entry || entry.agentId !== agentId) {
624
+ this.unrestorableAgents.add(agentId);
625
+ return;
626
+ }
627
+ const parent = entry.parentAgentId ? this.children.get(entry.parentAgentId) : undefined;
628
+ await this.openGeneration(agentId, undefined, {
629
+ parentSessionId: parent && this.isLiveSession(parent.sessionId) ? parent.sessionId : this.rootSessionId,
630
+ parentToolUseId: entry.launchId,
631
+ name: subagentDisplayName(entry.name, entry.description, entry.agentType, agentId),
632
+ task: subagentDescription(undefined, entry.description),
633
+ ...promptField(promptText(prompt)),
634
+ }, true, deliver, true);
635
+ })().finally(() => {
636
+ if (this.agentRestores.get(agentId) === restoring)
637
+ this.agentRestores.delete(agentId);
638
+ });
639
+ this.agentRestores.set(agentId, restoring);
640
+ return restoring;
641
+ }
499
642
  /**
500
643
  * The RFD form: the child runs again, and keeps its session. `toolUseId` is
501
644
  * the call that delegated, usually a SendMessage: the SDK runs the resumed
@@ -668,6 +811,15 @@ export class NativeSubagentRuntime {
668
811
  this.generationByTaskId.clear();
669
812
  this.pending.clear();
670
813
  this.pendingCount = 0;
814
+ this.forgetRestores();
815
+ }
816
+ /** Lets a later resume signal restore an agent again, and look up a launch again. */
817
+ forgetRestores() {
818
+ this.agentRestores.clear();
819
+ this.launchRestores.clear();
820
+ this.unresolvedLaunches.clear();
821
+ this.unrestorableAgents.clear();
822
+ this.restoreEpoch++;
671
823
  }
672
824
  takePending(parentToolUseId) {
673
825
  const updates = this.pending.get(parentToolUseId) ?? [];
@@ -716,9 +868,9 @@ export class NativeSubagentRuntime {
716
868
  * parent tool call. With `announce`, it publishes `subagent_spawned` and
717
869
  * delivers the updates that waited for the child.
718
870
  */
719
- async openGeneration(taskId, previous, fields, announce, deliver) {
871
+ async openGeneration(taskId, previous, fields, announce, deliver, restored = false) {
720
872
  const child = {
721
- sessionId: this.nextChildSessionId(taskId, previous),
873
+ sessionId: this.nextChildSessionId(taskId, previous, restored),
722
874
  ...fields,
723
875
  };
724
876
  const toolUseId = child.parentToolUseId;
@@ -743,8 +895,21 @@ export class NativeSubagentRuntime {
743
895
  * `subagents/` history, so a fork cannot resume an agent of the original.
744
896
  * If a fork ever can, the RFD requires remapping the copied ids, for
745
897
  * example by deriving the child id from the root session id.
898
+ *
899
+ * An agent restored from the history (`restored`) is not on its first
900
+ * generation: before the restart the client already stored ids such as
901
+ * `taskId` and `taskId:generation:2`. Its generation is the current time in
902
+ * milliseconds, so it is `taskId:generation:1760000000000`, and each later
903
+ * resume in this process adds one.
746
904
  */
747
- nextChildSessionId(taskId, previous) {
905
+ nextChildSessionId(taskId, previous, restored = false) {
906
+ if (restored) {
907
+ // A clear() and a new restore within one millisecond must not repeat an id.
908
+ const generation = Math.max(Date.now(), this.lastRestoredGeneration + 1);
909
+ this.lastRestoredGeneration = generation;
910
+ this.generationByTaskId.set(taskId, generation);
911
+ return `${taskId}:generation:${generation}`;
912
+ }
748
913
  if (!previous) {
749
914
  this.generationByTaskId.set(taskId, 1);
750
915
  return taskId;
@@ -34,6 +34,11 @@ export interface SubagentHistory {
34
34
  * The ids are empty for a session without a local transcript.
35
35
  */
36
36
  export declare function subagentHistory(sessionId: string): Promise<SubagentHistory>;
37
+ /**
38
+ * The agent that the Agent or Task call `launchId` launched, from the subagent
39
+ * history of the session `sessionId`.
40
+ */
41
+ export declare function subagentLaunchedBy(sessionId: string, launchId: string): Promise<string | undefined>;
37
42
  /**
38
43
  * The agent `agentId` and its ancestors, root first, from the subagent history
39
44
  * of the session `sessionId`. The runtime restores an agent from it when Claude
@@ -1 +1 @@
1
- {"version":3,"file":"subagent-history.d.ts","sourceRoot":"","sources":["../src/subagent-history.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EAA+B,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAG7F,0DAA0D;AAC1D,MAAM,WAAW,cAAc;IAC7B,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,UAAU,EAAE,OAAO,CAAC;IACpB,mCAAmC;IACnC,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACpC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAwCjF;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE;IAAE,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;CAAE,GAC1C,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAqD/B;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAW5D;AAQD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE;IAAE,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;CAAE,GAC1C,OAAO,CAAC,cAAc,EAAE,CAAC,CAW3B"}
1
+ {"version":3,"file":"subagent-history.d.ts","sourceRoot":"","sources":["../src/subagent-history.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,gCAAgC,CAAC;AAKxC,OAAO,EAA+B,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAG7F,0DAA0D;AAC1D,MAAM,WAAW,cAAc;IAC7B,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,UAAU,EAAE,OAAO,CAAC;IACpB,mCAAmC;IACnC,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACpC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAwCjF;AAOD;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE7B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE;IAAE,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;CAAE,GAC1C,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAmD/B;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAW5D;AAkDD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE;IAAE,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;CAAE,GAC1C,OAAO,CAAC,cAAc,EAAE,CAAC,CAW3B"}
@@ -1,6 +1,8 @@
1
- import { getSessionInfo, getSessionMessages, getSubagentMessages, } from "@anthropic-ai/claude-agent-sdk";
1
+ import { getSessionInfo, getSubagentMessages, } from "@anthropic-ai/claude-agent-sdk";
2
+ import { createReadStream } from "node:fs";
2
3
  import { readdir, readFile } from "node:fs/promises";
3
4
  import path from "node:path";
5
+ import readline from "node:readline";
4
6
  import { isNativeSubagentControlTool } from "./native-subagents.js";
5
7
  import { findTranscript } from "./resumed-session.js";
6
8
  /**
@@ -16,7 +18,7 @@ export async function subagentHistory(sessionId) {
16
18
  const transcript = await findTranscript(sessionId);
17
19
  if (!transcript)
18
20
  return { agents };
19
- const directory = path.join(path.dirname(transcript), sessionId, "subagents");
21
+ const directory = subagentsDirectory(transcript, sessionId);
20
22
  let names;
21
23
  try {
22
24
  names = await readdir(directory);
@@ -53,6 +55,17 @@ export async function subagentHistory(sessionId) {
53
55
  const dir = (await getSessionInfo(sessionId).catch(() => undefined))?.cwd;
54
56
  return dir ? { agents, dir } : { agents };
55
57
  }
58
+ /** The folder of the subagent transcripts, next to the session transcript `transcript`. */
59
+ function subagentsDirectory(transcript, sessionId) {
60
+ return path.join(path.dirname(transcript), sessionId, "subagents");
61
+ }
62
+ /**
63
+ * The agent that the Agent or Task call `launchId` launched, from the subagent
64
+ * history of the session `sessionId`.
65
+ */
66
+ export async function subagentLaunchedBy(sessionId, launchId) {
67
+ return (await subagentHistory(sessionId)).agents.get(launchId)?.agentId;
68
+ }
56
69
  /**
57
70
  * The agent `agentId` and its ancestors, root first, from the subagent history
58
71
  * of the session `sessionId`. The runtime restores an agent from it when Claude
@@ -72,11 +85,12 @@ export async function subagentLineage(sessionId, agentId, logger) {
72
85
  }
73
86
  if (!launchOf.has(agentId))
74
87
  return [];
75
- // Without the project directory, as the replay reads it (`readResumedSession`).
76
- const rootLaunches = new Set(launchIdsIn(await getSessionMessages(sessionId).catch((error) => {
77
- logger.error(`Failed to read the transcript of session ${sessionId}:`, error);
88
+ // The SDK's readers return only the active chain, which starts after the
89
+ // last compaction, so the launch calls come from the transcript files.
90
+ const transcript = await findTranscript(sessionId);
91
+ if (!transcript)
78
92
  return [];
79
- })));
93
+ const rootLaunches = await readLaunchIdsFromTranscript(transcript, null, logger);
80
94
  const agentLaunches = new Map();
81
95
  /** The agent whose transcript has the launch `launchId`, `null` for the root's. */
82
96
  const launcherOf = async (launchId) => {
@@ -85,7 +99,7 @@ export async function subagentLineage(sessionId, agentId, logger) {
85
99
  for (const candidate of launchOf.keys()) {
86
100
  let launches = agentLaunches.get(candidate);
87
101
  if (!launches) {
88
- launches = new Set(launchIdsIn(await readSubagentMessages(sessionId, candidate, history, logger)));
102
+ launches = await readLaunchIdsFromTranscript(path.join(subagentsDirectory(transcript, sessionId), `agent-${candidate}.jsonl`), candidate, logger);
89
103
  agentLaunches.set(candidate, launches);
90
104
  }
91
105
  if (launches.has(launchId))
@@ -127,8 +141,47 @@ export function subagentLaunchIds(content) {
127
141
  ? [block.id]
128
142
  : []);
129
143
  }
130
- function launchIdsIn(messages) {
131
- return messages.flatMap((message) => subagentLaunchIds(message.message?.content));
144
+ /**
145
+ * The ids of the launch calls in the transcript `file`, every record of it,
146
+ * including those before a compaction. `owner` is the agent the transcript
147
+ * belongs to, null for the session's. The session's transcript can hold
148
+ * sidechain records, which are an agent's, not the root's; an agent's
149
+ * transcript keeps only the records of that agent. A file that cannot be read
150
+ * has no launches, so the agent is not placed under the root.
151
+ */
152
+ async function readLaunchIdsFromTranscript(file, owner, logger) {
153
+ const launches = new Set();
154
+ const stream = createReadStream(file, { encoding: "utf8" });
155
+ try {
156
+ for await (const line of readline.createInterface({ input: stream, crlfDelay: Infinity })) {
157
+ if (!line.includes('"tool_use"'))
158
+ continue;
159
+ let record;
160
+ try {
161
+ record = JSON.parse(line);
162
+ }
163
+ catch {
164
+ continue;
165
+ }
166
+ if (record?.type !== "assistant")
167
+ continue;
168
+ if (owner === null
169
+ ? record.isSidechain === true
170
+ : record.agentId !== undefined && record.agentId !== owner) {
171
+ continue;
172
+ }
173
+ for (const id of subagentLaunchIds(record.message?.content))
174
+ launches.add(id);
175
+ }
176
+ }
177
+ catch (error) {
178
+ logger.error(`Failed to read the transcript ${file}:`, error);
179
+ return new Set();
180
+ }
181
+ finally {
182
+ stream.destroy();
183
+ }
184
+ return launches;
132
185
  }
133
186
  /**
134
187
  * The transcript of the subagent `agentId` of a session, or an empty list
@@ -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;AAoB9E;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GACvC,EAAE,CAAC,QAAQ,CA2Eb"}
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;AAoB9E;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GACvC,EAAE,CAAC,QAAQ,CAkFb"}
package/dist/v2/agent.js CHANGED
@@ -50,6 +50,13 @@ export function v2AgentApp(logger, onAgent) {
50
50
  const response = restore.method === "resume"
51
51
  ? await agent.resumeSession(restore.request)
52
52
  : await client.replaying(params.sessionId, () => agent.loadSession(restore.request));
53
+ // v2 requires a snapshot of the foreground state after the replay and
54
+ // before the response, also without a replay: a resumed live session can
55
+ // be running a turn, or waiting on a permission request.
56
+ void client.send({
57
+ sessionId: params.sessionId,
58
+ update: stateSnapshot(agent.foregroundState(params.sessionId)),
59
+ });
53
60
  return {
54
61
  ...v2ResumeSessionResponse(response),
55
62
  ...(await setupCommands(agent, params.sessionId, logger)),
@@ -80,6 +87,14 @@ export function v2AgentApp(logger, onAgent) {
80
87
  }))
81
88
  .onNotification(v2.methods.agent.session.cancel, ({ params }) => agent.cancel(params));
82
89
  }
90
+ /** The `state_update` that reports `state` as the current foreground state. */
91
+ function stateSnapshot(state) {
92
+ if (state === "idle")
93
+ return { sessionUpdate: "state_update", state: "idle" };
94
+ if (state === "running")
95
+ return { sessionUpdate: "state_update", state: "running" };
96
+ return { sessionUpdate: "state_update", state: "requires_action" };
97
+ }
83
98
  /**
84
99
  * The `availableCommands` of a setup response, so a client can show the
85
100
  * session's commands without waiting for an update. The agent sends a v1
@@ -113,6 +128,13 @@ class V2ClientConnection {
113
128
  replays = new Map();
114
129
  /** The session whose replay announced each replaying subagent. */
115
130
  replayRoots = new Map();
131
+ /**
132
+ * The last work state that a replay reported for each subagent, as the
133
+ * `state_update` of the child's own stream. It goes out once the replay
134
+ * ends: v2 forbids replaying a child's past `running` and `idle` as current
135
+ * activity.
136
+ */
137
+ replayedChildStates = new Map();
116
138
  /** The display terminals of the commands that the agent runs. */
117
139
  terminals = new V2Terminals();
118
140
  /** The `_meta` that the client stores for each tool call. */
@@ -128,19 +150,28 @@ class V2ClientConnection {
128
150
  * replayed message, and chunks append, so before the first replayed chunk of
129
151
  * each message the client gets the message with no content, which clears it,
130
152
  * as v2 requires of a message replayed as chunks.
153
+ *
154
+ * Each replayed subagent gets one `state_update` on its own stream, with its
155
+ * last state, after the replay.
131
156
  */
132
157
  async replaying(sessionId, restore) {
133
158
  this.replays.set(sessionId, new Set());
159
+ const children = () => [...this.replayRoots].filter(([, root]) => root === sessionId).map(([child]) => child);
134
160
  try {
135
- return await restore();
161
+ const result = await restore();
162
+ for (const child of children()) {
163
+ const state = this.replayedChildStates.get(child);
164
+ if (state)
165
+ void this.send(state);
166
+ }
167
+ return result;
136
168
  }
137
169
  finally {
138
170
  this.replays.delete(sessionId);
139
- for (const [child, root] of this.replayRoots) {
140
- if (root !== sessionId)
141
- continue;
171
+ for (const child of children()) {
142
172
  this.replays.delete(child);
143
173
  this.replayRoots.delete(child);
174
+ this.replayedChildStates.delete(child);
144
175
  }
145
176
  }
146
177
  }
@@ -178,8 +209,13 @@ class V2ClientConnection {
178
209
  return clear ? [clear, v2Update] : [v2Update];
179
210
  });
180
211
  // A subagent's work state also goes to the child's own stream, after the
181
- // parent's copy.
182
- const childState = v2ChildStateUpdate(update);
212
+ // parent's copy. During a replay, only its last state goes, after the
213
+ // replay (see `replayedChildStates`).
214
+ let childState = v2ChildStateUpdate(update);
215
+ if (childState && this.replays.has(notification.sessionId)) {
216
+ this.replayedChildStates.set(childState.sessionId, childState);
217
+ childState = undefined;
218
+ }
183
219
  // Every update goes to the connection before any is awaited, so an update
184
220
  // that another call sends meanwhile cannot come between them.
185
221
  await Promise.all([
@@ -190,7 +226,8 @@ class V2ClientConnection {
190
226
  /**
191
227
  * The v2 updates of a v1 update: for a tool call report, the updates of its
192
228
  * terminal first (see {@link V2Terminals}), then the report itself, with the
193
- * `_meta` that the client stores (see {@link V2ToolCallMeta}).
229
+ * `_meta` keys that it changes (see {@link V2ToolCallMeta}), and during a
230
+ * replay the `_meta` of a tool call that the report cleared.
194
231
  */
195
232
  v2Updates(sessionId, update) {
196
233
  if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
@@ -201,8 +238,16 @@ class V2ClientConnection {
201
238
  if (!report)
202
239
  return terminal;
203
240
  const starts = report.sessionUpdate === "tool_call";
204
- const reportUpdate = v2SessionUpdate(this.toolCallMeta.apply(sessionId, report, starts));
205
- return [...terminal, ...(reportUpdate ? [reportUpdate] : [])];
241
+ const [reported, metaPatch] = this.toolCallMeta.apply(sessionId, report, starts, this.replays.has(sessionId));
242
+ const reportUpdate = v2SessionUpdate(reported);
243
+ const metaUpdate = metaPatch
244
+ ? v2SessionUpdate({ sessionUpdate: "tool_call_update", ...metaPatch })
245
+ : undefined;
246
+ return [
247
+ ...terminal,
248
+ ...(reportUpdate ? [reportUpdate] : []),
249
+ ...(metaUpdate ? [metaUpdate] : []),
250
+ ];
206
251
  }
207
252
  /**
208
253
  * The update that clears the message of a replayed chunk, when it is the
@@ -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,CAmCpF;AASD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,EAAE,CAAC,kBAAkB,CAuBxF"}
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;AAG/D,0DAA0D;AAC1D,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,EAAE,CAAC,iBAAiB,GAAG,iBAAiB,CAmCpF;AASD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,EAAE,CAAC,kBAAkB,CAuBxF"}
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { PROTOCOL_VERSION as V1_PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
11
11
  import * as v2 from "@agentclientprotocol/sdk/experimental/v2";
12
+ import { SYSTEM_PROMPT_CAPABILITY } from "../client-system-prompt.js";
12
13
  /** The v1 `initialize` request that a v2 client means. */
13
14
  export function v1InitializeRequest(request) {
14
15
  const capabilities = request.capabilities;
@@ -91,6 +92,7 @@ export function v2InitializeResponse(response) {
91
92
  * - The prompt content and MCP transports that the agent's v1 `initialize`
92
93
  * lists: images, embedded context, and HTTP servers. Stdio servers are in the
93
94
  * v1 baseline; v2 has no SSE transport.
95
+ * - `systemPrompt`, in `append` and `override` mode.
94
96
  */
95
97
  const V2_SESSION_CAPABILITIES = {
96
98
  prompt: { image: {}, embeddedContext: {} },
@@ -98,6 +100,7 @@ const V2_SESSION_CAPABILITIES = {
98
100
  delete: {},
99
101
  fork: {},
100
102
  additionalDirectories: {},
103
+ systemPrompt: SYSTEM_PROMPT_CAPABILITY,
101
104
  };
102
105
  /** v2 names the id of a method `methodId`, requires `type`, and lists `env` as name and value pairs. */
103
106
  function v2AuthMethod(method) {
@@ -4,7 +4,7 @@
4
4
  * v2 answers the prompt once Claude Code takes it in, with the id of the user
5
5
  * message it became, and reports the rest of the turn as `state_update`s.
6
6
  */
7
- import { type PromptRequest } from "@agentclientprotocol/sdk";
7
+ import { type ContentBlock, type PromptRequest } from "@agentclientprotocol/sdk";
8
8
  import * as v2 from "@agentclientprotocol/sdk/experimental/v2";
9
9
  import type { ClaudeAcpAgent } from "../acp-agent.js";
10
10
  import type { ForegroundEvents } from "../turn-events.js";
@@ -50,4 +50,5 @@ export declare function v2Foreground(send: (update: v2.SessionUpdate) => void):
50
50
  * advertised, so such a block is rejected rather than silently dropped.
51
51
  */
52
52
  export declare function v1PromptRequest({ prompt, ...request }: v2.PromptRequest): PromptRequest;
53
+ export declare function v1ContentBlock(block: v2.ContentBlock): ContentBlock;
53
54
  //# sourceMappingURL=prompt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/v2/prompt.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAIL,KAAK,aAAa,EAEnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAuC,MAAM,mBAAmB,CAAC;AAE/F;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,EAAE,CAAC,aAAa,EACxB,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,aAAa,KAAK,IAAI,GACvC,OAAO,CAAC,EAAE,CAAC,cAAc,CAAC,CAiC5B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,aAAa,KAAK,IAAI,GAAG,gBAAgB,CAoBvF;AAgCD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,CAAC,aAAa,GAAG,aAAa,CAEvF"}
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/v2/prompt.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,aAAa,EAEnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAuC,MAAM,mBAAmB,CAAC;AAE/F;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,EAAE,CAAC,aAAa,EACxB,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,aAAa,KAAK,IAAI,GACvC,OAAO,CAAC,EAAE,CAAC,cAAc,CAAC,CAiC5B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,aAAa,KAAK,IAAI,GAAG,gBAAgB,CAoBvF;AAgCD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,CAAC,aAAa,GAAG,aAAa,CAEvF;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,GAAG,YAAY,CAenE"}
package/dist/v2/prompt.js CHANGED
@@ -129,7 +129,7 @@ function idle({ stopReason, usage, _meta }) {
129
129
  export function v1PromptRequest({ prompt, ...request }) {
130
130
  return { ...request, prompt: prompt.map(v1ContentBlock) };
131
131
  }
132
- function v1ContentBlock(block) {
132
+ export function v1ContentBlock(block) {
133
133
  if (v2.ContentBlock.isText(block) ||
134
134
  v2.ContentBlock.isImage(block) ||
135
135
  v2.ContentBlock.isAudio(block) ||
@@ -1 +1 @@
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;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,EAAE,CAAC,kBAAkB,GAAG,kBAAkB,CAGtF;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,EAEnC,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAG/D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,EAAE,CAAC,iBAAiB,GAAG,iBAAiB,CAQnF;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,CAmBlD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,EAAE,CAAC,kBAAkB,GAAG,kBAAkB,CAOtF;AAaD,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"}
@@ -5,10 +5,15 @@
5
5
  */
6
6
  import { RequestError, } from "@agentclientprotocol/sdk";
7
7
  import * as v2 from "@agentclientprotocol/sdk/experimental/v2";
8
+ import { v1ContentBlock } from "./prompt.js";
8
9
  export function v1NewSessionRequest(params) {
9
- const { mcpServers, ...request } = params;
10
+ const { mcpServers, systemPrompt, ...request } = params;
10
11
  // v1 requires the list; v2 treats an omitted list as an empty one.
11
- return { ...request, mcpServers: (mcpServers ?? []).map(v1McpServer) };
12
+ return {
13
+ ...request,
14
+ mcpServers: (mcpServers ?? []).map(v1McpServer),
15
+ ...v1SystemPromptField(systemPrompt),
16
+ };
12
17
  }
13
18
  /**
14
19
  * The v1 request that a v2 `session/resume` means.
@@ -20,7 +25,8 @@ export function v1NewSessionRequest(params) {
20
25
  * starts elsewhere is rejected.
21
26
  */
22
27
  export function v1RestoreSessionRequest(params) {
23
- const { replayFrom, mcpServers, ...request } = params;
28
+ const { replayFrom, mcpServers, systemPrompt, ...rest } = params;
29
+ const request = { ...rest, ...v1SystemPromptField(systemPrompt) };
24
30
  if (replayFrom == null) {
25
31
  return {
26
32
  method: "resume",
@@ -40,8 +46,21 @@ export function v1RestoreSessionRequest(params) {
40
46
  * returns a session that the client can prompt, with its config options.
41
47
  */
42
48
  export function v1ForkSessionRequest(params) {
43
- const { mcpServers, ...request } = params;
44
- return { ...request, ...(mcpServers ? { mcpServers: mcpServers.map(v1McpServer) } : {}) };
49
+ const { mcpServers, systemPrompt, ...request } = params;
50
+ return {
51
+ ...request,
52
+ ...(mcpServers ? { mcpServers: mcpServers.map(v1McpServer) } : {}),
53
+ ...v1SystemPromptField(systemPrompt),
54
+ };
55
+ }
56
+ /**
57
+ * The v1 `systemPrompt`. Its content converts like prompt content: a v2
58
+ * annotation audience can be any string, and v1 takes only `user` or `assistant`.
59
+ */
60
+ function v1SystemPromptField(systemPrompt) {
61
+ if (systemPrompt == null)
62
+ return {};
63
+ return { systemPrompt: { ...systemPrompt, content: systemPrompt.content.map(v1ContentBlock) } };
45
64
  }
46
65
  /** v2 drops `modes`: the session mode is the `mode` config option, which the agent always lists. */
47
66
  export function v2NewSessionResponse(response) {