@lucascouts/claude-agent-acp-plus 0.3.0 → 0.5.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/%40lucascouts%2Fclaude-agent-acp-plus)](https://www.npmjs.com/package/@lucascouts/claude-agent-acp-plus)
4
4
 
5
- > A **fork** of [claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp) that ports features from the Claude Code VS Code extension to ACP clients (like Zed), for a friendlier experience. Currently based on upstream v0.57.0.
5
+ > A **fork** of [claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp) that ports features from the Claude Code VS Code extension to ACP clients (like Zed), for a friendlier experience. Currently based on upstream v0.58.1. Requires Node.js >= 24.
6
6
 
7
7
  Use [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview#branding-guidelines) from [ACP-compatible](https://agentclientprotocol.com) clients!
8
8
 
@@ -1,5 +1,5 @@
1
- import { AuthenticateRequest, CancelNotification, ClientCapabilities, CompleteElicitationNotification, CreateElicitationRequest, CreateElicitationResponse, ForkSessionRequest, ForkSessionResponse, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, LoadSessionResponse, LogoutRequest, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, ReadTextFileRequest, ReadTextFileResponse, RequestPermissionRequest, RequestPermissionResponse, ResumeSessionRequest, ResumeSessionResponse, SessionConfigOption, SessionModeState, SessionNotification, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse, CloseSessionRequest, CloseSessionResponse, DeleteSessionRequest, DeleteSessionResponse, WriteTextFileRequest, WriteTextFileResponse } from "@agentclientprotocol/sdk";
2
- import { AgentInfo, CanUseTool, FastModeState, ModelInfo, Options, PermissionMode, PermissionUpdate, Query, SDKMessageOrigin, SDKPartialAssistantMessage, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
1
+ import { AuthenticateRequest, CancelNotification, ClientCapabilities, CompleteElicitationNotification, CreateElicitationRequest, CreateElicitationResponse, DisableProviderRequest, DisableProviderResponse, ForkSessionRequest, ForkSessionResponse, InitializeRequest, InitializeResponse, ListProvidersRequest, ListProvidersResponse, LlmProtocol, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, LoadSessionResponse, LogoutRequest, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, ReadTextFileRequest, ReadTextFileResponse, SetProviderRequest, SetProviderResponse, RequestPermissionRequest, RequestPermissionResponse, ResumeSessionRequest, ResumeSessionResponse, SessionConfigOption, SessionModeState, SessionNotification, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse, CloseSessionRequest, CloseSessionResponse, DeleteSessionRequest, DeleteSessionResponse, WriteTextFileRequest, WriteTextFileResponse } from "@agentclientprotocol/sdk";
2
+ import { AgentInfo, CanUseTool, FastModeDisabledReason, FastModeState, ModelInfo, Options, PermissionMode, PermissionUpdate, Query, SDKMessageOrigin, SDKPartialAssistantMessage, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
3
3
  import { ContentBlockParam } from "@anthropic-ai/sdk/resources";
4
4
  import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
5
5
  import { SettingsManager } from "./settings.js";
@@ -19,6 +19,34 @@ type AccumulatedUsage = {
19
19
  cachedReadTokens: number;
20
20
  cachedWriteTokens: number;
21
21
  };
22
+ /** Request-level steering options. `promptRequired` is opt-in so existing Hosts
23
+ * keep the established idle fallback behavior. */
24
+ type SteerMeta = {
25
+ [key: string]: unknown;
26
+ steering?: {
27
+ idleBehavior?: "promptRequired";
28
+ };
29
+ };
30
+ /** Params of a {@link STEER_METHOD} request. Shaped like the relevant subset of
31
+ * a `PromptRequest` so the same `promptToClaude` conversion applies. Delivery
32
+ * priority is deliberately NOT exposed here — it's an internal detail the agent
33
+ * chooses (see {@link STEER_PRIORITY}). */
34
+ export type SteerRequest = {
35
+ sessionId: string;
36
+ prompt: PromptRequest["prompt"];
37
+ _meta?: SteerMeta | null;
38
+ };
39
+ /** Result of a {@link STEER_METHOD} request. The legacy `startedNewTurn` result
40
+ * remains the default idle behavior; `promptRequired` is returned only when the
41
+ * Host explicitly opts into the host-owned fallback in request `_meta`. */
42
+ export type SteerResponse = {
43
+ outcome: "injected";
44
+ } | {
45
+ outcome: "startedNewTurn";
46
+ } | {
47
+ outcome: "promptRequired";
48
+ reason: "noRunningTurn";
49
+ };
22
50
  /** Internal model-selection state. Mirrors the shape the ACP SDK exposed as
23
51
  * `SessionModelState` before model selection moved entirely into
24
52
  * `SessionConfigOption` (category "model"). Retained internally to track the
@@ -47,6 +75,77 @@ type Turn = {
47
75
  /** Set once the deferred has been resolved/rejected, so the consumer never
48
76
  * settles a turn twice (idle + handoff + stream-end can all race). */
49
77
  settled: boolean;
78
+ /** Set when a `command_lifecycle` "started" frame arrives for this turn's
79
+ * uuid (msg_lifecycle_v1 CLIs): the SDK dispatched the command into a turn.
80
+ * Read by cancel() to seed the orphan's state — a started orphan's turn may
81
+ * still emit a result, an undispatched one may be dropped without one. */
82
+ commandStarted?: boolean;
83
+ /** Set when a terminal `command_lifecycle` frame arrives for this turn's
84
+ * uuid while the turn is still queued (msg_lifecycle_v1 CLIs). The command
85
+ * is already finished SDK-side, so a later cancel() must not seed an
86
+ * orphan entry for it — no terminal frame will ever come to drain it.
87
+ * "completed"/"discarded" leave nothing outstanding; "cancelled" after a
88
+ * dispatch means the dead turn's result may still arrive (seeded as a
89
+ * zombie) unless it already passed (`commandResultSeen`), and without a
90
+ * dispatch means dropped (nothing coming). */
91
+ commandFinished?: "completed" | "discarded" | "cancelled";
92
+ /** Set when a user-turn result arrives while this command is known
93
+ * dispatched (`commandStarted`) with no terminal frame yet. Turns run
94
+ * sequentially and frames arrive in stream order, so the turn this command
95
+ * was dispatched into IS the turn that emitted that result — including
96
+ * when the command was FOLDED into another turn (their shared result).
97
+ * Read by cancel() and the force-cancel wedge path so neither seeds an
98
+ * orphan entry for a result that has already passed: such an entry could
99
+ * never be drained by its result and would swallow an unrelated later
100
+ * echo-less one instead. */
101
+ commandResultSeen?: boolean;
102
+ /** Task ids of the background subagents launched while this turn was the
103
+ * active one — including during its held-open drain window, so an agent
104
+ * chain (a followup that launches another subagent) extends the hold.
105
+ * A turn only waits on its OWN spawned subagents: a long-running agent
106
+ * from an earlier turn must not stall every later prompt's settlement.
107
+ * Known residual: task_started carries no lineage, so a spawn made by a
108
+ * PREVIOUS turn's followup chain while a later turn happens to be held
109
+ * is attributed to the holder — extending that hold behind a foreign
110
+ * chain. Bounded: the hold still ends at drain, hand-off, or cancel. */
111
+ spawnedTaskIds?: Set<string>;
112
+ /** Set instead of settling when the turn's terminal result arrives while
113
+ * subagents it spawned are still live (`spawnedTaskIds` ∩
114
+ * `session.liveBackgroundTasks`). The turn is held open — its
115
+ * `session/prompt` stays pending — so the subagents' streamed output,
116
+ * their permission requests (which would otherwise block on an RPC a
117
+ * client that stops consuming at the prompt response never answers —
118
+ * issue #866), and the model's task-notification followup summary all
119
+ * land inside the turn.
120
+ *
121
+ * The CLI does NOT hold its trailing idle for background agents (observed
122
+ * on 2.1.206: `idle` follows the result immediately while the subagent
123
+ * still runs), so the hold spans multiple idle cycles: user result →
124
+ * idle → (subagent works) → task_notification → followup turn → idle.
125
+ * The stored outcome (the result's stop reason and usage snapshot) is
126
+ * what the turn settles with once its spawned subagents have settled —
127
+ * at the followup's terminal result (the summary has streamed by then),
128
+ * or at an idle with none of its subagents left (no followup came). A
129
+ * cancel or the next turn's echo hand-off settles it earlier, so a
130
+ * long-running subagent never holds the prompt hostage.
131
+ *
132
+ * Accepted residuals. (1) A subagent that ends WITHOUT waking the model —
133
+ * its task_notification lost or skipped (only the terminal task_updated
134
+ * patch is guaranteed per transition) — leaves no followup result and no
135
+ * further idle, so the held turn parks until `session/cancel` or the next
136
+ * prompt (either settles it: the echo hand-off or ensureActiveTurn's
137
+ * held-turn hand-off). Settling at the prune sites instead would preempt
138
+ * the followup summary in the normal ordering (prunes precede the
139
+ * notification), and a grace timer was judged not worth the machinery —
140
+ * the same rescue contract as the adapter's other wedge classes (issue
141
+ * #825's out-of-scope notes). (2) Drained-ness is judged by live-task
142
+ * membership only: with parallel subagents, a notification that prunes
143
+ * the last task during an earlier task's still-streaming followup lets
144
+ * that followup's result settle the turn before the LAST task's summary
145
+ * streams — degrading to post-turn delivery for it, never worse than the
146
+ * pre-hold behavior (pending wakes are not countable: notifications can
147
+ * batch into one followup). */
148
+ deferredSettle?: PromptResponse;
50
149
  resolve: (response: PromptResponse) => void;
51
150
  reject: (error: unknown) => void;
52
151
  };
@@ -70,8 +169,40 @@ type Session = {
70
169
  * the interrupt dropped (absent from `still_queued`) are uncounted as soon
71
170
  * as the receipt arrives (see cancel()). Reset to 0 on every activation as
72
171
  * a backstop against a dropped queued input this can't see (older CLIs, a
73
- * receipt lost to a failed control round-trip). */
172
+ * receipt lost to a failed control round-trip). Only used when the CLI does
173
+ * NOT emit lifecycle frames (see `orphanCommands` for the msg_lifecycle_v1
174
+ * lane); a count can't express command coalescing — N queued commands can
175
+ * fold into ONE turn emitting one result, leaving a stale skip of N-1. */
74
176
  pendingOrphanResults?: number;
177
+ /** msg_lifecycle_v1 lane of the orphan accounting (see
178
+ * `pendingOrphanResults` for the count lane): the uuids of cancelled queued
179
+ * turns whose SDK-side command may still produce an unaccounted result,
180
+ * keyed to what we know of its fate. "pending" = not seen dispatched; if
181
+ * the SDK drops it (interrupt, `cancelled` before "started") no result
182
+ * ever comes. "started" = dispatched into a turn whose result is still
183
+ * coming; exactly one terminal lifecycle frame will follow. "zombie" = its
184
+ * turn was aborted/failed after dispatch with no result seen since
185
+ * (`cancelled` after "started"); no more lifecycle frames come, but the
186
+ * dead turn's error result may still arrive. Entries are removed the
187
+ * moment their result is covered: EVERY user-turn result covers ALL
188
+ * started and zombie entries at once (turns run sequentially and frames
189
+ * arrive in stream order, so at any result the started entries were
190
+ * dispatched into — possibly folded into — the emitting turn, and any
191
+ * zombie's late result has already passed or never existed), whether that
192
+ * result was attributed to the active turn or skipped echo-less (see
193
+ * recordResultForOrphanCommands / ensureActiveTurn). A command's own
194
+ * terminal frame also drains its entry ("completed" is emitted after any
195
+ * result its turn produced; a bare `cancelled` deletes a pending entry —
196
+ * dropped without running — and zombifies a started one). An echo-less
197
+ * result is an orphan's iff this map is non-empty (FIFO: orphan turns run
198
+ * before any live turn's). Cleared on every activation, same self-heal as
199
+ * the count (covers a lost frame, which can leak an entry — each state
200
+ * bounds the damage to one wrong skip). */
201
+ orphanCommands?: Map<string, "pending" | "started" | "zombie">;
202
+ /** True once a `system`/init advertised the msg_lifecycle_v1 capability, so
203
+ * cancel() routes orphan accounting to `orphanCommands` (exact, per-uuid)
204
+ * instead of `pendingOrphanResults` (count, coalescing-blind). */
205
+ msgLifecycleV1?: boolean;
75
206
  /** The long-lived consumer task. Lazily started on the first `prompt()` and
76
207
  * kept alive for the session so between-turn/background messages are still
77
208
  * drained and forwarded. */
@@ -104,6 +235,13 @@ type Session = {
104
235
  * user's intent so it persists across model switches; the Fast mode config
105
236
  * option is only surfaced while the selected model supports it. */
106
237
  fastModeEnabled: boolean;
238
+ /** Why the SDK currently can't serve Fast mode, when the reason is one worth
239
+ * telling the user about (see {@link FAST_MODE_UNAVAILABLE_EXPLANATIONS} —
240
+ * routine states like the SDK's own opt-in requirement normalize to
241
+ * `undefined`). Refreshed from every `fast_mode_disabled_reason` the SDK
242
+ * reports on `system`/init and user-turn `result`s; surfaced in the Fast mode
243
+ * option's description so a toggle that snaps back off explains itself. */
244
+ fastModeDisabledReason?: FastModeDisabledReason;
107
245
  /** Tri-state Thinking intent (story 006): `true`/`false` once the client has
108
246
  * set the Thinking config option, `undefined` while untouched — i.e. the
109
247
  * env-driven `MAX_THINKING_TOKENS` behavior stays in charge (R1.6). Mirrors
@@ -148,12 +286,37 @@ type Session = {
148
286
  * cancel. */
149
287
  forceCancelTimer?: ReturnType<typeof setTimeout>;
150
288
  emitRawSDKMessages: boolean | SDKMessageFilter[];
151
- /** Context window size of the last top-level assistant model, carried across
289
+ /** Whether nested subagent text/thinking is forwarded to the ACP client.
290
+ * Enabled by either the ACP capability or the pre-existing SDK option. */
291
+ forwardSubagentText: boolean;
292
+ /** Context window size of the session's current model, carried across
152
293
  * prompts so mid-stream usage_update notifications report a correct `size`
153
- * before the turn's first result message arrives. Defaults to
154
- * DEFAULT_CONTEXT_WINDOW, refreshed from each result's modelUsage, and
155
- * invalidated when the user switches the session's model. */
294
+ * before the turn's first result message arrives. Seeded synchronously at
295
+ * session creation and on model switches from the per-model cache or the
296
+ * text heuristic (DEFAULT_CONTEXT_WINDOW when both miss; on session/load the
297
+ * resumed session's own `getContextUsage` report wins, see
298
+ * `readResumedLiveModel`), then confirmed — and the cache populated — by each
299
+ * result's modelUsage. No extra `getContextUsage` IPC is on these paths: on a
300
+ * fresh session it stalls until the first turn runs (see the seeding call
301
+ * sites and `contextWindowCache`). */
156
302
  contextWindowSize: number;
303
+ /** Whether `contextWindowSize` came from an authoritative source (the
304
+ * cross-session cache, a resumed session's `getContextUsage` report, or a
305
+ * `result.modelUsage`) rather than the text heuristic / default. Guards the
306
+ * mid-stream `message_start` heuristic upgrade: an authoritative window that
307
+ * happens to equal DEFAULT_CONTEXT_WINDOW must not be mistaken for "unseeded"
308
+ * and clobbered by a "1m" text match. */
309
+ contextWindowAuthoritative: boolean;
310
+ /** Stable identifier of the LLM backend this session's query was created
311
+ * against, derived from the routing-relevant vars of the exact `env` handed
312
+ * to the SDK at query creation (see {@link providerCacheKeyFor}). The context
313
+ * window is a property of (model id, backend) — the same resolved model id
314
+ * can name different windows behind different base URLs, routing headers, or
315
+ * credentials — so this scopes the module-global `contextWindowCache` per
316
+ * backend. Captured from the query's own env (not re-resolved later) because
317
+ * the process-wide provider config can change while a session is being
318
+ * created, while the query stays baked to the env it was created with. */
319
+ providerCacheKey: string;
157
320
  /** Accumulated task list for the session, keyed by task ID. Task IDs are
158
321
  * per-session, so this state must not be shared across sessions. */
159
322
  taskState: TaskState;
@@ -177,6 +340,113 @@ type Session = {
177
340
  * tool_use block streams; this set makes the two paths converge regardless of
178
341
  * order. Pruned at `tool_result` time alongside `toolUseCache`. */
179
342
  emittedToolCalls: Set<string>;
343
+ /** Registry of live background tasks, keyed by task id: populated at
344
+ * `task_started`, pruned when the task settles (a `task_notification` or
345
+ * a terminal `task_updated` patch), and reconciled against
346
+ * `background_tasks_changed`'s replace-semantics payload so a lost
347
+ * bookend can't leak an entry. One structure for both of its concerns so
348
+ * a future terminal path can't prune one and not the other:
349
+ *
350
+ * `parentToolUseId` — the tool_use id of the Agent/Task call that spawned
351
+ * the task. For subagent tasks the SDK keys its registry by agent id, so
352
+ * `task_started.task_id` IS the `agentID` that `canUseTool` later
353
+ * receives. Lets the permission flow attribute a subagent's
354
+ * eagerly-emitted `tool_call` (and the permission request itself) to its
355
+ * parent tool call via `_meta.claudeCode.parentToolUseId`, matching the
356
+ * streamed subagent path. Best-effort: a `canUseTool` that races ahead of
357
+ * the consumer processing `task_started` omits the attribution from the
358
+ * eager tool_call, and the streamed tool_use chunk's refining
359
+ * `tool_call_update` — which carries the message-level
360
+ * `parent_tool_use_id` — restores it for merging clients; that recovery
361
+ * is what makes best-effort acceptable here.
362
+ *
363
+ * `isSubagent` — whether the task is a Task/Agent-tool subagent
364
+ * (`task_started` carried a `subagent_type`). Read by
365
+ * `turnAwaitingSubagents` (with `spawnedTaskIds`) to decide whether a
366
+ * turn's settlement is deferred (see `Turn.deferredSettle`), so the
367
+ * subagents' post-result output and permission requests stay inside the
368
+ * turn (issues #864/#866). Deliberately false for non-subagent background
369
+ * tasks (e.g. a `run_in_background` dev server): those can outlive every
370
+ * turn, and the model's contract with them is a wake-on-exit
371
+ * notification, not a turn-scoped drain — a hold must NEVER wait on a
372
+ * shell.
373
+ *
374
+ * `endedPerLevel` — a `background_tasks_changed` payload did not include
375
+ * this subagent entry. The level's universe is BACKGROUND tasks only, so
376
+ * a live sync (foreground) subagent is legitimately absent — its entry is
377
+ * kept for permission attribution — but a hold must stop waiting on the
378
+ * id: an absent id can equally be a leaked async entry whose settle
379
+ * bookends were lost, and waiting on it would park the hold forever.
380
+ * Non-subagent entries are simply deleted instead (shells are always in
381
+ * the level's universe). */
382
+ liveBackgroundTasks: Map<string, {
383
+ parentToolUseId?: string;
384
+ isSubagent: boolean;
385
+ /** Absent-from-level lifecycle, one field so the illegal
386
+ * armed-but-not-ended state is unrepresentable: undefined = live per
387
+ * the level signal; "ended" = a level omitted the task (holds stop
388
+ * waiting on it; attribution is kept); "sweep-armed" = a turn
389
+ * activation saw it ended — the NEXT activation deletes it. The
390
+ * one-activation grace exists for the absent-mark race (a level
391
+ * payload built before a live async agent's registration): a
392
+ * corrective inclusive level resets the field to undefined — one
393
+ * assignment, disarming any in-flight sweep — if it arrives within a
394
+ * full turn, keeping the agent's attribution; eager deletion would
395
+ * be irreversible, since levels never ADD entries. A re-mark
396
+ * preserves an in-flight arm (`??=`), keeping a continuously absent
397
+ * entry on its two-activation clock. */
398
+ endedPerLevel?: "ended" | "sweep-armed";
399
+ }>;
400
+ /** Whether any top-level assistant text reached the client since the last
401
+ * stretch boundary. Set as a side effect of sending in the consumer's
402
+ * `sendUpdate`, never at an emission site; read at the terminal `result`
403
+ * to tell a turn whose answer was already delivered from one that only
404
+ * ever carried it on `result` (issue #453). Session-level (not
405
+ * consumer-scoped) so cancel()'s inline settle can clear it.
406
+ *
407
+ * The CURRENT boundary set — a new clear site must be added here: the
408
+ * result case's `finally` (user-turn results), settleActive's wasHeld
409
+ * clear (every held-turn settle lane: drain settle, both hand-offs,
410
+ * stream-done), failActive, the force-cancel backstop, the idle
411
+ * cancelled-settle, the autonomous-result close (only with no turn
412
+ * active OR queued — see its queued-turn guard), and cancel()'s inline
413
+ * mirror.
414
+ *
415
+ * Deliberately NOT reset on turn activation: activation can fire
416
+ * mid-message (see the echo hand-off), so a flag cleared there would
417
+ * forget text that already streamed and the result text would be emitted
418
+ * a second time. Neither the consolidated `assistant` message nor a
419
+ * `stream_event` carries `origin`, so an autonomous cycle's prose is
420
+ * indistinguishable from a user turn's here and sets the flag too; the
421
+ * autonomous-result close normally ends that stretch so a replayed
422
+ * prompt behind it still delivers, and only in the racing window (a
423
+ * turn already active or queued when the autonomous result lands) does
424
+ * the replayed turn stay silent rather than risk a duplicate. */
425
+ emittedAssistantText: boolean;
426
+ /** The most recent `session_state_changed` state the consumer processed.
427
+ * Read by cancel() to decide whether the interrupt will produce a
428
+ * trailing idle worth pre-counting: interrupting a RUNNING cycle yields
429
+ * one; interrupting an already-idle session (the common held-turn shape)
430
+ * yields none, and a pre-counted debt that never drains would mask one
431
+ * future issue-#825 detection. */
432
+ lastSessionState?: "idle" | "running" | "requires_action";
433
+ /** How many trailing `session_state_changed: idle` messages are already
434
+ * accounted for: every result is followed by one (user-turn results that
435
+ * terminate a turn — settle, reject, or orphan skip — and autonomous
436
+ * cycles alike), as is a cancelled turn settled by the next turn's echo
437
+ * hand-off or by cancel()'s inline settle of a held turn whose interrupt
438
+ * pre-empts a running cycle — the reason this lives on the Session:
439
+ * cancel() must be able to record the debt. The idle handler absorbs
440
+ * owed idles; an idle that arrives when NONE is owed while the active
441
+ * turn is still unsettled means the SDK ended the turn without ever
442
+ * emitting its result, so the turn will never settle on its own (issue
443
+ * #825). Stream-level debt, deliberately NOT reset per turn: a lagged
444
+ * idle can arrive after the next turn has already activated (issue
445
+ * #773), and the debt is what attributes it to the turn that owed it.
446
+ * Over-counting (an idle the SDK never emits) is benign: the counter
447
+ * just absorbs one future idle, and detection degrades to the status quo
448
+ * rather than misfiring. */
449
+ owedTrailingIdles: number;
180
450
  /** Maps the ACP `messageId` we expose to clients (see `messageIdForGrouping`)
181
451
  * to the SDK message uuid that the Agent SDK's rewind/resume APIs key on
182
452
  * (`Query.rewindFiles` takes a user-message uuid; `resumeSessionAt` takes an
@@ -251,13 +521,34 @@ type GatewayAuthMeta = {
251
521
  type GatewayAuthRequest = AuthenticateRequest & {
252
522
  _meta?: GatewayAuthMeta;
253
523
  };
524
+ /**
525
+ * Resolved, non-secret + secret routing config for the `main` provider. This is
526
+ * the shared shape produced by both `providers/set` and the legacy gateway auth
527
+ * path, and consumed by {@link createEnvForProvider}. `null` means the provider
528
+ * is unconfigured (no client-managed routing in effect).
529
+ */
530
+ type ProviderConfig = {
531
+ apiType: LlmProtocol;
532
+ baseUrl: string;
533
+ headers: Record<string, string>;
534
+ /** Present only for `apiType === "vertex"`. */
535
+ vertex?: {
536
+ projectId: string;
537
+ region: string;
538
+ };
539
+ };
254
540
  /**
255
541
  * Extra metadata that the agent provides for each tool_call / tool_update update.
256
542
  */
257
543
  export type ToolUpdateMeta = {
258
544
  claudeCode?: {
259
545
  toolName: string;
546
+ title?: string;
260
547
  toolResponse?: unknown;
548
+ parentToolUseId?: string;
549
+ nonExecutionKind?: string;
550
+ userFeedback?: string;
551
+ subagent?: true;
261
552
  };
262
553
  terminal_info?: {
263
554
  terminal_id: string;
@@ -280,6 +571,25 @@ export type ToolUseCache = {
280
571
  input: unknown;
281
572
  };
282
573
  };
574
+ type StreamedToolInput = {
575
+ id: string;
576
+ name: string;
577
+ partialJson: string;
578
+ /** Offset into `partialJson` the scanner has consumed; each delta only scans
579
+ * the newly appended fragment, so total scan work stays linear. */
580
+ scannedTo: number;
581
+ inString: boolean;
582
+ escaped: boolean;
583
+ objectDepth: number;
584
+ arrayDepth: number;
585
+ /** Offset of the most recent comma at the top level of the input object
586
+ * (-1 before the first). Everything before it is a complete field. */
587
+ lastTopLevelComma: number;
588
+ /** The comma offset the last emitted refinement was sliced at (-1 before the
589
+ * first), so a field boundary only triggers one recovery attempt. */
590
+ emittedThroughComma: number;
591
+ };
592
+ export type StreamedToolInputCache = Map<string, Map<number, StreamedToolInput>>;
283
593
  export declare function claudeCliPath(): Promise<string>;
284
594
  /**
285
595
  * Return user-message content with local-command marker tags removed, or
@@ -289,6 +599,23 @@ export declare function claudeCliPath(): Promise<string>;
289
599
  */
290
600
  export declare function stripLocalCommandMetadata(content: unknown): unknown | null;
291
601
  export declare function isLocalCommandMetadata(content: unknown): boolean;
602
+ /**
603
+ * True for the synthetic assistant message the CLI injects into the transcript
604
+ * when a turn fails authentication (e.g. "Not logged in · Please run /login",
605
+ * "Session expired. Please run /login to sign in again."). The `/login`
606
+ * instruction is Claude Code TUI-specific and meaningless to ACP clients
607
+ * (issue #863). The live prompt loop suppresses the text and fails the turn
608
+ * with `authRequired` so the client can run its own auth flow; replay must
609
+ * skip it too — both for parity with what the client saw live and because the
610
+ * message stays in the transcript forever, so it would resurface on every
611
+ * session/load even after the user has logged back in.
612
+ *
613
+ * Takes the API message (`message.message`), which replay only knows as
614
+ * `unknown`. The persisted record's structured `error: "authentication_failed"`
615
+ * marker is stripped by `getSessionMessages`, so the synthetic model + text is
616
+ * all both paths have to match on.
617
+ */
618
+ export declare function isSyntheticLoginMessage(apiMessage: unknown): boolean;
292
619
  export declare function resolvePermissionMode(defaultMode?: unknown, logger?: Logger): PermissionMode;
293
620
  /**
294
621
  * Builds the label for the "Always Allow" permission option so the user can see
@@ -326,6 +653,10 @@ export declare class ClaudeAcpAgent {
326
653
  clientCapabilities?: ClientCapabilities;
327
654
  logger: Logger;
328
655
  gatewayAuthRequest?: GatewayAuthRequest;
656
+ /** Client-managed LLM routing set via `providers/set`. Process-scoped and
657
+ * never persisted to disk (see the Configurable LLM Providers RFD). When
658
+ * set, it takes precedence over {@link gatewayAuthRequest}. */
659
+ providerConfig?: ProviderConfig;
329
660
  /** Grace period before a `session/cancel` forces a wedged prompt loop to
330
661
  * return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
331
662
  * tests can shrink it. */
@@ -344,8 +675,60 @@ export declare class ClaudeAcpAgent {
344
675
  * the title is best-effort and another turn will retry. */
345
676
  private maybeUpdateSessionTitle;
346
677
  authenticate(_params: AuthenticateRequest): Promise<void>;
678
+ /**
679
+ * `providers/list` — returns the single client-configurable custom gateway
680
+ * provider (`main`). `current` carries only non-secret routing (never headers,
681
+ * which may hold secrets); only `apiType`/`baseUrl` are surfaced for UI
682
+ * display, and is `null` when the provider is not configured/disabled. The
683
+ * provider is optional (`required: false`): while disabled/unconfigured the
684
+ * agent falls back to its own default routing (normal Claude login).
685
+ */
686
+ unstable_listProviders(_params: ListProvidersRequest): Promise<ListProvidersResponse>;
687
+ /**
688
+ * `providers/set` — replace the full configuration for the `main` provider.
689
+ * Rejects unknown IDs, unsupported protocols, and empty/invalid base URLs with
690
+ * `invalid_params`. Config is process-scoped and applies to sessions created or
691
+ * loaded after this call.
692
+ */
693
+ unstable_setProvider(params: SetProviderRequest): Promise<SetProviderResponse>;
694
+ /**
695
+ * `providers/disable` — disabling the `main` provider clears any client-managed
696
+ * routing (both a `providers/set` config and the legacy gateway auth request),
697
+ * so the agent reverts to its own default routing and `providers/list` reports
698
+ * `current: null`. Disabling any other (unknown) ID is treated as a successful
699
+ * no-op per the RFD's idempotency rule.
700
+ */
701
+ unstable_disableProvider(params: DisableProviderRequest): Promise<DisableProviderResponse>;
702
+ /**
703
+ * Resolve the effective client-managed routing config. `providers/set` takes
704
+ * precedence; otherwise fall back to the legacy gateway auth request. Returns
705
+ * `null` when neither is configured.
706
+ */
707
+ resolveProviderConfig(): ProviderConfig | null;
347
708
  logout(_params: LogoutRequest): Promise<void>;
348
709
  prompt(params: PromptRequest): Promise<PromptResponse>;
710
+ /** Steer the session per the ACP steering wire protocol: inject a follow-up
711
+ * message into the turn that is currently running. If that turn already
712
+ * settled, the established default starts a new detached turn; Hosts may opt
713
+ * into the host-owned `promptRequired` fallback through request `_meta`.
714
+ *
715
+ * When a turn is in flight this injects (returns `injected`): unlike
716
+ * `prompt()`, it does NOT create a Turn or enqueue on `turnQueue`; it pushes
717
+ * an `SDKUserMessage` onto the same streaming input, which the SDK routes
718
+ * into the in-flight turn. The injected message's echo carries a uuid that
719
+ * matches no queued turn, so the consumer drops it as an unrelated replay
720
+ * without promoting/settling anything. It is delivered at {@link
721
+ * STEER_PRIORITY} (`now`) so it pre-empts the current generation (interrupting
722
+ * a single-shot response, or slotting in between a multi-step turn's tool
723
+ * calls). The steered message's own output streams via `session/update`, not
724
+ * this response.
725
+ *
726
+ * When the session is idle, the opt-in path returns `promptRequired` WITHOUT
727
+ * calling `prompt()`, pushing SDK input, or mutating `turnQueue`: the content
728
+ * stays Host-owned so the Host can submit it through a standard
729
+ * `session/prompt`. Without the opt-in, the existing detached `prompt()` and
730
+ * `startedNewTurn` result are preserved for compatibility. */
731
+ steer(params: SteerRequest): Promise<SteerResponse>;
349
732
  /** Lazily start the per-session consumer that drains the SDK query stream for
350
733
  * the session's whole life. Idempotent: only the first `prompt()` starts it. */
351
734
  private ensureConsumer;
@@ -355,6 +738,22 @@ export declare class ClaudeAcpAgent {
355
738
  * Turn's deferred when that turn ends. Replaces the per-prompt message loop;
356
739
  * `params` only carries the (session-invariant) `sessionId`. */
357
740
  private runConsumer;
741
+ /** Route one orphaned command into the session's orphan-accounting lane:
742
+ * the per-uuid map on msg_lifecycle_v1 CLIs (drained by the command's own
743
+ * terminal lifecycle frame and the echo-less-result skip), the plain count
744
+ * elsewhere (the count lane can't express per-command states, so `state`
745
+ * only matters on the map lane). Both orphan-producing paths — cancel()'s
746
+ * queued-turn sweep and the consumer's force-cancel wedge path — must seed
747
+ * through here so the lane split stays a single mechanism.
748
+ *
749
+ * Known window: `msgLifecycleV1` is only learnable from the stream's first
750
+ * `system`/init (the control-channel initialize carries no capabilities),
751
+ * so a cancel that beats that drain seeds the COUNT lane on a
752
+ * lifecycle-capable CLI — where command coalescing can leave the count
753
+ * stale by N-1 (the pre-map bug, confined to this sub-second window and
754
+ * still healed by the next activation's reset). Structural until the SDK
755
+ * exposes capabilities before the stream starts. */
756
+ private trackOrphanCommand;
358
757
  cancel(params: CancelNotification): Promise<void>;
359
758
  /** Mark a session's SDK query stream as permanently ended and release the
360
759
  * resources tied to it: drop the consumer handle, dispose the settings
@@ -403,7 +802,12 @@ export declare class ClaudeAcpAgent {
403
802
  * instead of emitting a duplicate (see `emittedToolCalls`). Built via the same
404
803
  * `toolCallNotification` helper as the streamed path so the two are identical.
405
804
  * Tools the stream renders as a plan (TodoWrite) or suppresses (Task*) are
406
- * skipped so a permission prompt for them never surfaces a stray tool_call. */
805
+ * emitted too: a permission request referencing a tool call the client has
806
+ * never seen can trip strict clients (issue #851), so the reference must
807
+ * always resolve. Since the streamed path never completes those calls, they
808
+ * are resolved at tool_result time instead (see `toAcpNotifications`).
809
+ * `parentToolUseId` attributes a subagent's tool call to the Agent/Task call
810
+ * that spawned it, matching the streamed path's `_meta`. */
407
811
  private ensureToolCallEmitted;
408
812
  canUseTool(sessionId: string): CanUseTool;
409
813
  /**
@@ -438,10 +842,11 @@ export declare class ClaudeAcpAgent {
438
842
  * user-driven model change takes, then notifies the client. */
439
843
  private syncModelAfterRefusalFallback;
440
844
  /** Replace the Fast mode option in `session.configOptions` so it reflects
441
- * `enabled`. A no-op when the option isn't present, so callers must confirm
442
- * the current model surfaces it first. Rebuilds through the single-parameter
443
- * {@link createFastModeConfigOption} — the one source of the option's shape —
444
- * so the shape can't drift from what `buildConfigOptions` first emitted. */
845
+ * `enabled` (and the session's current disabled reason). A no-op when the
846
+ * option isn't present, so callers must confirm the current model surfaces
847
+ * it first. Rebuilds through {@link createFastModeConfigOption} — the one
848
+ * source of the option's shape — so the shape can't drift from what
849
+ * `buildConfigOptions` first emitted. */
445
850
  private refreshFastModeOption;
446
851
  /** Toggle Fast mode for a session: push the SDK flag, record the user's
447
852
  * intent, and refresh the Fast mode config option in place. Only reached
@@ -502,7 +907,14 @@ export declare class ClaudeAcpAgent {
502
907
  * here).
503
908
  * - `cooldown`: a transient suspension of an already-enabled fast mode.
504
909
  * Leave the toggle as-is rather than flapping it — and never let a stray
505
- * cooldown spuriously enable a toggle the user has off. */
910
+ * cooldown spuriously enable a toggle the user has off.
911
+ *
912
+ * `reason` is the SDK's `fast_mode_disabled_reason`, reported alongside the
913
+ * state. Only explainable reasons are retained (see
914
+ * {@link normalizeFastModeDisabledReason}), so the comparison below tracks
915
+ * exactly what the user can see: a routine `sdk_opt_in_required` report on
916
+ * every turn's result can't churn the option, while a real blocker updates
917
+ * the description even when the toggle's own value is unchanged. */
506
918
  private syncFastModeState;
507
919
  private getOrCreateSession;
508
920
  /**
@@ -539,13 +951,31 @@ export declare const FAST_MODE_OFF = "off";
539
951
  * docs) keeps the toggle on so it reflects the user's intent — only an
540
952
  * explicit `off` clears it. */
541
953
  export declare function fastModeStateEnabled(state: FastModeState): boolean;
954
+ /** Normalize an SDK-reported `fast_mode_disabled_reason` to the one we retain:
955
+ * a reason we have an explanation for, else `undefined`. Keeping only
956
+ * explainable reasons means state comparisons (see `syncFastModeState`) track
957
+ * exactly what the user can see, so routine reports like
958
+ * `sdk_opt_in_required` never churn the config option. */
959
+ export declare function normalizeFastModeDisabledReason(reason: FastModeDisabledReason | undefined): FastModeDisabledReason | undefined;
542
960
  /** Build the Fast mode config option as a two-value on/off `select`. Emitted
543
- * for EVERY Client — the boolean option shape is gone (story 006, R2.1). Only
544
- * the emitted SHAPE is fixed to a select; boolean VALUES are still honored on
545
- * set (see {@link resolveFastModeEnabled}). This factory is the single source
546
- * of the option's shape, re-rendered by `refreshFastModeOption` /
547
- * `syncFastModeState` so the shape can never desync. */
548
- export declare function createFastModeConfigOption(enabled: boolean): SessionConfigOption;
961
+ * for EVERY Client — the boolean option shape is gone (story 006, R2.1;
962
+ * retained through the v0.64.0 sync by story 008 R3.4). Only the emitted SHAPE
963
+ * is fixed to a select; boolean VALUES are still honored on set (see
964
+ * {@link resolveFastModeEnabled}). This factory is the single source of the
965
+ * option's shape, re-rendered by `refreshFastModeOption` / `syncFastModeState`
966
+ * so the shape can never desync.
967
+ *
968
+ * `disabledReason` (the SDK's `fast_mode_disabled_reason`, upstream v0.64.0) is
969
+ * folded into the description while the toggle reads off, so a user whose
970
+ * account or provider can't serve Fast mode sees why instead of a switch that
971
+ * silently refuses to stay on. Ignored while enabled: a reason reported
972
+ * alongside an `on`/`cooldown` state isn't blocking anything right now.
973
+ *
974
+ * Upstream's second parameter (`useBooleanOption`) is deliberately absent: the
975
+ * shape is unconditionally a select, so there is no branch to select. What
976
+ * guards that is behavioural, not structural — `tests/fast-mode-select-only.
977
+ * test.ts` proves no argument combination can yield the boolean shape. */
978
+ export declare function createFastModeConfigOption(enabled: boolean, disabledReason?: FastModeDisabledReason): SessionConfigOption;
549
979
  /** Resolve the requested Fast mode value from a `session/set_config_option`
550
980
  * request. Accepts the select's "on"/"off" strings or a native boolean,
551
981
  * kept for backward compatibility (R2.3). */
@@ -557,6 +987,9 @@ export declare function resolveFastModeEnabled(params: {
557
987
  export type FastModeOptionState = {
558
988
  supported: boolean;
559
989
  enabled: boolean;
990
+ /** Latest explainable `fast_mode_disabled_reason`, folded into the option's
991
+ * description while the toggle reads off. */
992
+ disabledReason?: FastModeDisabledReason;
560
993
  };
561
994
  export declare function buildConfigOptions(modes: SessionModeState, models: SessionModelState, modelInfos: ModelInfo[], currentEffortLevel?: string, agents?: AgentInfo[], currentAgent?: string, fastMode?: FastModeOptionState,
562
995
  /** Display state for the Thinking toggle: the session's tri-state intent
@@ -632,6 +1065,8 @@ export declare function toAcpNotifications(content: string | ContentBlockParam[]
632
1065
  taskState?: TaskState;
633
1066
  emittedToolCalls?: Set<string>;
634
1067
  messageId?: string;
1068
+ toolUseResult?: unknown;
1069
+ toolResultMeta?: unknown;
635
1070
  }): SessionNotification[];
636
1071
  export declare function streamEventToAcpNotifications(message: SDKPartialAssistantMessage, sessionId: string, toolUseCache: ToolUseCache, client: AcpClient, logger: Logger, options?: {
637
1072
  clientCapabilities?: ClientCapabilities;
@@ -639,6 +1074,7 @@ export declare function streamEventToAcpNotifications(message: SDKPartialAssista
639
1074
  taskState?: TaskState;
640
1075
  emittedToolCalls?: Set<string>;
641
1076
  messageId?: string;
1077
+ streamedToolInputs?: StreamedToolInputCache;
642
1078
  }): SessionNotification[];
643
1079
  /** Run a `session/prompt` while honoring `$/cancel_request` for it. ACP clients
644
1080
  * normally stop a turn with the `session/cancel` notification, but `signal`