@agentclientprotocol/claude-agent-acp 0.90.0 → 0.90.1-preview.10
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/README.md +10 -2
- package/dist/acp-agent.d.ts +92 -16
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +313 -66
- package/dist/native-subagents.d.ts +55 -5
- package/dist/native-subagents.d.ts.map +1 -1
- package/dist/native-subagents.js +95 -8
- package/dist/subagent-history.d.ts +17 -0
- package/dist/subagent-history.d.ts.map +1 -1
- package/dist/subagent-history.js +79 -1
- package/dist/tool-calls/renderer.d.ts +14 -1
- package/dist/tool-calls/renderer.d.ts.map +1 -1
- package/dist/tool-calls/renderer.js +28 -5
- package/dist/turn-events.d.ts +5 -1
- package/dist/turn-events.d.ts.map +1 -1
- package/dist/v2/prompt.d.ts +2 -1
- package/dist/v2/prompt.d.ts.map +1 -1
- package/dist/v2/prompt.js +2 -1
- package/package.json +1 -1
package/dist/acp-agent.js
CHANGED
|
@@ -16,7 +16,7 @@ import { promisify } from "node:util";
|
|
|
16
16
|
import packageJson from "../package.json" with { type: "json" };
|
|
17
17
|
import { applyAskElicitationResponse, askUserQuestionsToCreateRequest, createElicitationResponseToElicitResult, extractAskUserQuestions, extractRefusalFallbackPrompt, mcpElicitationToCreateRequest, REFUSAL_FALLBACK_DIALOG_KIND, refusalFallbackResultFromResponse, refusalFallbackToCreateRequest, } from "./elicitation.js";
|
|
18
18
|
import { forkSession } from "./fork-session.js";
|
|
19
|
-
import { readSubagentMessages, subagentHistory } from "./subagent-history.js";
|
|
19
|
+
import { readSubagentMessages, subagentHistory, subagentLaunchIds, subagentLineage, } from "./subagent-history.js";
|
|
20
20
|
import { RfdSubagentReplay, replaySubagentTerminalState, } from "./subagent-replay.js";
|
|
21
21
|
import { readResumedSession, readResumedTail, } from "./resumed-session.js";
|
|
22
22
|
import { SessionTiming } from "./session-timing.js";
|
|
@@ -87,6 +87,11 @@ const STRUCTURED_USAGE_TIMEOUT_MS = 5_000;
|
|
|
87
87
|
* The teardown wakes the consumer itself, so they end within a few ticks
|
|
88
88
|
* unless the consumer is wedged. */
|
|
89
89
|
const TEARDOWN_TURN_END_TIMEOUT_MS = 5_000;
|
|
90
|
+
/** How long `canUseTool` or a hook waits, while a cancel's window may be
|
|
91
|
+
* open, for the consumer to reach a tool use in the stream. Claude Code
|
|
92
|
+
* streams the tool use before it asks, so the consumer is usually there
|
|
93
|
+
* within a few ticks. */
|
|
94
|
+
const TOOL_USE_WAIT_MS = 2_000;
|
|
90
95
|
/** The number of settled subagents whose parent tool call a session keeps for
|
|
91
96
|
* a later resume (see `resumableSubagents`). */
|
|
92
97
|
const MAX_RESUMABLE_SUBAGENTS = 256;
|
|
@@ -285,6 +290,15 @@ const AUTONOMOUS_RESULT_ORIGINS = new Set([
|
|
|
285
290
|
* or a model cycle that Claude Code started on its own. At most one cycle
|
|
286
291
|
* runs at a time, so a constant names it. */
|
|
287
292
|
const AUTONOMOUS_CYCLE = "autonomous-cycle";
|
|
293
|
+
/** The events of a connection that reports no foreground work: v1, whose
|
|
294
|
+
* client has no session state. The agent still tracks the work, which
|
|
295
|
+
* decides what the window after a cancel drops (see `workEndReported`). */
|
|
296
|
+
const UNREPORTED_FOREGROUND = {
|
|
297
|
+
running: () => { },
|
|
298
|
+
awaitingUser: () => { },
|
|
299
|
+
ended: () => { },
|
|
300
|
+
failed: () => { },
|
|
301
|
+
};
|
|
288
302
|
/** Whether this turn's terminal result arrived but its settlement is being
|
|
289
303
|
* held for background subagents it spawned (see Turn.deferredSettle). The
|
|
290
304
|
* single spelling of the hold predicate, shared by the consumer's settle
|
|
@@ -537,18 +551,6 @@ function parentToolUseIdOf(message) {
|
|
|
537
551
|
return null;
|
|
538
552
|
return typeof message.parent_tool_use_id === "string" ? message.parent_tool_use_id : null;
|
|
539
553
|
}
|
|
540
|
-
/** The ids of the Agent and Task tool uses in the content of a message. */
|
|
541
|
-
function subagentLaunchIds(content) {
|
|
542
|
-
if (!Array.isArray(content))
|
|
543
|
-
return [];
|
|
544
|
-
return content.flatMap((block) => typeof block === "object" &&
|
|
545
|
-
block !== null &&
|
|
546
|
-
block.type === "tool_use" &&
|
|
547
|
-
typeof block.id === "string" &&
|
|
548
|
-
isNativeSubagentControlTool(block.name)
|
|
549
|
-
? [block.id]
|
|
550
|
-
: []);
|
|
551
|
-
}
|
|
552
554
|
function stripSubagentTextAndThinking(content) {
|
|
553
555
|
if (!Array.isArray(content))
|
|
554
556
|
return content;
|
|
@@ -1173,6 +1175,9 @@ export class ClaudeAcpAgent {
|
|
|
1173
1175
|
* return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
|
|
1174
1176
|
* tests can shrink it. */
|
|
1175
1177
|
forceCancelGraceMs = DEFAULT_FORCE_CANCEL_GRACE_MS;
|
|
1178
|
+
/** How long `canUseTool` or a hook waits for the consumer to reach a tool
|
|
1179
|
+
* use (see `isStoppedWorkToolUse`). Mutable so tests can shrink it. */
|
|
1180
|
+
toolUseWaitMs = TOOL_USE_WAIT_MS;
|
|
1176
1181
|
/**
|
|
1177
1182
|
* The connection serves ACP v2 (`src/v2/`). The agent still speaks v1
|
|
1178
1183
|
* types, except for the tool call content that v2 needs and v1 cannot
|
|
@@ -1183,7 +1188,8 @@ export class ClaudeAcpAgent {
|
|
|
1183
1188
|
foregroundEvents;
|
|
1184
1189
|
/**
|
|
1185
1190
|
* The foreground work of each session while it runs, by the session id that
|
|
1186
|
-
* the client knows
|
|
1191
|
+
* the client knows, on every connection: only the `foreground` option
|
|
1192
|
+
* reports it. A clear-context restart replaces the Claude Code session
|
|
1187
1193
|
* behind that id and moves the running turn to it, so the work belongs to the
|
|
1188
1194
|
* id, not to a {@link Session}.
|
|
1189
1195
|
*/
|
|
@@ -2564,7 +2570,7 @@ export class ClaudeAcpAgent {
|
|
|
2564
2570
|
* recognizable by the `parentToolUseId` meta that toAcpNotifications
|
|
2565
2571
|
* stamps from `parent_tool_use_id`, and never reach the top-level feed
|
|
2566
2572
|
* as the turn's answer. */
|
|
2567
|
-
const subagents = (session.nativeSubagentRuntime ??= new NativeSubagentRuntime(subagentForm(this.clientCapabilities), params.sessionId, session, async (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)), this.logger));
|
|
2573
|
+
const subagents = (session.nativeSubagentRuntime ??= new NativeSubagentRuntime(subagentForm(this.clientCapabilities), params.sessionId, session, async (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)), this.logger, (agentId) => subagentLineage(params.sessionId, agentId, this.logger)));
|
|
2568
2574
|
// Adapter-composed advisories (hook feedback, model fallbacks) are live
|
|
2569
2575
|
// events, not something the model said. Clients on the notice contract get
|
|
2570
2576
|
// them as `notice` updates; the rest keep the bold-label transcript line.
|
|
@@ -3162,8 +3168,14 @@ export class ClaudeAcpAgent {
|
|
|
3162
3168
|
/** Sends `report`, which ends the tool call `toolCallId` without its result
|
|
3163
3169
|
* (see `AcpToolCallRenderer.unfinished`), and forgets the call. Forgetting
|
|
3164
3170
|
* comes first, so that a late hook or a second close cannot overwrite the
|
|
3165
|
-
* report.
|
|
3171
|
+
* report. A command has a display terminal on a client that renders them,
|
|
3172
|
+
* and no result will end it, so the report ends it too. */
|
|
3166
3173
|
const closeToolCall = async (toolCallId, report) => {
|
|
3174
|
+
// Read before the cache entry goes: it names the tool.
|
|
3175
|
+
const toolUse = session.toolUseCache[toolCallId];
|
|
3176
|
+
const terminalExit = toolUse && new AcpToolCallRenderer(this.toolCallCapabilities).terminalExit(toolUse);
|
|
3177
|
+
if (terminalExit)
|
|
3178
|
+
report._meta = { ...report._meta, ...terminalExit };
|
|
3167
3179
|
unregisterHookCallback(toolCallId);
|
|
3168
3180
|
session.emittedToolCalls.delete(toolCallId);
|
|
3169
3181
|
delete session.toolUseCache[toolCallId];
|
|
@@ -3178,26 +3190,24 @@ export class ClaudeAcpAgent {
|
|
|
3178
3190
|
task.backgrounded === true &&
|
|
3179
3191
|
!task.endedPerLevel &&
|
|
3180
3192
|
task.parentToolUseId === agentToolUseId);
|
|
3181
|
-
/**
|
|
3182
|
-
*
|
|
3183
|
-
*
|
|
3184
|
-
*
|
|
3185
|
-
*
|
|
3186
|
-
*
|
|
3187
|
-
*
|
|
3188
|
-
*
|
|
3189
|
-
*
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
*
|
|
3193
|
-
*
|
|
3194
|
-
|
|
3195
|
-
* for the first (see `Turn.closingCancelledToolCalls`). */
|
|
3196
|
-
const closeCancelledToolCalls = (turn) => (turn.closingCancelledToolCalls ??= (async () => {
|
|
3193
|
+
/** Whether output of the conversation `parentToolUseId` (null for the main
|
|
3194
|
+
* one) is a background subagent's that still runs. A cancel does not stop
|
|
3195
|
+
* such a subagent's work (agentclientprotocol/agent-client-protocol#2308):
|
|
3196
|
+
* the closing at the cancel leaves its tool calls open for their results
|
|
3197
|
+
* (`closedAtCancel`), and the window after the cancel lets its output
|
|
3198
|
+
* through (see the `session.cancelled` checks of the stream and the
|
|
3199
|
+
* user/assistant cases). Claude Code currently stops background subagents
|
|
3200
|
+
* at an interrupt, so this decides only the output that comes before the
|
|
3201
|
+
* SDK reports a subagent's end. */
|
|
3202
|
+
const isRunningBackgroundSubagentOutput = (parentToolUseId) => parentToolUseId !== null && runsInBackground(parentToolUseId);
|
|
3203
|
+
/** Ends the open tool calls that `include` accepts, with the report of a call
|
|
3204
|
+
* that a cancel stopped and sent no result for. `include` gets the Agent/Task
|
|
3205
|
+
* call of the subagent that made the tool call, if a subagent did. */
|
|
3206
|
+
const closeOpenToolCalls = async (include) => {
|
|
3197
3207
|
const renderer = new AcpToolCallRenderer(this.toolCallCapabilities);
|
|
3198
3208
|
for (const toolCallId of [...session.emittedToolCalls]) {
|
|
3199
3209
|
const parentToolUseId = session.toolCallParents?.get(toolCallId);
|
|
3200
|
-
if (
|
|
3210
|
+
if (!include(parentToolUseId))
|
|
3201
3211
|
continue;
|
|
3202
3212
|
const report = renderer.unfinished({
|
|
3203
3213
|
toolCallId,
|
|
@@ -3210,8 +3220,48 @@ export class ClaudeAcpAgent {
|
|
|
3210
3220
|
stampParentToolUseId(report, parentToolUseId);
|
|
3211
3221
|
await closeToolCall(toolCallId, report);
|
|
3212
3222
|
}
|
|
3213
|
-
}
|
|
3223
|
+
};
|
|
3224
|
+
/** Whether the closing at the end of the work a cancel stopped takes a tool
|
|
3225
|
+
* call that the subagent started by the Agent call `parentToolUseId` made
|
|
3226
|
+
* (undefined for a call of the main conversation): not one of a running
|
|
3227
|
+
* background subagent, which the cancel may not have stopped. */
|
|
3228
|
+
const closedAtCancel = (parentToolUseId) => parentToolUseId === undefined || !runsInBackground(parentToolUseId);
|
|
3229
|
+
/** Closes the tool calls that the work a cancel stopped left open, as that
|
|
3230
|
+
* work ends. Claude Code's interrupt ends each running tool of the main
|
|
3231
|
+
* conversation with an error result, which `sendInterruptedToolResults`
|
|
3232
|
+
* delivers. What is still open here has no result to deliver:
|
|
3233
|
+
* - a tool of a subagent: Claude Code writes its interrupted result only to
|
|
3234
|
+
* the subagent's own transcript, not to the stream;
|
|
3235
|
+
* - a tool call that Claude abandoned before it ran;
|
|
3236
|
+
* - every tool call, when the stream gives no results at all, as when the
|
|
3237
|
+
* force-cancel backstop ends a wedged turn.
|
|
3238
|
+
* A foreground subagent ends with the work that waits on it. A background
|
|
3239
|
+
* subagent keeps its tool calls open until the SDK reports its end, which
|
|
3240
|
+
* Claude Code 2.1.293 does for the ones it stops, before the turn's result
|
|
3241
|
+
* (see `NativeSubagentRuntime.parentCancelled`). When the SDK reports it
|
|
3242
|
+
* only after the work ended, `closeStoppedSubagentToolCalls` closes them.
|
|
3243
|
+
* Runs once per turn: a second call, from the other settling path, waits
|
|
3244
|
+
* for the first (see `Turn.closingCancelledToolCalls`). */
|
|
3245
|
+
const closeCancelledToolCalls = (turn) => (turn.closingCancelledToolCalls ??= closeOpenToolCalls(closedAtCancel));
|
|
3214
3246
|
session.closeCancelledToolCalls = closeCancelledToolCalls;
|
|
3247
|
+
/** Closes the tool calls of the subagent that the Agent call `agentToolUseId`
|
|
3248
|
+
* started, once the SDK reports the subagent stopped, if a cancel stopped it
|
|
3249
|
+
* after the work that waited on it ended. Nothing else ends them then.
|
|
3250
|
+
* Example: the cancel settles a turn that v1 holds for a background
|
|
3251
|
+
* subagent, so `closeCancelledToolCalls` leaves the subagent's command
|
|
3252
|
+
* open, since the subagent runs. Claude Code's interrupt then stops the
|
|
3253
|
+
* subagent. Claude Code 2.1.296 sends the command's interrupted result
|
|
3254
|
+
* after that, but the result is dropped like all output after the work's
|
|
3255
|
+
* end (see the `session.cancelled` check of the user/assistant case).
|
|
3256
|
+
* While the work still runs, the calls of a stopped subagent get Claude
|
|
3257
|
+
* Code's result, or `closeCancelledToolCalls` at the work's end. */
|
|
3258
|
+
const closeStoppedSubagentToolCalls = async (agentToolUseId) => {
|
|
3259
|
+
if (!agentToolUseId || !session.cancelled)
|
|
3260
|
+
return;
|
|
3261
|
+
if (!this.workEndReported(params.sessionId, session))
|
|
3262
|
+
return;
|
|
3263
|
+
await closeOpenToolCalls((parentToolUseId) => parentToolUseId === agentToolUseId);
|
|
3264
|
+
};
|
|
3215
3265
|
/** Delivers the results that the work a cancel stopped sends for tool calls
|
|
3216
3266
|
* that the client holds open. Claude Code's interrupt ends each running tool
|
|
3217
3267
|
* of the main conversation with an error result, before the result that
|
|
@@ -3245,6 +3295,47 @@ export class ClaudeAcpAgent {
|
|
|
3245
3295
|
await sendUpdate(notification);
|
|
3246
3296
|
}
|
|
3247
3297
|
};
|
|
3298
|
+
/** The model cycle that Claude Code started on its own ends, and its end is
|
|
3299
|
+
* about to be reported: closes the tool calls that it left open first. A
|
|
3300
|
+
* cycle is foreground work that no turn settles, so neither
|
|
3301
|
+
* `closeCancelledToolCalls` nor the check of `settleActive` for unfinished
|
|
3302
|
+
* tools runs for it. A turn that took the cycle over closes its own calls,
|
|
3303
|
+
* when it settles.
|
|
3304
|
+
*
|
|
3305
|
+
* After a cancel, Claude Code's interrupt gives the tools of the main
|
|
3306
|
+
* conversation results, which `sendInterruptedToolResults` delivers while
|
|
3307
|
+
* the cycle runs. What is still open has none, such as the command of a
|
|
3308
|
+
* foreground subagent. The follow-up of a turn that v1 holds for its
|
|
3309
|
+
* background subagents is a cycle too, and the cancel ends the held turn
|
|
3310
|
+
* as well, possibly in `cancelTurns` while these reports are in flight. So
|
|
3311
|
+
* the cycle's calls close with the held turn's closing, which each settle
|
|
3312
|
+
* of the turn waits for, and the response never comes before them.
|
|
3313
|
+
*
|
|
3314
|
+
* At any other end, every call that ran has its result. What is open is a
|
|
3315
|
+
* call of the main conversation that Claude abandoned before it ran, such
|
|
3316
|
+
* as a streamed tool_use that never reached a complete message, which a
|
|
3317
|
+
* turn's end closes the same way (`closeUnfinished`). The calls of a
|
|
3318
|
+
* subagent are not the cycle's: a background subagent runs on. */
|
|
3319
|
+
const closeCycleToolCalls = async () => {
|
|
3320
|
+
if (!this.runsAutonomousCycle(params.sessionId))
|
|
3321
|
+
return;
|
|
3322
|
+
if (session.cancelled) {
|
|
3323
|
+
const held = session.activeTurn;
|
|
3324
|
+
if (held && !held.settled)
|
|
3325
|
+
await closeCancelledToolCalls(held);
|
|
3326
|
+
else
|
|
3327
|
+
await closeOpenToolCalls(closedAtCancel);
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
const renderer = new AcpToolCallRenderer(this.toolCallCapabilities);
|
|
3331
|
+
for (const toolCallId of [...session.emittedToolCalls]) {
|
|
3332
|
+
if (session.toolCallParents?.has(toolCallId))
|
|
3333
|
+
continue;
|
|
3334
|
+
if (session.dispatchedToolCalls?.has(toolCallId))
|
|
3335
|
+
continue;
|
|
3336
|
+
await closeToolCall(toolCallId, renderer.unfinished({ toolCallId, ran: false }));
|
|
3337
|
+
}
|
|
3338
|
+
};
|
|
3248
3339
|
/** At the actual turn boundary, preview its checkpoint, settle the active
|
|
3249
3340
|
* turn exactly once, disarm the force-cancel backstop, and drop it from
|
|
3250
3341
|
* the queue. Cancellation and provider failures skip checkpoint I/O. */
|
|
@@ -3894,6 +3985,7 @@ export class ClaudeAcpAgent {
|
|
|
3894
3985
|
// here. An owed idle belongs to an earlier result: it can lag
|
|
3895
3986
|
// past the cycle's first output.
|
|
3896
3987
|
if (session.owedTrailingIdles === 0) {
|
|
3988
|
+
await closeCycleToolCalls();
|
|
3897
3989
|
this.endAutonomousCycle(params.sessionId, {
|
|
3898
3990
|
outcome: { stopReason: session.cancelled ? "cancelled" : "end_turn" },
|
|
3899
3991
|
});
|
|
@@ -4244,6 +4336,9 @@ export class ClaudeAcpAgent {
|
|
|
4244
4336
|
droppedStreams.delete(message.tool_use_id);
|
|
4245
4337
|
}
|
|
4246
4338
|
await endBackgroundTerminal(message.task_id);
|
|
4339
|
+
if (message.status === "stopped") {
|
|
4340
|
+
await closeStoppedSubagentToolCalls(message.tool_use_id);
|
|
4341
|
+
}
|
|
4247
4342
|
settleLiveTask(message.task_id);
|
|
4248
4343
|
break;
|
|
4249
4344
|
case "task_updated":
|
|
@@ -4275,6 +4370,9 @@ export class ClaudeAcpAgent {
|
|
|
4275
4370
|
droppedStreams.delete(parentToolUseId);
|
|
4276
4371
|
}
|
|
4277
4372
|
await endBackgroundTerminal(message.task_id);
|
|
4373
|
+
if (message.patch.status === "killed") {
|
|
4374
|
+
await closeStoppedSubagentToolCalls(parentToolUseId);
|
|
4375
|
+
}
|
|
4278
4376
|
settleLiveTask(message.task_id);
|
|
4279
4377
|
}
|
|
4280
4378
|
else if (message.patch.status === "running" ||
|
|
@@ -4710,6 +4808,7 @@ export class ClaudeAcpAgent {
|
|
|
4710
4808
|
pendingExitPlanContextReset?.toolUseId ===
|
|
4711
4809
|
pendingExitPlanModeInterruption?.toolUseId;
|
|
4712
4810
|
if (!continuesPlan && !(isAutonomousResult && message.num_turns === 0)) {
|
|
4811
|
+
await closeCycleToolCalls();
|
|
4713
4812
|
this.endAutonomousCycle(params.sessionId, autonomousCycleEnd(message, resultModelUsage, session.cancelled || exitPlanInterruption || isAbortedResult(message)));
|
|
4714
4813
|
}
|
|
4715
4814
|
if (session.cancelled) {
|
|
@@ -5168,7 +5267,8 @@ export class ClaudeAcpAgent {
|
|
|
5168
5267
|
// `cancelled` check in the user/assistant case). Otherwise the
|
|
5169
5268
|
// answer to a prompt the cancel abandoned, which Claude Code had
|
|
5170
5269
|
// already taken off its queue, would stream to the client after the
|
|
5171
|
-
// cancel was reported.
|
|
5270
|
+
// cancel was reported. A background subagent that still runs is not
|
|
5271
|
+
// work the cancel stopped (see isRunningBackgroundSubagentOutput).
|
|
5172
5272
|
//
|
|
5173
5273
|
// Where the consolidated message delivers the text (the same
|
|
5174
5274
|
// condition as in the user/assistant case), a stream that loses a
|
|
@@ -5180,7 +5280,13 @@ export class ClaudeAcpAgent {
|
|
|
5180
5280
|
// prefix of the text, so the text would be sent again (see
|
|
5181
5281
|
// unstreamedRemainder).
|
|
5182
5282
|
const streamKey = message.parent_tool_use_id ?? "";
|
|
5183
|
-
|
|
5283
|
+
const stoppedWorkOutput = this.dropsStoppedWorkOutput(params.sessionId, session) &&
|
|
5284
|
+
!isRunningBackgroundSubagentOutput(message.parent_tool_use_id);
|
|
5285
|
+
if (message.event.type === "content_block_start" &&
|
|
5286
|
+
isToolUseBlock(message.event.content_block)) {
|
|
5287
|
+
recordReachedToolUse(session, message.event.content_block.id, stoppedWorkOutput);
|
|
5288
|
+
}
|
|
5289
|
+
if (stoppedWorkOutput) {
|
|
5184
5290
|
if (message.parent_tool_use_id === null || forwardsSubagentText()) {
|
|
5185
5291
|
droppedStreams.add(streamKey);
|
|
5186
5292
|
}
|
|
@@ -5250,8 +5356,18 @@ export class ClaudeAcpAgent {
|
|
|
5250
5356
|
}
|
|
5251
5357
|
case "user":
|
|
5252
5358
|
case "assistant": {
|
|
5253
|
-
if (message.type === "assistant")
|
|
5359
|
+
if (message.type === "assistant") {
|
|
5254
5360
|
recordDispatchedToolUses(session, message.message.content);
|
|
5361
|
+
// A tool use that did not stream is reached here first.
|
|
5362
|
+
const content = message.message.content;
|
|
5363
|
+
const stoppedWorkOutput = this.dropsStoppedWorkOutput(params.sessionId, session) &&
|
|
5364
|
+
!isRunningBackgroundSubagentOutput(message.parent_tool_use_id);
|
|
5365
|
+
for (const block of Array.isArray(content) ? content : []) {
|
|
5366
|
+
if (isToolUseBlock(block)) {
|
|
5367
|
+
recordReachedToolUse(session, block.id, stoppedWorkOutput);
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5255
5371
|
// Record the ACP messageId -> SDK uuid mapping for this message
|
|
5256
5372
|
// (including replays). The consolidated message carries both ids, so
|
|
5257
5373
|
// this is where we learn the uuid the SDK's rewind/resume APIs key on
|
|
@@ -5355,11 +5471,13 @@ export class ClaudeAcpAgent {
|
|
|
5355
5471
|
break;
|
|
5356
5472
|
}
|
|
5357
5473
|
}
|
|
5358
|
-
if (session.cancelled
|
|
5474
|
+
if (session.cancelled &&
|
|
5475
|
+
!isRunningBackgroundSubagentOutput(message.parent_tool_use_id)) {
|
|
5359
5476
|
// The work the cancel stopped ends its tools with error results.
|
|
5360
5477
|
// Until the client is told that the work ended, they close the
|
|
5361
5478
|
// tool calls it opened. Everything else that work sends is
|
|
5362
|
-
// dropped, and so is everything after that report.
|
|
5479
|
+
// dropped, and so is everything after that report. A background
|
|
5480
|
+
// subagent that still runs is not that work: its messages go on.
|
|
5363
5481
|
if (message.type === "user" && !this.workEndReported(params.sessionId, session)) {
|
|
5364
5482
|
await sendInterruptedToolResults(message);
|
|
5365
5483
|
}
|
|
@@ -5790,6 +5908,10 @@ export class ClaudeAcpAgent {
|
|
|
5790
5908
|
return;
|
|
5791
5909
|
}
|
|
5792
5910
|
session.cancelled = true;
|
|
5911
|
+
// The interrupt below makes Claude Code withdraw the requests that are open
|
|
5912
|
+
// now (see `withPendingUserInput`).
|
|
5913
|
+
for (const input of session.openUserInputs ?? [])
|
|
5914
|
+
input.cancelled = true;
|
|
5793
5915
|
// Before anything awaits, so a model request that the consumer reads in
|
|
5794
5916
|
// the meantime is already judged against these prompts.
|
|
5795
5917
|
session.cancelledPrompts ??= new Set();
|
|
@@ -6718,19 +6840,29 @@ export class ClaudeAcpAgent {
|
|
|
6718
6840
|
/** Runs `request`, a request that `requestSessionId` sends for the session
|
|
6719
6841
|
* `ownerSessionId`. When that is a subagent's child session, the child
|
|
6720
6842
|
* reports that it awaits the user while the request is open (the RFD form,
|
|
6721
|
-
* see `NativeSubagentRuntime.awaitingUser`).
|
|
6722
|
-
|
|
6843
|
+
* see `NativeSubagentRuntime.awaitingUser`). `signal` aborts when Claude
|
|
6844
|
+
* Code withdraws the request. */
|
|
6845
|
+
withChildAwaitingUser(ownerSessionId, requestSessionId, signal, request) {
|
|
6723
6846
|
const runtime = this.sessions[ownerSessionId]?.nativeSubagentRuntime;
|
|
6724
6847
|
return runtime && requestSessionId !== ownerSessionId
|
|
6725
|
-
? runtime.awaitingUser(requestSessionId, request)
|
|
6848
|
+
? runtime.awaitingUser(requestSessionId, request, signal)
|
|
6726
6849
|
: request();
|
|
6727
6850
|
}
|
|
6728
6851
|
/** Mark a client request of the subagent `agentID` (absent for the main
|
|
6729
6852
|
* conversation) as awaiting user input for exactly the lifetime of its
|
|
6730
6853
|
* promise (see `Session.openUserInputs`). Steering consults the open
|
|
6731
6854
|
* requests synchronously, so a message arriving while any permission or
|
|
6732
|
-
* elicitation card is open uses non-interrupting SDK delivery.
|
|
6733
|
-
|
|
6855
|
+
* elicitation card is open uses non-interrupting SDK delivery.
|
|
6856
|
+
*
|
|
6857
|
+
* `signal` aborts when Claude Code withdraws the request, which is not an
|
|
6858
|
+
* answer. Example: the user cancels while a permission request is open.
|
|
6859
|
+
* Claude Code's interrupt withdraws it as it stops the work, and the work
|
|
6860
|
+
* ends `idle cancelled` right after. The end of the request then reports
|
|
6861
|
+
* no `running`: the client would see `requires_action`, `running`, `idle
|
|
6862
|
+
* cancelled`. A withdrawal without a cancel means that something else
|
|
6863
|
+
* answered the request in the user's place, such as a `PermissionRequest`
|
|
6864
|
+
* hook, and the work goes on, so the end reports `running`. */
|
|
6865
|
+
async withPendingUserInput(sessionId, signal, request, agentID) {
|
|
6734
6866
|
const session = this.sessions[sessionId];
|
|
6735
6867
|
if (!session)
|
|
6736
6868
|
return request();
|
|
@@ -6742,7 +6874,8 @@ export class ClaudeAcpAgent {
|
|
|
6742
6874
|
}
|
|
6743
6875
|
finally {
|
|
6744
6876
|
session.openUserInputs.delete(input);
|
|
6745
|
-
|
|
6877
|
+
const withdrawnByCancel = signal.aborted && input.cancelled === true;
|
|
6878
|
+
this.syncAwaitingUser(sessionId, session, withdrawnByCancel);
|
|
6746
6879
|
}
|
|
6747
6880
|
}
|
|
6748
6881
|
/** Report whether the session's foreground work awaits the user: exactly
|
|
@@ -6751,8 +6884,13 @@ export class ClaudeAcpAgent {
|
|
|
6751
6884
|
* and Claude Code can ask for a queued prompt before the consumer activates
|
|
6752
6885
|
* it, so a request is not tied to the work that was running when it
|
|
6753
6886
|
* opened. Called when a request opens or closes, when a turn activates, and
|
|
6754
|
-
* when a task registers or moves to the background.
|
|
6755
|
-
|
|
6887
|
+
* when a task registers or moves to the background.
|
|
6888
|
+
*
|
|
6889
|
+
* When the last request ends, the work reports `running` again, unless a
|
|
6890
|
+
* cancel withdrew that request (`withdrawnByCancel`, see
|
|
6891
|
+
* {@link withPendingUserInput}): the work is being stopped, and reports its
|
|
6892
|
+
* end next. */
|
|
6893
|
+
syncAwaitingUser(sessionId, session, withdrawnByCancel = false) {
|
|
6756
6894
|
const work = this.foregroundWork.get(sessionId);
|
|
6757
6895
|
if (!work)
|
|
6758
6896
|
return;
|
|
@@ -6762,7 +6900,7 @@ export class ClaudeAcpAgent {
|
|
|
6762
6900
|
work.awaitingUser = waiting;
|
|
6763
6901
|
if (waiting)
|
|
6764
6902
|
work.events.awaitingUser();
|
|
6765
|
-
else
|
|
6903
|
+
else if (!withdrawnByCancel)
|
|
6766
6904
|
work.events.running();
|
|
6767
6905
|
}
|
|
6768
6906
|
/** Make `owner` the foreground work of the session `sessionId` (see
|
|
@@ -6771,14 +6909,12 @@ export class ClaudeAcpAgent {
|
|
|
6771
6909
|
* reports nothing new: a turn takes over a cycle that Claude Code started on
|
|
6772
6910
|
* its own, and only the owner's end ends the work. */
|
|
6773
6911
|
startForeground(sessionId, owner) {
|
|
6774
|
-
if (!this.foregroundEvents)
|
|
6775
|
-
return;
|
|
6776
6912
|
const current = this.foregroundWork.get(sessionId);
|
|
6777
6913
|
if (current) {
|
|
6778
6914
|
current.owner = owner;
|
|
6779
6915
|
return;
|
|
6780
6916
|
}
|
|
6781
|
-
const events = this.foregroundEvents(sessionId);
|
|
6917
|
+
const events = this.foregroundEvents?.(sessionId) ?? UNREPORTED_FOREGROUND;
|
|
6782
6918
|
this.foregroundWork.set(sessionId, { owner, events, awaitingUser: false });
|
|
6783
6919
|
events.running();
|
|
6784
6920
|
const session = this.sessions[sessionId];
|
|
@@ -6804,7 +6940,7 @@ export class ClaudeAcpAgent {
|
|
|
6804
6940
|
* whose work ended at its result, so its follow-up is a cycle. Output of
|
|
6805
6941
|
* the work a cancel stopped starts nothing (see Session.cancelled). */
|
|
6806
6942
|
startAutonomousCycle(sessionId, session, message) {
|
|
6807
|
-
if (
|
|
6943
|
+
if (this.foregroundWork.has(sessionId) || session.cancelled)
|
|
6808
6944
|
return;
|
|
6809
6945
|
if (message.type !== "assistant" && message.type !== "stream_event")
|
|
6810
6946
|
return;
|
|
@@ -6822,15 +6958,79 @@ export class ClaudeAcpAgent {
|
|
|
6822
6958
|
/** Whether the client has been told that the session's work ended: on v2 by
|
|
6823
6959
|
* an idle state, so no foreground work is open, including a cycle that
|
|
6824
6960
|
* Claude Code started on its own; on v1 by the response to the turn's
|
|
6825
|
-
* prompt, so no turn is active.
|
|
6961
|
+
* prompt, so no turn is active. v1 reports nothing for a cycle, so its end
|
|
6962
|
+
* is its result or idle: until then, Claude Code's results for the cycle's
|
|
6963
|
+
* tool calls still reach the client after a cancel. */
|
|
6826
6964
|
workEndReported(sessionId, session) {
|
|
6827
|
-
return this.foregroundEvents
|
|
6965
|
+
return this.foregroundEvents
|
|
6966
|
+
? !this.foregroundWork.has(sessionId)
|
|
6967
|
+
: !session.activeTurn && !this.runsAutonomousCycle(sessionId);
|
|
6968
|
+
}
|
|
6969
|
+
/** Whether the consumer drops what it reaches now as output of the work a
|
|
6970
|
+
* cancel stopped: the client has been told that the work ended, and the
|
|
6971
|
+
* window after the cancel is still open (see Session.cancelled). */
|
|
6972
|
+
dropsStoppedWorkOutput(sessionId, session) {
|
|
6973
|
+
return session.cancelled && this.workEndReported(sessionId, session);
|
|
6974
|
+
}
|
|
6975
|
+
/**
|
|
6976
|
+
* Whether the tool use `toolUseId`, which reaches `canUseTool` or a hook, is
|
|
6977
|
+
* output of the work a cancel stopped that the consumer dropped (see
|
|
6978
|
+
* `dropsStoppedWorkOutput`). Example: Claude Code runs a prompt that the
|
|
6979
|
+
* cancel abandoned, and it asks to run a command. The consumer drops the
|
|
6980
|
+
* tool use and its result, so a tool call shown for it would never end, and
|
|
6981
|
+
* the client would keep it open (issue #1061).
|
|
6982
|
+
*
|
|
6983
|
+
* Claude Code calls `canUseTool` and hooks on its control channel, after it
|
|
6984
|
+
* streamed the tool use, and the consumer can lag behind. So the session can
|
|
6985
|
+
* still look cancelled while the consumer has the next prompt's echo, which
|
|
6986
|
+
* ends the window, still ahead of the tool use. While a cancel's window may
|
|
6987
|
+
* be open, this waits until the consumer reaches the tool use, for at most
|
|
6988
|
+
* {@link toolUseWaitMs} or until `signal` aborts: the stream never shows
|
|
6989
|
+
* some tool uses, such as SandboxNetworkAccess's. Without a cancel nothing
|
|
6990
|
+
* is dropped, so it answers at once. It returns a promise only when it
|
|
6991
|
+
* waits, so a permission request without a cancel goes out as soon as
|
|
6992
|
+
* before.
|
|
6993
|
+
*/
|
|
6994
|
+
isStoppedWorkToolUse(session, toolUseId, signal) {
|
|
6995
|
+
const reached = session.reachedToolUses?.get(toolUseId);
|
|
6996
|
+
if (reached !== undefined)
|
|
6997
|
+
return reached;
|
|
6998
|
+
if (!session.cancelled || signal?.aborted)
|
|
6999
|
+
return false;
|
|
7000
|
+
return this.reachedInStream(session, toolUseId, signal);
|
|
7001
|
+
}
|
|
7002
|
+
/** Waits until the consumer reaches the tool use `toolUseId` (see
|
|
7003
|
+
* `isStoppedWorkToolUse`), then says whether it dropped it. */
|
|
7004
|
+
async reachedInStream(session, toolUseId, signal) {
|
|
7005
|
+
await new Promise((resolve) => {
|
|
7006
|
+
const waiters = (session.toolUseWaiters ??= new Map());
|
|
7007
|
+
let waiting = waiters.get(toolUseId);
|
|
7008
|
+
if (!waiting)
|
|
7009
|
+
waiters.set(toolUseId, (waiting = new Set()));
|
|
7010
|
+
const done = () => {
|
|
7011
|
+
clearTimeout(timer);
|
|
7012
|
+
signal?.removeEventListener("abort", done);
|
|
7013
|
+
waiting.delete(done);
|
|
7014
|
+
if (waiting.size === 0 && waiters.get(toolUseId) === waiting)
|
|
7015
|
+
waiters.delete(toolUseId);
|
|
7016
|
+
resolve();
|
|
7017
|
+
};
|
|
7018
|
+
const timer = setTimeout(done, this.toolUseWaitMs);
|
|
7019
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
7020
|
+
waiting.add(done);
|
|
7021
|
+
});
|
|
7022
|
+
return session.reachedToolUses?.get(toolUseId) ?? false;
|
|
6828
7023
|
}
|
|
6829
7024
|
/** End the session's model cycle that Claude Code started on its own, if one
|
|
6830
7025
|
* runs. */
|
|
6831
7026
|
endAutonomousCycle(sessionId, end) {
|
|
6832
7027
|
this.endForeground(sessionId, AUTONOMOUS_CYCLE, end);
|
|
6833
7028
|
}
|
|
7029
|
+
/** Whether a model cycle that Claude Code started on its own is the session's
|
|
7030
|
+
* foreground work. A turn that took a cycle over owns the work from then on. */
|
|
7031
|
+
runsAutonomousCycle(sessionId) {
|
|
7032
|
+
return this.foregroundWork.get(sessionId)?.owner === AUTONOMOUS_CYCLE;
|
|
7033
|
+
}
|
|
6834
7034
|
/** Forward a permission request to the client, wiring the tool call's
|
|
6835
7035
|
* `signal` through as a `cancellationSignal`. When the turn is cancelled
|
|
6836
7036
|
* while the client's prompt is still open the signal aborts, the SDK sends
|
|
@@ -6855,7 +7055,7 @@ export class ClaudeAcpAgent {
|
|
|
6855
7055
|
// The request goes to a subagent's child session for a tool call of that
|
|
6856
7056
|
// subagent, but it blocks the turn of the owner session.
|
|
6857
7057
|
try {
|
|
6858
|
-
return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, () => this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal), agentID));
|
|
7058
|
+
return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, signal, () => this.withPendingUserInput(ownerSessionId, signal, () => raceWithAbort(this.client.requestPermission(params, signal), signal), agentID));
|
|
6859
7059
|
}
|
|
6860
7060
|
catch (error) {
|
|
6861
7061
|
if (signal.aborted) {
|
|
@@ -6864,6 +7064,17 @@ export class ClaudeAcpAgent {
|
|
|
6864
7064
|
throw error;
|
|
6865
7065
|
}
|
|
6866
7066
|
}
|
|
7067
|
+
/** Send the client an elicitation for a callback of Claude Code. When the
|
|
7068
|
+
* callback's `signal` aborts, the SDK sends `$/cancel_request` and keeps
|
|
7069
|
+
* waiting for the client's reply, which a client that ignores the cancel
|
|
7070
|
+
* never sends. So the call also rejects at once on the abort, as
|
|
7071
|
+
* {@link requestPermissionFromClient} does for a permission request. Left
|
|
7072
|
+
* open, the request would stay in `openUserInputs` (see
|
|
7073
|
+
* {@link withPendingUserInput}), which keeps steering non-interrupting, and
|
|
7074
|
+
* a child's `openRequests` would not drop. */
|
|
7075
|
+
elicitFromClient(request, signal) {
|
|
7076
|
+
return raceWithAbort(this.client.createElicitation(request, signal), signal);
|
|
7077
|
+
}
|
|
6867
7078
|
/** Emit the `tool_call` a permission request references if it hasn't been sent
|
|
6868
7079
|
* yet, so the client has the tool call before being asked to approve it. The
|
|
6869
7080
|
* matching streamed tool_use chunk later refines it with a `tool_call_update`
|
|
@@ -6960,17 +7171,21 @@ export class ClaudeAcpAgent {
|
|
|
6960
7171
|
* decision, and a failure only leaves the preview out.
|
|
6961
7172
|
*/
|
|
6962
7173
|
fileChangePreviewHook(sessionId) {
|
|
6963
|
-
return async (input, toolUseID) => {
|
|
7174
|
+
return async (input, toolUseID, { signal }) => {
|
|
6964
7175
|
if (input.hook_event_name === "PreToolUse" && toolUseID) {
|
|
6965
|
-
await this.previewFileChange(sessionId, input, toolUseID).catch((error) => this.logger.error(`Session ${sessionId}: failed to preview ${toolUseID}: ${error}`));
|
|
7176
|
+
await this.previewFileChange(sessionId, input, toolUseID, signal).catch((error) => this.logger.error(`Session ${sessionId}: failed to preview ${toolUseID}: ${error}`));
|
|
6966
7177
|
}
|
|
6967
7178
|
return { continue: true };
|
|
6968
7179
|
};
|
|
6969
7180
|
}
|
|
6970
|
-
async previewFileChange(sessionId, input, toolUseID) {
|
|
7181
|
+
async previewFileChange(sessionId, input, toolUseID, signal) {
|
|
6971
7182
|
const session = this.sessions[sessionId];
|
|
6972
7183
|
if (!session)
|
|
6973
7184
|
return;
|
|
7185
|
+
// As in `canUseTool`: the work a cancel stopped shows nothing.
|
|
7186
|
+
const stopped = this.isStoppedWorkToolUse(session, toolUseID, signal);
|
|
7187
|
+
if (stopped instanceof Promise ? await stopped : stopped)
|
|
7188
|
+
return;
|
|
6974
7189
|
const toolInput = (input.tool_input ?? {});
|
|
6975
7190
|
const content = await previewPatchContent(input.tool_name, toolInput, session.cwd, "v2");
|
|
6976
7191
|
if (!content)
|
|
@@ -7012,6 +7227,14 @@ export class ClaudeAcpAgent {
|
|
|
7012
7227
|
message: "Session not found",
|
|
7013
7228
|
};
|
|
7014
7229
|
}
|
|
7230
|
+
// The work a cancel stopped shows the client nothing after it was told
|
|
7231
|
+
// that the work ended: neither a tool call nor a request. Its tool does
|
|
7232
|
+
// not run.
|
|
7233
|
+
const stopped = this.isStoppedWorkToolUse(session, toolUseID, signal);
|
|
7234
|
+
if (stopped instanceof Promise ? await stopped : stopped) {
|
|
7235
|
+
this.logger.log(`Session ${sessionId}: denied ${toolName} ${toolUseID}, which the cancelled work asked for`);
|
|
7236
|
+
return { behavior: "deny", message: "The user cancelled this prompt." };
|
|
7237
|
+
}
|
|
7015
7238
|
const { parentToolUseId, toolCallSessionId: permissionSessionId } = this.toolUseOwner(session, sessionId, agentID);
|
|
7016
7239
|
if (agentID && !parentToolUseId) {
|
|
7017
7240
|
// The attribution rests on task_started.task_id === canUseTool's
|
|
@@ -7164,7 +7387,7 @@ export class ClaudeAcpAgent {
|
|
|
7164
7387
|
return { action: "decline" };
|
|
7165
7388
|
}
|
|
7166
7389
|
try {
|
|
7167
|
-
const response = await this.withPendingUserInput(sessionId, () => this.
|
|
7390
|
+
const response = await this.withPendingUserInput(sessionId, signal, () => this.elicitFromClient(createRequest, signal));
|
|
7168
7391
|
if (signal.aborted) {
|
|
7169
7392
|
return { action: "cancel" };
|
|
7170
7393
|
}
|
|
@@ -7196,7 +7419,7 @@ export class ClaudeAcpAgent {
|
|
|
7196
7419
|
const createRequest = askUserQuestionsToCreateRequest(questions, requestSessionId, toolUseID, this.toolCallCapabilities.air.client);
|
|
7197
7420
|
let response;
|
|
7198
7421
|
try {
|
|
7199
|
-
response = await this.withChildAwaitingUser(sessionId, requestSessionId, () => this.withPendingUserInput(sessionId, () => this.
|
|
7422
|
+
response = await this.withChildAwaitingUser(sessionId, requestSessionId, signal, () => this.withPendingUserInput(sessionId, signal, () => this.elicitFromClient(createRequest, signal), agentID));
|
|
7200
7423
|
}
|
|
7201
7424
|
catch (error) {
|
|
7202
7425
|
// A cancellation we requested (signal aborted) settles as an aborted tool
|
|
@@ -7236,7 +7459,7 @@ export class ClaudeAcpAgent {
|
|
|
7236
7459
|
}
|
|
7237
7460
|
let response;
|
|
7238
7461
|
try {
|
|
7239
|
-
response = await this.withPendingUserInput(sessionId, () => this.
|
|
7462
|
+
response = await this.withPendingUserInput(sessionId, signal, () => this.elicitFromClient(refusalFallbackToCreateRequest(prompt, sessionId), signal));
|
|
7240
7463
|
}
|
|
7241
7464
|
catch (error) {
|
|
7242
7465
|
// A cancellation we requested (signal aborted) is expected teardown;
|
|
@@ -9229,14 +9452,38 @@ function recordDispatchedToolUses(session, content) {
|
|
|
9229
9452
|
if (!Array.isArray(content))
|
|
9230
9453
|
return;
|
|
9231
9454
|
for (const block of content) {
|
|
9232
|
-
if ((block
|
|
9233
|
-
block?.type === "server_tool_use" ||
|
|
9234
|
-
block?.type === "mcp_tool_use") &&
|
|
9235
|
-
typeof block.id === "string") {
|
|
9455
|
+
if (isToolUseBlock(block))
|
|
9236
9456
|
recordDispatchedToolCall(session, block.id);
|
|
9237
|
-
}
|
|
9238
9457
|
}
|
|
9239
9458
|
}
|
|
9459
|
+
function isToolUseBlock(block) {
|
|
9460
|
+
const { type, id } = (block ?? {});
|
|
9461
|
+
return ((type === "tool_use" || type === "server_tool_use" || type === "mcp_tool_use") &&
|
|
9462
|
+
typeof id === "string");
|
|
9463
|
+
}
|
|
9464
|
+
const MAX_REACHED_TOOL_USES = 1000;
|
|
9465
|
+
/**
|
|
9466
|
+
* Records that the consumer reached the tool use `toolUseId`, and whether it
|
|
9467
|
+
* dropped it as output of the work a cancel stopped, then wakes what waits for
|
|
9468
|
+
* it (see `isStoppedWorkToolUse`). The first record of a tool use stays.
|
|
9469
|
+
*/
|
|
9470
|
+
function recordReachedToolUse(session, toolUseId, dropped) {
|
|
9471
|
+
const reached = (session.reachedToolUses ??= new Map());
|
|
9472
|
+
if (reached.has(toolUseId))
|
|
9473
|
+
return;
|
|
9474
|
+
reached.set(toolUseId, dropped);
|
|
9475
|
+
if (reached.size > MAX_REACHED_TOOL_USES) {
|
|
9476
|
+
const oldest = reached.keys().next().value;
|
|
9477
|
+
if (oldest !== undefined)
|
|
9478
|
+
reached.delete(oldest);
|
|
9479
|
+
}
|
|
9480
|
+
const waiters = session.toolUseWaiters?.get(toolUseId);
|
|
9481
|
+
if (!waiters)
|
|
9482
|
+
return;
|
|
9483
|
+
session.toolUseWaiters?.delete(toolUseId);
|
|
9484
|
+
for (const wake of waiters)
|
|
9485
|
+
wake();
|
|
9486
|
+
}
|
|
9240
9487
|
/** Streamed and permission-surfaced tools can precede the SDK's user echo. */
|
|
9241
9488
|
function recordForegroundToolCall(session, toolCallId) {
|
|
9242
9489
|
const turn = session.activeTurn ?? session.turnQueue?.find((queued) => !queued.settled);
|