Skip to content
Draft
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
2 changes: 1 addition & 1 deletion src/vs/platform/agentHost/OTEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ the deadline remain missing; the marker is not proof that a turn completed.
| Host `timeToProviderDispatch` | Turn start to provider dispatch; absent if not observed |
| 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 `sendStageCheckpointMs` | **Residual critical-path wait** for the checkpoint after overlap with earlier preparation, not the entire checkpoint operation; absent whenever `chat.agentHost.experimental.deferTurnStartCheckpoint` is enabled and the provider supports a turn-start barrier — dispatch then never waits for the capture, even when it already completed — and the provider holds the turn's tools and hook commands until the capture completes |
| 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 |
Expand Down
15 changes: 15 additions & 0 deletions src/vs/platform/agentHost/common/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,14 @@ export interface IAgentChatContext {
readonly hostInstructions?: readonly string[];
/** Records provider stage timing for the turn being sent; supplied only for a send. */
readonly sendStageRecorder?: IAgentProviderSendStageRecorder;
/**
* Settles once the turn-start checkpoint has been captured; supplied only
* for a send to a provider that sets {@link IAgentChats.supportsTurnStartBarrier}
* when the host dispatches without waiting for that capture. The provider
* must await it before running any tool of the turn, because tools can
* modify the working tree the checkpoint describes.
*/
readonly turnStartBarrier?: Promise<void>;
/** Whether the current turn is an automated Agent Merge repair turn. */
readonly agentMergeTurn?: boolean;
}
Expand Down Expand Up @@ -829,6 +837,13 @@ export interface IAgentPrepareChatResult {
* the provider needs the owning session or storage scope.
*/
export interface IAgentChats {
/**
* Whether the provider honors {@link IAgentChatContext.turnStartBarrier} by
* running no tool of a turn before it settles. Only then may the host send
* a turn before its turn-start checkpoint has been captured.
*/
readonly supportsTurnStartBarrier?: boolean;

/**
* Optional pre-send preparation, such as materializing a deferred provider
* session, after the turn's model/agent selection and working directories
Expand Down
15 changes: 15 additions & 0 deletions src/vs/platform/agentHost/common/agentHostSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,15 @@ export const AgentHostAutoAttachPullRequestsConfigKey = 'autoAttachPullRequests'
*/
export const AgentHostOverlapProviderPreparationConfigKey = 'overlapProviderPreparation';

/**
* Root config key forwarded from the renderer's
* `chat.agentHost.experimental.deferTurnStartCheckpoint` setting. When `true`,
* a turn is sent to a provider that supports a turn-start barrier while its
* turn-start checkpoint is still being captured; the provider holds the
* turn's tools until the capture completes.
*/
export const AgentHostDeferTurnStartCheckpointConfigKey = 'deferTurnStartCheckpoint';

// 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 @@ -943,6 +952,12 @@ export const platformRootSchema = createSchema({
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,
}),
[AgentHostDeferTurnStartCheckpointConfigKey]: schemaProperty<boolean>({
type: 'boolean',
title: localize('agentHost.config.deferTurnStartCheckpoint.title', "Defer Turn-Start Checkpoint"),
description: localize('agentHost.config.deferTurnStartCheckpoint.description', "Whether turns are sent while their turn-start checkpoint is still being captured, with the agent's tools held until the capture completes."),
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 @@ -26,6 +26,7 @@ import {
AgentHostCopilotMultiRootEnabledSettingId,
AgentHostMarkdownPlanRichLinksEnabledSettingId,
AgentHostOverlapProviderPreparationSettingId,
AgentHostDeferTurnStartCheckpointSettingId,
AgentHostOTelCaptureContentSettingId,
AgentHostOTelCaptureIdentitySettingId,
AgentHostOTelDbSpanExporterEnabledSettingId,
Expand All @@ -51,6 +52,7 @@ import {
AgentHostMcpToolRoutingEnabledConfigKey,
AgentHostMarkdownPlanRichLinksEnabledConfigKey,
AgentHostOverlapProviderPreparationConfigKey,
AgentHostDeferTurnStartCheckpointConfigKey,
AgentHostSystemProxyEnabledConfigKey,
AgentHostWorkspaceSnapshotEnabledConfigKey,
} from './agentHostSchema.js';
Expand Down Expand Up @@ -217,6 +219,15 @@ configurationRegistry.registerConfiguration({
experiment: { mode: 'auto' },
agentHost: { key: AgentHostOverlapProviderPreparationConfigKey },
},
[AgentHostDeferTurnStartCheckpointSettingId]: {
type: 'boolean',
description: nls.localize('chat.agentHost.experimental.deferTurnStartCheckpoint', "When enabled, a turn is sent to the agent while its turn-start checkpoint is still being captured, and the agent's tools wait until the capture completes. This removes the checkpoint from the time to the first response."),
default: false,
scope: ConfigurationScope.APPLICATION,
tags: ['experimental', 'advanced'],
experiment: { mode: 'auto' },
agentHost: { key: AgentHostDeferTurnStartCheckpointConfigKey },
},
[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
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 @@ -138,6 +138,9 @@ export const AgentHostAutoAttachPullRequestsSettingId = 'chat.agentHost.experime
/** Configuration key controlling whether providers prepare a turn alongside the turn-start checkpoint. */
export const AgentHostOverlapProviderPreparationSettingId = 'chat.agentHost.experimental.overlapProviderPreparation';

/** Configuration key controlling whether turns are sent before their turn-start checkpoint is captured. */
export const AgentHostDeferTurnStartCheckpointSettingId = 'chat.agentHost.experimental.deferTurnStartCheckpoint';

/**
* Configuration key gating multiple-working-directory support for the Copilot
* agent-host provider. When `true`, the Copilot provider advertises the
Expand Down
47 changes: 40 additions & 7 deletions src/vs/platform/agentHost/node/agentHostCheckpointService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost
*/
private readonly _sequencer = new SequencerByKey<string>();
private readonly _turnStartCheckpoints = new Map<string, Map<string, ITurnStartCheckpoint>>();
/** Settling markers of the turn-start captures queued per session key. */
private readonly _pendingTurnStartCaptures = new Map<string, Set<Promise<void>>>();

constructor(
@ISessionDataService private readonly _sessionDataService: ISessionDataService,
Expand Down Expand Up @@ -75,7 +77,7 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost
}

// Check if the baseline ref already exists
const baselineCheckpointRef = await this.getBaselineCheckpoint(sessionUri, repositoryRootUri);
const baselineCheckpointRef = await this._readBaselineCheckpoint(sessionUri, repositoryRootUri);
if (baselineCheckpointRef) {
continue;
}
Expand All @@ -96,7 +98,31 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost
}

captureTurnStartCheckpoint(sessionUri: URI, chatUri: URI, turnId: string, workingDirectories: readonly URI[] | undefined): Promise<void> {
return this._sequencer.queue(sessionUri.toString(), () => this._captureTurnStartCheckpoint(sessionUri, chatUri, turnId, workingDirectories));
const sessionKey = sessionUri.toString();
const capture = this._sequencer.queue(sessionKey, () => this._captureTurnStartCheckpoint(sessionUri, chatUri, turnId, workingDirectories));
// Readers wait for in-flight captures: a turn can be sent before its capture
// completes, and the first capture of a session also creates its baseline.
let pending = this._pendingTurnStartCaptures.get(sessionKey);
if (!pending) {
pending = new Set();
this._pendingTurnStartCaptures.set(sessionKey, pending);
}
const settled = capture.then(undefined, () => undefined);
pending.add(settled);
void settled.then(() => {
pending.delete(settled);
if (pending.size === 0 && this._pendingTurnStartCaptures.get(sessionKey) === pending) {
this._pendingTurnStartCaptures.delete(sessionKey);
}
});
return capture;
}

private async _whenTurnStartCapturesSettled(sessionUri: URI): Promise<void> {
const pending = this._pendingTurnStartCaptures.get(sessionUri.toString());
if (pending?.size) {
await Promise.all(pending);
}
}

private async _captureTurnStartCheckpoint(sessionUri: URI, chatUri: URI, turnId: string, workingDirectories: readonly URI[] | undefined): Promise<void> {
Expand Down Expand Up @@ -143,7 +169,7 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost
// The baseline lookup is independent of the capture, so overlap it.
const [tree, hasBaseline] = await Promise.all([
this._gitService.captureWorkingTreeAsTree(repositoryRootUri),
this.getBaselineCheckpoint(sessionUri, repositoryRootUri),
this._readBaselineCheckpoint(sessionUri, repositoryRootUri),
]);
if (tree) {
if (!hasBaseline) {
Expand Down Expand Up @@ -233,7 +259,7 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost

// Check if the baseline ref exists for this repository. If it
// doesn't exist, we cannot capture a turn checkpoint for this repository.
const baselineCheckpointRef = await this.getBaselineCheckpoint(sessionUri, repositoryRootUri);
const baselineCheckpointRef = await this._readBaselineCheckpoint(sessionUri, repositoryRootUri);
if (!baselineCheckpointRef) {
continue;
}
Expand Down Expand Up @@ -306,6 +332,7 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost
turnId: string,
workingDirectory?: URI
): Promise<{ parent: string; current: string } | undefined> {
await this._whenTurnStartCapturesSettled(sessionUri);
if (!workingDirectory) {
const workingDirectories = this._agentConfigService.getEffectiveWorkingDirectories(sessionUri.toString());
if (!workingDirectories || workingDirectories.length === 0) {
Expand All @@ -319,7 +346,7 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost
const [currentCheckpointRef, previousCheckpointRef, baselineCheckpointRef] = await Promise.all([
ref.object.getTurnCheckpointRef(turnId),
ref.object.getPreviousCheckpointRef(turnId),
this.getBaselineCheckpoint(sessionUri, workingDirectory)
this._readBaselineCheckpoint(sessionUri, workingDirectory)
]);
if (!currentCheckpointRef || !baselineCheckpointRef) {
return undefined;
Expand All @@ -344,6 +371,12 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost
}

async getBaselineCheckpoint(sessionUri: URI, workingDirectory?: URI): Promise<string | undefined> {
await this._whenTurnStartCapturesSettled(sessionUri);
return this._readBaselineCheckpoint(sessionUri, workingDirectory);
}

/** Reads the baseline ref without waiting for pending captures; safe inside sequenced captures. */
private async _readBaselineCheckpoint(sessionUri: URI, workingDirectory?: URI): Promise<string | undefined> {
if (!workingDirectory) {
const workingDirectories = this._agentConfigService.getEffectiveWorkingDirectories(sessionUri.toString());
if (!workingDirectories || workingDirectories.length === 0) {
Expand Down Expand Up @@ -437,7 +470,7 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost
continue;
}

const baselineCheckpointRef = await this.getBaselineCheckpoint(sessionUri, repositoryRootUri);
const baselineCheckpointRef = await this._readBaselineCheckpoint(sessionUri, repositoryRootUri);
if (!baselineCheckpointRef) {
continue;
}
Expand Down Expand Up @@ -478,7 +511,7 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost
}

private async _ensureBaselineCheckpoint(sessionUri: URI, repositoryRootUri: URI, tree: string): Promise<void> {
if (await this.getBaselineCheckpoint(sessionUri, repositoryRootUri)) {
if (await this._readBaselineCheckpoint(sessionUri, repositoryRootUri)) {
return;
}

Expand Down
Loading
Loading