@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.
- package/dist/acp-agent.d.ts +8 -24
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +26 -29
- package/dist/background-output.d.ts +54 -0
- package/dist/background-output.d.ts.map +1 -0
- package/dist/background-output.js +122 -0
- package/dist/background-work.d.ts +152 -92
- package/dist/background-work.d.ts.map +1 -1
- package/dist/background-work.js +354 -264
- 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
|
@@ -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
|
-
*
|
|
35
|
-
* the
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
continue;
|
|
158
|
+
await terminal.output?.finish();
|
|
159
|
+
this.terminals.delete(taskId);
|
|
160
|
+
await send(terminalExit(this.sessionId, terminal));
|
|
118
161
|
}
|
|
119
|
-
const toolCall =
|
|
162
|
+
const toolCall = this.toolCalls.get(taskId);
|
|
120
163
|
if (toolCall) {
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
lists
|
|
140
|
-
.get(parent?.sessionId ?? rootSessionId)
|
|
141
|
-
.push({ type: "_claude/task", taskId, description: task.description });
|
|
204
|
+
this.level = level;
|
|
142
205
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
390
|
+
...(output.replaces ? { terminal_output: data } : { terminal_output_delta: data }),
|
|
207
391
|
},
|
|
208
392
|
},
|
|
209
393
|
};
|
|
210
394
|
}
|
|
211
|
-
/**
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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.
|
|
446
|
+
if (!this.sink || !session.background?.reported)
|
|
347
447
|
return;
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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. */
|