claudeSubagentRegistry.ts ×16

Frontier kind: Code frontier

unlabeled · c_a65cc9b451ae

282 tests · 7228 LOC · 26 files · introduces 0 tests · 158 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
16 ranges158 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
503 ranges7228 lines · 26 files · Browse complete extent
All tests (intent)
282 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

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 native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 158 introduced LOC across 16 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/claude/claudeSubagentRegistry.ts 158 introduced LOC · 16 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- claudeSubagentRegistry.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 { 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;
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) {
79 this._agentId = agentId;
80 }
81 }
83 > markAnnounced(): boolean {
84 if (this._announced) {
85 return false;
88 return true;
89 }
91 > markCompleted(): boolean {
92 if (this._completed) {
93 return false;
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>();
128 private readonly _innerToParent = new Map<string, string>();
130 > override dispose(): void {
131 this._spawns.clear();
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);
146 if (!spawn) {
162 return spawn;
163 }
165 > getSpawn(toolUseId: string): SubagentSpawn | undefined {
166 return this._spawns.get(toolUseId);
167 }
169 > removeSpawn(toolUseId: string): void {
170 this._spawns.delete(toolUseId);
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);
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);
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[] = [];
194 for (const spawn of this._spawns.values()) {
203 return drained;
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)) {
213 this.recordSpawn(toolCallId, { agentId });
214 }
215 }
217 > private _evictInnerEdgesFor(parentToolUseId: string): void {
218 for (const [innerId, parentId] of this._innerToParent) {
219 if (parentId === parentToolUseId) {
222 }
223 }
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>();
234 for (const turn of transcript) {
242 return out;
243 }
245 function extractAgentIdPair(part: ResponsePart): { toolCallId: string; agentId: string } | undefined {
246 if (part.kind !== ResponsePartKind.ToolCall) {