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.
/*---------------------------------------------------------------------------------------------
claudeSubagentRegistry.ts ×16
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import {
ResponsePartKind,
ToolCallStatus,
ToolResultContentType,
type ResponsePart,
type Turn,
} from '../../common/state/protocol/state.js';
/**
* Tool names whose `tool_use` blocks spawn a subagent. The SDK's
* `Task` (and legacy `Agent`) tools encode subagent invocations as
* normal tool_use entries; we observe them here at spawn time and
* track each one as a {@link SubagentSpawn}.
*/
export const SUBAGENT_TOOL_NAMES: ReadonlySet<string> = new Set(['Task', 'Agent']);
/**
* Regex matching the SDK's synthetic per-subagent suffix appended to
* `Task`/`Agent` `tool_result` text blocks. Empirically observed
* format: `agentId: <hex> (use SendMessage with to: '<hex>') ...`.
* Tolerant by design — case-insensitive, lenient whitespace, anchored
* only by line start — so minor wording drift between SDK versions
* doesn't silently break correlation.
*/
export const SUBAGENT_ID_SUFFIX_REGEX = /^\s*agentId:\s+([a-z0-9]+)\b/im;
/**
* One Task tool_use in the parent session that did (or may have)
* spawned a subagent. All lifecycle state for *this* spawn lives here:
*
* - {@link agentId}: the SDK's identity for the spawned subagent.
* Set when learned (`canUseTool` `options.agentID`, strategy
* resolution, or transcript priming).
* - {@link background}: foreground vs. background mode. Defaults to
* `false` (foreground is the common case); flipped to `true` when
* the SDK emits `system.task_started`. Background spawns have
* deferred completion via `system.task_notification`.
* - {@link subagentType} / {@link description} / {@link prompt}:
* metadata from the `tool_use.input` (`subagent_type`,
* `description` and `prompt` fields). Available once the canonical
* `assistant` message arrives with the complete input bag (the
* early `content_block_start` has empty input). Used for UI labels
* and to seed the subagent's opening request.
* - {@link markAnnounced} / {@link markCompleted}: idempotency
* guards for the workbench-facing `subagent_started` /
* `subagent_completed` signals.
*/
export class SubagentSpawn {
background = false;
subagentType: string | undefined;
description: string | undefined;
prompt: string | undefined;
private _agentId: string | undefined;
private _announced = false;
private _completed = false;
constructor(readonly toolUseId: string) { }
get agentId(): string | undefined {
}
/**
* Set the SDK's agent id for this spawn. First-writer-wins: once
* set, subsequent calls are no-ops. Multiple call sites converge on
* the same value (canUseTool's `options.agentID`, the strategy chain,
* and transcript priming all surface the SDK's single identity), so
* the invariant is enforced here rather than at every caller.
*/
setAgentId(agentId: string): void {
this._agentId = agentId;
}
}
markAnnounced(): boolean {
}
return true;
}
markCompleted(): boolean {
}
return true;
}
/**
* Optional fields that may be supplied to {@link SubagentRegistry.recordSpawn}.
* Each field is **first-writer-wins**: once set on a spawn, subsequent
* `recordSpawn` calls with a new value for the same field are ignored.
* The invariant is enforced inside the registry so multiple converging
* call sites (canUseTool, canonical assistant, transcript priming)
* agree on a single record per `toolUseId`.
*/
export interface ISubagentSpawnInit {
readonly agentId?: string;
readonly subagentType?: string;
readonly description?: string;
readonly prompt?: string;
}
/**
* Per-parent-session collection of {@link SubagentSpawn} entries plus
* a reverse index from inner `tool_use_id` to its parent Task. Owned
* by `ClaudeAgentSession` (the registry dies with the session).
*
* Replaces the singleton-keyed-by-URI `IClaudeSubagentResolver`
* tracker surface from earlier Phase 12: lifecycle is implicit, no
* `parentUri` parameter on any method, no `disposeParent` needed, and
* the parallel `noteX` / `getX` accessor pairs collapse to one
* `getSpawn(toolUseId)` plus direct field reads/writes on the spawn.
*/
export class SubagentRegistry extends Disposable {
private readonly _innerToParent = new Map<string, string>();
override dispose(): void {
this._innerToParent.clear();
super.dispose();
}
/**
* Insert a spawn (or return the existing one) for `toolUseId`.
* Any fields supplied in `init` are written to the spawn under
* first-writer-wins semantics (see {@link ISubagentSpawnInit}).
* Idempotent so live writes (canUseTool / strategy resolution /
* transcript priming / canonical assistant) can converge on the
* same record.
*/
recordSpawn(toolUseId: string, init?: ISubagentSpawnInit): SubagentSpawn {
if (!spawn) {
spawn = new SubagentSpawn(toolUseId);
this._spawns.set(toolUseId, spawn);
}
if (init?.agentId !== undefined) {
}
if (init?.subagentType !== undefined && spawn.subagentType === undefined) {
claudeSubagentRegistry.ts ×5
}
if (init?.description !== undefined && spawn.description === undefined) {
claudeSubagentRegistry.ts ×5
}
}
}
getSpawn(toolUseId: string): SubagentSpawn | undefined {
}
removeSpawn(toolUseId: string): void {
this._evictInnerEdgesFor(toolUseId);
}
/** Mapper records the parent of an inner `tool_use` block when an inner subagent message arrives. */
noteInnerTool(innerToolUseId: string, parentToolUseId: string): void {
}
/** canUseTool reads this to attach `parentToolCallId` onto a `pending_confirmation` / `ChatInputRequested`. */
getParentSpawn(innerToolUseId: string): SubagentSpawn | undefined {
return parentId !== undefined ? this._spawns.get(parentId) : undefined;
}
/**
* Turn-end cleanup: remove and return foreground spawns whose
* completion never closed them. Background spawns survive across
* turns by design (their completion arrives later via
* `system.task_notification`). Inner-edge entries pointing at
* drained spawns are evicted too. Caller logs each returned orphan.
*/
drainForegroundSpawns(): readonly SubagentSpawn[] {
for (const spawn of this._spawns.values()) {
drained.push(spawn);
}
}
this._evictInnerEdgesFor(spawn.toolUseId);
}
}
/**
* Replay-path bulk populate: scan a parent transcript for the
* SDK's synthetic `agentId: <hex>` suffix on Task/Agent tool_result
* text blocks and record each `(toolUseId, agentId)` pair. Idempotent.
*/
primeFromTranscript(transcript: readonly Turn[]): void {
for (const [toolCallId, agentId] of scanTranscriptForAgentIds(transcript)) {
claudeSubagentRegistry.ts ×2
}
private _evictInnerEdgesFor(parentToolUseId: string): void {
this._innerToParent.delete(innerId);
}
}
/**
* Pure scan: locate `(toolCallId, agentId)` pairs encoded by the SDK
* in Task/Agent `tool_result` text blocks. Exported for the resolver's
* `TextSuffixStrategy` (which scans on demand) — registry priming
* uses {@link SubagentRegistry.primeFromTranscript}.
*/
export function scanTranscriptForAgentIds(transcript: readonly Turn[]): ReadonlyMap<string, string> {
for (const turn of transcript) {
const pair = extractAgentIdPair(part);
if (pair) {
}
}
}
function extractAgentIdPair(part: ResponsePart): { toolCallId: string; agentId: string } | undefined {
claudeSubagentRegistry.ts ×4
if (part.kind !== ResponsePartKind.ToolCall) {
}
if (!SUBAGENT_TOOL_NAMES.has(state.toolName)) {
}
if (state.status !== ToolCallStatus.Completed && state.status !== ToolCallStatus.PendingResultConfirmation) {
claudeSubagentRegistry.ts ×4
}
if (!content) {
return undefined;
}
const block = content[i];
if (block.type !== ToolResultContentType.Text) {
continue;
}
if (m) {
return { toolCallId: state.toolCallId, agentId: m[1] };
}
}
}