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
14 changes: 14 additions & 0 deletions src/vs/platform/agentHost/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,20 @@ Copilot also has no AH-session container:

No `CopilotSessionEntry`, `AgentSessionEntry`, default-chat URI helper, or sibling cascade remains. Send/history/model/agent/abort/tool/config/dispose/release operations resolve one leaf. Active-client state remains keyed by the owning SDK session where it is genuinely shared, while each live leaf owns its own SDK and MCP lifecycle. Capabilities remain `multipleChats: { fork: true }`.

#### Persisted conversation search (preview)

The optional `IAgent.searchChatHistory` seam receives one exact chat, its host-owned persistence context, and opaque provider data. It must not create, resume, or materialize the conversation. `AgentService.searchSessionHistory` enumerates the orchestrator's default and peer-chat catalog and delegates through that seam; it never interprets SDK backing identifiers. Results carry chat and turn locators, an author role, and a bounded snippet. Loaded host turn IDs are reconciled with persisted event IDs before results leave the host.

Remote clients discover `vscode/searchSessionHistory` through the `vscode.searchSessionHistory` initialize capability. Local utility-process connections keep protocol extensions disabled and use the management channel's `supportsSessionHistorySearch` and `searchSessionHistory` methods instead. Callers consult the connection's transport-aware support query rather than assuming protocol metadata covers the local management channel. The preview is exposed by **Chat: Search Agent Session Content (Preview)**, independently of the existing title-only Find widget. Unsupported hosts and failed reads must remain distinguishable from an empty result.

Copilot reads the SDK's paginated persisted event journal and supplies normalized documents to `IAgentHostSessionSearchIndex`. That service owns one rebuildable `agent-host-search.db` beside the host's profile storage file. Harness/chat identities, turns, and documents are normalized through integer keys; the FTS rowid is the document ID. Queries constrain the owning chat before limiting results. This provider-neutral storage does not itself enable Claude or Codex search.

The journal remains authoritative; search never modifies SDK or session-history databases. Index freshness is checked per chat against the persisted journal and backing identity. Rebuild and deletion synchronize metadata and FTS rows atomically. Existing session-data deletion notifications remove the corresponding cache rows. Recognized legacy `session-search.db` sidecars are removed lazily only after successful shared-cache indexing; unrelated files are left alone. Keyword search runs locally on the owning host, including for remote connections, without sending content to an embeddings endpoint. Tool-origin subagent chats, reasoning, attachments, and arbitrary tool output remain outside the preview's search scope.

Semantic search adds an opt-in client-orchestrated path. The workbench obtains embeddings through registered Copilot extension providers; the host does not import extension services, own endpoint credentials, or make embedding network calls. The `sessionSemanticSearch` operation exposes bounded pending chunks, accepts vectors for unchanged chunk identities, and ranks stored embeddings within the host-authorized chat catalog. Remote transport advertises this separately from lexical search; local clients use management IPC. Vector and chunk rows belong to the same rebuildable search cache, and are invalidated with their source documents.

The client must obtain explicit permission before sending saved content or queries to an embedding provider. Consent is scoped to the open search picker and its workspace scope, not persisted as general permission for future searches. Keyword search remains available without embeddings, and semantic failures or partial indexing are surfaced rather than appearing as complete empty results.

### Codex (`node/codex/codexAgent.ts`)

Codex supports multiple chats per session. Each conversation — the session's default chat and every additional chat — is a distinct top-level Codex thread, explicitly bound to the concrete chat URI AH supplies:
Expand Down
328 changes: 328 additions & 0 deletions src/vs/platform/agentHost/SESSION_SEARCH.md

Large diffs are not rendered by default.

