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.
/*---------------------------------------------------------------------------------------------
agentHostToolCallTracker.ts ×11
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { disposableTimeout } from '../../../base/common/async.js';
import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
import { StopWatch } from '../../../base/common/stopwatch.js';
import type { SessionToolAuthenticationRequest, SessionToolClientExecutionRequest, SessionToolConfirmationRequest } from '../common/state/protocol/state.js';
import { ToolCallContributorKind, type ToolCallContributor, type ToolCallResult } from '../common/state/sessionState.js';
import type { AgentHostTelemetryReporter } from './agentHostTelemetryReporter.js';
export type ToolInvokedResult = 'success' | 'error' | 'userCancelled';
const TOOL_CALL_STALL_THRESHOLD_MS = 5 * 60 * 1000;
type ToolCallBlockerRequest = SessionToolConfirmationRequest | SessionToolClientExecutionRequest | SessionToolAuthenticationRequest;
/**
* Maps a completed tool call's result to the telemetry result bucket. Mirrors
* the derivation previously done inline in `CopilotAgentSession`: a denied,
* rejected, or cancelled tool call counts as `userCancelled`; any other
* failure counts as `error`.
*/
export function deriveToolInvokedResult(result: ToolCallResult): ToolInvokedResult {
}
if (code === 'rejected' || code === 'denied' || code === 'cancelled') {
agentHostToolCallTracker.ts ×2
}
}
/**
* Maps a tool call's contributor to the telemetry `toolSourceKind`. A tool with
* no contributor is provided by the agent host itself; an MCP contributor maps
* to `mcp` and a client contributor to `client`.
*/
export function toolSourceKindFromContributor(contributor: ToolCallContributor | undefined): string {
}
// Widen to `string` so an unrecognized kind from a newer protocol version
agentHostToolCallTracker.ts ×1
// falls through to a valid telemetry value rather than `undefined`.
const kind: string = contributor.kind;
switch (kind) {
case ToolCallContributorKind.MCP:
return kind;
}
/** Per-tool-call timing state, keyed by `session:toolCallId`. */
interface IToolCallTiming {
readonly stopWatch: StopWatch;
readonly provider: string;
readonly session: string;
readonly toolId: string;
readonly toolSourceKind: string;
}
interface IStalledToolCall {
readonly blockerKind: ToolCallBlockerRequest['kind'];
readonly completionStopWatch: StopWatch;
}
/**
* Tracks completed and stalled tool calls for agent host sessions.
*
* Lifecycle per tool call:
* 1. {@link toolCallStarted} — begins a stopwatch and records the tool's
* name and source kind (only the start action carries these)
* 2. {@link toolCallCompleted} — emits the telemetry event and clears state
* 3. {@link toolCallBlocked} / {@link toolCallUnblocked} — emits once when a
* confirmation or client execution remains unresolved past the threshold
*
* In-flight tool calls that never complete (e.g. the turn is cancelled mid
* tool call) are dropped via {@link clearSession} / {@link clear} so the
* tracking map cannot leak.
*/
export class AgentHostToolCallTracker extends Disposable {
private readonly _toolCalls = new Map<string, IToolCallTiming>();
private readonly _toolCallStallTimers = this._register(new DisposableMap<string>());
private readonly _stalledToolCalls = new Map<string, IStalledToolCall>();
constructor(private readonly _reporter: AgentHostTelemetryReporter) {
}
toolCallStarted(provider: string, session: string, toolCallId: string, toolName: string, contributor: ToolCallContributor | undefined): void {
stopWatch: StopWatch.create(true),
provider,
session,
toolId: toolName,
toolSourceKind: toolSourceKindFromContributor(contributor),
});
}
toolCallCompleted(session: string, toolCallId: string, result: ToolCallResult): void {
const timing = this._toolCalls.get(key);
if (!timing) {
// No matching start: either the start was never observed, or this is
agentHostToolCallTracker.ts ×1
// a duplicate completion (the entry was already consumed). Either
// way, do not emit so volume stays accurate.
return;
}
const resultBucket = deriveToolInvokedResult(result);
const totalTimeMs = timing.stopWatch.elapsed();
this._reporter.toolInvoked({
provider: timing.provider,
session: timing.session,
toolId: timing.toolId,
toolSourceKind: timing.toolSourceKind,
result: resultBucket,
invocationTimeMs: totalTimeMs,
});
const stalled = this._stalledToolCalls.get(key);
if (stalled) {
this._reporter.stalledToolCallCompleted({
provider: timing.provider,
session: timing.session,
blockerKind: stalled.blockerKind,
toolId: timing.toolId,
toolSourceKind: timing.toolSourceKind,
result: resultBucket,
totalTimeMs,
timeAfterStallMs: stalled.completionStopWatch.elapsed(),
});
}
toolCallBlocked(provider: string, session: string, request: ToolCallBlockerRequest): void {
const toolCallKey = this._key(session, request.toolCall.toolCallId);
if (this._toolCallStallTimers.has(key) || this._stalledToolCalls.has(toolCallKey)) {
return;
}
const stopWatch = StopWatch.create(true);
this._toolCallStallTimers.set(key, disposableTimeout(() => {
this._stalledToolCalls.set(toolCallKey, { blockerKind: request.kind, completionStopWatch: StopWatch.create(true) });
this._reporter.toolCallStalled({
provider,
session,
blockerKind: request.kind,
toolId: request.toolCall.toolName,
toolSourceKind: toolSourceKindFromContributor(request.toolCall.contributor),
stalledTimeMs,
});
}
toolCallUnblocked(session: string, requestId: string): void {
this._toolCallStallTimers.deleteAndDispose(this._key(session, requestId));
agentHostToolCallTracker.ts ×2
}
/**
* Drops any in-flight (never-completed) tool calls for a session. Called
* when a turn ends or a session is torn down so the tracking map cannot
* leak. A no-op in the normal case where every tool call completes.
*/
clearSession(session: string): void {
for (const key of this._toolCalls.keys()) {
}
if (key.startsWith(prefix)) {
this._toolCallStallTimers.deleteAndDispose(key);
}
}
if (key.startsWith(prefix)) {
this._stalledToolCalls.delete(key);
}
}
clear(): void {
this._toolCallStallTimers.clearAndDisposeAll();
this._stalledToolCalls.clear();
}
private _key(session: string, toolCallId: string): string {
}