@agentclientprotocol/claude-agent-acp 0.99.2-preview.9 → 0.100.1-preview.1
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 +64 -21
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +138 -49
- package/dist/background-output.d.ts +54 -0
- package/dist/background-output.d.ts.map +1 -0
- package/dist/background-output.js +122 -0
- package/dist/background-work.d.ts +212 -0
- package/dist/background-work.d.ts.map +1 -0
- package/dist/background-work.js +428 -0
- package/dist/native-subagents.d.ts +7 -0
- package/dist/native-subagents.d.ts.map +1 -1
- package/dist/native-subagents.js +25 -0
- package/dist/startup-failure.d.ts +16 -0
- package/dist/startup-failure.d.ts.map +1 -0
- package/dist/startup-failure.js +38 -0
- package/dist/tool-calls/background.d.ts +10 -0
- package/dist/tool-calls/background.d.ts.map +1 -1
- package/dist/tool-calls/background.js +18 -0
- package/dist/tool-calls/renderer.d.ts +8 -0
- package/dist/tool-calls/renderer.d.ts.map +1 -1
- package/dist/tool-calls/renderer.js +6 -5
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +40 -18
- package/dist/v2/background.d.ts +72 -0
- package/dist/v2/background.d.ts.map +1 -0
- package/dist/v2/background.js +166 -0
- package/dist/v2/terminal.d.ts +8 -0
- package/dist/v2/terminal.d.ts.map +1 -1
- package/dist/v2/terminal.js +22 -0
- package/package.json +2 -2
package/dist/acp-agent.js
CHANGED
|
@@ -9,6 +9,7 @@ import { childModelOption, isNativeSubagentControlTool, isNativeSubagentControlU
|
|
|
9
9
|
import { AIR_ASYNC_TASKS_CAPABILITY, AIR_BACKGROUND_SUBAGENTS_CAPABILITY, AIR_CUSTOM_INSTRUCTIONS_CAPABILITY, airCustomInstructions, AIR_DIFF_PATCH_CAPABILITY, AIR_PLAN_FILE_CAPABILITY, AIR_GOAL_KEY, AIR_KIND_KEY, AIR_LOAD_REPLAY_END_CAPABILITY, AIR_LOAD_REPLAY_END_KEY, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, AIR_SKILL_PATH_KEY, clientSupportsAirCapability, withAirMeta, } from "./air-extension.js";
|
|
10
10
|
import * as sessionIndex from "./session-index/connection.js";
|
|
11
11
|
import { AsyncTaskRuntime, backgroundBashTaskFromToolResult, backgroundedBashToolCallIds, clientSupportsAsyncTasks, } from "./async-tasks.js";
|
|
12
|
+
import { backgroundLevel, backgroundToolCallPause, backgroundToolCallStates, BackgroundWorkReporter, cancelBackgroundWork, droppedTasks, endBackgroundWork, finishBackgroundTerminalOutput, followBackgroundTerminalOutput, registerBackgroundToolCalls, takeBackgroundTerminalExit, takeBackgroundToolCallEnd, } from "./background-work.js";
|
|
12
13
|
import { AUTH_STATUS_PROBE_TIMEOUT_MS, AUTH_STATUS_UPDATE_METHOD, authStatusCapability, fromAccountInfo, fromCliStatus, gatewayAuthStatus, mergeAuthStatus, notLoggedInAuthStatus, sameAuthStatus, } from "./auth-status.js";
|
|
13
14
|
import { execFile } from "node:child_process";
|
|
14
15
|
import { randomUUID } from "node:crypto";
|
|
@@ -36,7 +37,7 @@ import { ContextCompactionLifecycle, clientSupportsCompactionUpdates, contextCom
|
|
|
36
37
|
import { clientSupportsNotices, MAX_NOTICE_TITLE_LENGTH, normalizeNoticeText, noticeOrTranscriptUpdate, sentenceCase, splitNoticeText, } from "./session-notices.js";
|
|
37
38
|
import { applyTaskCreate, applyTaskList, applyTaskUpdate, clearHookCallbacks, completeHookCallback, createPostToolUseHook, hasHookCallback, createTaskHook, parseTaskCreateOutput, parseTaskListOutput, parseTaskUpdateOutput, planEntries, registerHookCallback, changedTaskPlanEntries, forgetPublishedTaskPlan, unregisterHookCallback, } from "./tools.js";
|
|
38
39
|
import { previewPatchContent } from "./diff.js";
|
|
39
|
-
import { backgroundedBashToolCall, withRunningTerminal } from "./tool-calls/background.js";
|
|
40
|
+
import { backgroundedBashToolCall, withRunningTerminal, withRunningToolCall, } from "./tool-calls/background.js";
|
|
40
41
|
import { ChangedMetaFilter } from "./tool-calls/changed-meta-filter.js";
|
|
41
42
|
import { ToolCallFieldTracker } from "./tool-calls/field-tracker.js";
|
|
42
43
|
import { ClientCapabilities as ToolCallClientCapabilities } from "./tool-calls/client-capabilities.js";
|
|
@@ -55,6 +56,7 @@ import { applyAvailableModelsAllowlist, buildModelConfigOption, getAvailableMode
|
|
|
55
56
|
import { GatewayListingError, listBedrockModels, listGatewayModels, PROVIDER_AUTH_TOKEN_PLACEHOLDER, selectBedrockRegion, } from "./gateway-models.js";
|
|
56
57
|
import { buildEffortConfigOption, EFFORT_CONFIG_ID, effortFlagSettings, effortLevelName, mergeEffortSettings, settingsEffortForModel, } from "./session-effort.js";
|
|
57
58
|
import { createUltracodeConfigOption, readUltracodeSettings, resolveUltracodeEnabled, ULTRACODE_CONFIG_ID, ultracodeSupported, ultracodeWorkflowsEnabled, } from "./session-ultracode.js";
|
|
59
|
+
import { STARTUP_FAILURE_RESULTS_ENV, startupFailureError } from "./startup-failure.js";
|
|
58
60
|
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
|
|
59
61
|
export { applyAvailableModelsAllowlist, matchResumedModel, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
|
|
60
62
|
const execFileAsync = promisify(execFile);
|
|
@@ -954,6 +956,7 @@ class ClientConnection {
|
|
|
954
956
|
requestPermission(params, signal) {
|
|
955
957
|
return this.ctx.request(methods.client.session.requestPermission, params, {
|
|
956
958
|
cancellationSignal: signal,
|
|
959
|
+
stopWaitingOnCancel: true,
|
|
957
960
|
});
|
|
958
961
|
}
|
|
959
962
|
readTextFile(params) {
|
|
@@ -965,6 +968,7 @@ class ClientConnection {
|
|
|
965
968
|
createElicitation(params, signal) {
|
|
966
969
|
return this.ctx.request(methods.client.elicitation.create, params, {
|
|
967
970
|
cancellationSignal: signal,
|
|
971
|
+
stopWaitingOnCancel: true,
|
|
968
972
|
});
|
|
969
973
|
}
|
|
970
974
|
completeElicitation(params) {
|
|
@@ -1222,6 +1226,8 @@ export class ClaudeAcpAgent {
|
|
|
1222
1226
|
replayedChildModels = new Map();
|
|
1223
1227
|
/** See the `foreground` constructor option. */
|
|
1224
1228
|
foregroundEvents;
|
|
1229
|
+
/** See the `background` constructor option. */
|
|
1230
|
+
backgroundWork;
|
|
1225
1231
|
/** Counts the prompts `startTurn()` has received, across sessions. */
|
|
1226
1232
|
promptsReceived = 0;
|
|
1227
1233
|
/** Per session, the `promptsReceived` count when its last cancel started.
|
|
@@ -1256,6 +1262,7 @@ export class ClaudeAcpAgent {
|
|
|
1256
1262
|
constructor(client, logger, options = {}) {
|
|
1257
1263
|
this.v2 = options.v2 ?? false;
|
|
1258
1264
|
this.foregroundEvents = options.foreground;
|
|
1265
|
+
this.backgroundWork = new BackgroundWorkReporter(options.background);
|
|
1259
1266
|
this.sessions = {};
|
|
1260
1267
|
this.client = new ChangedMetaClient(client, () => this.toolCallCapabilities.air.client);
|
|
1261
1268
|
this.logger = logger ?? console;
|
|
@@ -3431,28 +3438,18 @@ export class ClaudeAcpAgent {
|
|
|
3431
3438
|
}
|
|
3432
3439
|
this.syncAwaitingUser(params.sessionId, session);
|
|
3433
3440
|
};
|
|
3434
|
-
/** Ends the display terminal of a command that went to the background,
|
|
3435
|
-
*
|
|
3436
|
-
*
|
|
3437
|
-
*
|
|
3438
|
-
const
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
sessionUpdate: "tool_call_update",
|
|
3447
|
-
toolCallId: terminal.toolCallId,
|
|
3448
|
-
_meta: {
|
|
3449
|
-
...(terminal.parentToolUseId
|
|
3450
|
-
? { claudeCode: { parentToolUseId: terminal.parentToolUseId } }
|
|
3451
|
-
: {}),
|
|
3452
|
-
terminal_exit: { terminal_id: terminal.toolCallId, exit_code: null, signal: null },
|
|
3453
|
-
},
|
|
3454
|
-
},
|
|
3455
|
-
});
|
|
3441
|
+
/** Ends the display terminal of a command that went to the background, and
|
|
3442
|
+
* gives a tool call whose task went on in the background its final
|
|
3443
|
+
* status, once the task ends (see `Session.backgroundTerminals` and
|
|
3444
|
+
* `Session.backgroundToolCalls`). */
|
|
3445
|
+
const endBackgroundTask = async (taskId, end) => {
|
|
3446
|
+
await finishBackgroundTerminalOutput(session, taskId);
|
|
3447
|
+
const exit = takeBackgroundTerminalExit(session, params.sessionId, taskId);
|
|
3448
|
+
if (exit)
|
|
3449
|
+
await sendUpdate(exit);
|
|
3450
|
+
const toolCallEnd = takeBackgroundToolCallEnd(session, params.sessionId, taskId, end);
|
|
3451
|
+
if (toolCallEnd)
|
|
3452
|
+
await sendUpdate(toolCallEnd);
|
|
3456
3453
|
};
|
|
3457
3454
|
/** Settles a task. Its request may still be open, and an agent without a
|
|
3458
3455
|
* record no longer blocks the foreground work, so that is checked again. */
|
|
@@ -3932,6 +3929,9 @@ export class ClaudeAcpAgent {
|
|
|
3932
3929
|
const pause = messageLoopYielder.maybeYield();
|
|
3933
3930
|
if (pause)
|
|
3934
3931
|
await pause;
|
|
3932
|
+
// The last message may have changed the background work, and its
|
|
3933
|
+
// updates are sent, so the list follows them.
|
|
3934
|
+
this.backgroundWork.sync(params.sessionId, session, this.foregroundState(params.sessionId));
|
|
3935
3935
|
pendingNext ??= session.query
|
|
3936
3936
|
.next()
|
|
3937
3937
|
.then((result) => ({ kind: "message", result }));
|
|
@@ -4763,7 +4763,7 @@ export class ClaudeAcpAgent {
|
|
|
4763
4763
|
streamedBlocksByParent.delete(message.tool_use_id);
|
|
4764
4764
|
droppedStreams.delete(message.tool_use_id);
|
|
4765
4765
|
}
|
|
4766
|
-
await
|
|
4766
|
+
await endBackgroundTask(message.task_id, message.status);
|
|
4767
4767
|
if (message.status === "stopped") {
|
|
4768
4768
|
await closeStoppedSubagentToolCalls(message.tool_use_id);
|
|
4769
4769
|
}
|
|
@@ -4797,14 +4797,22 @@ export class ClaudeAcpAgent {
|
|
|
4797
4797
|
streamedBlocksByParent.delete(parentToolUseId);
|
|
4798
4798
|
droppedStreams.delete(parentToolUseId);
|
|
4799
4799
|
}
|
|
4800
|
-
await
|
|
4800
|
+
await endBackgroundTask(message.task_id, message.patch.status === "killed" ? "stopped" : message.patch.status);
|
|
4801
4801
|
if (message.patch.status === "killed") {
|
|
4802
4802
|
await closeStoppedSubagentToolCalls(parentToolUseId);
|
|
4803
4803
|
}
|
|
4804
4804
|
settleLiveTask(message.task_id);
|
|
4805
4805
|
}
|
|
4806
|
+
else if (message.patch.status === "paused") {
|
|
4807
|
+
const pause = backgroundToolCallPause(session, params.sessionId, message.task_id, true);
|
|
4808
|
+
if (pause)
|
|
4809
|
+
await sendUpdate(pause);
|
|
4810
|
+
}
|
|
4806
4811
|
else if (message.patch.status === "running" ||
|
|
4807
4812
|
message.patch.status === "pending") {
|
|
4813
|
+
const resume = backgroundToolCallPause(session, params.sessionId, message.task_id, false);
|
|
4814
|
+
if (resume)
|
|
4815
|
+
await sendUpdate(resume);
|
|
4808
4816
|
// The SDK can resume a finished subagent under the same
|
|
4809
4817
|
// agent id without a new task_started.
|
|
4810
4818
|
resumeLiveTask(message.task_id);
|
|
@@ -4958,8 +4966,15 @@ export class ClaudeAcpAgent {
|
|
|
4958
4966
|
// control requests, which this adapter never issues.
|
|
4959
4967
|
case "control_request_progress":
|
|
4960
4968
|
break;
|
|
4961
|
-
case "background_tasks_changed":
|
|
4969
|
+
case "background_tasks_changed": {
|
|
4962
4970
|
await asyncTasks.backgroundTasksChanged(message.tasks);
|
|
4971
|
+
if (session.reportsBackground) {
|
|
4972
|
+
const level = backgroundLevel(message.tasks);
|
|
4973
|
+
for (const taskId of droppedTasks(session, level)) {
|
|
4974
|
+
await endBackgroundTask(taskId, undefined);
|
|
4975
|
+
}
|
|
4976
|
+
session.backgroundLevel = level;
|
|
4977
|
+
}
|
|
4963
4978
|
// A level signal: the full live background-task set on every
|
|
4964
4979
|
// membership change, with REPLACE semantics. Used only to
|
|
4965
4980
|
// reconcile `liveBackgroundTasks` — dropping (or, for
|
|
@@ -5004,6 +5019,7 @@ export class ClaudeAcpAgent {
|
|
|
5004
5019
|
}
|
|
5005
5020
|
}
|
|
5006
5021
|
break;
|
|
5022
|
+
}
|
|
5007
5023
|
default:
|
|
5008
5024
|
unreachable(message, this.logger);
|
|
5009
5025
|
break;
|
|
@@ -6142,6 +6158,8 @@ export class ClaudeAcpAgent {
|
|
|
6142
6158
|
const acceptedPlanToolUseId = observeExitPlanToolResults(message, content, session);
|
|
6143
6159
|
let backgroundedToolCalls = new Set();
|
|
6144
6160
|
let runningTerminal;
|
|
6161
|
+
let runningTerminalTask;
|
|
6162
|
+
let runningToolCalls = new Set();
|
|
6145
6163
|
if (message.type === "user") {
|
|
6146
6164
|
rememberResolvedToolNames(session, content);
|
|
6147
6165
|
recordSubagentFileEdits(session.activeTurn, message.parent_tool_use_id, content, session.toolUseCache);
|
|
@@ -6162,8 +6180,13 @@ export class ClaudeAcpAgent {
|
|
|
6162
6180
|
...(message.parent_tool_use_id
|
|
6163
6181
|
? { parentToolUseId: message.parent_tool_use_id }
|
|
6164
6182
|
: {}),
|
|
6183
|
+
...(backgroundBashTask.outputFilePath
|
|
6184
|
+
? { outputFilePath: backgroundBashTask.outputFilePath }
|
|
6185
|
+
: {}),
|
|
6165
6186
|
});
|
|
6187
|
+
runningTerminalTask = backgroundBashTask.taskId;
|
|
6166
6188
|
}
|
|
6189
|
+
runningToolCalls = registerBackgroundToolCalls(session, content, toolUseResult, session.toolUseCache, message.parent_tool_use_id);
|
|
6167
6190
|
await asyncTasks.toolResults(content);
|
|
6168
6191
|
backgroundedToolCalls = backgroundedBashToolCallIds(content, session.toolUseCache, asyncTasks, backgroundBashTask);
|
|
6169
6192
|
const resumedAgentId = resumedNativeSubagentId(toolUseResult);
|
|
@@ -6199,7 +6222,14 @@ export class ClaudeAcpAgent {
|
|
|
6199
6222
|
// filtered out of `content` above; blocks that do pass through
|
|
6200
6223
|
// (e.g. a subagent image) carry the stamped parentToolUseId
|
|
6201
6224
|
// meta and are excluded there.
|
|
6202
|
-
await sendUpdate(withRunningTerminal(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled), runningTerminal));
|
|
6225
|
+
await sendUpdate(withRunningToolCall(withRunningTerminal(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled), runningTerminal), runningToolCalls));
|
|
6226
|
+
}
|
|
6227
|
+
// After the result's own updates: the command's output replaces
|
|
6228
|
+
// the placeholder that they gave its terminal.
|
|
6229
|
+
if (runningTerminalTask) {
|
|
6230
|
+
followBackgroundTerminalOutput(session, params.sessionId, runningTerminalTask, (notification) => sendUpdate(notification).catch((error) => {
|
|
6231
|
+
this.logger.error(`Session ${params.sessionId}: failed to send background command output:`, error);
|
|
6232
|
+
}));
|
|
6203
6233
|
}
|
|
6204
6234
|
break;
|
|
6205
6235
|
}
|
|
@@ -6847,10 +6877,18 @@ export class ClaudeAcpAgent {
|
|
|
6847
6877
|
return;
|
|
6848
6878
|
}
|
|
6849
6879
|
session.queryClosed = true;
|
|
6850
|
-
// Its list row now shows what the registry and the transcript tell.
|
|
6851
6880
|
for (const [sessionId, candidate] of Object.entries(this.sessions)) {
|
|
6852
|
-
if (candidate
|
|
6853
|
-
|
|
6881
|
+
if (candidate !== session)
|
|
6882
|
+
continue;
|
|
6883
|
+
// Its list row now shows what the registry and the transcript tell.
|
|
6884
|
+
this.sessionIndex.onOwnSessionChanged(sessionId);
|
|
6885
|
+
// Its background tasks end with the query's process.
|
|
6886
|
+
for (const exit of endBackgroundWork(session, sessionId)) {
|
|
6887
|
+
this.client.sessionUpdate(exit).catch((error) => {
|
|
6888
|
+
this.logger.error(`Session ${sessionId}: failed to end a background terminal:`, error);
|
|
6889
|
+
});
|
|
6890
|
+
}
|
|
6891
|
+
this.backgroundWork.sync(sessionId, session, this.foregroundState(sessionId));
|
|
6854
6892
|
}
|
|
6855
6893
|
session.consumer = undefined;
|
|
6856
6894
|
session.contextCompaction = undefined;
|
|
@@ -6904,6 +6942,7 @@ export class ClaudeAcpAgent {
|
|
|
6904
6942
|
session.asyncTaskRuntime?.clear();
|
|
6905
6943
|
this.replayedChildModels.delete(sessionId);
|
|
6906
6944
|
this.cancelledThrough.delete(sessionId);
|
|
6945
|
+
this.backgroundWork.forget(sessionId);
|
|
6907
6946
|
delete this.sessions[sessionId];
|
|
6908
6947
|
this.sessionIndex.onTeardown(sessionId);
|
|
6909
6948
|
const ended = await raceTimeoutAndAbort(turnsEnded, TEARDOWN_TURN_END_TIMEOUT_MS, new AbortController().signal);
|
|
@@ -6942,6 +6981,57 @@ export class ClaudeAcpAgent {
|
|
|
6942
6981
|
await this.teardownSession(params.sessionId);
|
|
6943
6982
|
return {};
|
|
6944
6983
|
}
|
|
6984
|
+
/** Stops a command running in the background (v2 `terminal/cancel`, see
|
|
6985
|
+
* {@link cancelBackgroundWork}). */
|
|
6986
|
+
cancelBackgroundTerminal(params) {
|
|
6987
|
+
return cancelBackgroundWork(this.sessions, params.sessionId, {
|
|
6988
|
+
terminalId: params.terminalId,
|
|
6989
|
+
});
|
|
6990
|
+
}
|
|
6991
|
+
/** Stops the task of a tool call running in the background (v2
|
|
6992
|
+
* `tool_call/cancel`, see {@link cancelBackgroundWork}). */
|
|
6993
|
+
cancelBackgroundToolCall(params) {
|
|
6994
|
+
return cancelBackgroundWork(this.sessions, params.sessionId, {
|
|
6995
|
+
toolCallId: params.toolCallId,
|
|
6996
|
+
});
|
|
6997
|
+
}
|
|
6998
|
+
/** Sends the current state of each tool call in `toolCallIds` that the root
|
|
6999
|
+
* session `sessionId` runs in the background: the call as its tool use
|
|
7000
|
+
* shows it, with its status now. v2 requires it for each tool call that a
|
|
7001
|
+
* `session/resume` snapshot lists. A failure is logged, as for
|
|
7002
|
+
* {@link publishLiveSubagents}. */
|
|
7003
|
+
async publishBackgroundToolCalls(sessionId, toolCallIds) {
|
|
7004
|
+
const session = this.sessions[sessionId];
|
|
7005
|
+
if (!session)
|
|
7006
|
+
return;
|
|
7007
|
+
const renderer = new AcpToolCallRenderer(this.toolCallCapabilities);
|
|
7008
|
+
try {
|
|
7009
|
+
for (const update of backgroundToolCallStates(session, sessionId, toolCallIds, renderer)) {
|
|
7010
|
+
await this.client.sessionUpdate(update);
|
|
7011
|
+
}
|
|
7012
|
+
}
|
|
7013
|
+
catch (error) {
|
|
7014
|
+
this.logger.error(`Session ${sessionId}: failed to send background tool calls:`, error);
|
|
7015
|
+
}
|
|
7016
|
+
}
|
|
7017
|
+
/** Sends the current `subagent_update` of each live subagent session in
|
|
7018
|
+
* `children`, of the session `sessionId` (see
|
|
7019
|
+
* {@link NativeSubagentRuntime.currentUpdate}). v2 requires it for each
|
|
7020
|
+
* child that a `session/resume` snapshot lists. A failure is logged, as
|
|
7021
|
+
* for {@link publishCurrentSubagentStates}. */
|
|
7022
|
+
async publishLiveSubagents(sessionId, children) {
|
|
7023
|
+
const runtime = this.sessions[sessionId]?.nativeSubagentRuntime;
|
|
7024
|
+
try {
|
|
7025
|
+
for (const child of children) {
|
|
7026
|
+
const update = runtime?.currentUpdate(child);
|
|
7027
|
+
if (update)
|
|
7028
|
+
await this.client.sessionUpdate(asSdkSessionNotification(update));
|
|
7029
|
+
}
|
|
7030
|
+
}
|
|
7031
|
+
catch (error) {
|
|
7032
|
+
this.logger.error(`Session ${sessionId}: failed to send current subagent states:`, error);
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
6945
7035
|
/** Per client: see {@link sessionIndex.SessionIndexConnection.deleteSession}. */
|
|
6946
7036
|
async deleteSession(params) {
|
|
6947
7037
|
return this.sessionIndex.deleteSession(params);
|
|
@@ -7739,10 +7829,9 @@ export class ClaudeAcpAgent {
|
|
|
7739
7829
|
/** Forward a permission request to the client, wiring the tool call's
|
|
7740
7830
|
* `signal` through as a `cancellationSignal`. When the turn is cancelled
|
|
7741
7831
|
* while the client's prompt is still open the signal aborts, the SDK sends
|
|
7742
|
-
* `$/cancel_request
|
|
7743
|
-
*
|
|
7744
|
-
*
|
|
7745
|
-
* `cancelled` outcome means. */
|
|
7832
|
+
* `$/cancel_request` and stops waiting for the client's answer. The
|
|
7833
|
+
* rejection surfaces as the "Tool use aborted" that the callers expect.
|
|
7834
|
+
* `canUseTool` decides what a `cancelled` outcome means. */
|
|
7746
7835
|
async requestPermissionFromClient(params, toolName, toolSignal, parentToolUseId, ownerSessionId, agentID, toolInput, previewContent) {
|
|
7747
7836
|
const signal = this.untilSessionClosed(ownerSessionId, toolSignal);
|
|
7748
7837
|
if (signal.aborted)
|
|
@@ -7756,13 +7845,10 @@ export class ClaudeAcpAgent {
|
|
|
7756
7845
|
await this.ensureToolCallEmitted(ownerSessionId, toolName, params.toolCall.toolCallId, toolInput, parentToolUseId, signal, params.sessionId, previewContent);
|
|
7757
7846
|
if (signal.aborted)
|
|
7758
7847
|
throw new Error("Tool use aborted");
|
|
7759
|
-
// Do not rely on every ACP client settling requestPermission after the
|
|
7760
|
-
// cancellation signal. The local race guarantees that Claude's tool call
|
|
7761
|
-
// is released even when an older or broken client ignores $/cancel_request.
|
|
7762
7848
|
// The request goes to a subagent's child session for a tool call of that
|
|
7763
7849
|
// subagent, but it blocks the turn of the owner session.
|
|
7764
7850
|
try {
|
|
7765
|
-
return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, signal, () => this.withPendingUserInput(ownerSessionId, signal, () =>
|
|
7851
|
+
return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, signal, () => this.withPendingUserInput(ownerSessionId, signal, () => this.client.requestPermission(params, signal), agentID));
|
|
7766
7852
|
}
|
|
7767
7853
|
catch (error) {
|
|
7768
7854
|
if (signal.aborted) {
|
|
@@ -7772,21 +7858,20 @@ export class ClaudeAcpAgent {
|
|
|
7772
7858
|
}
|
|
7773
7859
|
}
|
|
7774
7860
|
/** Send the client an elicitation for a callback of Claude Code. When the
|
|
7775
|
-
* callback's `signal` aborts, the SDK sends `$/cancel_request` and
|
|
7776
|
-
* waiting for
|
|
7777
|
-
* never sends.
|
|
7778
|
-
* {@link
|
|
7779
|
-
*
|
|
7780
|
-
* {@link withPendingUserInput}), which keeps steering non-interrupting, and
|
|
7781
|
-
* a child's `openRequests` would not drop. */
|
|
7861
|
+
* callback's `signal` aborts, the SDK sends `$/cancel_request` and the call
|
|
7862
|
+
* rejects at once, without waiting for a reply that a client ignoring the
|
|
7863
|
+
* cancel never sends. Left open, the request would stay in
|
|
7864
|
+
* `openUserInputs` (see {@link withPendingUserInput}), which keeps steering
|
|
7865
|
+
* non-interrupting, and a child's `openRequests` would not drop. */
|
|
7782
7866
|
elicitFromClient(request, signal) {
|
|
7783
|
-
return
|
|
7867
|
+
return this.client.createElicitation(request, signal);
|
|
7784
7868
|
}
|
|
7785
7869
|
/** `signal`, also aborted when `teardownSession` aborts the
|
|
7786
7870
|
* `abortController` of session `sessionId`. Claude Code aborts a callback's
|
|
7787
7871
|
* own signal when its query closes, but not necessarily before
|
|
7788
|
-
* `session/close` responds, and v2 requires every pending
|
|
7789
|
-
* the session to be cancelled with
|
|
7872
|
+
* `session/close` responds, and v2 requires every pending permission
|
|
7873
|
+
* request and elicitation of the session to be cancelled with
|
|
7874
|
+
* `$/cancel_request` before that response. */
|
|
7790
7875
|
untilSessionClosed(sessionId, signal) {
|
|
7791
7876
|
const sessionSignal = this.sessions[sessionId]?.abortController?.signal;
|
|
7792
7877
|
return sessionSignal ? AbortSignal.any([signal, sessionSignal]) : signal;
|
|
@@ -9118,6 +9203,7 @@ export class ClaudeAcpAgent {
|
|
|
9118
9203
|
...providerEnv,
|
|
9119
9204
|
// Opt-in to session state events like when the agent is idle
|
|
9120
9205
|
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
|
9206
|
+
...STARTUP_FAILURE_RESULTS_ENV,
|
|
9121
9207
|
};
|
|
9122
9208
|
// Scopes the context-window cache to this query's backend (see
|
|
9123
9209
|
// `contextWindowCache`). Derived from the same `env` object handed to the
|
|
@@ -9362,7 +9448,7 @@ export class ClaudeAcpAgent {
|
|
|
9362
9448
|
error.message.includes("No conversation found with session ID"))) {
|
|
9363
9449
|
throw RequestError.sessionNotFound(sessionId);
|
|
9364
9450
|
}
|
|
9365
|
-
throw error;
|
|
9451
|
+
throw startupFailureError(error) ?? error;
|
|
9366
9452
|
}
|
|
9367
9453
|
timing.phase("sdk-initialize");
|
|
9368
9454
|
// Publish the identity BEFORE the guard can refuse this session. A
|
|
@@ -9586,7 +9672,9 @@ export class ClaudeAcpAgent {
|
|
|
9586
9672
|
emittedToolCalls: new Set(),
|
|
9587
9673
|
eagerToolCallSessions: new Map(),
|
|
9588
9674
|
liveBackgroundTasks: new Map(),
|
|
9675
|
+
reportsBackground: this.backgroundWork.enabled && options.perTaskStopAffordance === true,
|
|
9589
9676
|
stopsWorkflowsOnCancel: options.perTaskStopAffordance === true &&
|
|
9677
|
+
!this.backgroundWork.enabled &&
|
|
9590
9678
|
!clientSupportsAsyncTasks(this.clientCapabilities),
|
|
9591
9679
|
nativeSubagentsByTaskId: new Map(),
|
|
9592
9680
|
nativeSubagentTaskIdByToolUseId: new Map(),
|
|
@@ -9600,6 +9688,7 @@ export class ClaudeAcpAgent {
|
|
|
9600
9688
|
fileChangeReporter,
|
|
9601
9689
|
};
|
|
9602
9690
|
timing.phase("register");
|
|
9691
|
+
this.backgroundWork.sync(sessionId, this.sessions[sessionId], this.foregroundState(sessionId));
|
|
9603
9692
|
this.refreshContextWindowInBackground(sessionId, this.sessions[sessionId]);
|
|
9604
9693
|
return {
|
|
9605
9694
|
sessionId,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** How often a running command's output file is read. */
|
|
2
|
+
export declare const BACKGROUND_OUTPUT_POLL_MS = 500;
|
|
3
|
+
/** The most bytes one read takes, so a burst of output does not block the loop. */
|
|
4
|
+
export declare const BACKGROUND_OUTPUT_READ_BYTES: number;
|
|
5
|
+
/**
|
|
6
|
+
* The most unread bytes that are still sent. A command that writes faster
|
|
7
|
+
* than the polls read falls behind. Past this, the reads skip to the file's
|
|
8
|
+
* last bytes, as a snapshot: a terminal shows the latest output, and a
|
|
9
|
+
* client keeps a bounded amount of it anyway.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES: number;
|
|
12
|
+
/**
|
|
13
|
+
* Text that the file adds. A snapshot replaces the terminal's output so far:
|
|
14
|
+
* the first one replaces the placeholder of the Bash result, and a later one
|
|
15
|
+
* follows a skip or a file that shrank. Other text appends to it.
|
|
16
|
+
*/
|
|
17
|
+
export type BackgroundOutput = {
|
|
18
|
+
text: string;
|
|
19
|
+
replaces: boolean;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Follows the output file `path` of one background command and hands each
|
|
23
|
+
* piece of new text to `publish`, in order. Reads never overlap. A read that
|
|
24
|
+
* fails, for example because the file does not exist yet, is tried again at
|
|
25
|
+
* the next poll. `publish` must not reject: the next read waits for it.
|
|
26
|
+
*/
|
|
27
|
+
export declare class BackgroundOutputTail {
|
|
28
|
+
private readonly path;
|
|
29
|
+
private readonly publish;
|
|
30
|
+
private offset;
|
|
31
|
+
private decoder;
|
|
32
|
+
/** Whether the next text replaces the terminal's output. */
|
|
33
|
+
private replaces;
|
|
34
|
+
private stopped;
|
|
35
|
+
private reading;
|
|
36
|
+
private readonly timer;
|
|
37
|
+
constructor(path: string, publish: (output: BackgroundOutput) => Promise<void>);
|
|
38
|
+
/**
|
|
39
|
+
* Reads the rest of the file and stops. The command ended, so what the file
|
|
40
|
+
* holds is its whole output, and it goes out before the terminal's exit.
|
|
41
|
+
*/
|
|
42
|
+
finish(): Promise<void>;
|
|
43
|
+
/** Stops without reading more: the session ended. */
|
|
44
|
+
stop(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Reads what the file adds, one read of at most
|
|
47
|
+
* {@link BACKGROUND_OUTPUT_READ_BYTES} at a time, and publishes each read's
|
|
48
|
+
* text before the next. `toEnd` reads until the end of the file, and ends a
|
|
49
|
+
* character that the command cut off with a replacement character.
|
|
50
|
+
*/
|
|
51
|
+
private read;
|
|
52
|
+
private send;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=background-output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"background-output.d.ts","sourceRoot":"","sources":["../src/background-output.ts"],"names":[],"mappings":"AAcA,yDAAyD;AACzD,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,mFAAmF;AACnF,eAAO,MAAM,4BAA4B,QAAa,CAAC;AAEvD;;;;;GAKG;AACH,eAAO,MAAM,mCAAmC,QAAc,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAEnE;;;;;GAKG;AACH,qBAAa,oBAAoB;IAU7B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAV1B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAA6B;IAC5C,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAoC;IACnD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;gBAGpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC;IAOvE;;;OAGG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAM7B,qDAAqD;IACrD,IAAI,IAAI,IAAI;IAKZ;;;;;OAKG;IACH,OAAO,CAAC,IAAI;YA4CE,IAAI;CAKnB"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The output of a command that went to the background, read from the file
|
|
3
|
+
* that Claude Code writes it to. Claude Code streams no output of such a
|
|
4
|
+
* command, so its display terminal would show only the placeholder of the
|
|
5
|
+
* Bash result ("Command running in background with ID …") until the command
|
|
6
|
+
* exits. ACP v2 asks an Agent to keep a listed terminal's output current
|
|
7
|
+
* whenever it can read it (the v2 prompt lifecycle, "Background work").
|
|
8
|
+
*
|
|
9
|
+
* The file is polled: watching a file is not reliable on every platform, and
|
|
10
|
+
* a missing file only means that nothing was written yet.
|
|
11
|
+
*/
|
|
12
|
+
import { open } from "node:fs/promises";
|
|
13
|
+
import { StringDecoder } from "node:string_decoder";
|
|
14
|
+
/** How often a running command's output file is read. */
|
|
15
|
+
export const BACKGROUND_OUTPUT_POLL_MS = 500;
|
|
16
|
+
/** The most bytes one read takes, so a burst of output does not block the loop. */
|
|
17
|
+
export const BACKGROUND_OUTPUT_READ_BYTES = 256 * 1024;
|
|
18
|
+
/**
|
|
19
|
+
* The most unread bytes that are still sent. A command that writes faster
|
|
20
|
+
* than the polls read falls behind. Past this, the reads skip to the file's
|
|
21
|
+
* last bytes, as a snapshot: a terminal shows the latest output, and a
|
|
22
|
+
* client keeps a bounded amount of it anyway.
|
|
23
|
+
*/
|
|
24
|
+
export const BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES = 1024 * 1024;
|
|
25
|
+
/**
|
|
26
|
+
* Follows the output file `path` of one background command and hands each
|
|
27
|
+
* piece of new text to `publish`, in order. Reads never overlap. A read that
|
|
28
|
+
* fails, for example because the file does not exist yet, is tried again at
|
|
29
|
+
* the next poll. `publish` must not reject: the next read waits for it.
|
|
30
|
+
*/
|
|
31
|
+
export class BackgroundOutputTail {
|
|
32
|
+
path;
|
|
33
|
+
publish;
|
|
34
|
+
offset = 0;
|
|
35
|
+
decoder = new StringDecoder("utf8");
|
|
36
|
+
/** Whether the next text replaces the terminal's output. */
|
|
37
|
+
replaces = true;
|
|
38
|
+
stopped = false;
|
|
39
|
+
reading = Promise.resolve();
|
|
40
|
+
timer;
|
|
41
|
+
constructor(path, publish) {
|
|
42
|
+
this.path = path;
|
|
43
|
+
this.publish = publish;
|
|
44
|
+
this.timer = setInterval(() => void this.read(false), BACKGROUND_OUTPUT_POLL_MS);
|
|
45
|
+
// A command that runs on must not keep the agent process alive.
|
|
46
|
+
this.timer.unref();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reads the rest of the file and stops. The command ended, so what the file
|
|
50
|
+
* holds is its whole output, and it goes out before the terminal's exit.
|
|
51
|
+
*/
|
|
52
|
+
async finish() {
|
|
53
|
+
clearInterval(this.timer);
|
|
54
|
+
await this.read(true);
|
|
55
|
+
this.stopped = true;
|
|
56
|
+
}
|
|
57
|
+
/** Stops without reading more: the session ended. */
|
|
58
|
+
stop() {
|
|
59
|
+
clearInterval(this.timer);
|
|
60
|
+
this.stopped = true;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Reads what the file adds, one read of at most
|
|
64
|
+
* {@link BACKGROUND_OUTPUT_READ_BYTES} at a time, and publishes each read's
|
|
65
|
+
* text before the next. `toEnd` reads until the end of the file, and ends a
|
|
66
|
+
* character that the command cut off with a replacement character.
|
|
67
|
+
*/
|
|
68
|
+
read(toEnd) {
|
|
69
|
+
this.reading = this.reading.then(async () => {
|
|
70
|
+
if (this.stopped)
|
|
71
|
+
return;
|
|
72
|
+
let file;
|
|
73
|
+
try {
|
|
74
|
+
file = await open(this.path, "r");
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Nothing written yet, or the file is gone: try again at the next
|
|
78
|
+
// poll. At the end, the terminal keeps the output it has.
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const { size } = await file.stat();
|
|
83
|
+
// A file that shrank is a new one: its output starts over. Past the
|
|
84
|
+
// backlog, the reads skip to the file's last bytes.
|
|
85
|
+
const start = size < this.offset ? 0 : this.offset;
|
|
86
|
+
const skipTo = Math.max(start, size - BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES);
|
|
87
|
+
if (skipTo !== this.offset) {
|
|
88
|
+
this.offset = skipTo;
|
|
89
|
+
this.decoder = new StringDecoder("utf8");
|
|
90
|
+
this.replaces = true;
|
|
91
|
+
}
|
|
92
|
+
const buffer = Buffer.alloc(BACKGROUND_OUTPUT_READ_BYTES);
|
|
93
|
+
let bytesRead;
|
|
94
|
+
do {
|
|
95
|
+
({ bytesRead } = await file.read(buffer, 0, buffer.length, this.offset));
|
|
96
|
+
this.offset += bytesRead;
|
|
97
|
+
const text = this.decoder.write(buffer.subarray(0, bytesRead));
|
|
98
|
+
if (text !== "")
|
|
99
|
+
await this.send(text);
|
|
100
|
+
} while (toEnd && bytesRead > 0);
|
|
101
|
+
if (toEnd) {
|
|
102
|
+
const rest = this.decoder.end();
|
|
103
|
+
// A command that wrote nothing still replaces the placeholder.
|
|
104
|
+
if (rest !== "" || this.replaces)
|
|
105
|
+
await this.send(rest);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// The read failed: try again at the next poll.
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await file.close();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return this.reading;
|
|
116
|
+
}
|
|
117
|
+
async send(text) {
|
|
118
|
+
const replaces = this.replaces;
|
|
119
|
+
this.replaces = false;
|
|
120
|
+
await this.publish({ text, replaces });
|
|
121
|
+
}
|
|
122
|
+
}
|