@agentclientprotocol/claude-agent-acp 0.74.0 → 0.75.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * `authStatus` — interim `_meta`-based ACP extension that reports which auth
3
+ * identity the agent process itself uses (Claude subscription, API key,
4
+ * gateway, external cloud credentials, or nothing).
5
+ *
6
+ * Push only. One carrier, connection-scoped (never per session): the
7
+ * notification `_auth/status_update` → `{authStatus}`. The client never asks;
8
+ * there is no request method. The empty `agentCapabilities._meta.authStatus`
9
+ * marker means "this agent pushes its identity", nothing more.
10
+ *
11
+ * The agent reads the identity at these moments:
12
+ * - `initialize`, with an asynchronous CLI probe. Nothing waits for it, and its
13
+ * result is the first push of the connection;
14
+ * - a completed `authenticate` or a `logout`;
15
+ * - each `createSession`, from the session account. A refused session reports
16
+ * too: the client must know which account was refused;
17
+ * - each turn the `--hide-claude-auth` guard checks. The guard reads the
18
+ * account anyway, so this costs nothing;
19
+ * - the START of each user prompt, with an asynchronous CLI probe. This finds a
20
+ * login or a logout done in another terminal, including the sign-in a client
21
+ * performs after a refusal, because the retried prompt is a new prompt.
22
+ * Nothing waits for it and the push goes out mid-turn if that is when the
23
+ * read lands.
24
+ *
25
+ * A probe whose kind differs from a live session's account marks that session
26
+ * for recreation under `--hide-claude-auth`. The mark is a flag, consumed at
27
+ * the next turn boundary: a running turn is never interrupted. The probe judges
28
+ * nothing itself — the next turn runs a new `initialize`, and the creation
29
+ * guard decides on the account it reports.
30
+ *
31
+ * A read does not always cause a push. The agent sends `_auth/status_update`
32
+ * only when the payload is different from the last one it sent.
33
+ *
34
+ * "Cannot determine" is expressed by silence: an agent that cannot read its own
35
+ * identity pushes nothing, and the client shows "not reported". A known
36
+ * logged-out state is a payload of its own (`kind: "none"`), which is what
37
+ * distinguishes the two.
38
+ *
39
+ * Reporting only — there is no write path in v1. When the upstream RFD lands,
40
+ * the same payload moves from `_meta` to first-class fields and this module
41
+ * is retired.
42
+ */
43
+ import type { AccountInfo } from "@anthropic-ai/claude-agent-sdk";
44
+ /** Change notification, agent → client. Fire and forget: clients that do not
45
+ * know the method drop it silently, so it is sent unconditionally. */
46
+ export declare const AUTH_STATUS_UPDATE_METHOD = "_auth/status_update";
47
+ /** Upper bound on one `claude auth status --json` run, so a hung CLI cannot
48
+ * wedge every later probe that joins it. */
49
+ export declare const AUTH_STATUS_PROBE_TIMEOUT_MS = 5000;
50
+ export type AuthStatusKind = "account" | "api_key" | "gateway" | "external" | "none";
51
+ export type AuthStatusAccount = {
52
+ email?: string;
53
+ organization?: string;
54
+ /** Vendor plan/license string, not normalized. */
55
+ plan?: string;
56
+ };
57
+ export type AuthStatus = {
58
+ kind: AuthStatusKind;
59
+ /** Human-readable and usable as a UI string on its own. The "type" line:
60
+ * "Claude Max", "Anthropic API key", "AWS Bedrock". */
61
+ label: string;
62
+ /** Optional second line carrying the specifics (key source, gateway host, …).
63
+ * Clients render it under `label`, falling back to `account.email`. */
64
+ detail?: string;
65
+ account?: AuthStatusAccount;
66
+ /** Vendor-namespaced extras, e.g. `{claudeCode: {...}}`. */
67
+ vendor?: Record<string, unknown>;
68
+ };
69
+ export type AuthStatusUpdateNotification = {
70
+ authStatus: AuthStatus;
71
+ };
72
+ /** Advertisement carried in `agentCapabilities._meta.authStatus`, never a
73
+ * status payload: its mere presence means "this agent pushes its identity",
74
+ * and it stays empty. Informational — a client that never saw it still
75
+ * accepts `_auth/status_update`, because there is nothing to ask for. */
76
+ export type AuthStatusCapability = Record<string, never>;
77
+ export declare function authStatusCapability(): AuthStatusCapability;
78
+ /** Shape of `claude auth status --json`. Absent fields are omitted, not null,
79
+ * and the logged-out case exits 1 while still printing valid JSON. */
80
+ export type CliAuthStatus = {
81
+ loggedIn?: boolean;
82
+ authMethod?: string;
83
+ apiProvider?: string;
84
+ apiKeySource?: string;
85
+ email?: string;
86
+ orgId?: string;
87
+ orgName?: string;
88
+ subscriptionType?: string;
89
+ };
90
+ export declare function notLoggedInAuthStatus(): AuthStatus;
91
+ /** ACP-level gateway auth (`authenticate` with `gateway`/`gateway-bedrock`).
92
+ * The gateway owns the credentials, so it wins over anything the CLI reports. */
93
+ export declare function gatewayAuthStatus(baseUrl?: string): AuthStatus;
94
+ /**
95
+ * Does this `AccountInfo` say anything about the identity?
96
+ *
97
+ * The SDK always fills `apiProvider`, but the identity fields come from the
98
+ * claude.ai profile: with an `apiKeyHelper` and no subscription login the
99
+ * session reports `{apiProvider: "firstParty"}` and nothing else. That is "no
100
+ * information", not "logged out" — an empty read must never be mapped to
101
+ * `none`, or it destroys what the CLI probe already established.
102
+ *
103
+ * `tokenSource` is deliberately not a signal: the CLI sets it to the OAuth
104
+ * token's origin (`CLAUDE_CODE_OAUTH_TOKEN`,
105
+ * `CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR`, …), never to a key source, so it
106
+ * cannot attribute a key and carries no plan of its own.
107
+ */
108
+ export declare function accountInfoHasIdentitySignal(account: AccountInfo | undefined): boolean;
109
+ /** Session-time mapping from the SDK's `AccountInfo`, the richest source when
110
+ * it is populated. Returns `undefined` when the account carries no identity
111
+ * signal, so callers keep the status they already know. */
112
+ export declare function fromAccountInfo(account: AccountInfo | undefined): AuthStatus | undefined;
113
+ /** Pre-session mapping from `claude auth status --json` stdout. Returns
114
+ * `undefined` when the output is not the expected JSON object, so callers can
115
+ * report "not known" instead of guessing. */
116
+ export declare function fromCliStatus(stdout: string): AuthStatus | undefined;
117
+ /**
118
+ * Do two payloads describe the same login? Compared on the field that
119
+ * identifies each kind — the key source for `api_key`, the email for
120
+ * `account`, the kind alone for the rest, which carry no per-identity key.
121
+ */
122
+ export declare function sameIdentity(a: AuthStatus, b: AuthStatus): boolean;
123
+ /**
124
+ * Folds a fresh read into what is already known. Sources differ in richness for
125
+ * the very same login: the SDK's session `AccountInfo` can carry an
126
+ * organization the CLI probe omits. So when the read describes the same
127
+ * identity, its values win field by field but the fields it lacks are kept;
128
+ * when it describes a different identity, it replaces the old one wholesale.
129
+ */
130
+ export declare function mergeAuthStatus(previous: AuthStatus | undefined, next: AuthStatus): AuthStatus;
131
+ /**
132
+ * Do two payloads carry the same information? Unlike {@link sameIdentity},
133
+ * which asks "is this the same login", this compares every rendered field, so
134
+ * a richer read of one login is NOT the same. Callers use it to suppress a
135
+ * push that would tell the client nothing new.
136
+ *
137
+ * `a` undefined means "nothing reported yet", which never equals a payload.
138
+ */
139
+ export declare function sameAuthStatus(a: AuthStatus | undefined, b: AuthStatus): boolean;
140
+ //# sourceMappingURL=auth-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-status.d.ts","sourceRoot":"","sources":["../src/auth-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAElE;uEACuE;AACvE,eAAO,MAAM,yBAAyB,wBAAwB,CAAC;AAE/D;6CAC6C;AAC7C,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,CAAC;AAErF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,cAAc,CAAC;IACrB;4DACwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd;4EACwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IAAE,UAAU,EAAE,UAAU,CAAA;CAAE,CAAC;AAEtE;;;0EAG0E;AAC1E,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAEzD,wBAAgB,oBAAoB,IAAI,oBAAoB,CAE3D;AAED;uEACuE;AACvE,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAkBF,wBAAgB,qBAAqB,IAAI,UAAU,CAElD;AAED;kFACkF;AAClF,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,UAAU,CAc9D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAOtF;AAED;;4DAE4D;AAC5D,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAWxF;AAED;;8CAE8C;AAC9C,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CA4BpE;AA6DD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAYlE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,UAAU,GAAG,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,UAAU,CAe9F;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,UAAU,GAAG,SAAS,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAgBhF"}
@@ -0,0 +1,271 @@
1
+ /**
2
+ * `authStatus` — interim `_meta`-based ACP extension that reports which auth
3
+ * identity the agent process itself uses (Claude subscription, API key,
4
+ * gateway, external cloud credentials, or nothing).
5
+ *
6
+ * Push only. One carrier, connection-scoped (never per session): the
7
+ * notification `_auth/status_update` → `{authStatus}`. The client never asks;
8
+ * there is no request method. The empty `agentCapabilities._meta.authStatus`
9
+ * marker means "this agent pushes its identity", nothing more.
10
+ *
11
+ * The agent reads the identity at these moments:
12
+ * - `initialize`, with an asynchronous CLI probe. Nothing waits for it, and its
13
+ * result is the first push of the connection;
14
+ * - a completed `authenticate` or a `logout`;
15
+ * - each `createSession`, from the session account. A refused session reports
16
+ * too: the client must know which account was refused;
17
+ * - each turn the `--hide-claude-auth` guard checks. The guard reads the
18
+ * account anyway, so this costs nothing;
19
+ * - the START of each user prompt, with an asynchronous CLI probe. This finds a
20
+ * login or a logout done in another terminal, including the sign-in a client
21
+ * performs after a refusal, because the retried prompt is a new prompt.
22
+ * Nothing waits for it and the push goes out mid-turn if that is when the
23
+ * read lands.
24
+ *
25
+ * A probe whose kind differs from a live session's account marks that session
26
+ * for recreation under `--hide-claude-auth`. The mark is a flag, consumed at
27
+ * the next turn boundary: a running turn is never interrupted. The probe judges
28
+ * nothing itself — the next turn runs a new `initialize`, and the creation
29
+ * guard decides on the account it reports.
30
+ *
31
+ * A read does not always cause a push. The agent sends `_auth/status_update`
32
+ * only when the payload is different from the last one it sent.
33
+ *
34
+ * "Cannot determine" is expressed by silence: an agent that cannot read its own
35
+ * identity pushes nothing, and the client shows "not reported". A known
36
+ * logged-out state is a payload of its own (`kind: "none"`), which is what
37
+ * distinguishes the two.
38
+ *
39
+ * Reporting only — there is no write path in v1. When the upstream RFD lands,
40
+ * the same payload moves from `_meta` to first-class fields and this module
41
+ * is retired.
42
+ */
43
+ /** Change notification, agent → client. Fire and forget: clients that do not
44
+ * know the method drop it silently, so it is sent unconditionally. */
45
+ export const AUTH_STATUS_UPDATE_METHOD = "_auth/status_update";
46
+ /** Upper bound on one `claude auth status --json` run, so a hung CLI cannot
47
+ * wedge every later probe that joins it. */
48
+ export const AUTH_STATUS_PROBE_TIMEOUT_MS = 5000;
49
+ export function authStatusCapability() {
50
+ return {};
51
+ }
52
+ const NOT_LOGGED_IN = {
53
+ kind: "none",
54
+ label: "Not logged in",
55
+ };
56
+ /** Display names for the non-firstParty backends, where auth lives outside the
57
+ * agent (AWS credentials, gcloud ADC, …). */
58
+ const EXTERNAL_PROVIDER_LABELS = {
59
+ bedrock: "AWS Bedrock",
60
+ vertex: "Google Vertex AI",
61
+ foundry: "Azure AI Foundry",
62
+ anthropicAws: "Anthropic on AWS",
63
+ anthropicGoogleCloud: "Anthropic on Google Cloud",
64
+ mantle: "Mantle",
65
+ };
66
+ export function notLoggedInAuthStatus() {
67
+ return { ...NOT_LOGGED_IN };
68
+ }
69
+ /** ACP-level gateway auth (`authenticate` with `gateway`/`gateway-bedrock`).
70
+ * The gateway owns the credentials, so it wins over anything the CLI reports. */
71
+ export function gatewayAuthStatus(baseUrl) {
72
+ let host;
73
+ if (baseUrl) {
74
+ try {
75
+ host = new URL(baseUrl).host;
76
+ }
77
+ catch {
78
+ host = baseUrl;
79
+ }
80
+ }
81
+ return {
82
+ kind: "gateway",
83
+ label: "Custom model gateway",
84
+ ...(host ? { detail: host } : {}),
85
+ };
86
+ }
87
+ /**
88
+ * Does this `AccountInfo` say anything about the identity?
89
+ *
90
+ * The SDK always fills `apiProvider`, but the identity fields come from the
91
+ * claude.ai profile: with an `apiKeyHelper` and no subscription login the
92
+ * session reports `{apiProvider: "firstParty"}` and nothing else. That is "no
93
+ * information", not "logged out" — an empty read must never be mapped to
94
+ * `none`, or it destroys what the CLI probe already established.
95
+ *
96
+ * `tokenSource` is deliberately not a signal: the CLI sets it to the OAuth
97
+ * token's origin (`CLAUDE_CODE_OAUTH_TOKEN`,
98
+ * `CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR`, …), never to a key source, so it
99
+ * cannot attribute a key and carries no plan of its own.
100
+ */
101
+ export function accountInfoHasIdentitySignal(account) {
102
+ if (!account)
103
+ return false;
104
+ return Boolean(account.apiKeySource ||
105
+ account.subscriptionType ||
106
+ (account.apiProvider && account.apiProvider !== "firstParty"));
107
+ }
108
+ /** Session-time mapping from the SDK's `AccountInfo`, the richest source when
109
+ * it is populated. Returns `undefined` when the account carries no identity
110
+ * signal, so callers keep the status they already know. */
111
+ export function fromAccountInfo(account) {
112
+ if (!account || !accountInfoHasIdentitySignal(account)) {
113
+ return undefined;
114
+ }
115
+ return mapAuthFields({
116
+ apiProvider: account.apiProvider,
117
+ subscriptionType: account.subscriptionType,
118
+ apiKeySource: account.apiKeySource,
119
+ email: account.email,
120
+ organization: account.organization,
121
+ });
122
+ }
123
+ /** Pre-session mapping from `claude auth status --json` stdout. Returns
124
+ * `undefined` when the output is not the expected JSON object, so callers can
125
+ * report "not known" instead of guessing. */
126
+ export function fromCliStatus(stdout) {
127
+ let parsed;
128
+ try {
129
+ parsed = JSON.parse(stdout);
130
+ }
131
+ catch {
132
+ return undefined;
133
+ }
134
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
135
+ return undefined;
136
+ }
137
+ const status = parsed;
138
+ if (typeof status.loggedIn !== "boolean") {
139
+ return undefined;
140
+ }
141
+ // `loggedIn` tracks the claude.ai login only. On a 3P backend (Bedrock,
142
+ // Vertex, …) it is false while the credentials live outside the agent, so the
143
+ // backend must decide the kind before the logged-out shortcut runs.
144
+ const externalBackend = Boolean(status.apiProvider && status.apiProvider !== "firstParty");
145
+ if (!externalBackend && !status.loggedIn && !status.apiKeySource) {
146
+ return notLoggedInAuthStatus();
147
+ }
148
+ return mapAuthFields({
149
+ apiProvider: status.apiProvider,
150
+ subscriptionType: status.subscriptionType,
151
+ apiKeySource: status.apiKeySource,
152
+ email: status.email,
153
+ organization: status.orgName,
154
+ });
155
+ }
156
+ /** Single mapping table shared by both sources; see the `kind`/`label` table in
157
+ * `extension-authstatus-meta.md`. Order matters: a 3P backend or a gateway
158
+ * decides the kind before any firstParty identity fields are considered, and
159
+ * an API key outranks a stored subscription login.
160
+ *
161
+ * Both can be reported at once — an `apiKeyHelper` in settings.json next to a
162
+ * claude.ai OAuth login yields `apiKeySource` AND `subscriptionType`. Claude
163
+ * Code's authentication precedence puts `apiKeyHelper` above the `/login`
164
+ * subscription, so the key is what actually pays for the turn and is what we
165
+ * report. */
166
+ function mapAuthFields(fields) {
167
+ const { apiProvider, subscriptionType, apiKeySource, email, organization } = fields;
168
+ if (apiProvider === "gateway") {
169
+ return gatewayAuthStatus();
170
+ }
171
+ if (apiProvider && apiProvider !== "firstParty") {
172
+ return {
173
+ kind: "external",
174
+ label: EXTERNAL_PROVIDER_LABELS[apiProvider] ?? apiProvider,
175
+ };
176
+ }
177
+ if (apiKeySource) {
178
+ return {
179
+ kind: "api_key",
180
+ label: "Anthropic API key",
181
+ // The source ("apiKeyHelper", "env", …) is the specifics line.
182
+ detail: apiKeySource,
183
+ };
184
+ }
185
+ if (subscriptionType) {
186
+ // `account.plan` keeps the raw vendor string; only the label is titled.
187
+ const account = { plan: subscriptionType };
188
+ if (email)
189
+ account.email = email;
190
+ if (organization)
191
+ account.organization = organization;
192
+ return {
193
+ kind: "account",
194
+ label: planLabel(subscriptionType),
195
+ // No detail: the client falls back to `account.email` for line 2.
196
+ account,
197
+ };
198
+ }
199
+ return notLoggedInAuthStatus();
200
+ }
201
+ /** Names the plan with exactly one "Claude". A newer CLI already reports
202
+ * "Claude Max", an older one reports "max", and both must read "Claude Max".
203
+ * Each word is title-cased; a word that already carries capitals is kept. */
204
+ function planLabel(plan) {
205
+ const titled = plan.replace(/\S+/g, (word) => word.charAt(0).toUpperCase() + word.slice(1));
206
+ return /^claude(\s|$)/i.test(plan) ? titled : `Claude ${titled}`;
207
+ }
208
+ /**
209
+ * Do two payloads describe the same login? Compared on the field that
210
+ * identifies each kind — the key source for `api_key`, the email for
211
+ * `account`, the kind alone for the rest, which carry no per-identity key.
212
+ */
213
+ export function sameIdentity(a, b) {
214
+ if (a.kind !== b.kind) {
215
+ return false;
216
+ }
217
+ switch (a.kind) {
218
+ case "api_key":
219
+ return a.detail === b.detail;
220
+ case "account":
221
+ return (a.account?.email ?? "") === (b.account?.email ?? "");
222
+ default:
223
+ return true;
224
+ }
225
+ }
226
+ /**
227
+ * Folds a fresh read into what is already known. Sources differ in richness for
228
+ * the very same login: the SDK's session `AccountInfo` can carry an
229
+ * organization the CLI probe omits. So when the read describes the same
230
+ * identity, its values win field by field but the fields it lacks are kept;
231
+ * when it describes a different identity, it replaces the old one wholesale.
232
+ */
233
+ export function mergeAuthStatus(previous, next) {
234
+ if (!previous || !sameIdentity(previous, next)) {
235
+ return next;
236
+ }
237
+ const merged = { ...next };
238
+ if (merged.detail === undefined && previous.detail !== undefined) {
239
+ merged.detail = previous.detail;
240
+ }
241
+ if (previous.account || next.account) {
242
+ merged.account = { ...previous.account, ...next.account };
243
+ }
244
+ if (merged.vendor === undefined && previous.vendor !== undefined) {
245
+ merged.vendor = previous.vendor;
246
+ }
247
+ return merged;
248
+ }
249
+ /**
250
+ * Do two payloads carry the same information? Unlike {@link sameIdentity},
251
+ * which asks "is this the same login", this compares every rendered field, so
252
+ * a richer read of one login is NOT the same. Callers use it to suppress a
253
+ * push that would tell the client nothing new.
254
+ *
255
+ * `a` undefined means "nothing reported yet", which never equals a payload.
256
+ */
257
+ export function sameAuthStatus(a, b) {
258
+ if (!a) {
259
+ return false;
260
+ }
261
+ return (a.kind === b.kind &&
262
+ a.label === b.label &&
263
+ a.detail === b.detail &&
264
+ a.account?.email === b.account?.email &&
265
+ a.account?.organization === b.account?.organization &&
266
+ a.account?.plan === b.account?.plan &&
267
+ // Vendor extras are an open bag, so compare them structurally. Both sides
268
+ // are built by this module from the same key order, so a string compare is
269
+ // enough and costs nothing on the usual `undefined`/`undefined` pair.
270
+ JSON.stringify(a.vendor) === JSON.stringify(b.vendor));
271
+ }
@@ -0,0 +1,18 @@
1
+ export declare const CONTEXT_COMPACTION_META_KEY = "contextCompaction";
2
+ export declare const CONTEXT_COMPACTION_META_VERSION = 1;
3
+ export type ContextCompactionTrigger = "manual" | "automatic";
4
+ export interface ContextCompactionMetadata {
5
+ version: typeof CONTEXT_COMPACTION_META_VERSION;
6
+ trigger?: ContextCompactionTrigger;
7
+ preTokens?: number;
8
+ postTokens?: number;
9
+ durationMs?: number;
10
+ error?: string;
11
+ }
12
+ /**
13
+ * Provider-neutral metadata for a synthetic ACP context-compaction tool call.
14
+ * The standard toolCallId and status fields own lifecycle identity and phase;
15
+ * this extension carries only compaction-specific facts.
16
+ */
17
+ export declare function createContextCompactionMeta(metadata?: Omit<ContextCompactionMetadata, "version">): Record<typeof CONTEXT_COMPACTION_META_KEY, ContextCompactionMetadata>;
18
+ //# sourceMappingURL=context-compaction-meta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-compaction-meta.d.ts","sourceRoot":"","sources":["../src/context-compaction-meta.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,2BAA2B,sBAAsB,CAAC;AAC/D,eAAO,MAAM,+BAA+B,IAAI,CAAC;AAEjD,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE9D,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,OAAO,+BAA+B,CAAC;IAChD,OAAO,CAAC,EAAE,wBAAwB,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,GAAE,IAAI,CAAC,yBAAyB,EAAE,SAAS,CAAM,GACxD,MAAM,CAAC,OAAO,2BAA2B,EAAE,yBAAyB,CAAC,CAOvE"}
@@ -0,0 +1,15 @@
1
+ export const CONTEXT_COMPACTION_META_KEY = "contextCompaction";
2
+ export const CONTEXT_COMPACTION_META_VERSION = 1;
3
+ /**
4
+ * Provider-neutral metadata for a synthetic ACP context-compaction tool call.
5
+ * The standard toolCallId and status fields own lifecycle identity and phase;
6
+ * this extension carries only compaction-specific facts.
7
+ */
8
+ export function createContextCompactionMeta(metadata = {}) {
9
+ return {
10
+ [CONTEXT_COMPACTION_META_KEY]: {
11
+ version: CONTEXT_COMPACTION_META_VERSION,
12
+ ...metadata,
13
+ },
14
+ };
15
+ }
@@ -0,0 +1,43 @@
1
+ import { SessionNotification } from "@agentclientprotocol/sdk";
2
+ import { ContextCompactionMetadata } from "./context-compaction-meta.js";
3
+ type CompactionStatus = "completed" | "failed";
4
+ type CompactionState = {
5
+ toolCallId: string;
6
+ terminalStatus?: CompactionStatus;
7
+ heartbeatSent: boolean;
8
+ };
9
+ type SendUpdate = (notification: SessionNotification) => Promise<void>;
10
+ /**
11
+ * Translates Claude's compaction signals into one idempotent ACP tool lifecycle.
12
+ *
13
+ * The SDK can duplicate terminal compact_result messages and can omit the
14
+ * opening status on replay. State therefore lives until the owning turn's
15
+ * result (or abort), while a new compacting status after a terminal outcome
16
+ * starts a fresh lifecycle.
17
+ */
18
+ export declare class ContextCompactionLifecycle {
19
+ private readonly sendUpdate;
20
+ private activeCompaction;
21
+ private outputDelivered;
22
+ private duplicateErrorOutput;
23
+ constructor(sendUpdate: SendUpdate);
24
+ get hasDeliveredOutput(): boolean;
25
+ reset(): void;
26
+ /**
27
+ * Claude also emits a failed manual compaction's error as local-command
28
+ * stdout. Consume that one duplicate after the tool lifecycle carried it,
29
+ * without hiding unrelated command output.
30
+ */
31
+ consumeDuplicateErrorOutput(content: string): boolean;
32
+ start(sessionId: string, toolCallId: string): Promise<CompactionState>;
33
+ heartbeat(sessionId: string, fallbackId: string): Promise<void>;
34
+ finish(sessionId: string, fallbackId: string, status: CompactionStatus, metadata?: Omit<ContextCompactionMetadata, "version">, enrichTerminal?: boolean): Promise<void>;
35
+ }
36
+ export declare function contextCompactionMetadataFromBoundary(compactMetadata: {
37
+ trigger: "manual" | "auto";
38
+ pre_tokens: number;
39
+ post_tokens?: number;
40
+ duration_ms?: number;
41
+ }): Omit<ContextCompactionMetadata, "version">;
42
+ export {};
43
+ //# sourceMappingURL=context-compaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-compaction.d.ts","sourceRoot":"","sources":["../src/context-compaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EACL,yBAAyB,EAE1B,MAAM,8BAA8B,CAAC;AAEtC,KAAK,gBAAgB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE/C,KAAK,eAAe,GAAG;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,aAAa,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,KAAK,UAAU,GAAG,CAAC,YAAY,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvE;;;;;;;GAOG;AACH,qBAAa,0BAA0B;IAKzB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAJvC,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,oBAAoB,CAAqB;gBAEpB,UAAU,EAAE,UAAU;IAEnD,IAAI,kBAAkB,IAAI,OAAO,CAEhC;IAED,KAAK,IAAI,IAAI;IAMb;;;;OAIG;IACH,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAW/C,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAqBtE,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB/D,MAAM,CACV,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,gBAAgB,EACxB,QAAQ,GAAE,IAAI,CAAC,yBAAyB,EAAE,SAAS,CAAM,EACzD,cAAc,UAAQ,GACrB,OAAO,CAAC,IAAI,CAAC;CAoDjB;AAED,wBAAgB,qCAAqC,CAAC,eAAe,EAAE;IACrE,OAAO,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,IAAI,CAAC,yBAAyB,EAAE,SAAS,CAAC,CAW7C"}
@@ -0,0 +1,152 @@
1
+ import { createContextCompactionMeta, } from "./context-compaction-meta.js";
2
+ /**
3
+ * Translates Claude's compaction signals into one idempotent ACP tool lifecycle.
4
+ *
5
+ * The SDK can duplicate terminal compact_result messages and can omit the
6
+ * opening status on replay. State therefore lives until the owning turn's
7
+ * result (or abort), while a new compacting status after a terminal outcome
8
+ * starts a fresh lifecycle.
9
+ */
10
+ export class ContextCompactionLifecycle {
11
+ sendUpdate;
12
+ activeCompaction;
13
+ outputDelivered = false;
14
+ duplicateErrorOutput;
15
+ constructor(sendUpdate) {
16
+ this.sendUpdate = sendUpdate;
17
+ }
18
+ get hasDeliveredOutput() {
19
+ return this.outputDelivered;
20
+ }
21
+ reset() {
22
+ this.activeCompaction = undefined;
23
+ this.outputDelivered = false;
24
+ this.duplicateErrorOutput = undefined;
25
+ }
26
+ /**
27
+ * Claude also emits a failed manual compaction's error as local-command
28
+ * stdout. Consume that one duplicate after the tool lifecycle carried it,
29
+ * without hiding unrelated command output.
30
+ */
31
+ consumeDuplicateErrorOutput(content) {
32
+ if (this.duplicateErrorOutput === undefined ||
33
+ content.trim() !== this.duplicateErrorOutput.trim()) {
34
+ return false;
35
+ }
36
+ this.duplicateErrorOutput = undefined;
37
+ return true;
38
+ }
39
+ async start(sessionId, toolCallId) {
40
+ if (this.activeCompaction && !this.activeCompaction.terminalStatus) {
41
+ return this.activeCompaction;
42
+ }
43
+ this.activeCompaction = { toolCallId, heartbeatSent: false };
44
+ this.outputDelivered = true;
45
+ await this.sendUpdate({
46
+ sessionId,
47
+ update: {
48
+ sessionUpdate: "tool_call",
49
+ toolCallId,
50
+ title: "Compact conversation",
51
+ kind: "think",
52
+ status: "in_progress",
53
+ _meta: compactionToolMeta(),
54
+ },
55
+ });
56
+ return this.activeCompaction;
57
+ }
58
+ async heartbeat(sessionId, fallbackId) {
59
+ const state = this.activeCompaction ?? (await this.start(sessionId, fallbackId));
60
+ if (state.terminalStatus || state.heartbeatSent)
61
+ return;
62
+ state.heartbeatSent = true;
63
+ await this.sendUpdate({
64
+ sessionId,
65
+ update: {
66
+ sessionUpdate: "tool_call_update",
67
+ toolCallId: state.toolCallId,
68
+ status: "in_progress",
69
+ _meta: compactionToolMeta(),
70
+ },
71
+ });
72
+ }
73
+ async finish(sessionId, fallbackId, status, metadata = {}, enrichTerminal = false) {
74
+ const rawOutput = Object.keys(metadata).length > 0 ? metadata : undefined;
75
+ if (!this.activeCompaction) {
76
+ this.activeCompaction = {
77
+ toolCallId: fallbackId,
78
+ heartbeatSent: false,
79
+ terminalStatus: status,
80
+ };
81
+ this.outputDelivered = true;
82
+ if (status === "failed" && metadata.error) {
83
+ this.duplicateErrorOutput = metadata.error;
84
+ }
85
+ await this.sendUpdate({
86
+ sessionId,
87
+ update: {
88
+ sessionUpdate: "tool_call",
89
+ toolCallId: fallbackId,
90
+ title: "Compact conversation",
91
+ kind: "think",
92
+ status,
93
+ ...(status === "failed" && metadata.error
94
+ ? { content: [compactionErrorContent(metadata.error)] }
95
+ : {}),
96
+ ...(rawOutput ? { rawOutput } : {}),
97
+ _meta: compactionToolMeta(metadata),
98
+ },
99
+ });
100
+ return;
101
+ }
102
+ const state = this.activeCompaction;
103
+ if (state.terminalStatus && !enrichTerminal)
104
+ return;
105
+ const firstTerminal = state.terminalStatus === undefined;
106
+ if (firstTerminal)
107
+ state.terminalStatus = status;
108
+ if (status === "failed" && metadata.error) {
109
+ this.duplicateErrorOutput = metadata.error;
110
+ }
111
+ await this.sendUpdate({
112
+ sessionId,
113
+ update: {
114
+ sessionUpdate: "tool_call_update",
115
+ toolCallId: state.toolCallId,
116
+ ...(firstTerminal ? { status } : {}),
117
+ ...(status === "failed" && metadata.error
118
+ ? { content: [compactionErrorContent(metadata.error)] }
119
+ : {}),
120
+ ...(rawOutput ? { rawOutput } : {}),
121
+ _meta: compactionToolMeta(metadata),
122
+ },
123
+ });
124
+ }
125
+ }
126
+ export function contextCompactionMetadataFromBoundary(compactMetadata) {
127
+ return {
128
+ trigger: compactMetadata.trigger === "auto" ? "automatic" : "manual",
129
+ preTokens: compactMetadata.pre_tokens,
130
+ ...(compactMetadata.post_tokens !== undefined
131
+ ? { postTokens: compactMetadata.post_tokens }
132
+ : {}),
133
+ ...(compactMetadata.duration_ms !== undefined
134
+ ? { durationMs: compactMetadata.duration_ms }
135
+ : {}),
136
+ };
137
+ }
138
+ function compactionToolMeta(metadata = {}) {
139
+ return {
140
+ ...createContextCompactionMeta(metadata),
141
+ claudeCode: { toolName: "compact" },
142
+ };
143
+ }
144
+ function compactionErrorContent(error) {
145
+ return {
146
+ type: "content",
147
+ content: {
148
+ type: "text",
149
+ text: `Compaction failed: ${error}`,
150
+ },
151
+ };
152
+ }
@@ -1,6 +1,9 @@
1
1
  import { ForkSessionRequest, ForkSessionResponse } from "@agentclientprotocol/sdk";
