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

311 LOC · 288 covered · 23 uncovered · 35 ranges · 507 concepts · 15 introducers · 261 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 > /*--------------------------------------------------------------------------------------------- claudeMapSessionEvents.ts ×22
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 { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
7 > import type { URI } from '../../../../base/common/uri.js';
8 > import type { Mutable } from '../../../../base/common/types.js';
9 > import { toToolCallMeta, type IToolCallMeta } from '../../common/meta/agentToolCallMeta.js';
10 > import type { AgentSignal, IAgentSubagentStartedSignal } from '../../common/agentService.js';
11 > import { ActionType } from '../../common/state/sessionActions.js';
12 > import { ResponsePartKind, ToolCallConfirmationReason } from '../../common/state/sessionState.js';
13 > import type { ClaudeMapperState } from './claudeMapSessionEvents.js';
14 > import { SUBAGENT_TOOL_NAMES, type SubagentRegistry } from './claudeSubagentRegistry.js';
15 > import { buildClaudeToolCallMeta, buildClaudeToolMeta, getClaudeInvocationMessage, getClaudeToolDisplayName, getClaudeToolInputString } from './claudeToolDisplay.js';
16 > import { stripClientToolNamePrefix } from './clientTools/claudeClientToolMcpServer.js';
17 >
18 > /**
19 > * Phase 12 — SDK tool names that spawn subagent sessions. Re-exported
20 > * from the registry's canonical set so callers can keep importing it
21 > * from this signals module (the live mapper, replay handling, etc.).
22 > */
23 > export const SUBAGENT_SPAWNING_TOOL_NAMES: ReadonlySet<string> = SUBAGENT_TOOL_NAMES;
24 >
25 > /**
26 > * Phase 12 — post-process the signals produced from a single SDK
27 > * message envelope. When the envelope's `parent_tool_use_id` is set,
28 > * every action / pending_confirmation gets tagged with
29 > * `parentToolCallId` so {@link import('../agentSideEffects.js').AgentSideEffects}
30 > * can re-route it to the subagent session. The first inner emission
31 > * for a given parent additionally prepends an `IAgentSubagentStartedSignal`
32 > * so the child session exists before any of its actions arrive.
33 > *
34 > * The Started signal's labels come straight off the parent's
35 > * {@link SubagentSpawn}: `subagentType` (e.g. `"Explore"`) for both
36 > * the agent name and display name, and `description` for the
37 > * description. When the spawn is missing (rare race) or has no
38 > * metadata yet, falls back to the literal `"subagent"` / `"Subagent"`.
39 > */
40 > export function tagWithParent(
41 > signals: AgentSignal[], claudeSubagentSignals.ts ×3
42 > chat: URI,
43 > parentToolUseId: string | null,
44 > registry: SubagentRegistry,
45 > ): AgentSignal[] {
46 > if (!parentToolUseId) {
47 > return signals; claudeSubagentSignals.ts ×1
48 > }
49 > const tagged: AgentSignal[] = signals.map(s => { claudeSubagentSignals.ts ×2
50 > if (s.kind === 'action') {
51 > return { ...s, parentToolCallId: parentToolUseId };
52 > }
53 if (s.kind === 'pending_confirmation') {
54 return { ...s, parentToolCallId: parentToolUseId };
55 }
56 return s;
58 > const spawn = registry.getSpawn(parentToolUseId);
59 > if (!spawn || !spawn.markAnnounced()) { claudeSubagentSignals.ts ×3
60 > return tagged; claudeSubagentSignals.ts ×1
61 > }
62 > const started: IAgentSubagentStartedSignal = { claudeSubagentSignals.ts ×1
63 > kind: 'subagent_started',
64 > chat,
65 > toolCallId: parentToolUseId,
66 > agentName: spawn.subagentType ?? 'subagent',
67 > agentDisplayName: spawn.subagentType ?? 'Subagent', claudeSubagentSignals.ts ×3
68 > agentDescription: spawn.description,
69 > // The Task tool's short `description` input doubles as the concise
70 > // per-task tab title for the subagent's read-only peer chat.
71 > taskDescription: spawn.description,
72 > // The Task tool's `prompt` input is the full delegated instruction
73 > // that seeds the subagent peer chat's opening request.
74 > taskPrompt: spawn.prompt,
75 > // When the spawning Task tool is itself an inner tool of another
76 > // subagent, its parent Task (one level up) is the tool call in
77 > // whose chat this spawning tool lives. The host uses it to route
78 > // the discovery content block to that immediate parent chat, at
79 > // any nesting depth.
80 > parentToolCallId: registry.getParentSpawn(parentToolUseId)?.toolUseId,
81 > };
82 > return [started, ...tagged];
83 > }
85 > /**
86 > * Phase 12 step 7 — handle the two `type: 'system'` subtypes that drive
87 > * background-subagent lifecycle. `task_started` flips the matching
88 > * spawning entry to background so the foreground `tool_result` path
89 > * skips its `subagent_completed`. `task_notification` (with a terminal
90 > * status) is the deferred completion trigger for those background
91 > * entries.
92 > *
93 > * All other system subtypes (`compact_boundary`, `task_progress`,
94 > * `task_updated`, hooks, etc.) fall through with `[]`; non-subagent
95 > * system handling stays in the mapper proper.
96 > */
97 > export function mapSubagentSystemMessage(
98 > message: Extract<SDKMessage, { type: 'system' }>, claudeSubagentSignals.ts ×2
99 > chat: URI,
100 > registry: SubagentRegistry,
101 > ): AgentSignal[] {
102 > const sub = (message as { subtype?: string }).subtype;
103 > if (sub === 'task_started') {
104 > const toolUseId = (message as { tool_use_id?: string }).tool_use_id; claudeSubagentSignals.ts ×2
105 > const spawn = toolUseId ? registry.getSpawn(toolUseId) : undefined;
106 > if (spawn) {
107 > spawn.background = true;
108 > }
109 > return [];
110 > }
111 > if (sub === 'task_notification') { claudeSubagentSignals.ts ×2
112 > const m = message as { tool_use_id?: string; status?: string }; claudeSubagentSignals.ts ×3
113 > if (!m.tool_use_id) {
114 > return []; claudeSubagentSignals.ts ×2
115 > }
116 > const status = m.status; claudeSubagentSignals.ts ×3
117 > if (status !== 'completed' && status !== 'failed' && status !== 'stopped') {
118 > return []; claudeSubagentSignals.ts ×2
119 > }
120 > const spawn = registry.getSpawn(m.tool_use_id); claudeSubagentSignals.ts ×3
121 > if (!spawn || !spawn.markCompleted()) {
122 > return [];
123 > }
124 > const toolUseId = m.tool_use_id; claudeSubagentSignals.ts ×2
125 > registry.removeSpawn(toolUseId);
126 > return [{ kind: 'subagent_completed', chat, toolCallId: toolUseId }];
127 > }
128 > return []; claudeSdkPipeline.ts ×3
129 > }
131 > /**
132 > * Phase 12 fix — build the `ChatToolCallReady` signal for a top-level
133 > * Task/Agent tool_use block AND record the spawn's metadata onto the
134 > * registry. The workbench's
135 > * [stateToProgressAdapter.ts](../../../../workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts)
136 > * reads `_meta.subagentDescription` and `_meta.subagentAgentName` to
137 > * render the subagent UI before any inner content arrives.
138 > *
139 > * The metadata side effect (`spawn.description = ...`,
140 > * `spawn.subagentType = ...`) is written here because the canonical
141 > * `assistant` envelope is the first place where `block.input` is
142 > * complete (the early `content_block_start` carries an empty input bag
143 > * that gets filled in via `input_json_delta` events).
144 > *
145 > * Inputs:
146 > * - `block.id` / `block.name` — SDK-supplied tool_use identifiers.
147 > * - `block.input.description` → `spawn.description` and
148 > * `_meta.subagentDescription` and `action.invocationMessage`.
149 > * - `block.input.subagent_type` → `spawn.subagentType` and
150 > * `_meta.subagentAgentName`.
151 > * - `block.input.prompt` → `spawn.prompt` (seeds the subagent's
152 > * opening request via the `subagent_started` signal's `taskPrompt`).
153 > */
154 > export function buildTopLevelSubagentReadyAction(
155 > block: Extract<import('@anthropic-ai/claude-agent-sdk').SDKAssistantMessage['message']['content'][number], { type: 'tool_use' }>, claudeSubagentSignals.ts ×6
156 > chat: URI,
157 > turnId: string,
158 > registry: SubagentRegistry,
159 > ): AgentSignal {
160 > const input = block.input as Record<string, unknown> | undefined;
161 > const description = typeof input?.description === 'string' ? input.description : undefined;
162 > const agentName = typeof input?.subagent_type === 'string' ? input.subagent_type : undefined;
163 > const prompt = typeof input?.prompt === 'string' ? input.prompt : undefined;
164 > const inputJson = block.input !== undefined ? safeStringify(block.input) : undefined;
165 > registry.recordSpawn(block.id, { subagentType: agentName, description, prompt });
166 > const meta: Mutable<IToolCallMeta> = { ...buildClaudeToolCallMeta(block.name) };
167 > if (!meta.toolKind) {
168 meta.toolKind = 'subagent';
169 }
170 > if (description) { claudeSubagentSignals.ts ×6
171 > meta.subagentDescription = description; claudeSubagentSignals.ts ×2
172 > }
173 > if (agentName) { claudeSubagentSignals.ts ×6
174 > meta.subagentAgentName = agentName; claudeSubagentSignals.ts ×2
175 > }
177 > kind: 'action',
178 > resource: chat,
179 > action: {
180 > type: ActionType.ChatToolCallReady,
181 > turnId,
182 > toolCallId: block.id,
183 > invocationMessage: getClaudeInvocationMessage(block.name, getClaudeToolDisplayName(block.name), block.input),
184 > ...(inputJson !== undefined ? { toolInput: inputJson } : {}),
185 > confirmed: ToolCallConfirmationReason.NotNeeded,
186 > _meta: toToolCallMeta(meta),
187 > },
188 > };
189 > }
191 > /**
192 > * Phase 12 fix — walk an inner subagent canonical assistant message
193 > * (`parent_tool_use_id !== null`) and emit one signal per content block.
194 > *
195 > * The SDK does NOT deliver inner subagent content via `stream_event`
196 > * partials, only via canonical `assistant` (and `user` for tool_result)
197 > * envelopes. So this canonical envelope IS the only signal source for
198 > * inner content. We emit:
199 > *
200 > * - `text` / `thinking` → `ChatResponsePart` (Markdown / Reasoning)
201 > * with the full block content.
202 > * - `tool_use` → `ChatToolCallStart` + `ChatToolCallReady`
203 > * (`confirmed: NotNeeded`, since the SDK runs inner tools in
204 > * `bypassPermissions` and the parent's `canUseTool` is skipped),
205 > * plus side effects on `state` (cross-message lookup) and
206 > * `registry` (inner→parent edge for the canUseTool bridge).
207 > *
208 > * Returns the emitted signals; the caller (`tagWithParent`) is
209 > * responsible for stamping `parentToolCallId` on every action.
210 > */
211 > export function emitInnerAssistantSignals(
212 > message: Extract<SDKMessage, { type: 'assistant' }>, claudeSubagentSignals.ts ×2
213 > chat: URI,
214 > turnId: string,
215 > state: ClaudeMapperState,
216 > parentToolUseId: string,
217 > registry: SubagentRegistry,
218 > ): AgentSignal[] {
219 > const messageId = message.message.id;
220 > const signals: AgentSignal[] = [];
221 > for (let index = 0; index < message.message.content.length; index++) {
222 > const block = message.message.content[index];
223 > if (block.type === 'text') {
224 > signals.push({
225 > kind: 'action',
226 > resource: chat,
227 > action: {
228 > type: ActionType.ChatResponsePart,
229 > turnId,
230 > part: {
231 > kind: ResponsePartKind.Markdown,
232 > id: `${turnId}#${messageId}#${index}`,
233 > content: block.text,
234 > },
235 > },
236 > });
237 > continue;
238 > }
239 > if (block.type === 'thinking') { claudeSubagentSignals.ts ×2
240 signals.push({
241 kind: 'action',
242 resource: chat,
243 action: {
244 type: ActionType.ChatResponsePart,
245 turnId,
246 part: {
247 kind: ResponsePartKind.Reasoning,
248 id: `${turnId}#${messageId}#${index}`,
249 content: block.thinking,
250 },
251 },
252 });
253 continue;
254 }
255 > if (block.type === 'tool_use') { claudeSubagentSignals.ts ×2
256 > // Strip the in-process MCP server prefix so subagent client-tool
257 > // calls render with their real name (matches the top-level stream
258 > // mapper). SDK-owned tools and Task/Agent passes through unchanged.
259 > const toolName = stripClientToolNamePrefix(block.name);
260 > state.startToolBlock(index, block.id, toolName, turnId);
261 > // Inner tool input arrives pre-parsed on the synthesized
262 > // `assistant` message (not via `input_json_delta` chunks), so
263 > // seed the registry directly. Without this the live
264 > // `tool_result` handler falls back to a generic
265 > // `"{displayName} finished"` past-tense and replay (which
266 > // always computes rich text) drifts from live — violating D6.
267 > state.toolCalls.seedParsedInput(block.id, block.input);
268 > registry.noteInnerTool(block.id, parentToolUseId);
269 > const displayName = getClaudeToolDisplayName(toolName);
270 > const meta = buildClaudeToolMeta(toolName);
271 > const toolInputStr = getClaudeToolInputString(toolName, block.input);
272 > signals.push({
273 > kind: 'action',
274 > resource: chat,
275 > action: {
276 > type: ActionType.ChatToolCallStart,
277 > turnId,
278 > toolCallId: block.id,
279 > toolName,
280 > displayName,
281 > ...(meta ? { _meta: meta } : {}),
282 > },
283 > });
284 > signals.push({
285 > kind: 'action',
286 > resource: chat,
287 > action: {
288 > type: ActionType.ChatToolCallReady,
289 > turnId,
290 > toolCallId: block.id,
291 > invocationMessage: getClaudeInvocationMessage(toolName, displayName, block.input),
292 > ...(toolInputStr !== undefined ? { toolInput: toolInputStr } : {}),
293 > confirmed: ToolCallConfirmationReason.NotNeeded,
294 > },
295 > });
296 > continue;
297 > }
298 > // Unknown inner block kind — skip silently (caller will trace at claudeSubagentSignals.ts ×2
299 > // the mapper level if needed; we don't want to import ILogService
300 > // here just for one trace).
301 > }
302 > return signals;
303 > }
305 > function safeStringify(value: unknown): string | undefined { claudeSubagentSignals.ts ×6
306 > try {
307 > return JSON.stringify(value);
308 > } catch {
309 return undefined;
310 }