claudeSubagentResolver.ts ×15

Frontier kind: Code frontier

unlabeled · c_28fe5bb5dd0c

213 tests · 20150 LOC · 76 files · introduces 0 tests · 149 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
15 ranges149 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1649 ranges20150 lines · 76 files · Browse complete extent
All tests (intent)
213 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: 149 introduced LOC across 15 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/claude/claudeSubagentResolver.ts 149 introduced LOC · 15 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- claudeSubagentResolver.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 { CancellationToken } from '../../../../base/common/cancellation.js';
7 > import { URI } from '../../../../base/common/uri.js';
8 > import { vObjAny, vString as vStringValidator } from '../../../../base/common/validation.js';
9 > import { ILogService } from '../../../log/common/log.js';
10 > import { AgentSession } from '../../common/agentService.js';
11 > import { parseSubagentSessionUri } from '../../common/state/sessionState.js';
12 > import {
13 > ResponsePartKind,
14 > ToolCallStatus,
15 > type Turn,
16 > } from '../../common/state/protocol/state.js';
17 > import { IClaudeAgentSdkService } from './claudeAgentSdkService.js';
18 > import { mapSessionMessagesToTurns } from './claudeReplayMapper.js';
19 > import { scanTranscriptForAgentIds, SUBAGENT_TOOL_NAMES, type SubagentRegistry } from './claudeSubagentRegistry.js';
20 >
21 > /**
22 > * One link in the resolver chain. Each strategy consults a different
23 > * SDK primitive to map a `toolCallId` back to an `agentId`. Strategies
24 > * are ordered cheapest-first inside {@link getSubagentTranscript}
25 > * (TextSuffix → PromptMatch → Native).
26 > */
27 > export interface ISubagentLookupStrategy {
28 > /** Short label, used as a category in any future telemetry counter. */
29 > readonly name: string;
30 > /** Returns the agentId or `undefined` if this strategy cannot resolve the input. */
31 > lookup(toolCallId: string, ctx: ISubagentLookupContext): Promise<string | undefined>;
32 > }
33 >
34 > export interface ISubagentLookupContext {
35 > readonly parentUri: URI;
36 > /** SDK session id (`AgentSession.id(parentUri)`). Precomputed so strategies don't repeat the work. */
37 > readonly parentSessionId: string;
38 > /** Pre-fetched parent transcript when available; otherwise the strategy must fetch its own. */
39 > readonly parentTranscript?: readonly Turn[];
40 > readonly token: CancellationToken;
41 > }
42 >
43 > /**
44 > * Strategy 1 — scan the parent transcript for the synthetic agentId
45 > * suffix. Cheapest path: the transcript is usually already in memory
46 > * from the live session or the parent's own replay fetch.
47 > */
48 > export class TextSuffixStrategy implements ISubagentLookupStrategy {
49 > readonly name = 'text_suffix';
50 >
51 > constructor(
52 private readonly _sdk: IClaudeAgentSdkService,
53 private readonly _logService: ILogService,
54 ) { }
56 > async lookup(toolCallId: string, ctx: ISubagentLookupContext): Promise<string | undefined> {
57 const transcript = await fetchParentTurns(this._sdk, this._logService, ctx, 'TextSuffix');
58 if (!transcript) {
61 return scanTranscriptForAgentIds(transcript).get(toolCallId);
62 }
64 >
65 > /**
66 > * Shared transcript-fetch helper for strategies that need the parent
67 > * transcript and weren't given one via {@link ISubagentLookupContext.parentTranscript}.
68 > * Returns `undefined` (and logs a warning tagged with `strategyLabel`)
69 > * on any SDK error so callers can short-circuit cleanly.
70 > */
71 export async function fetchParentTurns(
72 sdk: IClaudeAgentSdkService,
86 }
87 }
89 > // #region locally-defined validator adapters
90 >
91 > /**
92 > * Thin convenience wrappers over the base validators in
93 > * `src/vs/base/common/validation.ts`. The base API returns a
94 > * `{content, error}` discriminated union (built for full schema
95 > * validation); these wrappers collapse that to `T | undefined` for the
96 > * narrow `is-it-this-type?` checks below where we just need a
97 > * structural narrowing of arbitrary SDK JSON payloads.
98 > */
99 function vString(input: unknown): string | undefined {
100 const r = vStringValidator().validate(input);
101 return r.error ? undefined : r.content;
102 }
104 function vObj(input: unknown): Record<string, unknown> | undefined {
105 const r = vObjAny().validate(input);
109 return r.content as Record<string, unknown>;
110 }
112 > // #endregion
113 >
114 > /**
115 > * Strategy 2 — list every subagent the SDK knows for this parent and
116 > * compare each one's first user message against the parent's
117 > * `Agent.tool_use.input.prompt`. Bulletproof when TextSuffix misses
118 > * (e.g. SDK reformats the suffix) at the cost of two extra SDK calls.
119 > */
120 > export class PromptMatchStrategy implements ISubagentLookupStrategy {
121 > readonly name = 'prompt_match';
122 >
123 > constructor(
124 private readonly _sdk: IClaudeAgentSdkService,
125 private readonly _logService: ILogService,
126 ) { }
128 > async lookup(toolCallId: string, ctx: ISubagentLookupContext): Promise<string | undefined> {
129 const prompt = await this._loadParentPrompt(toolCallId, ctx);
130 if (!prompt) {
159 return undefined;
160 }
162 > private async _loadParentPrompt(toolCallId: string, ctx: ISubagentLookupContext): Promise<string | undefined> {
163 const transcript = await fetchParentTurns(this._sdk, this._logService, ctx, 'PromptMatch');
164 if (!transcript) {
167 return extractSpawningPromptFromTranscript(transcript, toolCallId);
168 }
170 >
171 > /**
172 > * Pure transcript scan: locate the spawning subagent tool call by id
173 > * and return its `prompt` input field, or `undefined` if not a
174 > * subagent tool, still streaming, or input is malformed. Exported so
175 > * it can be tested independently of SDK fetch behavior.
176 > */
177 > export function extractSpawningPromptFromTranscript(transcript: readonly Turn[], toolCallId: string): string | undefined {
178 for (const turn of transcript) {
179 for (const part of turn.responseParts) {
210 return undefined;
211 }
213 function extractFirstUserText(messages: readonly { readonly type?: string; readonly message?: unknown }[]): string | undefined {
214 for (const msg of messages) {
240 return undefined;
241 }
243 > /**
244 > * Strategy 3 — placeholder for a future SDK primitive that returns the
245 > * spawning `tool_use_id` alongside each subagent id. Currently the SDK
246 > * exposes no such API; this class returns `undefined` and the resolver
247 > * falls back to the other strategies.
248 > */
249 > export class NativeStrategy implements ISubagentLookupStrategy {
250 readonly name = 'native';
251 > async lookup(): Promise<string | undefined> { claudeSubagentResolver.ts
252 return undefined;
253 }
255 >
256 > /**
257 > * Side-effecting collaborators the strategy chain uses. Pulled out so
258 > * {@link resolveAgentIdViaChain} is a pure orchestrator: tests can
259 > * drive it with in-memory stubs without standing up a full SDK or
260 > * registry.
261 > */
262 > interface IResolveChainDeps {
263 > readonly strategies: readonly ISubagentLookupStrategy[];
264 > readonly cacheGet: (toolCallId: string) => string | undefined;
265 > readonly cacheSet: (toolCallId: string, agentId: string) => void;
266 > }
267 >
268 > /**
269 > * Chain orchestration extracted so it can be tested in isolation. Cache
270 > * hit short-circuits the chain; otherwise strategies run in order, the
271 > * first non-undefined hit wins, the cache is populated.
272 > *
273 > * Cancellation is checked between strategies — a cancelled token resolves
274 > * to `undefined`.
275 > */
276 export async function resolveAgentIdViaChain(
277 toolCallId: string,
295 return undefined;
296 }
298 > /**
299 > * Build the production strategy chain. Pulled out so callers
300 > * ({@link getSubagentTranscript}, tests, etc.) construct a single
301 > * canonical ordering instead of inlining it.
302 > */
303 function buildDefaultStrategies(sdk: IClaudeAgentSdkService, logService: ILogService): readonly ISubagentLookupStrategy[] {
304 return [
308 ];
309 }
311 > /**
312 > * Phase 12 — fetch a subagent's transcript by URI. Cache lookup goes
313 > * through the parent session's {@link SubagentRegistry} (each spawn
314 > * carries its `agentId`); on a miss, the strategy chain runs and the
315 > * resolved agentId is recorded back onto the spawn (first-writer-wins)
316 > * for future calls.
317 > *
318 > * Resilient: returns `[]` on any unresolvable agentId or SDK error
319 > * after warn-logging. Throws only on a malformed (non-subagent) URI,
320 > * which indicates a programming error in the caller.
321 > */
322 export async function getSubagentTranscript(
323 subagentUri: URI,