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