src/vs/platform/agentHost/node/agentHostToolCallTracker.ts

203 LOC · 192 covered · 11 uncovered · 46 ranges · 1062 concepts · 22 introducers · 508 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 > /*--------------------------------------------------------------------------------------------- agentHostToolCallTracker.ts ×11
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 { disposableTimeout } from '../../../base/common/async.js';
7 > import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
8 > import { StopWatch } from '../../../base/common/stopwatch.js';
9 > import type { SessionToolAuthenticationRequest, SessionToolClientExecutionRequest, SessionToolConfirmationRequest } from '../common/state/protocol/state.js';
10 > import { ToolCallContributorKind, type ToolCallContributor, type ToolCallResult } from '../common/state/sessionState.js';
11 > import type { AgentHostTelemetryReporter } from './agentHostTelemetryReporter.js';
12 >
13 > export type ToolInvokedResult = 'success' | 'error' | 'userCancelled';
14 >
15 > const TOOL_CALL_STALL_THRESHOLD_MS = 5 * 60 * 1000;
16 >
17 > type ToolCallBlockerRequest = SessionToolConfirmationRequest | SessionToolClientExecutionRequest | SessionToolAuthenticationRequest;
18 >
19 > /**
20 > * Maps a completed tool call's result to the telemetry result bucket. Mirrors
21 > * the derivation previously done inline in `CopilotAgentSession`: a denied,
22 > * rejected, or cancelled tool call counts as `userCancelled`; any other
23 > * failure counts as `error`.
24 > */
25 > export function deriveToolInvokedResult(result: ToolCallResult): ToolInvokedResult {
26 > if (result.success) { agentHostToolCallTracker.ts ×2
27 > return 'success'; agentHostToolCallTracker.ts ×1
28 > }
29 > const code = result.error?.code; agentHostToolCallTracker.ts ×1
30 > if (code === 'rejected' || code === 'denied' || code === 'cancelled') { agentHostToolCallTracker.ts ×2
31 > return 'userCancelled'; agentHostToolCallTracker.ts ×1
32 > }
33 > return 'error'; agentHostToolCallTracker.ts ×1
34 > }
36 > /**
37 > * Maps a tool call's contributor to the telemetry `toolSourceKind`. A tool with
38 > * no contributor is provided by the agent host itself; an MCP contributor maps
39 > * to `mcp` and a client contributor to `client`.
40 > */
41 > export function toolSourceKindFromContributor(contributor: ToolCallContributor | undefined): string {
42 > if (!contributor) { agentHostToolCallTracker.ts ×4
43 > return 'agentHost'; agentHostToolCallTracker.ts ×1
44 > }
45 > // Widen to `string` so an unrecognized kind from a newer protocol version agentHostToolCallTracker.ts ×1
46 > // falls through to a valid telemetry value rather than `undefined`.
47 > const kind: string = contributor.kind;
48 > switch (kind) {
49 > case ToolCallContributorKind.MCP:
50 > return 'mcp'; agentHostToolCallTracker.ts ×1
51 > case ToolCallContributorKind.Client: agentHostToolCallTracker.ts ×4
52 > return 'client'; agentHostToolCallTracker.ts ×1
54 return kind;
56 > }
58 > /** Per-tool-call timing state, keyed by `session:toolCallId`. */
59 > interface IToolCallTiming {
60 > readonly stopWatch: StopWatch;
61 > readonly provider: string;
62 > readonly session: string;
63 > readonly toolId: string;
64 > readonly toolSourceKind: string;
65 > }
66 >
67 > interface IStalledToolCall {
68 > readonly blockerKind: ToolCallBlockerRequest['kind'];
69 > readonly completionStopWatch: StopWatch;
70 > }
71 >
72 > /**
73 > * Tracks completed and stalled tool calls for agent host sessions.
74 > *
75 > * Lifecycle per tool call:
76 > * 1. {@link toolCallStarted} — begins a stopwatch and records the tool's
77 > * name and source kind (only the start action carries these)
78 > * 2. {@link toolCallCompleted} — emits the telemetry event and clears state
79 > * 3. {@link toolCallBlocked} / {@link toolCallUnblocked} — emits once when a
80 > * confirmation or client execution remains unresolved past the threshold
81 > *
82 > * In-flight tool calls that never complete (e.g. the turn is cancelled mid
83 > * tool call) are dropped via {@link clearSession} / {@link clear} so the
84 > * tracking map cannot leak.
85 > */
86 > export class AgentHostToolCallTracker extends Disposable {
87 >
88 > private readonly _toolCalls = new Map<string, IToolCallTiming>();
89 > private readonly _toolCallStallTimers = this._register(new DisposableMap<string>());
90 > private readonly _stalledToolCalls = new Map<string, IStalledToolCall>();
91 >
92 > constructor(private readonly _reporter: AgentHostTelemetryReporter) {
93 > super(); agentSideEffects.ts ×6
94 > }
96 > toolCallStarted(provider: string, session: string, toolCallId: string, toolName: string, contributor: ToolCallContributor | undefined): void {
97 > this._toolCalls.set(this._key(session, toolCallId), { agentSideEffects.ts ×2
98 > stopWatch: StopWatch.create(true),
99 > provider,
100 > session,
101 > toolId: toolName,
102 > toolSourceKind: toolSourceKindFromContributor(contributor),
103 > });
104 > }
106 > toolCallCompleted(session: string, toolCallId: string, result: ToolCallResult): void {
107 > const key = this._key(session, toolCallId); agentSideEffects.ts ×4
108 > const timing = this._toolCalls.get(key);
109 > if (!timing) {
110 > // No matching start: either the start was never observed, or this is agentHostToolCallTracker.ts ×1
111 > // a duplicate completion (the entry was already consumed). Either
112 > // way, do not emit so volume stays accurate.
113 > return;
114 > }
115 > this._toolCalls.delete(key); agentSideEffects.ts ×4
116 > const resultBucket = deriveToolInvokedResult(result);
117 > const totalTimeMs = timing.stopWatch.elapsed();
118 >
119 > this._reporter.toolInvoked({
120 > provider: timing.provider,
121 > session: timing.session,
122 > toolId: timing.toolId,
123 > toolSourceKind: timing.toolSourceKind,
124 > result: resultBucket,
125 > invocationTimeMs: totalTimeMs,
126 > });
127 >
128 > const stalled = this._stalledToolCalls.get(key);
129 > if (stalled) {
130 > this._stalledToolCalls.delete(key); agentHostTelemetryReporter.ts ×1
131 > this._reporter.stalledToolCallCompleted({
132 > provider: timing.provider,
133 > session: timing.session,
134 > blockerKind: stalled.blockerKind,
135 > toolId: timing.toolId,
136 > toolSourceKind: timing.toolSourceKind,
137 > result: resultBucket,
138 > totalTimeMs,
139 > timeAfterStallMs: stalled.completionStopWatch.elapsed(),
140 > });
141 > }
144 > toolCallBlocked(provider: string, session: string, request: ToolCallBlockerRequest): void {
145 > const key = this._key(session, request.id); agentHostToolCallTracker.ts ×3
146 > const toolCallKey = this._key(session, request.toolCall.toolCallId);
147 > if (this._toolCallStallTimers.has(key) || this._stalledToolCalls.has(toolCallKey)) {
148 return;
149 }
151 > const stopWatch = StopWatch.create(true);
152 > this._toolCallStallTimers.set(key, disposableTimeout(() => {
153 > const stalledTimeMs = stopWatch.elapsed(); agentHostTelemetryReporter.ts ×1
154 > this._stalledToolCalls.set(toolCallKey, { blockerKind: request.kind, completionStopWatch: StopWatch.create(true) });
155 > this._reporter.toolCallStalled({
156 > provider,
157 > session,
158 > blockerKind: request.kind,
159 > toolId: request.toolCall.toolName,
160 > toolSourceKind: toolSourceKindFromContributor(request.toolCall.contributor),
161 > stalledTimeMs,
162 > });
163 > }, TOOL_CALL_STALL_THRESHOLD_MS)); agentHostToolCallTracker.ts ×3
164 > }
166 > toolCallUnblocked(session: string, requestId: string): void {
167 > this._toolCallStallTimers.deleteAndDispose(this._key(session, requestId)); agentHostToolCallTracker.ts ×2
168 > }
170 > /**
171 > * Drops any in-flight (never-completed) tool calls for a session. Called
172 > * when a turn ends or a session is torn down so the tracking map cannot
173 > * leak. A no-op in the normal case where every tool call completes.
174 > */
175 > clearSession(session: string): void {
176 > const prefix = `${session}\0`; agentHostToolCallTracker.ts ×4
177 > for (const key of this._toolCalls.keys()) {
178 > if (key.startsWith(prefix)) { agentHostToolCallTracker.ts ×2
179 > this._toolCalls.delete(key); agentHostToolCallTracker.ts ×1
180 > }
182 > for (const key of this._toolCallStallTimers.keys()) { agentHostToolCallTracker.ts ×4
183 if (key.startsWith(prefix)) {
184 this._toolCallStallTimers.deleteAndDispose(key);
185 }
186 }
187 > for (const key of this._stalledToolCalls.keys()) { agentHostToolCallTracker.ts ×4
188 if (key.startsWith(prefix)) {
189 this._stalledToolCalls.delete(key);
190 }
191 }
194 > clear(): void {
195 > this._toolCalls.clear(); agentSideEffects.ts ×6
196 > this._toolCallStallTimers.clearAndDisposeAll();
197 > this._stalledToolCalls.clear();
198 > }
200 > private _key(session: string, toolCallId: string): string {
201 > return `${session}\0${toolCallId}`; agentHostToolCallTracker.ts ×2
202 > }