@lucascouts/claude-agent-acp-plus 0.11.1 → 0.13.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.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The versioned `_meta` extension that carries context-compaction facts on the
3
+ * synthetic ACP tool call (story 010, R2.1/D3).
4
+ *
5
+ * Why an extension rather than the protocol's own variant: ACP 1.4 does define
6
+ * a `compaction_update`, but the Rust crate the packaged Zed links
7
+ * (`agent-client-protocol 2.0.0`) carries no `compaction` symbol at all, so
8
+ * that variant cannot be deserialised no matter what this adapter sends. The
9
+ * reachable pattern is the one downstream patch `0011` already proves: emit a
10
+ * `_meta` key no ACP variant claims, and let a downstream patch read it. The
11
+ * tool call renders with `ToolKind::Think`'s icon even with no patch present,
12
+ * so R2.1 ships and works on its own.
13
+ *
14
+ * Why the version field is not optional (D3): it is what makes the two halves
15
+ * independently releasable in BOTH directions — a newer adapter against an
16
+ * older Zed degrades to generic tool rendering, and an older adapter against a
17
+ * newer Zed simply never sends the key. A payload with no version is a payload
18
+ * a reader cannot degrade from, so it can only be guessed at.
19
+ *
20
+ * Kept self-contained — no import from `acp-agent.ts`, mirroring
21
+ * `thinking-option.ts` and `rewind-command.ts`. The reason is merge cost:
22
+ * `acp-agent.ts` is a ~9,700-line file that changes upstream almost daily, so
23
+ * every coupling to it is paid again at each sync.
24
+ */
25
+ /** The `_meta` key the downstream Zed patch looks for. */
26
+ export declare const CONTEXT_COMPACTION_META_KEY = "contextCompaction";
27
+ /**
28
+ * Schema version of the payload under {@link CONTEXT_COMPACTION_META_KEY}.
29
+ * Bump it whenever a field's meaning changes; a reader that does not know the
30
+ * value it receives must fall back to generic tool rendering rather than
31
+ * interpret unknown fields.
32
+ */
33
+ export declare const CONTEXT_COMPACTION_META_VERSION = 1;
34
+ /**
35
+ * Why the compaction ran: `"manual"` for a user-issued `/compact`,
36
+ * `"automatic"` when the SDK compacted on its own to stay inside the window.
37
+ * Provider-neutral by design — the SDK spells the automatic case `"auto"`.
38
+ */
39
+ export type ContextCompactionTrigger = "manual" | "automatic";
40
+ /**
41
+ * Compaction-specific facts carried alongside the tool call. Every field
42
+ * except `version` is optional because the SDK frame that produced the event
43
+ * may not carry it: a terminal-only `status` message has no token counts, and
44
+ * an older CLI's `compact_boundary` omits `post_tokens`/`duration_ms`.
45
+ */
46
+ export interface ContextCompactionMetadata {
47
+ /** Always {@link CONTEXT_COMPACTION_META_VERSION}; see D3 above. */
48
+ version: typeof CONTEXT_COMPACTION_META_VERSION;
49
+ /** Manual `/compact` or an automatic window-pressure compaction. */
50
+ trigger?: ContextCompactionTrigger;
51
+ /** Tokens occupying the context window before compaction. */
52
+ preTokens?: number;
53
+ /** Tokens occupying it after — absent on CLIs that don't report it. */
54
+ postTokens?: number;
55
+ /** How long the compaction took, in milliseconds. */
56
+ durationMs?: number;
57
+ /** Failure reason, present only on a `failed` lifecycle. */
58
+ error?: string;
59
+ }
60
+ /**
61
+ * Provider-neutral metadata for a synthetic ACP context-compaction tool call.
62
+ *
63
+ * The standard `toolCallId` and `status` fields own lifecycle identity and
64
+ * phase; this extension deliberately carries only compaction-specific facts,
65
+ * so a client that ignores `_meta` entirely still sees a correct tool
66
+ * lifecycle.
67
+ *
68
+ * @param metadata The facts known at this point in the lifecycle; the version
69
+ * is stamped here rather than by each call site, so no caller can emit an
70
+ * unversioned payload.
71
+ */
72
+ export declare function createContextCompactionMeta(metadata?: Omit<ContextCompactionMetadata, "version">): Record<typeof CONTEXT_COMPACTION_META_KEY, ContextCompactionMetadata>;
73
+ //# 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;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,0DAA0D;AAC1D,eAAO,MAAM,2BAA2B,sBAAsB,CAAC;AAE/D;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,IAAI,CAAC;AAEjD;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,oEAAoE;IACpE,OAAO,EAAE,OAAO,+BAA+B,CAAC;IAChD,oEAAoE;IACpE,OAAO,CAAC,EAAE,wBAAwB,CAAC;IACnC,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,GAAE,IAAI,CAAC,yBAAyB,EAAE,SAAS,CAAM,GACxD,MAAM,CAAC,OAAO,2BAA2B,EAAE,yBAAyB,CAAC,CAOvE"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The versioned `_meta` extension that carries context-compaction facts on the
3
+ * synthetic ACP tool call (story 010, R2.1/D3).
4
+ *
5
+ * Why an extension rather than the protocol's own variant: ACP 1.4 does define
6
+ * a `compaction_update`, but the Rust crate the packaged Zed links
7
+ * (`agent-client-protocol 2.0.0`) carries no `compaction` symbol at all, so
8
+ * that variant cannot be deserialised no matter what this adapter sends. The
9
+ * reachable pattern is the one downstream patch `0011` already proves: emit a
10
+ * `_meta` key no ACP variant claims, and let a downstream patch read it. The
11
+ * tool call renders with `ToolKind::Think`'s icon even with no patch present,
12
+ * so R2.1 ships and works on its own.
13
+ *
14
+ * Why the version field is not optional (D3): it is what makes the two halves
15
+ * independently releasable in BOTH directions — a newer adapter against an
16
+ * older Zed degrades to generic tool rendering, and an older adapter against a
17
+ * newer Zed simply never sends the key. A payload with no version is a payload
18
+ * a reader cannot degrade from, so it can only be guessed at.
19
+ *
20
+ * Kept self-contained — no import from `acp-agent.ts`, mirroring
21
+ * `thinking-option.ts` and `rewind-command.ts`. The reason is merge cost:
22
+ * `acp-agent.ts` is a ~9,700-line file that changes upstream almost daily, so
23
+ * every coupling to it is paid again at each sync.
24
+ */
25
+ /** The `_meta` key the downstream Zed patch looks for. */
26
+ export const CONTEXT_COMPACTION_META_KEY = "contextCompaction";
27
+ /**
28
+ * Schema version of the payload under {@link CONTEXT_COMPACTION_META_KEY}.
29
+ * Bump it whenever a field's meaning changes; a reader that does not know the
30
+ * value it receives must fall back to generic tool rendering rather than
31
+ * interpret unknown fields.
32
+ */
33
+ export const CONTEXT_COMPACTION_META_VERSION = 1;
34
+ /**
35
+ * Provider-neutral metadata for a synthetic ACP context-compaction tool call.
36
+ *
37
+ * The standard `toolCallId` and `status` fields own lifecycle identity and
38
+ * phase; this extension deliberately carries only compaction-specific facts,
39
+ * so a client that ignores `_meta` entirely still sees a correct tool
40
+ * lifecycle.
41
+ *
42
+ * @param metadata The facts known at this point in the lifecycle; the version
43
+ * is stamped here rather than by each call site, so no caller can emit an
44
+ * unversioned payload.
45
+ */
46
+ export function createContextCompactionMeta(metadata = {}) {
47
+ return {
48
+ [CONTEXT_COMPACTION_META_KEY]: {
49
+ version: CONTEXT_COMPACTION_META_VERSION,
50
+ ...metadata,
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Context compaction as ONE idempotent ACP tool lifecycle (story 010, R2.1/R2.2).
3
+ *
4
+ * What this replaces: the adapter used to infer compaction from a
5
+ * `compactionInProgress` boolean and narrate it as assistant text
6
+ * ("Compacting…", "Compacting completed."). The flag existed only because, in
7
+ * the adapter's own comment, the SDK's two terminal `status` messages were
8
+ * "indistinguishable" — a guess, not a signal. Once the lifecycle is explicit
9
+ * the guess is not merely redundant, it double-reports: the tool call says the
10
+ * compaction finished and the banner says so again. So the inference is
11
+ * DELETED, not left inert (D4) — a flag that no longer decides anything is a
12
+ * thing the next reader has to prove is inert.
13
+ *
14
+ * What replaces it: a small state machine that folds every event shape the SDK
15
+ * can produce onto a single tool call.
16
+ *
17
+ * SDK event → lifecycle call
18
+ * ------------------------------------------- -----------------------------
19
+ * `status` = "compacting" → start() tool_call, in_progress
20
+ * `stream_event` compaction block/delta → heartbeat() tool_call_update, in_progress
21
+ * `status` with compact_result success/failed → finish() tool_call_update, terminal
22
+ * `compact_boundary` (token counts) → finish(enrich) tool_call_update, metadata only
23
+ * a terminal with no preceding "compacting" → finish() tool_call, terminal (standalone)
24
+ * a DUPLICATED terminal → finish() nothing
25
+ *
26
+ * The two hard cases pull in opposite directions and both are real:
27
+ *
28
+ * - The SDK duplicates the terminal `compact_result` message for a single
29
+ * failed compaction. Two terminals for one compaction must stay ONE report.
30
+ * - One model turn can legitimately compact more than once. Two genuine
31
+ * compactions must stay TWO distinguishable reports.
32
+ *
33
+ * The state machine separates them by phase rather than by identity: a
34
+ * terminal that arrives when the lifecycle has already terminated is a
35
+ * duplicate and changes nothing, while a fresh `compacting` status AFTER a
36
+ * terminal opens a new lifecycle with a new tool call id. A rule that instead
37
+ * suppressed by matching ids or payloads would satisfy one case by breaking
38
+ * the other.
39
+ *
40
+ * State lives until the owning turn's `result` (or an abort) rather than being
41
+ * cleared at each terminal, because the SDK can also omit the opening status
42
+ * on replay — the terminal has to be able to stand alone.
43
+ *
44
+ * Kept self-contained — no import from `acp-agent.ts`, mirroring
45
+ * `thinking-option.ts` and `rewind-command.ts`; what it needs from the adapter
46
+ * (the send chokepoint) is injected, which also leaves the whole state machine
47
+ * unit-testable without a live SDK session. The reason is merge cost:
48
+ * `acp-agent.ts` is a ~9,700-line file that changes upstream almost daily, so
49
+ * every coupling to it is paid again at each sync.
50
+ */
51
+ import { SessionNotification } from "@agentclientprotocol/sdk";
52
+ import { ContextCompactionMetadata } from "./context-compaction-meta.js";
53
+ /** The phases a compaction can END in. `in_progress` is not terminal. */
54
+ type CompactionStatus = "completed" | "failed";
55
+ /** One compaction's identity and phase. */
56
+ type CompactionState = {
57
+ /** The ACP tool call id every update for this compaction carries. */
58
+ toolCallId: string;
59
+ /** Set once the lifecycle has reported an outcome; the duplicate guard. */
60
+ terminalStatus?: CompactionStatus;
61
+ /** Stream heartbeats are collapsed to one keep-alive per lifecycle. */
62
+ heartbeatSent: boolean;
63
+ };
64
+ /**
65
+ * The adapter's send chokepoint, injected. Deliberately the ACP SDK's
66
+ * `SessionNotification` rather than an adapter-local alias, so this module
67
+ * needs nothing from `acp-agent.ts`.
68
+ */
69
+ type SendUpdate = (notification: SessionNotification) => Promise<void>;
70
+ /**
71
+ * Translates Claude's compaction signals into one idempotent ACP tool
72
+ * lifecycle. One instance per consumer; `reset()` at each turn boundary.
73
+ */
74
+ export declare class ContextCompactionLifecycle {
75
+ private readonly sendUpdate;
76
+ private activeCompaction;
77
+ private outputDelivered;
78
+ private duplicateErrorOutput;
79
+ constructor(sendUpdate: SendUpdate);
80
+ /**
81
+ * Whether this turn already showed the user something about a compaction.
82
+ *
83
+ * The adapter reads it for two decisions the deleted banners used to make
84
+ * implicitly, by counting as delivered assistant text: an echo-less turn
85
+ * that only compacted (e.g. `/compact`) must not have its result text
86
+ * re-emitted by the issue-#453 fallback, and a replayed synthetic
87
+ * local-command frame from an earlier compact attempt must not be forwarded
88
+ * on top of the lifecycle.
89
+ */
90
+ get hasDeliveredOutput(): boolean;
91
+ /** Return to the unstarted state; call at each turn boundary. */
92
+ reset(): void;
93
+ /**
94
+ * Claude also emits a failed manual compaction's error as local-command
95
+ * stdout. Consume that one duplicate after the tool lifecycle already
96
+ * carried it, without hiding unrelated command output.
97
+ *
98
+ * Matching is by exact (trimmed) text and consumes at most once, so a
99
+ * command that genuinely prints the same string later still reaches the
100
+ * client.
101
+ *
102
+ * @returns `true` when the caller should drop this content.
103
+ */
104
+ consumeDuplicateErrorOutput(content: string): boolean;
105
+ /**
106
+ * Open the tool call the client renders. Idempotent while a compaction is
107
+ * still running (a repeated `compacting` status re-uses the open call); a
108
+ * `compacting` that arrives AFTER a terminal starts a fresh lifecycle, which
109
+ * is how two real compactions in one turn stay two reports.
110
+ *
111
+ * @param toolCallId The SDK message uuid — replay-stable, so a re-delivered
112
+ * frame addresses the same tool call rather than inventing one.
113
+ */
114
+ start(sessionId: string, toolCallId: string): Promise<CompactionState>;
115
+ /**
116
+ * Keep the open call alive while compaction streams. Sent at most once per
117
+ * lifecycle: the deltas carry the generated summary, which stays internal to
118
+ * the agent, so repeating them would add noise and no information.
119
+ *
120
+ * @param fallbackId Used only when the opening status never arrived (replay).
121
+ */
122
+ heartbeat(sessionId: string, fallbackId: string): Promise<void>;
123
+ /**
124
+ * Report the outcome — exactly once per compaction.
125
+ *
126
+ * Three shapes converge here:
127
+ * - no lifecycle open (the SDK omitted the opening status on replay): emit a
128
+ * standalone terminal `tool_call`, so the event is reported rather than
129
+ * dropped for lack of a start;
130
+ * - the first terminal for an open lifecycle: a `tool_call_update` carrying
131
+ * the status;
132
+ * - a duplicate terminal: nothing at all.
133
+ *
134
+ * @param fallbackId Tool call id for the standalone shape.
135
+ * @param metadata Compaction facts; also emitted as `rawOutput` when non-empty,
136
+ * so a client that ignores `_meta` can still show something.
137
+ * @param enrichTerminal Allow a SECOND update on an already-terminal
138
+ * lifecycle that adds facts without re-reporting the outcome. Used by
139
+ * `compact_boundary`, which arrives after the terminal `status` and is the
140
+ * only frame carrying the token counts. The status field is omitted on that
141
+ * update, so the client still sees exactly one terminal transition.
142
+ */
143
+ finish(sessionId: string, fallbackId: string, status: CompactionStatus, metadata?: Omit<ContextCompactionMetadata, "version">, enrichTerminal?: boolean): Promise<void>;
144
+ /** Arm {@link consumeDuplicateErrorOutput} for the stdout copy Claude emits. */
145
+ private rememberDuplicateErrorOutput;
146
+ }
147
+ /**
148
+ * Map the SDK's `compact_boundary` metadata onto the provider-neutral names of
149
+ * {@link ContextCompactionMetadata}. Fields an older CLI omits stay omitted
150
+ * rather than becoming `undefined` keys, so `rawOutput` and `_meta` never
151
+ * advertise a number nobody reported.
152
+ */
153
+ export declare function contextCompactionMetadataFromBoundary(compactMetadata: {
154
+ trigger: "manual" | "auto";
155
+ pre_tokens: number;
156
+ post_tokens?: number;
157
+ duration_ms?: number;
158
+ }): Omit<ContextCompactionMetadata, "version">;
159
+ export {};
160
+ //# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EACL,yBAAyB,EAE1B,MAAM,8BAA8B,CAAC;AAEtC,yEAAyE;AACzE,KAAK,gBAAgB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE/C,2CAA2C;AAC3C,KAAK,eAAe,GAAG;IACrB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,uEAAuE;IACvE,aAAa,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF;;;;GAIG;AACH,KAAK,UAAU,GAAG,CAAC,YAAY,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvE;;;GAGG;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;;;;;;;;;OASG;IACH,IAAI,kBAAkB,IAAI,OAAO,CAEhC;IAED,iEAAiE;IACjE,KAAK,IAAI,IAAI;IAMb;;;;;;;;;;OAUG;IACH,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAWrD;;;;;;;;OAQG;IACG,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAqB5E;;;;;;OAMG;IACG,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBrE;;;;;;;;;;;;;;;;;;;OAmBG;IACG,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;IAgDhB,gFAAgF;IAChF,OAAO,CAAC,4BAA4B;CAQrC;AAED;;;;;GAKG;AACH,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,275 @@
1
+ /**
2
+ * Context compaction as ONE idempotent ACP tool lifecycle (story 010, R2.1/R2.2).
3
+ *
4
+ * What this replaces: the adapter used to infer compaction from a
5
+ * `compactionInProgress` boolean and narrate it as assistant text
6
+ * ("Compacting…", "Compacting completed."). The flag existed only because, in
7
+ * the adapter's own comment, the SDK's two terminal `status` messages were
8
+ * "indistinguishable" — a guess, not a signal. Once the lifecycle is explicit
9
+ * the guess is not merely redundant, it double-reports: the tool call says the
10
+ * compaction finished and the banner says so again. So the inference is
11
+ * DELETED, not left inert (D4) — a flag that no longer decides anything is a
12
+ * thing the next reader has to prove is inert.
13
+ *
14
+ * What replaces it: a small state machine that folds every event shape the SDK
15
+ * can produce onto a single tool call.
16
+ *
17
+ * SDK event → lifecycle call
18
+ * ------------------------------------------- -----------------------------
19
+ * `status` = "compacting" → start() tool_call, in_progress
20
+ * `stream_event` compaction block/delta → heartbeat() tool_call_update, in_progress
21
+ * `status` with compact_result success/failed → finish() tool_call_update, terminal
22
+ * `compact_boundary` (token counts) → finish(enrich) tool_call_update, metadata only
23
+ * a terminal with no preceding "compacting" → finish() tool_call, terminal (standalone)
24
+ * a DUPLICATED terminal → finish() nothing
25
+ *
26
+ * The two hard cases pull in opposite directions and both are real:
27
+ *
28
+ * - The SDK duplicates the terminal `compact_result` message for a single
29
+ * failed compaction. Two terminals for one compaction must stay ONE report.
30
+ * - One model turn can legitimately compact more than once. Two genuine
31
+ * compactions must stay TWO distinguishable reports.
32
+ *
33
+ * The state machine separates them by phase rather than by identity: a
34
+ * terminal that arrives when the lifecycle has already terminated is a
35
+ * duplicate and changes nothing, while a fresh `compacting` status AFTER a
36
+ * terminal opens a new lifecycle with a new tool call id. A rule that instead
37
+ * suppressed by matching ids or payloads would satisfy one case by breaking
38
+ * the other.
39
+ *
40
+ * State lives until the owning turn's `result` (or an abort) rather than being
41
+ * cleared at each terminal, because the SDK can also omit the opening status
42
+ * on replay — the terminal has to be able to stand alone.
43
+ *
44
+ * Kept self-contained — no import from `acp-agent.ts`, mirroring
45
+ * `thinking-option.ts` and `rewind-command.ts`; what it needs from the adapter
46
+ * (the send chokepoint) is injected, which also leaves the whole state machine
47
+ * unit-testable without a live SDK session. The reason is merge cost:
48
+ * `acp-agent.ts` is a ~9,700-line file that changes upstream almost daily, so
49
+ * every coupling to it is paid again at each sync.
50
+ */
51
+ import { createContextCompactionMeta, } from "./context-compaction-meta.js";
52
+ /**
53
+ * Translates Claude's compaction signals into one idempotent ACP tool
54
+ * lifecycle. One instance per consumer; `reset()` at each turn boundary.
55
+ */
56
+ export class ContextCompactionLifecycle {
57
+ sendUpdate;
58
+ activeCompaction;
59
+ outputDelivered = false;
60
+ duplicateErrorOutput;
61
+ constructor(sendUpdate) {
62
+ this.sendUpdate = sendUpdate;
63
+ }
64
+ /**
65
+ * Whether this turn already showed the user something about a compaction.
66
+ *
67
+ * The adapter reads it for two decisions the deleted banners used to make
68
+ * implicitly, by counting as delivered assistant text: an echo-less turn
69
+ * that only compacted (e.g. `/compact`) must not have its result text
70
+ * re-emitted by the issue-#453 fallback, and a replayed synthetic
71
+ * local-command frame from an earlier compact attempt must not be forwarded
72
+ * on top of the lifecycle.
73
+ */
74
+ get hasDeliveredOutput() {
75
+ return this.outputDelivered;
76
+ }
77
+ /** Return to the unstarted state; call at each turn boundary. */
78
+ reset() {
79
+ this.activeCompaction = undefined;
80
+ this.outputDelivered = false;
81
+ this.duplicateErrorOutput = undefined;
82
+ }
83
+ /**
84
+ * Claude also emits a failed manual compaction's error as local-command
85
+ * stdout. Consume that one duplicate after the tool lifecycle already
86
+ * carried it, without hiding unrelated command output.
87
+ *
88
+ * Matching is by exact (trimmed) text and consumes at most once, so a
89
+ * command that genuinely prints the same string later still reaches the
90
+ * client.
91
+ *
92
+ * @returns `true` when the caller should drop this content.
93
+ */
94
+ consumeDuplicateErrorOutput(content) {
95
+ if (this.duplicateErrorOutput === undefined ||
96
+ content.trim() !== this.duplicateErrorOutput.trim()) {
97
+ return false;
98
+ }
99
+ this.duplicateErrorOutput = undefined;
100
+ return true;
101
+ }
102
+ /**
103
+ * Open the tool call the client renders. Idempotent while a compaction is
104
+ * still running (a repeated `compacting` status re-uses the open call); a
105
+ * `compacting` that arrives AFTER a terminal starts a fresh lifecycle, which
106
+ * is how two real compactions in one turn stay two reports.
107
+ *
108
+ * @param toolCallId The SDK message uuid — replay-stable, so a re-delivered
109
+ * frame addresses the same tool call rather than inventing one.
110
+ */
111
+ async start(sessionId, toolCallId) {
112
+ if (this.activeCompaction && !this.activeCompaction.terminalStatus) {
113
+ return this.activeCompaction;
114
+ }
115
+ this.activeCompaction = { toolCallId, heartbeatSent: false };
116
+ this.outputDelivered = true;
117
+ await this.sendUpdate({
118
+ sessionId,
119
+ update: {
120
+ sessionUpdate: "tool_call",
121
+ toolCallId,
122
+ title: COMPACTION_TOOL_TITLE,
123
+ kind: "think",
124
+ status: "in_progress",
125
+ _meta: compactionToolMeta(),
126
+ },
127
+ });
128
+ return this.activeCompaction;
129
+ }
130
+ /**
131
+ * Keep the open call alive while compaction streams. Sent at most once per
132
+ * lifecycle: the deltas carry the generated summary, which stays internal to
133
+ * the agent, so repeating them would add noise and no information.
134
+ *
135
+ * @param fallbackId Used only when the opening status never arrived (replay).
136
+ */
137
+ async heartbeat(sessionId, fallbackId) {
138
+ const state = this.activeCompaction ?? (await this.start(sessionId, fallbackId));
139
+ if (state.terminalStatus || state.heartbeatSent)
140
+ return;
141
+ state.heartbeatSent = true;
142
+ await this.sendUpdate({
143
+ sessionId,
144
+ update: {
145
+ sessionUpdate: "tool_call_update",
146
+ toolCallId: state.toolCallId,
147
+ status: "in_progress",
148
+ _meta: compactionToolMeta(),
149
+ },
150
+ });
151
+ }
152
+ /**
153
+ * Report the outcome — exactly once per compaction.
154
+ *
155
+ * Three shapes converge here:
156
+ * - no lifecycle open (the SDK omitted the opening status on replay): emit a
157
+ * standalone terminal `tool_call`, so the event is reported rather than
158
+ * dropped for lack of a start;
159
+ * - the first terminal for an open lifecycle: a `tool_call_update` carrying
160
+ * the status;
161
+ * - a duplicate terminal: nothing at all.
162
+ *
163
+ * @param fallbackId Tool call id for the standalone shape.
164
+ * @param metadata Compaction facts; also emitted as `rawOutput` when non-empty,
165
+ * so a client that ignores `_meta` can still show something.
166
+ * @param enrichTerminal Allow a SECOND update on an already-terminal
167
+ * lifecycle that adds facts without re-reporting the outcome. Used by
168
+ * `compact_boundary`, which arrives after the terminal `status` and is the
169
+ * only frame carrying the token counts. The status field is omitted on that
170
+ * update, so the client still sees exactly one terminal transition.
171
+ */
172
+ async finish(sessionId, fallbackId, status, metadata = {}, enrichTerminal = false) {
173
+ const rawOutput = Object.keys(metadata).length > 0 ? metadata : undefined;
174
+ if (!this.activeCompaction) {
175
+ this.activeCompaction = {
176
+ toolCallId: fallbackId,
177
+ heartbeatSent: false,
178
+ terminalStatus: status,
179
+ };
180
+ this.outputDelivered = true;
181
+ this.rememberDuplicateErrorOutput(status, metadata);
182
+ await this.sendUpdate({
183
+ sessionId,
184
+ update: {
185
+ sessionUpdate: "tool_call",
186
+ toolCallId: fallbackId,
187
+ title: COMPACTION_TOOL_TITLE,
188
+ kind: "think",
189
+ status,
190
+ ...compactionErrorFields(status, metadata),
191
+ ...(rawOutput ? { rawOutput } : {}),
192
+ _meta: compactionToolMeta(metadata),
193
+ },
194
+ });
195
+ return;
196
+ }
197
+ const state = this.activeCompaction;
198
+ // The duplicate guard: a terminal on an already-terminal lifecycle is the
199
+ // SDK repeating itself, and must change nothing the client can see.
200
+ if (state.terminalStatus && !enrichTerminal)
201
+ return;
202
+ const firstTerminal = state.terminalStatus === undefined;
203
+ if (firstTerminal)
204
+ state.terminalStatus = status;
205
+ this.rememberDuplicateErrorOutput(status, metadata);
206
+ await this.sendUpdate({
207
+ sessionId,
208
+ update: {
209
+ sessionUpdate: "tool_call_update",
210
+ toolCallId: state.toolCallId,
211
+ ...(firstTerminal ? { status } : {}),
212
+ ...compactionErrorFields(status, metadata),
213
+ ...(rawOutput ? { rawOutput } : {}),
214
+ _meta: compactionToolMeta(metadata),
215
+ },
216
+ });
217
+ }
218
+ /** Arm {@link consumeDuplicateErrorOutput} for the stdout copy Claude emits. */
219
+ rememberDuplicateErrorOutput(status, metadata) {
220
+ if (status === "failed" && metadata.error) {
221
+ this.duplicateErrorOutput = metadata.error;
222
+ }
223
+ }
224
+ }
225
+ /**
226
+ * Map the SDK's `compact_boundary` metadata onto the provider-neutral names of
227
+ * {@link ContextCompactionMetadata}. Fields an older CLI omits stay omitted
228
+ * rather than becoming `undefined` keys, so `rawOutput` and `_meta` never
229
+ * advertise a number nobody reported.
230
+ */
231
+ export function contextCompactionMetadataFromBoundary(compactMetadata) {
232
+ return {
233
+ trigger: compactMetadata.trigger === "auto" ? "automatic" : "manual",
234
+ preTokens: compactMetadata.pre_tokens,
235
+ ...(compactMetadata.post_tokens !== undefined
236
+ ? { postTokens: compactMetadata.post_tokens }
237
+ : {}),
238
+ ...(compactMetadata.duration_ms !== undefined
239
+ ? { durationMs: compactMetadata.duration_ms }
240
+ : {}),
241
+ };
242
+ }
243
+ /** The title every compaction tool call carries; part of the R2.1 contract. */
244
+ const COMPACTION_TOOL_TITLE = "Compact conversation";
245
+ /**
246
+ * The `_meta` payload for a compaction tool update: the versioned extension
247
+ * plus the `claudeCode.toolName` every other tool call in this adapter carries,
248
+ * so a client keying off it treats the synthetic call like any real one.
249
+ *
250
+ * Returns a plain record rather than the adapter's `ToolUpdateMeta` to keep the
251
+ * module free of `acp-agent.ts` imports; the adapter widens `ToolUpdateMeta`
252
+ * with the matching optional field for its own call sites.
253
+ */
254
+ function compactionToolMeta(metadata = {}) {
255
+ return {
256
+ ...createContextCompactionMeta(metadata),
257
+ claudeCode: { toolName: "compact" },
258
+ };
259
+ }
260
+ /**
261
+ * The failure reason as renderable tool content — `_meta` is optional for a
262
+ * client, the reason for a failure is not.
263
+ */
264
+ function compactionErrorFields(status, metadata) {
265
+ if (status !== "failed" || !metadata.error)
266
+ return {};
267
+ return {
268
+ content: [
269
+ {
270
+ type: "content",
271
+ content: { type: "text", text: `Compaction failed: ${metadata.error}` },
272
+ },
273
+ ],
274
+ };
275
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The "Ultracode" entry of the Effort picker.
3
+ *
4
+ * **Ultracode is not a sixth effort level, and reading it as one produces the
5
+ * wrong control.** Claude Code's VS Code extension models it as a boolean that
6
+ * turns on two things at once — `xhigh` effort *and* standing dynamic-workflow
7
+ * orchestration — and its own tooltip says so: `"Ultracode - xhigh +
8
+ * workflows"`. Its five real levels are `low` `medium` `high` `xhigh` `max`;
9
+ * Ultracode is drawn as one extra notch past the last of them, styled
10
+ * differently, and selecting any ordinary level clears it.
11
+ *
12
+ * ACP has no compound control to carry that, so the adapter projects it the way
13
+ * the extension's UI reads rather than the way its state is stored: one more
14
+ * entry in the `effort` select. The compound stays here, in one module, instead
15
+ * of being spread across the three places `acp-agent.ts` applies effort.
16
+ *
17
+ * The SDK carries both halves and names the channel: `Settings.ultracode` is
18
+ * *"xhigh effort plus standing dynamic-workflow orchestration. Session-scoped —
19
+ * typically provided via --settings or the apply_flag_settings control
20
+ * request"*. `apply_flag_settings` is already how the effort option is applied,
21
+ * so no new transport is involved.
22
+ *
23
+ * Kept self-contained — no import from `acp-agent.ts` — for the reason
24
+ * `thinking-option.ts` and `rewind-command.ts` state: that file is ~9,900 lines
25
+ * and moves upstream almost daily, so every coupling to it is paid again at
26
+ * each sync.
27
+ */
28
+ import type { EffortLevel, Settings } from "@anthropic-ai/claude-agent-sdk";
29
+ /** Value of the synthetic "Ultracode" entry in the effort picker. It is a
30
+ * reserved sentinel: no `EffortLevel` is spelled this way, so it cannot
31
+ * collide with a level the SDK reports in `supportedEffortLevels`. */
32
+ export declare const ULTRACODE_OPTION_VALUE = "ultracode";
33
+ /** Display name of that entry, matching the extension's own label. */
34
+ export declare const ULTRACODE_OPTION_NAME = "Ultracode";
35
+ /** The effort level Ultracode implies. Not configurable: the extension hardcodes
36
+ * `xhigh` in `enableUltracode()`, and the SDK's own description of the setting
37
+ * names the same level. A different level here would silently mean something
38
+ * else than it does in the reference client. */
39
+ export declare const ULTRACODE_EFFORT_LEVEL: EffortLevel;
40
+ /** Display state for the Ultracode entry, resolved by the caller. Mirrors the
41
+ * shape `FastModeOptionState` uses for the same job — availability and current
42
+ * value are two different questions, and folding them into one boolean loses
43
+ * the case where the entry should render but not be selected. */
44
+ export interface UltracodeOptionState {
45
+ /** Whether the entry should appear at all. */
46
+ available: boolean;
47
+ /** Whether it is the currently selected effort. */
48
+ enabled: boolean;
49
+ }
50
+ /**
51
+ * Whether the Ultracode entry may be offered, under the same two conditions the
52
+ * extension checks in its own `ultracodeAvailable`:
53
+ *
54
+ * 1. workflows are not disabled — Ultracode *is* the workflow half, so offering
55
+ * it where `disableWorkflows` is set would advertise something that cannot
56
+ * happen;
57
+ * 2. the current model supports `xhigh` — Ultracode has no meaning on a model
58
+ * that cannot reach the level it implies.
59
+ *
60
+ * Both are read rather than assumed: an unknown settings shape leaves
61
+ * `disableWorkflows` undefined, which is *not* `true`, so the gate opens — the
62
+ * same direction the extension's `=== !0` comparison takes.
63
+ */
64
+ export declare function isUltracodeAvailable(supportedEffortLevels: readonly string[], settings: Pick<Settings, "disableWorkflows"> | undefined): boolean;
65
+ /** Whether a picker value denotes Ultracode rather than a plain effort level. */
66
+ export declare function isUltracodeValue(value: string | undefined): boolean;
67
+ /**
68
+ * The `apply_flag_settings` payload for an effort selection, Ultracode included.
69
+ *
70
+ * **The `null` is the load-bearing part.** The SDK shallow-merges flag settings
71
+ * and only clears a key when an explicit `null` arrives — `undefined` is dropped
72
+ * by JSON transport and leaves the previous value standing. So every selection
73
+ * has to say something about `ultracode`, not just the one that turns it on:
74
+ * without the clear, picking `High` after Ultracode would lower the effort and
75
+ * leave workflow orchestration running, which is a state no picker entry names.
76
+ * The same reasoning applies to `effortLevel` — this function absorbed the older
77
+ * `toSdkEffortLevel` helper for exactly that reason: both keys are decided in
78
+ * one place, so neither can be forgotten on a path the other takes.
79
+ *
80
+ * @param value the picker value — `"default"`, an `EffortLevel`, or the
81
+ * Ultracode sentinel. `undefined` means the option is absent for this model.
82
+ */
83
+ export declare function ultracodeFlagSettings(value: string | undefined): {
84
+ effortLevel: EffortLevel | null;
85
+ ultracode: boolean | null;
86
+ };
87
+ /**
88
+ * The picker value to display, given the persisted effort level and whether the
89
+ * Ultracode flag is on.
90
+ *
91
+ * The flag wins over the level, and only when it is *available*: a session that
92
+ * carries `ultracode: true` from settings while the current model cannot reach
93
+ * `xhigh` must not show an entry the picker is not rendering. The extension
94
+ * reaches the same state through `adoptAppliedEffort`, which reads
95
+ * `applied.ultracode` first and falls back to inferring it from `xhigh`; we do
96
+ * not infer, because `xhigh` alone is a legitimate selection that is not
97
+ * Ultracode, and guessing would silently turn workflows on in the label.
98
+ */
99
+ export declare function effortOptionValue(effortLevel: string | undefined, ultracode: UltracodeOptionState | undefined): string | undefined;
100
+ //# sourceMappingURL=ultracode.d.ts.map