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
6 changes: 6 additions & 0 deletions src/vs/platform/agentHost/common/state/sessionProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ export const AHP_UNSUPPORTED_PROTOCOL_VERSION = -32005 as const;
export const AHP_CONTENT_NOT_FOUND = -32006 as const;
export const AHP_AUTH_REQUIRED = -32007 as const;

/**
* A named resource does not exist on the host. Not always a failure: a client may address a
* session it is about to create.
*/
export const AHP_NOT_FOUND = -32008 as const;

// ---- Type guards -----------------------------------------------------------

import type { AhpRequest, AhpNotification, AhpSuccessResponse, ProtocolMessage, JsonRpcErrorResponse } from './protocol/messages.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ import { IAgentSubscription, observableFromSubscription } from '../../../../../.
import { ChatTruncatedAction } from '../../../../../../platform/agentHost/common/state/protocol/actions.js';
import { CompletionItemKind as AhpCompletionItemKind, ContentEncoding, type CompletionItem as AhpCompletionItem } from '../../../../../../platform/agentHost/common/state/protocol/commands.js';
import { ConfirmationOptionKind, CustomizationType, JsonPrimitive, McpServerAuthRequiredState, McpServerStatus, SessionInputRequestKind, TerminalClaimKind, ToolCallContributorKind, ToolResultContentType, type ConfirmationOption, type ProtectedResourceMetadata, type SessionActiveClient, type SessionInputRequest, type SessionToolClientExecutionRequest } from '../../../../../../platform/agentHost/common/state/protocol/state.js';
import { compareProtocolVersions } from '../../../../../../platform/agentHost/common/state/protocol/version/registry.js';
import { ActionType, ChatTurnStartedAction, isChatAction, type ClientChatAction, type ClientSessionAction } from '../../../../../../platform/agentHost/common/state/sessionActions.js';
import { AHP_AUTH_REQUIRED, ProtocolError } from '../../../../../../platform/agentHost/common/state/sessionProtocol.js';
import { AHP_AUTH_REQUIRED, AHP_NOT_FOUND, ProtocolError } from '../../../../../../platform/agentHost/common/state/sessionProtocol.js';
import { buildChatUri, buildDefaultChatUri, buildSubagentChatUri, ChatOriginKind, getInlineToolInput, getToolSubagentContent, isChatReadOnly, isDefaultChatUri, isMessageHiddenFromTranscript, MessageAttachmentKind, MessageKind, PendingMessageKind, ResponsePartKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, SessionStatus, StateComponents, ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallStatus, TurnState, parseChatUri, mergeSessionWithDefaultChat, readSessionWorkspaceless, readUsageInfoMeta, withMessageHiddenFromTranscript, type ChatState, type ISessionWithDefaultChat, type ICompletedToolCall, type InputRequestResponsePart, type MarkdownResponsePart, type Message, type MessageAttachment, type MessageAnnotationsAttachment, type MessageChatAttachment, type MessageResourceAttachment, type MessageEmbeddedResourceAttachment, type ModelSelection, type PendingMessage, type ReasoningResponsePart, type RootState, type ChatInputAnswer, type ChatInputQuestion, type ChatInputRequest, type ChatSummary, type SessionState, type StringOrMarkdown, type ToolCallResponsePart, type ToolCallState, type ToolInput, type Turn } from '../../../../../../platform/agentHost/common/state/sessionState.js';
import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js';
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
Expand Down Expand Up @@ -122,6 +123,15 @@ const MAX_INLINED_UNSAVED_EDITOR_BYTES = 1024 * 1024;
/** Stable id of the progress row mirroring the host's chat activity, so updates replace it in place. */
const CHAT_ACTIVITY_PROGRESS_ID = 'agentHost.chatActivity';

/**
* First protocol version whose customizations carry list-shaped `enablement` rather than a plain
* `enabled` boolean. Hosts below it reject the newer shape outright.
*
* TODO@osortega: delete this and `_withVersionedCustomizations` once every reachable host speaks
* 0.8.0 — the cloud sandbox image was still on 0.7.0, and those hosts lose client customizations.
*/
const CUSTOMIZATION_ENABLEMENT_PROTOCOL_VERSION = '0.8.0';

export const UNOBSERVED_CLIENT_TOOL_GRACE_MS = 5000;
type AgentHostInvocationFailureStage = 'resolveSession' | 'provisionalSession' | 'sessionState' | 'authentication' | 'createSession' | 'subscribeSession' | 'prepareTurn' | 'dispatchTurn' | 'observeTurn';

Expand Down Expand Up @@ -317,6 +327,14 @@ function userOriginMessage(text: string, attachments: readonly MessageAttachment
: { text, origin: { kind: MessageKind.User } };
}