28 changes: 27 additions & 1 deletion src/vs/platform/agentHost/browser/agentHostProtocolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import { FileSystemProviderErrorCode, toFileSystemProviderErrorCode } from '../.
import { ConfigurationTarget, ConfigurationTargetToString, IConfigurationService } from '../../configuration/common/configuration.js';
import { AgentSession, IAgentCreateChatRequestOptions, IAgentCreateSessionConfig, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IMcpNotification } from '../common/agent.js';
import { AGENT_HOST_DEBUG_LOGS_CHUNK_BYTES, AGENT_HOST_DEBUG_LOGS_MAX_ENTRIES, IAgentConnection, IAgentHostManagedSettingsDiagnostics, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkFetchResult, type AgentHostDebugLogsArtifactKind, type IAgentHostDebugLogsArtifact, type IAgentHostDebugLogsChunk } from '../common/agentService.js';
import { ClaimAgentHostDetachedWorktreeExtensionMethod, CollectAgentHostDebugLogsExtensionMethod, CreateAgentHostDetachedWorktreeExtensionMethod, DeleteAgentHostDetachedWorktreeExtensionMethod, GetAgentHostSessionStateFileExtensionMethod, ReadAgentHostDebugLogsChunkExtensionMethod, ReconcileAgentHostDetachedWorktreesExtensionMethod, RemoveSessionArtifactExtensionMethod, RequestAgentHostWorkspaceTrustExtensionMethod, SetAgentHostDetachedWorktreeArchivedExtensionMethod, supportsAgentHostChatStateFile, type IAgentHostExtensionCommandMap, type IAgentHostExtensionInitializeResult, type IAgentHostExtensionServerCommandMap } from '../common/agentHostExtensionProtocol.js';
import { ClaimAgentHostDetachedWorktreeExtensionMethod, CollectAgentHostDebugLogsExtensionMethod, CreateAgentHostDetachedWorktreeExtensionMethod, DeleteAgentHostDetachedWorktreeExtensionMethod, GetAgentHostSessionStateFileExtensionMethod, ReadAgentHostDebugLogsChunkExtensionMethod, ReconcileAgentHostDetachedWorktreesExtensionMethod, RemoveSessionArtifactExtensionMethod, RequestAgentHostWorkspaceTrustExtensionMethod, SearchSessionHistoryExtensionMethod, SessionSemanticSearchExtensionMethod, SetAgentHostDetachedWorktreeArchivedExtensionMethod, supportsAgentHostChatStateFile, type IAgentHostExtensionCommandMap, type IAgentHostExtensionInitializeResult, type IAgentHostExtensionServerCommandMap } from '../common/agentHostExtensionProtocol.js';
import type { IAgentSessionSearchResult } from '../common/agentHostSessionSearch.js';
import { supportsAgentHostSessionSemanticSearch, supportsAgentHostSessionSearch } from '../common/meta/agentHostSessionSearchMeta.js';
import { validateSessionSemanticRequest, type ISessionSemanticRequest, type ISessionSemanticResult } from '../common/sessionSemanticSearch.js';
import { AMBIENT_AGENT_HOST_AUTHORITY } from '../common/agentHostConnectionsService.js';
import { createRemoteWatchHandle, type IRemoteWatchHandle } from '../common/agentHostFileSystemProvider.js';
import { AgentSubscriptionManager, type IActiveSubscriptionInfo, type IAgentSubscription } from '../common/state/agentSubscription.js';
Expand Down Expand Up @@ -1665,6 +1668,29 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
}));
}

async searchSessionHistory(session: URI, query: string): Promise<IAgentSessionSearchResult> {
if (!supportsAgentHostSessionSearch(this._initializeResult.get())) {
throw new Error('This Agent Host does not support conversation content search. Update the host and reconnect.');
}
return this._sendExtensionRequest(SearchSessionHistoryExtensionMethod, { session: session.toString(), query });
}

async supportsSessionHistorySearch(): Promise<boolean> {
return supportsAgentHostSessionSearch(this._initializeResult.get());
}

async supportsSessionSemanticSearch(): Promise<boolean> {
return supportsAgentHostSessionSemanticSearch(this._initializeResult.get());
}

