@agentclientprotocol/claude-agent-acp 0.89.2-preview.1 → 0.89.2-preview.11
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 +105 -23
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +556 -165
- package/dist/async-tasks.d.ts +4 -1
- package/dist/async-tasks.d.ts.map +1 -1
- package/dist/tool-calls/background.d.ts +9 -0
- package/dist/tool-calls/background.d.ts.map +1 -1
- package/dist/tool-calls/background.js +25 -0
- package/dist/tool-calls/renderer.d.ts +21 -5
- package/dist/tool-calls/renderer.d.ts.map +1 -1
- package/dist/tool-calls/renderer.js +38 -11
- package/dist/turn-events.d.ts +39 -17
- package/dist/turn-events.d.ts.map +1 -1
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +7 -2
- package/dist/v2/prompt.d.ts +29 -18
- package/dist/v2/prompt.d.ts.map +1 -1
- package/dist/v2/prompt.js +54 -43
- package/package.json +1 -1
package/dist/acp-agent.js
CHANGED
|
@@ -32,7 +32,7 @@ import { ContextCompactionLifecycle, clientSupportsCompactionUpdates, contextCom
|
|
|
32
32
|
import { clientSupportsNotices, MAX_NOTICE_TITLE_LENGTH, normalizeNoticeText, noticeOrTranscriptUpdate, sentenceCase, splitNoticeText, } from "./session-notices.js";
|
|
33
33
|
import { applyTaskCreate, applyTaskList, applyTaskUpdate, clearHookCallbacks, completeHookCallback, createPostToolUseHook, hasHookCallback, createTaskHook, parseTaskCreateOutput, parseTaskListOutput, parseTaskUpdateOutput, planEntries, registerHookCallback, changedTaskPlanEntries, forgetPublishedTaskPlan, unregisterHookCallback, } from "./tools.js";
|
|
34
34
|
import { previewPatchContent } from "./diff.js";
|
|
35
|
-
import { backgroundedBashToolCall } from "./tool-calls/background.js";
|
|
35
|
+
import { backgroundedBashToolCall, withRunningTerminal } from "./tool-calls/background.js";
|
|
36
36
|
import { ChangedMetaFilter } from "./tool-calls/changed-meta-filter.js";
|
|
37
37
|
import { ToolCallFieldTracker } from "./tool-calls/field-tracker.js";
|
|
38
38
|
import { ClientCapabilities as ToolCallClientCapabilities } from "./tool-calls/client-capabilities.js";
|
|
@@ -280,6 +280,10 @@ const AUTONOMOUS_RESULT_ORIGINS = new Set([
|
|
|
280
280
|
"observer",
|
|
281
281
|
"observer-activity",
|
|
282
282
|
]);
|
|
283
|
+
/** The foreground work of a session (see ForegroundEvents): a prompt's turn,
|
|
284
|
+
* or a model cycle that Claude Code started on its own. At most one cycle
|
|
285
|
+
* runs at a time, so a constant names it. */
|
|
286
|
+
const AUTONOMOUS_CYCLE = "autonomous-cycle";
|
|
283
287
|
/** Whether this turn's terminal result arrived but its settlement is being
|
|
284
288
|
* held for background subagents it spawned (see Turn.deferredSettle). The
|
|
285
289
|
* single spelling of the hold predicate, shared by the consumer's settle
|
|
@@ -1194,6 +1198,15 @@ export class ClaudeAcpAgent {
|
|
|
1194
1198
|
* express: see {@link ToolCallClientCapabilities.v2}.
|
|
1195
1199
|
*/
|
|
1196
1200
|
v2;
|
|
1201
|
+
/** See the `foreground` constructor option. */
|
|
1202
|
+
foregroundEvents;
|
|
1203
|
+
/**
|
|
1204
|
+
* The foreground work of each session while it runs, by the session id that
|
|
1205
|
+
* the client knows. A clear-context restart replaces the Claude Code session
|
|
1206
|
+
* behind that id and moves the running turn to it, so the work belongs to the
|
|
1207
|
+
* id, not to a {@link Session}.
|
|
1208
|
+
*/
|
|
1209
|
+
foregroundWork = new Map();
|
|
1197
1210
|
/** The session index (docs/air-extensions.md, "Session index"). */
|
|
1198
1211
|
sessionIndex = new sessionIndex.SessionIndexConnection({
|
|
1199
1212
|
agent: this,
|
|
@@ -1203,6 +1216,7 @@ export class ClaudeAcpAgent {
|
|
|
1203
1216
|
});
|
|
1204
1217
|
constructor(client, logger, options = {}) {
|
|
1205
1218
|
this.v2 = options.v2 ?? false;
|
|
1219
|
+
this.foregroundEvents = options.foreground;
|
|
1206
1220
|
this.sessions = {};
|
|
1207
1221
|
this.client = new ChangedMetaClient(client, () => this.toolCallCapabilities.air.client);
|
|
1208
1222
|
this.logger = logger ?? console;
|
|
@@ -1987,13 +2001,7 @@ export class ClaudeAcpAgent {
|
|
|
1987
2001
|
async prompt(params) {
|
|
1988
2002
|
let events;
|
|
1989
2003
|
const outcome = new Promise((resolve, reject) => {
|
|
1990
|
-
events = {
|
|
1991
|
-
inserted() { },
|
|
1992
|
-
awaitingUser() { },
|
|
1993
|
-
resumed() { },
|
|
1994
|
-
ended: resolve,
|
|
1995
|
-
failed: reject,
|
|
1996
|
-
};
|
|
2004
|
+
events = { inserted() { }, ended: resolve, failed: reject };
|
|
1997
2005
|
});
|
|
1998
2006
|
await this.startTurn(params, events);
|
|
1999
2007
|
return outcome;
|
|
@@ -2066,10 +2074,12 @@ export class ClaudeAcpAgent {
|
|
|
2066
2074
|
completeTurn = resolve;
|
|
2067
2075
|
}),
|
|
2068
2076
|
resolve: (outcome) => {
|
|
2077
|
+
this.endForeground(params.sessionId, turn, { outcome });
|
|
2069
2078
|
events.ended(outcome);
|
|
2070
2079
|
completeTurn();
|
|
2071
2080
|
},
|
|
2072
2081
|
reject: (error) => {
|
|
2082
|
+
this.endForeground(params.sessionId, turn, { error });
|
|
2073
2083
|
events.failed(error);
|
|
2074
2084
|
completeTurn();
|
|
2075
2085
|
},
|
|
@@ -2387,7 +2397,7 @@ export class ClaudeAcpAgent {
|
|
|
2387
2397
|
// Deliver into the running turn rather than queuing behind it as a fresh
|
|
2388
2398
|
// prompt would.
|
|
2389
2399
|
userMessage.priority =
|
|
2390
|
-
(session.
|
|
2400
|
+
(session.openUserInputs?.size ?? 0) > 0 ? STEER_PRIORITY_LATER : STEER_PRIORITY_NOW;
|
|
2391
2401
|
// Mark before the push and in the same synchronous section as the in-flight
|
|
2392
2402
|
// check: the interrupt can have the CLI finalizing the aborted cycle by the
|
|
2393
2403
|
// time the consumer next runs, and an unmarked result would settle the turn
|
|
@@ -2400,6 +2410,8 @@ export class ClaudeAcpAgent {
|
|
|
2400
2410
|
if (turnInFlight.deferredSettle !== undefined) {
|
|
2401
2411
|
turnInFlight.steeredSettle = turnInFlight.deferredSettle;
|
|
2402
2412
|
turnInFlight.deferredSettle = undefined;
|
|
2413
|
+
// The held turn's work had ended at its result; the steer resumes it.
|
|
2414
|
+
this.startForeground(sessionId, turnInFlight);
|
|
2403
2415
|
}
|
|
2404
2416
|
session.input.push(userMessage);
|
|
2405
2417
|
const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
|
|
@@ -2488,6 +2500,10 @@ export class ClaudeAcpAgent {
|
|
|
2488
2500
|
streamedBlocksByParent.set(key, (blocks = []));
|
|
2489
2501
|
return blocks;
|
|
2490
2502
|
};
|
|
2503
|
+
// The streams, keyed like streamedBlocksByParent, that dropped a delta
|
|
2504
|
+
// after a cancel since their record last reset, so the record no longer
|
|
2505
|
+
// matches what the client got (see the stream_event case).
|
|
2506
|
+
const droppedStreams = new Set();
|
|
2491
2507
|
// A client gets the consolidated subagent text when it negotiated the
|
|
2492
2508
|
// transcript extension or the `forwardSubagentText` session option. AIR
|
|
2493
2509
|
// also gets it with native subagent sessions, and AIR gets no streamed
|
|
@@ -2706,6 +2722,7 @@ export class ClaudeAcpAgent {
|
|
|
2706
2722
|
const activateTurn = (turn) => {
|
|
2707
2723
|
session.activeTurn = turn;
|
|
2708
2724
|
session.cancelled = false;
|
|
2725
|
+
session.cancelledPrompts = undefined;
|
|
2709
2726
|
compaction.resume();
|
|
2710
2727
|
if (turn.localCommand?.startsAtActivation)
|
|
2711
2728
|
ensureLocalCommandMarkdown(turn);
|
|
@@ -2742,7 +2759,7 @@ export class ClaudeAcpAgent {
|
|
|
2742
2759
|
reportInserted(turn);
|
|
2743
2760
|
// A request can already be open: Claude Code asks for a queued prompt
|
|
2744
2761
|
// before the consumer reaches its echo.
|
|
2745
|
-
this.syncAwaitingUser(session);
|
|
2762
|
+
this.syncAwaitingUser(params.sessionId, session);
|
|
2746
2763
|
};
|
|
2747
2764
|
/** Report that Claude Code took the turn's prompt in, once. A turn can be
|
|
2748
2765
|
* settled by now: an echo hand-off awaits settling the previous turn, and
|
|
@@ -2752,6 +2769,7 @@ export class ClaudeAcpAgent {
|
|
|
2752
2769
|
return;
|
|
2753
2770
|
turn.insertedReported = true;
|
|
2754
2771
|
turn.events.inserted(turn.promptUuid);
|
|
2772
|
+
this.startForeground(params.sessionId, turn);
|
|
2755
2773
|
};
|
|
2756
2774
|
/** Ensure there is an active turn before a user-turn result that carries no
|
|
2757
2775
|
* echo to activate it, by promoting the queue head. Most turns are
|
|
@@ -2776,7 +2794,7 @@ export class ClaudeAcpAgent {
|
|
|
2776
2794
|
* a stamp naming an orphaned command consumes the result outright, and a
|
|
2777
2795
|
* stamp naming anything else positively refutes "this is a dead turn's
|
|
2778
2796
|
* result", so the dup-over-loss one-skip must not eat it. */
|
|
2779
|
-
const ensureActiveTurn = async (resultUserMessageUuid) => {
|
|
2797
|
+
const ensureActiveTurn = async (resultUserMessageUuid, resultUserMessageUuids) => {
|
|
2780
2798
|
if (session.activeTurn) {
|
|
2781
2799
|
if (!isHeldOpen(session.activeTurn)) {
|
|
2782
2800
|
return;
|
|
@@ -2802,8 +2820,8 @@ export class ClaudeAcpAgent {
|
|
|
2802
2820
|
}
|
|
2803
2821
|
// Orphan accounting runs BEFORE the head check: an orphan's echo-less
|
|
2804
2822
|
// result can arrive with an EMPTY queue (the common post-cancel
|
|
2805
|
-
// timeline — the
|
|
2806
|
-
// user hasn't typed yet), and it must still be consumed here. Skipping
|
|
2823
|
+
// timeline — the cancelled turn settled at its interrupted result and
|
|
2824
|
+
// the user hasn't typed yet), and it must still be consumed here. Skipping
|
|
2807
2825
|
// the bookkeeping when there is nothing to promote would leave a
|
|
2808
2826
|
// phantom entry/count that swallows the next live echo-less result
|
|
2809
2827
|
// (e.g. /compact) instead.
|
|
@@ -2872,6 +2890,14 @@ export class ClaudeAcpAgent {
|
|
|
2872
2890
|
}
|
|
2873
2891
|
}
|
|
2874
2892
|
}
|
|
2893
|
+
// A result that names a prompt a cancel stopped is that prompt's, even
|
|
2894
|
+
// when the orphan accounting above lost track of it: the interrupt
|
|
2895
|
+
// receipt leaves out a prompt that Claude Code had already taken off its
|
|
2896
|
+
// queue. It must not settle the prompt sent after the cancel.
|
|
2897
|
+
const stamps = resultUserMessageUuids ??
|
|
2898
|
+
(resultUserMessageUuid !== undefined ? [resultUserMessageUuid] : []);
|
|
2899
|
+
if (stamps.some((uuid) => session.cancelledPrompts?.has(uuid)))
|
|
2900
|
+
return;
|
|
2875
2901
|
const head = firstUnsettledQueuedTurn();
|
|
2876
2902
|
if (!head) {
|
|
2877
2903
|
return;
|
|
@@ -2971,17 +2997,53 @@ export class ClaudeAcpAgent {
|
|
|
2971
2997
|
};
|
|
2972
2998
|
/** Registers a live background task (see `liveBackgroundTasks`). A
|
|
2973
2999
|
* subagent is also recorded on the active turn, so that turn holds until
|
|
2974
|
-
* the subagent settles (see `Turn.spawnedTaskIds`).
|
|
2975
|
-
|
|
2976
|
-
|
|
3000
|
+
* the subagent settles (see `Turn.spawnedTaskIds`). Its requests may
|
|
3001
|
+
* already be open, and where it runs decides whether they block the
|
|
3002
|
+
* foreground work, so that is checked again. */
|
|
3003
|
+
const registerLiveTask = (taskId, parentToolUseId, isSubagent, placement = {}) => {
|
|
3004
|
+
session.liveBackgroundTasks.set(taskId, {
|
|
3005
|
+
parentToolUseId,
|
|
3006
|
+
isSubagent,
|
|
3007
|
+
...(placement.backgrounded !== undefined ? { backgrounded: placement.backgrounded } : {}),
|
|
3008
|
+
...(placement.parentTaskId !== undefined ? { parentTaskId: placement.parentTaskId } : {}),
|
|
3009
|
+
});
|
|
2977
3010
|
if (parentToolUseId)
|
|
2978
3011
|
forgetForegroundToolCall(session, parentToolUseId);
|
|
2979
3012
|
session.resumableSubagents?.delete(taskId);
|
|
2980
3013
|
if (isSubagent && session.activeTurn && !session.activeTurn.settled) {
|
|
2981
3014
|
(session.activeTurn.spawnedTaskIds ??= new Set()).add(taskId);
|
|
2982
3015
|
}
|
|
3016
|
+
this.syncAwaitingUser(params.sessionId, session);
|
|
3017
|
+
};
|
|
3018
|
+
/** Ends the display terminal of a command that went to the background, once
|
|
3019
|
+
* its task ends (see `Session.backgroundTerminals`). Claude Code reports no
|
|
3020
|
+
* exit code for it, and `completed` does not mean 0: Claude Code accepts
|
|
3021
|
+
* some non-zero codes as a success, such as grep's 1. */
|
|
3022
|
+
const endBackgroundTerminal = async (taskId) => {
|
|
3023
|
+
const terminal = session.backgroundTerminals?.get(taskId);
|
|
3024
|
+
if (!terminal)
|
|
3025
|
+
return;
|
|
3026
|
+
session.backgroundTerminals.delete(taskId);
|
|
3027
|
+
await sendUpdate({
|
|
3028
|
+
sessionId: params.sessionId,
|
|
3029
|
+
update: {
|
|
3030
|
+
sessionUpdate: "tool_call_update",
|
|
3031
|
+
toolCallId: terminal.toolCallId,
|
|
3032
|
+
_meta: {
|
|
3033
|
+
...(terminal.parentToolUseId
|
|
3034
|
+
? { claudeCode: { parentToolUseId: terminal.parentToolUseId } }
|
|
3035
|
+
: {}),
|
|
3036
|
+
terminal_exit: { terminal_id: terminal.toolCallId, exit_code: null, signal: null },
|
|
3037
|
+
},
|
|
3038
|
+
},
|
|
3039
|
+
});
|
|
3040
|
+
};
|
|
3041
|
+
/** Settles a task. Its request may still be open, and an agent without a
|
|
3042
|
+
* record no longer blocks the foreground work, so that is checked again. */
|
|
3043
|
+
const settleLiveTask = (taskId) => {
|
|
3044
|
+
settleLiveBackgroundTask(session, taskId);
|
|
3045
|
+
this.syncAwaitingUser(params.sessionId, session);
|
|
2983
3046
|
};
|
|
2984
|
-
const settleLiveTask = (taskId) => settleLiveBackgroundTask(session, taskId);
|
|
2985
3047
|
/** Registers a settled subagent again when the SDK resumes it without a
|
|
2986
3048
|
* new `task_started`. The active turn is the one that resumed it, so it
|
|
2987
3049
|
* holds until the subagent settles again. A repeated resume signal finds
|
|
@@ -2990,8 +3052,9 @@ export class ClaudeAcpAgent {
|
|
|
2990
3052
|
if (session.liveBackgroundTasks.has(taskId))
|
|
2991
3053
|
return;
|
|
2992
3054
|
const settled = session.resumableSubagents?.get(taskId);
|
|
3055
|
+
// Claude Code always resumes a subagent in the background.
|
|
2993
3056
|
if (settled)
|
|
2994
|
-
registerLiveTask(taskId, settled.parentToolUseId, true);
|
|
3057
|
+
registerLiveTask(taskId, settled.parentToolUseId, true, { backgrounded: true });
|
|
2995
3058
|
};
|
|
2996
3059
|
/** Whether any background subagent this turn spawned is still live —
|
|
2997
3060
|
* while true, the turn's settlement stays deferred so the subagent's
|
|
@@ -3044,6 +3107,8 @@ export class ClaudeAcpAgent {
|
|
|
3044
3107
|
!session.activeTurn.settled &&
|
|
3045
3108
|
turnAwaitingSubagents(session.activeTurn)) {
|
|
3046
3109
|
session.activeTurn.deferredSettle = outcome;
|
|
3110
|
+
// The hold delays v1's answer; the turn's work ended at its result.
|
|
3111
|
+
this.endForeground(params.sessionId, session.activeTurn, { outcome });
|
|
3047
3112
|
}
|
|
3048
3113
|
else {
|
|
3049
3114
|
await settleActive(outcome);
|
|
@@ -3067,9 +3132,14 @@ export class ClaudeAcpAgent {
|
|
|
3067
3132
|
// Check only this turn's tools. A tool_result removes its id from
|
|
3068
3133
|
// emittedToolCalls even when its PostToolUse hook has not arrived yet.
|
|
3069
3134
|
const unfinished = [...(turn.foregroundToolCallIds ?? [])].filter((id) => session.emittedToolCalls.has(id) && !backgroundTools.has(id));
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3135
|
+
const renderer = new AcpToolCallRenderer(this.toolCallCapabilities);
|
|
3136
|
+
/**
|
|
3137
|
+
* Closes a tool call that the turn ended without a result for
|
|
3138
|
+
* (`AcpToolCallRenderer.unfinished`). Returns false when the turn
|
|
3139
|
+
* ended meanwhile.
|
|
3140
|
+
*/
|
|
3141
|
+
const closeUnfinished = async (toolCallId, ran) => {
|
|
3142
|
+
// A late hook must not overwrite the report we are about to send.
|
|
3073
3143
|
unregisterHookCallback(toolCallId);
|
|
3074
3144
|
session.emittedToolCalls.delete(toolCallId);
|
|
3075
3145
|
delete session.toolUseCache[toolCallId];
|
|
@@ -3077,12 +3147,7 @@ export class ClaudeAcpAgent {
|
|
|
3077
3147
|
session.dispatchedToolCalls?.delete(toolCallId);
|
|
3078
3148
|
await sendUpdate({
|
|
3079
3149
|
sessionId: params.sessionId,
|
|
3080
|
-
update: {
|
|
3081
|
-
sessionUpdate: "tool_call_update",
|
|
3082
|
-
toolCallId,
|
|
3083
|
-
status: "failed",
|
|
3084
|
-
content: [{ type: "content", content: { type: "text", text } }],
|
|
3085
|
-
},
|
|
3150
|
+
update: renderer.unfinished({ toolCallId, ran }),
|
|
3086
3151
|
});
|
|
3087
3152
|
// Cancellation can arrive while we await the client update.
|
|
3088
3153
|
if (turn.settled || session.activeTurn !== turn)
|
|
@@ -3099,7 +3164,7 @@ export class ClaudeAcpAgent {
|
|
|
3099
3164
|
const abandoned = unfinished.filter((id) => !session.dispatchedToolCalls?.has(id));
|
|
3100
3165
|
const stuck = unfinished.filter((id) => session.dispatchedToolCalls?.has(id));
|
|
3101
3166
|
for (const toolCallId of abandoned) {
|
|
3102
|
-
if (!(await
|
|
3167
|
+
if (!(await closeUnfinished(toolCallId, false))) {
|
|
3103
3168
|
return;
|
|
3104
3169
|
}
|
|
3105
3170
|
}
|
|
@@ -3108,7 +3173,7 @@ export class ClaudeAcpAgent {
|
|
|
3108
3173
|
this.logger.error(`Session ${params.sessionId}, turn ${turn.promptUuid}, stopReason=${result.stopReason}: ${message}`);
|
|
3109
3174
|
// Fail every unfinished tool before reporting one error for the prompt.
|
|
3110
3175
|
for (const toolCallId of stuck) {
|
|
3111
|
-
if (!(await
|
|
3176
|
+
if (!(await closeUnfinished(toolCallId, true))) {
|
|
3112
3177
|
return;
|
|
3113
3178
|
}
|
|
3114
3179
|
}
|
|
@@ -3220,6 +3285,7 @@ export class ClaudeAcpAgent {
|
|
|
3220
3285
|
/** Reject every in-flight turn — used when the stream dies. */
|
|
3221
3286
|
const failAllTurns = (error) => {
|
|
3222
3287
|
disarmForceCancel(session);
|
|
3288
|
+
this.endAutonomousCycle(params.sessionId, { error });
|
|
3223
3289
|
const turns = session.activeTurn
|
|
3224
3290
|
? [session.activeTurn, ...(session.turnQueue ?? []).filter((t) => t !== session.activeTurn)]
|
|
3225
3291
|
: [...(session.turnQueue ?? [])];
|
|
@@ -3317,9 +3383,9 @@ export class ClaudeAcpAgent {
|
|
|
3317
3383
|
}
|
|
3318
3384
|
}
|
|
3319
3385
|
else if (active.commandResultSeen) {
|
|
3320
|
-
// Its result was already consumed (
|
|
3321
|
-
//
|
|
3322
|
-
//
|
|
3386
|
+
// Its result was already consumed (at the cancelled guard);
|
|
3387
|
+
// only the terminal frame is outstanding, which no-ops with no
|
|
3388
|
+
// entry. Nothing to track.
|
|
3323
3389
|
}
|
|
3324
3390
|
else {
|
|
3325
3391
|
// The wedged turn WAS dispatched (it's active), so track it
|
|
@@ -3386,6 +3452,9 @@ export class ClaudeAcpAgent {
|
|
|
3386
3452
|
// it), but the stored one is the authoritative source.
|
|
3387
3453
|
const inFlight = session.activeTurn;
|
|
3388
3454
|
await finishLifecycle(session.cancelled ? "cancelled" : "failed", session.cancelled ? "stopped" : "failed", "at end of stream");
|
|
3455
|
+
this.endAutonomousCycle(params.sessionId, session.cancelled
|
|
3456
|
+
? { outcome: { stopReason: "cancelled" } }
|
|
3457
|
+
: { error: RequestError.internalError(undefined, SESSION_ENDED_MESSAGE) });
|
|
3389
3458
|
await settleActive(session.cancelled
|
|
3390
3459
|
? cancelledOutcome(session, inFlight)
|
|
3391
3460
|
: (inFlight?.deferredSettle ?? turnOutcome(session, stopReason)));
|
|
@@ -3528,6 +3597,7 @@ export class ClaudeAcpAgent {
|
|
|
3528
3597
|
await this.publishRuntimeGoal(params.sessionId, toGoalSnapshot(activeGoal));
|
|
3529
3598
|
continue;
|
|
3530
3599
|
}
|
|
3600
|
+
this.startAutonomousCycle(params.sessionId, session, message);
|
|
3531
3601
|
switch (message.type) {
|
|
3532
3602
|
case "system":
|
|
3533
3603
|
switch (message.subtype) {
|
|
@@ -3539,6 +3609,9 @@ export class ClaudeAcpAgent {
|
|
|
3539
3609
|
if (message.capabilities?.includes("msg_lifecycle_v1")) {
|
|
3540
3610
|
session.msgLifecycleV1 = true;
|
|
3541
3611
|
}
|
|
3612
|
+
if (message.capabilities?.includes("request_marker_lists_v1")) {
|
|
3613
|
+
session.requestMarkerListsV1 = true;
|
|
3614
|
+
}
|
|
3542
3615
|
// A fresh `system`/init (e.g. after reinitialize) can carry an
|
|
3543
3616
|
// updated Fast mode state; reconcile it with what we seeded at
|
|
3544
3617
|
// session creation.
|
|
@@ -3574,6 +3647,24 @@ export class ClaudeAcpAgent {
|
|
|
3574
3647
|
}
|
|
3575
3648
|
break;
|
|
3576
3649
|
case "status": {
|
|
3650
|
+
// A model request that serves none of the prompts a cancel
|
|
3651
|
+
// stopped is new work, such as Claude Code's follow-up after a
|
|
3652
|
+
// background command finished. It comes after the stopped
|
|
3653
|
+
// work's result: Claude Code ends the interrupted turn first,
|
|
3654
|
+
// then runs any prompt it had already taken off its queue (its
|
|
3655
|
+
// request names it, so the cancel still drops it), then its own
|
|
3656
|
+
// follow-ups, whose requests name no prompt.
|
|
3657
|
+
if (message.status === "requesting" &&
|
|
3658
|
+
session.cancelled &&
|
|
3659
|
+
session.requestMarkerListsV1 &&
|
|
3660
|
+
!servesCancelledWork(session, message)) {
|
|
3661
|
+
session.cancelled = false;
|
|
3662
|
+
session.cancelledPrompts = undefined;
|
|
3663
|
+
// The stopped work's tail has drained, as at activation.
|
|
3664
|
+
compaction.resume();
|
|
3665
|
+
}
|
|
3666
|
+
if (message.status === "requesting")
|
|
3667
|
+
session.modelRequestSinceResult = true;
|
|
3577
3668
|
if (message.status === "compacting") {
|
|
3578
3669
|
await compaction.start(message.uuid);
|
|
3579
3670
|
}
|
|
@@ -3665,6 +3756,17 @@ export class ClaudeAcpAgent {
|
|
|
3665
3756
|
session.owedTrailingIdles = 0;
|
|
3666
3757
|
}
|
|
3667
3758
|
if (message.state === "idle") {
|
|
3759
|
+
// A model cycle that Claude Code started on its own normally
|
|
3760
|
+
// ends at its result. An idle that no earlier result owes
|
|
3761
|
+
// means that result will not come (the stream dropped
|
|
3762
|
+
// mid-cycle, as for a turn in issue #825), so the cycle ends
|
|
3763
|
+
// here. An owed idle belongs to an earlier result: it can lag
|
|
3764
|
+
// past the cycle's first output.
|
|
3765
|
+
if (session.owedTrailingIdles === 0) {
|
|
3766
|
+
this.endAutonomousCycle(params.sessionId, {
|
|
3767
|
+
outcome: { stopReason: session.cancelled ? "cancelled" : "end_turn" },
|
|
3768
|
+
});
|
|
3769
|
+
}
|
|
3668
3770
|
// A non-cancelled turn normally settled at its terminal
|
|
3669
3771
|
// `result` already (issue #773), and that result recorded an
|
|
3670
3772
|
// owed trailing idle — absorbed here via the decrement. We
|
|
@@ -3674,9 +3776,9 @@ export class ClaudeAcpAgent {
|
|
|
3674
3776
|
// the time it arrives the SDK may have echoed the NEXT turn
|
|
3675
3777
|
// and activated it — settling now would resolve that new
|
|
3676
3778
|
// turn prematurely with end_turn and ~zero usage, dropping
|
|
3677
|
-
// its real result. A cancelled turn
|
|
3678
|
-
//
|
|
3679
|
-
//
|
|
3779
|
+
// its real result. A cancelled turn settles at its
|
|
3780
|
+
// interrupted result (at the `session.cancelled` guard); one
|
|
3781
|
+
// whose result never came settles here.
|
|
3680
3782
|
//
|
|
3681
3783
|
// An idle that is NOT owed while the active turn is still
|
|
3682
3784
|
// unsettled is the issue #825 signature: `idle` is the SDK's
|
|
@@ -3688,12 +3790,9 @@ export class ClaudeAcpAgent {
|
|
|
3688
3790
|
// the turn NOW so its session/prompt gets a terminal
|
|
3689
3791
|
// response, instead of leaving it hanging until the next
|
|
3690
3792
|
// prompt drains the wreckage.
|
|
3691
|
-
// A cancelled turn
|
|
3692
|
-
//
|
|
3693
|
-
//
|
|
3694
|
-
// report it — clients metering spend would otherwise lose
|
|
3695
|
-
// the interrupted turn's tokens entirely (issue #844). Zero
|
|
3696
|
-
// when the cancel pre-empted the result (wedge/force-cancel).
|
|
3793
|
+
// A cancelled turn reaches this with no result of its own,
|
|
3794
|
+
// so its usage is zero (wedge/force-cancel), or, for a held
|
|
3795
|
+
// turn, the usage its result recorded (issue #844).
|
|
3697
3796
|
if (session.cancelled && session.activeTurn && !session.activeTurn.settled) {
|
|
3698
3797
|
// A held turn's result passed long ago; this idle is the
|
|
3699
3798
|
// trailer of the followup cycle the cancel interrupted,
|
|
@@ -3746,8 +3845,11 @@ export class ClaudeAcpAgent {
|
|
|
3746
3845
|
// Via the subagent gate, not settleActive: a steered turn
|
|
3747
3846
|
// can also have spawned background subagents, which own it
|
|
3748
3847
|
// from here (settles now if none is live, holds otherwise).
|
|
3848
|
+
const outcome = session.activeTurn.steeredSettle;
|
|
3749
3849
|
const steered = session.activeTurn;
|
|
3750
|
-
steered.deferredSettle =
|
|
3850
|
+
steered.deferredSettle = outcome;
|
|
3851
|
+
// A hold delays v1's answer; the turn's work ends here.
|
|
3852
|
+
this.endForeground(params.sessionId, steered, { outcome });
|
|
3751
3853
|
leaveSteerLane(steered);
|
|
3752
3854
|
await settleDeferredIfDrained();
|
|
3753
3855
|
}
|
|
@@ -3962,7 +4064,10 @@ export class ClaudeAcpAgent {
|
|
|
3962
4064
|
// turn: a turn only ever waits on its own subagents, and a
|
|
3963
4065
|
// spawn during a held-open drain window (an agent chain)
|
|
3964
4066
|
// extends that turn's hold.
|
|
3965
|
-
registerLiveTask(message.task_id, message.tool_use_id, !!message.subagent_type
|
|
4067
|
+
registerLiveTask(message.task_id, message.tool_use_id, !!message.subagent_type, {
|
|
4068
|
+
backgrounded: message.is_backgrounded,
|
|
4069
|
+
parentTaskId: message.parent_task_id,
|
|
4070
|
+
});
|
|
3966
4071
|
await subagents.taskStarted({
|
|
3967
4072
|
taskId: message.task_id,
|
|
3968
4073
|
toolUseId: message.tool_use_id,
|
|
@@ -4000,7 +4105,9 @@ export class ClaudeAcpAgent {
|
|
|
4000
4105
|
subagents.discardPending(message.tool_use_id);
|
|
4001
4106
|
// The subagent streams no more text under this parent.
|
|
4002
4107
|
streamedBlocksByParent.delete(message.tool_use_id);
|
|
4108
|
+
droppedStreams.delete(message.tool_use_id);
|
|
4003
4109
|
}
|
|
4110
|
+
await endBackgroundTerminal(message.task_id);
|
|
4004
4111
|
settleLiveTask(message.task_id);
|
|
4005
4112
|
break;
|
|
4006
4113
|
case "task_updated":
|
|
@@ -4009,6 +4116,14 @@ export class ClaudeAcpAgent {
|
|
|
4009
4116
|
break;
|
|
4010
4117
|
}
|
|
4011
4118
|
await asyncTasks.taskUpdated(message.task_id, message.patch);
|
|
4119
|
+
// A task moved to the background no longer blocks the tool
|
|
4120
|
+
// call that spawned it, nor its requests the foreground work.
|
|
4121
|
+
if (message.patch.is_backgrounded !== undefined) {
|
|
4122
|
+
const task = session.liveBackgroundTasks.get(message.task_id);
|
|
4123
|
+
if (task)
|
|
4124
|
+
task.backgrounded = message.patch.is_backgrounded;
|
|
4125
|
+
this.syncAwaitingUser(params.sessionId, session);
|
|
4126
|
+
}
|
|
4012
4127
|
// terminal-status task_updated patch and a (deduplicated)
|
|
4013
4128
|
// task_notification when a task settles, but only the patch is
|
|
4014
4129
|
// guaranteed per transition — prune on it too so the registry
|
|
@@ -4019,8 +4134,11 @@ export class ClaudeAcpAgent {
|
|
|
4019
4134
|
message.patch.status === "killed") {
|
|
4020
4135
|
await subagents.finishTask(message.task_id, message.patch.status, sendUpdate);
|
|
4021
4136
|
const parentToolUseId = session.liveBackgroundTasks.get(message.task_id)?.parentToolUseId;
|
|
4022
|
-
if (parentToolUseId)
|
|
4137
|
+
if (parentToolUseId) {
|
|
4023
4138
|
streamedBlocksByParent.delete(parentToolUseId);
|
|
4139
|
+
droppedStreams.delete(parentToolUseId);
|
|
4140
|
+
}
|
|
4141
|
+
await endBackgroundTerminal(message.task_id);
|
|
4024
4142
|
settleLiveTask(message.task_id);
|
|
4025
4143
|
}
|
|
4026
4144
|
else if (message.patch.status === "running" ||
|
|
@@ -4230,6 +4348,7 @@ export class ClaudeAcpAgent {
|
|
|
4230
4348
|
}
|
|
4231
4349
|
break;
|
|
4232
4350
|
case "result": {
|
|
4351
|
+
session.modelRequestSinceResult = false;
|
|
4233
4352
|
// The result ends the model turn. A background task that still
|
|
4234
4353
|
// waits for its tool call id gets its spawn now, without the id.
|
|
4235
4354
|
await asyncTasks.releaseHeld();
|
|
@@ -4264,7 +4383,7 @@ export class ClaudeAcpAgent {
|
|
|
4264
4383
|
// the map in that case).
|
|
4265
4384
|
if (!isAutonomousResult) {
|
|
4266
4385
|
recordResultForOrphanCommands();
|
|
4267
|
-
await ensureActiveTurn(message.user_message_uuid);
|
|
4386
|
+
await ensureActiveTurn(message.user_message_uuid, message.user_message_uuids);
|
|
4268
4387
|
// A result that names a steer consumed it (see
|
|
4269
4388
|
// Turn.steeredEchoes), so the steered work is done: retire
|
|
4270
4389
|
// its echo — the CLI may never have replayed it — and, with
|
|
@@ -4325,13 +4444,10 @@ export class ClaudeAcpAgent {
|
|
|
4325
4444
|
// orphan skip) and the SDK follows it with a trailing
|
|
4326
4445
|
// `session_state_changed: idle` — record the debt so the idle
|
|
4327
4446
|
// handler absorbs that idle rather than reading it as a turn the
|
|
4328
|
-
// SDK abandoned (issue #825).
|
|
4329
|
-
// turn
|
|
4330
|
-
// guard
|
|
4331
|
-
//
|
|
4332
|
-
// the debt there instead) — counting here too would double it.
|
|
4333
|
-
// Results skipped while cancelled with NO active turn — orphaned
|
|
4334
|
-
// queued turns the SDK still ran, or a force-cancelled turn's
|
|
4447
|
+
// SDK abandoned (issue #825). That includes the result of a
|
|
4448
|
+
// cancelled turn, which settles it at the `session.cancelled`
|
|
4449
|
+
// guard below. Results skipped while cancelled with NO active
|
|
4450
|
+
// turn — orphaned queued turns the SDK still ran, or a force-cancelled turn's
|
|
4335
4451
|
// late result after the backstop settled it — get no such settle,
|
|
4336
4452
|
// so their trailers must be counted here or they'd later be read
|
|
4337
4453
|
// as the next healthy turn being abandoned and false-fail it.
|
|
@@ -4344,20 +4460,17 @@ export class ClaudeAcpAgent {
|
|
|
4344
4460
|
// but a held turn settling AT a followup result unblocks the
|
|
4345
4461
|
// client at exactly that point, making the race the common
|
|
4346
4462
|
// case.
|
|
4347
|
-
//
|
|
4348
|
-
// turn's OWN result — a followup result arriving inside the
|
|
4349
|
-
// cancel window still gets its own trailer and must be counted,
|
|
4350
|
-
// or that idle would later false-fail the next prompt.
|
|
4351
|
-
// A second exclusion: a steered turn's own results (see
|
|
4463
|
+
// One exclusion: a steered turn's own results (see
|
|
4352
4464
|
// Turn.steeredEchoes). One idle covers the whole interrupted +
|
|
4353
4465
|
// steered sequence and that idle settles the turn, so counting
|
|
4354
4466
|
// either result would leave a debt that swallows it. Autonomous
|
|
4355
4467
|
// results inside a steered turn keep their own trailers, and so
|
|
4356
4468
|
// does a result that named the steer: it took the turn out of the
|
|
4357
|
-
// steer lane above and settles at the result, not the idle.
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4469
|
+
// steer lane above and settles at the result, not the idle. So
|
|
4470
|
+
// does a cancelled steered turn's result, which settles it at the
|
|
4471
|
+
// `session.cancelled` guard below.
|
|
4472
|
+
const owesTrailingIdle = isAutonomousResult || !isSteering(session.activeTurn) || session.cancelled;
|
|
4473
|
+
if (owesTrailingIdle) {
|
|
4361
4474
|
session.owedTrailingIdles++;
|
|
4362
4475
|
}
|
|
4363
4476
|
// Accumulate usage into the user turn's tally. Skip autonomous
|
|
@@ -4443,6 +4556,26 @@ export class ClaudeAcpAgent {
|
|
|
4443
4556
|
}),
|
|
4444
4557
|
});
|
|
4445
4558
|
}
|
|
4559
|
+
// A model cycle that Claude Code started on its own ends at its
|
|
4560
|
+
// result, after its usage, also after a cancel. A turn that
|
|
4561
|
+
// Claude Code takes in takes the cycle over, so a cycle that
|
|
4562
|
+
// still runs owns this result, whatever its origin says. A
|
|
4563
|
+
// placeholder result (`num_turns: 0`, see the autonomous lane
|
|
4564
|
+
// below) comes before the shared followup runs, so it ends
|
|
4565
|
+
// nothing. The diagnostic of an ExitPlanMode answer that
|
|
4566
|
+
// interrupted the cycle ends it `cancelled`, as it ends a turn,
|
|
4567
|
+
// unless a held turn continues the accepted plan in a fresh
|
|
4568
|
+
// context: that turn takes the cycle over (see the autonomous
|
|
4569
|
+
// lane below).
|
|
4570
|
+
const exitPlanInterruption = pendingExitPlanModeInterruption !== undefined &&
|
|
4571
|
+
isExitPlanInterruptionResult(message, pendingExitPlanModeInterruption);
|
|
4572
|
+
const continuesPlan = exitPlanInterruption &&
|
|
4573
|
+
isHeldOpen(session.activeTurn) &&
|
|
4574
|
+
pendingExitPlanContextReset?.toolUseId ===
|
|
4575
|
+
pendingExitPlanModeInterruption?.toolUseId;
|
|
4576
|
+
if (!continuesPlan && !(isAutonomousResult && message.num_turns === 0)) {
|
|
4577
|
+
this.endAutonomousCycle(params.sessionId, autonomousCycleEnd(message, resultModelUsage, session.cancelled || exitPlanInterruption || isAbortedResult(message)));
|
|
4578
|
+
}
|
|
4446
4579
|
if (session.cancelled) {
|
|
4447
4580
|
session.pendingExitPlanModeInterruption = undefined;
|
|
4448
4581
|
session.pendingExitPlanContextReset = undefined;
|
|
@@ -4450,6 +4583,21 @@ export class ClaudeAcpAgent {
|
|
|
4450
4583
|
await clearFailuresFromEarlierTurns();
|
|
4451
4584
|
stopReason = "cancelled";
|
|
4452
4585
|
}
|
|
4586
|
+
// The cancelled turn ends at the result of the work the
|
|
4587
|
+
// interrupt stopped: its own result, or for a turn held open
|
|
4588
|
+
// for its subagents (see Turn.deferredSettle), its follow-up's
|
|
4589
|
+
// aborted one. A follow-up result that is not aborted, such as
|
|
4590
|
+
// a placeholder or one that ended before the interrupt landed,
|
|
4591
|
+
// belongs to other work. Claude Code ends the stopped work
|
|
4592
|
+
// first, then goes on with its queue, so the idle can come
|
|
4593
|
+
// much later, after a follow-up. That idle, the next echo's
|
|
4594
|
+
// hand-off, and the force-cancel backstop settle a turn whose
|
|
4595
|
+
// result never comes.
|
|
4596
|
+
const active = session.activeTurn;
|
|
4597
|
+
const stoppedWork = !isAutonomousResult || (isHeldOpen(active) && isAbortedResult(message));
|
|
4598
|
+
if (active && !active.settled && stoppedWork) {
|
|
4599
|
+
await settleActive(cancelledOutcome(session, active));
|
|
4600
|
+
}
|
|
4453
4601
|
break;
|
|
4454
4602
|
}
|
|
4455
4603
|
// A held turn (see Turn.deferredSettle) settles at its
|
|
@@ -4486,8 +4634,11 @@ export class ClaudeAcpAgent {
|
|
|
4486
4634
|
pendingExitPlanModeInterruption.toolUseId) {
|
|
4487
4635
|
// The fresh query has none of this one's background
|
|
4488
4636
|
// tasks, so the hold can never drain there: drop it, and
|
|
4489
|
-
// let the continuation's own result settle the turn.
|
|
4637
|
+
// let the continuation's own result settle the turn. The
|
|
4638
|
+
// continuation is the turn's work again, which takes
|
|
4639
|
+
// over the follow-up cycle that planned it.
|
|
4490
4640
|
heldTurn.deferredSettle = undefined;
|
|
4641
|
+
this.startForeground(params.sessionId, heldTurn);
|
|
4491
4642
|
await this.exitPlan.restart(params.sessionId, session, pendingExitPlanContextReset);
|
|
4492
4643
|
return;
|
|
4493
4644
|
}
|
|
@@ -4754,9 +4905,10 @@ export class ClaudeAcpAgent {
|
|
|
4754
4905
|
// subagent never holds the prompt hostage.
|
|
4755
4906
|
//
|
|
4756
4907
|
// is_error/auth already settled via failActive (activeTurn is null
|
|
4757
|
-
// then, so both branches no-op);
|
|
4758
|
-
//
|
|
4759
|
-
//
|
|
4908
|
+
// then, so both branches no-op); a cancelled turn settles at
|
|
4909
|
+
// the `session.cancelled` guard above, or on the idle/abort
|
|
4910
|
+
// path when its result never comes. settleActive is idempotent,
|
|
4911
|
+
// so a duplicate idle is a no-op.
|
|
4760
4912
|
//
|
|
4761
4913
|
// A result also closes this compaction lifecycle — before the
|
|
4762
4914
|
// settle, so a `cancelled` terminal for an entity the runtime
|
|
@@ -4810,43 +4962,14 @@ export class ClaudeAcpAgent {
|
|
|
4810
4962
|
// reset, and the synthetic-auth/system/local-command paths
|
|
4811
4963
|
// `break` early too. Block indices restart at 0 each message, so
|
|
4812
4964
|
// leftover entries would otherwise collide with this message's
|
|
4813
|
-
// blocks and re-emit (or truncate) already-streamed text.
|
|
4814
|
-
//
|
|
4815
|
-
//
|
|
4816
|
-
//
|
|
4817
|
-
//
|
|
4965
|
+
// blocks and re-emit (or truncate) already-streamed text. Keyed by
|
|
4966
|
+
// the stream, so a subagent's message can't clear the top-level
|
|
4967
|
+
// record. Fires once, before any of this message's blocks, so it
|
|
4968
|
+
// doesn't disturb the mid-message turn-activation path the way
|
|
4969
|
+
// resetting on turn activation would. The record's drop latch
|
|
4970
|
+
// (see below) resets with it.
|
|
4818
4971
|
streamedBlocksOf(message.parent_tool_use_id).length = 0;
|
|
4819
|
-
|
|
4820
|
-
// Accumulate the text/thinking actually streamed live, so the
|
|
4821
|
-
// `assistant` case below can diff its assembled blocks against what
|
|
4822
|
-
// already reached the client as chunks and forward only the
|
|
4823
|
-
// remainder. Each stream (top level or one subagent) keeps its own
|
|
4824
|
-
// record, so a subagent cannot attribute content to another stream.
|
|
4825
|
-
// Contiguous deltas of the same block (same index and type) extend
|
|
4826
|
-
// the current entry; anything else opens a new one.
|
|
4827
|
-
if (message.event.type === "content_block_delta") {
|
|
4828
|
-
const delta = message.event.delta;
|
|
4829
|
-
const chunk = delta.type === "text_delta"
|
|
4830
|
-
? { type: "text", text: delta.text }
|
|
4831
|
-
: delta.type === "thinking_delta"
|
|
4832
|
-
? { type: "thinking", text: delta.thinking }
|
|
4833
|
-
: undefined;
|
|
4834
|
-
// Skip empty deltas (some gateways emit empty thinking chunks —
|
|
4835
|
-
// #793): appending "" is a no-op, but pushing a "" entry would
|
|
4836
|
-
// create a block the consolidated handler's `text.length > 0`
|
|
4837
|
-
// guard can never consume, stalling the diff cursor and
|
|
4838
|
-
// re-emitting the next block as a duplicate.
|
|
4839
|
-
if (chunk?.text) {
|
|
4840
|
-
const index = message.event.index;
|
|
4841
|
-
const streamedBlocks = streamedBlocksOf(message.parent_tool_use_id);
|
|
4842
|
-
const last = streamedBlocks[streamedBlocks.length - 1];
|
|
4843
|
-
if (last && last.index === index && last.type === chunk.type) {
|
|
4844
|
-
last.text += chunk.text;
|
|
4845
|
-
}
|
|
4846
|
-
else {
|
|
4847
|
-
streamedBlocks.push({ index, type: chunk.type, text: chunk.text });
|
|
4848
|
-
}
|
|
4849
|
-
}
|
|
4972
|
+
droppedStreams.delete(message.parent_tool_use_id ?? "");
|
|
4850
4973
|
}
|
|
4851
4974
|
if (message.parent_tool_use_id === null &&
|
|
4852
4975
|
(message.event.type === "message_start" || message.event.type === "message_delta")) {
|
|
@@ -4900,6 +5023,67 @@ export class ClaudeAcpAgent {
|
|
|
4900
5023
|
});
|
|
4901
5024
|
}
|
|
4902
5025
|
}
|
|
5026
|
+
// After a cancel, output streams to the client until the client is
|
|
5027
|
+
// told that the stopped work ended (see workEndReported), so what
|
|
5028
|
+
// the interrupt flushes before then still goes out, as both
|
|
5029
|
+
// protocol versions allow. After that, while the window after the
|
|
5030
|
+
// cancel is open, streamed text, thinking and tool calls are
|
|
5031
|
+
// dropped like the consolidated messages they belong to (see the
|
|
5032
|
+
// `cancelled` check in the user/assistant case). Otherwise the
|
|
5033
|
+
// answer to a prompt the cancel abandoned, which Claude Code had
|
|
5034
|
+
// already taken off its queue, would stream to the client after the
|
|
5035
|
+
// cancel was reported.
|
|
5036
|
+
//
|
|
5037
|
+
// Where the consolidated message delivers the text (the same
|
|
5038
|
+
// condition as in the user/assistant case), a stream that loses a
|
|
5039
|
+
// delta keeps dropping until the record below resets: at the next
|
|
5040
|
+
// message, or when a consolidated message consumes the record. If
|
|
5041
|
+
// the next prompt's echo ends the window mid-block, the record then
|
|
5042
|
+
// stays empty and the consolidated message delivers the block's
|
|
5043
|
+
// whole text once. A record holding only the block's tail is not a
|
|
5044
|
+
// prefix of the text, so the text would be sent again (see
|
|
5045
|
+
// unstreamedRemainder).
|
|
5046
|
+
const streamKey = message.parent_tool_use_id ?? "";
|
|
5047
|
+
if (session.cancelled && this.workEndReported(params.sessionId, session)) {
|
|
5048
|
+
if (message.parent_tool_use_id === null || forwardsSubagentText()) {
|
|
5049
|
+
droppedStreams.add(streamKey);
|
|
5050
|
+
}
|
|
5051
|
+
break;
|
|
5052
|
+
}
|
|
5053
|
+
if (droppedStreams.has(streamKey)) {
|
|
5054
|
+
break;
|
|
5055
|
+
}
|
|
5056
|
+
// Accumulate the text/thinking actually streamed live, so the
|
|
5057
|
+
// `assistant` case below can diff its assembled blocks against what
|
|
5058
|
+
// already reached the client as chunks and forward only the
|
|
5059
|
+
// remainder. Each stream (top level or one subagent) keeps its own
|
|
5060
|
+
// record, so a subagent cannot attribute content to another stream.
|
|
5061
|
+
// Contiguous deltas of the same block (same index and type) extend
|
|
5062
|
+
// the current entry; anything else opens a new one.
|
|
5063
|
+
if (message.event.type === "content_block_delta") {
|
|
5064
|
+
const delta = message.event.delta;
|
|
5065
|
+
const chunk = delta.type === "text_delta"
|
|
5066
|
+
? { type: "text", text: delta.text }
|
|
5067
|
+
: delta.type === "thinking_delta"
|
|
5068
|
+
? { type: "thinking", text: delta.thinking }
|
|
5069
|
+
: undefined;
|
|
5070
|
+
// Skip empty deltas (some gateways emit empty thinking chunks —
|
|
5071
|
+
// #793): appending "" is a no-op, but pushing a "" entry would
|
|
5072
|
+
// create a block the consolidated handler's `text.length > 0`
|
|
5073
|
+
// guard can never consume, stalling the diff cursor and
|
|
5074
|
+
// re-emitting the next block as a duplicate.
|
|
5075
|
+
if (chunk?.text) {
|
|
5076
|
+
const index = message.event.index;
|
|
5077
|
+
const streamedBlocks = streamedBlocksOf(message.parent_tool_use_id);
|
|
5078
|
+
const last = streamedBlocks[streamedBlocks.length - 1];
|
|
5079
|
+
if (last && last.index === index && last.type === chunk.type) {
|
|
5080
|
+
last.text += chunk.text;
|
|
5081
|
+
}
|
|
5082
|
+
else {
|
|
5083
|
+
streamedBlocks.push({ index, type: chunk.type, text: chunk.text });
|
|
5084
|
+
}
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
4903
5087
|
for (const notification of streamEventToAcpNotifications(message, params.sessionId, session.toolUseCache, this.client, this.logger, {
|
|
4904
5088
|
clientCapabilities: this.clientCapabilities,
|
|
4905
5089
|
toolCallCapabilities: this.toolCallCapabilities,
|
|
@@ -4955,7 +5139,7 @@ export class ClaudeAcpAgent {
|
|
|
4955
5139
|
if (session.activeTurn !== queued) {
|
|
4956
5140
|
if (session.activeTurn) {
|
|
4957
5141
|
// Hand off the previous turn. If a cancel is pending for it
|
|
4958
|
-
// (its
|
|
5142
|
+
// (neither its result nor its idle has arrived), settle it
|
|
4959
5143
|
// "cancelled" per the ACP contract rather than "end_turn" —
|
|
4960
5144
|
// otherwise a cancel followed quickly by the next prompt
|
|
4961
5145
|
// would report the cancelled turn as a normal completion.
|
|
@@ -5164,6 +5348,7 @@ export class ClaudeAcpAgent {
|
|
|
5164
5348
|
const streamed = streamedBlocksOf(message.parent_tool_use_id);
|
|
5165
5349
|
content = unstreamedRemainder(message.message.content, streamed);
|
|
5166
5350
|
streamed.length = 0;
|
|
5351
|
+
droppedStreams.delete(message.parent_tool_use_id ?? "");
|
|
5167
5352
|
}
|
|
5168
5353
|
else if (message.type === "assistant") {
|
|
5169
5354
|
// Nested text/thinking stays internal for a client that does
|
|
@@ -5176,11 +5361,28 @@ export class ClaudeAcpAgent {
|
|
|
5176
5361
|
}
|
|
5177
5362
|
const acceptedPlanToolUseId = observeExitPlanToolResults(message, content, session);
|
|
5178
5363
|
let backgroundedToolCalls = new Set();
|
|
5364
|
+
let runningTerminal;
|
|
5179
5365
|
if (message.type === "user") {
|
|
5180
5366
|
rememberResolvedToolNames(session, content);
|
|
5181
5367
|
const backgroundBashTask = backgroundBashTaskFromToolResult(content, message.tool_use_result, session.toolUseCache);
|
|
5182
5368
|
if (backgroundBashTask)
|
|
5183
5369
|
await asyncTasks.taskBackgrounded(backgroundBashTask);
|
|
5370
|
+
// The command's terminal runs until its task ends, unless the
|
|
5371
|
+
// task has ended already (a fast command can end before the tool
|
|
5372
|
+
// result comes).
|
|
5373
|
+
const terminalId = backgroundBashTask?.toolCallId;
|
|
5374
|
+
if (terminalId &&
|
|
5375
|
+
this.toolCallCapabilities.terminalOutput &&
|
|
5376
|
+
!this.toolCallCapabilities.air.client &&
|
|
5377
|
+
session.liveBackgroundTasks.has(backgroundBashTask.taskId)) {
|
|
5378
|
+
runningTerminal = terminalId;
|
|
5379
|
+
(session.backgroundTerminals ??= new Map()).set(backgroundBashTask.taskId, {
|
|
5380
|
+
toolCallId: terminalId,
|
|
5381
|
+
...(message.parent_tool_use_id
|
|
5382
|
+
? { parentToolUseId: message.parent_tool_use_id }
|
|
5383
|
+
: {}),
|
|
5384
|
+
});
|
|
5385
|
+
}
|
|
5184
5386
|
await asyncTasks.toolResults(content);
|
|
5185
5387
|
backgroundedToolCalls = backgroundedBashToolCallIds(content, session.toolUseCache, asyncTasks, backgroundBashTask);
|
|
5186
5388
|
const resumedAgentId = resumedNativeSubagentId(message.tool_use_result);
|
|
@@ -5211,7 +5413,7 @@ export class ClaudeAcpAgent {
|
|
|
5211
5413
|
// filtered out of `content` above; blocks that do pass through
|
|
5212
5414
|
// (e.g. a subagent image) carry the stamped parentToolUseId
|
|
5213
5415
|
// meta and are excluded there.
|
|
5214
|
-
await sendUpdate(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled));
|
|
5416
|
+
await sendUpdate(withRunningTerminal(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled), runningTerminal));
|
|
5215
5417
|
}
|
|
5216
5418
|
break;
|
|
5217
5419
|
}
|
|
@@ -5433,6 +5635,16 @@ export class ClaudeAcpAgent {
|
|
|
5433
5635
|
return;
|
|
5434
5636
|
}
|
|
5435
5637
|
session.cancelled = true;
|
|
5638
|
+
// Before anything awaits, so a model request that the consumer reads in
|
|
5639
|
+
// the meantime is already judged against these prompts.
|
|
5640
|
+
session.cancelledPrompts ??= new Set();
|
|
5641
|
+
for (const turn of session.turnQueue ?? []) {
|
|
5642
|
+
if (turn.settled)
|
|
5643
|
+
continue;
|
|
5644
|
+
session.cancelledPrompts.add(turn.promptUuid);
|
|
5645
|
+
for (const uuid of turn.steeredUuids ?? [])
|
|
5646
|
+
session.cancelledPrompts.add(uuid);
|
|
5647
|
+
}
|
|
5436
5648
|
for (const turn of session.turnQueue ?? [])
|
|
5437
5649
|
turn.localCommandAbort?.abort();
|
|
5438
5650
|
session.pendingExitPlanModeInterruption = undefined;
|
|
@@ -5483,9 +5695,10 @@ export class ClaudeAcpAgent {
|
|
|
5483
5695
|
// never decrements the count) to miss.
|
|
5484
5696
|
const lifecycleLane = session.msgLifecycleV1 === true;
|
|
5485
5697
|
// Settle queued turns that haven't started yet (no echo seen) right away —
|
|
5486
|
-
// they have no in-flight SDK work to interrupt. The
|
|
5487
|
-
//
|
|
5488
|
-
// backstop below). Mirrors the old pendingMessages
|
|
5698
|
+
// they have no in-flight SDK work to interrupt. The consumer settles the
|
|
5699
|
+
// active turn at its interrupted result, or at the idle when that never
|
|
5700
|
+
// comes (or via the backstop below). Mirrors the old pendingMessages
|
|
5701
|
+
// cancellation.
|
|
5489
5702
|
const orphanedTurns = [];
|
|
5490
5703
|
if (session.turnQueue) {
|
|
5491
5704
|
for (const turn of session.turnQueue) {
|
|
@@ -5571,14 +5784,19 @@ export class ClaudeAcpAgent {
|
|
|
5571
5784
|
// whose session sits idle gets no trailing idle (the interrupt emits
|
|
5572
5785
|
// nothing; with no state events consumed yet, the same is assumed).
|
|
5573
5786
|
// A held turn whose followup cycle is live (running, or blocked on a
|
|
5574
|
-
// permission request) is left to that
|
|
5575
|
-
//
|
|
5576
|
-
//
|
|
5577
|
-
//
|
|
5578
|
-
//
|
|
5787
|
+
// permission request) is left to that cycle's interrupted result, like any
|
|
5788
|
+
// live turn, or to the idle when no result comes. The captured outcome
|
|
5789
|
+
// preserves usage and metadata (issue #844), while reporter state makes a
|
|
5790
|
+
// late checkpoint response harmless.
|
|
5791
|
+
// Since CLI 2.1.270, Claude Code reports `running` for as long as
|
|
5792
|
+
// background agents live, so the session state cannot tell a live
|
|
5793
|
+
// followup from a hold. With request markers, a followup is live when one
|
|
5794
|
+
// of its model requests started after the turn's result.
|
|
5579
5795
|
{
|
|
5580
5796
|
const active = session.activeTurn;
|
|
5581
|
-
const followupLive = session.
|
|
5797
|
+
const followupLive = session.requestMarkerListsV1
|
|
5798
|
+
? session.modelRequestSinceResult === true
|
|
5799
|
+
: session.lastSessionState === "running" || session.lastSessionState === "requires_action";
|
|
5582
5800
|
if (active &&
|
|
5583
5801
|
(active.settling || (isHeldOpen(active) && !followupLive)) &&
|
|
5584
5802
|
(active.deferredSettle ?? active.settlingOutcome)) {
|
|
@@ -5618,7 +5836,7 @@ export class ClaudeAcpAgent {
|
|
|
5618
5836
|
}
|
|
5619
5837
|
}
|
|
5620
5838
|
// A steered active turn (see Turn.steeredEchoes) settles "cancelled" in the
|
|
5621
|
-
// consumer at
|
|
5839
|
+
// consumer at its interrupted result, or at the idle, like any live turn. But the
|
|
5622
5840
|
// steer lane pays for its last result's trailer by settling on it, which
|
|
5623
5841
|
// this cancel pre-empts, so count that trailer here or it arrives un-owed
|
|
5624
5842
|
// against the next prompt and false-fails it (issue #825). Over-counting
|
|
@@ -5649,6 +5867,10 @@ export class ClaudeAcpAgent {
|
|
|
5649
5867
|
}
|
|
5650
5868
|
// Before the interrupt: once it stops the running turn, Claude Code starts
|
|
5651
5869
|
// the next queued message right away.
|
|
5870
|
+
// A prompt or steer that arrived while this cancel awaited is abandoned
|
|
5871
|
+
// too, after the record above was taken.
|
|
5872
|
+
for (const uuid of abandoned)
|
|
5873
|
+
(session.cancelledPrompts ??= new Set()).add(uuid);
|
|
5652
5874
|
this.dropQueuedMessages(params.sessionId, session.query, abandoned);
|
|
5653
5875
|
const interrupt = session.query.interrupt();
|
|
5654
5876
|
if (!options.awaitInterrupt) {
|
|
@@ -5769,6 +5991,9 @@ export class ClaudeAcpAgent {
|
|
|
5769
5991
|
catch (error) {
|
|
5770
5992
|
this.logger.error(`Session ${sessionId}: cancellation failed during teardown`, error);
|
|
5771
5993
|
}
|
|
5994
|
+
// The turns end before this returns; so does a model cycle that Claude
|
|
5995
|
+
// Code started on its own, which no turn waits for.
|
|
5996
|
+
this.endAutonomousCycle(sessionId, { outcome: { stopReason: "cancelled" } });
|
|
5772
5997
|
// cancel() arms the force-cancel floor and interrupts gracefully, but a
|
|
5773
5998
|
// wedged consumer only wakes when `cancelController` aborts — closeQueryStream
|
|
5774
5999
|
// below doesn't touch it. Since we're tearing the session down anyway, wake
|
|
@@ -6275,43 +6500,111 @@ export class ClaudeAcpAgent {
|
|
|
6275
6500
|
const response = await this.client.writeTextFile(params);
|
|
6276
6501
|
return response;
|
|
6277
6502
|
}
|
|
6278
|
-
/** Mark a client request
|
|
6279
|
-
*
|
|
6280
|
-
*
|
|
6281
|
-
*
|
|
6282
|
-
*
|
|
6283
|
-
async withPendingUserInput(sessionId, request) {
|
|
6503
|
+
/** Mark a client request of the subagent `agentID` (absent for the main
|
|
6504
|
+
* conversation) as awaiting user input for exactly the lifetime of its
|
|
6505
|
+
* promise (see `Session.openUserInputs`). Steering consults the open
|
|
6506
|
+
* requests synchronously, so a message arriving while any permission or
|
|
6507
|
+
* elicitation card is open uses non-interrupting SDK delivery. */
|
|
6508
|
+
async withPendingUserInput(sessionId, request, agentID) {
|
|
6284
6509
|
const session = this.sessions[sessionId];
|
|
6285
6510
|
if (!session)
|
|
6286
6511
|
return request();
|
|
6287
|
-
|
|
6288
|
-
|
|
6512
|
+
const input = agentID !== undefined ? { agentID } : {};
|
|
6513
|
+
(session.openUserInputs ??= new Set()).add(input);
|
|
6514
|
+
this.syncAwaitingUser(sessionId, session);
|
|
6289
6515
|
try {
|
|
6290
6516
|
return await request();
|
|
6291
6517
|
}
|
|
6292
6518
|
finally {
|
|
6293
|
-
session.
|
|
6294
|
-
this.syncAwaitingUser(session);
|
|
6295
|
-
}
|
|
6296
|
-
}
|
|
6297
|
-
/** Report whether the session's
|
|
6298
|
-
* a permission request or question
|
|
6299
|
-
* not say which prompt it is for,
|
|
6300
|
-
*
|
|
6301
|
-
*
|
|
6302
|
-
* when a turn activates
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6519
|
+
session.openUserInputs.delete(input);
|
|
6520
|
+
this.syncAwaitingUser(sessionId, session);
|
|
6521
|
+
}
|
|
6522
|
+
}
|
|
6523
|
+
/** Report whether the session's foreground work awaits the user: exactly
|
|
6524
|
+
* while a permission request or question that blocks it is open (see
|
|
6525
|
+
* {@link blocksForeground}). A request does not say which prompt it is for,
|
|
6526
|
+
* and Claude Code can ask for a queued prompt before the consumer activates
|
|
6527
|
+
* it, so a request is not tied to the work that was running when it
|
|
6528
|
+
* opened. Called when a request opens or closes, when a turn activates, and
|
|
6529
|
+
* when a task registers or moves to the background. */
|
|
6530
|
+
syncAwaitingUser(sessionId, session) {
|
|
6531
|
+
const work = this.foregroundWork.get(sessionId);
|
|
6532
|
+
if (!work)
|
|
6306
6533
|
return;
|
|
6307
|
-
const waiting = (session.
|
|
6308
|
-
if (waiting ===
|
|
6534
|
+
const waiting = [...(session.openUserInputs ?? [])].some((input) => blocksForeground(session, input.agentID));
|
|
6535
|
+
if (waiting === work.awaitingUser)
|
|
6309
6536
|
return;
|
|
6310
|
-
|
|
6537
|
+
work.awaitingUser = waiting;
|
|
6311
6538
|
if (waiting)
|
|
6312
|
-
|
|
6539
|
+
work.events.awaitingUser();
|
|
6313
6540
|
else
|
|
6314
|
-
|
|
6541
|
+
work.events.running();
|
|
6542
|
+
}
|
|
6543
|
+
/** Make `owner` the foreground work of the session `sessionId` (see
|
|
6544
|
+
* {@link ForegroundEvents}): it reports `running`, then waits at once if a
|
|
6545
|
+
* request is open. If work already runs, it goes on under `owner` and
|
|
6546
|
+
* reports nothing new: a turn takes over a cycle that Claude Code started on
|
|
6547
|
+
* its own, and only the owner's end ends the work. */
|
|
6548
|
+
startForeground(sessionId, owner) {
|
|
6549
|
+
if (!this.foregroundEvents)
|
|
6550
|
+
return;
|
|
6551
|
+
const current = this.foregroundWork.get(sessionId);
|
|
6552
|
+
if (current) {
|
|
6553
|
+
current.owner = owner;
|
|
6554
|
+
return;
|
|
6555
|
+
}
|
|
6556
|
+
const events = this.foregroundEvents(sessionId);
|
|
6557
|
+
this.foregroundWork.set(sessionId, { owner, events, awaitingUser: false });
|
|
6558
|
+
events.running();
|
|
6559
|
+
const session = this.sessions[sessionId];
|
|
6560
|
+
if (session)
|
|
6561
|
+
this.syncAwaitingUser(sessionId, session);
|
|
6562
|
+
}
|
|
6563
|
+
/** End the foreground work of the session `sessionId` with `outcome`, or as
|
|
6564
|
+
* failed with `error`, if `owner` is that work. */
|
|
6565
|
+
endForeground(sessionId, owner, end) {
|
|
6566
|
+
const work = this.foregroundWork.get(sessionId);
|
|
6567
|
+
if (work?.owner !== owner)
|
|
6568
|
+
return;
|
|
6569
|
+
this.foregroundWork.delete(sessionId);
|
|
6570
|
+
if ("outcome" in end)
|
|
6571
|
+
work.events.ended(end.outcome);
|
|
6572
|
+
else
|
|
6573
|
+
work.events.failed(end.error);
|
|
6574
|
+
}
|
|
6575
|
+
/** Start a model cycle that Claude Code started on its own as the foreground
|
|
6576
|
+
* work (see {@link ForegroundEvents}) when `message` is its first output:
|
|
6577
|
+
* model output of the main conversation (not a subagent's) while no work
|
|
6578
|
+
* runs. That includes a turn that v1 holds for its background subagents,
|
|
6579
|
+
* whose work ended at its result, so its follow-up is a cycle. Output of
|
|
6580
|
+
* the work a cancel stopped starts nothing (see Session.cancelled). */
|
|
6581
|
+
startAutonomousCycle(sessionId, session, message) {
|
|
6582
|
+
if (!this.foregroundEvents || this.foregroundWork.has(sessionId) || session.cancelled)
|
|
6583
|
+
return;
|
|
6584
|
+
if (message.type !== "assistant" && message.type !== "stream_event")
|
|
6585
|
+
return;
|
|
6586
|
+
if (message.parent_tool_use_id !== null)
|
|
6587
|
+
return;
|
|
6588
|
+
// A turn that Claude Code started is the foreground work already. Without
|
|
6589
|
+
// lifecycle frames, a queued turn's output can come before its echo, so
|
|
6590
|
+
// output while a turn is queued may be that turn's. With them, a turn that
|
|
6591
|
+
// Claude Code has not started has no output yet.
|
|
6592
|
+
const queued = (session.turnQueue ?? []).some((turn) => !turn.settled && turn !== session.activeTurn);
|
|
6593
|
+
if (queued && !session.msgLifecycleV1)
|
|
6594
|
+
return;
|
|
6595
|
+
this.startForeground(sessionId, AUTONOMOUS_CYCLE);
|
|
6596
|
+
}
|
|
6597
|
+
/** Whether the client has been told that the session's work ended: on v2 by
|
|
6598
|
+
* an idle state, so no foreground work is open, including a cycle that
|
|
6599
|
+
* Claude Code started on its own; on v1 by the response to the turn's
|
|
6600
|
+
* prompt, so no turn is active. */
|
|
6601
|
+
workEndReported(sessionId, session) {
|
|
6602
|
+
return this.foregroundEvents ? !this.foregroundWork.has(sessionId) : !session.activeTurn;
|
|
6603
|
+
}
|
|
6604
|
+
/** End the session's model cycle that Claude Code started on its own, if one
|
|
6605
|
+
* runs. */
|
|
6606
|
+
endAutonomousCycle(sessionId, end) {
|
|
6607
|
+
this.endForeground(sessionId, AUTONOMOUS_CYCLE, end);
|
|
6315
6608
|
}
|
|
6316
6609
|
/** Forward a permission request to the client, wiring the tool call's
|
|
6317
6610
|
* `signal` through as a `cancellationSignal`. When the turn is cancelled
|
|
@@ -6319,7 +6612,7 @@ export class ClaudeAcpAgent {
|
|
|
6319
6612
|
* `$/cancel_request`, and our local abort race settles even if the client
|
|
6320
6613
|
* ignores it. A `cancelled` outcome, request rejection, and local abort all
|
|
6321
6614
|
* surface the same "Tool use aborted" the callers already expect. */
|
|
6322
|
-
async requestPermissionFromClient(params, toolName, signal, parentToolUseId, ownerSessionId, toolInput, previewContent) {
|
|
6615
|
+
async requestPermissionFromClient(params, toolName, signal, parentToolUseId, ownerSessionId, agentID, toolInput, previewContent) {
|
|
6323
6616
|
if (signal.aborted)
|
|
6324
6617
|
throw new Error("Tool use aborted");
|
|
6325
6618
|
// The SDK may invoke `canUseTool` (and therefore this permission request)
|
|
@@ -6337,7 +6630,7 @@ export class ClaudeAcpAgent {
|
|
|
6337
6630
|
// The request goes to a subagent's child session for a tool call of that
|
|
6338
6631
|
// subagent, but it blocks the turn of the owner session.
|
|
6339
6632
|
try {
|
|
6340
|
-
return await this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal));
|
|
6633
|
+
return await this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal), agentID);
|
|
6341
6634
|
}
|
|
6342
6635
|
catch (error) {
|
|
6343
6636
|
if (signal.aborted) {
|
|
@@ -6510,7 +6803,7 @@ export class ClaudeAcpAgent {
|
|
|
6510
6803
|
// Like permission requests, the elicitation references this toolUseID, so
|
|
6511
6804
|
// make sure the tool_call has surfaced to the client before we send it.
|
|
6512
6805
|
await this.ensureToolCallEmitted(sessionId, toolName, toolUseID, toolInput, parentToolUseId, signal, permissionSessionId);
|
|
6513
|
-
return this.handleAskUserQuestion(sessionId, permissionSessionId, toolInput, toolUseID, signal);
|
|
6806
|
+
return this.handleAskUserQuestion(sessionId, permissionSessionId, toolInput, toolUseID, signal, agentID);
|
|
6514
6807
|
}
|
|
6515
6808
|
// Do not auto-allow here based on the session's advertised mode. Claude
|
|
6516
6809
|
// Code applies bypassPermissions before invoking canUseTool; a request
|
|
@@ -6579,7 +6872,7 @@ export class ClaudeAcpAgent {
|
|
|
6579
6872
|
...presentation,
|
|
6580
6873
|
options: permissionOptions,
|
|
6581
6874
|
sessionId: permissionSessionId,
|
|
6582
|
-
}, toolName, signal, parentToolUseId, sessionId, toolInput, previewContent);
|
|
6875
|
+
}, toolName, signal, parentToolUseId, sessionId, agentID, toolInput, previewContent);
|
|
6583
6876
|
if (signal.aborted)
|
|
6584
6877
|
throw new Error("Tool use aborted");
|
|
6585
6878
|
const decodedPermission = decodeClaudePermissionResponse(response, toolName, toolInput, toolUseID, permissionOptions, durableChangeSet);
|
|
@@ -6667,7 +6960,7 @@ export class ClaudeAcpAgent {
|
|
|
6667
6960
|
* The elicitation goes to `requestSessionId`, a subagent's child session for
|
|
6668
6961
|
* a question it asks, and blocks the turn of `sessionId`.
|
|
6669
6962
|
*/
|
|
6670
|
-
async handleAskUserQuestion(sessionId, requestSessionId, toolInput, toolUseID, signal) {
|
|
6963
|
+
async handleAskUserQuestion(sessionId, requestSessionId, toolInput, toolUseID, signal, agentID) {
|
|
6671
6964
|
const questions = extractAskUserQuestions(toolInput);
|
|
6672
6965
|
if (!questions) {
|
|
6673
6966
|
return { behavior: "deny", message: "AskUserQuestion called with no valid questions." };
|
|
@@ -6675,7 +6968,7 @@ export class ClaudeAcpAgent {
|
|
|
6675
6968
|
const createRequest = askUserQuestionsToCreateRequest(questions, requestSessionId, toolUseID, this.toolCallCapabilities.air.client);
|
|
6676
6969
|
let response;
|
|
6677
6970
|
try {
|
|
6678
|
-
response = await this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal));
|
|
6971
|
+
response = await this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal), agentID);
|
|
6679
6972
|
}
|
|
6680
6973
|
catch (error) {
|
|
6681
6974
|
// A cancellation we requested (signal aborted) settles as an aborted tool
|
|
@@ -7338,28 +7631,23 @@ export class ClaudeAcpAgent {
|
|
|
7338
7631
|
// into the query.
|
|
7339
7632
|
const resolvedProvider = this.resolveProviderConfig();
|
|
7340
7633
|
const providerEnv = createEnvForProvider(resolvedProvider);
|
|
7341
|
-
const
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
const configuredSettingsObject = typeof configuredSettings === "string"
|
|
7349
|
-
? JSON.parse(await fs.readFile(path.resolve(params.cwd, configuredSettings), "utf8"))
|
|
7350
|
-
: configuredSettings;
|
|
7634
|
+
const providedSettings = userProvidedOptions?.settings;
|
|
7635
|
+
const providedSettingsObject = typeof providedSettings === "string"
|
|
7636
|
+
? JSON.parse(await fs.readFile(path.resolve(params.cwd, providedSettings), "utf8"))
|
|
7637
|
+
: providedSettings;
|
|
7638
|
+
const configuredSettingsObject = modelConfig
|
|
7639
|
+
? { ...modelConfig, ...providedSettingsObject }
|
|
7640
|
+
: providedSettingsObject;
|
|
7351
7641
|
// Claude Code applies env from settings.json after the subprocess env. Put
|
|
7352
7642
|
// an active ACP route in the programmatic settings tier too so user/project
|
|
7353
7643
|
// settings cannot silently restore a different ANTHROPIC_BASE_URL.
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
settings = {
|
|
7358
|
-
...baseSettings,
|
|
7644
|
+
const settings = resolvedProvider
|
|
7645
|
+
? {
|
|
7646
|
+
...configuredSettingsObject,
|
|
7359
7647
|
apiKeyHelper: "",
|
|
7360
|
-
env: { ...
|
|
7361
|
-
}
|
|
7362
|
-
|
|
7648
|
+
env: { ...configuredSettingsObject?.env, ...providerEnv },
|
|
7649
|
+
}
|
|
7650
|
+
: configuredSettingsObject;
|
|
7363
7651
|
const env = {
|
|
7364
7652
|
...process.env,
|
|
7365
7653
|
...userProvidedOptions?.env,
|
|
@@ -7925,6 +8213,37 @@ function cancelledOutcome(session, turn) {
|
|
|
7925
8213
|
...(recorded._meta ? { _meta: recorded._meta } : {}),
|
|
7926
8214
|
};
|
|
7927
8215
|
}
|
|
8216
|
+
/** Whether the model request that `status` (a `status: requesting` frame)
|
|
8217
|
+
* starts may belong to the work a cancel stopped. It may while:
|
|
8218
|
+
* - a turn in {@link Session.cancelledPrompts} is unsettled: the stopped
|
|
8219
|
+
* turn, whose result is still to come;
|
|
8220
|
+
* - the orphan accounting still expects the result of an abandoned prompt
|
|
8221
|
+
* that Claude Code may still run, which it can fold into its own
|
|
8222
|
+
* follow-up after that follow-up's first request;
|
|
8223
|
+
* - the request serves one of {@link Session.cancelledPrompts}, as an
|
|
8224
|
+
* abandoned prompt's request does even when the interrupt receipt left it
|
|
8225
|
+
* out of the orphan accounting;
|
|
8226
|
+
* - the request serves an unsettled prompt. One that arrived while the cancel
|
|
8227
|
+
* awaited is recorded only when the cancel sweeps the queue, and its echo
|
|
8228
|
+
* ends the cancel right after its request when the cancel does not.
|
|
8229
|
+
* A prompt that is merely queued does not make another request belong to
|
|
8230
|
+
* the stopped work: a follow-up that Claude Code runs before a prompt sent
|
|
8231
|
+
* after the cancel names no prompt. */
|
|
8232
|
+
function servesCancelledWork(session, status) {
|
|
8233
|
+
const cancelled = session.cancelledPrompts;
|
|
8234
|
+
if (!cancelled)
|
|
8235
|
+
return false;
|
|
8236
|
+
const unsettled = (session.turnQueue ?? []).filter((turn) => !turn.settled);
|
|
8237
|
+
if (unsettled.some((turn) => cancelled.has(turn.promptUuid)))
|
|
8238
|
+
return true;
|
|
8239
|
+
if ((session.pendingOrphanResults ?? 0) > 0 || (session.orphanCommands?.size ?? 0) > 0) {
|
|
8240
|
+
return true;
|
|
8241
|
+
}
|
|
8242
|
+
const served = status.user_message_uuids;
|
|
8243
|
+
return (Array.isArray(served) &&
|
|
8244
|
+
served.some((uuid) => typeof uuid === "string" &&
|
|
8245
|
+
(cancelled.has(uuid) || unsettled.some((turn) => turn.promptUuid === uuid))));
|
|
8246
|
+
}
|
|
7928
8247
|
/** `_meta.quota` for a prompt response: what the turn spent, shaped like
|
|
7929
8248
|
* codex-acp's (snake_case container keys, camelCase counters) so a client
|
|
7930
8249
|
* reads one shape from either agent.
|
|
@@ -7937,16 +8256,88 @@ function cancelledOutcome(session, turn) {
|
|
|
7937
8256
|
* so the rows can total more than `token_count`. They are the fuller picture,
|
|
7938
8257
|
* not a decomposition of it. */
|
|
7939
8258
|
function turnQuotaMeta(session) {
|
|
8259
|
+
return quotaMeta(session.accumulatedUsage, session.accumulatedModelUsage);
|
|
8260
|
+
}
|
|
8261
|
+
/** `_meta.quota` for work that spent `usage` in its main agent loop and
|
|
8262
|
+
* `modelUsage` by model (see {@link turnQuotaMeta}). */
|
|
8263
|
+
function quotaMeta(usage, modelUsage) {
|
|
7940
8264
|
return {
|
|
7941
8265
|
quota: {
|
|
7942
|
-
token_count: quotaTokenCount(
|
|
7943
|
-
model_usage: Object.entries(
|
|
8266
|
+
token_count: quotaTokenCount(usage),
|
|
8267
|
+
model_usage: Object.entries(modelUsage ?? {}).map(([model, usage]) => ({
|
|
7944
8268
|
model,
|
|
7945
8269
|
token_count: quotaTokenCount(usage),
|
|
7946
8270
|
})),
|
|
7947
8271
|
},
|
|
7948
8272
|
};
|
|
7949
8273
|
}
|
|
8274
|
+
/** Whether a request that awaits the user for the subagent `agentID` (absent
|
|
8275
|
+
* for the main conversation) blocks the session's foreground work. The main
|
|
8276
|
+
* conversation's requests do. So do those of a subagent that runs in the
|
|
8277
|
+
* foreground of the main conversation, since the tool call that spawned it
|
|
8278
|
+
* waits on it, and so on up a chain of such subagents. A request of a
|
|
8279
|
+
* subagent in the background, or nested inside one, does not: the main
|
|
8280
|
+
* conversation goes on without it. Nor does one of an agent without a
|
|
8281
|
+
* record: a teammate, a settled subagent, or one that Claude Code left out
|
|
8282
|
+
* of its background tasks and the consumer pruned, which only happens to a
|
|
8283
|
+
* subagent nested in a background one. A subagent that the main
|
|
8284
|
+
* conversation waits on has a record by the time it asks, as its
|
|
8285
|
+
* `task_started` comes first; when the consumer reads the request first, the
|
|
8286
|
+
* `task_started` checks again. */
|
|
8287
|
+
function blocksForeground(session, agentID) {
|
|
8288
|
+
const seen = new Set();
|
|
8289
|
+
for (let id = agentID; id !== undefined && !seen.has(id);) {
|
|
8290
|
+
seen.add(id);
|
|
8291
|
+
const task = session.liveBackgroundTasks.get(id);
|
|
8292
|
+
if (!task)
|
|
8293
|
+
return false;
|
|
8294
|
+
if (task.backgrounded)
|
|
8295
|
+
return false;
|
|
8296
|
+
id = task.parentTaskId;
|
|
8297
|
+
}
|
|
8298
|
+
return true;
|
|
8299
|
+
}
|
|
8300
|
+
/** Whether Claude Code ended the work of `result` because an interrupt
|
|
8301
|
+
* stopped it, while the model streamed or while a tool ran. */
|
|
8302
|
+
function isAbortedResult(result) {
|
|
8303
|
+
return (result.terminal_reason === "aborted_streaming" || result.terminal_reason === "aborted_tools");
|
|
8304
|
+
}
|
|
8305
|
+
/** How a model cycle that Claude Code started on its own ended, read from its
|
|
8306
|
+
* result as a turn's is: a refusal, the token limit, and the turn and budget
|
|
8307
|
+
* limits are their stop reasons, an error result fails the cycle, and a
|
|
8308
|
+
* cancel ends it `cancelled`. The usage and `_meta.quota` are the result's
|
|
8309
|
+
* own, `modelUsage` its increment by model, since autonomous results stay out
|
|
8310
|
+
* of the turn tally. */
|
|
8311
|
+
function autonomousCycleEnd(result, modelUsage, cancelled) {
|
|
8312
|
+
// Typed non-null, but third-party backends have been observed omitting
|
|
8313
|
+
// usage token fields (see snapshotFromUsage).
|
|
8314
|
+
const tally = {
|
|
8315
|
+
inputTokens: result.usage.input_tokens ?? 0,
|
|
8316
|
+
outputTokens: result.usage.output_tokens ?? 0,
|
|
8317
|
+
cachedReadTokens: result.usage.cache_read_input_tokens ?? 0,
|
|
8318
|
+
cachedWriteTokens: result.usage.cache_creation_input_tokens ?? 0,
|
|
8319
|
+
};
|
|
8320
|
+
const ended = (stopReason) => ({
|
|
8321
|
+
outcome: {
|
|
8322
|
+
stopReason,
|
|
8323
|
+
usage: { ...tally, totalTokens: tallyTotal(tally) },
|
|
8324
|
+
_meta: quotaMeta(tally, modelUsage),
|
|
8325
|
+
},
|
|
8326
|
+
});
|
|
8327
|
+
if (cancelled)
|
|
8328
|
+
return ended("cancelled");
|
|
8329
|
+
if (result.stop_reason === "refusal")
|
|
8330
|
+
return ended("refusal");
|
|
8331
|
+
if (result.stop_reason === "max_tokens")
|
|
8332
|
+
return ended("max_tokens");
|
|
8333
|
+
if (result.is_error) {
|
|
8334
|
+
const text = result.subtype === "success" ? result.result : result.errors.join(", ") || result.subtype;
|
|
8335
|
+
return { error: RequestError.internalError(undefined, text) };
|
|
8336
|
+
}
|
|
8337
|
+
return ended(result.subtype === "success" || result.subtype === "error_during_execution"
|
|
8338
|
+
? "end_turn"
|
|
8339
|
+
: "max_turn_requests");
|
|
8340
|
+
}
|
|
7950
8341
|
/** One `token_count` object. `cachedInputTokens` is cache reads, matching
|
|
7951
8342
|
* codex's field; Claude also reports cache writes, which codex has no slot
|
|
7952
8343
|
* for, so those ride along in an extra sibling (the same name the ACP `usage`
|