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

195 LOC · 195 covered · 0 uncovered · 42 ranges · 535 concepts · 24 introducers · 272 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 > /*--------------------------------------------------------------------------------------------- claudeToolCallRegistry.ts ×10
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 { ILogService } from '../../../log/common/log.js';
7 > import type { StringOrMarkdown } from '../../common/state/protocol/state.js';
8 > import { getClaudeInvocationMessage, getClaudeToolDisplayName, getClaudeToolInputString } from './claudeToolDisplay.js';
9 >
10 > /**
11 > * Phase 8.5 — per-tool-call info computed at `content_block_stop` and
12 > * reused at `tool_result` time. Mirrors Copilot's `IToolStartInfo`
13 > * shape: Copilot stashes it at `tool.execution_start` (where the
14 > * Copilot SDK hands over complete args); Claude stashes it at the
15 > * analogous `content_block_stop` (the first point where the
16 > * `input_json_delta` buffer is complete and parseable).
17 > */
18 > export interface IClaudeToolStartInfo {
19 > readonly toolName: string;
20 > readonly displayName: string;
21 > readonly parsedInput: Record<string, unknown> | undefined;
22 > readonly invocationMessage: StringOrMarkdown;
23 > readonly toolInput: string | undefined;
24 > }
25 >
26 > interface IRegistryEntry {
27 > readonly toolName: string;
28 > readonly turnId: string;
29 > inputBuffer: string;
30 > info: IClaudeToolStartInfo | undefined;
31 > }
32 >
33 > /**
34 > * Phase 8.5 — per-session, cross-message tool-call tracking for the
35 > * live mapper. Owns:
36 > *
37 > * - **Attribution** — `tool_use_id → { toolName, turnId }`. A
38 > * `tool_use` lands in one assistant message; the matching
39 > * `tool_result` arrives in a later synthetic `user` message. The
40 > * registry resolves the originating turn so each Complete action
41 > * lands on the correct turn.
42 > * - **Input accumulation** — `input_json_delta` chunks arrive across
43 > * `content_block_start` → `delta*` → `content_block_stop`. The
44 > * registry concatenates them and parses once at `finalize`.
45 > * - **Computed start-info** — `displayName`, rich `invocationMessage`,
46 > * `toolInput` string, parsed input. Computed once at `finalize`
47 > * and looked up at `tool_result` time so `pastTenseMessage` can
48 > * include the original parameters.
49 > *
50 > * Mirror of Copilot's `pendingTools: Map<toolCallId, IToolStartInfo>`
51 > * pattern in
52 > * [`mapSessionEvents.ts`](../copilot/mapSessionEvents.ts) — only the
53 > * seam differs (Copilot's SDK hands over complete args at
54 > * `tool.execution_start`; Claude's SDK streams them in deltas, so the
55 > * "ready" seam is `content_block_stop`).
56 > *
57 > * Encapsulated as a class with named lifecycle methods so the maps'
58 > * mutators are not part of the public surface — Phase 6.1's lesson.
59 > * One instance lives per `ClaudeAgentSession` and is composed by
60 > * `ClaudeMapperState`; the mapper threads `state` (which exposes the
61 > * registry as `state.toolCalls`) into every invocation.
62 > */
63 > export class ClaudeToolCallRegistry {
64 > private readonly _entries = new Map<string, IRegistryEntry>(); claudeToolCallRegistry.ts ×1
66 > /**
67 > * Begin tracking a tool call. Called from `content_block_start`
68 > * for a `tool_use` block. Allocates the delta buffer; the
69 > * computed info bag is filled in by {@link finalize}.
70 > */
71 > begin(toolUseId: string, toolName: string, turnId: string): void {
72 > this._entries.set(toolUseId, { claudeToolCallRegistry.ts ×1
73 > toolName,
74 > turnId,
75 > inputBuffer: '',
76 > info: undefined,
77 > });
78 > }
80 > /**
81 > * Append one `input_json_delta.partial_json` chunk. No-op if the
82 > * `tool_use_id` is unknown (the caller already logged a warning
83 > * about the index mismatch).
84 > */
85 > appendInputDelta(toolUseId: string, partialJson: string): void {
86 > const entry = this._entries.get(toolUseId); claudeToolCallRegistry.ts ×2
87 > if (!entry) {
89 > }
90 > entry.inputBuffer += partialJson; claudeToolCallRegistry.ts ×1
93 > /**
94 > * Parse the accumulated buffer and stash the computed
95 > * {@link IClaudeToolStartInfo}. Called from `content_block_stop`.
96 > * Parse failures fall back to `parsedInput: undefined`; the
97 > * past-tense helper handles that by returning a generic message.
98 > */
99 > finalize(toolUseId: string): void {
100 > const entry = this._entries.get(toolUseId); claudeToolCallRegistry.ts ×2
101 > if (!entry) {
103 > }
104 > let parsedInput: Record<string, unknown> | undefined; claudeToolCallRegistry.ts ×2
105 > if (entry.inputBuffer.length > 0) {
107 > const parsed: unknown = JSON.parse(entry.inputBuffer);
108 > if (parsed !== null && typeof parsed === 'object') {
109 > parsedInput = parsed as Record<string, unknown>; claudeToolCallRegistry.ts ×1
110 > }
112 > // Malformed JSON — fall through with `parsedInput: undefined`. claudeToolCallRegistry.ts ×1
113 > }
115 > // Preserve the raw buffer as a fallback `toolInput` so a malformed claudeToolCallRegistry.ts ×2
116 > // or non-object payload still surfaces SOMETHING in the UI rather
117 > // than leaving the input section empty.
118 > const rawFallback = entry.inputBuffer.length > 0 ? entry.inputBuffer : undefined; claudeToolCallRegistry.ts ×2
119 > this._writeInfo(entry, parsedInput, rawFallback);
120 > // Buffer is no longer needed once parsed.
121 > entry.inputBuffer = '';
122 > }
124 > /**
125 > * Seed {@link IClaudeToolStartInfo} directly from a pre-parsed
126 > * input object. Used for inner subagent tool uses, which arrive
127 > * already-parsed on the synthesized `assistant` message rather
128 > * than via streamed `input_json_delta` chunks. Without this the
129 > * registry entry's `info` would stay `undefined` and the live
130 > * `tool_result` handler would emit the generic
131 > * `"{displayName} finished"` past-tense, violating D6 (live/replay
132 > * parity).
133 > */
134 > seedParsedInput(toolUseId: string, parsedInput: unknown): void {
135 > const entry = this._entries.get(toolUseId); claudeToolCallRegistry.ts ×2
136 > if (!entry) {
138 > }
139 > const normalized = (parsedInput !== null && typeof parsedInput === 'object') claudeToolCallRegistry.ts ×1
140 > ? parsedInput as Record<string, unknown> claudeToolCallRegistry.ts ×1
141 > : undefined; claudeToolCallRegistry.ts ×1
142 > this._writeInfo(entry, normalized); claudeToolCallRegistry.ts ×2
143 > }
145 > private _writeInfo(entry: IRegistryEntry, parsedInput: Record<string, unknown> | undefined, rawFallback?: string): void {
146 > const displayName = getClaudeToolDisplayName(entry.toolName); claudeToolCallRegistry.ts ×1
147 > entry.info = {
148 > toolName: entry.toolName,
149 > displayName,
150 > parsedInput,
151 > invocationMessage: getClaudeInvocationMessage(entry.toolName, displayName, parsedInput),
152 > toolInput: getClaudeToolInputString(entry.toolName, parsedInput) ?? rawFallback,
153 > };
154 > }
156 > /**
157 > * Cross-message lookup. Returns `undefined` if the
158 > * `tool_use_id` is unknown (defense-in-depth against transport
159 > * drift / replay). The `info` field may be `undefined` if the
160 > * tool block never reached `content_block_stop`.
161 > */
162 > lookup(toolUseId: string): { readonly turnId: string; readonly toolName: string; readonly info: IClaudeToolStartInfo | undefined } | undefined {
163 > const entry = this._entries.get(toolUseId); claudeToolCallRegistry.ts ×2
164 > if (!entry) {
165 > return undefined; claudeToolCallRegistry.ts ×1
166 > }
167 > return { turnId: entry.turnId, toolName: entry.toolName, info: entry.info }; claudeToolCallRegistry.ts ×1
170 > /**
171 > * Drop the entry once the matching `tool_result` has been
172 > * delivered. Bounds the registry's memory across long turns.
173 > */
174 > complete(toolUseId: string): void {
175 > this._entries.delete(toolUseId); claudeToolCallRegistry.ts ×1
176 > }
178 > /**
179 > * Drop any tracking still pending at the end of a turn and warn
180 > * once per orphan. A `tool_use` whose `tool_result` never arrives
181 > * — model misbehavior, transport drop, future cancellation —
182 > * would otherwise survive in the maps for the lifetime of the
183 > * session and accumulate across turns. Called from `mapResult`
184 > * on every `result` envelope.
185 > */
186 > clearPending(logService: ILogService): void {
187 > if (this._entries.size === 0) { claudeToolCallRegistry.ts ×2
189 > }
190 > for (const [toolUseId, entry] of this._entries) { claudeToolCallRegistry.ts ×1
191 > logService.warn(`[claudeToolCallRegistry] turn ${entry.turnId} ended with pending tool_use ${toolUseId} (${entry.toolName}); dropping cross-message state`);
192 > }
193 > this._entries.clear();