2
2
  type ForkSessionDependencies = {
3
3
  liveMessageIdToUuid?: ReadonlyMap<string, string>;
4
+ logger?: {
5
+ log: (...args: unknown[]) => void;
6
+ };
4
7
  messageIdForGrouping: (message: {
5
8
  type?: string;
6
9
  uuid?: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"fork-session.d.ts","sourceRoot":"","sources":["../src/fork-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAgB,MAAM,0BAA0B,CAAC;AAkBjG,KAAK,uBAAuB,GAAG;IAC7B,mBAAmB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClD,oBAAoB,EAAE,CAAC,OAAO,EAAE;QAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,KAAK,MAAM,GAAG,SAAS,CAAC;CAC1B,CAAC;AAeF,wBAAsB,WAAW,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,YAAY,EAAE,uBAAuB,GACpC,OAAO,CAAC,mBAAmB,CAAC,CAoC9B"}
1
+ {"version":3,"file":"fork-session.d.ts","sourceRoot":"","sources":["../src/fork-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAgB,MAAM,0BAA0B,CAAC;AA0BjG,KAAK,uBAAuB,GAAG;IAC7B,mBAAmB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE;QAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;KAAE,CAAC;IAC/C,oBAAoB,EAAE,CAAC,OAAO,EAAE;QAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,KAAK,MAAM,GAAG,SAAS,CAAC;CAC1B,CAAC;AA+IF,wBAAsB,WAAW,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,YAAY,EAAE,uBAAuB,GACpC,OAAO,CAAC,mBAAmB,CAAC,CAsD9B"}