microsoft/vscode · #334378

agentHost: classify inline chat telemetry

connor4312 · merged Sep 3, 20267 files · 82 + / 36
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', () => {