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
8 changes: 8 additions & 0 deletions src/vs/platform/agentHost/browser/agentHostProtocolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,8 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
_meta: config?._meta,
provider,
workingDirectories: config?.workingDirectories?.map(d => fromAgentHostUri(d).toString()),
...(config?.repositorySource !== undefined ? { repositorySource: config.repositorySource.toString() } : {}),
...(config?.repositoryRevision !== undefined ? { repositoryRevision: config.repositoryRevision } : {}),
config: config?.config,
activeClient: config?.activeClient,
progressToken: config?.progressToken,
Expand Down Expand Up @@ -1386,6 +1388,8 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
channel: ROOT_STATE_URI,
provider: params.provider,
workingDirectory: params.workingDirectory ? fromAgentHostUri(params.workingDirectory).toString() : undefined,
...(params.repositorySource !== undefined ? { repositorySource: params.repositorySource.toString() } : {}),
...(params.repositoryRevision !== undefined ? { repositoryRevision: params.repositoryRevision } : {}),
config: params.config,
});
}
Expand All @@ -1395,6 +1399,8 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
channel: ROOT_STATE_URI,
provider: params.provider,
workingDirectory: params.workingDirectory ? fromAgentHostUri(params.workingDirectory).toString() : undefined,
...(params.repositorySource !== undefined ? { repositorySource: params.repositorySource.toString() } : {}),
...(params.repositoryRevision !== undefined ? { repositoryRevision: params.repositoryRevision } : {}),
config: params.config,
property: params.property,
query: params.query,
Expand Down Expand Up @@ -1659,6 +1665,8 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
activity: s.activity,
workingDirectory: typeof s.workingDirectories?.[0] === 'string' ? this._toClientUri(URI.parse(s.workingDirectories[0])) : undefined,
workingDirectories: s.workingDirectories?.map(d => this._toClientUri(URI.parse(d))),
...(s.repositorySource !== undefined ? { repositorySource: URI.parse(s.repositorySource) } : {}),
...(s.repositoryRevision !== undefined ? { repositoryRevision: s.repositoryRevision } : {}),
changes: s.changes,
// Carry durable host provenance for sessions first materialized from a listing.
...(s._meta !== undefined ? { _meta: s._meta } : {}),
Expand Down
13 changes: 11 additions & 2 deletions src/vs/platform/agentHost/common/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ export type IAgentKnownSessionsFilter = (sessions: readonly URI[]) => Promise<Re

export interface IAgentSessionMetadata extends Omit<IAgentChatMetadata, 'chat'> {
readonly session: URI;
readonly repositorySource?: URI;
readonly repositoryRevision?: string;
}

