1
>
/*---------------------------------------------------------------------------------------------
claudeSdkPipeline.ts
2
>
* Copyright (c) Microsoft Corporation. All rights reserved.
3
>
* Licensed under the MIT License. See License.txt in the project root for license information.
4
>
*--------------------------------------------------------------------------------------------*/
5
>
6
>
import type { AgentInfo, McpServerStatus, PermissionMode, Query, SDKUserMessage, SlashCommand, WarmQuery } from '@anthropic-ai/claude-agent-sdk';
7
>
import { CancellationError, isCancellationError } from '../../../../base/common/errors.js';
8
>
import { Emitter, Event } from '../../../../base/common/event.js';
9
>
import { Disposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js';
10
>
import { StopWatch } from '../../../../base/common/stopwatch.js';
11
>
import { URI } from '../../../../base/common/uri.js';
12
>
import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
13
>
import { ILogService } from '../../../log/common/log.js';
14
>
import { ClaudeRuntimeEffortLevel } from '../../common/claudeModelConfig.js';
15
>
import { AgentSignal } from '../../common/agentService.js';
16
>
import { ISessionDatabase } from '../../common/sessionDataService.js';
17
>
import { ActionType } from '../../common/state/sessionActions.js';
18
>
import { DeferredPromise } from '../../../../base/common/async.js';
19
>
import { ClaudePromptQueue, IPendingSdkMessage } from './claudePromptQueue.js';
20
>
import { ClaudeSdkMessageRouter } from './claudeSdkMessageRouter.js';
21
>
import type { SubagentRegistry } from './claudeSubagentRegistry.js';
22
>
23
>
/**
24
>
* Callback the agent supplies via {@link ClaudeSdkPipeline.attachRematerializer}
25
>
* so the pipeline can rebuild its underlying {@link WarmQuery} /
26
>
* {@link AbortController} on abort or crash recovery without depending on
27
>
* the materializer service directly. The callback MUST start the SDK in
28
>
* `resume` mode (i.e. pass `Options.resume = sessionId` instead of
29
>
* `Options.sessionId`) and MUST NOT re-fire the agent's
30
>
* `onDidMaterializeSession` event — that event is once-per-provisional
31
>
* promotion (see `claudeAgent.ts` materialize path).
32
>
*/
33
>
export interface IRematerializer {
34
>
(reason: 'restart' | 'recover'): Promise<{ readonly warm: WarmQuery; readonly abortController: AbortController }>;
35
>
}
36
>
37
>
/**
38
>
* Owns one SDK Query lifecycle for a Claude session. Knows nothing about
39
>
* protocol turns, the workbench mapper, file-edit observers, or
40
>
* permission registries — the consuming session subscribes to
41
>
* {@link onDidProduceSignal} and fans out to its own collaborators.
42
>
*
43
>
* Responsibilities:
44
>
* • Hold the {@link WarmQuery} + {@link AbortController} for the
45
>
* active SDK subprocess. Both are mutable: rebind on abort/crash
46
>
* recovery via the supplied {@link IRematerializer}.
47
>
* • Drive a {@link ClaudePromptQueue} whose iterable is handed to
48
>
* `WarmQuery.query()`.
49
>
* • Apply the current model / effort / permissionMode to the SDK
50
>
* eagerly when the consumer calls {@link setModel} /
51
>
* {@link setEffort} / {@link setPermissionMode}. The SDK only takes
52
>
* these into account on the NEXT user request, so mid-turn calls
53
>
* are safe — no need to align the SDK setter with the prompt yield.
54
>
* Re-applied to a fresh Query on rebind.
55
>
* • Drain the SDK message stream, dispatch each message to the
56
>
* {@link ClaudeSdkMessageRouter}, settle the matching entry's
57
>
* deferred on `result`, and emit `ChatTurnComplete` only when
58
>
* the queue fully drains (intermediate results during steering
59
>
* preemption do NOT fire turn-complete — CONTEXT.md M10).
60
>
*
61
>
* Disposing the pipeline aborts the controller (terminating the SDK
62
>
* subprocess per `sdk.d.ts:982`) and async-disposes the WarmQuery.
63
>
*/
64
>
/**
65
>
* Snapshot of everything the SDK has currently resolved for this
66
>
* session. Returned by {@link ClaudeSdkPipeline.snapshotResolvedCustomizations}.
67
>
*/
68
>
export interface ISdkResolvedCustomizations {
69
>
readonly commands: readonly SlashCommand[];
70
>
readonly agents: readonly AgentInfo[];
71
>
readonly mcpServers: readonly McpServerStatus[];
72
>
/**
73
>
* Native plugins the live session actually loaded, as reported by the
74
>
* SDK `system/init` message. Used to filter the disk-discovered native
75
>
* plugins post-materialize: a plugin declared in `enabledPlugins` but
76
>
* absent here (bad path, manifest error, untrusted workspace) is hidden.
77
>
*
78
>
* `source` is the plugin id (`<plugin>@<marketplace>`) and is the
79
>
* authoritative match key — the SDK's `path` is unreliable for
80
>
* workspace-`local`-scoped plugins (it can report a non-cache path). The
81
>
* SDK `.d.ts` types the element as `{ name, path }` but the runtime adds
82
>
* `source`, so it is captured as optional.
83
>
*/
84
>
readonly plugins: readonly { readonly name: string; readonly path: string; readonly source?: string }[];
85
>
}
86
>
87
>
export class ClaudeSdkPipeline extends Disposable {
88
>
/**
89
>
* Phase 11 — hot-swap the SDK's plugin set in place via
90
>
* `Query.reloadPlugins()`. Commands / agents / mcpServers added or
91
>
* removed by the new plugin set become visible to the SDK
92
>
* immediately, without a session restart. Throws if the query is
93
>
* not yet bound (session not materialized).
94
>
*/
95
>
async reloadPlugins(): Promise<void> {
96
>
const query = await this._ensureQueryBound();
97
>
await query.reloadPlugins();
98
>
}
99
>
100
>
/**
101
>
* Phase 11 — snapshot the SDK's currently-resolved customization
102
>
* surface (slash commands / skills, subagents, MCP servers). This
103
>
* is the SDK's view of "what does this session actually have
104
>
* access to right now" — covers everything the SDK loaded itself
105
>
* (`~/.claude/**`, `.claude/agents/`, `settings.json` MCP) AND
106
>
* anything we fed in via `Options.plugins`. The host overlays
107
>
* client-side enablement separately.
108
>
*/
109
>
async snapshotResolvedCustomizations(): Promise<ISdkResolvedCustomizations> {
110
const query = await this._ensureQueryBound();
111
const [commands, agents, mcpServers] = await Promise.all([