/**
* Whether `err` reports that the host has no such resource (AHP `NotFound`), which a client can
* legitimately provoke by addressing a session `createSession` has yet to bring into being.
*/
export function isNotFoundError(err: unknown): boolean {
return err instanceof ProtocolError && err.code === AHP_NOT_FOUND;
}

/**
* Extracts a user-facing message from a session-load failure so the actual cause
* (e.g. a git worktree-recreation error) is shown instead of a generic message.
Expand Down Expand Up @@ -1484,7 +1502,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC
// underlying error message (e.g. the git worktree-recreation
// failure) so the user sees the actual cause, falling back to a
// generic message.
if (history.length === 0) {
// Excluded: an id the host learns at `createSession` is not a load failure.
if (history.length === 0 && !isNotFoundError(err)) {
Comment thread
osortega marked this conversation as resolved.
history.push({
type: 'request',
prompt: '',
Expand Down Expand Up @@ -2119,7 +2138,7 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC
private _getCurrentActiveClient(sessionResource: URI): SessionActiveClient {
const entry = this._activeClientEntries.get(sessionResource);
if (entry) {
return entry.getActiveClient();
return this._withVersionedCustomizations(entry.getActiveClient());
}

return {
Expand All @@ -2129,6 +2148,22 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC
};
}

/**
* Drop client-published customizations for a host older than list-shaped enablement, which
* rejects the newer shape outright with `invalid params: missing field 'enabled'`. Temporary;
* see {@link CUSTOMIZATION_ENABLEMENT_PROTOCOL_VERSION}.
*/
private _withVersionedCustomizations(activeClient: SessionActiveClient): SessionActiveClient {
const hostVersion = this._config.connection.initializeResult.get()?.protocolVersion;
if (!activeClient.customizations?.length
|| !hostVersion
|| compareProtocolVersions(hostVersion, CUSTOMIZATION_ENABLEMENT_PROTOCOL_VERSION) >= 0) {
return activeClient;
}
this._logService.warn(`[AgentHost] Host speaks protocol ${hostVersion}; dropping ${activeClient.customizations.length} client customization(s) that require ${CUSTOMIZATION_ENABLEMENT_PROTOCOL_VERSION}.`);
return { ...activeClient, customizations: [] };
}

private _ensureActiveClient(sessionResource: URI, backendSession: URI): ActiveClientEntry | undefined {
const entry = this._ensureActiveClientEntry(sessionResource);
if (!entry) {
Expand All @@ -2154,7 +2189,10 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC
this._config.connection.clientId,
AgentHostSessionHandler.ACTIVE_CLIENT_RECONCILIATION_DEBOUNCE_MS,
backendSession => this._getSessionState(backendSession.toString()),
(backendSession, action) => this._dispatchAction(backendSession, action),
// Reconciliation republishes customizations, so it needs the same gate.
(backendSession, action) => this._dispatchAction(backendSession, action.type === ActionType.SessionActiveClientSet
? { ...action, activeClient: this._withVersionedCustomizations(action.activeClient) }
: action),
Comment thread
osortega marked this conversation as resolved.
);
this._activeClientEntries.set(sessionResource, entry);
return entry;
Expand Down Expand Up @@ -5534,7 +5572,27 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC
/** `undefined` is preserved for createSession to let the host choose its working directories. */
private _resolveRequestedWorkingDirectories(sessionResource: URI): readonly URI[] | undefined {
const primary = this._resolveRequestedWorkingDirectory(sessionResource);
return computeWorkingDirectories(primary, this._workspaceContextService.getWorkspace().folders.map(folder => folder.uri), this._getRootState(), this._config.provider);
return this._hostAddressableWorkingDirectories(
computeWorkingDirectories(primary, this._workspaceContextService.getWorkspace().folders.map(folder => folder.uri), this._getRootState(), this._config.provider)
);
}

/**
* Drop working directories whose scheme the host cannot address, falling back to `undefined` so
* it picks its own. Schemes are compared as the host receives them, after unwrapping.
*/
private _hostAddressableWorkingDirectories(directories: readonly URI[] | undefined): readonly URI[] | undefined {
const defaultDirectory = this._config.connection.initializeResult.get()?.defaultDirectory;
if (!directories?.length || !defaultDirectory) {
return directories;
}
const hostScheme = URI.isUri(defaultDirectory) ? URI.revive(defaultDirectory).scheme : URI.parse(defaultDirectory).scheme;
const addressable = directories.filter(directory => this._config.connection.resourceUris.toAgentHost(directory).scheme === hostScheme);
if (addressable.length === directories.length) {
return directories;
}
this._logService.warn(`[AgentHost] Host addresses '${hostScheme}' working directories; dropping ${directories.length - addressable.length} that it cannot use.`);
return addressable.length > 0 ? addressable : undefined;
}

/**
Expand Down
Loading
Loading