@agentclientprotocol/claude-agent-acp 0.59.0 → 0.61.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 +125 -5
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +594 -98
- package/dist/tools.d.ts +1 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +34 -9
- package/package.json +7 -7
package/dist/acp-agent.js
CHANGED
|
@@ -48,6 +48,38 @@ const DEFAULT_FORCE_CANCEL_GRACE_MS = 30_000;
|
|
|
48
48
|
* completed/stalled without the host turn resolving (issue #825). */
|
|
49
49
|
const TURN_NO_RESULT_MESSAGE = "The turn ended without a result: the agent went idle while this prompt was still in flight " +
|
|
50
50
|
"(e.g. the model stream dropped mid-turn). Any partial output may be incomplete; please retry.";
|
|
51
|
+
/** Custom (extension) request method a client uses to steer the turn that is
|
|
52
|
+
* currently running: the message is injected into the in-flight turn rather
|
|
53
|
+
* than queued as a separate `session/prompt`. Named `_session/steering` per the
|
|
54
|
+
* agreed ACP steering wire protocol; advertised to clients via the top-level
|
|
55
|
+
* `InitializeResponse._meta.steering.supported`. */
|
|
56
|
+
const STEER_METHOD = "_session/steering";
|
|
57
|
+
/** How urgently the SDK delivers a steered message relative to the running
|
|
58
|
+
* turn — an internal Claude implementation detail, not part of the wire
|
|
59
|
+
* contract. `now` pre-empts the current generation and handles the message
|
|
60
|
+
* immediately (interrupting a single-shot response, or slotting in between a
|
|
61
|
+
* multi-step turn's tool calls). Maps to `SDKUserMessage.priority`; injected
|
|
62
|
+
* steering always uses `now` so the running turn adapts as soon as possible. */
|
|
63
|
+
const STEER_PRIORITY = "now";
|
|
64
|
+
/** Validate raw JSON-RPC params into a {@link SteerRequest}. Kept minimal — the
|
|
65
|
+
* content blocks are handed to `promptToClaude`, which tolerates unknown block
|
|
66
|
+
* types — but `sessionId` and a non-empty `prompt` array are required. */
|
|
67
|
+
function parseSteerRequest(params) {
|
|
68
|
+
if (!params || typeof params !== "object") {
|
|
69
|
+
throw RequestError.invalidParams(undefined, "steer params must be an object");
|
|
70
|
+
}
|
|
71
|
+
const { sessionId, prompt } = params;
|
|
72
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
73
|
+
throw RequestError.invalidParams(undefined, "steer params require a non-empty sessionId");
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(prompt) || prompt.length === 0) {
|
|
76
|
+
throw RequestError.invalidParams(undefined, "steer params require a non-empty prompt array");
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
sessionId,
|
|
80
|
+
prompt: prompt,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
51
83
|
/** Result-message origin kinds that mark an AUTONOMOUS cycle — work the
|
|
52
84
|
* model did on its own (a task-notification followup, a peer/coordinator/
|
|
53
85
|
* observer message it handled) rather than the user's prompt. Absent,
|
|
@@ -97,6 +129,17 @@ function computeSessionFingerprint(params) {
|
|
|
97
129
|
const servers = [...(params.mcpServers ?? [])].sort((a, b) => a.name.localeCompare(b.name));
|
|
98
130
|
return JSON.stringify({ cwd: params.cwd, mcpServers: servers });
|
|
99
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* The single provider ID this agent exposes via `providers/*`. Claude Code has
|
|
134
|
+
* one LLM backend selected by protocol (anthropic / bedrock / vertex), so there
|
|
135
|
+
* is exactly one configurable provider.
|
|
136
|
+
*/
|
|
137
|
+
const PROVIDER_ID = "main";
|
|
138
|
+
/**
|
|
139
|
+
* Protocols the `main` provider can be configured with. These mirror the
|
|
140
|
+
* env-var mappings understood by {@link createEnvForProvider}.
|
|
141
|
+
*/
|
|
142
|
+
const SUPPORTED_PROTOCOLS = ["anthropic", "bedrock", "vertex"];
|
|
100
143
|
/**
|
|
101
144
|
* Advance the lexer state across the fragment appended since the last delta:
|
|
102
145
|
* just enough JSON awareness (string/escape, nesting depth) to spot commas
|
|
@@ -433,6 +476,10 @@ export class ClaudeAcpAgent {
|
|
|
433
476
|
clientCapabilities;
|
|
434
477
|
logger;
|
|
435
478
|
gatewayAuthRequest;
|
|
479
|
+
/** Client-managed LLM routing set via `providers/set`. Process-scoped and
|
|
480
|
+
* never persisted to disk (see the Configurable LLM Providers RFD). When
|
|
481
|
+
* set, it takes precedence over {@link gatewayAuthRequest}. */
|
|
482
|
+
providerConfig;
|
|
436
483
|
/** Grace period before a `session/cancel` forces a wedged prompt loop to
|
|
437
484
|
* return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
|
|
438
485
|
* tests can shrink it. */
|
|
@@ -558,6 +605,10 @@ export class ClaudeAcpAgent {
|
|
|
558
605
|
auth: {
|
|
559
606
|
logout: {},
|
|
560
607
|
},
|
|
608
|
+
// Client-managed LLM routing via `providers/list`, `providers/set`, and
|
|
609
|
+
// `providers/disable`. Advertised unconditionally; there is no client
|
|
610
|
+
// capability prerequisite for the provider methods.
|
|
611
|
+
providers: {},
|
|
561
612
|
loadSession: true,
|
|
562
613
|
sessionCapabilities: {
|
|
563
614
|
additionalDirectories: {},
|
|
@@ -577,6 +628,15 @@ export class ClaudeAcpAgent {
|
|
|
577
628
|
...terminalAuthMethods,
|
|
578
629
|
...(supportsGatewayAuth ? [gatewayAuthMethod, gatewayBedrockAuthMethod] : []),
|
|
579
630
|
],
|
|
631
|
+
// Top-level `_meta` (sibling of `agentCapabilities`), per the ACP steering
|
|
632
|
+
// wire protocol: advertises the `_session/steering` extension request so
|
|
633
|
+
// clients know they may inject a follow-up into the running turn (see
|
|
634
|
+
// STEER_METHOD) instead of queuing it as a separate `session/prompt`.
|
|
635
|
+
_meta: {
|
|
636
|
+
steering: {
|
|
637
|
+
supported: true,
|
|
638
|
+
},
|
|
639
|
+
},
|
|
580
640
|
};
|
|
581
641
|
}
|
|
582
642
|
async newSession(params) {
|
|
@@ -681,11 +741,100 @@ export class ClaudeAcpAgent {
|
|
|
681
741
|
}
|
|
682
742
|
throw new Error("Method not implemented.");
|
|
683
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* `providers/list` — returns the single client-configurable custom gateway
|
|
746
|
+
* provider (`main`). `current` carries only non-secret routing (never headers,
|
|
747
|
+
* which may hold secrets); only `apiType`/`baseUrl` are surfaced for UI
|
|
748
|
+
* display, and is `null` when the provider is not configured/disabled. The
|
|
749
|
+
* provider is optional (`required: false`): while disabled/unconfigured the
|
|
750
|
+
* agent falls back to its own default routing (normal Claude login).
|
|
751
|
+
*/
|
|
752
|
+
async unstable_listProviders(_params) {
|
|
753
|
+
const config = this.resolveProviderConfig();
|
|
754
|
+
const provider = {
|
|
755
|
+
providerId: PROVIDER_ID,
|
|
756
|
+
supported: SUPPORTED_PROTOCOLS,
|
|
757
|
+
required: false,
|
|
758
|
+
current: config ? { apiType: config.apiType, baseUrl: config.baseUrl } : null,
|
|
759
|
+
};
|
|
760
|
+
return { providers: [provider] };
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* `providers/set` — replace the full configuration for the `main` provider.
|
|
764
|
+
* Rejects unknown IDs, unsupported protocols, and empty/invalid base URLs with
|
|
765
|
+
* `invalid_params`. Config is process-scoped and applies to sessions created or
|
|
766
|
+
* loaded after this call.
|
|
767
|
+
*/
|
|
768
|
+
async unstable_setProvider(params) {
|
|
769
|
+
if (params.providerId !== PROVIDER_ID) {
|
|
770
|
+
throw RequestError.invalidParams({ providerId: params.providerId }, `Unknown provider ID "${params.providerId}"; expected "${PROVIDER_ID}".`);
|
|
771
|
+
}
|
|
772
|
+
if (!SUPPORTED_PROTOCOLS.includes(params.apiType)) {
|
|
773
|
+
throw RequestError.invalidParams({ apiType: params.apiType, supported: SUPPORTED_PROTOCOLS }, `Unsupported apiType "${params.apiType}" for provider "${PROVIDER_ID}".`);
|
|
774
|
+
}
|
|
775
|
+
if (!isValidBaseUrl(params.baseUrl)) {
|
|
776
|
+
throw RequestError.invalidParams({ baseUrl: params.baseUrl }, "baseUrl must be a non-empty absolute http(s) URL.");
|
|
777
|
+
}
|
|
778
|
+
const config = {
|
|
779
|
+
apiType: params.apiType,
|
|
780
|
+
baseUrl: params.baseUrl,
|
|
781
|
+
headers: params.headers ?? {},
|
|
782
|
+
};
|
|
783
|
+
// Vertex requires project + region, which the standard payload cannot
|
|
784
|
+
// carry, so they arrive via `_meta.claudeCode.vertex`.
|
|
785
|
+
if (params.apiType === "vertex") {
|
|
786
|
+
const vertex = params._meta?.claudeCode?.vertex;
|
|
787
|
+
if (!vertex ||
|
|
788
|
+
typeof vertex.projectId !== "string" ||
|
|
789
|
+
vertex.projectId.trim() === "" ||
|
|
790
|
+
typeof vertex.region !== "string" ||
|
|
791
|
+
vertex.region.trim() === "") {
|
|
792
|
+
throw RequestError.invalidParams(undefined, "vertex apiType requires non-empty `_meta.claudeCode.vertex.projectId` and `_meta.claudeCode.vertex.region`.");
|
|
793
|
+
}
|
|
794
|
+
config.vertex = { projectId: vertex.projectId, region: vertex.region };
|
|
795
|
+
}
|
|
796
|
+
this.providerConfig = config;
|
|
797
|
+
return {};
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* `providers/disable` — disabling the `main` provider clears any client-managed
|
|
801
|
+
* routing (both a `providers/set` config and the legacy gateway auth request),
|
|
802
|
+
* so the agent reverts to its own default routing and `providers/list` reports
|
|
803
|
+
* `current: null`. Disabling any other (unknown) ID is treated as a successful
|
|
804
|
+
* no-op per the RFD's idempotency rule.
|
|
805
|
+
*/
|
|
806
|
+
async unstable_disableProvider(params) {
|
|
807
|
+
if (params.providerId === PROVIDER_ID) {
|
|
808
|
+
this.providerConfig = undefined;
|
|
809
|
+
this.gatewayAuthRequest = undefined;
|
|
810
|
+
}
|
|
811
|
+
// Unknown provider: idempotent success.
|
|
812
|
+
return {};
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Resolve the effective client-managed routing config. `providers/set` takes
|
|
816
|
+
* precedence; otherwise fall back to the legacy gateway auth request. Returns
|
|
817
|
+
* `null` when neither is configured.
|
|
818
|
+
*/
|
|
819
|
+
resolveProviderConfig() {
|
|
820
|
+
if (this.providerConfig) {
|
|
821
|
+
return this.providerConfig;
|
|
822
|
+
}
|
|
823
|
+
return gatewayRequestToProviderConfig(this.gatewayAuthRequest);
|
|
824
|
+
}
|
|
684
825
|
async logout(_params) {
|
|
685
|
-
// Clear in-memory gateway credentials supplied via `authenticate
|
|
686
|
-
//
|
|
687
|
-
//
|
|
826
|
+
// Clear in-memory gateway credentials supplied via `authenticate` and any
|
|
827
|
+
// provider routing set via `providers/set`. Neither touches the on-disk
|
|
828
|
+
// credential store, so dropping these references is the whole logout for
|
|
829
|
+
// those paths.
|
|
688
830
|
this.gatewayAuthRequest = undefined;
|
|
831
|
+
this.providerConfig = undefined;
|
|
832
|
+
// Learned context windows are per-account state too: 1M-context
|
|
833
|
+
// entitlement is gated per org/tier, and an OAuth re-login is invisible to
|
|
834
|
+
// the env-derived provider cache key, so windows learned under the old
|
|
835
|
+
// login must not seed sessions under the next. Worst case of clearing is
|
|
836
|
+
// re-learning on each model's next turn.
|
|
837
|
+
contextWindowCache.clear();
|
|
689
838
|
// For the Claude/Console login methods the credentials live in the native
|
|
690
839
|
// CLI's store (keychain or config dir), which only the binary can clear.
|
|
691
840
|
// `claude auth logout` is non-interactive and idempotent.
|
|
@@ -739,6 +888,67 @@ export class ClaudeAcpAgent {
|
|
|
739
888
|
this.ensureConsumer(session, params.sessionId);
|
|
740
889
|
return response;
|
|
741
890
|
}
|
|
891
|
+
/** Steer the session per the ACP steering wire protocol: apply a follow-up
|
|
892
|
+
* message to the turn that is currently running, or — if that turn already
|
|
893
|
+
* finished — start a fresh turn with it. Never drops the message and never
|
|
894
|
+
* returns a JSON-RPC error for the "arrived too late" race; both paths are
|
|
895
|
+
* success outcomes (see {@link SteerOutcome}).
|
|
896
|
+
*
|
|
897
|
+
* When a turn is in flight this injects (returns `injected`): unlike
|
|
898
|
+
* `prompt()`, it does NOT create a Turn or enqueue on `turnQueue`; it pushes
|
|
899
|
+
* an `SDKUserMessage` onto the same streaming input, which the SDK routes
|
|
900
|
+
* into the in-flight turn. The injected message's echo carries a uuid that
|
|
901
|
+
* matches no queued turn, so the consumer drops it as an unrelated replay
|
|
902
|
+
* without promoting/settling anything. It is delivered at {@link
|
|
903
|
+
* STEER_PRIORITY} (`now`) so it pre-empts the current generation (interrupting
|
|
904
|
+
* a single-shot response, or slotting in between a multi-step turn's tool
|
|
905
|
+
* calls). The steered message's own output streams via `session/update`, not
|
|
906
|
+
* this response.
|
|
907
|
+
*
|
|
908
|
+
* When the session is idle (no unsettled turn — the turn we meant to steer
|
|
909
|
+
* raced ahead and finished), this starts a normal new turn with the message
|
|
910
|
+
* and returns `startedNewTurn`. That turn is fire-and-forget from the steer
|
|
911
|
+
* request's view: its `PromptResponse` and output flow through the usual
|
|
912
|
+
* `prompt()`/`session/update` path, so we return the outcome immediately
|
|
913
|
+
* rather than awaiting turn completion. */
|
|
914
|
+
async steer(params) {
|
|
915
|
+
const sessionId = params.sessionId;
|
|
916
|
+
const prompt = params.prompt;
|
|
917
|
+
const session = this.sessions[sessionId];
|
|
918
|
+
if (!session) {
|
|
919
|
+
throw new Error("Session not found");
|
|
920
|
+
}
|
|
921
|
+
if (session.queryClosed) {
|
|
922
|
+
throw RequestError.internalError(undefined, SESSION_ENDED_MESSAGE);
|
|
923
|
+
}
|
|
924
|
+
// "A turn is running" = the queue holds an unsettled turn. This covers both
|
|
925
|
+
// the activated turn and one just submitted but not yet echoed/activated,
|
|
926
|
+
// which is exactly the window in which steering is meaningful.
|
|
927
|
+
const turnInFlight = (session.turnQueue ?? []).some((t) => !t.settled);
|
|
928
|
+
const promptRequest = {
|
|
929
|
+
sessionId: sessionId,
|
|
930
|
+
prompt: prompt,
|
|
931
|
+
};
|
|
932
|
+
if (!turnInFlight) {
|
|
933
|
+
// Race: the turn we meant to steer already finished. Per the protocol the
|
|
934
|
+
// message must not be dropped nor surfaced as an error — start a fresh
|
|
935
|
+
// turn with it. Don't await: the new turn streams via session/update and
|
|
936
|
+
// its PromptResponse is consumed by the normal prompt() path; we only owe
|
|
937
|
+
// the client the outcome. `.catch` keeps the detached promise from
|
|
938
|
+
// becoming an unhandled rejection.
|
|
939
|
+
this.prompt(promptRequest).catch((error) => {
|
|
940
|
+
this.logger.error(`Session ${sessionId}: steered new turn failed: ${error}`);
|
|
941
|
+
});
|
|
942
|
+
return { outcome: "startedNewTurn" };
|
|
943
|
+
}
|
|
944
|
+
const userMessage = promptToClaude(promptRequest);
|
|
945
|
+
userMessage.uuid = randomUUID();
|
|
946
|
+
// Deliver into the running turn rather than queuing behind it as a fresh
|
|
947
|
+
// prompt would.
|
|
948
|
+
userMessage.priority = STEER_PRIORITY;
|
|
949
|
+
session.input.push(userMessage);
|
|
950
|
+
return { outcome: "injected" };
|
|
951
|
+
}
|
|
742
952
|
/** Lazily start the per-session consumer that drains the SDK query stream for
|
|
743
953
|
* the session's whole life. Idempotent: only the first `prompt()` starts it. */
|
|
744
954
|
ensureConsumer(session, sessionId) {
|
|
@@ -1592,9 +1802,9 @@ export class ClaudeAcpAgent {
|
|
|
1592
1802
|
// Push the full slash-command list after a mid-session change
|
|
1593
1803
|
// (e.g. skills discovered dynamically as the agent works in a
|
|
1594
1804
|
// subdirectory). The client should REPLACE its cached command
|
|
1595
|
-
// list with this payload
|
|
1596
|
-
//
|
|
1597
|
-
//
|
|
1805
|
+
// list with this payload. Forward message.commands directly —
|
|
1806
|
+
// it's authoritative, and re-querying supportedCommands()
|
|
1807
|
+
// would just return the same list with an extra round-trip.
|
|
1598
1808
|
await sendUpdate({
|
|
1599
1809
|
sessionId: message.session_id,
|
|
1600
1810
|
update: {
|
|
@@ -1943,12 +2153,35 @@ export class ClaudeAcpAgent {
|
|
|
1943
2153
|
const matchingModelUsage = lastAssistantModel
|
|
1944
2154
|
? getMatchingModelUsage(message.modelUsage, lastAssistantModel)
|
|
1945
2155
|
: null;
|
|
1946
|
-
// Only overwrite when we have an authoritative value
|
|
1947
|
-
// (e.g. a turn with no top-level assistant message)
|
|
1948
|
-
//
|
|
1949
|
-
//
|
|
1950
|
-
|
|
1951
|
-
|
|
2156
|
+
// Only overwrite when we have an authoritative, sane value. A miss
|
|
2157
|
+
// (e.g. a turn with no top-level assistant message), or a
|
|
2158
|
+
// nonsensical non-positive/NaN window (observed from third-party
|
|
2159
|
+
// backends), would otherwise discard the window learned on a prior
|
|
2160
|
+
// turn and leave the next prompt's mid-stream updates reporting a
|
|
2161
|
+
// wrong size. `cacheContextWindow` applies the same `> 0` guard, so
|
|
2162
|
+
// a bad value never reaches the cross-session cache either.
|
|
2163
|
+
if (matchingModelUsage &&
|
|
2164
|
+
typeof matchingModelUsage.usage.contextWindow === "number" &&
|
|
2165
|
+
matchingModelUsage.usage.contextWindow > 0) {
|
|
2166
|
+
session.contextWindowSize = matchingModelUsage.usage.contextWindow;
|
|
2167
|
+
session.contextWindowAuthoritative = true;
|
|
2168
|
+
// Authoritative: fold it into the cross-session cache keyed on
|
|
2169
|
+
// (this session's provider, the resolved model id —
|
|
2170
|
+
// matchingModelUsage.key, e.g. "claude-sonnet-5[1m]") so a later
|
|
2171
|
+
// session/new or switch on the same provider that resolves to
|
|
2172
|
+
// this model seeds the correct window synchronously, with no
|
|
2173
|
+
// getContextUsage IPC.
|
|
2174
|
+
cacheContextWindow(contextWindowCacheKey(session.providerCacheKey, matchingModelUsage.key), matchingModelUsage.usage.contextWindow);
|
|
2175
|
+
// Also cache under the assistant message's own (bare) spelling.
|
|
2176
|
+
// Seed-time reads fall back to a picker value / verbatim live id
|
|
2177
|
+
// when a row carries no resolvedModel (the synthesized
|
|
2178
|
+
// out-of-allowlist resume row sets it undefined on purpose), and
|
|
2179
|
+
// those spellings match `.model` from the assistant message, not
|
|
2180
|
+
// the decorated modelUsage key — without this entry such rows
|
|
2181
|
+
// could never hit the cache.
|
|
2182
|
+
if (lastAssistantModel && lastAssistantModel !== matchingModelUsage.key) {
|
|
2183
|
+
cacheContextWindow(contextWindowCacheKey(session.providerCacheKey, lastAssistantModel), matchingModelUsage.usage.contextWindow);
|
|
2184
|
+
}
|
|
1952
2185
|
}
|
|
1953
2186
|
// Send usage_update notification
|
|
1954
2187
|
if (lastAssistantTotalUsage !== null) {
|
|
@@ -2191,11 +2424,16 @@ export class ClaudeAcpAgent {
|
|
|
2191
2424
|
const model = message.event.message.model;
|
|
2192
2425
|
if (model && model !== "<synthetic>") {
|
|
2193
2426
|
lastAssistantModel = model;
|
|
2194
|
-
// Only upgrade from the default — once
|
|
2195
|
-
//
|
|
2196
|
-
//
|
|
2197
|
-
// confirmed by each `result`), trust it over
|
|
2198
|
-
|
|
2427
|
+
// Only upgrade from the heuristic default — once we have an
|
|
2428
|
+
// authoritative window (cache-seeded at session creation or
|
|
2429
|
+
// on a model switch, read from the resumed session on
|
|
2430
|
+
// session/load, confirmed by each `result`), trust it over
|
|
2431
|
+
// the heuristic. The flag, not the value, is the sentinel: an
|
|
2432
|
+
// authoritative window can legitimately equal
|
|
2433
|
+
// DEFAULT_CONTEXT_WINDOW (e.g. a backend serving a 200k lane
|
|
2434
|
+
// under a "[1m]"-spelled id) and must not be clobbered.
|
|
2435
|
+
if (!session.contextWindowAuthoritative &&
|
|
2436
|
+
session.contextWindowSize === DEFAULT_CONTEXT_WINDOW) {
|
|
2199
2437
|
const inferred = inferContextWindowFromModel(model);
|
|
2200
2438
|
if (inferred !== null) {
|
|
2201
2439
|
session.contextWindowSize = inferred;
|
|
@@ -2471,6 +2709,11 @@ export class ClaudeAcpAgent {
|
|
|
2471
2709
|
emittedToolCalls: session.emittedToolCalls,
|
|
2472
2710
|
messageId: messageIdForGrouping(message),
|
|
2473
2711
|
toolUseResult: message.type === "user" ? message.tool_use_result : undefined,
|
|
2712
|
+
// On the wire since CLI 2.1.216 but not in SDKUserMessage's
|
|
2713
|
+
// type, hence the cast. Validated by parseToolResultMeta.
|
|
2714
|
+
toolResultMeta: message.type === "user"
|
|
2715
|
+
? message.tool_result_meta
|
|
2716
|
+
: undefined,
|
|
2474
2717
|
})) {
|
|
2475
2718
|
// sendUpdate records delivery. Subagent text/thinking is
|
|
2476
2719
|
// filtered out of `content` above; blocks that do pass through
|
|
@@ -2490,7 +2733,20 @@ export class ClaudeAcpAgent {
|
|
|
2490
2733
|
_meta: {
|
|
2491
2734
|
claudeCode: {
|
|
2492
2735
|
toolName: message.tool_name,
|
|
2493
|
-
toolResponse: {
|
|
2736
|
+
toolResponse: {
|
|
2737
|
+
elapsedTimeSeconds: message.elapsed_time_seconds,
|
|
2738
|
+
// For Agent/Task calls: the subagent's type, and — when
|
|
2739
|
+
// the subagent is waiting out an API rate-limit retry —
|
|
2740
|
+
// the SDK's retry counters (attempt, max_retries,
|
|
2741
|
+
// retry_delay_ms, …), forwarded verbatim so clients can
|
|
2742
|
+
// show why a spawn looks stalled.
|
|
2743
|
+
...(message.subagent_type !== undefined && {
|
|
2744
|
+
subagentType: message.subagent_type,
|
|
2745
|
+
}),
|
|
2746
|
+
...(message.subagent_retry !== undefined && {
|
|
2747
|
+
subagentRetry: message.subagent_retry,
|
|
2748
|
+
}),
|
|
2749
|
+
},
|
|
2494
2750
|
},
|
|
2495
2751
|
},
|
|
2496
2752
|
},
|
|
@@ -3090,7 +3346,7 @@ export class ClaudeAcpAgent {
|
|
|
3090
3346
|
await this.client.sessionUpdate({ sessionId, update });
|
|
3091
3347
|
}
|
|
3092
3348
|
canUseTool(sessionId) {
|
|
3093
|
-
return async (toolName, toolInput, { signal, suggestions, toolUseID, agentID }) => {
|
|
3349
|
+
return async (toolName, toolInput, { signal, suggestions, toolUseID, agentID, matchedAskRule }) => {
|
|
3094
3350
|
const alwaysAllowLabel = describeAlwaysAllow(suggestions, toolName);
|
|
3095
3351
|
const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true;
|
|
3096
3352
|
const session = this.sessions[sessionId];
|
|
@@ -3198,7 +3454,14 @@ export class ClaudeAcpAgent {
|
|
|
3198
3454
|
};
|
|
3199
3455
|
}
|
|
3200
3456
|
}
|
|
3201
|
-
|
|
3457
|
+
// In bypass mode the CLI skips permission checks itself; the asks that
|
|
3458
|
+
// still reach canUseTool are the ones it insists on prompting for even
|
|
3459
|
+
// under --dangerously-skip-permissions. Keep auto-allowing those —
|
|
3460
|
+
// bypass means bypass — EXCEPT rule-forced asks (`matchedAskRule`): the
|
|
3461
|
+
// user explicitly configured a permissions.ask rule for this tool, and
|
|
3462
|
+
// the SDK's guidance is that hosts running auto-approval must treat such
|
|
3463
|
+
// asks as a human prompt. Fall through to the normal request below.
|
|
3464
|
+
if (session.modes.currentModeId === "bypassPermissions" && !matchedAskRule) {
|
|
3202
3465
|
return {
|
|
3203
3466
|
behavior: "allow",
|
|
3204
3467
|
updatedInput: toolInput,
|
|
@@ -3404,17 +3667,21 @@ export class ClaudeAcpAgent {
|
|
|
3404
3667
|
// carries no "1m" token.
|
|
3405
3668
|
const newModelInfo = session.modelInfos.find((m) => m.value === value);
|
|
3406
3669
|
if (session.models.currentModelId !== value) {
|
|
3407
|
-
//
|
|
3408
|
-
//
|
|
3409
|
-
// `
|
|
3410
|
-
//
|
|
3411
|
-
//
|
|
3412
|
-
//
|
|
3413
|
-
//
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3670
|
+
// Seed the new model's context window WITHOUT any IPC on the switch
|
|
3671
|
+
// path: cached authoritative value if we've already learned it (from a
|
|
3672
|
+
// prior turn's `result.modelUsage`), else the text heuristic, else the
|
|
3673
|
+
// default. We deliberately do NOT call `getContextUsage` here — before
|
|
3674
|
+
// a fresh session's first prompt turn that control request is not
|
|
3675
|
+
// serviced (~15s stall, issues #886/#880), and (because SDK control
|
|
3676
|
+
// requests are serialized over one channel) it would drag the awaited
|
|
3677
|
+
// `setModel` down with it. The authoritative window arrives on the
|
|
3678
|
+
// first `result.modelUsage` for the model and is cached from there;
|
|
3679
|
+
// until then a switched-to alias that has never run a turn shows the
|
|
3680
|
+
// heuristic/default window, which self-corrects on its first response
|
|
3681
|
+
// (matches pre-0.59.0 behavior).
|
|
3682
|
+
const seeded = immediateContextWindow(session.providerCacheKey, value, newModelInfo);
|
|
3683
|
+
session.contextWindowSize = seeded.size;
|
|
3684
|
+
session.contextWindowAuthoritative = seeded.authoritative;
|
|
3418
3685
|
}
|
|
3419
3686
|
session.models = { ...session.models, currentModelId: value };
|
|
3420
3687
|
// Recompute availableModes for the new model and clamp the current
|
|
@@ -3746,6 +4013,28 @@ export class ClaudeAcpAgent {
|
|
|
3746
4013
|
// below) so the TaskCreated/TaskCompleted hook callbacks can close over
|
|
3747
4014
|
// the same Map that the streaming message handler will read from.
|
|
3748
4015
|
const taskState = new Map();
|
|
4016
|
+
// The exact env the query will be created with. Built (and the provider
|
|
4017
|
+
// cache key derived from it, below) in one place so the key always
|
|
4018
|
+
// describes the backend this query actually talks to: `providers/set`,
|
|
4019
|
+
// `providers/disable`, and `logout` mutate the process-wide provider
|
|
4020
|
+
// config concurrently, so re-resolving it after any of the awaits between
|
|
4021
|
+
// here and the session registration could disagree with the env baked
|
|
4022
|
+
// into the query.
|
|
4023
|
+
const env = {
|
|
4024
|
+
...process.env,
|
|
4025
|
+
...userProvidedOptions?.env,
|
|
4026
|
+
// Client-managed LLM routing: `providers/set` config wins, else the
|
|
4027
|
+
// legacy gateway auth request. Baked into the query at creation, so it
|
|
4028
|
+
// only affects sessions started after the change (matching the RFD).
|
|
4029
|
+
...createEnvForProvider(this.resolveProviderConfig()),
|
|
4030
|
+
// Opt-in to session state events like when the agent is idle
|
|
4031
|
+
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
|
4032
|
+
};
|
|
4033
|
+
// Scopes the context-window cache to this query's backend (see
|
|
4034
|
+
// `contextWindowCache`). Derived from the same `env` object handed to the
|
|
4035
|
+
// SDK, so per-session `_meta` env routing and ambient process-env routing
|
|
4036
|
+
// are distinguished exactly as the CLI will see them.
|
|
4037
|
+
const providerCacheKey = providerCacheKeyFor(env);
|
|
3749
4038
|
const options = {
|
|
3750
4039
|
systemPrompt,
|
|
3751
4040
|
settingSources: ["user", "project", "local"],
|
|
@@ -3762,13 +4051,7 @@ export class ClaudeAcpAgent {
|
|
|
3762
4051
|
...(modelConfig.availableModels && { availableModels: modelConfig.availableModels }),
|
|
3763
4052
|
},
|
|
3764
4053
|
}),
|
|
3765
|
-
env
|
|
3766
|
-
...process.env,
|
|
3767
|
-
...userProvidedOptions?.env,
|
|
3768
|
-
...createEnvForGateway(this.gatewayAuthRequest),
|
|
3769
|
-
// Opt-in to session state events like when the agent is idle
|
|
3770
|
-
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
|
3771
|
-
},
|
|
4054
|
+
env,
|
|
3772
4055
|
// Override certain fields that must be controlled by ACP
|
|
3773
4056
|
cwd: params.cwd,
|
|
3774
4057
|
includePartialMessages: true,
|
|
@@ -3810,7 +4093,7 @@ export class ClaudeAcpAgent {
|
|
|
3810
4093
|
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
|
3811
4094
|
{
|
|
3812
4095
|
hooks: [
|
|
3813
|
-
createPostToolUseHook(
|
|
4096
|
+
createPostToolUseHook({
|
|
3814
4097
|
onEnterPlanMode: async () => {
|
|
3815
4098
|
await this.client.sessionUpdate({
|
|
3816
4099
|
sessionId,
|
|
@@ -3916,7 +4199,7 @@ export class ClaudeAcpAgent {
|
|
|
3916
4199
|
const allowedModels = Array.isArray(settingsAvailableModels)
|
|
3917
4200
|
? applyAvailableModelsAllowlist(initializationResult.models, settingsAvailableModels, settingsModelOverrides)
|
|
3918
4201
|
: initializationResult.models;
|
|
3919
|
-
const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined);
|
|
4202
|
+
const { modelState: models, resumedContextWindow } = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined);
|
|
3920
4203
|
// Gate `auto` (and future model-specific modes) on the resolved model's
|
|
3921
4204
|
// `ModelInfo`. See `buildAvailableModes` for the canonical SDK signal.
|
|
3922
4205
|
// A resumed session can be running a model outside the `availableModels`
|
|
@@ -4012,25 +4295,39 @@ export class ClaudeAcpAgent {
|
|
|
4012
4295
|
typeof initialEffort.currentValue === "string" &&
|
|
4013
4296
|
initialEffort.currentValue !== "default") {
|
|
4014
4297
|
await q.applyFlagSettings({
|
|
4015
|
-
effortLevel: initialEffort.currentValue,
|
|
4298
|
+
effortLevel: toSdkEffortLevel(initialEffort.currentValue),
|
|
4016
4299
|
});
|
|
4017
4300
|
}
|
|
4018
|
-
// Seed the context window
|
|
4019
|
-
//
|
|
4020
|
-
//
|
|
4021
|
-
//
|
|
4022
|
-
//
|
|
4023
|
-
//
|
|
4024
|
-
//
|
|
4301
|
+
// Seed the context window WITHOUT any extra IPC on the session/new path.
|
|
4302
|
+
// On session/load, the resumed session's own `getContextUsage` report — a
|
|
4303
|
+
// response `getAvailableModels` already awaited to learn the live model
|
|
4304
|
+
// (resumed sessions ARE serviced pre-turn, unlike fresh ones) — is
|
|
4305
|
+
// authoritative and wins. Otherwise: the cached authoritative window if a
|
|
4306
|
+
// prior turn has learned it for this model (`result.modelUsage`,
|
|
4307
|
+
// cross-session), else the text heuristic, else the default. We
|
|
4308
|
+
// deliberately do NOT issue a getContextUsage call here: on a fresh
|
|
4309
|
+
// session that control request is not serviced until the first prompt
|
|
4310
|
+
// turn runs, so awaiting it — as 0.59.0 did — made session/new take ~15s
|
|
4311
|
+
// (issues #886/#880). The authoritative window arrives on the first
|
|
4312
|
+
// `result.modelUsage` and is cached from there.
|
|
4313
|
+
//
|
|
4314
|
+
// Text inference alone misses aliases that resolve to extended-context
|
|
4315
|
+
// models with no "1m" token anywhere in their id or description (e.g.
|
|
4316
|
+
// `sonnet` → claude-sonnet-5, natively ~1M): those stream
|
|
4317
|
+
// `usage_update.size: 200000` until the first result's modelUsage corrects
|
|
4318
|
+
// it — but the cache means only the FIRST session to ever run a turn on such
|
|
4319
|
+
// a model eats that window, not every fresh session after a process
|
|
4320
|
+
// restart (issue #596; a post-restart session/load is covered by the
|
|
4321
|
+
// resumed report above).
|
|
4025
4322
|
//
|
|
4026
4323
|
// The inference fallback is deliberately keyed to the allowlisted entry: a
|
|
4027
4324
|
// fallback-resolved sibling's resolvedModel/displayName/description can
|
|
4028
4325
|
// describe a different context lane than the verbatim live id (e.g. an
|
|
4029
4326
|
// "opus[1m]" row matched for a bare 200k id), so on the fallback path only
|
|
4030
4327
|
// the id itself is a trustworthy window signal.
|
|
4031
|
-
const
|
|
4032
|
-
|
|
4033
|
-
|
|
4328
|
+
const seededWindow = resumedContextWindow !== null
|
|
4329
|
+
? { size: resumedContextWindow, authoritative: true }
|
|
4330
|
+
: immediateContextWindow(providerCacheKey, models.currentModelId, allowlistedModelInfo);
|
|
4034
4331
|
this.sessions[sessionId] = {
|
|
4035
4332
|
query: q,
|
|
4036
4333
|
input: input,
|
|
@@ -4053,7 +4350,9 @@ export class ClaudeAcpAgent {
|
|
|
4053
4350
|
fastModeEnabled,
|
|
4054
4351
|
abortController,
|
|
4055
4352
|
emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false,
|
|
4056
|
-
contextWindowSize,
|
|
4353
|
+
contextWindowSize: seededWindow.size,
|
|
4354
|
+
contextWindowAuthoritative: seededWindow.authoritative,
|
|
4355
|
+
providerCacheKey,
|
|
4057
4356
|
taskState,
|
|
4058
4357
|
toolUseCache: {},
|
|
4059
4358
|
emittedToolCalls: new Set(),
|
|
@@ -4129,27 +4428,76 @@ function snapshotFromUsage(usage) {
|
|
|
4129
4428
|
cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
|
|
4130
4429
|
};
|
|
4131
4430
|
}
|
|
4132
|
-
|
|
4431
|
+
/**
|
|
4432
|
+
* Adapt a legacy gateway `authenticate` request into the shared
|
|
4433
|
+
* {@link ProviderConfig} shape. Returns `null` when no gateway request is
|
|
4434
|
+
* present. `methodId` selects the protocol: `gateway-bedrock` → bedrock,
|
|
4435
|
+
* otherwise anthropic.
|
|
4436
|
+
*/
|
|
4437
|
+
function gatewayRequestToProviderConfig(request) {
|
|
4133
4438
|
if (!request?._meta) {
|
|
4439
|
+
return null;
|
|
4440
|
+
}
|
|
4441
|
+
return {
|
|
4442
|
+
apiType: request.methodId === "gateway-bedrock" ? "bedrock" : "anthropic",
|
|
4443
|
+
baseUrl: request._meta.gateway.baseUrl,
|
|
4444
|
+
headers: request._meta.gateway.headers,
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4447
|
+
/**
|
|
4448
|
+
* Map a resolved provider config into the Claude Code env vars that redirect API
|
|
4449
|
+
* traffic and inject headers. Returns an empty object when routing is
|
|
4450
|
+
* unconfigured. The token/bypass placeholders (`" "`) are required so the CLI
|
|
4451
|
+
* skips its normal login/credential checks when a gateway is in use.
|
|
4452
|
+
*/
|
|
4453
|
+
function createEnvForProvider(config) {
|
|
4454
|
+
if (!config) {
|
|
4134
4455
|
return {};
|
|
4135
4456
|
}
|
|
4136
|
-
const customHeaders = Object.entries(
|
|
4457
|
+
const customHeaders = Object.entries(config.headers)
|
|
4137
4458
|
.map(([key, value]) => `${key}: ${value}`)
|
|
4138
4459
|
.join("\n");
|
|
4139
|
-
if (
|
|
4460
|
+
if (config.apiType === "bedrock") {
|
|
4140
4461
|
return {
|
|
4141
4462
|
CLAUDE_CODE_USE_BEDROCK: "1",
|
|
4142
4463
|
AWS_BEARER_TOKEN_BEDROCK: " ", // Must be non-empty to bypass pass configuration check
|
|
4143
|
-
ANTHROPIC_BEDROCK_BASE_URL:
|
|
4464
|
+
ANTHROPIC_BEDROCK_BASE_URL: config.baseUrl,
|
|
4465
|
+
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
|
4466
|
+
};
|
|
4467
|
+
}
|
|
4468
|
+
if (config.apiType === "vertex") {
|
|
4469
|
+
// `config.vertex` is guaranteed present for vertex by `unstable_setProvider`
|
|
4470
|
+
// validation; fall back to empty strings defensively.
|
|
4471
|
+
return {
|
|
4472
|
+
CLAUDE_CODE_USE_VERTEX: "1",
|
|
4473
|
+
ANTHROPIC_VERTEX_BASE_URL: config.baseUrl,
|
|
4474
|
+
ANTHROPIC_VERTEX_PROJECT_ID: config.vertex?.projectId ?? "",
|
|
4475
|
+
CLOUD_ML_REGION: config.vertex?.region ?? "",
|
|
4144
4476
|
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
|
4145
4477
|
};
|
|
4146
4478
|
}
|
|
4147
4479
|
return {
|
|
4148
|
-
ANTHROPIC_BASE_URL:
|
|
4480
|
+
ANTHROPIC_BASE_URL: config.baseUrl,
|
|
4149
4481
|
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
|
4150
4482
|
ANTHROPIC_AUTH_TOKEN: " ", // Must be specified to bypass claude login requirement
|
|
4151
4483
|
};
|
|
4152
4484
|
}
|
|
4485
|
+
/**
|
|
4486
|
+
* Validate a provider base URL: must be a non-empty absolute http(s) URL.
|
|
4487
|
+
*/
|
|
4488
|
+
function isValidBaseUrl(baseUrl) {
|
|
4489
|
+
if (typeof baseUrl !== "string" || baseUrl.trim() === "") {
|
|
4490
|
+
return false;
|
|
4491
|
+
}
|
|
4492
|
+
let parsed;
|
|
4493
|
+
try {
|
|
4494
|
+
parsed = new URL(baseUrl);
|
|
4495
|
+
}
|
|
4496
|
+
catch {
|
|
4497
|
+
return false;
|
|
4498
|
+
}
|
|
4499
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
4500
|
+
}
|
|
4153
4501
|
/**
|
|
4154
4502
|
* Build the list of permission modes the agent will advertise for the given
|
|
4155
4503
|
* model. `auto` is gated by `ModelInfo.supportsAutoMode === true`, which is
|
|
@@ -4199,7 +4547,10 @@ function buildAvailableModes(modelInfo) {
|
|
|
4199
4547
|
// and only clears a key when an explicit `null` is sent — see
|
|
4200
4548
|
// `applyFlagSettings` in @anthropic-ai/claude-agent-sdk. Mapping both the
|
|
4201
4549
|
// `"default"` sentinel and `undefined` (effort option absent for the model) to
|
|
4202
|
-
// `null` ensures any previously-applied flag is actually cleared.
|
|
4550
|
+
// `null` ensures any previously-applied flag is actually cleared. Typed as
|
|
4551
|
+
// `EffortLevel` (not `Settings["effortLevel"]`): the picker offers whatever
|
|
4552
|
+
// `supportedEffortLevels` reports, which includes the session-scoped `"max"`
|
|
4553
|
+
// that the persisted Settings shape deliberately excludes.
|
|
4203
4554
|
function toSdkEffortLevel(value) {
|
|
4204
4555
|
return value === undefined || value === "default" ? null : value;
|
|
4205
4556
|
}
|
|
@@ -4651,23 +5002,37 @@ export function applyAvailableModelsAllowlist(sdkModels, allowlist, settingsMode
|
|
|
4651
5002
|
}
|
|
4652
5003
|
/** Read the model a resumed session is actually running (via the
|
|
4653
5004
|
* `getContextUsage` control request — the same source `/context` prints) and
|
|
4654
|
-
* map it onto the picker
|
|
4655
|
-
*
|
|
4656
|
-
*
|
|
5005
|
+
* map it onto the picker, along with the report's authoritative context
|
|
5006
|
+
* window (`rawMaxTokens`). Resumed sessions get this request serviced before
|
|
5007
|
+
* any turn runs in the new process — unlike fresh sessions, where it stalls
|
|
5008
|
+
* until the first prompt turn (issues #886/#880) — so the same response that
|
|
5009
|
+
* restores the live model (issue #845) also seeds the window for free,
|
|
5010
|
+
* covering post-restart reloads of models the text heuristic misses (issue
|
|
5011
|
+
* #596). Best-effort: a control-request failure is logged and returns nulls
|
|
5012
|
+
* so callers keep their current choice; failing the whole session/load over
|
|
5013
|
+
* an unreadable report would be worse. */
|
|
4657
5014
|
async function readResumedLiveModel(query, models, logger) {
|
|
4658
5015
|
try {
|
|
4659
|
-
const
|
|
4660
|
-
return
|
|
5016
|
+
const usage = await query.getContextUsage();
|
|
5017
|
+
return {
|
|
5018
|
+
model: usage.model ? matchResumedModel(models, usage.model) : null,
|
|
5019
|
+
contextWindow: usage.rawMaxTokens > 0 ? usage.rawMaxTokens : null,
|
|
5020
|
+
};
|
|
4661
5021
|
}
|
|
4662
5022
|
catch (error) {
|
|
4663
5023
|
logger.error("Failed to read the resumed session's live model:", error);
|
|
4664
|
-
return null;
|
|
5024
|
+
return { model: null, contextWindow: null };
|
|
4665
5025
|
}
|
|
4666
5026
|
}
|
|
4667
5027
|
async function getAvailableModels(query, models, sdkModels, settingsManager, logger, isResumedSession) {
|
|
4668
5028
|
const settings = settingsManager.getSettings();
|
|
4669
5029
|
let currentModel = models[0];
|
|
4670
5030
|
let resolvedFromInput;
|
|
5031
|
+
// The context window reported alongside a resumed session's live model.
|
|
5032
|
+
// Only ever non-null on the paths where `currentModel` IS the live model
|
|
5033
|
+
// (no override, or a failed override re-assert), so the window always
|
|
5034
|
+
// describes the model the session actually runs.
|
|
5035
|
+
let resumedContextWindow = null;
|
|
4671
5036
|
// Model priority (highest to lowest):
|
|
4672
5037
|
// 1. ANTHROPIC_MODEL environment variable
|
|
4673
5038
|
// 2. settings.model (user configuration)
|
|
@@ -4695,7 +5060,9 @@ async function getAvailableModels(query, models, sdkModels, settingsManager, log
|
|
|
4695
5060
|
// the SDK is already running this model, and pushing a picker alias back
|
|
4696
5061
|
// (e.g. "opus[1m]") could change the live model rather than describe it.
|
|
4697
5062
|
if (resolvedFromInput === undefined && isResumedSession) {
|
|
4698
|
-
|
|
5063
|
+
const live = await readResumedLiveModel(query, models, logger);
|
|
5064
|
+
currentModel = live.model ?? currentModel;
|
|
5065
|
+
resumedContextWindow = live.contextWindow;
|
|
4699
5066
|
}
|
|
4700
5067
|
// Skip the setModel round-trip when we can prove the SDK has already landed
|
|
4701
5068
|
// on the same model. Two cases qualify:
|
|
@@ -4728,16 +5095,21 @@ async function getAvailableModels(query, models, sdkModels, settingsManager, log
|
|
|
4728
5095
|
if (!isResumedSession)
|
|
4729
5096
|
throw error;
|
|
4730
5097
|
logger.error(`Failed to re-assert model "${currentModel.value}" on resume:`, error);
|
|
4731
|
-
|
|
5098
|
+
const live = await readResumedLiveModel(query, models, logger);
|
|
5099
|
+
currentModel = live.model ?? currentModel;
|
|
5100
|
+
resumedContextWindow = live.contextWindow;
|
|
4732
5101
|
}
|
|
4733
5102
|
}
|
|
4734
5103
|
return {
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
5104
|
+
modelState: {
|
|
5105
|
+
availableModels: models.map((model) => ({
|
|
5106
|
+
modelId: model.value,
|
|
5107
|
+
name: model.displayName,
|
|
5108
|
+
description: model.description,
|
|
5109
|
+
})),
|
|
5110
|
+
currentModelId: currentModel.value,
|
|
5111
|
+
},
|
|
5112
|
+
resumedContextWindow,
|
|
4741
5113
|
};
|
|
4742
5114
|
}
|
|
4743
5115
|
function getAvailableSlashCommands(commands) {
|
|
@@ -4864,6 +5236,12 @@ export function promptToClaude(prompt) {
|
|
|
4864
5236
|
},
|
|
4865
5237
|
session_id: prompt.sessionId,
|
|
4866
5238
|
parent_tool_use_id: null,
|
|
5239
|
+
// ACP prompts are the user's own input relayed by the client. Stamp the
|
|
5240
|
+
// provenance explicitly: per the SDK, a host wrapping keyboard input must
|
|
5241
|
+
// send `{kind: "human"}` — an absent `origin` is treated as unattributed
|
|
5242
|
+
// and fails closed at the CLI's strict isHuman() trust gates (e.g. the
|
|
5243
|
+
// ultracode keyword opt-in honors only human-originated turns).
|
|
5244
|
+
origin: { kind: "human" },
|
|
4867
5245
|
};
|
|
4868
5246
|
}
|
|
4869
5247
|
/**
|
|
@@ -4973,6 +5351,33 @@ function streamedInputRefinement(toolUse, input, supportsTerminalOutput, cwd) {
|
|
|
4973
5351
|
...(locations ? { locations } : {}),
|
|
4974
5352
|
};
|
|
4975
5353
|
}
|
|
5354
|
+
/** Validates the SDK user message's `tool_result_meta` sidecar (emitted on the
|
|
5355
|
+
* wire by CLI ≥ 2.1.216 but absent from sdk.d.ts, hence unknown-typed) into a
|
|
5356
|
+
* by-tool_use_id lookup. Each entry explains why an is_error tool_result
|
|
5357
|
+
* carries harness prose instead of the tool's own output — "user-rejected",
|
|
5358
|
+
* "permission-rule", "interrupted", "cancelled", … (open set: new kinds ship
|
|
5359
|
+
* on the wire ahead of schema updates, so no enum check). Malformed entries
|
|
5360
|
+
* are skipped rather than failing the message. */
|
|
5361
|
+
function parseToolResultMeta(raw) {
|
|
5362
|
+
if (!Array.isArray(raw)) {
|
|
5363
|
+
return undefined;
|
|
5364
|
+
}
|
|
5365
|
+
let byToolUseId;
|
|
5366
|
+
for (const entry of raw) {
|
|
5367
|
+
if (typeof entry !== "object" || entry === null) {
|
|
5368
|
+
continue;
|
|
5369
|
+
}
|
|
5370
|
+
const { id, non_execution_kind, user_feedback } = entry;
|
|
5371
|
+
if (typeof id !== "string" || typeof non_execution_kind !== "string") {
|
|
5372
|
+
continue;
|
|
5373
|
+
}
|
|
5374
|
+
(byToolUseId ??= new Map()).set(id, {
|
|
5375
|
+
nonExecutionKind: non_execution_kind,
|
|
5376
|
+
...(typeof user_feedback === "string" ? { userFeedback: user_feedback } : {}),
|
|
5377
|
+
});
|
|
5378
|
+
}
|
|
5379
|
+
return byToolUseId;
|
|
5380
|
+
}
|
|
4976
5381
|
/**
|
|
4977
5382
|
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
4978
5383
|
* Only handles text, image, and thinking chunks for now.
|
|
@@ -5013,6 +5418,9 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
5013
5418
|
.length === 1
|
|
5014
5419
|
? options.toolUseResult
|
|
5015
5420
|
: undefined;
|
|
5421
|
+
// Unlike `tool_use_result`, entries carry their own tool_use_id, so batched
|
|
5422
|
+
// messages need no single-block guard.
|
|
5423
|
+
const toolResultMeta = parseToolResultMeta(options?.toolResultMeta);
|
|
5016
5424
|
const output = [];
|
|
5017
5425
|
// Only handle the first chunk for streaming; extend as needed for batching
|
|
5018
5426
|
for (const chunk of content) {
|
|
@@ -5156,6 +5564,12 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
5156
5564
|
case "mcp_tool_result": {
|
|
5157
5565
|
const wasEmitted = options?.emittedToolCalls?.has(chunk.tool_use_id) === true;
|
|
5158
5566
|
options?.emittedToolCalls?.delete(chunk.tool_use_id);
|
|
5567
|
+
// Why this is_error result carries harness prose instead of tool
|
|
5568
|
+
// output (user-rejected / interrupted / …), when the SDK said so.
|
|
5569
|
+
// Spread into the claudeCode meta of every update emitted below; the
|
|
5570
|
+
// untracked-tool fallback can't carry it (claudeCode metas always
|
|
5571
|
+
// carry `toolName`, which is unknown there).
|
|
5572
|
+
const nonExecution = toolResultMeta?.get(chunk.tool_use_id);
|
|
5159
5573
|
const toolUse = toolUseCache[chunk.tool_use_id];
|
|
5160
5574
|
if (!toolUse) {
|
|
5161
5575
|
// The permission flow may have surfaced this tool_call even though
|
|
@@ -5195,6 +5609,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
5195
5609
|
_meta: {
|
|
5196
5610
|
claudeCode: {
|
|
5197
5611
|
toolName: toolUse.name,
|
|
5612
|
+
...(nonExecution ?? {}),
|
|
5198
5613
|
...(options?.parentToolUseId ? { parentToolUseId: options.parentToolUseId } : {}),
|
|
5199
5614
|
},
|
|
5200
5615
|
},
|
|
@@ -5255,6 +5670,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
5255
5670
|
_meta: {
|
|
5256
5671
|
claudeCode: {
|
|
5257
5672
|
toolName: toolUse.name,
|
|
5673
|
+
...(nonExecution ?? {}),
|
|
5258
5674
|
},
|
|
5259
5675
|
...(toolMeta?.terminal_exit ? { terminal_exit: toolMeta.terminal_exit } : {}),
|
|
5260
5676
|
},
|
|
@@ -5463,9 +5879,13 @@ export function runAcp() {
|
|
|
5463
5879
|
.onRequest(methods.agent.session.setMode, (ctx) => agent.setSessionMode(ctx.params))
|
|
5464
5880
|
.onRequest(methods.agent.session.setConfigOption, (ctx) => agent.setSessionConfigOption(ctx.params))
|
|
5465
5881
|
.onRequest(methods.agent.authenticate, (ctx) => agent.authenticate(ctx.params))
|
|
5882
|
+
.onRequest(methods.agent.providers.list, (ctx) => agent.unstable_listProviders(ctx.params))
|
|
5883
|
+
.onRequest(methods.agent.providers.set, (ctx) => agent.unstable_setProvider(ctx.params))
|
|
5884
|
+
.onRequest(methods.agent.providers.disable, (ctx) => agent.unstable_disableProvider(ctx.params))
|
|
5466
5885
|
.onRequest(methods.agent.logout, (ctx) => agent.logout(ctx.params))
|
|
5467
5886
|
.onRequest(methods.agent.session.prompt, (ctx) => runPromptWithCancellation(agent, ctx.params, ctx.signal))
|
|
5468
5887
|
.onNotification(methods.agent.session.cancel, (ctx) => agent.cancel(ctx.params))
|
|
5888
|
+
.onRequest(STEER_METHOD, { parse: parseSteerRequest }, (ctx) => agent.steer(ctx.params))
|
|
5469
5889
|
.connect(stream);
|
|
5470
5890
|
agent = new ClaudeAcpAgent(new ClientConnection(connection.client));
|
|
5471
5891
|
return { connection, agent };
|
|
@@ -5477,9 +5897,9 @@ function commonPrefixLength(a, b) {
|
|
|
5477
5897
|
}
|
|
5478
5898
|
return i;
|
|
5479
5899
|
}
|
|
5480
|
-
/** Best-effort first guess of a model's context window, used
|
|
5481
|
-
*
|
|
5482
|
-
*
|
|
5900
|
+
/** Best-effort first guess of a model's context window, used to seed the
|
|
5901
|
+
* window synchronously (via `immediateContextWindow`) until a `result` message
|
|
5902
|
+
* arrives with the authoritative `modelUsage` value.
|
|
5483
5903
|
*
|
|
5484
5904
|
* Anthropic 1M-context variants encode "1m" as a distinct token in the SDK
|
|
5485
5905
|
* model ID (e.g., "claude-opus-4-6-1m"), which `\b1m\b` catches without also
|
|
@@ -5489,9 +5909,12 @@ function commonPrefixLength(a, b) {
|
|
|
5489
5909
|
* "claude-opus-4-8[1m]", "Opus 4.7 (1M context)"), so callers pass those too.
|
|
5490
5910
|
* This text scan can't catch every model — some resolve to extended-context
|
|
5491
5911
|
* models with no "1m" anywhere (e.g. `sonnet` → claude-sonnet-5, natively
|
|
5492
|
-
* ~1M)
|
|
5493
|
-
*
|
|
5494
|
-
*
|
|
5912
|
+
* ~1M). Such a miss falls back to the default window and is corrected by
|
|
5913
|
+
* `result.modelUsage` (and cached) within one turn. We do NOT consult the
|
|
5914
|
+
* SDK's `getContextUsage` to close that gap: on a fresh session it is not
|
|
5915
|
+
* serviced before the first prompt turn (issues #886/#880, see
|
|
5916
|
+
* `contextWindowCache`; resumed sessions do get it, via
|
|
5917
|
+
* `readResumedLiveModel`). */
|
|
5495
5918
|
function inferContextWindowFromModel(...texts) {
|
|
5496
5919
|
if (texts.some((text) => text != null && /\b1m\b/i.test(text)))
|
|
5497
5920
|
return 1_000_000;
|
|
@@ -5512,28 +5935,95 @@ async function fetchContextUsedTokens(query, logger) {
|
|
|
5512
5935
|
return null;
|
|
5513
5936
|
}
|
|
5514
5937
|
}
|
|
5515
|
-
/**
|
|
5516
|
-
* `
|
|
5517
|
-
*
|
|
5518
|
-
*
|
|
5519
|
-
*
|
|
5520
|
-
*
|
|
5521
|
-
*
|
|
5938
|
+
/** Cross-session cache of authoritative context windows, keyed by
|
|
5939
|
+
* `${providerCacheKey}\0${modelId}` (see {@link contextWindowCacheKey}).
|
|
5940
|
+
* The window is a property of (model id, backend): the same resolved model id
|
|
5941
|
+
* (e.g. "claude-sonnet-5[1m]", the spelling of the `result.modelUsage` keys)
|
|
5942
|
+
* can name different context lanes behind different base URLs, routing
|
|
5943
|
+
* headers, or credentials, so the key carries both. Caching it module-level
|
|
5944
|
+
* lets a later session/new or switch that resolves to the same (backend,
|
|
5945
|
+
* model) — in this session or any other, within the adapter's lifetime — seed
|
|
5946
|
+
* the correct window synchronously with no IPC. Keying on the resolved id
|
|
5947
|
+
* (rather than the picker value) means aliases that resolve to the same
|
|
5948
|
+
* concrete model share one entry; the result handler additionally writes the
|
|
5949
|
+
* bare assistant-message spelling so seed-time reads that fall back to a
|
|
5950
|
+
* verbatim live id (rows without `resolvedModel`) can hit too.
|
|
5522
5951
|
*
|
|
5523
|
-
*
|
|
5524
|
-
*
|
|
5525
|
-
*
|
|
5526
|
-
*
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5952
|
+
* Populated authoritatively by each `result.modelUsage` a turn confirms (see
|
|
5953
|
+
* the consumer's result handler). We deliberately never populate it from a
|
|
5954
|
+
* fresh session's `getContextUsage`: before that session's first prompt turn
|
|
5955
|
+
* has run the control request is not serviced (it stalls ~15s, and serializes
|
|
5956
|
+
* ahead of an awaited `setModel` — issues #886/#880, regressed in 0.59.0), so
|
|
5957
|
+
* it can neither beat the first `result` nor be issued cheaply before one.
|
|
5958
|
+
* Resumed sessions are the exception — their report IS serviced pre-turn, and
|
|
5959
|
+
* the session/load path seeds (but does not cache) the window from the same
|
|
5960
|
+
* response that restores the live model, see `readResumedLiveModel`.
|
|
5961
|
+
* Cleared on `logout`: 1M-context entitlement can differ per account/tier, so
|
|
5962
|
+
* windows learned under one login must not seed sessions under the next. */
|
|
5963
|
+
const contextWindowCache = new Map();
|
|
5964
|
+
/** The env vars that determine which LLM backend — and which context lane on
|
|
5965
|
+
* it — a query's API traffic reaches: endpoint selection (base URLs and the
|
|
5966
|
+
* Bedrock/Vertex switches with their project/region), routing/beta headers
|
|
5967
|
+
* (an `anthropic-beta: context-1m-…` header flips the same model id at the
|
|
5968
|
+
* same endpoint between context lanes), and credential identity (extended
|
|
5969
|
+
* context is entitlement-gated per account). Used to derive the
|
|
5970
|
+
* provider-cache key from the exact env a query is created with, so
|
|
5971
|
+
* `providers/set` config, per-session `_meta` env overrides, and ambient
|
|
5972
|
+
* process env are all distinguished exactly as the CLI will see them. */
|
|
5973
|
+
const PROVIDER_ROUTING_ENV_VARS = [
|
|
5974
|
+
"ANTHROPIC_BASE_URL",
|
|
5975
|
+
"ANTHROPIC_BEDROCK_BASE_URL",
|
|
5976
|
+
"ANTHROPIC_VERTEX_BASE_URL",
|
|
5977
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
5978
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
5979
|
+
"ANTHROPIC_VERTEX_PROJECT_ID",
|
|
5980
|
+
"CLOUD_ML_REGION",
|
|
5981
|
+
"AWS_REGION",
|
|
5982
|
+
"ANTHROPIC_CUSTOM_HEADERS",
|
|
5983
|
+
"ANTHROPIC_API_KEY",
|
|
5984
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
5985
|
+
];
|
|
5986
|
+
/** Stable identifier for the LLM backend a session's query is created against,
|
|
5987
|
+
* used to scope {@link contextWindowCache} per backend. Positional `\0`-join
|
|
5988
|
+
* of {@link PROVIDER_ROUTING_ENV_VARS} values, so no segment can masquerade
|
|
5989
|
+
* as another and unset vars everywhere yield one stable "default" bucket.
|
|
5990
|
+
* Header/credential values can be secrets; the key only ever lives as an
|
|
5991
|
+
* in-memory Map key and is never logged or surfaced. Over-keying is the safe
|
|
5992
|
+
* side: a var change that didn't really change the backend costs one cache
|
|
5993
|
+
* miss (heuristic seed until the next result), while under-keying would serve
|
|
5994
|
+
* one backend's window for another's. */
|
|
5995
|
+
function providerCacheKeyFor(env) {
|
|
5996
|
+
return PROVIDER_ROUTING_ENV_VARS.map((name) => env[name] ?? "").join("\0");
|
|
5997
|
+
}
|
|
5998
|
+
/** Compose the `contextWindowCache` key from a session's provider key and a
|
|
5999
|
+
* model id. `\0`-joined so the model segment can't collide with a provider
|
|
6000
|
+
* segment. */
|
|
6001
|
+
function contextWindowCacheKey(providerCacheKey, modelId) {
|
|
6002
|
+
return `${providerCacheKey}\0${modelId}`;
|
|
6003
|
+
}
|
|
6004
|
+
function cacheContextWindow(modelKey, window) {
|
|
6005
|
+
if (window > 0) {
|
|
6006
|
+
contextWindowCache.set(modelKey, window);
|
|
5535
6007
|
}
|
|
5536
6008
|
}
|
|
6009
|
+
/** The context window to report *right now* for a model, with NO IPC on the
|
|
6010
|
+
* critical path: the cached authoritative value if we've learned it (from a
|
|
6011
|
+
* prior turn's `result.modelUsage`, this or any session on the same backend),
|
|
6012
|
+
* else the text heuristic over the model row's identity strings, else the
|
|
6013
|
+
* default. Derives the cache key itself — `modelInfo?.resolvedModel ?? modelId`,
|
|
6014
|
+
* the same rule at every seed site — so read keys can't drift from the write
|
|
6015
|
+
* site's spelling. `authoritative` reports whether the value came from the
|
|
6016
|
+
* cache: an authoritative window can legitimately equal
|
|
6017
|
+
* DEFAULT_CONTEXT_WINDOW, so the value alone can't tell the caller. */
|
|
6018
|
+
function immediateContextWindow(providerCacheKey, modelId, modelInfo) {
|
|
6019
|
+
const cached = contextWindowCache.get(contextWindowCacheKey(providerCacheKey, modelInfo?.resolvedModel ?? modelId));
|
|
6020
|
+
if (cached !== undefined)
|
|
6021
|
+
return { size: cached, authoritative: true };
|
|
6022
|
+
return {
|
|
6023
|
+
size: inferContextWindowFromModel(modelId, modelInfo?.resolvedModel, modelInfo?.displayName, modelInfo?.description) ?? DEFAULT_CONTEXT_WINDOW,
|
|
6024
|
+
authoritative: false,
|
|
6025
|
+
};
|
|
6026
|
+
}
|
|
5537
6027
|
/** Translate the legacy `MAX_THINKING_TOKENS` env var into the SDK's `thinking`
|
|
5538
6028
|
* option. The `maxThinkingTokens` option it used to feed is deprecated and
|
|
5539
6029
|
* reduced to on/off on current models, so map the value to explicit thinking
|
|
@@ -5575,6 +6065,12 @@ function getMatchingModelUsage(modelUsage, currentModel) {
|
|
|
5575
6065
|
}
|
|
5576
6066
|
}
|
|
5577
6067
|
if (bestKey) {
|
|
5578
|
-
|
|
6068
|
+
// `bestKey` is the SDK's resolved model id (e.g. "claude-sonnet-5[1m]"),
|
|
6069
|
+
// the same spelling as ModelInfo.resolvedModel — the primary key the
|
|
6070
|
+
// window is cached under. `currentModel` (the assistant message's
|
|
6071
|
+
// `.model`) can be the bare form (e.g. "claude-sonnet-5"); the result
|
|
6072
|
+
// handler caches under that spelling too, for seed-time reads that fall
|
|
6073
|
+
// back to a bare id (rows without `resolvedModel`).
|
|
6074
|
+
return { key: bestKey, usage: modelUsage[bestKey] };
|
|
5579
6075
|
}
|
|
5580
6076
|
}
|