export interface IAgentSessionProjectInfo {
Expand Down Expand Up @@ -413,6 +415,9 @@ export interface IAgentCreateSessionConfig {
* the compatibility phase callers supply exactly one directory (`[dir]`).
*/
readonly workingDirectories?: readonly URI[];
/** Requested source identity, separate from the resolved working directories. */
readonly repositorySource?: URI;
readonly repositoryRevision?: string;
readonly config?: Record<string, unknown>;
/**
* Eagerly claim the active client role for the new session. When provided,
Expand Down Expand Up @@ -829,8 +834,12 @@ export interface IAgentChatConfigCompletionsParams extends IAgentResolveChatConf
readonly query?: string;
}

export type IAgentResolveSessionConfigParams = IAgentResolveChatConfigParams;
export type IAgentSessionConfigCompletionsParams = IAgentChatConfigCompletionsParams;
export interface IAgentResolveSessionConfigParams extends IAgentResolveChatConfigParams {
readonly repositorySource?: URI;
readonly repositoryRevision?: string;
}

export interface IAgentSessionConfigCompletionsParams extends IAgentResolveSessionConfigParams, IAgentChatConfigCompletionsParams { }

/** Serializable model information from the agent host. */
export interface IAgentModelInfo {
Expand Down
55 changes: 55 additions & 0 deletions src/vs/platform/agentHost/common/agentHostRepositorySource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { URI } from '../../../base/common/uri.js';
import { localize } from '../../../nls.js';
import { RepositorySourceCapability } from './state/protocol/channels-root/state.js';
import { JsonRpcErrorCodes } from './state/protocol/errors.js';
import { ProtocolError } from './state/sessionProtocol.js';

export interface IAgentRepositorySource {
readonly repositorySource: URI;
readonly repositoryRevision?: string;
}

/** Validate typed source inputs without interpreting them as provider configuration. */
export function validateRepositorySource(
params: { readonly repositorySource?: URI | string; readonly repositoryRevision?: string; readonly config?: Readonly<Record<string, unknown>> } | undefined,
capability: RepositorySourceCapability | undefined,
): IAgentRepositorySource | undefined {
const config = params?.config;
if (config && ['repositorySource', 'repositoryRevision', 'repositoryUrl'].some(key => Object.hasOwn(config, key))) {
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('repositorySource.config', "Repository source and revision must be supplied as request fields, not configuration values."));
}
if (!params || (params.repositorySource === undefined && params.repositoryRevision === undefined)) {
return undefined;
}
if (!capability) {
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('repositorySource.unsupported', "The agent host does not support repository-backed session creation."));
}
if (params.repositorySource === undefined) {
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('repositorySource.required', "A repository revision requires a repository source."));
}
let source: URI;
const invalidSource = localize('repositorySource.invalid', "Select an absolute repository URI without credentials, a query, or a fragment.");
try {
source = typeof params.repositorySource === 'string' ? URI.parse(params.repositorySource, true) : params.repositorySource;
} catch {
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, invalidSource);
}
if (!URI.isUri(source) || !source.scheme || source.authority.includes('@') || source.query || source.fragment) {
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, invalidSource);
}
const revision = params.repositoryRevision;
if (revision !== undefined) {
if (capability.revision !== true) {
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('repositorySource.revisionUnsupported', "The agent host does not support repository revision selection."));
}
if (typeof revision !== 'string' || !revision.trim()) {
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('repositorySource.revisionInvalid', "The repository revision must be a nonempty string."));
}
}
return { repositorySource: source, ...(revision !== undefined ? { repositoryRevision: revision } : {}) };
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
fd0471d4
fa44ef3f
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export interface ListSessionsResult extends PaginatedResult {
* the full current property set (not a delta). The returned `values` contain
* server-resolved defaults to pass to `createSession`.
*
* This command MUST NOT clone or prepare a repository. Repository context
* requires the agent's `repositorySource` capability.
*
* @category Commands
* @method resolveSessionConfig
* @direction Client → Server
Expand Down Expand Up @@ -130,7 +133,11 @@ export interface ResolveSessionConfigParams extends BaseParams {
provider?: string;
/** Working directory for the session */
workingDirectory?: URI;
/** Current user-filled configuration values */
/** Credential-free source context; not a working-directory URI. */
repositorySource?: URI;
/** Requested revision; requires a source and the capability's revision option. */
repositoryRevision?: string;
/** Current user-filled configuration values; see {@link SessionConfigSchema}. */
config?: Record<string, unknown>;
}

Expand Down Expand Up @@ -195,6 +202,10 @@ export interface SessionConfigCompletionsParams extends BaseParams {
provider?: string;
/** Working directory for the session */
workingDirectory?: URI;
/** Repository context for configuration completions; this MUST NOT prepare a checkout. */
repositorySource?: URI;
/** Requested revision; requires a source and the capability's revision option. */
repositoryRevision?: string;
/** Current user-filled configuration values (provides context for the query) */
config?: Record<string, unknown>;
/** Property id from the schema to query values for */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ export interface SessionSummaryChangedParams {
* - Like all notifications this is ephemeral and is **not** replayed on
* reconnect. A client that never receives the terminal frame SHOULD expire
* the indicator after an idle timeout.
* - Completion of reported work does not establish session readiness.
* Observe session lifecycle state for the durable outcome.
*
* @category Protocol Notifications
* @method root/progress
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ export interface AgentInfo {
* @category Root State
*/
export interface AgentCapabilities {
/** The host accepts typed repository inputs for session creation and configuration queries. */
repositorySource?: RepositorySourceCapability;
/**
* The agent can host more than one concurrent chat per session. When absent,
* clients MUST NOT call `createChat` to open chats beyond the default one the
Expand All @@ -123,6 +125,15 @@ export interface AgentCapabilities {
multipleWorkingDirectories?: MultipleWorkingDirectoriesCapability;
}

/**
* Options for repository-backed session creation.
* @category Root State
*/
export interface RepositorySourceCapability {
/** When true, clients may supply an explicit repositoryRevision. */
revision?: boolean;
}

/**
* Options for the {@link AgentCapabilities.multipleChats} capability.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import type { MessageAttachment } from '../channels-chat/state.js';
* updates. The server also broadcasts a `root/sessionAdded` notification to all
* clients.
*
* Repository preparation MUST finish before `session/ready` or executing turns.
* Clients recover the outcome from session state, not progress notifications.
*
* @category Commands
* @method createSession
* @direction Client → Server
Expand Down Expand Up @@ -64,11 +67,18 @@ export interface CreateSessionParams extends BaseParams {
* and ignores the rest. Dispatch working-directory actions to change the set
* after the session has started.
*
* A non-empty list and `repositorySource` are mutually exclusive.
* A repository URI identifies the source, not a working-directory URI; one
* source may produce multiple directories.
*/
workingDirectories?: URI[];
/** Credential-free source to prepare; requires the agent's repositorySource capability. */
repositorySource?: URI;
/** Requested branch, tag, or commit; requires a source and the capability's revision option. */
repositoryRevision?: string;
/**
* Agent-specific configuration values collected via `resolveSessionConfig`.
* Keys and values correspond to the schema returned by the server.
* Session configuration values collected via `resolveSessionConfig`.
* Keys and values follow the advertised {@link SessionConfigSchema}.
*/
config?: Record<string, unknown>;
/**
Expand Down Expand Up @@ -101,6 +111,9 @@ export interface CreateSessionParams extends BaseParams {
* Disposes a session and cleans up server-side resources.
*
* The server broadcasts a `root/sessionRemoved` notification to all clients.
* Disposal MUST NOT erase a shared checkout or uncommitted user changes.
* Repository cleanup remains host-owned; ending a client's wait or subscription
* does not grant permission to delete repository data.
*
* @category Commands
* @method disposeSession
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ export interface SessionMetadata {
* chat that sets none operates against this full set.
*/
workingDirectories?: URI[];
/** Immutable requested source, separate from the host-resolved working directories. */
repositorySource?: URI;
/** Immutable requested revision, not the checkout's current HEAD. */
repositoryRevision?: string;
/**
* Lightweight summary of this session's inline annotations channel
* (`ahp-session:/<uuid>/annotations`). Surfaced so badge UI can render
Expand Down Expand Up @@ -174,7 +178,7 @@ export interface SessionState extends SessionMetadata {
* this over the session's lifetime.
*/
defaultChat?: URI;
/** Session configuration schema and current values */
/** Provider-specific session configuration schema and current values. */
config?: SessionConfigState;
/**
* Top-level customizations active in this session.
Expand Down
2 changes: 2 additions & 0 deletions src/vs/platform/agentHost/common/state/sessionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,8 @@ export function createSessionState(summary: SessionSummary): SessionState {
if (summary.activity !== undefined) { state.activity = summary.activity; }
if (summary.project !== undefined) { state.project = summary.project; }
if (summary.workingDirectories !== undefined) { state.workingDirectories = summary.workingDirectories; }
if (summary.repositorySource !== undefined) { state.repositorySource = summary.repositorySource; }
if (summary.repositoryRevision !== undefined) { state.repositoryRevision = summary.repositoryRevision; }
if (summary.annotations !== undefined) { state.annotations = summary.annotations; }
if (summary._meta !== undefined) { state._meta = summary._meta; }
return state;
Expand Down
7 changes: 7 additions & 0 deletions src/vs/platform/agentHost/node/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { FileChangeType, FileOperationResult, IFileChange, IFileService, toFileO
import { IInstantiationService } from '../../instantiation/common/instantiation.js';
import { ILogService } from '../../log/common/log.js';
import { AgentChatMigrationDeferred, AgentProvider, AgentSession, AgentSignal, IAgent, type IAgentAdoptedWorktree, IAgentChatContext, IAgentChatDataChange, IAgentChatMetadata, IAgentCreateChatOptions, IAgentCreateChatRequestOptions, IAgentCreateChatResult, IAgentCreateChatSideChatSelection, IAgentCreateChatSideChatSource, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDiscoveredChat, IAgentLegacyChat, IAgentMaterializeChatEvent, IAgentModelInfo, IAgentResolveSessionConfigParams, IAgentChatAdoptionResult, type AgentChatAdoptionReason, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSpawnChatEvent, AuthenticateParams, AuthenticateResult, SubagentChatSignal, subagentChatTitle } from '../common/agent.js';
import { validateRepositorySource } from '../common/agentHostRepositorySource.js';
import { type AgentHostDebugLogsArtifactKind, type IAgentHostDebugLogsArtifact, type IAgentHostDebugLogsChunk, IAgentHostManagedSettingsDiagnostics, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkFetchResult, IAgentService } from '../common/agentService.js';
import { ISessionDatabase, ISessionDataService, ISessionStorageAccessCounts, SESSION_ATTACHMENTS_DIRNAME } from '../common/sessionDataService.js';
import { IAgentEditAttributionService, ICancelEditAttributionFlushParams, ICommitEditAttributionFlushParams, IEditAttributionFlushResult, IPrepareEditAttributionFlushParams, IPreparedEditAttributionFlush, parseEditAttributionResource } from '../common/fileEditAttribution.js';
Expand Down Expand Up @@ -3720,11 +3721,15 @@ export class AgentService extends Disposable implements IAgentService {
modifiedAt: new Date(meta.modifiedTime).toISOString(),
...(meta.project ? { project: { uri: meta.project.uri.toString(), displayName: meta.project.displayName } } : {}),
workingDirectories: meta.workingDirectories?.map(d => d.toString()),
...(meta.repositorySource !== undefined ? { repositorySource: meta.repositorySource.toString() } : {}),
...(meta.repositoryRevision !== undefined ? { repositoryRevision: meta.repositoryRevision } : {}),
_meta: meta._meta,
};
}

async createSession(config?: IAgentCreateSessionConfig): Promise<URI> {
// This host does not advertise repository preparation.
validateRepositorySource(config, undefined);
const provider = this._providerService.resolveProvider(config?.provider);
const isEphemeral = config ? readEphemeralSessionMeta(config).isEphemeral === true : false;
if (!provider) {
Expand Down Expand Up @@ -5002,6 +5007,7 @@ export class AgentService extends Disposable implements IAgentService {
}

async resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult> {
validateRepositorySource(params, undefined);
const provider = this._providerService.resolveProvider(params.provider);
if (!provider) {
throw new Error(`No agent provider registered for: ${params.provider ?? '(none)'}`);
Expand Down Expand Up @@ -5076,6 +5082,7 @@ export class AgentService extends Disposable implements IAgentService {
}

async sessionConfigCompletions(params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult> {
validateRepositorySource(params, undefined);
// The host owns branch completions for every agent (they share the same
// git-backed branch list); all other properties stay provider-specific.
if (params.property === SessionConfigKey.Branch && this._worktree.supported) {
Expand Down
Loading
Loading