microsoft/vscode · #334378
agentHost: classify inline chat telemetry
src/vs/platform/agentHost/node/agentHostTelemetryReporter.ts26 + / 13 −
@@ -24,16 +24,18 @@ import { AgentHostClientConnectionKind, AgentHostLaunchKind, AgentHostTransportK export type AgentHostUserMessageSentSource = 'direct' | 'queued'; /**- * Who produced the message that started a turn. Extends the protocol's- * {@link MessageKind} with `agentMerge`: Agent Merge drives its repair turns- * with a host-generated message that carries the `systemNotification` origin,- * and reporting those under their own value keeps automated merge work- * separable from turns a person or an agent asked for.+ * The message origin used for telemetry. Extends the protocol's+ * {@link MessageKind} with host-owned classifications for Agent Merge repair+ * turns and VS Code sessions marked ephemeral, which telemetry reports as+ * `inline`. */-export type AgentHostMessageOriginTelemetryKind = MessageKind | 'agentMerge';+export type AgentHostMessageOriginTelemetryKind = MessageKind | 'agentMerge' | 'inline'; -/** Classifies the actor that produced a turn's message for telemetry. */-export function getMessageOriginTelemetryKind(message: Message): AgentHostMessageOriginTelemetryKind {+/** Classifies a turn's message origin, including host-owned session classifications. */+export function getMessageOriginTelemetryKind(message: Message, isEphemeralSession: boolean): AgentHostMessageOriginTelemetryKind {+ if (isEphemeralSession) {+ return 'inline';+ } // The marker only counts on the origin the host stamps it with, so a client // cannot dress a user message up as automated merge work. if (message.origin.kind === MessageKind.SystemNotification && isAgentMergeMessage(message)) {@@ -123,7 +125,7 @@ export type IAgentHostUserMessageSentClassification = IAgentHostCopilotSkuClassi 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 message was sent directly or from the queued-message flow.' };- messageOriginKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of actor that produced the message: a user, an agent (session orchestration tools such as create_session/send_message), Agent Merge, a tool, an automation, or a system notification.' };+ messageOriginKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The message origin: a user, an agent (session orchestration tools such as create_session/send_message), Agent Merge, an inline session (derived from the VS Code ephemeral-session marker), a tool, an automation, or a system notification.' }; 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.' };@@ -249,7 +251,7 @@ export type IAgentHostTurnCompletedClassification = IAgentHostEventClassificatio 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.' };- messageOriginKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of actor that started the turn: a user, an agent (session orchestration tools such as create_session/create_chat/send_message), Agent Merge, a tool, an automation, or a system notification.' };+ messageOriginKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The message origin that started the turn: a user, an agent (session orchestration tools such as create_session/create_chat/send_message), Agent Merge, an inline session (derived from the VS Code ephemeral-session marker), a tool, an automation, or a system notification.' }; 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.' };@@ -270,6 +272,7 @@ export interface IAgentHostTurnFailedEvent extends IAgentHostEventTelemetry { chatSessionId: string; isSubagentSession: boolean; turnId: string;+ messageOriginKind: AgentHostMessageOriginTelemetryKind | undefined; failureStage: AgentHostTurnFailureStage; errorType: string; errorName: string | undefined;@@ -286,6 +289,7 @@ export type IAgentHostTurnFailedClassification = IAgentHostEventClassification & 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.' };+ messageOriginKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The kind of actor or host-owned session classification that started the failed turn.' }; 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.' };@@ -406,6 +410,7 @@ export interface IAgentHostTurnHungEvent extends IAgentHostEventTelemetry { chatSessionId: string; isSubagentSession: boolean; turnId: string;+ messageOriginKind: AgentHostMessageOriginTelemetryKind | undefined; hangReason: AgentHostTurnHangReason; isExpected: boolean; hadAnyProgress: boolean;@@ -433,6 +438,7 @@ export type IAgentHostTurnHungClassification = IAgentHostEventClassification & { 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.' };+ messageOriginKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The kind of actor or host-owned session classification that started the hung turn.' }; 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.' };@@ -460,6 +466,7 @@ export interface IAgentHostTurnHungReport extends IAgentHostTurnAttributedReport provider: string; session: string; turnId: string;+ messageOriginKind: AgentHostMessageOriginTelemetryKind | undefined; hangReason: AgentHostTurnHangReason; hadAnyProgress: boolean; lastActivityKind: string;@@ -485,6 +492,7 @@ export interface IAgentHostHungTurnCompletedEvent extends IAgentHostEventTelemet chatSessionId: string; isSubagentSession: boolean; turnId: string;+ messageOriginKind: AgentHostMessageOriginTelemetryKind | undefined; hangReason: AgentHostTurnHangReason; result: AgentHostTurnResult; hangReportCount: number;@@ -498,6 +506,7 @@ export type IAgentHostHungTurnCompletedClassification = IAgentHostEventClassific 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.' };+ messageOriginKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The kind of actor or host-owned session classification that started the recovered turn.' }; 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.' };@@ -511,6 +520,7 @@ export interface IAgentHostHungTurnCompletedReport extends IAgentHostTurnAttribu provider: string; session: string; turnId: string;+ messageOriginKind: AgentHostMessageOriginTelemetryKind | undefined; hangReason: AgentHostTurnHangReason; result: AgentHostTurnResult; hangReportCount: number;@@ -886,7 +896,7 @@ export class AgentHostTelemetryReporter { }); } - userMessageSent(provider: string, clientId: string | undefined, clientContext: IAgentHostClientTelemetryContext, session: string, turnId: string, sessionState: ISessionWithDefaultChat | undefined, source: AgentHostUserMessageSentSource, message: Message): void {+ userMessageSent(provider: string, clientId: string | undefined, clientContext: IAgentHostClientTelemetryContext, session: string, turnId: string, sessionState: ISessionWithDefaultChat | undefined, source: AgentHostUserMessageSentSource, message: Message, isEphemeralSession: boolean): void { const attachmentCount = message.attachments?.length ?? 0; const activeClients = sessionState?.activeClients ?? []; const sessionUri = isAhpChatChannel(session) ? parseRequiredSessionUriFromChatUri(session) : session;@@ -901,7 +911,7 @@ export class AgentHostTelemetryReporter { ...(clientContext.devDeviceId ? { initiatorDevDeviceId: clientContext.devDeviceId } : {}), agentSessionId: AgentSession.id(sessionUri), source,- messageOriginKind: getMessageOriginTelemetryKind(message),+ messageOriginKind: getMessageOriginTelemetryKind(message, isEphemeralSession), isSubagentSession: isSubagentSession(sessionUri), turnCount: sessionState?.turns.length ?? 0, ...(activeClients.length > 0 ? {@@ -916,7 +926,7 @@ export class AgentHostTelemetryReporter { initiatorClientType: clientContext.clientType, conversationId: AgentSession.id(sessionUri), turnId,- messageOriginKind: getMessageOriginTelemetryKind(message),+ messageOriginKind: getMessageOriginTelemetryKind(message, isEphemeralSession), }); } @@ -1270,6 +1280,7 @@ export class AgentHostTelemetryReporter { chatSessionId, isSubagentSession: isSubagent, turnId: report.turnId,+ messageOriginKind: report.messageOriginKind, failureStage: report.failure.stage, errorType: report.failure.error.errorType, errorName: report.failure.errorName,@@ -1296,6 +1307,7 @@ export class AgentHostTelemetryReporter { chatSessionId: getTelemetryChatSessionId(report.session), isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session), turnId: report.turnId,+ messageOriginKind: report.messageOriginKind, hangReason: report.hangReason, isExpected: report.hangReason === 'waitingOnUser' || report.hangReason === 'runningTool', hadAnyProgress: report.hadAnyProgress,@@ -1330,6 +1342,7 @@ export class AgentHostTelemetryReporter { chatSessionId: getTelemetryChatSessionId(report.session), isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session), turnId: report.turnId,+ messageOriginKind: report.messageOriginKind, hangReason: report.hangReason, result: report.result, hangReportCount: report.hangReportCount,src/vs/platform/agentHost/node/agentHostTurnStarter.ts4 + / 2 −
@@ -116,8 +116,10 @@ export function startTurn(accessor: ServicesAccessor, request: ITurnStartRequest return undefined; } - telemetryReporter.userMessageSent(agent.id, request.clientId, request.clientContext, request.chat, request.turnId, state, request.source, request.message);+ const isEphemeralSession = stateManager.isEphemeralSession(request.session);+ const messageOriginKind = getMessageOriginTelemetryKind(request.message, isEphemeralSession);+ telemetryReporter.userMessageSent(agent.id, request.clientId, request.clientContext, request.chat, request.turnId, state, request.source, request.message, isEphemeralSession); const { model, modelTelemetryKind, modelSelectionKind, permissionLevel, interactionMode } = getTurnTelemetryContext(agent, request.chat, createAgentChatContext(stateManager, request.session, request.chat), state, request.message.model?.id);- turnTracker.turnStarted(agent, request.chat, request.turnId, model, modelTelemetryKind, modelSelectionKind, permissionLevel, interactionMode, request.clientContext, request.clientId, undefined, undefined, getMessageOriginTelemetryKind(request.message));+ turnTracker.turnStarted(agent, request.chat, request.turnId, model, modelTelemetryKind, modelSelectionKind, permissionLevel, interactionMode, request.clientContext, request.clientId, undefined, undefined, messageOriginKind); return { agent }; }src/vs/platform/agentHost/node/agentHostTurnTracker.ts6 + / 0 −
@@ -370,6 +370,10 @@ export class AgentHostTurnTracker extends Disposable { return this._turnTimings.get(this._key(session, turnId))?.clientContext; } + getMessageOriginKind(session: string, turnId: string): AgentHostMessageOriginTelemetryKind | undefined {+ return this._turnTimings.get(this._key(session, turnId))?.messageOriginKind;+ }+ getInitiatorClientId(session: string, turnId: string): string | undefined { return this._turnTimings.get(this._key(session, turnId))?.initiatorClientId; }@@ -424,6 +428,7 @@ export class AgentHostTurnTracker extends Disposable { provider: timing.agent.id, session: timing.session, turnId,+ messageOriginKind: timing.messageOriginKind, hangReason: timing.lastHangReason, result, hangReportCount: timing.hangReportCount,@@ -505,6 +510,7 @@ export class AgentHostTurnTracker extends Disposable { provider: timing.agent.id, session: timing.session, turnId: timing.turnId,+ messageOriginKind: timing.messageOriginKind, hangReason, hadAnyProgress: timing.lastActivityKind !== TURN_ACTIVITY_NONE, lastActivityKind: timing.lastActivityKind,src/vs/platform/agentHost/node/agentSideEffects.ts12 + / 6 −
@@ -72,7 +72,7 @@ import { IAgentHostSessionTitleController } from './agentHostSessionTitleControl import { AgentHostStateManager, resolveChatStateForUri } from './agentHostStateManager.js'; import { IAgentConfigurationService } from './agentConfigurationService.js'; import { createAgentChatContext, getSessionChatsForFanOut } from './agentChatContext.js';-import { AgentHostTelemetryReporter, IAgentHostTelemetryReporter, type AgentHostTurnFailureStage, type AgentHostTurnResult, type IAgentHostTurnFailure } from './agentHostTelemetryReporter.js';+import { AgentHostTelemetryReporter, getMessageOriginTelemetryKind, IAgentHostTelemetryReporter, type AgentHostMessageOriginTelemetryKind, type AgentHostTurnFailureStage, type AgentHostTurnResult, type IAgentHostTurnFailure } from './agentHostTelemetryReporter.js'; import { AgentHostToolCallTracker, IAgentHostToolCallTracker } from './agentHostToolCallTracker.js'; import { updateAgentHostTelemetryLevelFromConfig } from './agentHostTelemetryService.js'; import { getConfiguredSessionMode, getModelTelemetryContext, getTurnTelemetryContext } from './agentHostTurnTelemetryContext.js';@@ -131,6 +131,7 @@ interface ISubagentSessionRef { interface ISubagentParentTurnTelemetryContext { readonly parentTurnId: string | undefined; readonly parentClientContext: IAgentHostClientTelemetryContext | undefined;+ readonly messageOriginKind: AgentHostMessageOriginTelemetryKind; /** Hierarchy edge; set only when the immediate parent chat has an active turn, else omitted. */ readonly correlatedParentTurnId: string | undefined; readonly initiatorClientId: string | undefined;@@ -987,7 +988,7 @@ export class AgentSideEffects extends Disposable { // supplied by the provider on the `subagent_started` signal. const turnId = generateUuid(); const parentTurnId = this._stateManager.getActiveTurnId(contentChatUri);- const { parentClientContext, correlatedParentTurnId, initiatorClientId } = this._getSubagentParentTurnTelemetryContext(immediateParentChatUri, contentChatUri);+ const { parentClientContext, correlatedParentTurnId, initiatorClientId, messageOriginKind } = this._getSubagentParentTurnTelemetryContext(immediateParentChatUri, contentChatUri); this._stateManager.dispatchServerAction(subagentChatUri, { type: ActionType.ChatTurnStarted, turnId,@@ -997,7 +998,7 @@ export class AgentSideEffects extends Disposable { const agent = this._options.getAgent(parentSessionUri); if (agent) { const interactionMode = getConfiguredSessionMode(this._stateManager.getSessionState(parentSessionUri)?.config);- this._turnTracker.turnStarted(agent, subagentChatUri, turnId, undefined, undefined, 'default', undefined, interactionMode, parentClientContext, initiatorClientId, correlatedParentTurnId, toolCallId, MessageKind.Tool);+ this._turnTracker.turnStarted(agent, subagentChatUri, turnId, undefined, undefined, 'default', undefined, interactionMode, parentClientContext, initiatorClientId, correlatedParentTurnId, toolCallId, messageOriginKind); this._turnTracker.setCurrentStage(subagentChatUri, turnId, 'provider'); } @@ -1062,7 +1063,7 @@ export class AgentSideEffects extends Disposable { const turnId = generateUuid(); const correlatedParentChatUri = immediateParentChatURI ?? subagent.immediateParentChatUri; const parentChatUri = correlatedParentChatUri ?? parentChatURI;- const { parentClientContext, correlatedParentTurnId, initiatorClientId } = this._getSubagentParentTurnTelemetryContext(correlatedParentChatUri, parentChatUri);+ const { parentClientContext, correlatedParentTurnId, initiatorClientId, messageOriginKind } = this._getSubagentParentTurnTelemetryContext(correlatedParentChatUri, parentChatUri); this._logService.info(`[AgentSideEffects] Resuming subagent turn: ${subagent.chatUri} (parent=${parentChatURI}, toolCallId=${toolCallId})`); this._stateManager.dispatchServerAction(subagent.chatUri, { type: ActionType.ChatTurnStarted,@@ -1073,7 +1074,7 @@ export class AgentSideEffects extends Disposable { const agent = this._options.getAgent(subagent.sessionUri); if (agent) { const interactionMode = getConfiguredSessionMode(this._stateManager.getSessionState(subagent.sessionUri)?.config);- this._turnTracker.turnStarted(agent, subagent.chatUri, turnId, undefined, undefined, 'default', undefined, interactionMode, parentClientContext, initiatorClientId, correlatedParentTurnId, toolCallId, MessageKind.Tool);+ this._turnTracker.turnStarted(agent, subagent.chatUri, turnId, undefined, undefined, 'default', undefined, interactionMode, parentClientContext, initiatorClientId, correlatedParentTurnId, toolCallId, messageOriginKind); this._turnTracker.setCurrentStage(subagent.chatUri, turnId, 'provider'); } this._subagentChats.set({ ...subagent, immediateParentChatUri: correlatedParentChatUri, turnStopWatch: StopWatch.create(false) }, parentChatURI, toolCallId);@@ -1082,9 +1083,14 @@ export class AgentSideEffects extends Disposable { private _getSubagentParentTurnTelemetryContext(immediateParentChatUri: ProtocolURI | undefined, fallbackParentChatUri: ProtocolURI): ISubagentParentTurnTelemetryContext { const parentChatUri = immediateParentChatUri ?? fallbackParentChatUri; const parentTurnId = this._stateManager.getActiveTurnId(parentChatUri);+ const parentSessionUri = parseRequiredSessionUriFromChatUri(parentChatUri);+ const parentMessageOriginKind = parentTurnId ? this._turnTracker.getMessageOriginKind(parentChatUri, parentTurnId) : undefined; return { parentTurnId, parentClientContext: parentTurnId ? this._turnTracker.getClientTelemetryContext(parentChatUri, parentTurnId) : undefined,+ messageOriginKind: parentMessageOriginKind === 'inline' || (!parentMessageOriginKind && this._stateManager.isEphemeralSession(parentSessionUri))+ ? 'inline'+ : MessageKind.Tool, correlatedParentTurnId: immediateParentChatUri ? parentTurnId : undefined, initiatorClientId: parentTurnId ? this._turnTracker.getInitiatorClientId(parentChatUri, parentTurnId) : undefined, };@@ -1382,7 +1388,7 @@ export class AgentSideEffects extends Disposable { } const state = this._stateManager.getSessionState(channel); const { model, modelTelemetryKind, modelSelectionKind, permissionLevel, interactionMode } = getTurnTelemetryContext(agent, channel, this._chatContext(sessionChannel, channel), state, resumedTurn.message.model?.id);- this._turnTracker.turnStarted(agent, channel, action.turnId, model, modelTelemetryKind, modelSelectionKind, permissionLevel, interactionMode, clientContext, clientId);+ this._turnTracker.turnStarted(agent, channel, action.turnId, model, modelTelemetryKind, modelSelectionKind, permissionLevel, interactionMode, clientContext, clientId, undefined, undefined, getMessageOriginTelemetryKind(resumedTurn.message, this._stateManager.isEphemeralSession(sessionChannel))); this._turnTracker.setCurrentStage(channel, action.turnId, 'provider'); const key = this._resumedTurnExecutionKey(channel, action.turnId); const execution: IResumedTurnExecution = {src/vs/platform/agentHost/test/node/agentHostTelemetryReporter.test.ts14 + / 9 −
@@ -84,7 +84,7 @@ suite('AgentHostTelemetryReporter', () => { const reporter = new AgentHostTelemetryReporter(service); const chat = buildSubagentChatUri(session, 'tool-call-1'); - reporter.userMessageSent('copilot', 'client-1', createUnknownAgentHostClientTelemetryContext(AgentHostClientType.AgentsWindow), chat, 'turn-1', undefined, 'direct', userMessage);+ reporter.userMessageSent('copilot', 'client-1', createUnknownAgentHostClientTelemetryContext(AgentHostClientType.AgentsWindow), chat, 'turn-1', undefined, 'direct', userMessage, false); assert.deepStrictEqual(service.githubStandardEvents, [{ eventName: 'agentHost.userMessageSent',@@ -106,8 +106,8 @@ suite('AgentHostTelemetryReporter', () => { ...createUnknownAgentHostClientTelemetryContext(AgentHostClientType.AgentsWindow), machineId: 'client-machine-id', devDeviceId: 'client-dev-device-id',- }, session, 'turn-1', undefined, 'direct', userMessage);- reporter.userMessageSent('copilot', 'client-2', createUnknownAgentHostClientTelemetryContext(AgentHostClientType.EditorWindow), session, 'turn-2', undefined, 'direct', userMessage);+ }, session, 'turn-1', undefined, 'direct', userMessage, false);+ reporter.userMessageSent('copilot', 'client-2', createUnknownAgentHostClientTelemetryContext(AgentHostClientType.EditorWindow), session, 'turn-2', undefined, 'direct', userMessage, false); assert.deepStrictEqual(service.standardEvents.map(event => ({ initiatorMachineId: event.data?.initiatorMachineId,@@ -128,17 +128,18 @@ suite('AgentHostTelemetryReporter', () => { const agentMergeMessage: Message = { text: 'fix the failing checks', origin: { kind: MessageKind.SystemNotification }, _meta: toAgentMergeMessageMeta() }; const spoofedMergeMessage: Message = { text: 'hello', origin: { kind: MessageKind.User }, _meta: toAgentMergeMessageMeta() }; - reporter.userMessageSent('copilot', 'client-1', createUnknownAgentHostClientTelemetryContext(AgentHostClientType.AgentsWindow), session, 'turn-1', undefined, 'direct', agentMessage);- reporter.userMessageSent('copilot', undefined, createUnknownAgentHostClientTelemetryContext(AgentHostClientType.Unknown), session, 'turn-2', undefined, 'direct', agentMergeMessage);- reporter.userMessageSent('copilot', 'client-1', createUnknownAgentHostClientTelemetryContext(AgentHostClientType.AgentsWindow), session, 'turn-3', undefined, 'queued', userMessage);- reporter.userMessageSent('copilot', 'client-1', createUnknownAgentHostClientTelemetryContext(AgentHostClientType.AgentsWindow), session, 'turn-4', undefined, 'direct', spoofedMergeMessage);+ reporter.userMessageSent('copilot', 'client-1', createUnknownAgentHostClientTelemetryContext(AgentHostClientType.AgentsWindow), session, 'turn-1', undefined, 'direct', agentMessage, false);+ reporter.userMessageSent('copilot', undefined, createUnknownAgentHostClientTelemetryContext(AgentHostClientType.Unknown), session, 'turn-2', undefined, 'direct', agentMergeMessage, false);+ reporter.userMessageSent('copilot', 'client-1', createUnknownAgentHostClientTelemetryContext(AgentHostClientType.AgentsWindow), session, 'turn-3', undefined, 'queued', userMessage, false);+ reporter.userMessageSent('copilot', 'client-1', createUnknownAgentHostClientTelemetryContext(AgentHostClientType.AgentsWindow), session, 'turn-4', undefined, 'direct', spoofedMergeMessage, false);+ reporter.userMessageSent('copilot', 'client-1', createUnknownAgentHostClientTelemetryContext(AgentHostClientType.AgentsWindow), session, 'turn-5', undefined, 'direct', userMessage, true); assert.deepStrictEqual({ standard: service.standardEvents.map(event => event.data?.messageOriginKind), github: service.githubStandardEvents.map(event => event.properties?.messageOriginKind), }, {- standard: ['agent', 'agentMerge', 'user', 'user'],- github: ['agent', 'agentMerge', 'user', 'user'],+ standard: ['agent', 'agentMerge', 'user', 'user', 'inline'],+ github: ['agent', 'agentMerge', 'user', 'user', 'inline'], }); }); @@ -425,6 +426,7 @@ suite('AgentHostTelemetryReporter', () => { provider: 'copilot', session, turnId: 'turn-1',+ messageOriginKind: undefined, hangReason: 'stalledAfterProgress', hadAnyProgress: true, lastActivityKind: ActionType.ChatToolCallDelta,@@ -452,6 +454,7 @@ suite('AgentHostTelemetryReporter', () => { provider: 'copilot', session, turnId: 'turn-2',+ messageOriginKind: undefined, hangReason: 'stalledAfterProgress', hadAnyProgress: true, lastActivityKind: 'custom/path/value',@@ -479,6 +482,7 @@ suite('AgentHostTelemetryReporter', () => { chatSessionId: getTelemetryChatSessionId(session), isSubagentSession: false, turnId: 'turn-1',+ messageOriginKind: undefined, hangReason: 'stalledAfterProgress', isExpected: false, hadAnyProgress: true,@@ -507,6 +511,7 @@ suite('AgentHostTelemetryReporter', () => { chatSessionId: getTelemetryChatSessionId(session), isSubagentSession: false, turnId: 'turn-2',+ messageOriginKind: undefined, hangReason: 'stalledAfterProgress', isExpected: false, hadAnyProgress: true,src/vs/platform/agentHost/test/node/agentHostTurnHangTelemetry.test.ts8 + / 3 −
@@ -19,6 +19,7 @@ import { getTelemetryChatSessionId } from '../../common/agentTelemetryCorrelatio import { AgentSession, IAgent } from '../../common/agent.js'; import { AgentHostClientType } from '../../common/agentHostClientInfo.js'; import { createUnknownAgentHostClientTelemetryContext } from '../../common/agentHostTelemetry.js';+import { withEphemeralSessionMeta } from '../../common/meta/agentEphemeralSessionMeta.js'; import { SessionInputRequestKind } from '../../common/state/protocol/state.js'; import { ActionType, type ChatAction } from '../../common/state/sessionActions.js'; import { buildDefaultChatUri, buildSubagentChatUri, ChatInputQuestionKind, MessageKind, ResponsePartKind, SessionStatus, ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributorKind } from '../../common/state/sessionState.js';@@ -115,14 +116,15 @@ suite('AgentSideEffects — turn hang telemetry', () => { const sessionKey = sessionUri.toString(); const defaultChatUri = buildDefaultChatUri(sessionUri); - function setupSession(): void {+ function setupSession(isEphemeral = false): void { stateManager.createSession({ resource: sessionKey, provider: 'mock', title: 'Test', status: SessionStatus.Idle, createdAt: new Date().toISOString(), modifiedAt: new Date().toISOString(),+ ...(isEphemeral ? { _meta: withEphemeralSessionMeta(undefined, true) } : {}), }); stateManager.dispatchServerAction(sessionKey, { type: ActionType.SessionReady }); }@@ -257,7 +259,7 @@ suite('AgentSideEffects — turn hang telemetry', () => { test('reports noProgress for a turn that starts and is never heard from again', async () => { await runWithFakedTimers({}, async () => {- setupSession();+ setupSession(true); startTurn('turn-lost'); await timeout(TURN_HANG_THRESHOLD_MS); });@@ -270,6 +272,7 @@ suite('AgentSideEffects — turn hang telemetry', () => { chatSessionId: getTelemetryChatSessionId(defaultChatUri), isSubagentSession: false, turnId: 'turn-lost',+ messageOriginKind: 'inline', hangReason: 'noProgress', isExpected: false, hadAnyProgress: false,@@ -418,7 +421,7 @@ suite('AgentSideEffects — turn hang telemetry', () => { test('reports the paired recovery event when a hung turn later completes', async () => { await runWithFakedTimers({}, async () => {- setupSession();+ setupSession(true); startTurn('turn-recovered'); await timeout(TURN_HANG_THRESHOLD_MS); fire({ type: ActionType.ChatTurnComplete, turnId: 'turn-recovered', duration: 1000 });@@ -432,6 +435,7 @@ suite('AgentSideEffects — turn hang telemetry', () => { chatSessionId: getTelemetryChatSessionId(defaultChatUri), isSubagentSession: false, turnId: 'turn-recovered',+ messageOriginKind: 'inline', hangReason: 'noProgress', result: 'success', hangReportCount: 1,@@ -540,6 +544,7 @@ suite('AgentSideEffects — turn hang telemetry', () => { chatSessionId: getTelemetryChatSessionId(session), isSubagentSession: false, turnId: 'turn',+ messageOriginKind: undefined, hangReason: 'noProgress', isExpected: false, hadAnyProgress: false,src/vs/platform/agentHost/test/node/agentHostTurnTelemetry.test.ts12 + / 3 −
@@ -22,6 +22,7 @@ import { AgentHostClientConnectionKind, AgentHostLaunchKind, AgentHostTransportK import type { SessionMode } from '../../common/agentHostSchema.js'; import { SessionConfigKey } from '../../common/sessionConfigKeys.js'; import { ActionType, type ChatAction, type ChatUsageAction } from '../../common/state/sessionActions.js';+import { withEphemeralSessionMeta } from '../../common/meta/agentEphemeralSessionMeta.js'; import { toAgentMergeMessageMeta } from '../../common/meta/agentMergeMessageMeta.js'; import { buildDefaultChatUri, buildSubagentChatUri, createErrorResponsePart, type Message, MessageKind, PendingMessageKind, ResponsePartKind, SessionStatus } from '../../common/state/sessionState.js'; import { IAgentHostCheckpointService, NULL_CHECKPOINT_SERVICE } from '../../common/agentHostCheckpointService.js';@@ -118,7 +119,7 @@ suite('AgentSideEffects — turn tracker telemetry', () => { const sessionKey = sessionUri.toString(); const defaultChatUri = buildDefaultChatUri(sessionUri); - function setupSession(ready = true, workingDirectories?: string[]): void {+ function setupSession(ready = true, workingDirectories?: string[], isEphemeral = false): void { stateManager.createSession({ resource: sessionKey, provider: 'mock',@@ -127,6 +128,7 @@ suite('AgentSideEffects — turn tracker telemetry', () => { createdAt: new Date().toISOString(), modifiedAt: new Date().toISOString(), ...(workingDirectories ? { workingDirectories } : {}),+ ...(isEphemeral ? { _meta: withEphemeralSessionMeta(undefined, true) } : {}), }); if (ready) { stateManager.dispatchServerAction(sessionKey, { type: ActionType.SessionReady });@@ -267,7 +269,7 @@ suite('AgentSideEffects — turn tracker telemetry', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('emits turnCompleted with timing and turn-start context on success', () => {- setupSession();+ setupSession(true, undefined, true); agent.setModels([{ provider: 'mock', id: 'gpt-5.5', name: 'GPT 5.5', supportsVision: false }]); setSessionConfig({ autoApprove: 'autopilot', mode: 'interactive' }); startTurn('turn-1', 'hello', 'gpt-5.5');@@ -290,10 +292,12 @@ suite('AgentSideEffects — turn tracker telemetry', () => { assert.strictEqual(data.isSubagentSession, false); assert.strictEqual(data.isBYOK, false); assert.strictEqual(data.interactionMode, 'interactive');+ assert.strictEqual(data.messageOriginKind, 'inline'); assert.strictEqual(typeof data.totalTime, 'number'); assert.strictEqual(typeof data.timeToFirstProgress, 'number'); assert.strictEqual(data.isMultiRoot, false); assert.strictEqual(data.folderCount, 0);+ assert.strictEqual((telemetry.events.find(event => event.eventName === 'agentHost.userMessageSent')?.data as Record<string, unknown>).messageOriginKind, 'inline'); }); test('attributes completed and failed turns to the initiating client identity', () => {@@ -317,6 +321,7 @@ suite('AgentSideEffects — turn tracker telemetry', () => { initiatorConnectionKind: data.initiatorConnectionKind, initiatorTransportKind: data.initiatorTransportKind, hostLaunchKind: data.hostLaunchKind,+ messageOriginKind: data.messageOriginKind, initiatorMachineId: data.initiatorMachineId, initiatorDevDeviceId: data.initiatorDevDeviceId, };@@ -326,6 +331,7 @@ suite('AgentSideEffects — turn tracker telemetry', () => { initiatorConnectionKind: 'remote_extension_host', initiatorTransportKind: 'message_port', hostLaunchKind: 'vscode_main_process',+ messageOriginKind: 'user', initiatorMachineId: 'client-machine-id', initiatorDevDeviceId: 'client-dev-device-id', }, {@@ -334,6 +340,7 @@ suite('AgentSideEffects — turn tracker telemetry', () => { initiatorConnectionKind: 'remote_extension_host', initiatorTransportKind: 'message_port', hostLaunchKind: 'vscode_main_process',+ messageOriginKind: 'user', initiatorMachineId: 'client-machine-id', initiatorDevDeviceId: 'client-dev-device-id', }]);@@ -1009,7 +1016,7 @@ suite('AgentSideEffects — turn tracker telemetry', () => { }); test('emits result=error when a queued sendMessage rejects', async () => {- setupSession();+ setupSession(true, undefined, true); agent.sendMessage = async () => { throw new Error('boom'); }; const setAction: ChatAction = {@@ -1026,6 +1033,8 @@ suite('AgentSideEffects — turn tracker telemetry', () => { const events = completedEvents(); assert.strictEqual(events.length, 1); assert.strictEqual((events[0].data as Record<string, unknown>).result, 'error');+ assert.strictEqual((events[0].data as Record<string, unknown>).messageOriginKind, 'inline');+ assert.strictEqual((telemetry.events.find(event => event.eventName === 'agentHost.userMessageSent')?.data as Record<string, unknown>).messageOriginKind, 'inline'); }); test('captures interactionMode for queued turns', () => {