@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.
- package/dist/acp-agent.d.ts +8 -21
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +16 -32
- package/dist/background-work.d.ts +140 -104
- package/dist/background-work.d.ts.map +1 -1
- package/dist/background-work.js +335 -289
- package/dist/session-mode.d.ts +3 -3
- package/dist/session-mode.d.ts.map +1 -1
- package/dist/session-mode.js +55 -48
- package/package.json +1 -1
package/dist/background-work.js
CHANGED
|
@@ -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
|
-
*
|
|
36
|
-
* the
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
continue;
|
|
158
|
+
await terminal.output?.finish();
|
|
159
|
+
this.terminals.delete(taskId);
|
|
160
|
+
await send(terminalExit(this.sessionId, terminal));
|
|
119
161
|
}
|
|
120
|
-
const toolCall =
|
|
162
|
+
const toolCall = this.toolCalls.get(taskId);
|
|
121
163
|
if (toolCall) {
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
lists
|
|
141
|
-
.get(parent?.sessionId ?? rootSessionId)
|
|
142
|
-
.push({ type: "_claude/task", taskId, description: task.description });
|
|
204
|
+
this.level = level;
|
|
143
205
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
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
|
|
195
|
-
const
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
446
|
+
if (!this.sink || !session.background?.reported)
|
|
391
447
|
return;
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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. */
|