Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f9e30c1
agentHost: recover failed Copilot turns in place
roblourens Aug 21, 2026
285a93d
Merge origin/main into failed turn recovery
roblourens Aug 21, 2026
77aeddd
Merge latest origin/main into failed turn recovery
roblourens Aug 21, 2026
540942c
Merge remote-tracking branch 'origin/main' into roblou/agents/vs-code…
roblourens Aug 22, 2026
2741790
Merge latest origin/main into failed turn recovery
roblourens Aug 22, 2026
ed59e42
Merge current origin/main into failed turn recovery
roblourens Aug 23, 2026
21bd994
Merge latest origin/main into failed turn recovery
roblourens Aug 23, 2026
0411790
agentHost: sync final AHP recovery output
roblourens Aug 23, 2026
705f433
agentHost: harden failed turn recovery
roblourens Aug 24, 2026
f8d55c1
Merge origin/main into failed turn recovery
roblourens Aug 25, 2026
89393e3
Merge remote-tracking branch 'origin/main' into roblou/agents/vs-code…
roblourens Aug 25, 2026
5620172
agentHost: assert imported durable error parts
roblourens Aug 25, 2026
e7780d7
agentHost: address failed-turn recovery review
roblourens Aug 25, 2026
5616563
agentHost: add deterministic failed-turn UI testing
roblourens Aug 25, 2026
723e501
agentHost: cover recovery after completed tools
roblourens Aug 25, 2026
9327fdc
Merge latest origin/main into failed turn recovery
roblourens Aug 25, 2026
c95c9c0
Merge newest origin/main into failed turn recovery
roblourens Aug 25, 2026
3226174
Merge protocol error alignment from origin/main
roblourens Aug 26, 2026
d19737c
Merge remote-tracking branch 'origin/main' into roblou/agents/vs-code…
roblourens Aug 26, 2026
9342f6f
agentHost: simplify development error injection
roblourens Aug 26, 2026
3663f07
agentHost: harden concurrent turn recovery
roblourens Aug 26, 2026
6276101
Merge remote-tracking branch 'origin/main' into roblou/agents/vs-code…
roblourens Aug 26, 2026
b4b2387
agentHost: verify resumed usage presentation
roblourens Aug 26, 2026
89694c1
Merge latest origin/main into failed-turn recovery
roblourens Aug 26, 2026
9da2123
agentHost: clarify interrupted turn message (Written by Copilot)
roblourens Aug 26, 2026
ee8b3dc
Merge origin/main into failed-turn recovery (Written by Copilot)
roblourens Aug 26, 2026
e6bf53e
chat: trim retry accessibility help (Written by Copilot)
roblourens Aug 26, 2026
0d75d4b
Merge remote-tracking branch 'origin/main' into roblou/agents/vs-code…
roblourens Aug 26, 2026
e25b234
chat: continue interrupted turns in place (Written by Copilot)
roblourens Aug 26, 2026
ed69f65
Merge remote-tracking branch 'origin/main' into roblou/agents/vs-code…
roblourens Aug 26, 2026
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
3 changes: 3 additions & 0 deletions src/vs/platform/agentHost/common/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,9 @@ export interface IAgentChats {
*/
sendMessage(chat: URI, prompt: string, workingDirectoriesOrDirectory: readonly URI[] | URI | undefined, attachments?: readonly MessageAttachment[], turnId?: string, senderClientId?: string, clientTypeOrContext?: AgentHostClientType | URI | IAgentChatContext, context?: URI | IAgentChatContext): Promise<void>;

/** Resume a failed turn without adding another user message. */
resumeTurn?(chat: URI, turnId: string, context: AgentChatOperationContext, senderClientId?: string, clientType?: AgentHostClientType): Promise<void>;

/** Abort the in-flight turn for `chat`. */
abort(chat: URI, context: AgentChatOperationContext): Promise<void>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const IAgentHostChatContributions = createDecorator<IAgentHostChatContrib
export type TurnEndReason =
| { readonly kind: 'success' }
| { readonly kind: 'cancelled' }
| { readonly kind: 'error'; readonly error: ErrorInfo }
| { readonly kind: 'error'; readonly error: ErrorInfo; readonly resumable: boolean }
| { readonly kind: 'localCommand' };

/** A terminal turn outcome offered to contributions after a turn ended. */
Expand Down
5 changes: 5 additions & 0 deletions src/vs/platform/agentHost/common/agentHostPlanReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ export interface IAgentHostPlanReview {
export type ChatInputRequestWithPlanReview = ChatInputRequest & {
readonly planReview?: IAgentHostPlanReview;
};

export function isChatInputRequestWithPlanReview(request: ChatInputRequest): request is ChatInputRequestWithPlanReview {
const candidate: ChatInputRequestWithPlanReview = request;
return candidate.planReview !== undefined;
}
81 changes: 79 additions & 2 deletions src/vs/platform/agentHost/common/state/sessionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
type ChatState,
type ChatSummary,
type ErrorInfo,
type ErrorResponsePart,
type PendingMessage,
type Turn,
type AnnotationsState,
Expand Down Expand Up @@ -70,8 +71,8 @@ export {
type ConfigSchema,
type ContentRef, type Customization, type CustomizationDegradedState,
type CustomizationErrorState, type CustomizationLoadedState, type CustomizationLoadingState, type CustomizationLoadState, type DirectoryCustomization, type ErrorInfo, type HookCustomization, type FileEdit as ISessionFileDiff, type ToolResultEmbeddedResourceContent as IToolResultBinaryContent, type MarkdownResponsePart, type McpServerCustomization, type MessageAttachment,
type MessageResourceAttachment, type MessageEmbeddedResourceAttachment, type MessageAnnotationsAttachment, type MessageChatAttachment, type ModelSelection, type PendingMessage, type PluginCustomization, type ProjectInfo, type PromptCustomization, type ReasoningResponsePart, type ErrorResponsePart,
type ResponsePart,
type MessageResourceAttachment, type MessageEmbeddedResourceAttachment, type MessageAnnotationsAttachment, type MessageChatAttachment, type ModelSelection, type PendingMessage, type PluginCustomization, type ProjectInfo, type PromptCustomization, type ReasoningResponsePart,
type ErrorResponsePart, type ResponsePart,
type RootState, type RuleCustomization, type SessionActiveClient,
type SessionConfigState, type SessionModelInfo,
type SessionState,
Expand Down Expand Up @@ -100,6 +101,82 @@ export {
type Message
} from './protocol/state.js';

export function getErrorResponsePart(turn: Turn | ActiveTurn | undefined): ErrorResponsePart | undefined {
if (!turn) {
return undefined;
}
const part = turn.responseParts.at(-1);
return part?.kind === ResponsePartKind.Error ? part : undefined;
}

export function createErrorResponsePart(error: ErrorInfo, resumable = false): ErrorResponsePart {
return {
kind: ResponsePartKind.Error,
error,
...(resumable ? { resumable: true } : {}),
};
}

export function mergeLogicalTurnUsage(previous: UsageInfo | undefined, current: UsageInfo | undefined): UsageInfo | undefined {
if (!previous) {
return current;
}
if (!current) {
return previous;
}

const previousMeta = readUsageInfoMeta(previous);
const currentMeta = readUsageInfoMeta(current);
const cost = sumDefined(previousMeta.cost, currentMeta.cost);
const totalNanoAiu = sumDefined(previousMeta.copilotUsage?.totalNanoAiu, currentMeta.copilotUsage?.totalNanoAiu);
const turnTokenTotals = mergeTurnTokenTotals(previousMeta.turnTokenTotals, currentMeta.turnTokenTotals);
const directTotalNanoAiu = sumDefined(previousMeta.directCopilotUsage?.totalNanoAiu, currentMeta.directCopilotUsage?.totalNanoAiu);
const directTurnTokenTotals = mergeTurnTokenTotals(previousMeta.directTurnTokenTotals, currentMeta.directTurnTokenTotals);
const meta = previous._meta !== undefined || current._meta !== undefined ? {
...previous._meta,
...current._meta,
...(cost !== undefined ? { cost } : {}),
...(previousMeta.copilotUsage || currentMeta.copilotUsage ? {
copilotUsage: {
...previousMeta.copilotUsage,
...currentMeta.copilotUsage,
...(totalNanoAiu !== undefined ? { totalNanoAiu } : {}),
},
} : {}),
...(turnTokenTotals ? { turnTokenTotals } : {}),
...(directTotalNanoAiu !== undefined ? { directCopilotUsage: { totalNanoAiu: directTotalNanoAiu } } : {}),
...(directTurnTokenTotals ? { directTurnTokenTotals } : {}),
} : undefined;

return {
...previous,
...current,
model: current.model ?? previous.model,
...(meta ? { _meta: meta } : {}),
};
}

function sumDefined(first: number | undefined, second: number | undefined): number | undefined {
return first === undefined ? second : second === undefined ? first : first + second;
}

function mergeTurnTokenTotals(previous: UsageInfoMeta['turnTokenTotals'], current: UsageInfoMeta['turnTokenTotals']): UsageInfoMeta['turnTokenTotals'] {
if (!previous && !current) {
return undefined;
}
const totals = new Map<string, ITurnTokenTotal>();
for (const total of [...previous ?? [], ...current ?? []]) {
const existing = totals.get(total.model);
totals.set(total.model, existing ? {
model: total.model,
inputTokens: existing.inputTokens + total.inputTokens,
cachedTokens: existing.cachedTokens + total.cachedTokens,
outputTokens: existing.outputTokens + total.outputTokens,
} : { ...total });
}
return [...totals.values()];
}

/**
* Well-known keys that may appear on {@link UsageInfo._meta}.
* Clients MAY read these to provide enhanced UI (e.g. credit cost display).
Expand Down
40 changes: 38 additions & 2 deletions src/vs/platform/agentHost/node/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } f
import { AhpErrorCodes, AHP_SESSION_NOT_FOUND, ContentEncoding, JSON_RPC_INTERNAL_ERROR, ProtocolError, ResourceChangeType, ResourceType, ResourceWriteMode, type CreateResourceWatchParams, type CreateResourceWatchResult, type DirectoryEntry, type ResourceCopyParams, type ResourceCopyResult, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMkdirParams, type ResourceMkdirResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceResolveParams, type ResourceResolveResult, type ResourceWatchState, type ResourceWriteParams, type ResourceWriteResult, type IStateSnapshot } from '../common/state/sessionProtocol.js';
import { ChangesSummary, ChatInteractivity, ChatOriginKind, MessageAttachmentKind, type Annotation, type AnnotationEntry, type AnnotationOrigin, type AnnotationsState, type ChatOrigin, type Customization, type Message, type MessageAttachment, type MessageResourceAttachment, type TextRange } from '../common/state/protocol/state.js';
import type { ChatPendingMessageSetAction, ChatTurnStartedAction, SessionConfigChangedAction } from '../common/state/protocol/actions.js';
import { ISessionGitHubState, ISessionGitState, MessageKind, ResponsePartKind, SESSION_META_GITHUB_KEY, SESSION_META_GIT_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, AH_META_CREATED_BY_SESSION_DB_KEY, readSessionCreationReference, readSessionSpawnDepth, withSessionSpawnDepth, withSessionCreationReference, parseSessionCreationReference, SessionLifecycle, SessionStatus, ToolCallStatus, ToolResultContentType, AH_META_WORKSPACELESS_DB_KEY, AH_META_EHCLI_ADOPTED_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, AH_META_IS_READ_DB_KEY, buildChatUri, buildDefaultChatUri, buildResourceWatchChannelUri, buildSubagentChatUri, buildSubagentSessionUriPrefix, isAhpChatChannel, isDefaultChatUri, isSubagentChatUri, isSubagentSession, needsSessionGitStateRefresh, parseChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseResourceWatchChannelUri, parseSessionMultiRootMetadata, parseSubagentSessionUri, readSessionExternal, readSessionGitHubState, readSessionGitState, readSessionMultiRootMetadata, readSessionSourceControlState, readSessionWorkspaceless, withSessionExternal, withSessionGitHubState, withSessionGitState, withMessageHiddenFromTranscript, withSessionMultiRootMetadata, withSessionSourceControlState, withSessionStatusFlag, withSessionWorkspaceless, withSessionEhcliAdopted, withSessionFolderPickerDecision, readSessionFolderPickerDecision, parseSessionFolderPickerDecision, SESSION_META_FOLDER_PICKER_KEY, readSessionEhcliAdoptable, type ISessionSourceControlState, type SessionConfigState, type SessionSummary, type ToolResultSubagentContent, type Turn } from '../common/state/sessionState.js';
import { ISessionGitHubState, ISessionGitState, MessageKind, ResponsePartKind, SESSION_META_GITHUB_KEY, SESSION_META_GIT_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, AH_META_CREATED_BY_SESSION_DB_KEY, readSessionCreationReference, readSessionSpawnDepth, withSessionSpawnDepth, withSessionCreationReference, parseSessionCreationReference, SessionLifecycle, SessionStatus, ToolCallStatus, ToolResultContentType, TurnState, AH_META_WORKSPACELESS_DB_KEY, AH_META_EHCLI_ADOPTED_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, AH_META_IS_READ_DB_KEY, buildChatUri, buildDefaultChatUri, buildResourceWatchChannelUri, buildSubagentChatUri, buildSubagentSessionUriPrefix, getErrorResponsePart, isAhpChatChannel, isChatReadOnly, isDefaultChatUri, isSubagentChatUri, isSubagentSession, needsSessionGitStateRefresh, parseChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseResourceWatchChannelUri, parseSessionMultiRootMetadata, parseSubagentSessionUri, readSessionExternal, readSessionGitHubState, readSessionGitState, readSessionMultiRootMetadata, readSessionSourceControlState, readSessionWorkspaceless, withMessageHiddenFromTranscript, withSessionExternal, withSessionGitHubState, withSessionGitState, withSessionMultiRootMetadata, withSessionSourceControlState, withSessionStatusFlag, withSessionWorkspaceless, withSessionEhcliAdopted, withSessionFolderPickerDecision, readSessionFolderPickerDecision, parseSessionFolderPickerDecision, SESSION_META_FOLDER_PICKER_KEY, readSessionEhcliAdoptable, type ISessionSourceControlState, type SessionConfigState, type SessionSummary, type ToolResultSubagentContent, type Turn } from '../common/state/sessionState.js';
import { readToolCallMeta } from '../common/meta/agentToolCallMeta.js';
import { isHostSnapshotAttachment, toHostSnapshotAttachmentMeta } from '../common/meta/agentSnapshotAttachmentMeta.js';
import { readEphemeralSessionMeta, withEphemeralSessionMeta } from '../common/meta/agentEphemeralSessionMeta.js';
Expand Down Expand Up @@ -4391,6 +4391,42 @@ export class AgentService extends Disposable implements IAgentService {

private _dispatchActionNow(channel: string, sessionChannel: string, action: SessionAction | ChatAction | TerminalAction | ClientChangesetAction | ClientAnnotationsAction | IRootConfigChangedAction, clientId: string, clientSeq: number, clientContext: IAgentHostClientTelemetryContext): void {
const origin = { clientId, clientSeq };
if (action.type === ActionType.ChatTurnCancelled) {
const resumedDuration = this._sideEffects.getResumedTurnDuration(channel, action.turnId);
if (resumedDuration !== undefined) {
action = { ...action, duration: resumedDuration };
}
}
let resumedTurn: Turn | undefined;
if (action.type === ActionType.ChatTurnResume) {
if (!isAhpChatChannel(channel)) {
this._stateManager.rejectClientAction(channel, action, origin, 'Turn resume requires a chat channel.');
return;
}
const chatState = this._stateManager.getChatState(channel);
const sessionState = this._stateManager.getSessionState(sessionChannel);
const sessionArchived = ((sessionState?.status ?? 0) & SessionStatus.IsArchived) === SessionStatus.IsArchived;
const turn = chatState?.turns.at(-1);
const errorPart = getErrorResponsePart(turn);
const provider = this._findProviderForSession(sessionChannel);
if (chatState?.activeTurn) {
this._stateManager.rejectClientAction(channel, action, origin, 'Cannot resume while a turn is active.');
return;
}
if (isChatReadOnly(chatState?.interactivity, sessionArchived)) {
this._stateManager.rejectClientAction(channel, action, origin, 'Cannot resume a read-only or archived chat.');
return;
}
if (!turn || turn.id !== action.turnId || turn.state !== TurnState.Error || errorPart?.resumable !== true) {
this._stateManager.rejectClientAction(channel, action, origin, 'The requested turn is not the latest resumable errored turn.');
return;
}
if (!provider?.chats.resumeTurn) {
this._stateManager.rejectClientAction(channel, action, origin, 'The session provider does not support turn resume.');
return;
}
resumedTurn = turn;
}
if (action.type === ActionType.ChatTurnStarted && this._isTurnIdUsedByAnotherChat(sessionChannel, channel, action.turnId)) {
this._stateManager.rejectClientAction(channel, action, origin, 'Turn id is already used by another chat in this session.');
return;
Expand Down Expand Up @@ -4441,7 +4477,7 @@ export class AgentService extends Disposable implements IAgentService {
this._editAttributionService.setEnabled(editTelemetryEnabled);
}
}
this._sideEffects.handleAction(channel, action, clientId, clientContext);
this._sideEffects.handleAction(channel, action, clientId, clientContext, resumedTurn);
}
private _getUnresolvedPeerChats(sessionChannel: string): readonly string[] | undefined {
return this._stateManager.getSessionState(sessionChannel)?.chats.filter(chat => !isDefaultChatUri(chat.resource) && !this._stateManager.getChatState(chat.resource)).map(chat => chat.resource);
Expand Down
Loading
Loading