@agentclientprotocol/claude-agent-acp 0.100.1-preview.1 → 0.100.1-preview.2

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.
@@ -29,173 +29,351 @@
29
29
  import { RequestError } from "@agentclientprotocol/sdk/unstable";
30
30
  import { V2_CANCELLED, V2_PAUSED } from "./tool-calls/renderer.js";
31
31
  import { BackgroundOutputTail } from "./background-output.js";
32
+ /** The most returned tool calls that {@link SessionBackgroundWork} remembers. */
33
+ const MAX_RETURNED_CALLS = 1024;
32
34
  /** The tools whose structured result names the task that the call started. */
33
35
  const TASK_TOOLS = new Set(["Monitor", "Workflow"]);
34
36
  /**
35
- * Registers each tool call of a tool result message whose task goes on in
36
- * the background: a `Monitor` or `Workflow` call, whose structured result
37
- * names its task, or a call that a live task names as its tool use, such as
38
- * a backgrounded MCP call. The task must be in the level: one that ended
39
- * already, or that Claude Code marks `ambient`, leaves its call as it is.
40
- * Returns the ids of the calls, whose result then reports `in_progress` (see
41
- * `withRunningToolCall`).
37
+ * The background work of one root session, and of its subagent sessions: the
38
+ * terminals and tool calls of the tasks that run in the background, and what
39
+ * the client is told of them.
40
+ *
41
+ * Terminals are kept for every client that renders them, as before the
42
+ * background lists. The rest is kept only when `reported`.
42
43
  */