async sessionSemanticSearch(session: URI, request: ISessionSemanticRequest): Promise<ISessionSemanticResult> {
if (!supportsAgentHostSessionSemanticSearch(this._initializeResult.get())) {
throw new Error('This Agent Host does not support semantic search');
}
validateSessionSemanticRequest(request);
return this._sendExtensionRequest(SessionSemanticSearchExtensionMethod, { session: session.toString(), request });
}

private _toClientUri(uri: URI): URI {
return uri.scheme === Schemas.file ? toAgentHostUri(uri, this._connectionAuthority) : uri;
}
Expand Down
4 changes: 4 additions & 0 deletions src/vs/platform/agentHost/common/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { isEqual } from '../../../base/common/resources.js';
import { URI } from '../../../base/common/uri.js';
import type { IAgentServerToolHost } from './agentServerTools.js';
import type { AgentHostClientType } from './agentHostClientInfo.js';
import type { IAgentChatSearchResult } from './agentHostSessionSearch.js';
import type { IAgentHostClientTelemetryContext } from './agentHostTelemetry.js';
import type { ResolveSessionConfigResult, SessionConfigCompletionsResult } from './state/protocol/commands.js';
import { ProtectedResourceMetadata, type Changeset, type ChatOrigin, type ConfigSchema, type MessageAttachment, type ModelSelection, type AgentSelection, type SessionActiveClient, type ToolCallPendingConfirmationState, type ToolDefinition, ChangesSummary } from './state/protocol/state.js';
Expand Down Expand Up @@ -1201,6 +1202,9 @@ export interface IAgent {
/** Exact-chat operations: create, send, abort, mutate, restore history, release, and dispose. */
readonly chats: IAgentChats;

/** Search the persisted, user-visible history of an exact chat without materializing it. */
searchChatHistory?(chat: URI, context: IAgentChatContext, providerData: string | undefined, query: string): Promise<IAgentChatSearchResult>;

/** Re-attach an exact chat from opaque provider data without inferring its role. */
materializeChat(chat: URI, context: URI | IAgentChatContext, providerData: string | undefined): Promise<IAgentCreateChatResult | void>;

Expand Down
24 changes: 23 additions & 1 deletion src/vs/platform/agentHost/common/agentHostExtensionProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { vEnum, vObj, vOptionalProp, vString, type ValidatorType } from '../../.
import type { AgentHostDebugLogsArtifactKind, IAgentHostManagedSettingsDiagnostics, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkFetchResult } from './agentService.js';
import type { InitializeResult } from './state/protocol/common/commands.js';
import { AgentHostArtifactRemovalCapabilityMetaKey } from './meta/agentHostArtifactRemovalMeta.js';
import { AgentHostSessionSearchCapabilityMetaKey, AgentHostSessionSemanticSearchCapabilityMetaKey } from './meta/agentHostSessionSearchMeta.js';
import type { ISessionSemanticRequest, ISessionSemanticResult } from './sessionSemanticSearch.js';
import type { IAgentSessionSearchResult } from './agentHostSessionSearch.js';

export { supportsAgentHostArtifactRemoval } from './meta/agentHostArtifactRemovalMeta.js';

Expand All @@ -20,6 +23,8 @@ export const ReadAgentHostDebugLogsChunkExtensionMethod = 'vscode/readAgentHostD
export const SetAgentHostDetachedWorktreeArchivedExtensionMethod = 'vscode/setAgentHostDetachedWorktreeArchived';
export const RequestAgentHostWorkspaceTrustExtensionMethod = 'vscode/requestWorkspaceTrust';
export const RemoveSessionArtifactExtensionMethod = 'vscode/removeSessionArtifact';
export const SearchSessionHistoryExtensionMethod = 'vscode/searchSessionHistory';
export const SessionSemanticSearchExtensionMethod = 'vscode/sessionSemanticSearch';

const AgentHostChatStateFileCapabilityMetaKey = 'vscode.getAgentHostSessionStateFile.chat';
const AgentHostDetachedWorktreeCapabilityMetaKey = 'vscode.detachedWorktrees';
Expand All @@ -28,17 +33,21 @@ export interface IAgentHostExtensionInitializeResultMeta extends Record<string,
readonly [AgentHostChatStateFileCapabilityMetaKey]?: true;
readonly [AgentHostDetachedWorktreeCapabilityMetaKey]?: true;
readonly [AgentHostArtifactRemovalCapabilityMetaKey]?: true;
readonly [AgentHostSessionSearchCapabilityMetaKey]?: true;
readonly [AgentHostSessionSemanticSearchCapabilityMetaKey]?: true;
}

