106
return result;
107
}
109
>
/**
110
>
* Grace period before an idle resource watch is torn down after its last
111
>
* subscriber unsubscribes (mirrors {@link SESSION_GC_GRACE_MS}). Within
112
>
* this window, a re-subscribe (or reconnect) reuses the still-running
113
>
* {@link IFileService} watcher so transient drop-outs don't miss change
114
>
* events. Resource watch action envelopes flow through the normal
115
>
* envelope replay buffer for the same reason.
116
>
*/
117
>
const RESOURCE_WATCH_GRACE_MS = 30_000;
118
>
119
>
/** Bound on how long {@link AgentService.subscribe} waits for a pending subagent chat to register before giving up. */
120
>
const SUBAGENT_CHAT_PENDING_TIMEOUT_MS = 15_000;
121
>
122
>
/**
123
>
* Grace period before an idle session (one with turns, no remaining
124
>
* subscribers) is released from memory via {@link AgentService._maybeEvictIdleSession}.
125
>
* Deferring the release aligns it with the client disconnect-grace window: a
126
>
* client that disconnects and quickly reconnects (or a rapid unsubscribe/
127
>
* re-subscribe) reuses the live provider SDK session instead of forcing an
128
>
* immediate {@link IAgent.releaseSession} (SDK `disconnect`) followed by a
129
>
* resume-from-disk. Releasing synchronously on every last-unsubscribe churns
130
>
* the shared provider runtime and races concurrent session operations.
131
>
*
132
>
* Overridable via {@link AgentHostSessionReleaseGraceMsEnvVar} (test hook).
133
>
*/
134
>
const SESSION_RELEASE_GRACE_MS = (() => {
135
>
const raw = process.env[AgentHostSessionReleaseGraceMsEnvVar];
136
>
const parsed = raw !== undefined ? parseInt(raw, 10) : NaN;
137
>
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 30_000;
138
>
})();
139
>
140
>
/**
141
>
* Session-database metadata key under which the orchestrator persists its own
142
>
* catalog of additional (non-default) peer chats for a session. The value is a
143
>
* JSON array of {@link IPersistedPeerChat}. This is the orchestrator's single
144
>
* source of truth for peer-chat enumeration on restore. When the key is absent
145
>
* the session predates orchestrator-owned persistence and a one-time migration
146
>
* drains the agent's legacy `*.chats` (see
147
>
* {@link AgentService._migrateLegacyPeerChats}).
148
>
*/
149
>
const PEER_CHATS_METADATA_KEY = 'peerChats';
150
>
151
>
/**
152
>
* Session-database metadata key written on a peer chat's *backing* SDK session
153
>
* (see {@link IAgentCreateChatResult.backingSession}). Its presence marks that
154
>
* session as an internal peer-chat backing that must never surface as a
155
>
* top-level session; the value is the owning peer chat's channel URI string.
156
>
* Persisted, so it survives a host restart without re-stamping.
157
>
*/
158
>
const PEER_CHAT_BACKING_METADATA_KEY = 'peerChatBacking';
159
>
160
>
/**
161
>
* A single entry in the orchestrator's persisted peer-chat catalog. `uri` is
162
>
* the peer chat's channel URI; `providerData` is the opaque, agent-owned blob
163
>
* (see {@link IAgentCreateChatResult.providerData}) handed back to the agent on
164
>
* restore — the orchestrator never parses it. `providerData` may be omitted,
165
>
* in which case the agent recovers its backing from its own persistence on
166
>
* {@link IAgent.materializeChat}. `origin` records the chat's provenance
167
>
* (currently only {@link ChatOriginKind.SideChat}, carrying the source chat and
168
>
* stable source turn id) so it survives a restart; omitted for plain peer chats.
169
>
*/
170
>
interface IPersistedPeerChat {
171
>
readonly uri: string;
172
>
readonly providerData?: string;
173
>
readonly origin?: ChatOrigin;
174
>
}
175
>
176
>
/**
177
>
* The agent service implementation that runs inside the agent-host utility
178
>
* process. Dispatches to registered {@link IAgent} instances based
179
>
* on the provider identifier in the session configuration.
180
>
*/
181
>
export class AgentService extends Disposable implements IAgentService {
182
>
declare readonly _serviceBrand: undefined;
183
>
184
>
private readonly _resourceWriteQueue = this._register(new ResourceQueue());
185
>
186
>
/** Protocol: fires when state is mutated by an action. */
187
>
private readonly _onDidAction = this._register(new Emitter<ActionEnvelope>());
188
>
readonly onDidAction = this._onDidAction.event;
189
>
190
>
/** Protocol: fires for ephemeral notifications (sessionAdded/Removed). */
191
>
private readonly _onDidNotification = this._register(new Emitter<INotification>());
192
>
readonly onDidNotification = this._onDidNotification.event;
193
>
194
>
/** Protocol: fires for MCP server-originated notifications routed over `mcp://` channels. */
195
>
private readonly _onMcpNotification = this._register(new Emitter<IMcpNotification>());
196
>
readonly onMcpNotification = this._onMcpNotification.event;
197
>
198
>
/** Authoritative state manager for the sessions process protocol. */
199
>
private readonly _stateManager: AgentHostStateManager;
200
>
201
>
/** Exposes the state manager for co-hosting a WebSocket protocol server. */
202
>
get stateManager(): AgentHostStateManager { return this._stateManager; }
203
>
204
>
/** Exposes the configuration service so agent providers can share root config plumbing. */
205
>
get configurationService(): IAgentConfigurationService { return this._configurationService; }
206
>
207
>
/** Exposes the GitHub endpoint service so agent providers share GitHub (Enterprise) resource resolution. */
208
>
get gitHubEndpointService(): IAgentHostGitHubEndpointService { return this._gitHubEndpointService; }
209
>
210
>
/** Registered providers keyed by their {@link AgentProvider} id. */
211
>
private readonly _providers = new Map<AgentProvider, IAgent>();
212
>
/** Maps each active session URI (toString) to its owning provider. */
213
>
private readonly _sessionToProvider = new Map<string, AgentProvider>();
214
>
/**
215
>
* Sessions that have opted in to bring-up progress, keyed by provider id.
216
>
* A session is added here when its `createSession` carries a
217
>
* {@link IAgentCreateSessionConfig.progressToken} and removed once it
218
>
* materializes (the SDK is now resolved) or is disposed. The SDK download is
219
>
* host-level and shared across every session of a provider, so this only
220
>
* records *interest*: as long as one or more sessions of a provider is
221
>
* registered, {@link emitDownloadProgress} surfaces that provider's download as a single
222
>
* progress stream keyed by the download's own identity (the package id),
223
>
* rather than one stream per session.
224
>
*/
225
>
private readonly _downloadProgressInterest = new Map<AgentProvider, Set<string>>();
226
>
/** Subscriptions to provider progress events; cleared when providers change. */
227
>
private readonly _providerSubscriptions = this._register(new DisposableStore());
228
>
/**
229
>
* Per-session tail of in-flight persisted peer-chat catalog writes, keyed by
230
>
* session URI string. Read-modify-write updates to the {@link
231
>
* PEER_CHATS_METADATA_KEY} blob are chained per session so a `createChat`,
232
>
* `disposeChat`, and `onDidChangeChatData` racing for the same
233
>
* session can't clobber each other's edits.
234
>
*/
235
>
private readonly _peerChatCatalogWrites = new Map<string, Promise<void>>();
236
>
private readonly _authService: AgentHostAuthenticationService;
237
>
/** Default provider used when no explicit provider is specified. */
238
>
private _defaultProvider: AgentProvider | undefined;
239
>
/** Observable registered agents, drives `root/agentsChanged` via {@link AgentSideEffects}. */
240
>
private readonly _agents = observableValue<readonly IAgent[]>('agents', []);
241
>
/** Shared side-effect handler for action dispatch and session lifecycle. */
242
>
private readonly _sideEffects: AgentSideEffects;
243
>
/** Owns static / per-turn changeset compute, publish, persist, restore. */
244
>
private readonly _changesets: IAgentHostChangesetService;
245
>
/** Shared active changeset subscription registry. */
246
>
private readonly _changesetSubscriptions: IAgentHostChangesetSubscriptionService;
247
>
/** Owns changeset operation contributions and handler activation. */
248
>
private readonly _changesetOperationService: IAgentHostChangesetOperationService;
249
>
private readonly _reviewService: IAgentHostReviewService;
250
>
/** Owns AgentService-side orchestration of the changeset feature. */
251
>
private readonly _changesetCoordinator: AgentHostChangesetCoordinator;
252
>
/** Owns session git-state probing and git-backed catalogue decoration. */
253
>
private readonly _gitStateService: IAgentHostGitStateService;
254
>
/** Manages PTY-backed terminals for the agent host protocol. */
255
>
private readonly _terminalManager: AgentHostTerminalManager;
256
>
/** Persists host-injected `/rename` / `!command` turns for restore & fork/truncate. */
257
>
private readonly _localTurns: AgentHostLocalTurns;
258
>
/** Server-side host for the agent host's server tools. */
259
>
private readonly _serverToolHost: AgentServerToolHost;
260
>
private readonly _configurationService: AgentConfigurationService;
261
>
/**
262
>
* Host-owned worktree isolation controller. Set post-construction via
263
>
* {@link setWorktreeIsolation} because it depends on the branch-name
264
>
* generator, which is wired after this service is built. All worktree
265
>
* behavior — schema contribution, first-send resolution, project /
266
>
* announcement, archive, and cleanup — is driven from the host so individual
267
>
* agents stay unaware of the folder-vs-worktree distinction.
268
>
*/
269
>
private _worktree: WorktreeIsolation | undefined;
270
>
/** Single source of truth for GitHub (Enterprise) endpoints and protected resources. */
271
>
private readonly _gitHubEndpointService: IAgentHostGitHubEndpointService;
272
>
/** Pluggable completion item providers (e.g. workspace file completions, agent-specific @-mentions). */
273
>
private readonly _completions: IAgentHostCompletions;
274
>
private _skillCompletionProviderRegistered = false;
275
>
/** Backs {@link getNetworkDiagnosticsInfo} / {@link diagnosticsFetch}; wired via {@link setNetworkDiagnosticsService}. */
276
>
private _networkDiagnostics: INetworkDiagnosticsService | undefined;
277
>
278
>
/**
279
>
* Authoritative server-side per-resource subscription refcount, keyed by
280
>
* resource URI string and valued by the set of subscribed protocol
281
>
* client IDs. Populated by {@link subscribe} (or {@link addSubscriber}
282
>
* for handshake fast-paths) and drained by {@link unsubscribe}. When a
283
>
* resource's set becomes empty, the resource is dropped from the map and
284
>
* {@link _maybeEvictIdleSession} is invoked to release any cached state
285
>
* for it.
286
>
*/
287
>
private readonly _resourceSubscribers = new ResourceMap<Set<string>>();
288
>
private readonly _restoreSessionInFlight = new Map<string, Promise<void>>();
289
>
private readonly _restoreSubagentInFlight = new Map<string, Promise<void>>();
290
>
291
>
/** Subagent chats armed for a bounded wait (once execution is confirmed); resolved by {@link _onChatSpawned}, awaited by {@link subscribe}. */
292
>
private readonly _pendingSubagentChats = new Map<string /* subagentChatUri */, DeferredPromise<void>>();
293
>
private readonly _pendingSubagentChatTimeouts = this._register(new DisposableMap<string /* subagentChatUri */, IDisposable>());
294
>
/** Subagent chats announced via `_meta.subagentChatUri` but still awaiting confirmation, keyed by `${channel}:${toolCallId}`. */
295
>
private readonly _pendingSubagentToolCalls = new Map<string, string /* subagentChatUri */>();
296
>
297
>
/**
298
>
* Pending {@link _runSessionGc} timers, keyed by session URI. A timer is
299
>
* armed when a session loses its last subscriber while still empty (no
300
>
* turns, no active turn) — see {@link _maybeScheduleSessionGc}. Cleared
301
>
* whenever any client subscribes again or the timer fires.
302
>
*/
303
>
private readonly _pendingSessionGc = this._register(new DisposableResourceMap<IDisposable>());
304
>
305
>
/**
306
>
* Pending {@link _maybeEvictIdleSession} timers, keyed by session URI. A
307
>
* timer is armed when an idle session (with turns) loses its last subscriber
308
>
* — see {@link unsubscribe}. Cleared when any client subscribes again
309
>
* ({@link addSubscriber}) or the timer fires. Deferring the release avoids
310
>
* churning the provider SDK session on rapid disconnect/reconnect cycles.
311
>
*/
312
>
private readonly _pendingSessionRelease = this._register(new DisposableResourceMap<IDisposable>());
313
>
314
>
/**
315
>
* Active resource watches keyed by the channel URI string
316
>
* (`ahp-resource-watch:/<encoded>`).
317
>
*
318
>
* Each entry owns the {@link IFileService} watcher together with the
319
>
* decoded descriptor, the subscriber refcount, and the optional
320
>
* grace-window dispose timer. The watch URI itself is fully
321
>
* self-describing — {@link createResourceWatch} just encodes the
322
>
* caller's params into the URI and returns it. State only exists
323
>
* here once at least one client has subscribed.
324
>
*
325
>
* Lifecycle:
326
>
* - First subscriber to a channel: {@link onResourceWatchSubscribed}
327
>
* parses the URI, creates the {@link IFileService} watcher, and
328
>
* installs the entry with `subscribers = 1`.
329
>
* - Subsequent subscribers bump the refcount and cancel any pending
330
>
* grace-window dispose timer.
331
>
* - {@link onResourceWatchUnsubscribed} drops the refcount; when it
332
>
* reaches zero we arm a {@link RESOURCE_WATCH_GRACE_MS} dispose
333
>
* timer rather than tearing down immediately, giving disconnected
334
>
* clients time to reconnect.
335
>
*/
336
>
private readonly _resourceWatches = this._register(new DisposableMap<string, IActiveResourceWatch>());
337
>
338
>
/** Exposes the terminal manager for use by agent providers. */
339
>
get terminalManager(): IAgentHostTerminalManager { return this._terminalManager; }
340
>
341
>
/** Exposes the completions service for use by agent providers (e.g. to register agent-scoped completion item providers). */
342
>
get completionsService(): IAgentHostCompletions { return this._completions; }
343
>
344
>
/**
345
>
* Trigger characters announced to clients via `InitializeResult.completionTriggerCharacters`.
346
>
* Aggregated from all registered {@link IAgentHostCompletionItemProvider}s.
347
>
*/
348
>
get completionTriggerCharacters(): readonly string[] { return this._completions.triggerCharacters; }
349
>
350
>
constructor(
351
private readonly _logService: ILogService,
352
private readonly _fileService: IFileService,