43
- export function registerBackgroundToolCalls(session, content, toolUseResult, toolUses, parentToolUseId) {
44
- const registered = new Set();
45
- if (!session.reportsBackground || !Array.isArray(content))
46
- return registered;
47
- const results = content.filter((block) => typeof block === "object" &&
48
- block !== null &&
49
- block.type === "tool_result" &&
50
- typeof block.tool_use_id === "string" &&
51
- block.is_error !== true);
52
- const structuredTaskId = results.length === 1 &&
53
- typeof toolUseResult === "object" &&
54
- toolUseResult !== null &&
55
- typeof toolUseResult.taskId === "string"
56
- ? toolUseResult.taskId
57
- : undefined;
58
- for (const { tool_use_id: toolCallId } of results) {
59
- const toolUse = toolUses[toolCallId];
60
- if (!toolUse)
61
- continue;
62
- const taskId = (TASK_TOOLS.has(toolUse.name) ? structuredTaskId : undefined) ??
63
- [...(session.liveBackgroundTasks ?? [])].find(([, task]) => task.parentToolUseId === toolCallId && !task.isSubagent)?.[0];
64
- const task = taskId === undefined ? undefined : session.backgroundLevel?.get(taskId);
65
- if (!task ||
66
- task.ambient ||
67
- task.taskType === "local_bash" ||
68
- task.taskType === "local_agent") {
69
- continue;
44
+ export class SessionBackgroundWork {
45
+ sessionId;
46
+ reported;
47
+ /** Claude Code's live background task set, from its last `background_tasks_changed`. */
48
+ level = new Map();
49
+ /** The terminals of the commands that went to the background, by task id. */
50
+ terminals = new Map();
51
+ /** The tool calls whose tasks went on in the background, by task id. */
52
+ toolCalls = new Map();
53
+ /**
54
+ * The tool calls that returned, oldest first. A task that a call started
55
+ * is part of that call until it returns, and the foreground work waits on
56
+ * it. The task's `task_started` can come after the call returned. Bounded
57
+ * by {@link MAX_RETURNED_CALLS}: a task joins the level around the time its
58
+ * call returns.
59
+ */
60
+ returned = new Set();
61
+ /**
62
+ * `reported`: whether the client gets the lists. Only when Claude Code's
63
+ * interrupt spares background tasks, as `perTaskStopAffordance` makes it.
64
+ */
65
+ constructor(sessionId, reported) {
66
+ this.sessionId = sessionId;
67
+ this.reported = reported;
68
+ }
69
+ /**
70
+ * Keeps the display terminal of the command `taskId` running after its Bash
71
+ * result, until its task ends (see `withRunningTerminal`).
72
+ */
73
+ terminalStarted(taskId, terminal) {
74
+ this.terminals.set(taskId, terminal);
75
+ }
76
+ /**
77
+ * Reads what a tool result message tells of the background tasks:
78
+ *
79
+ * - The returned calls no longer hold the tasks that they started.
80
+ * - Each call whose task goes on in the background is registered: a
81
+ * `Monitor` or `Workflow` call, whose structured result names its task,
82
+ * or a call that a live task names as its tool use, such as a
83
+ * backgrounded MCP call. The task must be in the level: one that ended
84
+ * already, or that Claude Code marks `ambient`, leaves its call as it is.
85
+ *
86
+ * Returns the ids of the registered calls, whose result then reports
87
+ * `in_progress` (see `withRunningToolCall`).
88
+ */
89
+ toolResults(owner, content, toolUseResult, toolUses, parentToolUseId) {
90
+ const registered = new Set();
91
+ if (!this.reported || !Array.isArray(content))
92
+ return registered;
93
+ const results = content.filter((block) => typeof block === "object" &&
94
+ block !== null &&
95
+ block.type === "tool_result" &&
96
+ typeof block.tool_use_id === "string");
97
+ for (const { tool_use_id: toolCallId } of results) {
98
+ this.returned.add(toolCallId);
99
+ if (this.returned.size > MAX_RETURNED_CALLS) {
100
+ this.returned.delete(this.returned.values().next().value);
101
+ }
70
102
  }
71
- (session.backgroundToolCalls ??= new Map()).set(taskId, {
72
- toolCallId,
73
- toolUse,
74
- ...(parentToolUseId ? { parentToolUseId } : {}),
75
- });
76
- registered.add(toolCallId);
103
+ const succeeded = results.filter((block) => block.is_error !== true);
104
+ const structuredTaskId = succeeded.length === 1 &&
105
+ typeof toolUseResult === "object" &&
106
+ toolUseResult !== null &&
107
+ typeof toolUseResult.taskId === "string"
108
+ ? toolUseResult.taskId
109
+ : undefined;
110
+ for (const { tool_use_id: toolCallId } of succeeded) {
111
+ const toolUse = toolUses[toolCallId];
112
+ if (!toolUse)
113
+ continue;
114
+ const taskId = (TASK_TOOLS.has(toolUse.name) ? structuredTaskId : undefined) ??
115
+ [...owner.liveBackgroundTasks].find(([, task]) => task.parentToolUseId === toolCallId && !task.isSubagent)?.[0];
116
+ const task = taskId === undefined ? undefined : this.level.get(taskId);
117
+ if (!task ||
118
+ task.ambient ||
119
+ task.taskType === "local_bash" ||
120
+ task.taskType === "local_agent") {
121
+ continue;
122
+ }
123
+ this.toolCalls.set(taskId, {
124
+ toolCallId,
125
+ toolUse,
126
+ ...(parentToolUseId ? { parentToolUseId } : {}),
127
+ });
128
+ registered.add(toolCallId);
129
+ }
130
+ return registered;
77
131
  }
78
- return registered;
79
- }
80
- /** The level of a `background_tasks_changed` payload. */
81
- export function backgroundLevel(tasks) {
82
- return new Map(tasks.map((task) => [
83
- task.task_id,
84
- {
85
- ambient: task.ambient === true,
86
- taskType: task.task_type,
87
- description: task.description,
88
- ...(task.parent_task_id !== undefined ? { parentTaskId: task.parent_task_id } : {}),
89
- },
90
- ]));
91
- }
92
- /**
93
- * The `background` list of the root session `rootSessionId` and of each of
94
- * its announced subagent sessions, by session id, deeper sessions first and
95
- * the root last. Each list is empty while
96
- * the level has not been reported: Claude Code sends none before the first
97
- * background task starts.
98
- */
99
- export function backgroundLists(session, rootSessionId) {
100
- const children = [...(session.nativeSubagentsByTaskId ?? [])].filter(([, child]) => child.announced);
101
- const childByTask = new Map(children);
102
- const childByToolUse = new Map(children.flatMap(([, child]) => child.parentToolUseId ? [[child.parentToolUseId, child]] : []));
103
- const lists = new Map([
104
- [rootSessionId, []],
105
- ...children.map(([, child]) => [child.sessionId, []]),
106
- ]);
107
- const level = session.backgroundLevel ?? new Map();
108
- for (const [taskId, task] of level) {
109
- if (task.ambient)
110
- continue;
111
- // A subagent's command or tool call runs on the child's stream.
112
- const ownerOf = (parentToolUseId) => parentToolUseId ? childByToolUse.get(parentToolUseId)?.sessionId : rootSessionId;
113
- const terminal = session.backgroundTerminals?.get(taskId);
132
+ /**
133
+ * Starts reading the output of the background command `taskId` into its
134
+ * terminal, when the client gets the lists and the Bash result named the
135
+ * command's output file. `send` sends an update of the terminal's tool
136
+ * call, routed as the call's other updates. Called once the Bash result's
137
+ * own updates are sent: the first output replaces the placeholder that they
138
+ * gave the terminal.
139
+ */
140
+ followOutput(taskId, send) {
141
+ const terminal = this.terminals.get(taskId);
142
+ if (!this.reported || !terminal?.outputFilePath || terminal.output)
143
+ return;
144
+ terminal.output = new BackgroundOutputTail(terminal.outputFilePath, (output) => send(terminalOutputUpdate(this.sessionId, terminal, output)));
145
+ }
146
+ /**
147
+ * The task `taskId` ended. Reads the rest of its command's output, then
148
+ * exits its terminal, and gives a tool call whose task it was its final
149
+ * status: `cancelled` for a task that Claude Code stopped, and `completed`
150
+ * for one that ended without saying how, such as one that the level
151
+ * dropped: it no longer runs, and reported no failure. Claude Code reports
152
+ * no exit code for a command, and `completed` does not mean 0: Claude Code
153
+ * accepts some non-zero codes as a success, such as grep's 1.
154
+ */
155
+ async taskEnded(taskId, end, send) {
156
+ const terminal = this.terminals.get(taskId);
114
157
  if (terminal) {
115
- const owner = ownerOf(terminal.parentToolUseId);
116
- if (owner)
117
- lists.get(owner).push({ type: "terminal", terminalId: terminal.toolCallId });
118
- continue;
158
+ await terminal.output?.finish();
159
+ this.terminals.delete(taskId);
160
+ await send(terminalExit(this.sessionId, terminal));
119
161
  }
120
- const toolCall = session.backgroundToolCalls?.get(taskId);
162
+ const toolCall = this.toolCalls.get(taskId);
121
163
  if (toolCall) {
122
- const owner = ownerOf(toolCall.parentToolUseId);
123
- if (owner)
124
- lists.get(owner).push({ type: "tool_call", toolCallId: toolCall.toolCallId });
125
- continue;
164
+ this.toolCalls.delete(taskId);
165
+ await send(toolCallStatusUpdate(this.sessionId, toolCall, end === "stopped" ? V2_CANCELLED : end === "failed" ? "failed" : "completed"));
126
166
  }
127
- // A command without a terminal yet is one that foreground work waits on.
128
- // A subagent is listed below, as its session.
129
- if (task.taskType === "local_bash" || task.taskType === "local_agent")
130
- continue;
131
- // A task joins the level before its `task_started`, and before the tool
132
- // call that starts it returns. Until both, the task is unknown or part of
133
- // that tool call, which the foreground work waits on. Claude Code drops a
134
- // tool use from its cache at the tool's result.
135
- const live = session.liveBackgroundTasks?.get(taskId);
136
- if (!live || (live.parentToolUseId && session.toolUseCache?.[live.parentToolUseId])) {
137
- continue;
167
+ }
168
+ /**
169
+ * The update that reports the background tool call of the task `taskId` as
170
+ * `paused`, or as `in_progress` again, when `paused` changes it. Claude Code
171
+ * pauses a workflow this way.
172
+ */
173
+ taskPaused(taskId, paused) {
174
+ const toolCall = this.toolCalls.get(taskId);
175
+ if (!toolCall || (toolCall.paused ?? false) === paused)
176
+ return undefined;
177
+ toolCall.paused = paused;
178
+ return toolCallStatusUpdate(this.sessionId, toolCall, paused ? V2_PAUSED : "in_progress");
179
+ }
180
+ /**
181
+ * Takes Claude Code's new live background task set. The terminals and tool
182
+ * calls whose tasks it no longer holds end first, before a list drops them,
183
+ * as v2 requires. A terminal or tool call is registered at its tool result,
184
+ * after its task joined the level, so a later level without the task means
185
+ * that it ended. Claude Code usually sends a task's end before that level,
186
+ * which ends them first, but the SDK does not promise that order.
187
+ */
188
+ async levelChanged(tasks, send) {
189
+ if (!this.reported)
190
+ return;
191
+ const level = new Map(tasks.map((task) => [
192
+ task.task_id,
193
+ {
194
+ ambient: task.ambient === true,
195
+ taskType: task.task_type,
196
+ description: task.description,
197
+ ...(task.parent_task_id !== undefined ? { parentTaskId: task.parent_task_id } : {}),
198
+ },
199
+ ]));
200
+ for (const taskId of [...this.terminals.keys(), ...this.toolCalls.keys()]) {
201
+ if (!level.has(taskId))
202
+ await this.taskEnded(taskId, undefined, send);
138
203
  }
139
- const parent = task.parentTaskId ? childByTask.get(task.parentTaskId) : undefined;
140
- lists
141
- .get(parent?.sessionId ?? rootSessionId)
142
- .push({ type: "_claude/task", taskId, description: task.description });
204
+ this.level = level;
143
205
  }
144
- // A child is listed in its parent's list while it works in the background,
145
- // and while it is idle with background work of its own. Deeper children
146
- // first, so a child's list is whole before its parent's reads it.
147
- const sessions = new Map(children.map(([, child]) => [child.sessionId, child]));
148
- const depth = (child) => {
149
- const parent = sessions.get(child.parentSessionId);
150
- return parent ? depth(parent) + 1 : 0;
151
- };
152
- for (const [taskId, child] of children.sort(([, a], [, b]) => depth(b) - depth(a))) {
153
- const task = level.get(taskId);
154
- const working = child.workState !== undefined &&
155
- child.workState !== "idle" &&
156
- task !== undefined &&
157
- !task.ambient;
158
- if (working || lists.get(child.sessionId).length > 0) {
159
- lists.get(child.parentSessionId)?.push({ type: "subagent", sessionId: child.sessionId });
206
+ /**
207
+ * The query closed: Claude Code's background tasks end with its process.
208
+ * Stops reading output, and returns the updates that exit the root
209
+ * session's terminals and cancel its tool calls. The level empties, so the
210
+ * next lists are empty. A subagent's command or tool call is left as it
211
+ * was: it is on the child's stream, which the closed query's routing owned.
212
+ */
213
+ queryClosed() {
214
+ for (const terminal of this.terminals.values())
215
+ terminal.output?.stop();
216
+ if (!this.reported)
217
+ return [];
218
+ const updates = [];
219
+ for (const [taskId, terminal] of this.terminals) {
220
+ if (terminal.parentToolUseId)
221
+ continue;
222
+ this.terminals.delete(taskId);
223
+ updates.push(terminalExit(this.sessionId, terminal));
224
+ }
225
+ for (const [taskId, toolCall] of this.toolCalls) {
226
+ if (toolCall.parentToolUseId)
227
+ continue;
228
+ this.toolCalls.delete(taskId);
229
+ updates.push(toolCallStatusUpdate(this.sessionId, toolCall, V2_CANCELLED));
160
230
  }
231
+ this.level = new Map();
232
+ this.returned.clear();
233
+ return updates;
234
+ }
235
+ /**
236
+ * The `background` list of the root session and of each of its announced
237
+ * subagent sessions, with what their repeated states need, deeper sessions
238
+ * first and the root last: a child's own list goes out before its parent's
239
+ * list drops it. Each list is empty while the level has not been reported:
240
+ * Claude Code sends none before the first background task starts.
241
+ */
242
+ reports(owner, rootState) {
243
+ const children = [...(owner.nativeSubagentsByTaskId ?? [])].filter(([, child]) => child.announced);
244
+ const childByTask = new Map(children);
245
+ const childByToolUse = new Map(children.flatMap(([, child]) => child.parentToolUseId ? [[child.parentToolUseId, child]] : []));
246
+ const lists = new Map([
247
+ [this.sessionId, []],
248
+ ...children.map(([, child]) => [child.sessionId, []]),
249
+ ]);
250
+ // A subagent's command or tool call runs on the child's stream.
251
+ const ownerOf = (parentToolUseId) => parentToolUseId ? childByToolUse.get(parentToolUseId)?.sessionId : this.sessionId;
252
+ for (const [taskId, task] of this.level) {
253
+ if (task.ambient)
254
+ continue;
255
+ const terminal = this.terminals.get(taskId);
256
+ if (terminal) {
257
+ const session = ownerOf(terminal.parentToolUseId);
258
+ if (session)
259
+ lists.get(session).push({ type: "terminal", terminalId: terminal.toolCallId });
260
+ continue;
261
+ }
262
+ const toolCall = this.toolCalls.get(taskId);
263
+ if (toolCall) {
264
+ const session = ownerOf(toolCall.parentToolUseId);
265
+ if (session)
266
+ lists.get(session).push({ type: "tool_call", toolCallId: toolCall.toolCallId });
267
+ continue;
268
+ }
269
+ // A command without a terminal yet is one that foreground work waits on.
270
+ // A subagent is listed below, as its session.
271
+ if (task.taskType === "local_bash" || task.taskType === "local_agent")
272
+ continue;
273
+ // A task joins the level before its `task_started`, and before the tool
274
+ // call that starts it returns. Until both, the task is unknown or part
275
+ // of that tool call.
276
+ const live = owner.liveBackgroundTasks.get(taskId);
277
+ if (!live || (live.parentToolUseId && !this.returned.has(live.parentToolUseId)))
278
+ continue;
279
+ const parent = task.parentTaskId ? childByTask.get(task.parentTaskId) : undefined;
280
+ lists
281
+ .get(parent?.sessionId ?? this.sessionId)
282
+ .push({ type: "_claude/task", taskId, description: task.description });
283
+ }
284
+ // A child is listed in its parent's list while it works in the background,
285
+ // and while it is idle with background work of its own. Deeper children
286
+ // first, so a child's list is whole before its parent's reads it.
287
+ const sessions = new Map(children.map(([, child]) => [child.sessionId, child]));
288
+ const depth = (child) => {
289
+ const parent = sessions.get(child.parentSessionId);
290
+ return parent ? depth(parent) + 1 : 0;
291
+ };
292
+ const ordered = children.sort(([, a], [, b]) => depth(b) - depth(a));
293
+ for (const [taskId, child] of ordered) {
294
+ const task = this.level.get(taskId);
295
+ const working = child.workState !== undefined &&
296
+ child.workState !== "idle" &&
297
+ task !== undefined &&
298
+ !task.ambient;
299
+ if (working || lists.get(child.sessionId).length > 0) {
300
+ lists.get(child.parentSessionId)?.push({ type: "subagent", sessionId: child.sessionId });
301
+ }
302
+ }
303
+ return [
304
+ ...ordered.map(([, child]) => ({
305
+ sessionId: child.sessionId,
306
+ items: lists.get(child.sessionId),
307
+ state: workStateName(child.workState),
308
+ parentSessionId: child.parentSessionId,
309
+ })),
310
+ { sessionId: this.sessionId, items: lists.get(this.sessionId), state: rootState },
311
+ ];
312
+ }
313
+ /**
314
+ * The current state of each background tool call in `toolCallIds`: the
315
+ * call as `renderer` shows its tool use, with its status now (see
316
+ * `ClaudeAcpAgent.publishBackgroundToolCalls`).
317
+ */
318
+ toolCallStates(toolCallIds, renderer, cwd) {
319
+ return [...this.toolCalls.values()]
320
+ .filter((toolCall) => toolCallIds.includes(toolCall.toolCallId))
321
+ .map((toolCall) => {
322
+ const update = renderer.toolCall({ id: toolCall.toolCallId, ...toolCall.toolUse }, { cwd });
323
+ const status = toolCall.paused ? V2_PAUSED : "in_progress";
324
+ return {
325
+ sessionId: this.sessionId,
326
+ update: { ...update, status },
327
+ };
328
+ });
329
+ }
330
+ /**
331
+ * The task of the terminal or tool call `target` that the session
332
+ * `sessionId` lists, or undefined when it lists no such item.
333
+ */
334
+ listedTask(owner, sessionId, target) {
335
+ if (!this.reported)
336
+ return undefined;
337
+ const listed = this.reports(owner, "idle")
338
+ .find((report) => report.sessionId === sessionId)
339
+ ?.items.some((item) => "terminalId" in target
340
+ ? item.type === "terminal" && item.terminalId === target.terminalId
341
+ : item.type === "tool_call" && item.toolCallId === target.toolCallId);
342
+ if (!listed)
343
+ return undefined;
344
+ const id = "terminalId" in target ? target.terminalId : target.toolCallId;
345
+ const entries = "terminalId" in target ? this.terminals : this.toolCalls;
346
+ return [...entries].find(([, entry]) => entry.toolCallId === id)?.[0];
161
347
  }
162
- // In the same order, the root last: a child's own list goes out before its
163
- // parent's list drops it.
164
- return new Map([
165
- ...children.map(([, child]) => [
166
- child.sessionId,
167
- lists.get(child.sessionId),
168
- ]),
169
- [rootSessionId, lists.get(rootSessionId)],
170
- ]);
171
- }
172
- /**
173
- * The tasks of the terminals and tool calls that `level` does not hold:
174
- * Claude Code no longer runs them, so they end before a list drops them, as
175
- * v2 requires. A terminal or tool call is registered at its tool result,
176
- * after its task joined the level, so a later level without the task means
177
- * that it ended. Claude Code usually sends a task's end before that level,
178
- * which ends them first, but the SDK does not promise that order.
179
- */
180
- export function droppedTasks(session, level) {
181
- return [
182
- ...(session.backgroundTerminals?.keys() ?? []),
183
- ...(session.backgroundToolCalls?.keys() ?? []),
184
- ].filter((taskId) => !level.has(taskId));
185
348
  }
186
349
  /**
187
- * Starts reading the output of the background command `taskId` into its
188
- * terminal, when the session reports its background work and the Bash result
189
- * named the command's output file. `send` sends an update of the terminal's
190
- * tool call, routed as the call's other updates. Called once the Bash
191
- * result's own updates are sent: the first output replaces the placeholder
192
- * that they gave the terminal.
350
+ * Stops the task of a terminal or tool call that the session `sessionId`
351
+ * lists (v2 `terminal/cancel` and `tool_call/cancel`). `sessions` are the
352
+ * root sessions: a subagent session's list is built with its root's. The
353
+ * terminal exits, or the tool call gets its final status, when Claude Code
354
+ * reports the task stopped.
193
355
  */
194
- export function followBackgroundTerminalOutput(session, sessionId, taskId, send) {
195
- const terminal = session.backgroundTerminals?.get(taskId);
196
- if (!session.reportsBackground || !terminal?.outputFilePath || terminal.output)
356
+ export async function cancelBackgroundWork(sessions, sessionId, target) {
357
+ const notCancellable = () => "terminalId" in target
358
+ ? RequestError.invalidParams({ sessionId, terminalId: target.terminalId }, `Terminal ${target.terminalId} is not a command running in the background, so it cannot be cancelled.`)
359
+ : RequestError.invalidParams({ sessionId, toolCallId: target.toolCallId }, `Tool call ${target.toolCallId} is not running in the background, so it cannot be cancelled.`);
360
+ for (const session of sessions) {
361
+ const taskId = session.background?.listedTask(session, sessionId, target);
362
+ if (taskId === undefined)
363
+ continue;
364
+ try {
365
+ await session.query.stopTask(taskId);
366
+ }
367
+ catch (error) {
368
+ // The task ended meanwhile: it has ended on the client too, or is about to.
369
+ if (session.background?.listedTask(session, sessionId, target) === undefined) {
370
+ throw notCancellable();
371
+ }
372
+ throw error;
373
+ }
197
374
  return;
198
- terminal.output = new BackgroundOutputTail(terminal.outputFilePath, (output) => send(terminalOutputUpdate(sessionId, terminal, output)));
375
+ }
376
+ throw notCancellable();
199
377
  }
200
378
  /** The update that gives the terminal of `terminal` the text `output`. */
201
379
  function terminalOutputUpdate(sessionId, terminal, output) {
@@ -214,27 +392,8 @@ function terminalOutputUpdate(sessionId, terminal, output) {
214
392
  },
215
393
  };
216
394
  }
217
- /**
218
- * Reads the rest of the output of the background command `taskId` into its
219
- * terminal: the command ended, and its output goes before its exit, as v2
220
- * asks.
221
- */
222
- export async function finishBackgroundTerminalOutput(session, taskId) {
223
- await session.backgroundTerminals?.get(taskId)?.output?.finish();
224
- }
225
- /**
226
- * Removes the terminal of the background command `taskId` of the session
227
- * `sessionId`, and returns the update that exits it, or undefined when the
228
- * command has no terminal. Claude Code reports no exit code for it, and
229
- * `completed` does not mean 0: Claude Code accepts some non-zero codes as a
230
- * success, such as grep's 1.
231
- */
232
- export function takeBackgroundTerminalExit(session, sessionId, taskId) {
233
- const terminal = session.backgroundTerminals?.get(taskId);
234
- if (!terminal)
235
- return undefined;
236
- terminal.output?.stop();
237
- session.backgroundTerminals.delete(taskId);
395
+ /** The update that exits the terminal of `terminal`, with no exit code. */
396
+ function terminalExit(sessionId, terminal) {
238
397
  return {
239
398
  sessionId,
240
399
  update: {
@@ -249,46 +408,6 @@ export function takeBackgroundTerminalExit(session, sessionId, taskId) {
249
408
  },
250
409
  };
251
410
  }
252
- /**
253
- * Removes the background tool call of the task `taskId` of the session
254
- * `sessionId`, and returns the update that gives it its final status, or
255
- * undefined when the task has no such call. A task that Claude Code stopped
256
- * is `cancelled`. One that ended without saying how, such as one that the
257
- * level dropped, is `completed`: it no longer runs, and reported no failure.
258
- */
259
- export function takeBackgroundToolCallEnd(session, sessionId, taskId, end) {
260
- const toolCall = session.backgroundToolCalls?.get(taskId);
261
- if (!toolCall)
262
- return undefined;
263
- session.backgroundToolCalls.delete(taskId);
264
- return toolCallStatusUpdate(sessionId, toolCall, end === "stopped" ? V2_CANCELLED : end === "failed" ? "failed" : "completed");
265
- }
266
- /**
267
- * The update that reports the background tool call of the task `taskId` as
268
- * `paused`, or as `in_progress` again, when `paused` changes it. Claude Code
269
- * pauses a workflow this way.
270
- */
271
- export function backgroundToolCallPause(session, sessionId, taskId, paused) {
272
- const toolCall = session.backgroundToolCalls?.get(taskId);
273
- if (!toolCall || (toolCall.paused ?? false) === paused)
274
- return undefined;
275
- toolCall.paused = paused;
276
- return toolCallStatusUpdate(sessionId, toolCall, paused ? V2_PAUSED : "in_progress");
277
- }
278
- /**
279
- * The current state of each background tool call in `toolCallIds` of the
280
- * root session `sessionId`: the call as `renderer` shows its tool use, with
281
- * its status now (see `ClaudeAcpAgent.publishBackgroundToolCalls`).
282
- */
283
- export function backgroundToolCallStates(session, sessionId, toolCallIds, renderer) {
284
- return [...(session.backgroundToolCalls?.values() ?? [])]
285
- .filter((toolCall) => toolCallIds.includes(toolCall.toolCallId))
286
- .map((toolCall) => {
287
- const update = renderer.toolCall({ id: toolCall.toolCallId, ...toolCall.toolUse }, { cwd: session.cwd });
288
- const status = toolCall.paused ? V2_PAUSED : "in_progress";
289
- return { sessionId, update: { ...update, status } };
290
- });
291
- }
292
411
  function toolCallStatusUpdate(sessionId, toolCall, status) {
293
412
  return {
294
413
  sessionId,
@@ -302,69 +421,6 @@ function toolCallStatusUpdate(sessionId, toolCall, status) {
302
421
  },
303
422
  };
304
423
  }
305
- /**
306
- * Ends the background work of a session whose query closed: Claude Code's
307
- * background tasks end with its process. Returns the updates that exit the
308
- * root session's terminals and cancel its tool calls, and empties the level,
309
- * so the next lists are empty. A subagent's command or tool call is left as
310
- * it was: it is on the child's stream, which the closed query's routing
311
- * owned.
312
- */
313
- export function endBackgroundWork(session, sessionId) {
314
- if (!session.reportsBackground)
315
- return [];
316
- // No command writes output any more.
317
- for (const terminal of session.backgroundTerminals?.values() ?? [])
318
- terminal.output?.stop();
319
- const exits = [...(session.backgroundTerminals ?? [])].flatMap(([taskId, terminal]) => {
320
- const exit = terminal.parentToolUseId
321
- ? undefined
322
- : takeBackgroundTerminalExit(session, sessionId, taskId);
323
- return exit ? [exit] : [];
324
- });
325
- const ends = [...(session.backgroundToolCalls ?? [])].flatMap(([taskId, toolCall]) => {
326
- const end = toolCall.parentToolUseId
327
- ? undefined
328
- : takeBackgroundToolCallEnd(session, sessionId, taskId, "stopped");
329
- return end ? [end] : [];
330
- });
331
- session.backgroundLevel = undefined;
332
- return [...exits, ...ends];
333
- }
334
- /**
335
- * Stops the task of a terminal or tool call that the session `sessionId`
336
- * lists (v2 `terminal/cancel` and `tool_call/cancel`). `sessions` are the
337
- * root sessions, by id: a subagent session's list is built with its root's.
338
- * The terminal exits, or the tool call gets its final status, when Claude
339
- * Code reports the task stopped.
340
- */
341
- export async function cancelBackgroundWork(sessions, sessionId, target) {
342
- const notCancellable = () => "terminalId" in target
343
- ? RequestError.invalidParams({ sessionId, terminalId: target.terminalId }, `Terminal ${target.terminalId} is not a command running in the background, so it cannot be cancelled.`)
344
- : RequestError.invalidParams({ sessionId, toolCallId: target.toolCallId }, `Tool call ${target.toolCallId} is not running in the background, so it cannot be cancelled.`);
345
- const isTarget = (item) => "terminalId" in target
346
- ? item.type === "terminal" && item.terminalId === target.terminalId
347
- : item.type === "tool_call" && item.toolCallId === target.toolCallId;
348
- const listed = (root, session) => session.reportsBackground === true &&
349
- (backgroundLists(session, root).get(sessionId)?.some(isTarget) ?? false);
350
- const owner = Object.entries(sessions).find(([root, session]) => listed(root, session));
351
- if (!owner)
352
- throw notCancellable();
353
- const [root, session] = owner;
354
- const id = "terminalId" in target ? target.terminalId : target.toolCallId;
355
- const taskId = [
356
- ...(("terminalId" in target ? session.backgroundTerminals : session.backgroundToolCalls) ?? []),
357
- ].find(([, entry]) => entry.toolCallId === id)[0];
358
- try {
359
- await session.query.stopTask(taskId);
360
- }
361
- catch (error) {
362
- // The task ended meanwhile: it has ended on the client too, or is about to.
363
- if (!listed(root, session))
364
- throw notCancellable();
365
- throw error;
366
- }
367
- }
368
424
  /**
369
425
  * Publishes the lists of each session that reports them, each time one
370
426
  * changes. The sink sends them to the client synchronously, so a list follows
@@ -387,24 +443,14 @@ export class BackgroundWorkReporter {
387
443
  * `rootState` is the root session's state now.
388
444
  */
389
445
  sync(rootSessionId, session, rootState) {
390
- if (!this.sink || !session.reportsBackground)
446
+ if (!this.sink || !session.background?.reported)
391
447
  return;
392
- const children = new Map([...(session.nativeSubagentsByTaskId?.values() ?? [])].map((child) => [
393
- child.sessionId,
394
- child,
395
- ]));
396
- for (const [sessionId, items] of backgroundLists(session, rootSessionId)) {
397
- const json = JSON.stringify(items);
398
- if (this.published.get(sessionId)?.json === json)
448
+ for (const report of session.background.reports(session, rootState)) {
449
+ const json = JSON.stringify(report.items);
450
+ if (this.published.get(report.sessionId)?.json === json)
399
451
  continue;
400
- this.published.set(sessionId, { json, root: rootSessionId });
401
- const child = children.get(sessionId);
402
- this.sink.changed({
403
- sessionId,
404
- items,
405
- state: child ? workStateName(child.workState) : rootState,
406
- ...(child ? { parentSessionId: child.parentSessionId } : {}),
407
- });
452
+ this.published.set(report.sessionId, { json, root: rootSessionId });
453
+ this.sink.changed(report);
408
454
  }
409
455
  }
410
456
  /** The root session `rootSessionId` ended, and its subagent sessions with it. */