export interface IAgentHostExtensionInitializeResult extends InitializeResult {
readonly _meta?: IAgentHostExtensionInitializeResultMeta;
}

export function getAgentHostExtensionInitializeResultMeta(artifactRemoval = true): IAgentHostExtensionInitializeResultMeta {
export function getAgentHostExtensionInitializeResultMeta(artifactRemoval = true, sessionSearch = false, semanticSearch = false): IAgentHostExtensionInitializeResultMeta {
return {
[AgentHostChatStateFileCapabilityMetaKey]: true,
[AgentHostDetachedWorktreeCapabilityMetaKey]: true,
[AgentHostArtifactRemovalCapabilityMetaKey]: artifactRemoval ? true : undefined,
...(sessionSearch ? { [AgentHostSessionSearchCapabilityMetaKey]: true as const } : {}),
...(semanticSearch ? { [AgentHostSessionSemanticSearchCapabilityMetaKey]: true as const } : {}),
};
}

Expand All @@ -65,7 +74,20 @@ export const removeSessionArtifactParamsValidator = vObj({
artifactId: vString(),
});

export const searchSessionHistoryParamsValidator = vObj({
session: vString(),
query: vString(),
});

export interface IAgentHostExtensionCommandMap {
[SessionSemanticSearchExtensionMethod]: {
params: { session: string; request: ISessionSemanticRequest };
result: ISessionSemanticResult;
};
[SearchSessionHistoryExtensionMethod]: {
params: ValidatorType<typeof searchSessionHistoryParamsValidator>;
result: IAgentSessionSearchResult;
};
[RemoveSessionArtifactExtensionMethod]: {
params: ValidatorType<typeof removeSessionArtifactParamsValidator>;
result: void;
Expand Down
42 changes: 42 additions & 0 deletions src/vs/platform/agentHost/common/agentHostSessionSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

export const MAX_SESSION_SEARCH_QUERY_LENGTH = 512;
export const AGENT_CHAT_SEARCH_MAX_RESULTS = 20;

export interface IAgentChatSearchMatch {
readonly turnId: string;
readonly role: 'user' | 'assistant';
readonly snippet: string;
}

export interface IAgentSessionSearchMatch extends IAgentChatSearchMatch {
readonly chat: string;
}

export interface IAgentChatSearchResult {
readonly matches: IAgentChatSearchMatch[];
readonly hasMore: boolean;
}

export interface IAgentSessionSearchResult {
readonly matches: IAgentSessionSearchMatch[];
readonly hasMore: boolean;
}

export function validateSessionSearchQuery(query: string): void {
if (query.length > MAX_SESSION_SEARCH_QUERY_LENGTH) {
throw new Error('Conversation search query exceeds the maximum length');
}
if (!query.trim()) {
throw new Error('Conversation search query must not be empty');
}
}

/** Search uses literal Unicode words joined with AND; punctuation and quotes are separators, not query syntax. */
export function getAgentSessionSearchTerms(query: string): string[] {
validateSessionSearchQuery(query);
return query.match(/[\p{L}\p{N}\p{M}\p{Co}]+/gu) ?? [];
}
15 changes: 15 additions & 0 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { AgentSandboxSettingId } from '../../sandbox/common/settings.js';
import type { IActiveSubscriptionInfo, IAgentSubscription } from './state/agentSubscription.js';
import type { IRemoteWatchHandle } from './agentHostFileSystemProvider.js';
import type { IAgentHostResourceUriMapper } from './agentHostUri.js';
import type { IAgentSessionSearchResult } from './agentHostSessionSearch.js';
import type { ISessionSemanticRequest, ISessionSemanticResult } from './sessionSemanticSearch.js';
import type { IAgentHostClientTelemetryContext } from './agentHostTelemetry.js';
import type { CompletionsParams, CompletionsResult, CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult } from './state/protocol/commands.js';
import type { AutomationCapabilities, InitializeResult } from './state/protocol/common/commands.js';
Expand Down Expand Up @@ -785,6 +787,10 @@ export interface IAgentHostManagementService {
getNetworkDiagnosticsInfo(): Promise<IAgentHostNetworkDiagnosticsInfo>;
getManagedSettingsDiagnostics(): Promise<readonly IAgentHostManagedSettingsDiagnostics[]>;
diagnosticsFetch(url: string): Promise<IAgentHostNetworkFetchResult>;
supportsSessionHistorySearch(): Promise<boolean>;
supportsSessionSemanticSearch(): Promise<boolean>;
sessionSemanticSearch(session: URI, request: ISessionSemanticRequest): Promise<ISessionSemanticResult>;
searchSessionHistory(session: URI, query: string): Promise<IAgentSessionSearchResult>;
getSessionStateFile(session: URI, chat?: URI): Promise<URI | undefined>;
collectDebugLogs(session: URI | undefined, kind: AgentHostDebugLogsArtifactKind, chat?: URI): Promise<IAgentHostDebugLogsArtifact>;
readDebugLogsChunk(resource: URI, position: number): Promise<IAgentHostDebugLogsChunk>;
Expand All @@ -805,6 +811,9 @@ export const IAgentService = createDecorator<IAgentService>('agentService');
* and mutate state by dispatching actions (e.g. session/turnStarted, session/turnCancelled).
*/
export interface IAgentService {
sessionSemanticSearch?(session: URI, request: ISessionSemanticRequest): Promise<ISessionSemanticResult>;
/** Search persisted user and assistant messages without materializing any chat. */
searchSessionHistory?(session: URI, query: string): Promise<IAgentSessionSearchResult>;
readonly _serviceBrand: undefined;

/**
Expand Down Expand Up @@ -1126,6 +1135,12 @@ export interface IAgentConnection {
// ---- Session lifecycle --------------------------------------------------
authenticate(params: AuthenticateParams): Promise<AuthenticateResult>;
listSessions(): Promise<IAgentSessionMetadata[]>;
/** Resolves search support on this connection's protocol or local management transport. */
supportsSessionHistorySearch?(): Promise<boolean>;
supportsSessionSemanticSearch?(): Promise<boolean>;
sessionSemanticSearch?(session: URI, request: ISessionSemanticRequest): Promise<ISessionSemanticResult>;
/** Searches over the transport selected by this connection. */
searchSessionHistory?(session: URI, query: string): Promise<IAgentSessionSearchResult>;
createSession(config?: IAgentCreateSessionConfig): Promise<URI>;
/** Requires the VS Code artifact removal capability advertised by initialize. */
removeSessionArtifact?(session: URI, artifactId: string): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { InitializeResult } from '../state/protocol/common/commands.js';

export const AgentHostSessionSearchCapabilityMetaKey = 'vscode.searchSessionHistory';
export const AgentHostSessionSemanticSearchCapabilityMetaKey = 'vscode.sessionSemanticSearch';

/** Whether the host supports searching persisted conversation content without restoring chats. */
export function supportsAgentHostSessionSearch(result: InitializeResult | undefined): boolean {
return result?._meta?.[AgentHostSessionSearchCapabilityMetaKey] === true;
}

export function supportsAgentHostSessionSemanticSearch(result: InitializeResult | undefined): boolean {
return result?._meta?.[AgentHostSessionSemanticSearchCapabilityMetaKey] === true;
}
Loading