src/vs/platform/agentHost/node/claude/claudeSubagentRegistry.ts

271 LOC · 267 covered · 4 uncovered · 65 ranges · 566 concepts · 29 introducers · 282 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- claudeSubagentRegistry.ts ×16
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 { Disposable } from '../../../../base/common/lifecycle.js';
7 > import {
8 > ResponsePartKind,
9 > ToolCallStatus,
10 > ToolResultContentType,
11 > type ResponsePart,
12 > type Turn,
13 > } from '../../common/state/protocol/state.js';
14 >
15 > /**
16 > * Tool names whose `tool_use` blocks spawn a subagent. The SDK's
17 > * `Task` (and legacy `Agent`) tools encode subagent invocations as
18 > * normal tool_use entries; we observe them here at spawn time and
19 > * track each one as a {@link SubagentSpawn}.
20 > */
21 > export const SUBAGENT_TOOL_NAMES: ReadonlySet<string> = new Set(['Task', 'Agent']);
22 >
23 > /**
24 > * Regex matching the SDK's synthetic per-subagent suffix appended to
25 > * `Task`/`Agent` `tool_result` text blocks. Empirically observed
26 > * format: `agentId: <hex> (use SendMessage with to: '<hex>') ...`.
27 > * Tolerant by design — case-insensitive, lenient whitespace, anchored
28 > * only by line start — so minor wording drift between SDK versions
29 > * doesn't silently break correlation.
30 > */
31 > export const SUBAGENT_ID_SUFFIX_REGEX = /^\s*agentId:\s+([a-z0-9]+)\b/im;
32 >
33 > /**
34 > * One Task tool_use in the parent session that did (or may have)
35 > * spawned a subagent. All lifecycle state for *this* spawn lives here:
36 > *
37 > * - {@link agentId}: the SDK's identity for the spawned subagent.
38 > * Set when learned (`canUseTool` `options.agentID`, strategy
39 > * resolution, or transcript priming).
40 > * - {@link background}: foreground vs. background mode. Defaults to
41 > * `false` (foreground is the common case); flipped to `true` when
42 > * the SDK emits `system.task_started`. Background spawns have
43 > * deferred completion via `system.task_notification`.
44 > * - {@link subagentType} / {@link description} / {@link prompt}:
45 > * metadata from the `tool_use.input` (`subagent_type`,
46 > * `description` and `prompt` fields). Available once the canonical
47 > * `assistant` message arrives with the complete input bag (the
48 > * early `content_block_start` has empty input). Used for UI labels
49 > * and to seed the subagent's opening request.
50 > * - {@link markAnnounced} / {@link markCompleted}: idempotency
51 > * guards for the workbench-facing `subagent_started` /
52 > * `subagent_completed` signals.
53 > */
54 > export class SubagentSpawn {
55 > background = false;
56 > subagentType: string | undefined;
57 > description: string | undefined;
58 > prompt: string | undefined;
59 >
60 > private _agentId: string | undefined;
61 > private _announced = false;
62 > private _completed = false;
63 >
64 > constructor(readonly toolUseId: string) { }
65 >
66 > get agentId(): string | undefined {
67 > return this._agentId; claudeSubagentRegistry.ts ×1
68 > }
70 > /**
71 > * Set the SDK's agent id for this spawn. First-writer-wins: once
72 > * set, subsequent calls are no-ops. Multiple call sites converge on
73 > * the same value (canUseTool's `options.agentID`, the strategy chain,
74 > * and transcript priming all surface the SDK's single identity), so
75 > * the invariant is enforced here rather than at every caller.
76 > */
77 > setAgentId(agentId: string): void {
78 > if (this._agentId === undefined) { claudeSubagentRegistry.ts ×1
79 > this._agentId = agentId;
80 > }
81 > }
83 > markAnnounced(): boolean {
84 > if (this._announced) { claudeSubagentRegistry.ts ×2
85 > return false; claudeSubagentRegistry.ts ×1
86 > }
87 > this._announced = true; claudeSubagentRegistry.ts ×2
88 > return true;
89 > }
91 > markCompleted(): boolean {
92 > if (this._completed) { claudeSubagentRegistry.ts ×2
93 > return false; claudeSubagentRegistry.ts ×1
94 > }
95 > this._completed = true; claudeSubagentRegistry.ts ×2
96 > return true;
97 > }
99 >
100 > /**
101 > * Optional fields that may be supplied to {@link SubagentRegistry.recordSpawn}.
102 > * Each field is **first-writer-wins**: once set on a spawn, subsequent
103 > * `recordSpawn` calls with a new value for the same field are ignored.
104 > * The invariant is enforced inside the registry so multiple converging
105 > * call sites (canUseTool, canonical assistant, transcript priming)
106 > * agree on a single record per `toolUseId`.
107 > */
108 > export interface ISubagentSpawnInit {
109 > readonly agentId?: string;
110 > readonly subagentType?: string;
111 > readonly description?: string;
112 > readonly prompt?: string;
113 > }
114 >
115 > /**
116 > * Per-parent-session collection of {@link SubagentSpawn} entries plus
117 > * a reverse index from inner `tool_use_id` to its parent Task. Owned
118 > * by `ClaudeAgentSession` (the registry dies with the session).
119 > *
120 > * Replaces the singleton-keyed-by-URI `IClaudeSubagentResolver`
121 > * tracker surface from earlier Phase 12: lifecycle is implicit, no
122 > * `parentUri` parameter on any method, no `disposeParent` needed, and
123 > * the parallel `noteX` / `getX` accessor pairs collapse to one
124 > * `getSpawn(toolUseId)` plus direct field reads/writes on the spawn.
125 > */
126 > export class SubagentRegistry extends Disposable {
127 > private readonly _spawns = new Map<string, SubagentSpawn>(); claudeSubagentRegistry.ts ×2
128 > private readonly _innerToParent = new Map<string, string>();
130 > override dispose(): void {
131 > this._spawns.clear(); claudeSubagentRegistry.ts ×2
132 > this._innerToParent.clear();
133 > super.dispose();
134 > }
136 > /**
137 > * Insert a spawn (or return the existing one) for `toolUseId`.
138 > * Any fields supplied in `init` are written to the spawn under
139 > * first-writer-wins semantics (see {@link ISubagentSpawnInit}).
140 > * Idempotent so live writes (canUseTool / strategy resolution /
141 > * transcript priming / canonical assistant) can converge on the
142 > * same record.
143 > */
144 > recordSpawn(toolUseId: string, init?: ISubagentSpawnInit): SubagentSpawn {
145 > let spawn = this._spawns.get(toolUseId); claudeSubagentRegistry.ts ×5
146 > if (!spawn) {
147 > spawn = new SubagentSpawn(toolUseId);
148 > this._spawns.set(toolUseId, spawn);
149 > }
150 > if (init?.agentId !== undefined) {
151 > spawn.setAgentId(init.agentId); claudeSubagentRegistry.ts ×1
152 > }
153 > if (init?.subagentType !== undefined && spawn.subagentType === undefined) { claudeSubagentRegistry.ts ×5
154 > spawn.subagentType = init.subagentType; claudeSubagentRegistry.ts ×2
155 > }
156 > if (init?.description !== undefined && spawn.description === undefined) { claudeSubagentRegistry.ts ×5
157 > spawn.description = init.description; claudeSubagentRegistry.ts ×2
158 > }
159 > if (init?.prompt !== undefined && spawn.prompt === undefined) { claudeSubagentRegistry.ts ×5
160 > spawn.prompt = init.prompt; claudeSubagentSignals.ts ×2
161 > }
162 > return spawn; claudeSubagentRegistry.ts ×5
163 > }
165 > getSpawn(toolUseId: string): SubagentSpawn | undefined {
166 > return this._spawns.get(toolUseId); claudeSubagentRegistry.ts ×1
167 > }
169 > removeSpawn(toolUseId: string): void {
170 > this._spawns.delete(toolUseId); claudeSubagentRegistry.ts ×1
171 > this._evictInnerEdgesFor(toolUseId);
172 > }
174 > /** Mapper records the parent of an inner `tool_use` block when an inner subagent message arrives. */
175 > noteInnerTool(innerToolUseId: string, parentToolUseId: string): void {
176 > this._innerToParent.set(innerToolUseId, parentToolUseId); claudeSubagentRegistry.ts ×1
177 > }
179 > /** canUseTool reads this to attach `parentToolCallId` onto a `pending_confirmation` / `ChatInputRequested`. */
180 > getParentSpawn(innerToolUseId: string): SubagentSpawn | undefined {
181 > const parentId = this._innerToParent.get(innerToolUseId); claudeSubagentRegistry.ts ×1
182 > return parentId !== undefined ? this._spawns.get(parentId) : undefined;
183 > }
185 > /**
186 > * Turn-end cleanup: remove and return foreground spawns whose
187 > * completion never closed them. Background spawns survive across
188 > * turns by design (their completion arrives later via
189 > * `system.task_notification`). Inner-edge entries pointing at
190 > * drained spawns are evicted too. Caller logs each returned orphan.
191 > */
192 > drainForegroundSpawns(): readonly SubagentSpawn[] {
193 > const drained: SubagentSpawn[] = []; claudeSubagentRegistry.ts ×3
194 > for (const spawn of this._spawns.values()) {
195 > if (!spawn.background) { claudeSubagentRegistry.ts ×2
196 > drained.push(spawn);
197 > }
198 > }
199 > for (const spawn of drained) { claudeSubagentRegistry.ts ×3
200 > this._spawns.delete(spawn.toolUseId); claudeSubagentRegistry.ts ×2
201 > this._evictInnerEdgesFor(spawn.toolUseId);
202 > }
203 > return drained; claudeSubagentRegistry.ts ×3
204 > }
206 > /**
207 > * Replay-path bulk populate: scan a parent transcript for the
208 > * SDK's synthetic `agentId: <hex>` suffix on Task/Agent tool_result
209 > * text blocks and record each `(toolUseId, agentId)` pair. Idempotent.
210 > */
211 > primeFromTranscript(transcript: readonly Turn[]): void {
212 > for (const [toolCallId, agentId] of scanTranscriptForAgentIds(transcript)) { claudeSubagentRegistry.ts ×2
213 > this.recordSpawn(toolCallId, { agentId }); claudeSubagentRegistry.ts ×1
214 > }
217 > private _evictInnerEdgesFor(parentToolUseId: string): void {
218 > for (const [innerId, parentId] of this._innerToParent) { claudeSubagentRegistry.ts ×2
219 > if (parentId === parentToolUseId) { claudeSubagentRegistry.ts ×1
220 > this._innerToParent.delete(innerId);
221 > }
222 > }
225 >
226 > /**
227 > * Pure scan: locate `(toolCallId, agentId)` pairs encoded by the SDK
228 > * in Task/Agent `tool_result` text blocks. Exported for the resolver's
229 > * `TextSuffixStrategy` (which scans on demand) — registry priming
230 > * uses {@link SubagentRegistry.primeFromTranscript}.
231 > */
232 > export function scanTranscriptForAgentIds(transcript: readonly Turn[]): ReadonlyMap<string, string> {
233 > const out = new Map<string, string>(); claudeSubagentRegistry.ts ×2
234 > for (const turn of transcript) {
235 > for (const part of turn.responseParts) { claudeSubagentRegistry.ts ×4
236 > const pair = extractAgentIdPair(part);
237 > if (pair) {
238 > out.set(pair.toolCallId, pair.agentId); claudeSubagentRegistry.ts ×5
239 > }
241 > }
242 > return out; claudeSubagentRegistry.ts ×2
243 > }
245 > function extractAgentIdPair(part: ResponsePart): { toolCallId: string; agentId: string } | undefined { claudeSubagentRegistry.ts ×4
246 > if (part.kind !== ResponsePartKind.ToolCall) {
247 > return undefined; claudeAgent.ts ×6
248 > }
249 > const state = part.toolCall; claudeSubagentRegistry.ts ×5
250 > if (!SUBAGENT_TOOL_NAMES.has(state.toolName)) {
251 > return undefined; claudeSubagentRegistry.ts ×1
252 > }
253 > if (state.status !== ToolCallStatus.Completed && state.status !== ToolCallStatus.PendingResultConfirmation) { claudeSubagentRegistry.ts ×4
254 > return undefined; claudeSubagentRegistry.ts ×1
255 > }
256 > const content = state.content; claudeSubagentRegistry.ts ×5
257 > if (!content) {
258 return undefined;
259 }
260 > for (let i = content.length - 1; i >= 0; i--) { claudeSubagentRegistry.ts ×5
261 > const block = content[i];
262 > if (block.type !== ToolResultContentType.Text) {
263 continue;
264 }
265 > const m = SUBAGENT_ID_SUFFIX_REGEX.exec(block.text); claudeSubagentRegistry.ts ×5
266 > if (m) {
267 > return { toolCallId: state.toolCallId, agentId: m[1] };
268 > }
269 > }
270 > return undefined; claudeSubagentRegistry.ts ×1
271 > }