/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type { LanguageModelToolInvokedClassification, LanguageModelToolInvokedEvent } from '../../telemetry/common/languageModelToolTelemetry.js'; import type { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { TelemetryTrustedValue } from '../../telemetry/common/telemetryUtils.js'; import { hash } from '../../../base/common/hash.js'; import { AgentSession } from '../common/agent.js'; import type { SessionMode } from '../common/agentHostSchema.js'; import { getTelemetryChatSessionId } from '../common/agentTelemetryCorrelation.js'; import { readAgentErrorTelemetryMeta } from '../common/meta/agentErrorMeta.js'; import type { ErrorInfo, MessageAttachment, SessionInputRequestKind, ToolDefinition } from '../common/state/protocol/state.js'; import { ActionType } from '../common/state/sessionActions.js'; import { isAhpChatChannel, isSubagentChatUri, isSubagentSession, parseRequiredSessionUriFromChatUri, type ISessionWithDefaultChat } from '../common/state/sessionState.js'; import type { ToolInvokedResult } from './agentHostToolCallTracker.js'; import { multiplexProperties, type IAgentHostRestrictedTelemetry, type IAgentHostRestrictedTelemetryContext } from './agentHostRestrictedTelemetry.js'; import { AgentHostClientType } from '../common/agentHostClientInfo.js'; import { AgentHostClientConnectionKind, AgentHostLaunchKind, AgentHostTransportKind, type IAgentHostClientTelemetryContext } from '../common/agentHostTelemetry.js'; export type AgentHostUserMessageSentSource = 'direct' | 'queued'; export interface IAgentHostInitiatorTelemetry { initiatorClientType?: AgentHostClientType; initiatorConnectionKind?: AgentHostClientConnectionKind; initiatorTransportKind?: AgentHostTransportKind; hostLaunchKind?: AgentHostLaunchKind; initiatorMachineId?: string; initiatorDevDeviceId?: string; } export type IAgentHostInitiatorClassification = { initiatorClientType?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of VS Code client that initiated the event.' }; initiatorConnectionKind?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The route the initiating client used to reach the agent host.' }; initiatorTransportKind?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The physical transport on which the agent host received the initiating client action.' }; hostLaunchKind?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the agent host process was launched by the VS Code main process or VS Code CLI.' }; initiatorMachineId?: { classification: 'EndUserPseudonymizedInformation'; purpose: 'FeatureInsight'; endpoint: 'MacAddressHash'; comment: 'The machine identifier of the VS Code client that initiated the event.' }; initiatorDevDeviceId?: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight'; endpoint: 'SqmMachineId'; comment: 'The development device identifier of the VS Code client that initiated the event.' }; }; export interface IAgentHostExecutionModeChangedEvent extends IAgentHostInitiatorTelemetry { provider: string; agentSessionId: string; isSubagentSession: boolean; previousMode: SessionMode; newMode: SessionMode; turnCount: number; } export type IAgentHostExecutionModeChangedClassification = IAgentHostInitiatorClassification & { provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider handling the agent host session.' }; agentSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent host session identifier.' }; isSubagentSession: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the mode change belongs to a subagent session.' }; previousMode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The previous agent host execution mode.' }; newMode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The new agent host execution mode.' }; turnCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of completed turns in the agent host chat.' }; owner: 'amunger'; comment: 'Reports agent host execution mode changes.'; }; export interface IAgentHostUserMessageSentEvent { provider: string; hostLaunchKind: AgentHostLaunchKind; initiatorClientId: string | undefined; initiatorClientType: AgentHostClientType; initiatorConnectionKind: AgentHostClientConnectionKind; initiatorTransportKind: AgentHostTransportKind; initiatorMachineId?: string; initiatorDevDeviceId?: string; agentSessionId: string; source: AgentHostUserMessageSentSource; isSubagentSession: boolean; turnCount: number; activeClientId?: string; activeClientToolCount?: number; activeClientCustomizationCount?: number; attachmentCount: number; } export type IAgentHostUserMessageSentClassification = { provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider handling the agent host session.' }; hostLaunchKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the agent host process was launched by the VS Code main process or VS Code CLI.' }; initiatorClientId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The opaque AHP client identifier that initiated the user message.' }; initiatorClientType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of AHP client that initiated the user message.' }; initiatorConnectionKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The route the initiating client declared it used to reach the agent host.' }; initiatorTransportKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The physical transport on which the agent host received the initiating client action.' }; initiatorMachineId?: { classification: 'EndUserPseudonymizedInformation'; purpose: 'FeatureInsight'; endpoint: 'MacAddressHash'; comment: 'The initiating VS Code client machine identifier.' }; initiatorDevDeviceId?: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight'; endpoint: 'SqmMachineId'; comment: 'The initiating VS Code client development device identifier.' }; agentSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent host session identifier.' }; source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user message was sent directly or from the queued-message flow.' }; isSubagentSession: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the message was sent to a subagent session.' }; turnCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of completed turns in the session when the message was sent.' }; activeClientId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the first active client for the session, if any.' }; activeClientToolCount?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The total number of tools provided by the active clients, if any.' }; activeClientCustomizationCount?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The total number of customizations provided by the active clients, if any.' }; attachmentCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of attachments included with the user message.' }; owner: 'roblourens'; comment: 'Tracks user messages sent from the agent host process to an agent provider.'; }; export type AgentHostClientConnectionAction = 'connected' | 'disconnected'; export interface IAgentHostClientConnectionEvent { action: AgentHostClientConnectionAction; hostLaunchKind: AgentHostLaunchKind; clientId: string; clientType: AgentHostClientType; clientImplementationName: string | undefined; clientImplementationVersion: string | undefined; connectionKind: AgentHostClientConnectionKind; transportKind: AgentHostTransportKind; clientMachineId?: string; clientDevDeviceId?: string; protocolVersion: string; isReconnect: boolean; connectedClientCount: number; connectedTransportCount: number; clientTransportCount: number; connectionDurationMs: number | undefined; subscriptionCount: number | undefined; } export type IAgentHostClientConnectionClassification = { action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether an initialized AHP client transport connected or disconnected.' }; hostLaunchKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the agent host process was launched by the VS Code main process or VS Code CLI.' }; clientId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The opaque AHP client identifier.' }; clientType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The bounded type of the connected AHP client.' }; clientImplementationName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The implementation name declared by the AHP client.' }; clientImplementationVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The implementation version declared by the AHP client.' }; connectionKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The route the client declared it used to reach the agent host.' }; transportKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The physical transport accepted by the agent host.' }; clientMachineId?: { classification: 'EndUserPseudonymizedInformation'; purpose: 'FeatureInsight'; endpoint: 'MacAddressHash'; comment: 'The connected VS Code client machine identifier.' }; clientDevDeviceId?: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight'; endpoint: 'SqmMachineId'; comment: 'The connected VS Code client development device identifier.' }; protocolVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The negotiated AHP protocol version.' }; isReconnect: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether this client identifier was previously known to the agent host.' }; connectedClientCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of logical AHP clients with at least one live transport after this lifecycle change.' }; connectedTransportCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The total number of live initialized AHP transports after this lifecycle change.' }; clientTransportCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of live initialized transports for this client after this lifecycle change.' }; connectionDurationMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The duration of the disconnected transport in milliseconds.' }; subscriptionCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of protocol subscriptions held by the client transport when it disconnected.' }; owner: 'roblourens'; comment: 'Tracks initialized Agent Host client connection topology and lifecycle.'; }; export interface IAgentHostClientConnectionReport { action: AgentHostClientConnectionAction; context: IAgentHostClientTelemetryContext; clientId: string; clientImplementationName: string | undefined; clientImplementationVersion: string | undefined; protocolVersion: string; isReconnect: boolean; connectedClientCount: number; connectedTransportCount: number; clientTransportCount: number; connectionDurationMs?: number; subscriptionCount?: number; } export type AgentHostTurnResult = 'success' | 'error' | 'cancelled'; export type AgentHostModelTelemetryKind = 'trusted' | 'byok' | 'unknown'; type AgentHostModelSelectionKind = 'default' | 'auto' | 'explicit'; export type AgentHostTurnFailureStage = 'validation' | 'workingDirectory' | 'modelSelection' | 'sendMessage' | 'provider'; interface IAgentHostTurnAttributedReport { clientContext?: IAgentHostClientTelemetryContext; } export interface IAgentHostTurnCompletedEvent extends IAgentHostInitiatorTelemetry { provider: string; agentSessionId: string; chatSessionId: string; isSubagentSession: boolean; turnId: string; timeToFirstProgress: number | undefined; totalTime: number; result: AgentHostTurnResult; model: string | TelemetryTrustedValue | undefined; modelSelectionKind: AgentHostModelSelectionKind; isBYOK: boolean | undefined; permissionLevel: string | undefined; interactionMode: SessionMode | undefined; errorType: string | undefined; failureStage: AgentHostTurnFailureStage | undefined; isMultiRoot: boolean; folderCount: number; } export type IAgentHostTurnCompletedClassification = IAgentHostInitiatorClassification & { provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider handling the agent host session.' }; agentSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent host session identifier.' }; chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat identifier within the agent host session.' }; isSubagentSession: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the turn belongs to a subagent session.' }; turnId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the turn within the agent host session.' }; timeToFirstProgress: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds from turn start to the first visible progress (text delta, response part, tool call start, or reasoning).' }; totalTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Total time in milliseconds from turn start to turn completion.' }; result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the turn completed successfully, with an error, or was cancelled.' }; model: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The trusted provider model identifier selected at turn start, or a generic value for BYOK and unknown models.' }; modelSelectionKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the client used the provider default, Auto, or an explicit model.' }; isBYOK: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the selected model is a bring-your-own-key model, when model context is available.' }; permissionLevel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The tool auto-approval level configured for the session at turn start (e.g. default, autoApprove, autopilot).' }; interactionMode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent host interaction mode configured at turn start.' }; errorType: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The structured agent host or provider error type when the turn fails.' }; failureStage: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The bounded stage at which the agent host turn failed.' }; isMultiRoot: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the session spans more than one working directory.' }; folderCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of effective working directories for the session at turn completion.' }; owner: 'roblourens'; comment: 'Tracks agent host turn performance including time to first visible progress and total turn duration.'; }; export interface IAgentHostTurnFailedEvent extends IAgentHostInitiatorTelemetry { provider: string; agentSessionId: string; chatSessionId: string; isSubagentSession: boolean; turnId: string; failureStage: AgentHostTurnFailureStage; errorType: string; errorName: string | undefined; errorCode: string | undefined; providerCallId: string | undefined; serviceRequestId: string | undefined; msg: string; callstack: string | undefined; } export type IAgentHostTurnFailedClassification = IAgentHostInitiatorClassification & { provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the failed agent host turn.' }; agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' }; chatSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The chat identifier within the agent host session.' }; isSubagentSession: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the failed turn belongs to a subagent session.' }; turnId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the failed turn within the agent host session.' }; failureStage: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The bounded stage at which the agent host turn failed.' }; errorType: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The structured agent host or provider error type.' }; errorName: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The name of the exception, when available.' }; errorCode: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The exception or protocol error code, when available.' }; providerCallId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The GitHub provider request identifier, when available.' }; serviceRequestId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The Copilot service request identifier, when available.' }; msg: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The error message. VS Code telemetry scrubs file paths and likely secrets before transmission.' }; callstack: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The error stack. VS Code telemetry scrubs file paths and likely secrets before transmission.' }; owner: 'roblourens'; comment: 'Captures diagnostic details for failed agent host turns.'; }; export interface IAgentHostTurnFailure { stage: AgentHostTurnFailureStage; error: ErrorInfo; errorName?: string; errorCode?: string; errorStack?: string; } export interface IAgentHostTurnCompletedReport extends IAgentHostTurnAttributedReport { provider: string; session: string; turnId: string; timeToFirstProgress: number | undefined; totalTime: number; result: AgentHostTurnResult; model: string | undefined; modelTelemetryKind: AgentHostModelTelemetryKind | undefined; modelSelectionKind: AgentHostModelSelectionKind; permissionLevel: string | undefined; interactionMode: SessionMode | undefined; failure: IAgentHostTurnFailure | undefined; isMultiRoot: boolean; folderCount: number; } /** * Why a turn was quiet when the hang watchdog fired. * * Unexpected (these are real bugs and are what dashboards should alert on): * - `noProgress`: the turn started but literally nothing was ever observed for * it — no text, no reasoning, no tool call, no error, no completion. This is * the signature of a lost turn (e.g. a dropped provider callback * registration) where the UI sits on "Working…" forever. * - `stalledAfterProgress`: the turn made progress and then went quiet without * anything outstanding to explain the silence. * * Expected (the turn is legitimately waiting; reported so the two populations * can be told apart in queries rather than silently dropped): * - `waitingOnUser`: a request that blocks on a *human* is outstanding — a * tool confirmation, a tool authentication, or an elicitation. Client tool * execution is deliberately excluded: it is delegated running work rather * than a prompt, and is reported as `runningTool` instead. * - `runningTool`: a tool call is still in flight. Covers genuinely long tool * invocations (builds, test runs), client-executed tools, and subagents, * whose work is reported on the subagent's own chat channel rather than the * parent turn's. */ export type AgentHostTurnHangReason = 'noProgress' | 'stalledAfterProgress' | 'waitingOnUser' | 'runningTool'; const turnActivityKindsByActionType = { [ActionType.ChatTurnStarted]: 'chat.turnStarted', [ActionType.ChatDelta]: 'chat.delta', [ActionType.ChatResponsePart]: 'chat.responsePart', [ActionType.ChatToolCallStart]: 'chat.toolCallStart', [ActionType.ChatToolCallDelta]: 'chat.toolCallDelta', [ActionType.ChatToolCallReady]: 'chat.toolCallReady', [ActionType.ChatToolCallConfirmed]: 'chat.toolCallConfirmed', [ActionType.ChatToolCallComplete]: 'chat.toolCallComplete', [ActionType.ChatToolCallResultConfirmed]: 'chat.toolCallResultConfirmed', [ActionType.ChatToolCallContentChanged]: 'chat.toolCallContentChanged', [ActionType.ChatToolCallAuthRequired]: 'chat.toolCallAuthRequired', [ActionType.ChatToolCallAuthResolved]: 'chat.toolCallAuthResolved', [ActionType.ChatTurnComplete]: 'chat.turnComplete', [ActionType.ChatTurnCancelled]: 'chat.turnCancelled', [ActionType.ChatError]: 'chat.error', [ActionType.ChatActivityChanged]: 'chat.activityChanged', [ActionType.ChatWorkingDirectorySet]: 'chat.workingDirectorySet', [ActionType.ChatWorkingDirectoryRemoved]: 'chat.workingDirectoryRemoved', [ActionType.ChatUsage]: 'chat.usage', [ActionType.ChatReasoning]: 'chat.reasoning', [ActionType.ChatPendingMessageSet]: 'chat.pendingMessageSet', [ActionType.ChatPendingMessageRemoved]: 'chat.pendingMessageRemoved', [ActionType.ChatQueuedMessagesReordered]: 'chat.queuedMessagesReordered', [ActionType.ChatDraftChanged]: 'chat.draftChanged', [ActionType.ChatInputRequested]: 'chat.inputRequested', [ActionType.ChatInputAnswerChanged]: 'chat.inputAnswerChanged', [ActionType.ChatInputCompleted]: 'chat.inputCompleted', [ActionType.ChatTruncated]: 'chat.truncated', [ActionType.ChatTurnsLoaded]: 'chat.turnsLoaded', [ActionType.SessionInputNeededSet]: 'session.inputNeededSet', [ActionType.SessionInputNeededRemoved]: 'session.inputNeededRemoved', } as const; export type AgentHostTurnActivityTelemetryKind = 'none' | 'other' | typeof turnActivityKindsByActionType[keyof typeof turnActivityKindsByActionType]; function normalizeTurnActivityKind(activityKind: string): AgentHostTurnActivityTelemetryKind { if (activityKind === 'none') { return 'none'; } return turnActivityKindsByActionType[activityKind as keyof typeof turnActivityKindsByActionType] ?? 'other'; } export interface IAgentHostTurnHungEvent extends IAgentHostInitiatorTelemetry { provider: string; agentSessionId: string; chatSessionId: string; isSubagentSession: boolean; turnId: string; hangReason: AgentHostTurnHangReason; isExpected: boolean; hadAnyProgress: boolean; lastActivityKind: AgentHostTurnActivityTelemetryKind; blockedOn: SessionInputRequestKind | undefined; toolId: string | undefined; toolSourceKind: string | undefined; inFlightToolCallCount: number; quietTimeMs: number; turnElapsedMs: number; model: string | TelemetryTrustedValue | undefined; modelSelectionKind: AgentHostModelSelectionKind; permissionLevel: string | undefined; } export type IAgentHostTurnHungClassification = IAgentHostInitiatorClassification & { provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the hung agent host turn.' }; agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' }; chatSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The chat identifier within the agent host session.' }; isSubagentSession: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the hung turn belongs to a subagent session.' }; turnId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the hung turn within the agent host session.' }; hangReason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The bounded state the turn was quiet in: noProgress, stalledAfterProgress, waitingOnUser, or runningTool.' }; isExpected: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the quiet period is explained by a legitimate wait (blocked on the user or running a tool) rather than an unexplained hang.' }; hadAnyProgress: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether any turn activity at all was observed before the watchdog fired.' }; lastActivityKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'A bounded category for the last observed turn activity, preserving the AHP action namespace and action name without slash-like syntax. Values are none, other, or categories such as chat.delta and chat.toolCallReady.' }; blockedOn: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The kind of outstanding user-blocking session input request, when there is one. Client tool execution is not counted, since it is delegated work rather than a prompt.' }; toolId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the tool the turn appears to be stuck on. When hangReason is waitingOnUser this is the tool gated by the blocking request, which is exact; when it is runningTool this is the longest-running in-flight tool call, which is a best guess when several are running. Undefined when no tool explains the hang.' }; toolSourceKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the stuck tool is provided by the agent host, an MCP server, or a client.' }; inFlightToolCallCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Number of tool calls that had started but not completed when the watchdog fired. When hangReason is runningTool, a value above one means toolId is a best guess among several running tools; when it is waitingOnUser, toolId comes from the blocking request and is exact regardless of this count.' }; quietTimeMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds since the last observed turn activity.' }; turnElapsedMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds from turn start to the hang report.' }; model: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The trusted provider model identifier for the turn, or a generic value for BYOK and unknown models.' }; modelSelectionKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the client used the provider default, Auto, or an explicit model.' }; permissionLevel: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The tool auto-approval level configured for the session at turn start (e.g. default, autoApprove, autopilot).' }; owner: 'roblourens'; comment: 'Tracks agent host turns that stop making progress for longer than the hang threshold, so permanently stuck sessions are visible as a positive signal instead of missing turnCompleted events.'; }; export interface IAgentHostTurnHungReport extends IAgentHostTurnAttributedReport { provider: string; session: string; turnId: string; hangReason: AgentHostTurnHangReason; hadAnyProgress: boolean; lastActivityKind: string; blockedOn: SessionInputRequestKind | undefined; toolId: string | undefined; toolSourceKind: string | undefined; inFlightToolCallCount: number; quietTimeMs: number; turnElapsedMs: number; model: string | undefined; modelTelemetryKind: AgentHostModelTelemetryKind | undefined; modelSelectionKind: AgentHostModelSelectionKind; permissionLevel: string | undefined; } export interface IAgentHostHungTurnCompletedEvent extends IAgentHostInitiatorTelemetry { provider: string; agentSessionId: string; chatSessionId: string; isSubagentSession: boolean; turnId: string; hangReason: AgentHostTurnHangReason; result: AgentHostTurnResult; hangReportCount: number; totalTimeMs: number; timeAfterHangMs: number; } export type IAgentHostHungTurnCompletedClassification = IAgentHostInitiatorClassification & { provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the recovered agent host turn.' }; agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' }; chatSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The chat identifier within the agent host session.' }; isSubagentSession: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the recovered turn belongs to a subagent session.' }; turnId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the recovered turn within the agent host session.' }; hangReason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The most recently reported hang reason for the turn before it completed.' }; result: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the previously hung turn eventually completed successfully, with an error, or was cancelled.' }; hangReportCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Number of hang reports emitted for the turn before it completed.' }; totalTimeMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Total time in milliseconds from turn start to turn completion.' }; timeAfterHangMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds from the most recent hang report to turn completion.' }; owner: 'roblourens'; comment: 'Tracks agent host turns that complete after previously being reported as hung, so permanent hangs can be separated from merely slow ones.'; }; export interface IAgentHostHungTurnCompletedReport extends IAgentHostTurnAttributedReport { provider: string; session: string; turnId: string; hangReason: AgentHostTurnHangReason; result: AgentHostTurnResult; hangReportCount: number; totalTimeMs: number; timeAfterHangMs: number; } export interface IAgentHostToolInvokedReport extends IAgentHostTurnAttributedReport { provider: string; session: string; turnId: string; toolId: string; toolSourceKind: string; toolCallId: string; result: ToolInvokedResult; invocationTimeMs?: number; resultSizeInCharacters: number; model: string | undefined; modelTelemetryKind: AgentHostModelTelemetryKind | undefined; } export interface IAgentHostAskQuestionsToolInvokedEvent extends IAgentHostInitiatorTelemetry { requestId: string; questionCount: number; answeredCount: number; skippedCount: number; freeTextCount: number; recommendedAvailableCount: number; recommendedSelectedCount: number; duration: number; provider: string; agentSessionId: string; isSubagentSession: boolean; } export type IAgentHostAskQuestionsToolInvokedClassification = IAgentHostInitiatorClassification & { requestId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the current request turn.' }; questionCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The total number of questions asked' }; answeredCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of questions that were answered' }; skippedCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of questions that were skipped' }; freeTextCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of questions answered with free text input' }; recommendedAvailableCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of questions that had a recommended option' }; recommendedSelectedCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of questions where the user selected the recommended option' }; duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The total time in milliseconds to complete all questions' }; provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider handling the agent host session.' }; agentSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent host session identifier.' }; isSubagentSession: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the questions belong to a subagent session.' }; owner: 'digitarald'; comment: 'Tracks usage of the AskQuestions tool for agent clarifications'; }; export interface IAgentHostAskQuestionsToolInvokedReport extends IAgentHostTurnAttributedReport { provider: string; session: string; requestId: string; questionCount: number; answeredCount: number; skippedCount: number; freeTextCount: number; recommendedAvailableCount: number; recommendedSelectedCount: number; duration: number; } type AgentHostToolCallResponseType = 'success' | 'cancelled' | 'failed'; export interface IAgentHostToolCallDetailsEvent extends IAgentHostInitiatorTelemetry { provider: string; agentSessionId: string; isSubagentSession: boolean; conversationId: string; requestId: string; responseType: AgentHostToolCallResponseType; toolCounts: string; model: string | undefined; numRequests: number; turnIndex: number; turnDuration: number; messageCharLen: number | undefined; availableToolCount: number; totalToolCalls: number; parallelToolCallRounds: number; parallelToolCallsTotal: number; } export type IAgentHostToolCallDetailsClassification = IAgentHostInitiatorClassification & { provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider handling the agent host session.' }; agentSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent host session identifier.' }; isSubagentSession: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the tool-call aggregate belongs to a subagent session.' }; conversationId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the current chat conversation.' }; requestId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the current turn request.' }; responseType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the turn completed successfully, was cancelled, or failed.' }; toolCounts: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of times each tool was requested during the turn.' }; model: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The model used for the final model call in the turn, when known.' }; numRequests: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of model-call rounds in the turn.' }; turnIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The zero-based turn ordinal within the agent host session.' }; turnDuration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The elapsed time in milliseconds for the turn.' }; messageCharLen: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of characters in the user message, when known.' }; availableToolCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tools offered to the model.' }; totalToolCalls: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The total number of tool calls requested during the turn.' }; parallelToolCallRounds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of model-call rounds containing multiple tool calls.' }; parallelToolCallsTotal: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tool calls belonging to parallel tool-call rounds.' }; owner: 'roblourens'; comment: 'Records aggregate information about tool calls during an agent host turn.'; }; export interface IAgentHostToolCallDetailsReport extends IAgentHostTurnAttributedReport { provider: string; session: string; turnId: string; clientType: AgentHostClientType; model: string | undefined; responseType: AgentHostToolCallResponseType; /** Count of invocations keyed by tool name, across all rounds in the turn. */ toolCounts: Record; /** Names of the tools offered to the model for this turn. */ availableTools: readonly string[]; /** Number of model-call rounds in the turn, including the final tool-free response round (matches the extension's `toolCallRounds.length`). */ numRequests: number; turnIndex: number; turnDuration: number; messageCharLen: number | undefined; totalToolCalls: number; parallelToolCallRounds: number; parallelToolCallsTotal: number; } export interface IAgentHostToolApprovalReport extends IAgentHostTurnAttributedReport { provider: string; session: string; turnId: string; toolId: string; toolSourceKind: string; confirmKind: AgentHostToolApprovalConfirmKind; confirmationNotNeededReason: string | undefined; requestUnsandboxedExecution: boolean | undefined; } type AgentHostToolApprovalConfirmKind = 'userAction' | 'setting' | 'confirmationNotNeeded' | 'denied'; export interface IAgentHostToolApprovalEvent extends IAgentHostInitiatorTelemetry { provider: string; agentSessionId: string; isSubagentSession: boolean; chatSessionId: string; requestId: string; toolId: string; toolExtensionId: string | undefined; toolSourceKind: string; confirmKind: AgentHostToolApprovalConfirmKind; settingId: string | undefined; lmServiceScope: string | undefined; customButtonKind: string | undefined; confirmationNotNeededReason: string | undefined; sandboxWrapped: boolean | undefined; requestUnsandboxedExecution: boolean | undefined; } export type IAgentHostToolApprovalClassification = IAgentHostInitiatorClassification & { provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider handling the agent host session.' }; agentSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent host session identifier.' }; isSubagentSession: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the tool approval belongs to a subagent session.' }; chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat session that the tool was used within, if applicable.' }; requestId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat request turn that this tool approval is associated with, if available.' }; toolId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the tool used.' }; toolExtensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension that contributed the tool.' }; toolSourceKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source kind of the tool.' }; confirmKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the confirmation was resolved (userAction, setting, confirmationNotNeeded, denied).' }; settingId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When confirmKind is setting, the configuration id that auto-approved the tool.' }; lmServiceScope: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When confirmKind is lmServicePerTool, the scope (session/workspace/profile).' }; customButtonKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the user clicked a custom button on the confirmation widget, whether the button represents approve or deny semantics.' }; confirmationNotNeededReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When confirmKind is confirmationNotNeeded, a stable identifier for why the tool did not require confirmation.' }; sandboxWrapped: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'For terminal tool calls, whether this specific invocation runs inside the agent terminal sandbox.' }; requestUnsandboxedExecution: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'For terminal tool calls, whether the model requested to bypass the sandbox for this invocation.' }; owner: 'roblourens'; comment: 'Provides insight into how tool confirmations are resolved (user action vs. auto-approval) in agent host sessions.'; }; export interface IAgentHostAutoModeRouterDecisionReport { session: string; turnId: string; clientType: AgentHostClientType; chosenModel: string; predictedLabel: string | undefined; confidence: number | undefined; candidateModels: readonly string[] | undefined; categoryScores: Readonly> | undefined; routingMethod: string | undefined; availableModels: readonly string[] | undefined; fallback: boolean | undefined; fallbackReason: string | undefined; stickyOverride: boolean | undefined; routerLatencyMs: number | undefined; endToEndLatencyMs: number | undefined; chosenShortfall: number | undefined; hasImage: boolean | undefined; } export interface IAgentHostSkillContentReadReport { clientType: AgentHostClientType; /** The skill name. */ name: string; /** Path to the SKILL.md file. */ path: string; /** Full skill content; hashed (never sent raw), matching the extension. */ content: string; /** Where the skill was discovered (project, personal-copilot, plugin, builtin, …). */ source: string | undefined; /** Name of the plugin the skill came from, when applicable (AH-native analog of the extension's skill extension id). */ pluginName: string | undefined; /** Version of the plugin the skill came from, when applicable. */ pluginVersion: string | undefined; } export type AgentHostRepoInfoResult = 'success' | 'filesChanged' | 'diffTooLarge' | 'noChanges' | 'tooManyChanges' | 'mergeBaseTooOld' | 'virtualFileSystem' | 'tooManyCommits'; export interface IAgentHostRepoInfoReport { telemetryMessageId: string; clientType: AgentHostClientType; location: 'begin' | 'end'; remoteUrl: string; repoId: string; repoType: 'github' | 'ado'; headCommitHash: string; headBranchName: string | undefined; fileRelativePaths: string | undefined; diffsJSON: string | undefined; result: AgentHostRepoInfoResult; isActiveRepository: 'true'; workspaceFileCount: number; changedFileCount: number; diffSizeBytes: number; } export interface IAgentHostToolCallStalledEvent extends IAgentHostInitiatorTelemetry { provider: string; agentSessionId: string; isSubagentSession: boolean; blockerKind: SessionInputRequestKind.ToolConfirmation | SessionInputRequestKind.ToolClientExecution | SessionInputRequestKind.ToolAuthentication; toolId: string; toolSourceKind: string; stalledTimeMs: number; } export type IAgentHostToolCallStalledClassification = IAgentHostInitiatorClassification & { provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the stalled agent host tool call.' }; agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' }; isSubagentSession: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the stalled tool call belongs to a subagent session.' }; blockerKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the tool call is waiting for confirmation or client execution.' }; toolId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the stalled tool.' }; toolSourceKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the stalled tool is provided by the agent host, an MCP server, or a client.' }; stalledTimeMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds that the tool call has remained blocked.' }; owner: 'roblourens'; comment: 'Tracks agent host tool calls that remain blocked beyond the stall threshold.'; }; export interface IAgentHostToolCallStalledReport extends IAgentHostTurnAttributedReport { provider: string; session: string; blockerKind: SessionInputRequestKind.ToolConfirmation | SessionInputRequestKind.ToolClientExecution | SessionInputRequestKind.ToolAuthentication; toolId: string; toolSourceKind: string; stalledTimeMs: number; } export interface IAgentHostStalledToolCallCompletedEvent extends IAgentHostInitiatorTelemetry { provider: string; agentSessionId: string; isSubagentSession: boolean; blockerKind: SessionInputRequestKind.ToolConfirmation | SessionInputRequestKind.ToolClientExecution | SessionInputRequestKind.ToolAuthentication; toolId: string; toolSourceKind: string; result: ToolInvokedResult; totalTimeMs: number; timeAfterStallMs: number; } export type IAgentHostStalledToolCallCompletedClassification = IAgentHostInitiatorClassification & { provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the completed agent host tool call.' }; agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' }; isSubagentSession: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the completed tool call belongs to a subagent session.' }; blockerKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the tool call had stalled waiting for confirmation or client execution.' }; toolId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the completed tool.' }; toolSourceKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the completed tool is provided by the agent host, an MCP server, or a client.' }; result: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the stalled tool call eventually completed successfully, with an error, or through user cancellation.' }; totalTimeMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Total time in milliseconds from tool call start to completion.' }; timeAfterStallMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds from the stall report to tool call completion.' }; owner: 'roblourens'; comment: 'Tracks agent host tool calls that complete after previously exceeding the stall threshold.'; }; export interface IAgentHostStalledToolCallCompletedReport extends IAgentHostTurnAttributedReport { provider: string; session: string; blockerKind: SessionInputRequestKind.ToolConfirmation | SessionInputRequestKind.ToolClientExecution | SessionInputRequestKind.ToolAuthentication; toolId: string; toolSourceKind: string; result: ToolInvokedResult; totalTimeMs: number; timeAfterStallMs: number; } function toTelemetryModel(model: string | undefined, modelTelemetryKind: AgentHostModelTelemetryKind | undefined): string | TelemetryTrustedValue | undefined { if (model === undefined) { return undefined; } if (modelTelemetryKind === 'trusted') { return new TelemetryTrustedValue(model); } return modelTelemetryKind === 'byok' ? 'byokModel' : 'unknown'; } export function toInitiatorTelemetry(clientContext: IAgentHostClientTelemetryContext | undefined): IAgentHostInitiatorTelemetry { return { ...(clientContext?.clientType !== undefined && clientContext.clientType !== AgentHostClientType.Unknown ? { initiatorClientType: clientContext.clientType } : {}), ...(clientContext?.connectionKind !== undefined && clientContext.connectionKind !== AgentHostClientConnectionKind.Unknown ? { initiatorConnectionKind: clientContext.connectionKind } : {}), ...(clientContext?.transportKind !== undefined && clientContext.transportKind !== AgentHostTransportKind.Unknown ? { initiatorTransportKind: clientContext.transportKind } : {}), ...(clientContext?.hostLaunchKind !== undefined && clientContext.hostLaunchKind !== AgentHostLaunchKind.Unknown ? { hostLaunchKind: clientContext.hostLaunchKind } : {}), ...(clientContext?.machineId ? { initiatorMachineId: clientContext.machineId } : {}), ...(clientContext?.devDeviceId ? { initiatorDevDeviceId: clientContext.devDeviceId } : {}), }; } export class AgentHostTelemetryReporter { constructor(private readonly _telemetryService: ITelemetryService) { } /** The restricted GH/MSFT telemetry surface, present when the agent-host telemetry service is wired. */ private get _restricted(): IAgentHostRestrictedTelemetry | undefined { const ts = this._telemetryService as Partial; return typeof ts.sendEnhancedGHTelemetryEvent === 'function' ? ts as IAgentHostRestrictedTelemetry : undefined; } executionModeChanged(provider: string, session: string, previousMode: SessionMode, newMode: SessionMode, turnCount: number, clientContext?: IAgentHostClientTelemetryContext): void { this._telemetryService.publicLog2('agentHost.executionModeChanged', { ...toInitiatorTelemetry(clientContext), provider, agentSessionId: AgentSession.id(session), isSubagentSession: isSubagentSession(session), previousMode, newMode, turnCount, }); } userMessageSent(provider: string, clientId: string | undefined, clientContext: IAgentHostClientTelemetryContext, session: string, turnId: string, sessionState: ISessionWithDefaultChat | undefined, source: AgentHostUserMessageSentSource, attachments: readonly MessageAttachment[] | undefined): void { const attachmentCount = attachments?.length ?? 0; const activeClients = sessionState?.activeClients ?? []; const sessionUri = isAhpChatChannel(session) ? parseRequiredSessionUriFromChatUri(session) : session; this._telemetryService.publicLog2('agentHost.userMessageSent', { provider, hostLaunchKind: clientContext.hostLaunchKind, initiatorClientId: clientId, initiatorClientType: clientContext.clientType, initiatorConnectionKind: clientContext.connectionKind, initiatorTransportKind: clientContext.transportKind, ...(clientContext.machineId ? { initiatorMachineId: clientContext.machineId } : {}), ...(clientContext.devDeviceId ? { initiatorDevDeviceId: clientContext.devDeviceId } : {}), agentSessionId: AgentSession.id(sessionUri), source, isSubagentSession: isSubagentSession(sessionUri), turnCount: sessionState?.turns.length ?? 0, ...(activeClients.length > 0 ? { activeClientId: activeClients[0].clientId, activeClientToolCount: activeClients.reduce((sum, client) => sum + client.tools.length, 0), activeClientCustomizationCount: activeClients.reduce((sum, client) => sum + (client.customizations?.length ?? 0), 0), } : {}), attachmentCount, }); this._restricted?.sendGHTelemetryEvent('agentHost.userMessageSent', { provider, initiatorClientType: clientContext.clientType, conversationId: AgentSession.id(sessionUri), turnId, }); } clientConnection(report: IAgentHostClientConnectionReport): void { this._telemetryService.publicLog2('agentHost.clientConnection', { action: report.action, hostLaunchKind: report.context.hostLaunchKind, clientId: report.clientId, clientType: report.context.clientType, clientImplementationName: report.clientImplementationName, clientImplementationVersion: report.clientImplementationVersion, connectionKind: report.context.connectionKind, transportKind: report.context.transportKind, ...(report.context.machineId ? { clientMachineId: report.context.machineId } : {}), ...(report.context.devDeviceId ? { clientDevDeviceId: report.context.devDeviceId } : {}), protocolVersion: report.protocolVersion, isReconnect: report.isReconnect, connectedClientCount: report.connectedClientCount, connectedTransportCount: report.connectedTransportCount, clientTransportCount: report.clientTransportCount, connectionDurationMs: report.connectionDurationMs, subscriptionCount: report.subscriptionCount, }); } /** * Mirrors the Copilot extension's enhanced GH `request.options.tools` event for the agent-host * flow. The extension emits it per LLM request from its model fetcher; the agent host observes * the equivalent boundary when an `assistant.message` arrives (one per model call). The * `headerRequestId` is the client-minted `x-request-id`, matching the extension. `messagesJson` * is the raw tool definitions offered for the call, multiplexed across ~8192-char chunks like * the extension, so it lands identically downstream. * * @param session Session URI string; its id becomes `conversationId`. * @param clientRequestId The model call's client-minted `x-request-id`, mapped to the extension's `headerRequestId`. No-ops when absent (e.g. providers that don't surface it). * @param tools The tool definitions offered to the model for this call. */ async assistantMessageReceived(session: string, clientType: AgentHostClientType, clientRequestId: string | undefined, tools: readonly ToolDefinition[]): Promise { const restricted = this._restricted; if (!restricted || !clientRequestId || tools.length === 0) { return; } restricted.sendEnhancedGHTelemetryEvent('request.options.tools', await multiplexProperties({ headerRequestId: clientRequestId, conversationId: AgentSession.id(session), initiatorClientType: clientType, messagesJson: JSON.stringify(tools), })); } /** * Mirrors the Copilot extension's restricted `conversation.messageText` event (the panel-chat * prefix of `sendConversationalMessageTelemetry`) for the user's prompt. The extension emits it * for every user and model message, carrying the raw message text to the enhanced GH * (`copilot_v0_restricted_copilot_event`) and internal MSFT pipelines; the agent host observes * the same boundary at the SDK `user.message` event. The text is multiplexed across ~8192-char * chunks (`messageText`, `messageText_02`, …) so long prompts land untruncated, matching the * extension's `multiplexProperties`. * * @param session Session URI string; its id becomes `conversationId`. * @param content The user's prompt text. No-ops when empty. * @param turnIndex The 0-based ordinal of the turn this message belongs to, matching the extension's numeric `turnIndex` (`conversation.turns.length`). CTS parses `turn_index` as an integer, so a numeric ordinal is required here (a non-numeric id lands empty). */ async userMessageText(session: string, clientType: AgentHostClientType, content: string, turnIndex: number): Promise { const restricted = this._restricted; if (!restricted || !content) { return; } const properties = await multiplexProperties({ source: 'user', conversationId: AgentSession.id(session), initiatorClientType: clientType, turnIndex: String(turnIndex), messageText: content, }); const measurements = { messageCharLen: content.length }; restricted.sendEnhancedGHTelemetryEvent('conversation.messageText', properties, measurements); restricted.sendInternalMSFTTelemetryEvent('conversation.messageText', properties, measurements); } /** * The model-message counterpart to {@link userMessageText}. Emitted when an `assistant.message` * arrives (the agent host's per-model-call boundary), carrying the assistant's response text. * `headerRequestId` is filled with the model call's client-minted `x-request-id`, matching the * extension. VS Code-only enrichment dims (code-block languages/counts) are not reconstructed here. * * @param session Session URI string; its id becomes `conversationId`. * @param content The assistant's response text. No-ops when empty. * @param turnIndex The 0-based ordinal of the turn this message belongs to, matching the extension's numeric `turnIndex` (`conversation.turns.length`). CTS parses `turn_index` as an integer, so a numeric ordinal is required here. * @param clientRequestId The model call's client-minted `x-request-id`, mapped to `headerRequestId`. */ async modelMessageText(session: string, clientType: AgentHostClientType, content: string, turnIndex: number, clientRequestId: string | undefined): Promise { const restricted = this._restricted; if (!restricted || !content) { return; } const properties = await multiplexProperties({ source: 'model', conversationId: AgentSession.id(session), initiatorClientType: clientType, turnIndex: String(turnIndex), ...(clientRequestId ? { headerRequestId: clientRequestId } : {}), messageText: content, }); const measurements = { messageCharLen: content.length }; restricted.sendEnhancedGHTelemetryEvent('conversation.messageText', properties, measurements); restricted.sendInternalMSFTTelemetryEvent('conversation.messageText', properties, measurements); } /** * Emits the local-compatible tool-call aggregate on standard and restricted telemetry channels. */ async toolCallDetails(report: IAgentHostToolCallDetailsReport): Promise { if (report.availableTools.length === 0) { return; } const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session; const conversationId = AgentSession.id(session); const toolCounts = JSON.stringify(report.toolCounts); this._telemetryService.publicLog2('toolCallDetails', { ...toInitiatorTelemetry(report.clientContext), provider: report.provider, agentSessionId: conversationId, isSubagentSession: isSubagentSession(session), conversationId, requestId: report.turnId, responseType: report.responseType, toolCounts, model: report.model, numRequests: report.numRequests, turnIndex: report.turnIndex, turnDuration: report.turnDuration, messageCharLen: report.messageCharLen, availableToolCount: report.availableTools.length, totalToolCalls: report.totalToolCalls, parallelToolCallRounds: report.parallelToolCallRounds, parallelToolCallsTotal: report.parallelToolCallsTotal, }); const restricted = this._restricted; if (!restricted) { return; } const properties = await multiplexProperties({ conversationId, requestId: report.turnId, messageId: report.turnId, initiatorClientType: report.clientType, responseType: report.responseType, ...(report.model ? { model: report.model } : {}), toolCounts, availableTools: JSON.stringify(report.availableTools), }); const measurements = { numRequests: report.numRequests, turnIndex: report.turnIndex, turnDuration: report.turnDuration, ...(report.messageCharLen !== undefined ? { messageCharLen: report.messageCharLen } : {}), availableToolCount: report.availableTools.length, totalToolCalls: report.totalToolCalls, parallelToolCallRounds: report.parallelToolCallRounds, parallelToolCallsTotal: report.parallelToolCallsTotal, }; restricted.sendEnhancedGHTelemetryEvent('toolCallDetailsExternal', properties, measurements); restricted.sendInternalMSFTTelemetryEvent('toolCallDetailsInternal', properties, measurements); } /** Emits the workbench-compatible tool-approval telemetry from the agent host on the standard telemetry channel. */ toolApproval(report: IAgentHostToolApprovalReport): void { const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session; const agentSessionId = AgentSession.id(session); this._telemetryService.publicLog2('chat.toolApproval', { ...toInitiatorTelemetry(report.clientContext), provider: report.provider, agentSessionId, isSubagentSession: isSubagentSession(session), chatSessionId: agentSessionId, requestId: report.turnId, toolId: report.toolId, toolExtensionId: undefined, toolSourceKind: report.toolSourceKind, confirmKind: report.confirmKind, settingId: undefined, lmServiceScope: undefined, customButtonKind: undefined, confirmationNotNeededReason: report.confirmationNotNeededReason, sandboxWrapped: undefined, requestUnsandboxedExecution: report.requestUnsandboxedExecution, }); } /** Emits the extension's restricted `automode.routerDecisionRestricted` event from authoritative SDK fields. */ autoModeRouterDecision(report: IAgentHostAutoModeRouterDecisionReport): void { const restricted = this._restricted; if (!restricted) { return; } const categoryScores = report.categoryScores ?? {}; const isBinary = categoryScores.needs_reasoning !== undefined || categoryScores.no_reasoning !== undefined; const scoreKeys = Object.keys(categoryScores).filter(key => categoryScores[key] !== undefined); const candidateModels = report.candidateModels ?? []; const properties = { conversationId: AgentSession.id(report.session), vscodeRequestId: report.turnId, initiatorClientType: report.clientType, ...(report.predictedLabel !== undefined ? { predictedLabel: report.predictedLabel } : {}), ...(report.routingMethod !== undefined ? { routingMethod: report.routingMethod } : {}), ...(report.fallback !== undefined ? { fallback: String(report.fallback) } : {}), ...(report.fallbackReason !== undefined ? { fallbackReason: report.fallbackReason } : {}), candidateModel: candidateModels[0] ?? '', chosenModel: report.chosenModel, candidateModels: JSON.stringify(candidateModels), ...(report.availableModels !== undefined ? { availableModels: JSON.stringify(report.availableModels) } : {}), ...(report.stickyOverride !== undefined ? { stickyOverrideStr: String(report.stickyOverride) } : {}), ...(report.hasImage !== undefined ? { hasImage: String(report.hasImage) } : {}), ...(scoreKeys.length > 0 ? { [isBinary ? 'binaryScores' : 'hydraScores']: JSON.stringify(categoryScores), } : {}), }; const measurements = { ...(report.confidence !== undefined ? { confidence: report.confidence } : {}), ...(report.routerLatencyMs !== undefined ? { latencyMs: report.routerLatencyMs } : {}), ...(report.endToEndLatencyMs !== undefined ? { e2eLatencyMs: report.endToEndLatencyMs } : {}), ...(report.stickyOverride !== undefined ? { stickyOverride: report.stickyOverride ? 1 : 0 } : {}), ...(report.chosenShortfall !== undefined ? { chosenShortfall: report.chosenShortfall } : {}), ...(categoryScores.needs_reasoning !== undefined ? { scoreNeedsReasoning: categoryScores.needs_reasoning } : {}), ...(categoryScores.no_reasoning !== undefined ? { scoreNoReasoning: categoryScores.no_reasoning } : {}), }; restricted.sendEnhancedGHTelemetryEvent('automode.routerDecisionRestricted', properties, measurements); } /** * Mirrors the Copilot extension's restricted `skillContentRead` event (`skillTelemetry.ts` -> * `sendSkillContentReadTelemetry`) — records which skill file was loaded into the conversation. * The extension emits it from the skill/readFile tools; the agent host observes the equivalent * boundary at the SDK `skill.invoked` event, whose payload already carries the content (hashed * here, never sent raw), the discovery `source`, and the plugin identity. The extension's * `skillExtensionId` / `skillExtensionVersion` encode the contributing *VS Code extension*, which * does not exist in the agent host; the AH-native provenance is the plugin, so `pluginName` / * `pluginVersion` fill those columns. No-ops when the skill name is empty. * * @param report The invoked skill's metadata (from the SDK `skill.invoked` payload). */ skillContentRead(report: IAgentHostSkillContentReadReport): void { const restricted = this._restricted; if (!restricted || !report.name) { return; } const contentHash = report.content ? String(hash(report.content)) : ''; const skillStorage = report.source ?? ''; // Match the extension: the version is only reported when the (plugin) id is known, so we // never emit a `skillExtensionVersion` without a corresponding `skillExtensionId`. const skillExtensionVersion = report.pluginName ? (report.pluginVersion ?? '') : ''; const plaintextProps = { initiatorClientType: report.clientType, skillName: report.name, skillPath: report.path, skillExtensionId: report.pluginName ?? '', skillExtensionVersion, skillStorage, skillContentHash: contentHash, }; restricted.sendGHTelemetryEvent('skillContentRead', { initiatorClientType: report.clientType, skillNameHash: String(hash(report.name)), skillExtensionIdHash: report.pluginName ? String(hash(report.pluginName)) : '', skillExtensionVersion, skillStorage, skillContentHash: contentHash, }); restricted.sendEnhancedGHTelemetryEvent('skillContentRead', plaintextProps); restricted.sendInternalMSFTTelemetryEvent('skillContentRead', plaintextProps); } async reportRepoInfo(context: IAgentHostRestrictedTelemetryContext, report: IAgentHostRepoInfoReport): Promise { const restricted = this._restricted; if (!restricted) { return; } const properties = { initiatorClientType: report.clientType, remoteUrl: report.remoteUrl, repoId: report.repoId, repoType: report.repoType, headCommitHash: report.headCommitHash, headBranchName: report.headBranchName, fileRelativePaths: report.fileRelativePaths, diffsJSON: report.diffsJSON, result: report.result, isActiveRepository: report.isActiveRepository, location: report.location, telemetryMessageId: report.telemetryMessageId, }; const measurements = { workspaceFileCount: report.workspaceFileCount, changedFileCount: report.changedFileCount, diffSizeBytes: report.diffSizeBytes, repoIndex: 0, repoCount: 1, }; const { headBranchName: _, fileRelativePaths: _2, ...internalProperties } = properties; const [enhancedProperties, internalMultiplexedProperties] = await Promise.all([ multiplexProperties(properties), multiplexProperties(internalProperties), ]); restricted.sendEnhancedGHTelemetryEventForContext(context, 'request.repoInfo', enhancedProperties, measurements); restricted.sendInternalMSFTTelemetryEventForContext(context, 'request.repoInfo', internalMultiplexedProperties, measurements); } turnCompleted(report: IAgentHostTurnCompletedReport): void { const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session; const chatSessionId = getTelemetryChatSessionId(report.session); const isSubagent = isSubagentChatUri(report.session) || isSubagentSession(session); const model = toTelemetryModel(report.model, report.modelTelemetryKind); this._telemetryService.publicLog2('agentHost.turnCompleted', { ...toInitiatorTelemetry(report.clientContext), provider: report.provider, agentSessionId: AgentSession.id(session), chatSessionId, isSubagentSession: isSubagent, turnId: report.turnId, timeToFirstProgress: report.timeToFirstProgress, totalTime: report.totalTime, result: report.result, model, modelSelectionKind: report.modelSelectionKind, isBYOK: report.modelTelemetryKind === undefined ? undefined : report.modelTelemetryKind === 'byok', permissionLevel: report.permissionLevel, interactionMode: report.interactionMode, errorType: report.failure?.error.errorType, failureStage: report.failure?.stage, isMultiRoot: report.isMultiRoot, folderCount: report.folderCount, }); if (report.failure) { const { providerCallId, serviceRequestId } = readAgentErrorTelemetryMeta(report.failure.error); this._telemetryService.publicLogError2('agentHost.turnFailed', { ...toInitiatorTelemetry(report.clientContext), provider: report.provider, agentSessionId: AgentSession.id(session), chatSessionId, isSubagentSession: isSubagent, turnId: report.turnId, failureStage: report.failure.stage, errorType: report.failure.error.errorType, errorName: report.failure.errorName, errorCode: report.failure.errorCode, providerCallId, serviceRequestId, msg: report.failure.error.message, callstack: report.failure.errorStack ?? report.failure.error.stack, }); } } /** * Reports a turn that has stopped producing activity for longer than the * hang threshold. See {@link AgentHostTurnHangReason} for which reasons are * expected waits and which indicate a real hang. */ turnHung(report: IAgentHostTurnHungReport): void { const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session; this._telemetryService.publicLog2('agentHost.turnHung', { ...toInitiatorTelemetry(report.clientContext), provider: report.provider, agentSessionId: AgentSession.id(session), chatSessionId: getTelemetryChatSessionId(report.session), isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session), turnId: report.turnId, hangReason: report.hangReason, isExpected: report.hangReason === 'waitingOnUser' || report.hangReason === 'runningTool', hadAnyProgress: report.hadAnyProgress, lastActivityKind: normalizeTurnActivityKind(report.lastActivityKind), blockedOn: report.blockedOn, toolId: report.toolId, toolSourceKind: report.toolSourceKind, inFlightToolCallCount: report.inFlightToolCallCount, quietTimeMs: report.quietTimeMs, turnElapsedMs: report.turnElapsedMs, model: toTelemetryModel(report.model, report.modelTelemetryKind), modelSelectionKind: report.modelSelectionKind, permissionLevel: report.permissionLevel, }); } /** Paired recovery event for a turn previously reported by {@link turnHung}. */ hungTurnCompleted(report: IAgentHostHungTurnCompletedReport): void { const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session; this._telemetryService.publicLog2('agentHost.hungTurnCompleted', { ...toInitiatorTelemetry(report.clientContext), provider: report.provider, agentSessionId: AgentSession.id(session), chatSessionId: getTelemetryChatSessionId(report.session), isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session), turnId: report.turnId, hangReason: report.hangReason, result: report.result, hangReportCount: report.hangReportCount, totalTimeMs: report.totalTimeMs, timeAfterHangMs: report.timeAfterHangMs, }); } toolInvoked(report: IAgentHostToolInvokedReport): void { // `chatSessionId` is the full session URI string (matching the value // previously emitted by `CopilotAgentSession`). Action signals are keyed // by their chat-channel URI, so normalize it back to the session URI. const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session; this._telemetryService.publicLog2('languageModelToolInvoked', { ...toInitiatorTelemetry(report.clientContext), result: report.result, chatSessionId: session, toolId: report.toolId, toolExtensionId: undefined, toolSourceKind: report.toolSourceKind, toolCallId: report.toolCallId, invocationTimeMs: report.invocationTimeMs, provider: report.provider, resultSizeInCharacters: report.resultSizeInCharacters, turnId: report.turnId, model: toTelemetryModel(report.model, report.modelTelemetryKind), }); } askQuestionsToolInvoked(report: IAgentHostAskQuestionsToolInvokedReport): void { const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session; this._telemetryService.publicLog2('askQuestionsToolInvoked', { ...toInitiatorTelemetry(report.clientContext), requestId: report.requestId, questionCount: report.questionCount, answeredCount: report.answeredCount, skippedCount: report.skippedCount, freeTextCount: report.freeTextCount, recommendedAvailableCount: report.recommendedAvailableCount, recommendedSelectedCount: report.recommendedSelectedCount, duration: report.duration, provider: report.provider, agentSessionId: AgentSession.id(session), isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session), }); } toolCallStalled(report: IAgentHostToolCallStalledReport): void { const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session; this._telemetryService.publicLog2('agentHost.toolCallStalled', { ...toInitiatorTelemetry(report.clientContext), provider: report.provider, agentSessionId: AgentSession.id(session), isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session), blockerKind: report.blockerKind, toolId: report.toolId, toolSourceKind: report.toolSourceKind, stalledTimeMs: report.stalledTimeMs, }); } stalledToolCallCompleted(report: IAgentHostStalledToolCallCompletedReport): void { const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session; this._telemetryService.publicLog2('agentHost.stalledToolCallCompleted', { ...toInitiatorTelemetry(report.clientContext), provider: report.provider, agentSessionId: AgentSession.id(session), isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session), blockerKind: report.blockerKind, toolId: report.toolId, toolSourceKind: report.toolSourceKind, result: report.result, totalTimeMs: report.totalTimeMs, timeAfterStallMs: report.timeAfterStallMs, }); } }