@agentclientprotocol/claude-agent-acp 0.68.0 → 0.70.0
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 +31 -1
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +313 -45
- package/dist/file-change-audit.d.ts +56 -0
- package/dist/file-change-audit.d.ts.map +1 -0
- package/dist/file-change-audit.js +379 -0
- package/dist/index.js +24 -1
- package/dist/session-failure-extension.d.ts +1 -1
- package/dist/session-failure-extension.d.ts.map +1 -1
- package/dist/session-failure-extension.js +2 -2
- package/package.json +1 -1
package/dist/acp-agent.js
CHANGED
|
@@ -12,6 +12,7 @@ import packageJson from "../package.json" with { type: "json" };
|
|
|
12
12
|
import { applyAskElicitationResponse, askUserQuestionsToCreateRequest, createElicitationResponseToElicitResult, extractAskUserQuestions, extractRefusalFallbackPrompt, mcpElicitationToCreateRequest, REFUSAL_FALLBACK_DIALOG_KIND, refusalFallbackResultFromResponse, refusalFallbackToCreateRequest, } from "./elicitation.js";
|
|
13
13
|
import { SettingsManager } from "./settings.js";
|
|
14
14
|
import { activeUsageLimitMessage, airSessionFailureCapabilityMeta, assistantMessageText, createSessionFailureState, isSyntheticUsageLimitMessage, providerFailureCategory, SessionFailureController, sessionFailureMeta, supportsAirSessionFailures, } from "./session-failure-extension.js";
|
|
15
|
+
import { AGENT_FILE_CHANGE_REPORT_CAPABILITY, agentFileChangeReportMeta, agentFileChangeReportRequestId, containsFileChangeAuditMarker, createFileChangeAuditSupport, createFileChangeAuditTurnState, FILE_CHANGE_AUDIT_SERVER_NAME, isFileChangeAuditReportPhase, isFileChangeAuditTool, supportsAgentFileChangeReport, } from "./file-change-audit.js";
|
|
15
16
|
import { applyTaskCreate, applyTaskList, applyTaskUpdate, createPostToolUseHook, createTaskHook, parseTaskCreateOutput, parseTaskListOutput, parseTaskUpdateOutput, planEntries, registerHookCallback, taskStateToPlanEntries, toolInfoFromToolUse, toolUpdateFromDiffToolResponse, toolUpdateFromToolResult, } from "./tools.js";
|
|
16
17
|
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
|
17
18
|
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
|
@@ -577,6 +578,8 @@ export class ClaudeAcpAgent {
|
|
|
577
578
|
gatewayAuthRequest;
|
|
578
579
|
/** Set while ACP overrides the agent's native provider configuration. */
|
|
579
580
|
providerConfig;
|
|
581
|
+
/** Serializes provider changes while every open query is recreated between turns. */
|
|
582
|
+
providerUpdate = null;
|
|
580
583
|
/** Grace period before a `session/cancel` forces a wedged prompt loop to
|
|
581
584
|
* return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
|
|
582
585
|
* tests can shrink it. */
|
|
@@ -729,7 +732,7 @@ export class ClaudeAcpAgent {
|
|
|
729
732
|
// steering extension contract: advertises the `_session/steering` request
|
|
730
733
|
// so clients know they may inject a follow-up into a running turn.
|
|
731
734
|
_meta: {
|
|
732
|
-
...airSessionFailureCapabilityMeta(),
|
|
735
|
+
...airSessionFailureCapabilityMeta(AGENT_FILE_CHANGE_REPORT_CAPABILITY),
|
|
733
736
|
steering: {
|
|
734
737
|
supported: true,
|
|
735
738
|
},
|
|
@@ -742,6 +745,8 @@ export class ClaudeAcpAgent {
|
|
|
742
745
|
};
|
|
743
746
|
}
|
|
744
747
|
async newSession(params) {
|
|
748
|
+
if (this.providerUpdate)
|
|
749
|
+
await this.providerUpdate;
|
|
745
750
|
const response = await this.createSession(params, {
|
|
746
751
|
// Revisit these meta values once we support resume
|
|
747
752
|
resume: params._meta?.claudeCode?.options?.resume,
|
|
@@ -753,6 +758,8 @@ export class ClaudeAcpAgent {
|
|
|
753
758
|
return response;
|
|
754
759
|
}
|
|
755
760
|
async unstable_forkSession(params) {
|
|
761
|
+
if (this.providerUpdate)
|
|
762
|
+
await this.providerUpdate;
|
|
756
763
|
const response = await this.createSession({
|
|
757
764
|
cwd: params.cwd,
|
|
758
765
|
mcpServers: params.mcpServers ?? [],
|
|
@@ -769,6 +776,8 @@ export class ClaudeAcpAgent {
|
|
|
769
776
|
return response;
|
|
770
777
|
}
|
|
771
778
|
async resumeSession(params) {
|
|
779
|
+
if (this.providerUpdate)
|
|
780
|
+
await this.providerUpdate;
|
|
772
781
|
const result = await this.getOrCreateSession(params);
|
|
773
782
|
// Needs to happen after we return the session
|
|
774
783
|
setTimeout(() => {
|
|
@@ -777,6 +786,8 @@ export class ClaudeAcpAgent {
|
|
|
777
786
|
return result;
|
|
778
787
|
}
|
|
779
788
|
async loadSession(params) {
|
|
789
|
+
if (this.providerUpdate)
|
|
790
|
+
await this.providerUpdate;
|
|
780
791
|
const result = await this.getOrCreateSession(params);
|
|
781
792
|
await this.replaySessionHistory(params.sessionId);
|
|
782
793
|
// Send available commands after replay so it doesn't interleave with history
|
|
@@ -845,6 +856,7 @@ export class ClaudeAcpAgent {
|
|
|
845
856
|
}
|
|
846
857
|
async unstable_listProviders(_params) {
|
|
847
858
|
const config = this.providerConfig ?? this.defaultProviderConfig();
|
|
859
|
+
this.logger.log(`[providers/list] apiType=${config.apiType} baseUrl=${config.baseUrl} overridden=${this.providerConfig !== undefined}`);
|
|
848
860
|
const provider = {
|
|
849
861
|
providerId: PROVIDER_ID,
|
|
850
862
|
supported: SUPPORTED_PROTOCOLS,
|
|
@@ -887,7 +899,8 @@ export class ClaudeAcpAgent {
|
|
|
887
899
|
}
|
|
888
900
|
config.vertex = { projectId: vertex.projectId, region: vertex.region };
|
|
889
901
|
}
|
|
890
|
-
this.
|
|
902
|
+
this.logger.log(`[providers/set] apiType=${config.apiType} baseUrl=${config.baseUrl} sessions=${Object.keys(this.sessions).length}`);
|
|
903
|
+
await this.enqueueProviderUpdate(config);
|
|
891
904
|
return {};
|
|
892
905
|
}
|
|
893
906
|
/**
|
|
@@ -896,7 +909,8 @@ export class ClaudeAcpAgent {
|
|
|
896
909
|
*/
|
|
897
910
|
async unstable_disableProvider(params) {
|
|
898
911
|
if (params.providerId === PROVIDER_ID) {
|
|
899
|
-
this.
|
|
912
|
+
this.logger.log(`[providers/disable] sessions=${Object.keys(this.sessions).length}`);
|
|
913
|
+
await this.enqueueProviderUpdate(undefined);
|
|
900
914
|
}
|
|
901
915
|
// Unknown provider: idempotent success.
|
|
902
916
|
return {};
|
|
@@ -957,6 +971,8 @@ export class ClaudeAcpAgent {
|
|
|
957
971
|
}
|
|
958
972
|
}
|
|
959
973
|
async prompt(params) {
|
|
974
|
+
if (this.providerUpdate)
|
|
975
|
+
await this.providerUpdate;
|
|
960
976
|
const session = this.sessions[params.sessionId];
|
|
961
977
|
if (!session) {
|
|
962
978
|
throw new Error("Session not found");
|
|
@@ -977,6 +993,15 @@ export class ClaudeAcpAgent {
|
|
|
977
993
|
// user message, so the consumer can't promote the turn from the echo.
|
|
978
994
|
const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
|
|
979
995
|
const isLocalOnlyCommand = firstText.startsWith("/") && LOCAL_ONLY_COMMANDS.has(firstText.split(" ", 1)[0]);
|
|
996
|
+
const fileChangeReportRequestId = supportsAgentFileChangeReport(this.clientCapabilities)
|
|
997
|
+
? agentFileChangeReportRequestId(params._meta)
|
|
998
|
+
: undefined;
|
|
999
|
+
let fileChangeAudit;
|
|
1000
|
+
if (fileChangeReportRequestId &&
|
|
1001
|
+
!session.fileChangeReportRequestIds.has(fileChangeReportRequestId)) {
|
|
1002
|
+
session.fileChangeReportRequestIds.add(fileChangeReportRequestId);
|
|
1003
|
+
fileChangeAudit = createFileChangeAuditTurnState(fileChangeReportRequestId);
|
|
1004
|
+
}
|
|
980
1005
|
// Each prompt is a Turn whose deferred the persistent consumer settles once
|
|
981
1006
|
// the turn's outcome is known. `prompt()` owns no loop: it enqueues the
|
|
982
1007
|
// turn, pushes the user message onto the streaming input, makes sure the
|
|
@@ -984,13 +1009,24 @@ export class ClaudeAcpAgent {
|
|
|
984
1009
|
const turn = {
|
|
985
1010
|
promptUuid,
|
|
986
1011
|
isLocalOnlyCommand,
|
|
1012
|
+
...(fileChangeAudit ? { fileChangeAudit } : {}),
|
|
987
1013
|
settled: false,
|
|
988
1014
|
resolve: () => { },
|
|
989
1015
|
reject: () => { },
|
|
990
1016
|
};
|
|
1017
|
+
let completeTurn;
|
|
1018
|
+
turn.completion = new Promise((resolve) => {
|
|
1019
|
+
completeTurn = resolve;
|
|
1020
|
+
});
|
|
991
1021
|
const response = new Promise((resolve, reject) => {
|
|
992
|
-
turn.resolve =
|
|
993
|
-
|
|
1022
|
+
turn.resolve = (result) => {
|
|
1023
|
+
resolve(result);
|
|
1024
|
+
completeTurn();
|
|
1025
|
+
};
|
|
1026
|
+
turn.reject = (error) => {
|
|
1027
|
+
reject(error);
|
|
1028
|
+
completeTurn();
|
|
1029
|
+
};
|
|
994
1030
|
});
|
|
995
1031
|
session.turnQueue ??= [];
|
|
996
1032
|
session.turnQueue.push(turn);
|
|
@@ -1147,6 +1183,15 @@ export class ClaudeAcpAgent {
|
|
|
1147
1183
|
await this.publishGoalFromPrompt(sessionId, firstText, steeredUuid);
|
|
1148
1184
|
return { outcome: "injected" };
|
|
1149
1185
|
}
|
|
1186
|
+
/** Publish the audit terminal for every turn path that did not reach the
|
|
1187
|
+
* report tool. The support flips the turn state synchronously before its
|
|
1188
|
+
* transport await, so callers can stay fail-open and settle the ACP prompt
|
|
1189
|
+
* immediately without allowing a racing lifecycle path to publish twice. */
|
|
1190
|
+
finishFileChangeAudit(session, turn, reason) {
|
|
1191
|
+
if (!turn.fileChangeAudit || !session.fileChangeAuditSupport)
|
|
1192
|
+
return;
|
|
1193
|
+
void session.fileChangeAuditSupport.finishUnavailable(turn.fileChangeAudit, reason);
|
|
1194
|
+
}
|
|
1150
1195
|
/** Lazily start the per-session consumer that drains the SDK query stream for
|
|
1151
1196
|
* the session's whole life. Idempotent: only the first `prompt()` starts it. */
|
|
1152
1197
|
ensureConsumer(session, sessionId) {
|
|
@@ -1228,6 +1273,14 @@ export class ClaudeAcpAgent {
|
|
|
1228
1273
|
* as the turn's answer. */
|
|
1229
1274
|
const sendUpdate = async (notification) => {
|
|
1230
1275
|
const { update } = notification;
|
|
1276
|
+
if (isFileChangeAuditReportPhase(session.activeTurn?.fileChangeAudit) &&
|
|
1277
|
+
(update.sessionUpdate === "agent_message_chunk" ||
|
|
1278
|
+
update.sessionUpdate === "agent_thought_chunk" ||
|
|
1279
|
+
update.sessionUpdate === "user_message_chunk" ||
|
|
1280
|
+
update.sessionUpdate === "tool_call" ||
|
|
1281
|
+
update.sessionUpdate === "tool_call_update")) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1231
1284
|
if (update.sessionUpdate === "agent_message_chunk") {
|
|
1232
1285
|
const claudeMeta = update._meta?.claudeCode;
|
|
1233
1286
|
if (!claudeMeta?.parentToolUseId) {
|
|
@@ -1523,11 +1576,14 @@ export class ClaudeAcpAgent {
|
|
|
1523
1576
|
};
|
|
1524
1577
|
/** Settle the active turn's deferred exactly once, disarm the force-cancel
|
|
1525
1578
|
* backstop (the turn is over), and drop it from the queue. */
|
|
1526
|
-
const settleActive = (result
|
|
1579
|
+
const settleActive = (result, auditReason = result.stopReason === "cancelled"
|
|
1580
|
+
? "cancelled"
|
|
1581
|
+
: "notReported") => {
|
|
1527
1582
|
const turn = session.activeTurn;
|
|
1528
1583
|
if (!turn || turn.settled) {
|
|
1529
1584
|
return;
|
|
1530
1585
|
}
|
|
1586
|
+
this.finishFileChangeAudit(session, turn, auditReason);
|
|
1531
1587
|
// Captured before the settled flip below (isHeldOpen tests !settled).
|
|
1532
1588
|
const wasHeld = isHeldOpen(turn);
|
|
1533
1589
|
turn.settled = true;
|
|
@@ -1561,6 +1617,7 @@ export class ClaudeAcpAgent {
|
|
|
1561
1617
|
this.logger.error(`Session ${params.sessionId}: cannot fail active turn because no unsettled active turn exists: ${error}`);
|
|
1562
1618
|
return;
|
|
1563
1619
|
}
|
|
1620
|
+
this.finishFileChangeAudit(session, turn, "providerError");
|
|
1564
1621
|
turn.settled = true;
|
|
1565
1622
|
session.turnQueue = (session.turnQueue ?? []).filter((t) => t !== turn);
|
|
1566
1623
|
session.activeTurn = null;
|
|
@@ -1595,7 +1652,7 @@ export class ClaudeAcpAgent {
|
|
|
1595
1652
|
stopReason: "end_turn",
|
|
1596
1653
|
usage: sessionUsage(session),
|
|
1597
1654
|
_meta: sessionFailureMeta(failure),
|
|
1598
|
-
});
|
|
1655
|
+
}, "providerError");
|
|
1599
1656
|
};
|
|
1600
1657
|
/** Reject every in-flight turn — used when the stream dies. */
|
|
1601
1658
|
const failAllTurns = (error) => {
|
|
@@ -1607,6 +1664,7 @@ export class ClaudeAcpAgent {
|
|
|
1607
1664
|
session.turnQueue = [];
|
|
1608
1665
|
for (const turn of turns) {
|
|
1609
1666
|
if (!turn.settled) {
|
|
1667
|
+
this.finishFileChangeAudit(session, turn, "providerError");
|
|
1610
1668
|
const wasHeld = isHeldOpen(turn);
|
|
1611
1669
|
turn.settled = true;
|
|
1612
1670
|
if (wasHeld) {
|
|
@@ -1755,6 +1813,7 @@ export class ClaudeAcpAgent {
|
|
|
1755
1813
|
// still here was enqueued afterward and was not part of the cancel.)
|
|
1756
1814
|
for (const queued of [...(session.turnQueue ?? [])]) {
|
|
1757
1815
|
if (!queued.settled) {
|
|
1816
|
+
this.finishFileChangeAudit(session, queued, "providerError");
|
|
1758
1817
|
queued.settled = true;
|
|
1759
1818
|
queued.reject(RequestError.internalError(undefined, SESSION_ENDED_MESSAGE));
|
|
1760
1819
|
}
|
|
@@ -3417,6 +3476,7 @@ export class ClaudeAcpAgent {
|
|
|
3417
3476
|
if (session.turnQueue) {
|
|
3418
3477
|
for (const turn of session.turnQueue) {
|
|
3419
3478
|
if (turn !== session.activeTurn && !turn.settled) {
|
|
3479
|
+
this.finishFileChangeAudit(session, turn, "cancelled");
|
|
3420
3480
|
turn.settled = true;
|
|
3421
3481
|
// Deliberately no `usage`: a queued turn never ran, so the session
|
|
3422
3482
|
// accumulator (the active turn's tally) is not its spend.
|
|
@@ -3478,6 +3538,7 @@ export class ClaudeAcpAgent {
|
|
|
3478
3538
|
{
|
|
3479
3539
|
const active = session.activeTurn;
|
|
3480
3540
|
if (isHeldOpen(active)) {
|
|
3541
|
+
this.finishFileChangeAudit(session, active, "cancelled");
|
|
3481
3542
|
active.settled = true;
|
|
3482
3543
|
// Mirror settleActive's invariants (it is consumer-scoped and
|
|
3483
3544
|
// unreachable from here): disarm the backstop — none should be
|
|
@@ -3815,6 +3876,13 @@ export class ClaudeAcpAgent {
|
|
|
3815
3876
|
})
|
|
3816
3877
|
: undefined;
|
|
3817
3878
|
let replayTurnId;
|
|
3879
|
+
// Stop-hook additionalContext is persisted as an internal user message.
|
|
3880
|
+
// Once that marker (or the internal tool itself) appears, suppress the
|
|
3881
|
+
// whole audit exchange until its tool result. This also hides a disobedient
|
|
3882
|
+
// model's separate prose message, while an ordinary next user prompt safely
|
|
3883
|
+
// ends an incomplete audit lane.
|
|
3884
|
+
let replayingFileChangeAudit = false;
|
|
3885
|
+
const replayFileChangeAuditToolUseIds = new Set();
|
|
3818
3886
|
for (const message of messages) {
|
|
3819
3887
|
if (message.type === "user" &&
|
|
3820
3888
|
message.parent_tool_use_id === null &&
|
|
@@ -3864,6 +3932,57 @@ export class ClaudeAcpAgent {
|
|
|
3864
3932
|
if (content === null)
|
|
3865
3933
|
continue;
|
|
3866
3934
|
}
|
|
3935
|
+
const auditBlocks = Array.isArray(content)
|
|
3936
|
+
? content.filter((block) => typeof block === "object" && block !== null)
|
|
3937
|
+
: [];
|
|
3938
|
+
const hasFileChangeAuditMarker = (typeof content === "string" && containsFileChangeAuditMarker(content)) ||
|
|
3939
|
+
auditBlocks.some((block) => typeof block.text === "string" && containsFileChangeAuditMarker(block.text));
|
|
3940
|
+
const fileChangeAuditToolUseIds = auditBlocks.flatMap((block) => (block.type === "tool_use" ||
|
|
3941
|
+
block.type === "server_tool_use" ||
|
|
3942
|
+
block.type === "mcp_tool_use") &&
|
|
3943
|
+
typeof block.name === "string" &&
|
|
3944
|
+
isFileChangeAuditTool(block.name) &&
|
|
3945
|
+
typeof block.id === "string"
|
|
3946
|
+
? [block.id]
|
|
3947
|
+
: []);
|
|
3948
|
+
const replayMessageRole = message.message
|
|
3949
|
+
?.role;
|
|
3950
|
+
if (hasFileChangeAuditMarker || fileChangeAuditToolUseIds.length > 0) {
|
|
3951
|
+
replayingFileChangeAudit = true;
|
|
3952
|
+
for (const toolUseId of fileChangeAuditToolUseIds) {
|
|
3953
|
+
replayFileChangeAuditToolUseIds.add(toolUseId);
|
|
3954
|
+
}
|
|
3955
|
+
continue;
|
|
3956
|
+
}
|
|
3957
|
+
if (replayingFileChangeAudit) {
|
|
3958
|
+
const toolResultIds = auditBlocks.flatMap((block) => (block.type === "tool_result" || block.type === "mcp_tool_result") &&
|
|
3959
|
+
typeof block.tool_use_id === "string"
|
|
3960
|
+
? [block.tool_use_id]
|
|
3961
|
+
: []);
|
|
3962
|
+
let completedReport = false;
|
|
3963
|
+
for (const toolUseId of toolResultIds) {
|
|
3964
|
+
if (replayFileChangeAuditToolUseIds.delete(toolUseId))
|
|
3965
|
+
completedReport = true;
|
|
3966
|
+
}
|
|
3967
|
+
if (completedReport && replayFileChangeAuditToolUseIds.size === 0) {
|
|
3968
|
+
replayingFileChangeAudit = false;
|
|
3969
|
+
continue;
|
|
3970
|
+
}
|
|
3971
|
+
// A denied attempt to call another tool is still part of the hidden
|
|
3972
|
+
// lane. Its result must not end replay suppression before the report.
|
|
3973
|
+
if (toolResultIds.length > 0) {
|
|
3974
|
+
continue;
|
|
3975
|
+
}
|
|
3976
|
+
// The next real user prompt is already represented by the client and
|
|
3977
|
+
// starts a new turn; do not let a missing audit result hide it or the
|
|
3978
|
+
// rest of the replay.
|
|
3979
|
+
if (replayMessageRole === "user") {
|
|
3980
|
+
replayingFileChangeAudit = false;
|
|
3981
|
+
}
|
|
3982
|
+
else {
|
|
3983
|
+
continue;
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3867
3986
|
for (const notification of toAcpNotifications(
|
|
3868
3987
|
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
3869
3988
|
content,
|
|
@@ -3957,6 +4076,26 @@ export class ClaudeAcpAgent {
|
|
|
3957
4076
|
message: "Session not found",
|
|
3958
4077
|
};
|
|
3959
4078
|
}
|
|
4079
|
+
const fileChangeAudit = session.activeTurn?.fileChangeAudit;
|
|
4080
|
+
if (isFileChangeAuditReportPhase(fileChangeAudit)) {
|
|
4081
|
+
// The hidden continuation is an audit-only lane: it may submit the
|
|
4082
|
+
// wrapper-owned report, but it must not run another command after the
|
|
4083
|
+
// user-visible answer has already completed.
|
|
4084
|
+
if (isFileChangeAuditTool(toolName) && fileChangeAudit?.phase === "collecting") {
|
|
4085
|
+
return { behavior: "allow", updatedInput: toolInput };
|
|
4086
|
+
}
|
|
4087
|
+
return {
|
|
4088
|
+
behavior: "deny",
|
|
4089
|
+
message: "Only the internal file-change report is allowed during the audit.",
|
|
4090
|
+
};
|
|
4091
|
+
}
|
|
4092
|
+
// The tool is intentionally unusable outside a negotiated audit turn.
|
|
4093
|
+
if (isFileChangeAuditTool(toolName)) {
|
|
4094
|
+
return {
|
|
4095
|
+
behavior: "deny",
|
|
4096
|
+
message: "No file-change report was requested for this turn.",
|
|
4097
|
+
};
|
|
4098
|
+
}
|
|
3960
4099
|
// When the tool call originates inside a subagent, attribute the eagerly
|
|
3961
4100
|
// emitted tool_call (and the permission request itself) to the Agent/Task
|
|
3962
4101
|
// tool call that spawned the subagent, mirroring the streamed subagent
|
|
@@ -4654,6 +4793,30 @@ export class ClaudeAcpAgent {
|
|
|
4654
4793
|
// below) so the TaskCreated/TaskCompleted hook callbacks can close over
|
|
4655
4794
|
// the same Map that the streaming message handler will read from.
|
|
4656
4795
|
const taskState = new Map();
|
|
4796
|
+
// Resolve every workspace root once. The hidden report tool uses this same
|
|
4797
|
+
// set for lexical path validation, and the SDK receives it below.
|
|
4798
|
+
const acpAdditionalDirectories = params.additionalDirectories ?? sessionMeta?.additionalRoots ?? [];
|
|
4799
|
+
const additionalDirectories = [
|
|
4800
|
+
...(userProvidedOptions?.additionalDirectories ?? []),
|
|
4801
|
+
...acpAdditionalDirectories,
|
|
4802
|
+
];
|
|
4803
|
+
const fileChangeAuditSupport = supportsAgentFileChangeReport(this.clientCapabilities)
|
|
4804
|
+
? createFileChangeAuditSupport({
|
|
4805
|
+
cwd: params.cwd,
|
|
4806
|
+
additionalDirectories,
|
|
4807
|
+
getActiveState: () => this.sessions[sessionId]?.activeTurn?.fileChangeAudit,
|
|
4808
|
+
publish: async (result) => {
|
|
4809
|
+
await this.client.sessionUpdate({
|
|
4810
|
+
sessionId,
|
|
4811
|
+
update: {
|
|
4812
|
+
sessionUpdate: "session_info_update",
|
|
4813
|
+
_meta: agentFileChangeReportMeta(result),
|
|
4814
|
+
},
|
|
4815
|
+
});
|
|
4816
|
+
},
|
|
4817
|
+
logError: (message) => this.logger.error(message),
|
|
4818
|
+
})
|
|
4819
|
+
: undefined;
|
|
4657
4820
|
// The exact env the query will be created with. Built (and the provider
|
|
4658
4821
|
// cache key derived from it, below) in one place so the key always
|
|
4659
4822
|
// describes the backend this query actually talks to: `providers/set`,
|
|
@@ -4661,13 +4824,36 @@ export class ClaudeAcpAgent {
|
|
|
4661
4824
|
// config concurrently, so re-resolving it after any of the awaits between
|
|
4662
4825
|
// here and the session registration could disagree with the env baked
|
|
4663
4826
|
// into the query.
|
|
4827
|
+
const resolvedProvider = this.resolveProviderConfig();
|
|
4828
|
+
const providerEnv = createEnvForProvider(resolvedProvider);
|
|
4829
|
+
const configuredSettings = userProvidedOptions?.settings ??
|
|
4830
|
+
(modelConfig
|
|
4831
|
+
? {
|
|
4832
|
+
...(modelConfig.modelOverrides && { modelOverrides: modelConfig.modelOverrides }),
|
|
4833
|
+
...(modelConfig.availableModels && { availableModels: modelConfig.availableModels }),
|
|
4834
|
+
}
|
|
4835
|
+
: undefined);
|
|
4836
|
+
// Claude Code applies env from settings.json after the subprocess env. Put
|
|
4837
|
+
// an active ACP route in the programmatic settings tier too so user/project
|
|
4838
|
+
// settings cannot silently restore a different ANTHROPIC_BASE_URL.
|
|
4839
|
+
let settings = configuredSettings;
|
|
4840
|
+
if (resolvedProvider) {
|
|
4841
|
+
const baseSettings = typeof configuredSettings === "string"
|
|
4842
|
+
? JSON.parse(await fs.readFile(path.resolve(params.cwd, configuredSettings), "utf8"))
|
|
4843
|
+
: configuredSettings;
|
|
4844
|
+
settings = {
|
|
4845
|
+
...baseSettings,
|
|
4846
|
+
apiKeyHelper: "",
|
|
4847
|
+
env: { ...baseSettings?.env, ...providerEnv },
|
|
4848
|
+
};
|
|
4849
|
+
}
|
|
4664
4850
|
const env = {
|
|
4665
4851
|
...process.env,
|
|
4666
4852
|
...userProvidedOptions?.env,
|
|
4667
4853
|
// Client-managed LLM routing: `providers/set` config wins, else the
|
|
4668
|
-
// legacy gateway auth request.
|
|
4669
|
-
//
|
|
4670
|
-
...
|
|
4854
|
+
// legacy gateway auth request. Routing is baked into the query at
|
|
4855
|
+
// creation; provider updates recreate loaded queries between turns.
|
|
4856
|
+
...providerEnv,
|
|
4671
4857
|
// Opt-in to session state events like when the agent is idle
|
|
4672
4858
|
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
|
4673
4859
|
};
|
|
@@ -4676,28 +4862,25 @@ export class ClaudeAcpAgent {
|
|
|
4676
4862
|
// SDK, so per-session `_meta` env routing and ambient process-env routing
|
|
4677
4863
|
// are distinguished exactly as the CLI will see them.
|
|
4678
4864
|
const providerCacheKey = providerCacheKeyFor(env);
|
|
4865
|
+
this.logger.log(`[session/query] sessionId=${sessionId} resume=${creationOpts?.resume ?? "none"} apiType=${resolvedProvider?.apiType ?? "native"} baseUrl=${resolvedProvider?.baseUrl ?? "native"}`);
|
|
4679
4866
|
const options = {
|
|
4680
4867
|
systemPrompt,
|
|
4681
4868
|
settingSources: ["user", "project", "local"],
|
|
4682
4869
|
...(thinking !== undefined && { thinking }),
|
|
4683
4870
|
...userProvidedOptions,
|
|
4684
|
-
|
|
4685
|
-
// configuration (e.g. Bedrock model ID overrides). When the caller
|
|
4686
|
-
// provides settings via _meta, we intentionally ignore the env var —
|
|
4687
|
-
// the caller is assumed to have full control over model configuration.
|
|
4688
|
-
...(!userProvidedOptions?.settings &&
|
|
4689
|
-
modelConfig && {
|
|
4690
|
-
settings: {
|
|
4691
|
-
...(modelConfig.modelOverrides && { modelOverrides: modelConfig.modelOverrides }),
|
|
4692
|
-
...(modelConfig.availableModels && { availableModels: modelConfig.availableModels }),
|
|
4693
|
-
},
|
|
4694
|
-
}),
|
|
4871
|
+
...(settings && { settings }),
|
|
4695
4872
|
env,
|
|
4696
4873
|
// Override certain fields that must be controlled by ACP
|
|
4697
4874
|
cwd: params.cwd,
|
|
4698
4875
|
includePartialMessages: true,
|
|
4699
4876
|
forwardSubagentText,
|
|
4700
|
-
mcpServers: {
|
|
4877
|
+
mcpServers: {
|
|
4878
|
+
...(userProvidedOptions?.mcpServers || {}),
|
|
4879
|
+
...mcpServers,
|
|
4880
|
+
...(fileChangeAuditSupport
|
|
4881
|
+
? { [FILE_CHANGE_AUDIT_SERVER_NAME]: fileChangeAuditSupport.mcpServer }
|
|
4882
|
+
: {}),
|
|
4883
|
+
},
|
|
4701
4884
|
// If we want bypassPermissions to be an option, we have to allow it here.
|
|
4702
4885
|
// But it doesn't work in root mode, so we only activate it if it will work.
|
|
4703
4886
|
allowDangerouslySkipPermissions: ALLOW_BYPASS,
|
|
@@ -4731,6 +4914,14 @@ export class ClaudeAcpAgent {
|
|
|
4731
4914
|
tools,
|
|
4732
4915
|
hooks: {
|
|
4733
4916
|
...userProvidedOptions?.hooks,
|
|
4917
|
+
...(fileChangeAuditSupport
|
|
4918
|
+
? {
|
|
4919
|
+
PreToolUse: [
|
|
4920
|
+
...(userProvidedOptions?.hooks?.PreToolUse || []),
|
|
4921
|
+
{ hooks: [fileChangeAuditSupport.preToolUseHook] },
|
|
4922
|
+
],
|
|
4923
|
+
}
|
|
4924
|
+
: {}),
|
|
4734
4925
|
PostToolUse: [
|
|
4735
4926
|
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
|
4736
4927
|
{
|
|
@@ -4750,6 +4941,14 @@ export class ClaudeAcpAgent {
|
|
|
4750
4941
|
],
|
|
4751
4942
|
},
|
|
4752
4943
|
],
|
|
4944
|
+
...(fileChangeAuditSupport
|
|
4945
|
+
? {
|
|
4946
|
+
Stop: [
|
|
4947
|
+
...(userProvidedOptions?.hooks?.Stop || []),
|
|
4948
|
+
{ hooks: [fileChangeAuditSupport.stopHook] },
|
|
4949
|
+
],
|
|
4950
|
+
}
|
|
4951
|
+
: {}),
|
|
4753
4952
|
TaskCreated: [
|
|
4754
4953
|
...(userProvidedOptions?.hooks?.TaskCreated || []),
|
|
4755
4954
|
{
|
|
@@ -4780,11 +4979,7 @@ export class ClaudeAcpAgent {
|
|
|
4780
4979
|
// legacy `_meta.additionalRoots` extension for clients that haven't been
|
|
4781
4980
|
// updated yet. Either source is merged with directories supplied via
|
|
4782
4981
|
// `_meta.claudeCode.options.additionalDirectories` (SDK pass-through).
|
|
4783
|
-
|
|
4784
|
-
options.additionalDirectories = [
|
|
4785
|
-
...(userProvidedOptions?.additionalDirectories ?? []),
|
|
4786
|
-
...acpAdditionalDirectories,
|
|
4787
|
-
];
|
|
4982
|
+
options.additionalDirectories = additionalDirectories;
|
|
4788
4983
|
if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
|
|
4789
4984
|
// Set our own session id if not resuming an existing session.
|
|
4790
4985
|
options.sessionId = sessionId;
|
|
@@ -4968,6 +5163,7 @@ export class ClaudeAcpAgent {
|
|
|
4968
5163
|
cancelled: false,
|
|
4969
5164
|
cwd: params.cwd,
|
|
4970
5165
|
sessionFingerprint: computeSessionFingerprint(params),
|
|
5166
|
+
creationParams: params,
|
|
4971
5167
|
settingsManager,
|
|
4972
5168
|
accumulatedUsage: {
|
|
4973
5169
|
inputTokens: 0,
|
|
@@ -4997,6 +5193,8 @@ export class ClaudeAcpAgent {
|
|
|
4997
5193
|
owedTrailingIdles: 0,
|
|
4998
5194
|
messageIdToUuid: new Map(),
|
|
4999
5195
|
sessionFailureState: createSessionFailureState(),
|
|
5196
|
+
fileChangeReportRequestIds: new Set(),
|
|
5197
|
+
fileChangeAuditSupport,
|
|
5000
5198
|
};
|
|
5001
5199
|
return {
|
|
5002
5200
|
sessionId,
|
|
@@ -5004,6 +5202,41 @@ export class ClaudeAcpAgent {
|
|
|
5004
5202
|
configOptions,
|
|
5005
5203
|
};
|
|
5006
5204
|
}
|
|
5205
|
+
/**
|
|
5206
|
+
* Provider routing is baked into the environment of each SDK Query. Wait for
|
|
5207
|
+
* all submitted turns to settle, close every query, then resume each Claude
|
|
5208
|
+
* session with the same ID so subsequent turns inherit the new environment.
|
|
5209
|
+
*/
|
|
5210
|
+
async enqueueProviderUpdate(config) {
|
|
5211
|
+
const previous = this.providerUpdate?.catch(() => undefined) ?? Promise.resolve();
|
|
5212
|
+
const update = previous.then(async () => {
|
|
5213
|
+
const sessions = Object.entries(this.sessions);
|
|
5214
|
+
const activeTurns = sessions.flatMap(([, session]) => (session.turnQueue ?? []).flatMap((turn) => (turn.completion ? [turn.completion] : [])));
|
|
5215
|
+
if (activeTurns.length > 0) {
|
|
5216
|
+
this.logger.log(`Waiting for ${activeTurns.length} active Claude turn(s) before provider update`);
|
|
5217
|
+
await Promise.all(activeTurns);
|
|
5218
|
+
}
|
|
5219
|
+
this.providerConfig = config;
|
|
5220
|
+
for (const [sessionId, session] of sessions) {
|
|
5221
|
+
if (this.sessions[sessionId] !== session || !session.creationParams) {
|
|
5222
|
+
continue;
|
|
5223
|
+
}
|
|
5224
|
+
this.logger.log(`Recreating Claude session ${sessionId} for provider update`);
|
|
5225
|
+
this.closeQueryStream(session);
|
|
5226
|
+
delete this.sessions[sessionId];
|
|
5227
|
+
await this.createSession(session.creationParams, { resume: sessionId });
|
|
5228
|
+
}
|
|
5229
|
+
});
|
|
5230
|
+
this.providerUpdate = update;
|
|
5231
|
+
try {
|
|
5232
|
+
await update;
|
|
5233
|
+
}
|
|
5234
|
+
finally {
|
|
5235
|
+
if (this.providerUpdate === update) {
|
|
5236
|
+
this.providerUpdate = null;
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5007
5240
|
}
|
|
5008
5241
|
function shouldEmitRawMessage(config, message) {
|
|
5009
5242
|
if (config === true)
|
|
@@ -5091,13 +5324,27 @@ function createEnvForProvider(config) {
|
|
|
5091
5324
|
if (!config) {
|
|
5092
5325
|
return {};
|
|
5093
5326
|
}
|
|
5327
|
+
const resetRouting = {
|
|
5328
|
+
ANTHROPIC_BASE_URL: "",
|
|
5329
|
+
ANTHROPIC_BEDROCK_BASE_URL: "",
|
|
5330
|
+
ANTHROPIC_VERTEX_BASE_URL: "",
|
|
5331
|
+
CLAUDE_CODE_USE_BEDROCK: "0",
|
|
5332
|
+
CLAUDE_CODE_USE_VERTEX: "0",
|
|
5333
|
+
ANTHROPIC_VERTEX_PROJECT_ID: "",
|
|
5334
|
+
CLOUD_ML_REGION: "",
|
|
5335
|
+
AWS_REGION: "",
|
|
5336
|
+
ANTHROPIC_API_KEY: "",
|
|
5337
|
+
ANTHROPIC_AUTH_TOKEN: "",
|
|
5338
|
+
CLAUDE_CODE_OAUTH_TOKEN: "",
|
|
5339
|
+
};
|
|
5094
5340
|
const customHeaders = Object.entries(config.headers)
|
|
5095
5341
|
.map(([key, value]) => `${key}: ${value}`)
|
|
5096
5342
|
.join("\n");
|
|
5097
5343
|
if (config.apiType === "bedrock") {
|
|
5098
5344
|
return {
|
|
5345
|
+
...resetRouting,
|
|
5099
5346
|
CLAUDE_CODE_USE_BEDROCK: "1",
|
|
5100
|
-
AWS_BEARER_TOKEN_BEDROCK: "
|
|
5347
|
+
AWS_BEARER_TOKEN_BEDROCK: "acp-proxy", // Bypass local AWS credential checks
|
|
5101
5348
|
ANTHROPIC_BEDROCK_BASE_URL: config.baseUrl,
|
|
5102
5349
|
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
|
5103
5350
|
};
|
|
@@ -5106,6 +5353,7 @@ function createEnvForProvider(config) {
|
|
|
5106
5353
|
// `config.vertex` is guaranteed present for vertex by `unstable_setProvider`
|
|
5107
5354
|
// validation; fall back to empty strings defensively.
|
|
5108
5355
|
return {
|
|
5356
|
+
...resetRouting,
|
|
5109
5357
|
CLAUDE_CODE_USE_VERTEX: "1",
|
|
5110
5358
|
ANTHROPIC_VERTEX_BASE_URL: config.baseUrl,
|
|
5111
5359
|
ANTHROPIC_VERTEX_PROJECT_ID: config.vertex?.projectId ?? "",
|
|
@@ -5114,9 +5362,10 @@ function createEnvForProvider(config) {
|
|
|
5114
5362
|
};
|
|
5115
5363
|
}
|
|
5116
5364
|
return {
|
|
5365
|
+
...resetRouting,
|
|
5117
5366
|
ANTHROPIC_BASE_URL: config.baseUrl,
|
|
5118
5367
|
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
|
5119
|
-
ANTHROPIC_AUTH_TOKEN: "
|
|
5368
|
+
ANTHROPIC_AUTH_TOKEN: "acp-proxy", // Bypass local Claude login checks
|
|
5120
5369
|
};
|
|
5121
5370
|
}
|
|
5122
5371
|
/**
|
|
@@ -5983,7 +6232,7 @@ function isTaskTool(toolName) {
|
|
|
5983
6232
|
* permission-surfaced tool_call for them (see `ensureToolCallEmitted`) must be
|
|
5984
6233
|
* resolved explicitly at tool_result time. */
|
|
5985
6234
|
function shouldEmitToolCall(toolName) {
|
|
5986
|
-
return toolName !== "TodoWrite" && !isTaskTool(toolName);
|
|
6235
|
+
return toolName !== "TodoWrite" && !isTaskTool(toolName) && !isFileChangeAuditTool(toolName);
|
|
5987
6236
|
}
|
|
5988
6237
|
/** Build the Claude Code-specific metadata for a tool call. Bash descriptions
|
|
5989
6238
|
* are kept out of ACP's standard `title`, which clients may use as the shell
|
|
@@ -6136,7 +6385,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
6136
6385
|
const registerHooks = options?.registerHooks !== false;
|
|
6137
6386
|
const supportsTerminalOutput = options?.clientCapabilities?._meta?.["terminal_output"] === true;
|
|
6138
6387
|
if (typeof content === "string") {
|
|
6139
|
-
if (content.length === 0) {
|
|
6388
|
+
if (content.length === 0 || containsFileChangeAuditMarker(content)) {
|
|
6140
6389
|
return [];
|
|
6141
6390
|
}
|
|
6142
6391
|
const update = {
|
|
@@ -6170,6 +6419,13 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
6170
6419
|
// Unlike `tool_use_result`, entries carry their own tool_use_id, so batched
|
|
6171
6420
|
// messages need no single-block guard.
|
|
6172
6421
|
const toolResultMeta = parseToolResultMeta(options?.toolResultMeta);
|
|
6422
|
+
// A report-phase assistant message may contain a short text preface and the
|
|
6423
|
+
// internal tool call in the same content array. Hide the whole message, not
|
|
6424
|
+
// only the tool block, so it stays absent on session replay as well as live.
|
|
6425
|
+
const containsFileChangeAuditToolUse = content.some((chunk) => (chunk.type === "tool_use" ||
|
|
6426
|
+
chunk.type === "server_tool_use" ||
|
|
6427
|
+
chunk.type === "mcp_tool_use") &&
|
|
6428
|
+
isFileChangeAuditTool(chunk.name));
|
|
6173
6429
|
const output = [];
|
|
6174
6430
|
// Only handle the first chunk for streaming; extend as needed for batching
|
|
6175
6431
|
for (const chunk of content) {
|
|
@@ -6177,7 +6433,9 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
6177
6433
|
switch (chunk.type) {
|
|
6178
6434
|
case "text":
|
|
6179
6435
|
case "text_delta": {
|
|
6180
|
-
if (chunk.text
|
|
6436
|
+
if (chunk.text &&
|
|
6437
|
+
!containsFileChangeAuditToolUse &&
|
|
6438
|
+
!containsFileChangeAuditMarker(chunk.text)) {
|
|
6181
6439
|
update = {
|
|
6182
6440
|
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
6183
6441
|
content: {
|
|
@@ -6189,21 +6447,22 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
6189
6447
|
break;
|
|
6190
6448
|
}
|
|
6191
6449
|
case "image":
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6450
|
+
if (!containsFileChangeAuditToolUse)
|
|
6451
|
+
update = {
|
|
6452
|
+
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
6453
|
+
content: {
|
|
6454
|
+
type: "image",
|
|
6455
|
+
data: chunk.source.type === "base64" ? chunk.source.data : "",
|
|
6456
|
+
mimeType: chunk.source.type === "base64" ? chunk.source.media_type : "",
|
|
6457
|
+
uri: chunk.source.type === "url" ? chunk.source.url : undefined,
|
|
6458
|
+
},
|
|
6459
|
+
};
|
|
6201
6460
|
break;
|
|
6202
6461
|
case "thinking":
|
|
6203
6462
|
case "thinking_delta": {
|
|
6204
6463
|
// Recent models default `thinking.display` to "omitted", which streams
|
|
6205
6464
|
// signature-only thinking blocks whose text is empty.
|
|
6206
|
-
if (chunk.thinking) {
|
|
6465
|
+
if (chunk.thinking && !containsFileChangeAuditToolUse) {
|
|
6207
6466
|
update = {
|
|
6208
6467
|
sessionUpdate: "agent_thought_chunk",
|
|
6209
6468
|
content: {
|
|
@@ -6219,7 +6478,11 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
6219
6478
|
case "mcp_tool_use": {
|
|
6220
6479
|
const alreadyCached = chunk.id in toolUseCache;
|
|
6221
6480
|
toolUseCache[chunk.id] = chunk;
|
|
6222
|
-
if (chunk.name
|
|
6481
|
+
if (isFileChangeAuditTool(chunk.name)) {
|
|
6482
|
+
// Wrapper-owned audit protocol: never surface or register generic
|
|
6483
|
+
// PostToolUse callbacks for the internal tool.
|
|
6484
|
+
}
|
|
6485
|
+
else if (chunk.name === "TodoWrite") {
|
|
6223
6486
|
// @ts-expect-error - sometimes input is empty object or undefined
|
|
6224
6487
|
if (Array.isArray(chunk.input?.todos)) {
|
|
6225
6488
|
update = {
|
|
@@ -6343,6 +6606,10 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
6343
6606
|
logger.error(`[claude-agent-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`);
|
|
6344
6607
|
break;
|
|
6345
6608
|
}
|
|
6609
|
+
if (isFileChangeAuditTool(toolUse.name)) {
|
|
6610
|
+
delete toolUseCache[chunk.tool_use_id];
|
|
6611
|
+
break;
|
|
6612
|
+
}
|
|
6346
6613
|
// A permission request may have surfaced a plan-rendered (TodoWrite) or
|
|
6347
6614
|
// suppressed (Task*) tool as a real tool_call so the request referenced
|
|
6348
6615
|
// a tool call the client knows about (see `ensureToolCallEmitted`,
|
|
@@ -6624,7 +6891,7 @@ export async function runPromptWithCancellation(agent, params, signal) {
|
|
|
6624
6891
|
signal.removeEventListener("abort", onAbort);
|
|
6625
6892
|
}
|
|
6626
6893
|
}
|
|
6627
|
-
export function runAcp() {
|
|
6894
|
+
export function runAcp(logger) {
|
|
6628
6895
|
const input = nodeToWebWritable(process.stdout);
|
|
6629
6896
|
const output = nodeToWebReadable(process.stdin);
|
|
6630
6897
|
const stream = ndJsonStream(input, output);
|
|
@@ -6657,7 +6924,7 @@ export function runAcp() {
|
|
|
6657
6924
|
.onRequest(STEER_METHOD, { parse: parseSteerRequest }, (ctx) => agent.steer(ctx.params))
|
|
6658
6925
|
.onRequest(GOAL_CONTROL_METHOD, { parse: parseGoalRequest }, (ctx) => agent.goal(ctx.params))
|
|
6659
6926
|
.connect(stream);
|
|
6660
|
-
agent = new ClaudeAcpAgent(new ClientConnection(connection.client));
|
|
6927
|
+
agent = new ClaudeAcpAgent(new ClientConnection(connection.client), logger);
|
|
6661
6928
|
return { connection, agent };
|
|
6662
6929
|
}
|
|
6663
6930
|
function commonPrefixLength(a, b) {
|
|
@@ -6752,6 +7019,7 @@ const PROVIDER_ROUTING_ENV_VARS = [
|
|
|
6752
7019
|
"ANTHROPIC_CUSTOM_HEADERS",
|
|
6753
7020
|
"ANTHROPIC_API_KEY",
|
|
6754
7021
|
"ANTHROPIC_AUTH_TOKEN",
|
|
7022
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
6755
7023
|
];
|
|
6756
7024
|
/** Stable identifier for the LLM backend a session's query is created against,
|
|
6757
7025
|
* used to scope {@link contextWindowCache} per backend. Positional `\0`-join
|