Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/vs/platform/agentHost/OTEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ the deadline remain missing; the marker is not proof that a turn completed.
| Host `timeToFirstProgress`, `timeToFirstSubstantiveProgress` | Turn start to existing first visible/substantive progress boundaries; absent if not observed |
| Host `sendStageWorkingDirectoryMs`, `sendStageModelSelectionMs`, `sendStageAttachmentsMs`, `sendStageContributionsMs` | Elapsed time in each existing pre-send stage that ran; an interrupted open stage retains its partial duration |
| Host `sendStageCheckpointMs` | **Residual critical-path wait** for the checkpoint after overlap with earlier preparation, not the entire checkpoint operation |
| Host `sendStageProviderPreparationMs` | **Residual critical-path wait** for provider turn preparation (`IAgentChats.prepareTurn`, enabled by `chat.agentHost.experimental.overlapProviderPreparation`) after it overlapped the earlier pre-send stages and the checkpoint capture; absent when preparation did not run. Provider stage marks made during preparation precede dispatch and are not reported as `providerStage*Ms` |
| Host `providerStageQueueMs`, `providerStageClientMs`, `providerStageSnapshotMs`, `providerStageConfigMs`, `providerStageCreateMs`, `providerStageFinalizeMs`, `providerStagePersistMs`, `providerStageRefreshMs`, `providerStageTurnPrepareMs`, `providerStageModelResponseMs` | Sequential provider-marked stages between provider dispatch and first progress (chat queue wait, SDK client acquisition, customization snapshot, session config, SDK create/resume, post-create setup, session registration/persistence, live-session refresh, per-turn preparation, and SDK send until first progress). Only stages the provider ran are present; a turn ending before first progress retains its partial open stage. Copilot marks all of them; other providers currently mark none |
| Host `hostRootTurnOrdinal`, `hostProcessAgeMs`, `titleGenerationStrategy` | Existing root ordinal and process age captured at turn start, and effective `activeAgent`, `utility`, or `deferred` strategy when observed |
| Renderer `requestId` | Exact client request ID, duplicated as `turnId` for joins |
Expand Down
10 changes: 10 additions & 0 deletions src/vs/platform/agentHost/common/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,16 @@ export interface IAgentPrepareChatResult {
* the provider needs the owning session or storage scope.
*/
export interface IAgentChats {
/**
* Optional pre-send preparation, such as materializing a deferred provider
* session, after the turn's model/agent selection and working directories
* are resolved. The host may run it concurrently with the turn-start
* checkpoint capture and awaits both before {@link sendMessage} for the same
* `turnId`, so implementations must not send a prompt or modify the working
* tree. Preparation that is not needed must resolve without doing work.
*/
prepareTurn?(chat: URI, turnId: string, workingDirectories: readonly URI[] | undefined, context: AgentChatOperationContext): Promise<void>;

/** Prepare an existing chat for input without sending a turn. May acquire its native writer lock. */
prepareChat?(chat: URI, context: AgentChatOperationContext): Promise<IAgentPrepareChatResult>;

Expand Down
14 changes: 14 additions & 0 deletions src/vs/platform/agentHost/common/agentHostSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,14 @@ export const AgentHostArtifactToolsConfigKey = 'artifactTools';
/** Root config key controlling automatic pull request association for the checked-out branch. */
export const AgentHostAutoAttachPullRequestsConfigKey = 'autoAttachPullRequests';

/**
* Root config key forwarded from the renderer's
* `chat.agentHost.experimental.overlapProviderPreparation` setting. When
* `true`, a provider that implements `IAgentChats.prepareTurn` prepares its
* session while the turn-start checkpoint is captured, instead of after it.
*/
export const AgentHostOverlapProviderPreparationConfigKey = 'overlapProviderPreparation';

// Root config key forwarded from the renderer when the `chat.agentSessions.migrateLegacyCopilotCli`
// setting changes. When `true`, `listSessions` surfaces un-adopted extension-host Copilot CLI
// sessions as adoptable agent-host sessions, and opening one adopts it in place. Experimental; off.
Expand Down Expand Up @@ -929,6 +937,12 @@ export const platformRootSchema = createSchema({
description: localize('agentHost.config.autoAttachPullRequests.description', "Whether the Agent Host automatically discovers and associates a pull request for the currently checked-out branch. When disabled, only pull requests recorded as artifacts or explicitly associated by session actions are considered."),
default: true,
}),
[AgentHostOverlapProviderPreparationConfigKey]: schemaProperty<boolean>({
type: 'boolean',
title: localize('agentHost.config.overlapProviderPreparation.title', "Overlap Provider Preparation"),
description: localize('agentHost.config.overlapProviderPreparation.description', "Whether agents prepare their session for a turn while the turn-start checkpoint is captured, instead of after it."),
default: false,
}),
[AgentHostMigrateLegacyCopilotCliEnabledConfigKey]: schemaProperty<boolean>({
type: 'boolean',
title: localize('agentHost.config.migrateLegacyCopilotCliEnabled.title', "Migrate Legacy Copilot CLI Sessions"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
AgentHostCodexAgentCodexHomeSettingId,
AgentHostCopilotMultiRootEnabledSettingId,
AgentHostMarkdownPlanRichLinksEnabledSettingId,
AgentHostOverlapProviderPreparationSettingId,
AgentHostOTelCaptureContentSettingId,
AgentHostOTelCaptureIdentitySettingId,
AgentHostOTelDbSpanExporterEnabledSettingId,
Expand All @@ -49,6 +50,7 @@ import {
AgentHostGitHubMcpServerEnabledConfigKey,
AgentHostMcpToolRoutingEnabledConfigKey,
AgentHostMarkdownPlanRichLinksEnabledConfigKey,
AgentHostOverlapProviderPreparationConfigKey,
AgentHostSystemProxyEnabledConfigKey,
AgentHostWorkspaceSnapshotEnabledConfigKey,
} from './agentHostSchema.js';
Expand Down Expand Up @@ -206,6 +208,15 @@ configurationRegistry.registerConfiguration({
experiment: { mode: 'auto' },
agentHost: { key: AgentHostMarkdownPlanRichLinksEnabledConfigKey },
},
[AgentHostOverlapProviderPreparationSettingId]: {
type: 'boolean',
description: nls.localize('chat.agentHost.experimental.overlapProviderPreparation', "When enabled, agents prepare their session for a new turn, such as starting the runtime session, while the turn-start checkpoint is captured instead of after it. This can shorten the time to the first response, especially on the first turn of a session."),
default: false,
scope: ConfigurationScope.APPLICATION,
tags: ['experimental', 'advanced'],
experiment: { mode: 'auto' },
agentHost: { key: AgentHostOverlapProviderPreparationConfigKey },
},
[AgentHostWorkspaceSnapshotEnabledSettingId]: {
type: 'boolean',
description: nls.localize('chat.experimental.workspaceSnapshot', "When enabled, the first turn of a new Copilot agent host chat includes a bounded file-name snapshot of its working directories, so the agent can orient itself without listing the workspace first. File contents are never included."),
Expand Down
7 changes: 7 additions & 0 deletions src/vs/platform/agentHost/common/agentHostTelemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ export type AgentHostTurnSendStage =
| 'attachments'
/** Running the outgoing-turn chat contributions. */
| 'contributions'
/**
* Waiting for the provider's turn preparation (`IAgentChats.prepareTurn`).
* Preparation is started earlier and runs alongside the stages above and
* the checkpoint capture, so this measures only the time it still costs
* the critical path — not the preparation's total cost.
*/
| 'providerPreparation'
/**
* Waiting for the turn-start checkpoint. The capture is started earlier and
* runs alongside the stages above, so this measures only the time it still
Expand Down
3 changes: 3 additions & 0 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ export const ArtifactToolsSettingId = 'chat.artifactTools.enabled';
/** Configuration key controlling automatic pull request association for the checked-out branch. */
export const AgentHostAutoAttachPullRequestsSettingId = 'chat.agentHost.experimental.autoAttachPullRequests';

/** Configuration key controlling whether providers prepare a turn alongside the turn-start checkpoint. */
export const AgentHostOverlapProviderPreparationSettingId = 'chat.agentHost.experimental.overlapProviderPreparation';

/**
* Configuration key gating multiple-working-directory support for the Copilot
* agent-host provider. When `true`, the Copilot provider advertises the
Expand Down
3 changes: 2 additions & 1 deletion src/vs/platform/agentHost/common/otel/agentHostTiming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface IAgentHostTurnTimingDiagnostic {
sendStageModelSelectionMs?: number;
sendStageAttachmentsMs?: number;
sendStageContributionsMs?: number;
sendStageProviderPreparationMs?: number;
sendStageCheckpointMs?: number;
providerStageQueueMs?: number;
providerStageClientMs?: number;
Expand Down Expand Up @@ -90,7 +91,7 @@ export function agentHostTimingAttributes(diagnostic: IAgentHostTurnTimingDiagno
if (diagnostic.titleGenerationStrategy !== undefined) {
put('titleGenerationStrategy', diagnostic.titleGenerationStrategy);
}
for (const key of ['totalTime', 'timeToProviderDispatch', 'timeToFirstProgress', 'timeToFirstSubstantiveProgress', 'sendStageWorkingDirectoryMs', 'sendStageModelSelectionMs', 'sendStageAttachmentsMs', 'sendStageContributionsMs', 'sendStageCheckpointMs', 'providerStageQueueMs', 'providerStageClientMs', 'providerStageSnapshotMs', 'providerStageConfigMs', 'providerStageCreateMs', 'providerStageFinalizeMs', 'providerStagePersistMs', 'providerStageRefreshMs', 'providerStageTurnPrepareMs', 'providerStageModelResponseMs', 'hostRootTurnOrdinal', 'hostProcessAgeMs'] as const) {
for (const key of ['totalTime', 'timeToProviderDispatch', 'timeToFirstProgress', 'timeToFirstSubstantiveProgress', 'sendStageWorkingDirectoryMs', 'sendStageModelSelectionMs', 'sendStageAttachmentsMs', 'sendStageContributionsMs', 'sendStageProviderPreparationMs', 'sendStageCheckpointMs', 'providerStageQueueMs', 'providerStageClientMs', 'providerStageSnapshotMs', 'providerStageConfigMs', 'providerStageCreateMs', 'providerStageFinalizeMs', 'providerStagePersistMs', 'providerStageRefreshMs', 'providerStageTurnPrepareMs', 'providerStageModelResponseMs', 'hostRootTurnOrdinal', 'hostProcessAgeMs'] as const) {
measurement(key, diagnostic[key]);
}
} else {
Expand Down
4 changes: 4 additions & 0 deletions src/vs/platform/agentHost/node/agentHostTelemetryReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ export interface IAgentHostTurnCompletedEvent extends IAgentHostEventTelemetry,
sendStageModelSelectionMs: number | undefined;
sendStageAttachmentsMs: number | undefined;
sendStageContributionsMs: number | undefined;
sendStageProviderPreparationMs: number | undefined;
sendStageCheckpointMs: number | undefined;
timeToProviderDispatch: number | undefined;
providerStageQueueMs?: number;
Expand Down Expand Up @@ -321,6 +322,7 @@ export type IAgentHostTurnCompletedClassification = IAgentHostEventClassificatio
sendStageModelSelectionMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds the host spent applying the model and agent selection on the provider before dispatching the turn.' };
sendStageAttachmentsMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds the host spent resolving chat attachments before dispatching the turn.' };
sendStageContributionsMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds the host spent running outgoing-turn chat contributions before dispatching the turn.' };
sendStageProviderPreparationMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds the host still waited on provider turn preparation before dispatching the turn, after it overlapped the earlier pre-send stages and the checkpoint capture.' };
sendStageCheckpointMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds the host spent still waiting on the turn-start checkpoint before dispatching the turn, after it overlapped the earlier pre-send stages.' };
timeToProviderDispatch: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds from turn start until the message was handed to the provider, covering all host pre-send stages.' };
providerStageQueueMs?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds after dispatch the provider waited behind earlier operations on the same chat, before first progress.' };
Expand Down Expand Up @@ -1507,6 +1509,7 @@ export class AgentHostTelemetryReporter {
sendStageModelSelectionMs: report.sendStageDurationsMs?.get('modelSelection'),
sendStageAttachmentsMs: report.sendStageDurationsMs?.get('attachments'),
sendStageContributionsMs: report.sendStageDurationsMs?.get('contributions'),
sendStageProviderPreparationMs: report.sendStageDurationsMs?.get('providerPreparation'),
sendStageCheckpointMs: report.sendStageDurationsMs?.get('checkpoint'),
...providerStages,
hostRootTurnOrdinal: report.hostRootTurnOrdinal,
Expand Down Expand Up @@ -1538,6 +1541,7 @@ export class AgentHostTelemetryReporter {
sendStageModelSelectionMs: report.sendStageDurationsMs?.get('modelSelection'),
sendStageAttachmentsMs: report.sendStageDurationsMs?.get('attachments'),
sendStageContributionsMs: report.sendStageDurationsMs?.get('contributions'),
sendStageProviderPreparationMs: report.sendStageDurationsMs?.get('providerPreparation'),
sendStageCheckpointMs: report.sendStageDurationsMs?.get('checkpoint'),
timeToProviderDispatch: report.sendDispatchedMs,
...providerStages,
Expand Down
50 changes: 50 additions & 0 deletions src/vs/platform/agentHost/node/agentSideEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import { isPresentationOnlyToolCall, readToolCallMeta, toToolCallMeta } from '..
import { isAgentMergeMessage } from '../common/meta/agentMergeMessageMeta.js';

import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { logSettingExperimentTrigger } from '../../telemetry/common/experimentTrigger.js';
import { AgentHostOverlapProviderPreparationConfigKey, platformRootSchema } from '../common/agentHostSchema.js';
import { AgentHostOverlapProviderPreparationSettingId } from '../common/agentService.js';
import { CopilotCliVSCodeAssignmentContextKey } from '../common/copilotCliConfig.js';
import { ISessionDataService } from '../common/sessionDataService.js';
import { SessionConfigKey } from '../common/sessionConfigKeys.js';
import { resolveChatAttachment } from '../common/state/chatAttachmentContext.js';
Expand Down Expand Up @@ -245,6 +249,8 @@ export class AgentSideEffects extends Disposable {
private readonly _pendingSessionCustomizationPublishes = new Map<ProtocolURI, Promise<void>>();
private readonly _pendingMcpServerStarts = new NKeyMap<CancellationTokenSource, [ProtocolURI, string]>();
private readonly _pendingCustomizationEnablementRefreshes = new Set<ProtocolURI>();
/** Set while a turn reached the overlap experiment's divergence before the assignment context arrived. */
private _overlapExperimentTriggerPending = false;

/**
* Buffers signals whose `parentToolCallId` references a subagent
Expand Down Expand Up @@ -297,6 +303,16 @@ export class AgentSideEffects extends Disposable {
hostLaunchKind: this._options.hostLaunchKind ?? AgentHostLaunchKind.Unknown,
sendTurnMessage: options => void this._sendTurnMessage(options),
}));
this._register(this._agentConfigService.onDidRootConfigChange(() => {
if (this._overlapExperimentTriggerPending) {
// Deferred so that the listener installing the forwarded assignment context on telemetry runs first.
queueMicrotask(() => {
if (this._overlapExperimentTriggerPending && !this._store.isDisposed) {
this._reportOverlapExperimentTrigger();
}
});
}
}));
this._register(this._stateManager.onDidChangeSessionConfig(e => {
const previousMode = getConfiguredSessionMode(e.previous);
const currentMode = getConfiguredSessionMode(e.current);
Expand Down Expand Up @@ -2008,6 +2024,22 @@ export class AgentSideEffects extends Disposable {

await Promise.all(selectionUpdates);

// A provider can prepare the turn — e.g. materialize a deferred session
// with the selection applied above — while attachments, contributions
// and the checkpoint capture run. Dispatch still waits for both, so the
// checkpoint keeps describing the tree the agent starts from. A failed
// preparation is only logged: `sendMessage` then prepares as usual and
// surfaces any error exactly as it would without the overlap.
let providerPreparation: Promise<void> | undefined;
if (agent.chats.prepareTurn) {
this._reportOverlapExperimentTrigger();
if (this._agentConfigService.getRootValue(platformRootSchema, AgentHostOverlapProviderPreparationConfigKey) === true) {
providerPreparation = agent.chats.prepareTurn(chatUri, turnId, resolvedWorkingDirectories, clientOperationContext).catch(err => {
this._logService.warn(`[AgentSideEffects] Turn preparation failed for ${chat}; sending will prepare again`, err);
});
}
}

failureStage = 'sendMessage';
this._turnTracker.setCurrentStage(turnChannel, turnId, failureStage);
this._turnTracker.markSendStage(turnChannel, turnId, 'attachments');
Expand All @@ -2024,6 +2056,12 @@ export class AgentSideEffects extends Disposable {
await this._discardPendingTurnStartCheckpoint(checkpointCapture, sessionChannel, chatUri, turnId);
return;
}
if (providerPreparation) {
// Measures only what preparation still costs the critical path
// after overlapping the work above, not its total cost.
this._turnTracker.markSendStage(turnChannel, turnId, 'providerPreparation');
await providerPreparation;
}
if (checkpointCapture) {
// Measures only what the checkpoint still costs the critical path
// after overlapping the work above, not the capture's total cost.
Expand Down Expand Up @@ -2088,6 +2126,18 @@ export class AgentSideEffects extends Disposable {
* caller. It is the only cleanup on the failure path, where no such
* cancellation discard exists.
*/
/**
* Reports where overlapped and sequential provider preparation diverge. Agent host
* telemetry only carries the assignment context that ExP attributes the event by once
* the workbench has forwarded it, so until then the trigger stays pending.
*/
private _reportOverlapExperimentTrigger(): void {
this._overlapExperimentTriggerPending = typeof this._agentConfigService.getRootConfigValues?.()[CopilotCliVSCodeAssignmentContextKey] !== 'string';
if (!this._overlapExperimentTriggerPending) {
logSettingExperimentTrigger(this._telemetryService, AgentHostOverlapProviderPreparationSettingId);
}
}

private async _discardPendingTurnStartCheckpoint(capture: Promise<void> | undefined, sessionChannel: ProtocolURI, chatUri: URI, turnId: string): Promise<void> {
if (!capture) {
return;
Expand Down
Loading
Loading