claudeMapSessionEvents.ts ×22

Frontier kind: Code frontier

unlabeled · c_d18d04fdaa6c

261 tests · 23744 LOC · 106 files · introduces 0 tests · 324 LOC · 2 files

Introduces — evidence that enters the hierarchy at this concept

Code
27 ranges324 lines · 2 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1837 ranges23744 lines · 106 files · Browse complete extent
All tests (intent)
261 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.

2 files ranked by introduced lines: 324 introduced LOC across 27 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts 222 introduced LOC · 22 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- claudeMapSessionEvents.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 { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
7 > import type { URI } from '../../../../base/common/uri.js';
8 > import { LogLevel, type ILogService } from '../../../log/common/log.js';
9 > import type { AgentSignal } from '../../common/agentService.js';
10 > import { ActionType } from '../../common/state/sessionActions.js';
11 > import { ResponsePartKind, ToolResultContentType, type ToolResultContent, type ToolResultFileEditContent } from '../../common/state/sessionState.js';
12 > import { extractForwardedErrorInfo } from '../shared/forwardedChatError.js';
13 > import { buildTopLevelSubagentReadyAction, emitInnerAssistantSignals, mapSubagentSystemMessage, SUBAGENT_SPAWNING_TOOL_NAMES, tagWithParent } from './claudeSubagentSignals.js';
14 > import type { SubagentRegistry } from './claudeSubagentRegistry.js';
15 > import { stripClientToolNamePrefix, hasClientToolNamePrefix } from './clientTools/claudeClientToolMcpServer.js';
16 > import { buildClaudeToolMeta, getClaudePastTenseMessage, getClaudeToolDisplayName } from './claudeToolDisplay.js';
17 > import { claudeToolDenialCode } from './claudeToolDenial.js';
18 > import { ClaudeToolCallRegistry } from './claudeToolCallRegistry.js';
19 > import { ToolCallConfirmationReason, ToolCallContributorKind, type StringOrMarkdown } from '../../common/state/protocol/state.js';
20 >
21 > /**
22 > * Cross-call state for {@link mapSDKMessageToAgentSignals}. One instance
23 > * lives per {@link ClaudeAgentSession} and is threaded through every
24 > * mapper invocation for that session's lifetime.
25 > *
26 > * Three scopes:
27 > *
28 > * - **Per-message** (`activeToolBlocks`, `currentMessageId`): mirror
29 > * the SDK's per-message `BetaRawContentBlockStartEvent.index`
30 > * namespace. Reset on every `message_start`. `activeToolBlocks` lets
31 > * `input_json_delta` look up the tool block that owns the current
32 > * index. `currentMessageId` qualifies text/thinking part ids so a
33 > * later message in the same turn does not collide with an earlier
34 > * message that used the same `index` for a different block kind
35 > * (e.g. turn one: `thinking@0`; turn two after `tool_result`:
36 > * `text@0`).
37 > * - **Cross-message** (`toolCallTurnIds`, `toolCallNames`): a `tool_use`
38 > * lands in one assistant message, the matching `tool_result` arrives
39 > * in a later synthetic `user` message. Keyed by the SDK's globally-
40 > * unique `block.id` so re-use of `index` between messages is harmless.
41 > * Drained on `tool_result` (happy path) or on the turn's `result`
42 > * envelope as a defense-in-depth fallback so an SDK that never
43 > * delivers `tool_result` cannot leak entries across turns.
44 > *
45 > * Encapsulated as a class (vs. a plain interface) so the maps' mutators
46 > * are not part of the public surface — Phase 6.1's lesson — and the
47 > * lifecycle invariants live behind named methods.
48 > */
49 > export class ClaudeMapperState {
50 private readonly _activeToolBlocks = new Map<number, { toolUseId: string; toolName: string }>();
51 /**
69 */
70 private readonly _completedFileEdits = new Map<string, ToolResultFileEditContent>();
72 > /**
73 > * Reset per-message state. Called on `message_start`. Cross-message
74 > * tool-call tracking is deliberately NOT cleared here — the
75 > * `tool_result` for a `tool_use` arrives in a later message.
76 > */
77 > resetMessage(messageId: string): void {
78 this._activeToolBlocks.clear();
79 this._currentMessageId = messageId;
80 }
82 > getCurrentMessageId(): string | undefined {
83 return this._currentMessageId;
84 }
86 > /**
87 > * Open a tool block at the given content-block index. Seeds both
88 > * scopes; the per-message map gets drained on `content_block_stop`,
89 > * the cross-message maps survive until the matching `tool_result`.
90 > */
91 > startToolBlock(index: number, toolUseId: string, toolName: string, turnId: string): void {
92 this._activeToolBlocks.set(index, { toolUseId, toolName });
93 this.toolCalls.begin(toolUseId, toolName, turnId);
94 }
96 > getActiveToolBlock(index: number): { toolUseId: string; toolName: string } | undefined {
97 return this._activeToolBlocks.get(index);
98 }
100 > endToolBlock(index: number): void {
101 this._activeToolBlocks.delete(index);
102 }
104 > /**
105 > * Phase 8.5 — forward an `input_json_delta.partial_json` chunk
106 > * to the registry. Resolves the index → `tool_use_id` mapping
107 > * locally (the registry is keyed by id, not by index) and is a
108 > * no-op when the index is unknown.
109 > */
110 > appendToolBlockInputDelta(index: number, partialJson: string): void {
111 const tracked = this._activeToolBlocks.get(index);
112 if (!tracked) {
115 this.toolCalls.appendInputDelta(tracked.toolUseId, partialJson);
116 }
118 > /**
119 > * Phase 8.5 — forward the `content_block_stop` signal to the
120 > * registry, which parses the buffer and stashes the computed
121 > * start-info.
122 > */
123 > finalizeToolBlock(index: number): void {
124 const tracked = this._activeToolBlocks.get(index);
125 if (!tracked) {
128 this.toolCalls.finalize(tracked.toolUseId);
129 }
131 > /**
132 > * Cross-message lookup for `tool_result` handling. Returns
133 > * `undefined` if the `tool_use_id` is unknown (defense-in-depth
134 > * against transport drift / replay).
135 > */
136 > lookupToolCall(toolUseId: string): { turnId: string; toolName: string } | undefined {
137 const entry = this.toolCalls.lookup(toolUseId);
138 return entry ? { turnId: entry.turnId, toolName: entry.toolName } : undefined;
139 }
141 > /** Drain cross-message tracking once a `tool_result` is delivered. */
142 > completeToolCall(toolUseId: string): void {
143 this.toolCalls.complete(toolUseId);
144 }
146 > /**
147 > * Phase 8 — stash a {@link ToolResultFileEditContent} produced by
148 > * `ClaudeAgentSession._observeUserMessage` so the synchronous mapper
149 > * can append it to the matching `ChatToolCallComplete` action.
150 > */
151 > cacheFileEdit(toolUseId: string, content: ToolResultFileEditContent): void {
152 this._completedFileEdits.set(toolUseId, content);
153 }
155 > /**
156 > * Phase 8 — consume and remove the cached file edit for this
157 > * `tool_use_id`. Returns `undefined` for non-file-edit tools or for
158 > * file-edit tools where snapshotting was skipped (e.g. denied before
159 > * the SDK ran the tool, or no actual file change occurred).
160 > */
161 > takeFileEdit(toolUseId: string): ToolResultFileEditContent | undefined {
162 const content = this._completedFileEdits.get(toolUseId);
163 if (content) {
166 return content;
167 }
169 > /**
170 > * Drop any cross-message tracking that is still pending at the end
171 > * of a turn. A `tool_use` whose `tool_result` never arrives — model
172 > * misbehavior, transport drop, future cancellation — would otherwise
173 > * survive in the maps for the lifetime of the session and accumulate
174 > * across turns. Called from {@link mapResult} on every `result`
175 > * envelope; warns once per orphan to surface the protocol break.
176 > *
177 > * Phase 12 subagent state lives on {@link SubagentRegistry}, not
178 > * here; the mapper drives that drain via
179 > * `registry.drainForegroundSpawns()` from {@link mapResult}.
180 > */
181 > clearPendingToolCalls(logService: ILogService): void {
182 this.toolCalls.clearPending(logService);
183 }
185 >
186 > /**
187 > * Map one SDK message to zero or more agent signals.
188 > *
189 > * Stateful via {@link ClaudeMapperState} as of Phase 7: per-block tool
190 > * tracking is per-message, cross-block `tool_use` → `tool_result`
191 > * linkage is cross-message. Callers MUST thread one shared state
192 > * instance through every invocation for a given session.
193 > *
194 > * Phase 6 emissions (text / thinking / usage / turn complete) are
195 > * unchanged and stateless. Phase 7 adds:
196 > *
197 > * - {@link ActionType.ChatToolCallStart} on
198 > * `content_block_start` with a `tool_use` block.
199 > * - {@link ActionType.ChatToolCallDelta} on `content_block_delta`
200 > * with an `input_json_delta`.
201 > * - {@link ActionType.ChatToolCallComplete} on a synthetic `user`
202 > * message whose `message.content` includes a `tool_result` block —
203 > * the originating `turnId` is recovered from {@link ClaudeMapperState}
204 > * so the action lands on the correct turn even when the result
205 > * arrives in a later message.
206 > *
207 > * Reducer ordering invariant: `ChatResponsePart` MUST precede the
208 > * first `ChatDelta` / `ChatReasoning` for that part id (see
209 > * `actions.ts:233, 540`). The same holds for tool calls
210 > * (`ChatToolCallStart` precedes `ChatToolCallDelta` and
211 > * `ChatToolCallComplete`). The SDK protocol orders
212 > * `content_block_start` before any delta at the same index, and
213 > * `tool_result` cannot arrive before its matching `tool_use`, so the
214 > * invariant holds by construction.
215 > */
216 > export function mapSDKMessageToAgentSignals(
217 message: SDKMessage,
218 chat: URI,
264 }
265 }
267 > /**
268 > * Handle the canonical {@link SDKAssistantMessage} (`type: 'assistant'`).
269 > *
270 > * **Top-level (`parent_tool_use_id === null`)**: the SDK delivered each
271 > * block via `stream_event` partials and `mapStreamEvent` emitted the
272 > * matching signals, so most blocks here are no-ops. **Exception**: for
273 > * Task/Agent tool_use blocks we synthesise a `ChatToolCallReady`
274 > * (via {@link buildTopLevelSubagentReadyAction}) because the SDK skips
275 > * `canUseTool` for them and the parent tool would otherwise stay in
276 > * `Streaming` — see that function's JSDoc.
277 > *
278 > * **Inner subagent context (`parent_tool_use_id !== null`)**: empirically
279 > * the SDK does NOT deliver inner content via `stream_event` — only via
280 > * canonical `assistant` and `user` messages, even with
281 > * `Options.forwardSubagentText: true`. Delegated to
282 > * {@link emitInnerAssistantSignals} which emits one signal per content
283 > * block. `tagWithParent` then stamps every emitted action with the
284 > * envelope's `parent_tool_use_id` so `AgentSideEffects` routes them to
285 > * the subagent session.
286 > */
287 function mapAssistantCanonical(
288 message: Extract<SDKMessage, { type: 'assistant' }>,
305 return emitInnerAssistantSignals(message, chat, turnId, state, parentToolUseId, registry);
306 }
308 > /**
309 > * Handle synthetic `user` messages whose `message.content` carries
310 > * `tool_result` blocks. The SDK delivers these as the response to a
311 > * prior `tool_use`. Per Phase 7 S3.3.4, each such block emits a
312 > * {@link ActionType.ChatToolCallComplete} action targeting the turn
313 > * that owned the original `tool_use`.
314 > *
315 > * Cross-message linkage is via {@link ClaudeMapperState.lookupToolCall};
316 > * unknown `tool_use_id`s warn and drop (defense-in-depth, mirrors the
317 > * Phase 7 plan S3.3.5 directive).
318 > */
319 function mapUserMessage(
320 message: Extract<SDKMessage, { type: 'user' }>,
389 return signals;
390 }
392 > /**
393 > * Project the SDK's `ToolResultBlockParam.content` into the protocol's
394 > * text content shape. The SDK accepts either a bare string (legacy
395 > * shape) or an array of typed blocks; non-text blocks are dropped
396 > * here. Phase 8 file-edit content is appended separately by
397 > * {@link mapUserMessage} from {@link ClaudeMapperState.takeFileEdit}.
398 > */
399 function extractToolResultContent(content: unknown): { type: ToolResultContentType.Text; text: string }[] | undefined {
400 if (typeof content === 'string') {
412 return out.length > 0 ? out : undefined;
413 }
415 function isToolResultTextBlock(block: unknown): block is { type: 'text'; text: string } {
416 if (block === null || typeof block !== 'object') {
420 return candidate.type === 'text' && typeof candidate.text === 'string';
421 }
423 function mapResult(
424 message: Extract<SDKMessage, { type: 'result' }>,
494 return signals;
495 }
497 > /**
498 > * Extracts the error text from an SDK result message for the error subtypes
499 > * the proxy can relay. Mirrors the Copilot Chat extension's
500 > * `getResultErrorText`.
501 > */
502 function getResultErrorText(message: Extract<SDKMessage, { type: 'result' }>): string | undefined {
503 if (message.subtype === 'success') {
509 return undefined;
510 }
512 function mapStreamEvent(
513 event: Extract<SDKMessage, { type: 'stream_event' }>['event'],
695 }
696 }
698 > /**
699 > * Build the {@link ResponsePartKind.Markdown}/{@link ResponsePartKind.Reasoning}
700 > * id for a text or thinking content block. Qualifying with the SDK's
701 > * per-message id is required: a single turn can span multiple SDK
702 > * messages (e.g. assistant message → tool_use → tool_result → assistant
703 > * message) and `event.index` resets to 0 on each new `message_start`.
704 > * Without the message-id qualifier, a `text@0` block in the second
705 > * message collides with a `thinking@0` block in the first and the
706 > * reducer treats it as a duplicate, dropping the follow-up text.
707 > *
708 > * If `currentMessageId` is missing we fall back to the legacy
709 > * `${turnId}#${index}` form and warn — the SDK protocol guarantees
710 > * `message_start` precedes any content block, so the absence is a real
711 > * bug, not a transport reorder.
712 > */
713 function makeContentBlockPartId(
714 turnId: string,
src/vs/platform/agentHost/node/claude/claudeSubagentSignals.ts 102 introduced LOC · 5 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- claudeSubagentSignals.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 { 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[],
42 chat: URI,
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' }>,
99 chat: URI,
128 return [];
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' }>,
156 chat: URI,
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' }>,
213 chat: URI,
302 return signals;
303 }
305 function safeStringify(value: unknown): string | undefined {
306 try {