diff --git a/src/vs/platform/agentHost/browser/nullAgentHostService.ts b/src/vs/platform/agentHost/browser/nullAgentHostService.ts index c401bb3b02d..103ccaf7cc6 100644 --- a/src/vs/platform/agentHost/browser/nullAgentHostService.ts +++ b/src/vs/platform/agentHost/browser/nullAgentHostService.ts @@ -36,7 +36,7 @@ export class NullAgentHostService implements IAgentHostService { getSubscription(_kind: T, _resource: URI): IReference> { return notSupported(); } getSubscriptionUnmanaged(_kind: T, _resource: URI): IAgentSubscription | undefined { return undefined; } - dispatch(_action: SessionAction | TerminalAction | IRootConfigChangedAction): void { notSupported(); } + dispatch(_channel: string, _action: SessionAction | TerminalAction | IRootConfigChangedAction): void { notSupported(); } async restartAgentHost(): Promise { notSupported(); } async authenticate(_params: AuthenticateParams): Promise { return notSupported(); } diff --git a/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts b/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts index ffc26c3aa0e..03efe2b5899 100644 --- a/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts +++ b/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts @@ -24,7 +24,7 @@ import { agentHostAuthority, fromAgentHostUri, toAgentHostUri } from '../common/ import { AgentHostPermissionMode, IAgentHostPermissionService } from '../common/agentHostPermissionService.js'; import type { ClientNotificationMap, CommandMap, JsonRpcErrorResponse, JsonRpcRequest } from '../common/state/protocol/messages.js'; import { ActionType, type ActionEnvelope, type INotification, type IRootConfigChangedAction, type SessionAction, type TerminalAction } from '../common/state/sessionActions.js'; -import { SessionSummary, SessionStatus, ROOT_STATE_URI, StateComponents, type CustomizationRef, type RootState } from '../common/state/sessionState.js'; +import { SessionSummary, SessionStatus, ROOT_STATE_URI, StateComponents, isAhpRootChannel, type CustomizationRef, type RootState } from '../common/state/sessionState.js'; import { PROTOCOL_VERSION } from '../common/state/protocol/version/registry.js'; import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, ProtocolError, ReconnectResultType, type ProtocolMessage, type IStateSnapshot } from '../common/state/sessionProtocol.js'; import { type IVscodeUpgradeResult } from '../common/state/protocolUpgrade.js'; @@ -348,6 +348,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC } const result = await this._sendRequest('initialize', { + channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: this._clientId, initialSubscriptions: [ROOT_STATE_URI], @@ -356,7 +357,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC // Hydrate root state from the initial snapshot for (const snapshot of result.snapshots ?? []) { - if (snapshot.resource === ROOT_STATE_URI) { + if (isAhpRootChannel(snapshot.resource)) { this._subscriptionManager.handleRootSnapshot(snapshot.state as RootState, snapshot.fromSeq); } } @@ -524,9 +525,8 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC // the action instead of applying it to confirmed state. if (envelope.origin?.clientId === this._clientId && envelope.origin.clientSeq !== undefined - && !envelope.rejectionReason - && hasKey(envelope.action, { session: true })) { - this._subscriptionManager.dropPendingSessionAction(envelope.action.session, envelope.origin.clientSeq); + && !envelope.rejectionReason) { + this._subscriptionManager.dropPendingSessionAction(envelope.channel, envelope.origin.clientSeq); } if (envelope.serverSeq > maxSeq) { maxSeq = envelope.serverSeq; @@ -541,7 +541,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC } else { let maxSeq = this._serverSeq; for (const snapshot of result.snapshots) { - this._subscriptionManager.applyReconnectSnapshot(URI.parse(snapshot.resource), snapshot.state, snapshot.fromSeq); + this._subscriptionManager.applyReconnectSnapshot(snapshot.resource, snapshot.state, snapshot.fromSeq); if (snapshot.fromSeq > maxSeq) { maxSeq = snapshot.fromSeq; } @@ -584,7 +584,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC replays.push({ jsonrpc: '2.0', method: 'dispatchAction', - params: { clientSeq: entry.clientSeq, action: entry.action }, + params: { channel: entry.sessionUri, clientSeq: entry.clientSeq, action: entry.action }, }); } @@ -617,16 +617,19 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC return this._subscriptionManager.getSubscriptionUnmanaged(resource); } - dispatch(action: SessionAction | TerminalAction | IRootConfigChangedAction): void { - const seq = this._subscriptionManager.dispatchOptimistic(action); - this.dispatchAction(action, this._clientId, seq); + dispatch(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): void { + const seq = this._subscriptionManager.dispatchOptimistic(channel, action); + this.dispatchAction(channel, action, this._clientId, seq); } /** * Subscribe to state at a URI. Returns the current state snapshot. */ async subscribe(resource: URI): Promise { - const result = await this._sendRequest('subscribe', { resource: resource.toString() }); + const result = await this._sendRequest('subscribe', { channel: resource.toString() }); + if (!result.snapshot) { + throw new Error(`subscribe to ${resource.toString()} returned no snapshot`); + } return result.snapshot; } @@ -634,15 +637,15 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC * Unsubscribe from state at a URI. */ unsubscribe(resource: URI): void { - this._sendNotification('unsubscribe', { resource: resource.toString() }); + this._sendNotification('unsubscribe', { channel: resource.toString() }); } /** * Dispatch a client action to the server. Returns the clientSeq used. */ - private dispatchAction(action: SessionAction | TerminalAction | IRootConfigChangedAction, _clientId: string, clientSeq: number): void { + private dispatchAction(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction, _clientId: string, clientSeq: number): void { this._grantImplicitReadsForOutgoingAction(action); - this._sendNotification('dispatchAction', { clientSeq, action }); + this._sendNotification('dispatchAction', { channel, clientSeq, action }); } /** @@ -658,7 +661,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC this._grantImplicitReadsForCustomizations(config.activeClient.customizations); } await this._sendRequest('createSession', { - session: session.toString(), + channel: session.toString(), provider, model: config?.model, workingDirectory: config?.workingDirectory ? fromAgentHostUri(config.workingDirectory).toString() : undefined, @@ -670,6 +673,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC async resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise { return this._sendRequest('resolveSessionConfig', { + channel: ROOT_STATE_URI, provider: params.provider, workingDirectory: params.workingDirectory ? fromAgentHostUri(params.workingDirectory).toString() : undefined, config: params.config, @@ -678,6 +682,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC async sessionConfigCompletions(params: IAgentSessionConfigCompletionsParams): Promise { return this._sendRequest('sessionConfigCompletions', { + channel: ROOT_STATE_URI, provider: params.provider, workingDirectory: params.workingDirectory ? fromAgentHostUri(params.workingDirectory).toString() : undefined, config: params.config, @@ -699,7 +704,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC * connection closes before a response arrives. */ async ping(): Promise { - await this._sendRequest('ping', {}); + await this._sendRequest('ping', { channel: ROOT_STATE_URI }); } /** @@ -714,7 +719,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC * Authenticate with the remote agent host using a specific scheme. */ async authenticate(params: AuthenticateParams): Promise { - await this._sendRequest('authenticate', params); + await this._sendRequest('authenticate', { channel: ROOT_STATE_URI, ...params }); return { authenticated: true }; } @@ -729,7 +734,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC * Dispose a session on the remote agent host. */ async disposeSession(session: URI): Promise { - await this._sendRequest('disposeSession', { session: session.toString() }); + await this._sendRequest('disposeSession', { channel: session.toString() }); } /** @@ -743,14 +748,14 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC * Dispose a terminal on the remote agent host. */ async disposeTerminal(terminal: URI): Promise { - await this._sendRequest('disposeTerminal', { terminal: terminal.toString() }); + await this._sendRequest('disposeTerminal', { channel: terminal.toString() }); } /** * List all sessions from the remote agent host. */ async listSessions(): Promise { - const result = await this._sendRequest('listSessions', {}); + const result = await this._sendRequest('listSessions', { channel: ROOT_STATE_URI }); return result.items.map((s: SessionSummary) => ({ session: URI.parse(s.resource), startTime: s.createdAt, @@ -816,14 +821,14 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC * List the contents of a directory on the remote host's filesystem. */ async resourceList(uri: URI): Promise { - return await this._sendRequest('resourceList', { uri: uri.toString() }); + return await this._sendRequest('resourceList', { channel: ROOT_STATE_URI, uri: uri.toString() }); } /** * Read the content of a resource on the remote host. */ async resourceRead(uri: URI): Promise { - return this._sendRequest('resourceRead', { uri: uri.toString() }); + return this._sendRequest('resourceRead', { channel: ROOT_STATE_URI, uri: uri.toString() }); } async resourceWrite(params: CommandMap['resourceWrite']['params']): Promise { @@ -898,10 +903,16 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC this._onDidAction.fire(envelope); break; } - case 'notification': { - const notification = msg.params.notification; - this._logService.trace(`[RemoteAgentHostProtocol] Notification: ${notification.type}`); - this._onDidNotification.fire(notification); + case 'root/sessionAdded': + case 'root/sessionRemoved': + case 'root/sessionSummaryChanged': + case 'auth/required': { + this._logService.trace(`[RemoteAgentHostProtocol] Notification: ${msg.method}`); + // The case narrows `msg.method` to a single literal; the matching params + // shape is paired with that literal by the {@link ServerNotificationMap} + // definition, so spreading is safe. + // eslint-disable-next-line local/code-no-dangerous-type-assertions + this._onDidNotification.fire({ type: msg.method, ...msg.params } as INotification); break; } default: @@ -1027,7 +1038,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC case 'resourceList': { if (!p.uri) { sendError(new Error('Missing uri')); return; } const uri = URI.parse(p.uri as string); - return void gateAndHandle(uri, AgentHostPermissionMode.Read, { uri: uri.toString(), read: true }, async () => { + return void gateAndHandle(uri, AgentHostPermissionMode.Read, { channel: ROOT_STATE_URI, uri: uri.toString(), read: true }, async () => { const stat = await this._fileService.resolve(uri); return { entries: (stat.children ?? []).map(c => ({ name: c.name, type: c.isDirectory ? 'directory' as const : 'file' as const })) }; }); @@ -1035,7 +1046,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC case 'resourceRead': { if (!p.uri) { sendError(new Error('Missing uri')); return; } const uri = URI.parse(p.uri as string); - return void gateAndHandle(uri, AgentHostPermissionMode.Read, { uri: uri.toString(), read: true }, async () => { + return void gateAndHandle(uri, AgentHostPermissionMode.Read, { channel: ROOT_STATE_URI, uri: uri.toString(), read: true }, async () => { const content = await this._fileService.readFile(uri); return { data: encodeBase64(content.value), encoding: ContentEncoding.Base64 }; }); @@ -1043,7 +1054,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC case 'resourceWrite': { if (!p.uri || !p.data) { sendError(new Error('Missing uri or data')); return; } const writeUri = URI.parse(p.uri as string); - return void gateAndHandle(writeUri, AgentHostPermissionMode.Write, { uri: writeUri.toString(), write: true }, async () => { + return void gateAndHandle(writeUri, AgentHostPermissionMode.Write, { channel: ROOT_STATE_URI, uri: writeUri.toString(), write: true }, async () => { const buf = p.encoding === ContentEncoding.Base64 ? decodeBase64(p.data as string) : VSBuffer.fromString(p.data as string); @@ -1058,7 +1069,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC case 'resourceDelete': { if (!p.uri) { sendError(new Error('Missing uri')); return; } const deleteUri = URI.parse(p.uri as string); - return void gateAndHandle(deleteUri, AgentHostPermissionMode.Write, { uri: deleteUri.toString(), write: true }, () => + return void gateAndHandle(deleteUri, AgentHostPermissionMode.Write, { channel: ROOT_STATE_URI, uri: deleteUri.toString(), write: true }, () => this._fileService.del(deleteUri, { recursive: !!p.recursive }).then(() => ({}))); } case 'resourceMove': { @@ -1072,11 +1083,11 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC this._permissionService.check(this._address, destUri, AgentHostPermissionMode.Write), ]); if (!sourceOk) { - sendPermissionDenied({ uri: sourceUri.toString(), write: true }); + sendPermissionDenied({ channel: ROOT_STATE_URI, uri: sourceUri.toString(), write: true }); return; } if (!destOk) { - sendPermissionDenied({ uri: destUri.toString(), write: true }); + sendPermissionDenied({ channel: ROOT_STATE_URI, uri: destUri.toString(), write: true }); return; } await this._fileService.move(sourceUri, destUri, !p.failIfExists); @@ -1135,7 +1146,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC } private _updateTelemetryLevel(): void { - this.dispatchAction({ + this.dispatchAction(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, config: { [AgentHostTelemetryLevelConfigKey]: telemetryLevelToAgentHostConfigValue(getTelemetryLevel(this._configurationService)) }, }, this._clientId, 0); diff --git a/src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts b/src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts index 3ebdccb01bb..e082c525858 100644 --- a/src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts +++ b/src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts @@ -14,6 +14,7 @@ import { type IAgentConnection } from './agentService.js'; import { ContentEncoding, type DirectoryEntry, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceRequestParams, type ResourceRequestResult, type ResourceWriteParams, type ResourceWriteResult } from './state/protocol/commands.js'; import { AhpErrorCodes } from './state/protocol/errors.js'; import { ProtocolError } from './state/sessionProtocol.js'; +import { ROOT_STATE_URI } from './state/sessionState.js'; /** * Interface for performing resource operations on a remote endpoint. @@ -149,6 +150,7 @@ export abstract class AHPFileSystemProvider extends Disposable implements IFileS try { const originalUri = this._decodeUri(resource); await connection.resourceWrite({ + channel: ROOT_STATE_URI, uri: originalUri.toString(), data: VSBuffer.wrap(content).toString(), encoding: ContentEncoding.Utf8, @@ -166,7 +168,7 @@ export abstract class AHPFileSystemProvider extends Disposable implements IFileS const connection = this._getConnection(resource.authority); try { const originalUri = this._decodeUri(resource); - await connection.resourceDelete({ uri: originalUri.toString(), recursive: opts.recursive }); + await connection.resourceDelete({ channel: ROOT_STATE_URI, uri: originalUri.toString(), recursive: opts.recursive }); } catch (err) { throw this._mapError(err, FileSystemProviderErrorCode.NoPermissions); } @@ -177,7 +179,7 @@ export abstract class AHPFileSystemProvider extends Disposable implements IFileS try { const originalFrom = this._decodeUri(from); const originalTo = this._decodeUri(to); - await connection.resourceMove({ source: originalFrom.toString(), destination: originalTo.toString(), failIfExists: !opts.overwrite }); + await connection.resourceMove({ channel: ROOT_STATE_URI, source: originalFrom.toString(), destination: originalTo.toString(), failIfExists: !opts.overwrite }); } catch (err) { throw this._mapError(err, FileSystemProviderErrorCode.NoPermissions); } @@ -202,6 +204,7 @@ export abstract class AHPFileSystemProvider extends Disposable implements IFileS const originalUri = this._decodeUri(resource); try { await connection.resourceRequest({ + channel: ROOT_STATE_URI, uri: originalUri.toString(), read: opts.read, write: opts.write, diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 637b6542646..6a188b838a4 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -802,8 +802,14 @@ export interface IAgentService { * Dispatch a client-originated action to the server. The server applies * it to state, triggers side effects, and echoes it back via * {@link onDidAction} with the client's origin for reconciliation. + * + * `channel` is the protocol URI string identifying the channel the action + * targets (a session URI for session actions, terminal URI for terminal + * actions, or {@link ROOT_STATE_URI} for root actions). Strings are used + * rather than {@link URI} objects so that authority-less scheme URIs + * like `ahp-root://` survive the wire format without normalization. */ - dispatchAction(action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void; + dispatchAction(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void; /** * List the contents of a directory on the agent host's filesystem. @@ -856,7 +862,15 @@ export interface IAgentConnection { getSubscriptionUnmanaged(kind: T, resource: URI): IAgentSubscription | undefined; // ---- Action dispatch ---------------------------------------------------- - dispatch(action: SessionAction | TerminalAction | IRootConfigChangedAction): void; + /** + * Dispatch a client-originated action. `channel` is the protocol URI + * string identifying the channel the action targets (a session URI for + * session actions, terminal URI for terminal actions, or + * `ROOT_STATE_URI` for root-config actions). Strings are used rather + * than {@link URI} objects so authority-less scheme URIs like + * `ahp-root://` survive the wire format without normalization. + */ + dispatch(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): void; // ---- Events (connection-level) ------------------------------------------ readonly onDidNotification: Event; diff --git a/src/vs/platform/agentHost/common/state/agentSubscription.ts b/src/vs/platform/agentHost/common/state/agentSubscription.ts index 9b20f1109e6..f402af91de8 100644 --- a/src/vs/platform/agentHost/common/state/agentSubscription.ts +++ b/src/vs/platform/agentHost/common/state/agentSubscription.ts @@ -15,7 +15,7 @@ import { terminalReducer } from './protocol/reducers.js'; import type { RootAction, SessionAction as IProtocolSessionAction, TerminalAction } from './protocol/action-origin.generated.js'; import type { ChangesetState, RootState, SessionState, TerminalState } from './protocol/state.js'; import type { IStateSnapshot } from './sessionProtocol.js'; -import { ROOT_STATE_URI, StateComponents } from './sessionState.js'; +import { isAhpRootChannel, ROOT_STATE_URI, StateComponents } from './sessionState.js'; // --- Public API -------------------------------------------------------------- @@ -116,10 +116,10 @@ abstract class BaseAgentSubscription extends Disposable implements IAgentSubs /** * Process an incoming action envelope. The subscription determines - * whether the action is relevant via {@link _isRelevantAction}. + * whether the action is relevant via {@link _isRelevantEnvelope}. */ receiveEnvelope(envelope: ActionEnvelope): void { - if (!this._isRelevantAction(envelope.action)) { + if (!this._isRelevantEnvelope(envelope)) { return; } @@ -144,8 +144,8 @@ abstract class BaseAgentSubscription extends Disposable implements IAgentSubs /** Apply the reducer to confirmed state. Subclasses must implement. */ protected abstract _applyReducer(state: T, action: StateAction): T; - /** Whether the given action targets this subscription. */ - protected abstract _isRelevantAction(action: StateAction): boolean; + /** Whether the given envelope targets this subscription. */ + protected abstract _isRelevantEnvelope(envelope: ActionEnvelope): boolean; /** Return optimistic state if write-ahead is active, otherwise `undefined`. */ protected _getOptimisticState(): T | undefined { @@ -190,8 +190,8 @@ export class RootStateSubscription extends BaseAgentSubscription { return rootReducer(state, action as RootAction, this._log); } - protected override _isRelevantAction(action: StateAction): boolean { - return action.type.startsWith('root/'); + protected override _isRelevantEnvelope(envelope: ActionEnvelope): boolean { + return isAhpRootChannel(envelope.channel) && envelope.action.type.startsWith('root/'); } } @@ -253,8 +253,8 @@ export class SessionStateSubscription extends BaseAgentSubscription; /** When `true`, replaces all config values instead of merging */ @@ -804,8 +765,6 @@ export interface SessionConfigChangedAction { */ export interface SessionMetaChangedAction { type: ActionType.SessionMetaChanged; - /** Session URI */ - session: URI; /** New `_meta` payload, or `undefined` to clear it */ _meta: Record | undefined; } @@ -829,8 +788,6 @@ export interface SessionMetaChangedAction { */ export interface SessionTruncatedAction { type: ActionType.SessionTruncated; - /** Session URI */ - session: URI; /** Keep turns up to and including this turn. Omit to clear all turns. */ turnId?: string; } @@ -852,8 +809,6 @@ export interface SessionTruncatedAction { */ export interface SessionPendingMessageSetAction { type: ActionType.SessionPendingMessageSet; - /** Session URI */ - session: URI; /** Whether this is a steering or queued message */ kind: PendingMessageKind; /** Unique identifier for this pending message */ @@ -875,8 +830,6 @@ export interface SessionPendingMessageSetAction { */ export interface SessionPendingMessageRemovedAction { type: ActionType.SessionPendingMessageRemoved; - /** Session URI */ - session: URI; /** Whether this is a steering or queued message */ kind: PendingMessageKind; /** Identifier of the pending message to remove */ @@ -898,8 +851,6 @@ export interface SessionPendingMessageRemovedAction { */ export interface SessionQueuedMessagesReorderedAction { type: ActionType.SessionQueuedMessagesReordered; - /** Session URI */ - session: URI; /** Queued message IDs in the desired order */ order: string[]; } @@ -918,8 +869,6 @@ export interface SessionQueuedMessagesReorderedAction { */ export interface SessionInputRequestedAction { type: ActionType.SessionInputRequested; - /** Session URI */ - session: URI; /** Input request to create or replace */ request: SessionInputRequest; } @@ -935,8 +884,6 @@ export interface SessionInputRequestedAction { */ export interface SessionInputAnswerChangedAction { type: ActionType.SessionInputAnswerChanged; - /** Session URI */ - session: URI; /** Input request identifier */ requestId: string; /** Question identifier within the input request */ @@ -957,8 +904,6 @@ export interface SessionInputAnswerChangedAction { */ export interface SessionInputCompletedAction { type: ActionType.SessionInputCompleted; - /** Session URI */ - session: URI; /** Input request identifier */ requestId: string; /** Completion outcome */ @@ -986,8 +931,6 @@ export interface SessionInputCompletedAction { */ export interface TerminalDataAction { type: ActionType.TerminalData; - /** Terminal URI */ - terminal: URI; /** Output data (may contain ANSI escape sequences) */ data: string; } @@ -1007,8 +950,6 @@ export interface TerminalDataAction { */ export interface TerminalInputAction { type: ActionType.TerminalInput; - /** Terminal URI */ - terminal: URI; /** Input data to send to the pty */ data: string; } @@ -1025,8 +966,6 @@ export interface TerminalInputAction { */ export interface TerminalResizedAction { type: ActionType.TerminalResized; - /** Terminal URI */ - terminal: URI; /** Terminal width in columns */ cols: number; /** Terminal height in rows */ @@ -1045,8 +984,6 @@ export interface TerminalResizedAction { */ export interface TerminalClaimedAction { type: ActionType.TerminalClaimed; - /** Terminal URI */ - terminal: URI; /** The new claim */ claim: TerminalClaim; } @@ -1063,8 +1000,6 @@ export interface TerminalClaimedAction { */ export interface TerminalTitleChangedAction { type: ActionType.TerminalTitleChanged; - /** Terminal URI */ - terminal: URI; /** New terminal title */ title: string; } @@ -1077,8 +1012,6 @@ export interface TerminalTitleChangedAction { */ export interface TerminalCwdChangedAction { type: ActionType.TerminalCwdChanged; - /** Terminal URI */ - terminal: URI; /** New working directory */ cwd: URI; } @@ -1091,8 +1024,6 @@ export interface TerminalCwdChangedAction { */ export interface TerminalExitedAction { type: ActionType.TerminalExited; - /** Terminal URI */ - terminal: URI; /** Process exit code. `undefined` if the process was killed without an exit code. */ exitCode?: number; } @@ -1106,8 +1037,6 @@ export interface TerminalExitedAction { */ export interface TerminalClearedAction { type: ActionType.TerminalCleared; - /** Terminal URI */ - terminal: URI; } /** @@ -1123,8 +1052,6 @@ export interface TerminalClearedAction { */ export interface TerminalCommandDetectionAvailableAction { type: ActionType.TerminalCommandDetectionAvailable; - /** Terminal URI */ - terminal: URI; } /** @@ -1137,8 +1064,6 @@ export interface TerminalCommandDetectionAvailableAction { */ export interface TerminalCommandExecutedAction { type: ActionType.TerminalCommandExecuted; - /** Terminal URI */ - terminal: URI; /** * Stable identifier for this command, scoped to the terminal URI. * Allows correlating `commandExecuted` → `commandFinished` pairs. @@ -1165,8 +1090,6 @@ export interface TerminalCommandExecutedAction { */ export interface TerminalCommandFinishedAction { type: ActionType.TerminalCommandFinished; - /** Terminal URI */ - terminal: URI; /** Matches the `commandId` from the corresponding `commandExecuted` */ commandId: string; /** Shell exit code. `undefined` if the shell did not report one. */ @@ -1190,8 +1113,6 @@ export interface TerminalCommandFinishedAction { */ export interface ChangesetStatusChangedAction { type: ActionType.ChangesetStatusChanged; - /** Expanded changeset URI (matches the URI the client subscribed to). */ - changeset: URI; /** New computation lifecycle status. */ status: ChangesetStatus; /** Cause when `status === ChangesetStatus.Error`; otherwise omitted. */ @@ -1207,8 +1128,6 @@ export interface ChangesetStatusChangedAction { */ export interface ChangesetFileSetAction { type: ActionType.ChangesetFileSet; - /** Expanded changeset URI. */ - changeset: URI; /** The new or replacement file entry. */ file: ChangesetFile; } @@ -1224,8 +1143,6 @@ export interface ChangesetFileSetAction { */ export interface ChangesetFileRemovedAction { type: ActionType.ChangesetFileRemoved; - /** Expanded changeset URI. */ - changeset: URI; /** The {@link ChangesetFile.id} of the file to remove. */ fileId: string; } @@ -1240,8 +1157,6 @@ export interface ChangesetFileRemovedAction { */ export interface ChangesetOperationsChangedAction { type: ActionType.ChangesetOperationsChanged; - /** Expanded changeset URI. */ - changeset: URI; /** Updated operation list. Pass `undefined` to clear all operations. */ operations: ChangesetOperation[] | undefined; } @@ -1260,15 +1175,13 @@ export interface ChangesetOperationsChangedAction { * Clients SHOULD release any references on receipt and SHOULD NOT * distinguish the two cases from the action alone — instead, react to * the corresponding session-level lifecycle signal (e.g. - * `notify/sessionRemoved`) for the "going away" case. + * `root/sessionRemoved`) for the "going away" case. * * @category Changeset Actions * @version 2 */ export interface ChangesetClearedAction { type: ActionType.ChangesetCleared; - /** Expanded changeset URI. */ - changeset: URI; } // ─── Discriminated Union ───────────────────────────────────────────────────── diff --git a/src/vs/platform/agentHost/common/state/protocol/commands.ts b/src/vs/platform/agentHost/common/state/protocol/commands.ts index 2ce9c49d1e0..bcace0ac031 100644 --- a/src/vs/platform/agentHost/common/state/protocol/commands.ts +++ b/src/vs/platform/agentHost/common/state/protocol/commands.ts @@ -11,6 +11,32 @@ import type { ActionEnvelope, StateAction } from './actions.js'; export type { ConfigPropertySchema, ConfigSchema, SessionConfigPropertySchema, SessionConfigSchema } from './state.js'; +// ─── BaseParams ────────────────────────────────────────────────────────────── + +/** + * Base shape every command's params extends. + * + * `channel` identifies the channel the command targets, mirroring the + * `channel` field on every protocol notification. For commands that operate + * on a specific channel (a session, terminal, or changeset), `channel` is + * that channel's URI. For commands that are connection-level rather than + * channel-scoped (e.g. {@link InitializeParams | `initialize`}, + * {@link PingParams | `ping`}, {@link ListSessionsParams | `listSessions`}, + * the `resource*` filesystem commands, and {@link AuthenticateParams | + * `authenticate`}), the params type narrows `channel` to the literal + * root URI `'ahp-root://'`. + * + * This invariant lets implementations route every incoming message — + * request, response, or notification — by inspecting `params.channel` + * without needing to know the per-method param shape. + * + * @category Commands + */ +export interface BaseParams { + /** Channel URI this command targets. */ + channel: URI; +} + // ─── initialize ────────────────────────────────────────────────────────────── /** @@ -24,7 +50,8 @@ export type { ConfigPropertySchema, ConfigSchema, SessionConfigPropertySchema, S * @version 1 * @see {@link /specification/lifecycle | Lifecycle} for the full handshake flow. */ -export interface InitializeParams { +export interface InitializeParams extends BaseParams { + channel: 'ahp-root://'; /** * Protocol versions the client is willing to speak, ordered from most * preferred to least preferred. Each entry is a [SemVer](https://semver.org) @@ -94,7 +121,8 @@ export interface InitializeResult { * @messageType Request * @version 0.1.0 */ -export interface PingParams { +export interface PingParams extends BaseParams { + channel: 'ahp-root://'; } // ─── reconnect ─────────────────────────────────────────────────────────────── @@ -120,7 +148,8 @@ export const enum ReconnectResultType { * @version 1 * @see {@link /specification/lifecycle | Lifecycle} for details. */ -export interface ReconnectParams { +export interface ReconnectParams extends BaseParams { + channel: 'ahp-root://'; /** Client identifier from the original connection */ clientId: string; /** Last `serverSeq` the client received */ @@ -164,7 +193,12 @@ export type ReconnectResult = ReconnectReplayResult | ReconnectSnapshotResult; // ─── subscribe ─────────────────────────────────────────────────────────────── /** - * Subscribe to a URI-identified state resource. + * Subscribe to a URI-identified channel. + * + * A channel MAY have state associated with it (e.g. root, sessions, + * terminals) or be stateless (pure pub/sub for streaming data). For + * state-bearing channels the result includes a snapshot; for stateless + * channels `snapshot` is omitted. * * @category Commands * @method subscribe @@ -173,17 +207,17 @@ export type ReconnectResult = ReconnectReplayResult | ReconnectSnapshotResult; * @version 1 * @see {@link /specification/subscriptions | Subscriptions} */ -export interface SubscribeParams { - /** URI to subscribe to */ - resource: URI; -} +export interface SubscribeParams extends BaseParams { } /** * Result of the `subscribe` command. + * + * `snapshot` is present when the subscribed channel has associated state, and + * absent for stateless channels. */ export interface SubscribeResult { - /** Snapshot of the subscribed resource */ - snapshot: Snapshot; + /** Snapshot of the subscribed channel's state (omitted for stateless channels) */ + snapshot?: Snapshot; } // ─── createSession ─────────────────────────────────────────────────────────── @@ -195,7 +229,7 @@ export interface SubscribeResult { * `-32003` (`SessionAlreadyExists`). * * After creation, the client should subscribe to the session URI to receive state - * updates. The server also broadcasts a `notify/sessionAdded` notification to all + * updates. The server also broadcasts a `root/sessionAdded` notification to all * clients. * * @category Commands @@ -207,7 +241,7 @@ export interface SubscribeResult { * ```jsonc * // Client → Server * { "jsonrpc": "2.0", "id": 2, "method": "createSession", - * "params": { "session": "copilot:/", "provider": "copilot", "model": "gpt-4o" } } + * "params": { "channel": "ahp-session:/", "provider": "copilot", "model": "gpt-4o" } } * * // Server → Client (success) * { "jsonrpc": "2.0", "id": 2, "result": null } @@ -233,9 +267,9 @@ export interface SessionForkSource { turnId: string; } -export interface CreateSessionParams { - /** Session URI (client-chosen, e.g. `copilot:/`) */ - session: URI; +export interface CreateSessionParams extends BaseParams { + /** Session URI (client-chosen, e.g. `ahp-session:/`) */ + channel: URI; /** Agent provider ID */ provider?: string; /** Model selection (ID and optional model-specific configuration) */ @@ -268,7 +302,7 @@ export interface CreateSessionParams { /** * Disposes a session and cleans up server-side resources. * - * The server broadcasts a `notify/sessionRemoved` notification to all clients. + * The server broadcasts a `root/sessionRemoved` notification to all clients. * * @category Commands * @method disposeSession @@ -276,10 +310,7 @@ export interface CreateSessionParams { * @messageType Request * @version 1 */ -export interface DisposeSessionParams { - /** Session URI to dispose */ - session: URI; -} +export interface DisposeSessionParams extends BaseParams { } // ─── createTerminal ────────────────────────────────────────────────────────── @@ -296,9 +327,9 @@ export interface DisposeSessionParams { * @messageType Request * @version 1 */ -export interface CreateTerminalParams { - /** Terminal URI (client-chosen) */ - terminal: URI; +export interface CreateTerminalParams extends BaseParams { + /** Terminal URI (client-chosen). */ + channel: URI; /** Initial owner of the terminal */ claim: TerminalClaim; /** Human-readable terminal name */ @@ -325,10 +356,7 @@ export interface CreateTerminalParams { * @messageType Request * @version 1 */ -export interface DisposeTerminalParams { - /** Terminal URI to dispose */ - terminal: URI; -} +export interface DisposeTerminalParams extends BaseParams { } // ─── listSessions ──────────────────────────────────────────────────────────── @@ -337,7 +365,7 @@ export interface DisposeTerminalParams { * * The session list is **not** part of the state tree because it can be arbitrarily * large. Clients fetch it imperatively and maintain a local cache updated by - * `notify/sessionAdded` and `notify/sessionRemoved` notifications. + * `root/sessionAdded` and `root/sessionRemoved` notifications. * * @category Commands * @method listSessions @@ -345,7 +373,8 @@ export interface DisposeTerminalParams { * @messageType Request * @version 1 */ -export interface ListSessionsParams { +export interface ListSessionsParams extends BaseParams { + channel: 'ahp-root://'; /** Optional filter criteria */ filter?: object; } @@ -388,7 +417,7 @@ export const enum ContentEncoding { * ```jsonc * // Client → Server * { "jsonrpc": "2.0", "id": 10, "method": "resourceRead", - * "params": { "uri": "copilot://content/img-1" } } + * "params": { "uri": "ahp-session://content/img-1" } } * * // Server → Client * { "jsonrpc": "2.0", "id": 10, "result": { @@ -398,7 +427,8 @@ export const enum ContentEncoding { * }} * ``` */ -export interface ResourceReadParams { +export interface ResourceReadParams extends BaseParams { + channel: 'ahp-root://'; /** Content URI from a `ContentRef` */ uri: string; /** Preferred encoding for the returned data (default: server-chosen) */ @@ -451,7 +481,8 @@ export interface ResourceReadResult { * { "jsonrpc": "2.0", "id": 11, "result": {} } * ``` */ -export interface ResourceWriteParams { +export interface ResourceWriteParams extends BaseParams { + channel: 'ahp-root://'; /** Target file URI on the server filesystem */ uri: URI; /** Content encoded as a string */ @@ -495,7 +526,8 @@ export interface ResourceWriteResult { * @throws `NotFound` (`-32008`) if the directory does not exist. * @throws `PermissionDenied` (`-32009`) if the client is not permitted to browse the directory. */ -export interface ResourceListParams { +export interface ResourceListParams extends BaseParams { + channel: 'ahp-root://'; /** Directory URI on the server filesystem */ uri: URI; } @@ -533,7 +565,7 @@ export interface ResourceListResult { * ```jsonc * // Client → Server (fetch the 20 most recent turns) * { "jsonrpc": "2.0", "id": 8, "method": "fetchTurns", - * "params": { "session": "copilot:/", "limit": 20 } } + * "params": { "channel": "ahp-session:/", "limit": 20 } } * * // Server → Client * { "jsonrpc": "2.0", "id": 8, "result": { @@ -543,12 +575,12 @@ export interface ResourceListResult { * * // Client → Server (fetch 20 turns before t1) * { "jsonrpc": "2.0", "id": 9, "method": "fetchTurns", - * "params": { "session": "copilot:/", "before": "t1", "limit": 20 } } + * "params": { "channel": "ahp-session:/", "before": "t1", "limit": 20 } } * ``` */ -export interface FetchTurnsParams { +export interface FetchTurnsParams extends BaseParams { /** Session URI */ - session: URI; + channel: URI; /** Turn ID to fetch before (exclusive). Omit to fetch from the most recent turn. */ before?: string; /** Maximum number of turns to return. Server MAY impose its own upper bound. */ @@ -568,7 +600,7 @@ export interface FetchTurnsResult { // ─── unsubscribe ───────────────────────────────────────────────────────────── /** - * Stop receiving updates for a URI. + * Stop receiving updates for a channel. * * @category Commands * @method unsubscribe @@ -578,15 +610,20 @@ export interface FetchTurnsResult { * @see {@link /specification/subscriptions | Subscriptions} */ export interface UnsubscribeParams { - /** URI to unsubscribe from */ - resource: URI; + /** Channel URI to unsubscribe from */ + channel: URI; } // ─── dispatchAction ────────────────────────────────────────────────────────── /** * Fire-and-forget action dispatch (write-ahead). The client applies actions - * optimistically to local state. + * optimistically to local state and the server echoes them back as an + * {@link ActionEnvelope} once accepted. + * + * The client → server method is named `dispatchAction`; the server's reply + * arrives on the server → client `action` notification (params: + * {@link ActionEnvelope}). * * @category Commands * @method dispatchAction @@ -596,6 +633,8 @@ export interface UnsubscribeParams { * @see {@link /guide/actions | Actions} for the full list of client-dispatchable actions. */ export interface DispatchActionParams { + /** Channel URI this action targets */ + channel: URI; /** Client sequence number */ clientSeq: number; /** The action to dispatch */ @@ -619,7 +658,8 @@ export interface DispatchActionParams { * @throws `PermissionDenied` (`-32009`) if the client is not permitted to read the source or write to the destination. * @throws `AlreadyExists` (`-32010`) if `failIfExists` is set and the destination already exists. */ -export interface ResourceCopyParams { +export interface ResourceCopyParams extends BaseParams { + channel: 'ahp-root://'; /** Source URI to copy from */ source: URI; /** Destination URI to copy to */ @@ -652,7 +692,8 @@ export interface ResourceCopyResult { * @throws `NotFound` (`-32008`) if the resource does not exist. * @throws `PermissionDenied` (`-32009`) if the client is not permitted to delete the resource. */ -export interface ResourceDeleteParams { +export interface ResourceDeleteParams extends BaseParams { + channel: 'ahp-root://'; /** URI of the resource to delete */ uri: URI; /** @@ -702,7 +743,8 @@ export interface ResourceDeleteResult { * @version 1 * @throws `PermissionDenied` (`-32009`) if access is denied. */ -export interface ResourceRequestParams { +export interface ResourceRequestParams extends BaseParams { + channel: 'ahp-root://'; /** * Resource URI being requested. Typically a `file:` URI on the receiver's * filesystem, but any URI scheme that the receiver mediates access to is @@ -740,7 +782,8 @@ export interface ResourceRequestResult { * @throws `PermissionDenied` (`-32009`) if the client is not permitted to move the resource. * @throws `AlreadyExists` (`-32010`) if `failIfExists` is set and the destination already exists. */ -export interface ResourceMoveParams { +export interface ResourceMoveParams extends BaseParams { + channel: 'ahp-root://'; /** Source URI to move from */ source: URI; /** Destination URI to move to */ @@ -782,7 +825,7 @@ export interface ResourceMoveResult { * ```jsonc * // Client → Server * { "jsonrpc": "2.0", "id": 3, "method": "authenticate", - * "params": { "resource": "https://api.github.com", "token": "gho_xxxx" } } + * "params": { "channel": "ahp-root://", "resource": "https://api.github.com", "token": "gho_xxxx" } } * * // Server → Client (success) * { "jsonrpc": "2.0", "id": 3, "result": {} } @@ -791,7 +834,8 @@ export interface ResourceMoveResult { * { "jsonrpc": "2.0", "id": 3, "error": { "code": -32007, "message": "Invalid token" } } * ``` */ -export interface AuthenticateParams { +export interface AuthenticateParams extends BaseParams { + channel: 'ahp-root://'; /** * The protected resource identifier. MUST match a `resource` value from * `ProtectedResourceMetadata` declared in `AgentInfo.protectedResources`. @@ -869,7 +913,8 @@ export interface AuthenticateResult { * }} * ``` */ -export interface ResolveSessionConfigParams { +export interface ResolveSessionConfigParams extends BaseParams { + channel: 'ahp-root://'; /** Agent provider ID */ provider?: string; /** Working directory for the session */ @@ -933,7 +978,8 @@ export interface SessionConfigValueItem { * }} * ``` */ -export interface SessionConfigCompletionsParams { +export interface SessionConfigCompletionsParams extends BaseParams { + channel: 'ahp-root://'; /** Agent provider ID */ provider?: string; /** Working directory for the session */ @@ -989,7 +1035,7 @@ export const enum CompletionItemKind { * // User has typed "look at @foo" and the cursor is just after "@foo". * // Client → Server * { "jsonrpc": "2.0", "id": 12, "method": "completions", - * "params": { "kind": "userMessage", "session": "copilot:/", + * "params": { "kind": "userMessage", "channel": "ahp-session:/", * "text": "look at @foo", "offset": 12 } } * * // Server → Client @@ -1010,11 +1056,11 @@ export const enum CompletionItemKind { * }} * ``` */ -export interface CompletionsParams { +export interface CompletionsParams extends BaseParams { /** What kind of completion is being requested. */ kind: CompletionItemKind; /** The session URI the completion is being requested for. */ - session: URI; + channel: URI; /** * The complete text of the input being completed (e.g. the full user * message text typed so far). @@ -1142,9 +1188,9 @@ export interface ChangesetOperationFollowUp { * @messageType Request * @version 2 */ -export interface InvokeChangesetOperationParams { +export interface InvokeChangesetOperationParams extends BaseParams { /** The expanded changeset URI. */ - changeset: URI; + channel: URI; /** Matches {@link ChangesetOperation.id} from the changeset's `operations` list. */ operationId: string; /** diff --git a/src/vs/platform/agentHost/common/state/protocol/messages.ts b/src/vs/platform/agentHost/common/state/protocol/messages.ts index a4141db7b20..5712b8d94cd 100644 --- a/src/vs/platform/agentHost/common/state/protocol/messages.ts +++ b/src/vs/platform/agentHost/common/state/protocol/messages.ts @@ -9,7 +9,7 @@ import type { InitializeParams, InitializeResult, PingParams, ReconnectParams, ReconnectResult, SubscribeParams, SubscribeResult, CreateSessionParams, DisposeSessionParams, CreateTerminalParams, DisposeTerminalParams, ListSessionsParams, ListSessionsResult, ResourceReadParams, ResourceReadResult, ResourceWriteParams, ResourceWriteResult, ResourceListParams, ResourceListResult, ResourceCopyParams, ResourceCopyResult, ResourceDeleteParams, ResourceDeleteResult, ResourceMoveParams, ResourceMoveResult, ResourceRequestParams, ResourceRequestResult, FetchTurnsParams, FetchTurnsResult, UnsubscribeParams, DispatchActionParams, AuthenticateParams, AuthenticateResult, ResolveSessionConfigParams, ResolveSessionConfigResult, SessionConfigCompletionsParams, SessionConfigCompletionsResult, CompletionsParams, CompletionsResult, InvokeChangesetOperationParams, InvokeChangesetOperationResult } from './commands.js'; import type { ActionEnvelope } from './actions.js'; -import type { ProtocolNotification } from './notifications.js'; +import type { SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams, AuthRequiredParams } from './notifications.js'; import type { AhpError } from './errors.js'; // ─── JSON-RPC Base Types ───────────────────────────────────────────────────── @@ -114,14 +114,13 @@ export interface ServerCommandMap { // ─── Notification Maps ─────────────────────────────────────────────────────── -/** Params for the server → client `notification` method. */ -export interface NotificationMethodParams { - notification: ProtocolNotification; -} - /** * Registry mapping each client → server notification method to its params type. * + * Every notification's params MUST carry a top-level `channel: URI` so that + * the server can route the message to the correct subscription. See + * {@link UnsubscribeParams} for the canonical "base" shape. + * * @category Notifications */ export interface ClientNotificationMap { @@ -132,16 +131,19 @@ export interface ClientNotificationMap { /** * Registry mapping each server → client notification method to its params type. * + * Every notification's params MUST carry a top-level `channel: URI` so that + * the client can dispatch the message to the right subscription. + * * @category Notifications */ export interface ServerNotificationMap { 'action': { params: ActionEnvelope }; - 'notification': { params: NotificationMethodParams }; + 'root/sessionAdded': { params: SessionAddedParams }; + 'root/sessionRemoved': { params: SessionRemovedParams }; + 'root/sessionSummaryChanged': { params: SessionSummaryChangedParams }; + 'auth/required': { params: AuthRequiredParams }; } -/** Combined notification map for all directions. */ -export type NotificationMap = ClientNotificationMap & ServerNotificationMap; - // ─── Typed Requests ────────────────────────────────────────────────────────── /** @@ -223,26 +225,6 @@ export type AhpServerResponse = - M extends unknown ? { - readonly jsonrpc: '2.0'; - readonly method: M; - readonly params: NotificationMap[M]['params']; - } : never; - /** A client → server notification. */ export type AhpClientNotification = M extends unknown ? { @@ -259,6 +241,15 @@ export type AhpServerNotification", - * "provider": "copilot", - * "title": "New Session", - * "status": 1, - * "createdAt": 1710000000000, - * "modifiedAt": 1710000000000 - * } + * "channel": "ahp-root://", + * "summary": { + * "resource": "ahp-session:/", + * "provider": "copilot", + * "title": "New Session", + * "status": 1, + * "createdAt": 1710000000000, + * "modifiedAt": 1710000000000 * } * } * } * ``` */ -export interface SessionAddedNotification { - type: NotificationType.SessionAdded; +export interface SessionAddedParams { + /** Channel URI this notification belongs to (the root channel) */ + channel: URI; /** Summary of the new session */ summary: SessionSummary; } +// ─── root/sessionRemoved ───────────────────────────────────────────────────── + /** - * Broadcast to all connected clients when a session is disposed. + * Broadcast to all clients subscribed to the root channel when a session is + * disposed. * * @category Protocol Notifications + * @method root/sessionRemoved + * @direction Server → Client + * @messageType Notification * @version 1 * @example * ```json * { * "jsonrpc": "2.0", - * "method": "notification", + * "method": "root/sessionRemoved", * "params": { - * "notification": { - * "type": "notify/sessionRemoved", - * "session": "copilot:/" - * } + * "channel": "ahp-root://", + * "session": "ahp-session:/" * } * } * ``` */ -export interface SessionRemovedNotification { - type: NotificationType.SessionRemoved; +export interface SessionRemovedParams { + /** Channel URI this notification belongs to (the root channel) */ + channel: URI; /** URI of the removed session */ session: URI; } +// ─── root/sessionSummaryChanged ────────────────────────────────────────────── + /** - * Broadcast to all connected clients when an existing session's summary - * changes (title, status, `modifiedAt`, model, working directory, read/done - * state, or diff statistics). + * Broadcast to all clients subscribed to the root channel when an existing + * session's summary changes (title, status, `modifiedAt`, model, working + * directory, read/done state, or diff statistics). * * This notification lets clients that maintain a cached session list — for * example, the result of a previous `listSessions()` call — stay in sync with * in-flight sessions without having to subscribe to every session URI * individually. It is complementary to, not a replacement for, - * `notify/sessionAdded` and `notify/sessionRemoved`: those signal lifecycle + * `root/sessionAdded` and `root/sessionRemoved`: those signal lifecycle * (creation/disposal), while this signals summary-level mutations on an * already-known session. * @@ -115,36 +113,38 @@ export interface SessionRemovedNotification { * catalog via `listSessions()` as usual. * - The server SHOULD emit this notification whenever any mutable field on * {@link SessionSummary | `SessionSummary`} changes for a session the - * server has surfaced via `listSessions()` or `notify/sessionAdded`. + * server has surfaced via `listSessions()` or `root/sessionAdded`. * Servers MAY coalesce or debounce updates for noisy fields (for example, * `modifiedAt` bumps while a turn is streaming, or rapidly changing * `changesets`) at their discretion. * - Clients that have no cached entry for `session` MAY ignore the - * notification; it is not a substitute for `notify/sessionAdded`. + * notification; it is not a substitute for `root/sessionAdded`. * * @category Protocol Notifications + * @method root/sessionSummaryChanged + * @direction Server → Client + * @messageType Notification * @version 1 * @example * ```json * { * "jsonrpc": "2.0", - * "method": "notification", + * "method": "root/sessionSummaryChanged", * "params": { - * "notification": { - * "type": "notify/sessionSummaryChanged", - * "session": "copilot:/", - * "changes": { - * "title": "Refactor auth middleware", - * "status": 8, - * "modifiedAt": 1710000123456 - * } + * "channel": "ahp-root://", + * "session": "ahp-session:/", + * "changes": { + * "title": "Refactor auth middleware", + * "status": 8, + * "modifiedAt": 1710000123456 * } * } * } * ``` */ -export interface SessionSummaryChangedNotification { - type: NotificationType.SessionSummaryChanged; +export interface SessionSummaryChangedParams { + /** Channel URI this notification belongs to (the root channel) */ + channel: URI; /** URI of the session whose summary changed */ session: URI; /** @@ -156,45 +156,45 @@ export interface SessionSummaryChangedNotification { changes: Partial; } +// ─── auth/required ─────────────────────────────────────────────────────────── + /** * Sent by the server when a protected resource requires (re-)authentication. * - * This notification is sent when a previously valid token expires or is - * revoked, or when the server discovers a new authentication requirement. + * This notification MAY be associated with any channel — for example, an + * agent advertised on the root channel, or a per-session resource. The + * `channel` field identifies the subscription the auth requirement belongs + * to; the `resource` field carries the OAuth-protected resource identifier + * (per RFC 9728). + * * Clients should obtain a fresh token and push it via the `authenticate` * command. * * @category Protocol Notifications + * @method auth/required + * @direction Server → Client + * @messageType Notification * @version 1 * @see {@link /specification/authentication | Authentication} * @example * ```json * { * "jsonrpc": "2.0", - * "method": "notification", + * "method": "auth/required", * "params": { - * "notification": { - * "type": "notify/authRequired", - * "resource": "https://api.github.com", - * "reason": "expired" - * } + * "channel": "ahp-root://", + * "resource": "https://api.github.com", + * "reason": "expired" * } * } * ``` */ -export interface AuthRequiredNotification { - type: NotificationType.AuthRequired; +export interface AuthRequiredParams { + /** Channel URI this notification belongs to */ + channel: URI; /** The protected resource identifier that requires authentication */ resource: string; /** Why authentication is required */ reason?: AuthRequiredReason; } -/** - * Discriminated union of all protocol notifications. - */ -export type ProtocolNotification = - | SessionAddedNotification - | SessionRemovedNotification - | SessionSummaryChangedNotification - | AuthRequiredNotification; diff --git a/src/vs/platform/agentHost/common/state/protocol/state.ts b/src/vs/platform/agentHost/common/state/protocol/state.ts index af16c39edbf..3c57c6fb566 100644 --- a/src/vs/platform/agentHost/common/state/protocol/state.ts +++ b/src/vs/platform/agentHost/common/state/protocol/state.ts @@ -8,7 +8,7 @@ // ─── Type Aliases ──────────────────────────────────────────────────────────── -/** A URI string (e.g. `agenthost:/root` or `copilot:/`). */ +/** A URI string (e.g. `ahp-root://` or `ahp-session:/`). */ export type URI = string; /** @@ -143,7 +143,7 @@ export const enum PolicyState { } /** - * Global state shared with every client subscribed to `agenthost:/root`. + * Global state shared with every client subscribed to `ahp-root://`. * * @category Root State */ @@ -2024,7 +2024,7 @@ export interface ErrorInfo { * @category Common Types */ export interface Snapshot { - /** The subscribed resource URI (e.g. `agenthost:/root` or `copilot:/`) */ + /** The subscribed channel URI (e.g. `ahp-root://` or `ahp-session:/`) */ resource: URI; /** The current state of the resource */ state: RootState | SessionState | TerminalState | ChangesetState; diff --git a/src/vs/platform/agentHost/common/state/protocol/version/registry.ts b/src/vs/platform/agentHost/common/state/protocol/version/registry.ts index 08cec006882..15ee73a05fe 100644 --- a/src/vs/platform/agentHost/common/state/protocol/version/registry.ts +++ b/src/vs/platform/agentHost/common/state/protocol/version/registry.ts @@ -7,7 +7,7 @@ // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts import { ActionType, type StateAction } from '../actions.js'; -import { NotificationType, type ProtocolNotification } from '../notifications.js'; +import type { ServerNotificationMap } from '../messages.js'; // ─── Protocol Version Constants ────────────────────────────────────────────── @@ -123,25 +123,34 @@ export function isActionKnownToVersion(action: StateAction, clientVersion: strin return compareProtocolVersions(ACTION_INTRODUCED_IN[action.type], clientVersion) <= 0; } -// ─── Exhaustive Notification → Version Map ───────────────────────────────── +// ─── Exhaustive Notification Method → Version Map ────────────────────────── /** - * Maps every notification type to the protocol version that introduced it. - * Adding a new notification to `ProtocolNotification` without adding it here - * is a compile error. + * Server → client notification method names that are part of the AHP + * protocol surface. The set is a subset of {@link ServerNotificationMap} + * keys that excludes `action` (the action envelope) since action versions + * are tracked via {@link ACTION_INTRODUCED_IN}. + */ +export type ProtocolNotificationMethod = Exclude; + +/** + * Maps every server → client protocol notification method to the protocol + * version that introduced it. Adding a new notification method to + * {@link ServerNotificationMap} without adding it here is a compile error. * * Versions are SemVer `MAJOR.MINOR.PATCH` strings (see `PROTOCOL_VERSION`). */ -export const NOTIFICATION_INTRODUCED_IN: { readonly [K in ProtocolNotification['type']]: string } = { - [NotificationType.SessionAdded]: '0.1.0', - [NotificationType.SessionRemoved]: '0.1.0', - [NotificationType.SessionSummaryChanged]: '0.1.0', - [NotificationType.AuthRequired]: '0.1.0', +export const NOTIFICATION_INTRODUCED_IN: { readonly [K in ProtocolNotificationMethod]: string } = { + 'root/sessionAdded': '0.1.0', + 'root/sessionRemoved': '0.1.0', + 'root/sessionSummaryChanged': '0.1.0', + 'auth/required': '0.1.0', }; /** - * Returns whether the given notification type is known to the specified protocol version. + * Returns whether the given notification method is known to the specified + * protocol version. */ -export function isNotificationKnownToVersion(notification: ProtocolNotification, clientVersion: string): boolean { - return compareProtocolVersions(NOTIFICATION_INTRODUCED_IN[notification.type], clientVersion) <= 0; +export function isNotificationKnownToVersion(method: ProtocolNotificationMethod, clientVersion: string): boolean { + return compareProtocolVersions(NOTIFICATION_INTRODUCED_IN[method], clientVersion) <= 0; } diff --git a/src/vs/platform/agentHost/common/state/sessionActions.ts b/src/vs/platform/agentHost/common/state/sessionActions.ts index a2ea1209ba1..23665220cf7 100644 --- a/src/vs/platform/agentHost/common/state/sessionActions.ts +++ b/src/vs/platform/agentHost/common/state/sessionActions.ts @@ -61,13 +61,27 @@ export { } from './protocol/actions.js'; export { - NotificationType, AuthRequiredReason, - type SessionAddedNotification, - type SessionRemovedNotification, - type AuthRequiredNotification, + type SessionAddedParams, + type SessionRemovedParams, + type SessionSummaryChangedParams, + type AuthRequiredParams, } from './protocol/notifications.js'; +/** + * String discriminants for the protocol notification methods that previously + * lived inside a `notification` wrapper. These values are the JSON-RPC method + * names sent over the wire by a channels-era server; they are also the `type` + * discriminant on {@link ProtocolNotification} variants. + */ +export const NotificationType = { + SessionAdded: 'root/sessionAdded', + SessionRemoved: 'root/sessionRemoved', + SessionSummaryChanged: 'root/sessionSummaryChanged', + AuthRequired: 'auth/required', +} as const; +export type NotificationType = typeof NotificationType[keyof typeof NotificationType]; + // ---- Local aliases for short names ------------------------------------------ // Consumers use these shorter names; they're type-only aliases. @@ -100,9 +114,21 @@ import type { RootConfigChangedAction, } from './protocol/actions.js'; -import type { ProtocolNotification } from './protocol/notifications.js'; +import type { SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams, AuthRequiredParams } from './protocol/notifications.js'; import type { RootAction as IRootAction_, SessionAction as ISessionAction_, ClientSessionAction as IClientSessionAction_, ServerSessionAction as IServerSessionAction_, TerminalAction as ITerminalAction_, ClientTerminalAction as IClientTerminalAction_, ChangesetAction as IChangesetAction_ } from './protocol/action-origin.generated.js'; +/** + * Discriminated union of all server→client protocol notifications other than + * the action envelope. Each variant carries its protocol `method` so callers + * can switch on `type` the same way they did against the old `NotificationType` + * enum. + */ +export type ProtocolNotification = + | ({ type: 'root/sessionAdded' } & SessionAddedParams) + | ({ type: 'root/sessionRemoved' } & SessionRemovedParams) + | ({ type: 'root/sessionSummaryChanged' } & SessionSummaryChangedParams) + | ({ type: 'auth/required' } & AuthRequiredParams); + export type RootAction = IRootAction_; export type SessionAction = ISessionAction_; export type ClientSessionAction = IClientSessionAction_; diff --git a/src/vs/platform/agentHost/common/state/sessionProtocol.ts b/src/vs/platform/agentHost/common/state/sessionProtocol.ts index 4f2bb901816..47716c46dd6 100644 --- a/src/vs/platform/agentHost/common/state/sessionProtocol.ts +++ b/src/vs/platform/agentHost/common/state/sessionProtocol.ts @@ -31,8 +31,6 @@ export type { AhpSuccessResponse, CommandMap, ClientNotificationMap, - NotificationMap, - NotificationMethodParams, ProtocolMessage, ServerNotificationMap, } from './protocol/messages.js'; diff --git a/src/vs/platform/agentHost/common/state/sessionState.ts b/src/vs/platform/agentHost/common/state/sessionState.ts index 94a8307ef52..e7aefe5850e 100644 --- a/src/vs/platform/agentHost/common/state/sessionState.ts +++ b/src/vs/platform/agentHost/common/state/sessionState.ts @@ -135,7 +135,29 @@ export const enum FileEditKind { // ---- Well-known URIs -------------------------------------------------------- /** URI for the root state subscription. */ -export const ROOT_STATE_URI = 'agenthost:/root'; +export const ROOT_STATE_URI = 'ahp-root://'; + +/** Scheme used by {@link ROOT_STATE_URI}. */ +export const AHP_ROOT_SCHEME = 'ahp-root'; + +/** + * Returns `true` when `uri` identifies the root channel, regardless of + * whether the caller passes the canonical wire form (`'ahp-root://'`) or a + * variant that has been round-tripped through the workbench {@link URI} class + * (which normalizes the authority-less form to `'ahp-root:'`). Always prefer + * this helper over a direct `=== ROOT_STATE_URI` comparison so the two + * spellings stay interchangeable. + */ +export function isAhpRootChannel(uri: string): boolean { + if (uri === ROOT_STATE_URI) { + return true; + } + try { + return ResourceURI.parse(uri).scheme === AHP_ROOT_SCHEME; + } catch { + return false; + } +} // ---- VS Code-specific derived types ----------------------------------------- diff --git a/src/vs/platform/agentHost/electron-browser/localAgentHostService.ts b/src/vs/platform/agentHost/electron-browser/localAgentHostService.ts index 347625c4862..d7d80e72149 100644 --- a/src/vs/platform/agentHost/electron-browser/localAgentHostService.ts +++ b/src/vs/platform/agentHost/electron-browser/localAgentHostService.ts @@ -172,7 +172,7 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos } private _updateTelemetryLevel(): void { - this.dispatchAction({ + this.dispatchAction(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, config: { [AgentHostTelemetryLevelConfigKey]: telemetryLevelToAgentHostConfigValue(getTelemetryLevel(this._configurationService)) }, }, this.clientId, 0); @@ -220,8 +220,8 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos private unsubscribe(resource: URI): void { this._proxy.unsubscribe(resource, this.clientId); } - dispatchAction(action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { - this._proxy.dispatchAction(action, clientId, clientSeq); + dispatchAction(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { + this._proxy.dispatchAction(channel, action, clientId, clientSeq); } private _nextSeq = 1; nextClientSeq(): number { @@ -240,9 +240,9 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos return this._subscriptionManager.getSubscriptionUnmanaged(resource); } - dispatch(action: SessionAction | TerminalAction | IRootConfigChangedAction): void { - const seq = this._subscriptionManager.dispatchOptimistic(action); - this.dispatchAction(action, this.clientId, seq); + dispatch(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): void { + const seq = this._subscriptionManager.dispatchOptimistic(channel, action); + this.dispatchAction(channel, action, this.clientId, seq); } resourceList(uri: URI): Promise { diff --git a/src/vs/platform/agentHost/node/agentConfigurationService.ts b/src/vs/platform/agentHost/node/agentConfigurationService.ts index 49f22de65de..17899e9ec08 100644 --- a/src/vs/platform/agentHost/node/agentConfigurationService.ts +++ b/src/vs/platform/agentHost/node/agentConfigurationService.ts @@ -15,7 +15,7 @@ import { AgentHostConfigKey, agentHostCustomizationConfigSchema, defaultAgentHos import type { ISchema, SchemaDefinition, SchemaValue } from '../common/agentHostSchema.js'; import { ProtocolError } from '../common/state/sessionProtocol.js'; import { ActionType } from '../common/state/sessionActions.js'; -import { parseSubagentSessionUri, type URI as ProtocolURI } from '../common/state/sessionState.js'; +import { parseSubagentSessionUri, ROOT_STATE_URI, type URI as ProtocolURI } from '../common/state/sessionState.js'; import { AgentHostStateManager } from './agentHostStateManager.js'; export const IAgentConfigurationService = createDecorator('agentConfigurationService'); @@ -174,9 +174,8 @@ export class AgentConfigurationService extends Disposable implements IAgentConfi } updateSessionConfig(session: ProtocolURI, patch: Record): void { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(session, { type: ActionType.SessionConfigChanged, - session, config: patch, }); } @@ -205,7 +204,7 @@ export class AgentConfigurationService extends Disposable implements IAgentConfi } updateRootConfig(patch: Record, replace = false): void { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, config: patch, replace, diff --git a/src/vs/platform/agentHost/node/agentHostChangesetService.ts b/src/vs/platform/agentHost/node/agentHostChangesetService.ts index 573bbb62a30..d4b7a0f2de1 100644 --- a/src/vs/platform/agentHost/node/agentHostChangesetService.ts +++ b/src/vs/platform/agentHost/node/agentHostChangesetService.ts @@ -388,9 +388,8 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC ref = this._sessionDataService.openDatabase(URI.parse(session)); } catch (err) { this._logService.warn(`[AgentHostChangesetService] Failed to open session database for turn diff: ${session}`, err); - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(turnUri, { type: ActionType.ChangesetStatusChanged, - changeset: turnUri, status: ChangesetStatus.Error, error: { errorType: 'computeFailed', message: err instanceof Error ? err.message : String(err) }, }); @@ -401,9 +400,8 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC this._publishChangesetDiffs(session, turnUri, diffs); } catch (err) { this._logService.warn(`[AgentHostChangesetService] Failed to compute turn diffs for ${session}/${turnId}`, err); - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(turnUri, { type: ActionType.ChangesetStatusChanged, - changeset: turnUri, status: ChangesetStatus.Error, error: { errorType: 'computeFailed', message: err instanceof Error ? err.message : String(err) }, }); @@ -565,9 +563,8 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC } } catch (err) { this._logService.warn(`[AgentHostChangesetService] Failed to compute ${kind} diffs`, err); - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetStatusChanged, - changeset: changesetUri, status: ChangesetStatus.Error, error: { errorType: 'computeFailed', message: err instanceof Error ? err.message : String(err) }, }); @@ -616,9 +613,8 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC } nextFilesById.set(id, edit); const file: ChangesetFile = { id, edit }; - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetFileSet, - changeset: changesetUri, file, }); } @@ -626,9 +622,8 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC // Emit removals for any file that disappeared in this pass. for (const id of previousIds) { if (!nextFilesById.has(id)) { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetFileRemoved, - changeset: changesetUri, fileId: id, }); } @@ -638,9 +633,8 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC // now that we have a fresh, complete file list. const status = this._stateManager.getChangesetState(changesetUri)?.status; if (status !== ChangesetStatus.Ready) { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetStatusChanged, - changeset: changesetUri, status: ChangesetStatus.Ready, }); } diff --git a/src/vs/platform/agentHost/node/agentHostFileCompletionProvider.ts b/src/vs/platform/agentHost/node/agentHostFileCompletionProvider.ts index 4d00bc96b83..da035ee31d3 100644 --- a/src/vs/platform/agentHost/node/agentHostFileCompletionProvider.ts +++ b/src/vs/platform/agentHost/node/agentHostFileCompletionProvider.ts @@ -110,7 +110,7 @@ export class AgentHostFileCompletionProvider implements IAgentHostCompletionItem ) { } async provideCompletionItems(params: CompletionsParams, token: CancellationToken): Promise { - const workingDirectoryStr = this._stateManager.getSessionState(params.session)?.summary.workingDirectory; + const workingDirectoryStr = this._stateManager.getSessionState(params.channel)?.summary.workingDirectory; if (!workingDirectoryStr) { return []; } diff --git a/src/vs/platform/agentHost/node/agentHostSkillCompletionProvider.ts b/src/vs/platform/agentHost/node/agentHostSkillCompletionProvider.ts index 9dd5dda8b4e..6d0b0b4a47e 100644 --- a/src/vs/platform/agentHost/node/agentHostSkillCompletionProvider.ts +++ b/src/vs/platform/agentHost/node/agentHostSkillCompletionProvider.ts @@ -65,13 +65,13 @@ export class AgentHostSkillCompletionProvider extends Disposable implements IAge return []; } - const agent = this._getAgent(params.session); + const agent = this._getAgent(params.channel); if (!agent) { return []; } this._watchAgent(agent); - const candidates = await this._getCandidates(agent, typeof params.session === 'string' ? URI.parse(params.session) : params.session); + const candidates = await this._getCandidates(agent, typeof params.channel === 'string' ? URI.parse(params.channel) : params.channel); if (token.isCancellationRequested || candidates.length === 0) { return []; } diff --git a/src/vs/platform/agentHost/node/agentHostStateManager.ts b/src/vs/platform/agentHost/node/agentHostStateManager.ts index 0520b72bb16..741059d2b14 100644 --- a/src/vs/platform/agentHost/node/agentHostStateManager.ts +++ b/src/vs/platform/agentHost/node/agentHostStateManager.ts @@ -9,10 +9,10 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { equals } from '../../../base/common/objects.js'; import { ILogService } from '../../log/common/log.js'; import { TelemetryLevel } from '../../telemetry/common/telemetry.js'; -import { ActionType, NotificationType, ActionEnvelope, ActionOrigin, INotification, IRootConfigChangedAction, SessionAction, RootAction, StateAction, TerminalAction, ChangesetAction, isRootAction, isSessionAction, isChangesetAction } from '../common/state/sessionActions.js'; +import { ActionType, ActionEnvelope, ActionOrigin, INotification, IRootConfigChangedAction, SessionAction, RootAction, StateAction, TerminalAction, ChangesetAction, isRootAction, isSessionAction, isChangesetAction } from '../common/state/sessionActions.js'; import type { IStateSnapshot } from '../common/state/sessionProtocol.js'; import { rootReducer, sessionReducer, changesetReducer } from '../common/state/sessionReducers.js'; -import { createRootState, createSessionState, SessionLifecycle, type ChangesetState, type ChangesetSummary, type RootState, type SessionMeta, type SessionState, type SessionSummary, type Turn, type URI, ROOT_STATE_URI, ChangesetStatus } from '../common/state/sessionState.js'; +import { createRootState, createSessionState, isAhpRootChannel, SessionLifecycle, type ChangesetState, type ChangesetSummary, type RootState, type SessionMeta, type SessionState, type SessionSummary, type Turn, type URI, ROOT_STATE_URI, ChangesetStatus } from '../common/state/sessionState.js'; import { AgentHostTelemetryLevelConfigKey, IPermissionsValue, platformRootSchema, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js'; import { SessionConfigKey } from '../common/sessionConfigKeys.js'; import { parseChangesetUri } from '../common/changesetUri.js'; @@ -151,9 +151,9 @@ export class AgentHostStateManager extends Disposable { * the client should process subsequent envelopes with serverSeq > fromSeq. */ getSnapshot(resource: URI): IStateSnapshot | undefined { - if (resource === ROOT_STATE_URI) { + if (isAhpRootChannel(resource)) { return { - resource, + resource: ROOT_STATE_URI, state: this._rootState, fromSeq: this._serverSeq, }; @@ -222,7 +222,8 @@ export class AgentHostStateManager extends Disposable { // intentionally skip both until they are persisted. this._lastNotifiedSummaries.set(key, summary); this._onDidEmitNotification.fire({ - type: NotificationType.SessionAdded, + type: 'root/sessionAdded', + channel: ROOT_STATE_URI, summary, }); } @@ -261,7 +262,8 @@ export class AgentHostStateManager extends Disposable { state.summary = summary; this._lastNotifiedSummaries.set(key, summary); this._onDidEmitNotification.fire({ - type: NotificationType.SessionAdded, + type: 'root/sessionAdded', + channel: ROOT_STATE_URI, summary, }); } @@ -336,7 +338,7 @@ export class AgentHostStateManager extends Disposable { // Without this, evicting a session that still has an active turn // silently strands the active-sessions count above zero forever. if (this._sessionsWithActiveTurn.delete(session)) { - this.dispatchServerAction({ type: ActionType.RootActiveSessionsChanged, activeSessions: this._sessionsWithActiveTurn.size }); + this.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootActiveSessionsChanged, activeSessions: this._sessionsWithActiveTurn.size }); } this._sessionStates.delete(session); @@ -371,7 +373,8 @@ export class AgentHostStateManager extends Disposable { this.removeSession(session); if (wasAnnounced) { this._onDidEmitNotification.fire({ - type: NotificationType.SessionRemoved, + type: 'root/sessionRemoved', + channel: ROOT_STATE_URI, session, }); } @@ -389,7 +392,7 @@ export class AgentHostStateManager extends Disposable { * helpers in `sessionState.ts` to combine slots. */ setSessionMeta(session: URI, meta: SessionMeta | undefined): void { - this.dispatchServerAction({ type: ActionType.SessionMetaChanged, session, _meta: meta }); + this.dispatchServerAction(session, { type: ActionType.SessionMetaChanged, _meta: meta }); } // ---- Changeset registry ------------------------------------------------- @@ -448,9 +451,8 @@ export class AgentHostStateManager extends Disposable { // Take a defensive copy so callers can't mutate the catalogue array // after dispatch; the reducer otherwise stores the reference as-is. const next: ChangesetSummary[] | undefined = changesets ? [...changesets] : undefined; - this.dispatchServerAction({ + this.dispatchServerAction(session, { type: ActionType.SessionChangesetsChanged, - session, changesets: next, }); } @@ -474,9 +476,8 @@ export class AgentHostStateManager extends Disposable { if (!this._changesetStates.has(changeset)) { return; } - this.dispatchServerAction({ + this.dispatchServerAction(changeset, { type: ActionType.ChangesetCleared, - changeset, }); this._changesetStates.delete(changeset); } @@ -519,9 +520,13 @@ export class AgentHostStateManager extends Disposable { * Dispatch a server-originated action (from the agent backend). * The action is applied to state via the reducer and emitted as an * envelope with no origin (server-produced). + * + * `channel` identifies the channel the action targets — `ROOT_STATE_URI` + * for root actions, a session URI for session actions, a terminal URI + * for terminal actions, an expanded changeset URI for changeset actions. */ - dispatchServerAction(action: StateAction): void { - this._applyAndEmit(action, undefined); + dispatchServerAction(channel: URI, action: StateAction): void { + this._applyAndEmit(channel, action, undefined); } /** @@ -529,13 +534,13 @@ export class AgentHostStateManager extends Disposable { * The action is applied to state and emitted with the client's origin * so the originating client can reconcile. */ - dispatchClientAction(action: SessionAction | TerminalAction | IRootConfigChangedAction, origin: ActionOrigin): unknown { - return this._applyAndEmit(action, origin); + dispatchClientAction(channel: URI, action: SessionAction | TerminalAction | IRootConfigChangedAction, origin: ActionOrigin): unknown { + return this._applyAndEmit(channel, action, origin); } // ---- Internal ----------------------------------------------------------- - private _applyAndEmit(action: StateAction, origin: ActionOrigin | undefined): unknown { + private _applyAndEmit(channel: URI, action: StateAction, origin: ActionOrigin | undefined): unknown { let resultingState: unknown = undefined; // Apply to state if (isRootAction(action)) { @@ -561,7 +566,7 @@ export class AgentHostStateManager extends Disposable { if (isSessionAction(action)) { const sessionAction = action as SessionAction; - const key = sessionAction.session; + const key = channel; const state = this._sessionStates.get(key); if (state) { const newState = sessionReducer(state, sessionAction, this._log); @@ -587,7 +592,7 @@ export class AgentHostStateManager extends Disposable { } else { this._sessionsWithActiveTurn.delete(key); } - this.dispatchServerAction({ type: ActionType.RootActiveSessionsChanged, activeSessions: this._sessionsWithActiveTurn.size }); + this.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootActiveSessionsChanged, activeSessions: this._sessionsWithActiveTurn.size }); } resultingState = newState; @@ -598,7 +603,7 @@ export class AgentHostStateManager extends Disposable { if (isChangesetAction(action)) { const changesetAction = action as ChangesetAction; - const key = changesetAction.changeset; + const key = channel; const state = this._changesetStates.get(key); if (!state) { // Unknown changeset: log and bail before envelope creation. @@ -617,6 +622,7 @@ export class AgentHostStateManager extends Disposable { // Emit envelope const envelope: ActionEnvelope = { + channel, action, serverSeq: ++this._serverSeq, origin, @@ -665,7 +671,8 @@ export class AgentHostStateManager extends Disposable { if (Object.keys(changes).length > 0) { this._onDidEmitNotification.fire({ - type: NotificationType.SessionSummaryChanged, + type: 'root/sessionSummaryChanged', + channel: ROOT_STATE_URI, session, changes, }); diff --git a/src/vs/platform/agentHost/node/agentHostTerminalManager.ts b/src/vs/platform/agentHost/node/agentHostTerminalManager.ts index bd52d961808..ac6642c1024 100644 --- a/src/vs/platform/agentHost/node/agentHostTerminalManager.ts +++ b/src/vs/platform/agentHost/node/agentHostTerminalManager.ts @@ -21,6 +21,7 @@ import { ActionType } from '../common/state/protocol/actions.js'; import type { CreateTerminalParams } from '../common/state/protocol/commands.js'; import { TerminalClaim, TerminalContentPart, TerminalInfo, TerminalState, TerminalClaimKind } from '../common/state/protocol/state.js'; import { isTerminalAction } from '../common/state/sessionActions.js'; +import { ROOT_STATE_URI } from '../common/state/sessionState.js'; import { IAgentConfigurationService } from './agentConfigurationService.js'; import { AgentHostHeadlessTerminal } from './agentHostHeadlessTerminal.js'; import { isZsh } from './agentHostShellUtils.js'; @@ -195,21 +196,22 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe if (!isTerminalAction(action)) { return; } + const channel = envelope.channel; switch (action.type) { case ActionType.TerminalInput: - this._writeInput(action.terminal, action.data); + this._writeInput(channel, action.data); break; case ActionType.TerminalResized: - this._resize(action.terminal, action.cols, action.rows); + this._resize(channel, action.cols, action.rows); break; case ActionType.TerminalClaimed: - this._setClaim(action.terminal, action.claim); + this._setClaim(channel, action.claim); break; case ActionType.TerminalTitleChanged: - this._setTitle(action.terminal, action.title); + this._setTitle(channel, action.title); break; case ActionType.TerminalCleared: - this._clearContent(action.terminal); + this._clearContent(channel); break; } })); @@ -248,7 +250,7 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe * Spawns the user's default shell. */ async createTerminal(params: CreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise { - const uri = params.terminal; + const uri = params.channel; if (this._terminals.has(uri)) { throw new Error(`Terminal already exists: ${uri}`); } @@ -414,9 +416,8 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe managed.exitCode = e.exitCode; managed.onExitEmitter.fire(e.exitCode); onFirstData.complete(); - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(uri, { type: ActionType.TerminalExited, - terminal: uri, exitCode: e.exitCode, }); this._broadcastTerminalList(); @@ -429,9 +430,8 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe const newTitle = ptyProcess.process; if (newTitle && newTitle !== managed.title) { managed.title = newTitle; - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(uri, { type: ActionType.TerminalTitleChanged, - terminal: uri, title: newTitle, }); this._broadcastTerminalList(); @@ -620,9 +620,8 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe // Fire data event and dispatch to protocol (cleaned, without OSC 633) if (cleanedData.length > 0) { managed.onDataEmitter.fire(cleanedData); - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(managed.uri, { type: ActionType.TerminalData, - terminal: managed.uri, data: cleanedData, }); } @@ -633,9 +632,8 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe // Emit TerminalCommandDetectionAvailable on first sequence if (!tracker.detectionAvailableEmitted) { tracker.detectionAvailableEmitted = true; - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(managed.uri, { type: ActionType.TerminalCommandDetectionAvailable, - terminal: managed.uri, }); } @@ -666,9 +664,8 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe isComplete: false, }); - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(managed.uri, { type: ActionType.TerminalCommandExecuted, - terminal: managed.uri, commandId, commandLine, timestamp, @@ -709,9 +706,8 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe output: commandOutput, }); - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(managed.uri, { type: ActionType.TerminalCommandFinished, - terminal: managed.uri, commandId: finishedCommandId, exitCode: event.exitCode, durationMs, @@ -722,9 +718,8 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe case Osc633EventType.Property: { if (event.key === 'Cwd') { managed.cwd = event.value; - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(managed.uri, { type: ActionType.TerminalCwdChanged, - terminal: managed.uri, cwd: event.value, }); } @@ -839,7 +834,7 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe /** Dispatch root/terminalsChanged with the current terminal list. */ private _broadcastTerminalList(): void { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootTerminalsChanged, terminals: this.getTerminalInfos(), }); diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index d3382f18d3d..bda63814f30 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -466,7 +466,7 @@ export class AgentService extends Disposable implements IAgentService { // this to {@link _onDidMaterializeSession} so subscribers // don't see `Ready` until the agent actually has an SDK // session, working directory, etc. - this._stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: session.toString() }); + this._stateManager.dispatchServerAction(session.toString(), { type: ActionType.SessionReady }); // Lazily compute git state for sessions with a working directory; // attaches under `state._meta.git` once ready. @@ -530,7 +530,7 @@ export class AgentService extends Disposable implements IAgentService { // the deferred `SessionAdded` notification atomically so subscribers // see consistent state through both paths. this._stateManager.markSessionPersisted(sessionKey, summary); - this._stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionKey }); + this._stateManager.dispatchServerAction(sessionKey, { type: ActionType.SessionReady }); this._attachGitState(e.session, e.workingDirectory); // If a client subscribed to this session's uncommitted changeset // before the working directory was known, the coordinator drains @@ -927,19 +927,19 @@ export class AgentService extends Disposable implements IAgentService { */ private readonly _clientDispatchQueues = new Map>(); - dispatchAction(action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { + dispatchAction(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { this._logService.trace(`[AgentService] dispatchAction: type=${action.type}, clientId=${clientId}, clientSeq=${clientSeq}`, action); const pending = this._clientDispatchQueues.get(clientId); - if (!pending && !this._needsAsyncRewrite(action)) { - this._dispatchActionNow(action, clientId, clientSeq); + if (!pending && !this._needsAsyncRewrite(channel, action)) { + this._dispatchActionNow(channel, action, clientId, clientSeq); return; } const next = (pending ?? Promise.resolve()).then(async () => { - const rewritten: SessionAction | TerminalAction | IRootConfigChangedAction = this._needsAsyncRewrite(action) - ? await this._rewriteUserMessageAttachments(action, clientId) + const rewritten: SessionAction | TerminalAction | IRootConfigChangedAction = this._needsAsyncRewrite(channel, action) + ? await this._rewriteUserMessageAttachments(channel, action, clientId) : action; - this._dispatchActionNow(rewritten, clientId, clientSeq); + this._dispatchActionNow(channel, rewritten, clientId, clientSeq); }).catch(err => { this._logService.error(`[AgentService] async dispatchAction failed: ${toErrorMessage(err)}`); }); @@ -951,23 +951,22 @@ export class AgentService extends Disposable implements IAgentService { })); } - private _dispatchActionNow(action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { + private _dispatchActionNow(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { const origin = { clientId, clientSeq }; - this._stateManager.dispatchClientAction(action, origin); + this._stateManager.dispatchClientAction(channel, action, origin); if (action.type === ActionType.RootConfigChanged) { this._configurationService.persistRootConfig(); } - this._sideEffects.handleAction(action); + this._sideEffects.handleAction(channel, action); } - private _needsAsyncRewrite(action: SessionAction | TerminalAction | IRootConfigChangedAction): action is SessionTurnStartedAction | SessionPendingMessageSetAction { + private _needsAsyncRewrite(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): action is SessionTurnStartedAction | SessionPendingMessageSetAction { if (action.type !== ActionType.SessionTurnStarted && action.type !== ActionType.SessionPendingMessageSet) { return false; } - const attachmentsRootStr = this._attachmentsRoot(URI.parse(action.session)).toString(); + const attachmentsRootStr = this._attachmentsRoot(channel).toString(); return !!action.userMessage.attachments?.some(a => this._isRewritableAttachment(a, attachmentsRootStr)); } - private _isRewritableAttachment(attachment: MessageAttachment, attachmentsRootStr: string): boolean { if (attachment.type === MessageAttachmentKind.EmbeddedResource) { return true; @@ -986,8 +985,8 @@ export class AgentService extends Disposable implements IAgentService { return false; } - private _attachmentsRoot(session: URI): URI { - return joinPath(this._sessionDataService.getSessionDataDir(session), SESSION_ATTACHMENTS_DIRNAME); + private _attachmentsRoot(session: string): URI { + return joinPath(this._sessionDataService.getSessionDataDir(URI.parse(session)), SESSION_ATTACHMENTS_DIRNAME); } /** @@ -1003,12 +1002,12 @@ export class AgentService extends Disposable implements IAgentService { * etc.) the original attachment is preserved so the agent still has a * chance to make use of it. */ - private async _rewriteUserMessageAttachments(action: T, clientId: string): Promise { + private async _rewriteUserMessageAttachments(channel: string, action: T, clientId: string): Promise { const attachments = action.userMessage.attachments; if (!attachments?.length) { return action; } - const attachmentsRoot = this._attachmentsRoot(URI.parse(action.session)); + const attachmentsRoot = this._attachmentsRoot(channel); const attachmentsRootStr = attachmentsRoot.toString(); const rewritten = await Promise.all(attachments.map(a => this._rewriteSingleAttachment(a, attachmentsRoot, attachmentsRootStr, clientId))); return { diff --git a/src/vs/platform/agentHost/node/agentSideEffects.ts b/src/vs/platform/agentHost/node/agentSideEffects.ts index 19dac7e575c..a1e8afb9120 100644 --- a/src/vs/platform/agentHost/node/agentSideEffects.ts +++ b/src/vs/platform/agentHost/node/agentSideEffects.ts @@ -16,12 +16,13 @@ import { IAgentHostChangesetService } from './agentHostChangesetService.js'; import { ISessionDataService } from '../common/sessionDataService.js'; import type { AgentInfo } from '../common/state/protocol/state.js'; -import { ActionType, isSessionAction, StateAction, type SessionToolCallCompleteAction } from '../common/state/sessionActions.js'; +import { ActionType, StateAction, type SessionToolCallCompleteAction } from '../common/state/sessionActions.js'; import { buildSubagentSessionUri, getToolFileEdits, PendingMessageKind, ResponsePartKind, + ROOT_STATE_URI, SessionStatus, ToolCallStatus, ToolResultContentType, @@ -122,8 +123,8 @@ export class AgentSideEffects extends Disposable { this._register(this._stateManager.onDidEmitEnvelope(envelope => { if (!envelope.origin && envelope.action.type === ActionType.SessionToolCallComplete) { const action = envelope.action; - const agent = this._options.getAgent(action.session); - agent?.onClientToolCallComplete(URI.parse(action.session), action.toolCallId, action.result); + const agent = this._options.getAgent(envelope.channel); + agent?.onClientToolCallComplete(URI.parse(envelope.channel), action.toolCallId, action.result); } })); } @@ -156,7 +157,7 @@ export class AgentSideEffects extends Disposable { return; } this._lastAgentInfos = infos; - this._stateManager.dispatchServerAction({ type: ActionType.RootAgentsChanged, agents: infos }); + this._stateManager.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootAgentsChanged, agents: infos }); } private async _publishSessionCustomizations(agent: IAgent, session: ProtocolURI): Promise { @@ -165,9 +166,8 @@ export class AgentSideEffects extends Disposable { } const customizations = await agent.getSessionCustomizations(URI.parse(session)); - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(session, { type: ActionType.SessionCustomizationsChanged, - session, customizations: [...customizations], }); } @@ -264,9 +264,8 @@ export class AgentSideEffects extends Disposable { } if (signal.kind === 'steering_consumed') { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(sessionKey, { type: ActionType.SessionPendingMessageRemoved, - session: sessionKey, kind: PendingMessageKind.Steering, id: signal.id, }); @@ -358,15 +357,13 @@ export class AgentSideEffects extends Disposable { } // The agent emits actions with its own view of the active turnId // targeting the top-level session. The state manager is the source - // of truth — rewrite `session` and `turnId` so the action lands in - // the right reducer (subagent session for routed signals, queued - // turn ID when the agent hasn't yet seen `sendMessage`, etc.). + // of truth — rewrite `turnId` so the action lands in the right + // reducer (queued turn ID when the agent hasn't yet seen + // `sendMessage`, etc.). Routing to subagent sessions is handled by + // the caller via the channel argument. // Actions without a `turnId` field (`SessionTitleChanged`, - // `SessionInputRequested`) only get their `session` rewritten. + // `SessionInputRequested`) only get their channel rewritten. let action = signal.action; - if (isSessionAction(action) && action.session !== sessionKey) { - action = { ...action, session: sessionKey }; - } if (hasKey(action, { turnId: true }) && action.turnId !== turnId) { action = { ...action, turnId }; } @@ -391,7 +388,7 @@ export class AgentSideEffects extends Disposable { } } - this._stateManager.dispatchServerAction(action); + this._stateManager.dispatchServerAction(sessionKey, action); if (action.type === ActionType.SessionToolCallComplete) { // Drop any events that were buffered for a subagent whose @@ -485,9 +482,8 @@ export class AgentSideEffects extends Disposable { // Start a turn on the subagent session const turnId = generateUuid(); - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(subagentSessionUri, { type: ActionType.SessionTurnStarted, - session: subagentSessionUri, turnId, userMessage: { text: '' }, }); @@ -510,9 +506,8 @@ export class AgentSideEffects extends Disposable { description: agentDescription, }, ]; - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(parentSession, { type: ActionType.SessionToolCallContentChanged, - session: parentSession, turnId: parentTurnId, toolCallId, content: mergedContent, @@ -547,9 +542,8 @@ export class AgentSideEffects extends Disposable { if (key.startsWith(`${parentSession}:`)) { const turnId = this._stateManager.getActiveTurnId(subagentUri); if (turnId) { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(subagentUri, { type: ActionType.SessionTurnCancelled, - session: subagentUri, turnId, }); } @@ -587,9 +581,8 @@ export class AgentSideEffects extends Disposable { const turnId = this._stateManager.getActiveTurnId(subagentUri); if (turnId) { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(subagentUri, { type: ActionType.SessionTurnComplete, - session: subagentUri, turnId, }); } @@ -671,11 +664,12 @@ export class AgentSideEffects extends Disposable { effective = { ...e, state: { ...e.state, confirmationTitle: undefined } }; } this._stateManager.dispatchServerAction( + sessionKey, this._permissionManager.createToolReadyAction(effective, sessionKey, turnId) ); } - handleAction(action: StateAction): void { + handleAction(channel: ProtocolURI, action: StateAction): void { switch (action.type) { case ActionType.SessionTurnStarted: { // Per-turn streaming part tracking is owned by the agent @@ -686,34 +680,31 @@ export class AgentSideEffects extends Disposable { // while waiting for the AI-generated title. Only apply when the // title is still the default placeholder to avoid clobbering a // title set by the user or provider before the first turn. - const state = this._stateManager.getSessionState(action.session); + const state = this._stateManager.getSessionState(channel); const fallbackTitle = action.userMessage.text.trim().replace(/\s+/g, ' ').slice(0, 200); if (state && state.turns.length === 0 && !state.summary.title && fallbackTitle.length > 0) { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(channel, { type: ActionType.SessionTitleChanged, - session: action.session, title: fallbackTitle, }); } - const agent = this._options.getAgent(action.session); + const agent = this._options.getAgent(channel); if (!agent) { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(channel, { type: ActionType.SessionError, - session: action.session, turnId: action.turnId, error: { errorType: 'noAgent', message: 'No agent found for session' }, }); return; } const attachments = action.userMessage.attachments; - this._telemetryReporter.userMessageSent(agent.id, action.session, state, 'direct', attachments); - agent.sendMessage(URI.parse(action.session), action.userMessage.text, attachments, action.turnId).catch(err => { + this._telemetryReporter.userMessageSent(agent.id, channel, state, 'direct', attachments); + agent.sendMessage(URI.parse(channel), action.userMessage.text, attachments, action.turnId).catch(err => { const errCode = (err as { code?: number })?.code; - this._logService.error(`[AgentSideEffects] sendMessage failed for session=${action.session}: code=${errCode}, message=${err instanceof Error ? err.message : String(err)}, type=${err?.constructor?.name}`, err); - this._stateManager.dispatchServerAction({ + this._logService.error(`[AgentSideEffects] sendMessage failed for session=${channel}: code=${errCode}, message=${err instanceof Error ? err.message : String(err)}, type=${err?.constructor?.name}`, err); + this._stateManager.dispatchServerAction(channel, { type: ActionType.SessionError, - session: action.session, turnId: action.turnId, error: { errorType: 'sendFailed', message: String(err) }, }); @@ -721,7 +712,7 @@ export class AgentSideEffects extends Disposable { break; } case ActionType.SessionToolCallConfirmed: { - const toolCallKey = `${action.session}:${action.toolCallId}`; + const toolCallKey = `${channel}:${action.toolCallId}`; const agentId = this._toolCallAgents.get(toolCallKey); if (agentId) { this._toolCallAgents.delete(toolCallKey); @@ -734,67 +725,67 @@ export class AgentSideEffects extends Disposable { // When the user chose "Allow in this Session", add the tool // to the session's permissions so future calls are auto-approved. if (action.approved) { - this._permissionManager.handleToolCallConfirmed(action.session, action.toolCallId, action.selectedOptionId); + this._permissionManager.handleToolCallConfirmed(channel, action.toolCallId, action.selectedOptionId); } break; } case ActionType.SessionInputCompleted: { - const agent = this._options.getAgent(action.session); + const agent = this._options.getAgent(channel); agent?.respondToUserInputRequest(action.requestId, action.response, action.answers); break; } case ActionType.SessionTurnCancelled: { // Cancel all subagent sessions for this parent - this.cancelSubagentSessions(action.session); - const agent = this._options.getAgent(action.session); - agent?.abortSession(URI.parse(action.session)).catch(err => { + this.cancelSubagentSessions(channel); + const agent = this._options.getAgent(channel); + agent?.abortSession(URI.parse(channel)).catch(err => { this._logService.error('[AgentSideEffects] abortSession failed', err); }); break; } case ActionType.SessionModelChanged: { - const agent = this._options.getAgent(action.session); - agent?.changeModel?.(URI.parse(action.session), action.model).catch(err => { + const agent = this._options.getAgent(channel); + agent?.changeModel?.(URI.parse(channel), action.model).catch(err => { this._logService.error('[AgentSideEffects] changeModel failed', err); }); break; } case ActionType.SessionTitleChanged: { - this._persistSessionFlag(action.session, 'customTitle', action.title); + this._persistSessionFlag(channel, 'customTitle', action.title); break; } case ActionType.SessionPendingMessageSet: case ActionType.SessionPendingMessageRemoved: case ActionType.SessionQueuedMessagesReordered: { - this._syncPendingMessages(action.session); + this._syncPendingMessages(channel); break; } case ActionType.SessionTruncated: { - const agent = this._options.getAgent(action.session); - agent?.truncateSession?.(URI.parse(action.session), action.turnId).catch(err => { + const agent = this._options.getAgent(channel); + agent?.truncateSession?.(URI.parse(channel), action.turnId).catch(err => { this._logService.error('[AgentSideEffects] truncateSession failed', err); }); - this._changesets.onSessionTruncated(action.session); + this._changesets.onSessionTruncated(channel); break; } case ActionType.SessionActiveClientChanged: { - const agent = this._options.getAgent(action.session); + const agent = this._options.getAgent(channel); if (!agent) { break; } // Always forward client tools, even if empty, to clear previous client's tools const clientId = action.activeClient?.clientId ?? ''; - agent.setClientTools(URI.parse(action.session), clientId, action.activeClient?.tools ?? []); + agent.setClientTools(URI.parse(channel), clientId, action.activeClient?.tools ?? []); const refs = action.activeClient?.customizations ?? []; agent.setClientCustomizations( clientId, refs, () => { - this._publishSessionCustomizationsSoon(agent, action.session); + this._publishSessionCustomizationsSoon(agent, channel); }, ).then(() => { - this._publishSessionCustomizationsSoon(agent, action.session); + this._publishSessionCustomizationsSoon(agent, channel); }).catch(err => { this._logService.error('[AgentSideEffects] setClientCustomizations failed', err); }); @@ -811,46 +802,46 @@ export class AgentSideEffects extends Disposable { break; } case ActionType.SessionActiveClientToolsChanged: { - const agent = this._options.getAgent(action.session); + const agent = this._options.getAgent(channel); if (agent) { - const sessionState = this._stateManager.getSessionState(action.session); + const sessionState = this._stateManager.getSessionState(channel); const toolClientId = sessionState?.activeClient?.clientId; if (toolClientId) { - agent.setClientTools(URI.parse(action.session), toolClientId, action.tools); + agent.setClientTools(URI.parse(channel), toolClientId, action.tools); } } break; } case ActionType.SessionCustomizationToggled: { - const agent = this._options.getAgent(action.session); + const agent = this._options.getAgent(channel); agent?.setCustomizationEnabled?.(action.uri, action.enabled); break; } case ActionType.SessionIsReadChanged: { - this._persistSessionFlag(action.session, 'isRead', action.isRead ? 'true' : ''); + this._persistSessionFlag(channel, 'isRead', action.isRead ? 'true' : ''); break; } case ActionType.SessionIsArchivedChanged: { - this._persistSessionFlag(action.session, 'isArchived', action.isArchived ? 'true' : ''); - const agent = this._options.getAgent(action.session); - agent?.onArchivedChanged?.(URI.parse(action.session), action.isArchived).catch(err => { - this._logService.warn(`[AgentSideEffects] onArchivedChanged failed for ${action.session}`, err); + this._persistSessionFlag(channel, 'isArchived', action.isArchived ? 'true' : ''); + const agent = this._options.getAgent(channel); + agent?.onArchivedChanged?.(URI.parse(channel), action.isArchived).catch(err => { + this._logService.warn(`[AgentSideEffects] onArchivedChanged failed for ${channel}`, err); }); break; } case ActionType.SessionConfigChanged: { // Persist merged values so a future `restoreSession` can re-hydrate // the user's previous selections (e.g. autoApprove). - const sessionState = this._stateManager.getSessionState(action.session); + const sessionState = this._stateManager.getSessionState(channel); const values = sessionState?.config?.values; if (values) { - this._persistSessionFlag(action.session, 'configValues', JSON.stringify(values)); + this._persistSessionFlag(channel, 'configValues', JSON.stringify(values)); } break; } case ActionType.SessionToolCallComplete: { - const agent = this._options.getAgent(action.session); - agent?.onClientToolCallComplete(URI.parse(action.session), action.toolCallId, action.result); + const agent = this._options.getAgent(channel); + agent?.onClientToolCallComplete(URI.parse(channel), action.toolCallId, action.result); break; } } @@ -925,9 +916,8 @@ export class AgentSideEffects extends Disposable { // inside its `send()` call), so no host-side reset is needed. // Dispatch server-initiated turn start; the reducer removes the queued message atomically - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(session, { type: ActionType.SessionTurnStarted, - session, turnId, userMessage: msg.userMessage, queuedMessageId: msg.id, @@ -936,9 +926,8 @@ export class AgentSideEffects extends Disposable { // Send the message to the agent backend const agent = this._options.getAgent(session); if (!agent) { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(session, { type: ActionType.SessionError, - session, turnId, error: { errorType: 'noAgent', message: 'No agent found for session' }, }); @@ -948,9 +937,8 @@ export class AgentSideEffects extends Disposable { this._telemetryReporter.userMessageSent(agent.id, session, this._stateManager.getSessionState(session), 'queued', attachments); agent.sendMessage(URI.parse(session), msg.userMessage.text, attachments, turnId).catch(err => { this._logService.error('[AgentSideEffects] sendMessage failed (queued)', err); - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(session, { type: ActionType.SessionError, - session, turnId, error: { errorType: 'sendFailed', message: String(err) }, }); diff --git a/src/vs/platform/agentHost/node/claude/claudeAgentSession.ts b/src/vs/platform/agentHost/node/claude/claudeAgentSession.ts index 907e4fab241..abe93b5cff1 100644 --- a/src/vs/platform/agentHost/node/claude/claudeAgentSession.ts +++ b/src/vs/platform/agentHost/node/claude/claudeAgentSession.ts @@ -217,7 +217,6 @@ export class ClaudeAgentSession extends Disposable { session: this.sessionUri, action: { type: ActionType.SessionInputRequested, - session: this.sessionUri.toString(), request, }, ...(parentToolCallId !== undefined ? { parentToolCallId } : {}), diff --git a/src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts b/src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts index 771b0978eba..cfa23b80a5f 100644 --- a/src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts +++ b/src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts @@ -323,7 +323,6 @@ function mapUserMessage( return []; } - const sessionStr = session.toString(); const signals: AgentSignal[] = []; for (const block of content) { if (block.type !== 'tool_result') { @@ -349,7 +348,6 @@ function mapUserMessage( session, action: { type: ActionType.SessionToolCallComplete, - session: sessionStr, turnId: tracked.turnId, toolCallId: block.tool_use_id, result: { @@ -416,7 +414,6 @@ function mapResult( logService: ILogService, registry: SubagentRegistry, ): AgentSignal[] { - const sessionStr = session.toString(); const signals: AgentSignal[] = []; if (message.subtype === 'success') { // `modelUsage` is keyed by model name; pick the first key as the @@ -428,7 +425,6 @@ function mapResult( session, action: { type: ActionType.SessionUsage, - session: sessionStr, turnId, usage: { inputTokens: message.usage.input_tokens, @@ -463,7 +459,6 @@ function mapStreamEvent( parentToolUseId: string | null, registry: SubagentRegistry, ): AgentSignal[] { - const sessionStr = session.toString(); switch (event.type) { case 'message_start': state.resetMessage(event.message.id); @@ -477,7 +472,6 @@ function mapStreamEvent( session, action: { type: ActionType.SessionResponsePart, - session: sessionStr, turnId, part: { kind: ResponsePartKind.Markdown, @@ -493,7 +487,6 @@ function mapStreamEvent( session, action: { type: ActionType.SessionResponsePart, - session: sessionStr, turnId, part: { kind: ResponsePartKind.Reasoning, @@ -531,7 +524,6 @@ function mapStreamEvent( session, action: { type: ActionType.SessionToolCallStart, - session: sessionStr, turnId, toolCallId: block.id, toolName: block.name, @@ -550,7 +542,6 @@ function mapStreamEvent( session, action: { type: ActionType.SessionDelta, - session: sessionStr, turnId, partId: makeContentBlockPartId(turnId, state, event.index, logService), content: event.delta.text, @@ -563,7 +554,6 @@ function mapStreamEvent( session, action: { type: ActionType.SessionReasoning, - session: sessionStr, turnId, partId: makeContentBlockPartId(turnId, state, event.index, logService), content: event.delta.thinking, @@ -582,7 +572,6 @@ function mapStreamEvent( session, action: { type: ActionType.SessionToolCallDelta, - session: sessionStr, turnId, toolCallId: tracked.toolUseId, content: event.delta.partial_json, @@ -618,7 +607,6 @@ function mapStreamEvent( session, action: { type: ActionType.SessionToolCallReady, - session: sessionStr, turnId, toolCallId: tracked.toolUseId, invocationMessage: info.invocationMessage, diff --git a/src/vs/platform/agentHost/node/claude/claudeSdkPipeline.ts b/src/vs/platform/agentHost/node/claude/claudeSdkPipeline.ts index 564ab4b6293..f94f59c51ee 100644 --- a/src/vs/platform/agentHost/node/claude/claudeSdkPipeline.ts +++ b/src/vs/platform/agentHost/node/claude/claudeSdkPipeline.ts @@ -443,7 +443,6 @@ export class ClaudeSdkPipeline extends Disposable { session: this.sessionUri, action: { type: ActionType.SessionTurnComplete, - session: this.sessionUri.toString(), turnId: completed.turnId, }, }); diff --git a/src/vs/platform/agentHost/node/claude/claudeSubagentSignals.ts b/src/vs/platform/agentHost/node/claude/claudeSubagentSignals.ts index dc45412b3e6..77f317ce856 100644 --- a/src/vs/platform/agentHost/node/claude/claudeSubagentSignals.ts +++ b/src/vs/platform/agentHost/node/claude/claudeSubagentSignals.ts @@ -157,7 +157,6 @@ export function buildTopLevelSubagentReadyAction( session, action: { type: ActionType.SessionToolCallReady, - session: session.toString(), turnId, toolCallId: block.id, invocationMessage: getClaudeInvocationMessage(block.name, getClaudeToolDisplayName(block.name), block.input), @@ -196,7 +195,6 @@ export function emitInnerAssistantSignals( parentToolUseId: string, registry: SubagentRegistry, ): AgentSignal[] { - const sessionStr = session.toString(); const messageId = message.message.id; const signals: AgentSignal[] = []; for (let index = 0; index < message.message.content.length; index++) { @@ -207,7 +205,6 @@ export function emitInnerAssistantSignals( session, action: { type: ActionType.SessionResponsePart, - session: sessionStr, turnId, part: { kind: ResponsePartKind.Markdown, @@ -224,7 +221,6 @@ export function emitInnerAssistantSignals( session, action: { type: ActionType.SessionResponsePart, - session: sessionStr, turnId, part: { kind: ResponsePartKind.Reasoning, @@ -253,7 +249,6 @@ export function emitInnerAssistantSignals( session, action: { type: ActionType.SessionToolCallStart, - session: sessionStr, turnId, toolCallId: block.id, toolName: block.name, @@ -266,7 +261,6 @@ export function emitInnerAssistantSignals( session, action: { type: ActionType.SessionToolCallReady, - session: sessionStr, turnId, toolCallId: block.id, invocationMessage: getClaudeInvocationMessage(block.name, displayName, block.input), diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts index b804e8bf8cf..fd1002cfae3 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts @@ -30,7 +30,7 @@ import { SessionConfigKey } from '../../common/sessionConfigKeys.js'; import { ISessionDatabase, ISessionDataService, SESSION_ATTACHMENTS_DIRNAME } from '../../common/sessionDataService.js'; import { MessageAttachmentKind, type FileEdit, type MessageAttachment, type ToolDefinition } from '../../common/state/protocol/state.js'; import { ActionType, type SessionAction } from '../../common/state/sessionActions.js'; -import { ResponsePartKind, SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, type PendingMessage, type SessionInputAnswer, type SessionInputOption, type SessionInputQuestion, type SessionInputRequest, type ToolCallResult, type ToolResultContent, type Turn, type URI as ProtocolURI, type UsageInfo } from '../../common/state/sessionState.js'; +import { ResponsePartKind, SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, type PendingMessage, type SessionInputAnswer, type SessionInputOption, type SessionInputQuestion, type SessionInputRequest, type ToolCallResult, type ToolResultContent, type Turn, type UsageInfo } from '../../common/state/sessionState.js'; import { IAgentConfigurationService } from '../agentConfigurationService.js'; import type { IExitPlanModeRequestParams, IExitPlanModeResponse } from './copilotAgent.js'; import { CopilotSessionWrapper } from './copilotSessionWrapper.js'; @@ -430,7 +430,6 @@ export class CopilotAgentSession extends Disposable { this._emitAction({ type: ActionType.SessionToolCallContentChanged, - session: this._protocolSession(), turnId: this._turnId, toolCallId, content: tracked.content, @@ -442,10 +441,6 @@ export class CopilotAgentSession extends Disposable { // ---- AgentSignal helpers ------------------------------------------------ - private _protocolSession(): ProtocolURI { - return this.sessionUri.toString(); - } - /** Wraps a {@link SessionAction} in an {@link AgentSignal} envelope and emits it. */ private _emitAction(action: SessionAction, parentToolCallId?: string): void { this._onDidSessionProgress.fire({ @@ -533,7 +528,6 @@ export class CopilotAgentSession extends Disposable { * markdown response part; subsequent deltas append to it. */ private _emitMarkdownDelta(content: string, parentToolCallId?: string): void { - const session = this._protocolSession(); const markdownScope = parentToolCallId ?? ''; let partId = this._currentMarkdownPartIds.get(markdownScope); if (!partId) { @@ -541,7 +535,6 @@ export class CopilotAgentSession extends Disposable { this._currentMarkdownPartIds.set(markdownScope, partId); this._emitAction({ type: ActionType.SessionResponsePart, - session, turnId: this._turnId, part: { kind: ResponsePartKind.Markdown, id: partId, content }, }, parentToolCallId); @@ -549,7 +542,6 @@ export class CopilotAgentSession extends Disposable { } this._emitAction({ type: ActionType.SessionDelta, - session, turnId: this._turnId, partId, content, @@ -558,7 +550,6 @@ export class CopilotAgentSession extends Disposable { /** Emits a reasoning delta, similar to {@link _emitMarkdownDelta} but for reasoning parts. */ private _emitReasoningDelta(content: string, parentToolCallId?: string): void { - const session = this._protocolSession(); const reasoningScope = parentToolCallId ?? ''; let partId = this._currentReasoningPartIds.get(reasoningScope); if (!partId) { @@ -566,7 +557,6 @@ export class CopilotAgentSession extends Disposable { this._currentReasoningPartIds.set(reasoningScope, partId); this._emitAction({ type: ActionType.SessionResponsePart, - session, turnId: this._turnId, part: { kind: ResponsePartKind.Reasoning, id: partId, content }, }, parentToolCallId); @@ -574,7 +564,6 @@ export class CopilotAgentSession extends Disposable { } this._emitAction({ type: ActionType.SessionReasoning, - session, turnId: this._turnId, partId, content, @@ -1138,7 +1127,6 @@ export class CopilotAgentSession extends Disposable { this._emitAction({ type: ActionType.SessionInputRequested, - session: this._protocolSession(), request: inputRequest, }); @@ -1215,7 +1203,6 @@ export class CopilotAgentSession extends Disposable { this._emitAction({ type: ActionType.SessionInputRequested, - session: this._protocolSession(), request: inputRequest, }); @@ -1428,7 +1415,6 @@ export class CopilotAgentSession extends Disposable { this._currentMarkdownPartIds.set(markdownScope, partId); this._emitAction({ type: ActionType.SessionResponsePart, - session: this._protocolSession(), turnId: this._turnId, part: { kind: ResponsePartKind.Markdown, id: partId, content: e.data.content }, }, parentToolCallId); @@ -1447,7 +1433,6 @@ export class CopilotAgentSession extends Disposable { this._hasReportedActivity = true; this._emitAction({ type: ActionType.SessionActivityChanged, - session: this._protocolSession(), activity: intent, }); } @@ -1501,10 +1486,8 @@ export class CopilotAgentSession extends Disposable { meta.mcpToolName = e.data.mcpToolName; } - const protocolSession = this._protocolSession(); this._emitAction({ type: ActionType.SessionToolCallStart, - session: protocolSession, turnId: this._turnId, toolCallId: e.data.toolCallId, toolName: e.data.toolName, @@ -1522,7 +1505,6 @@ export class CopilotAgentSession extends Disposable { this._emitAction({ type: ActionType.SessionToolCallReady, - session: protocolSession, turnId: this._turnId, toolCallId: e.data.toolCallId, invocationMessage: getInvocationMessage(e.data.toolName, displayName, parameters), @@ -1580,7 +1562,6 @@ export class CopilotAgentSession extends Disposable { this._sendToolInvokedTelemetry(e.data.success, e.data.error?.code, tracked); this._emitAction({ type: ActionType.SessionToolCallComplete, - session: this._protocolSession(), turnId: this._turnId, toolCallId: e.data.toolCallId, result: { @@ -1601,13 +1582,11 @@ export class CopilotAgentSession extends Disposable { this._hasReportedActivity = false; this._emitAction({ type: ActionType.SessionActivityChanged, - session: this._protocolSession(), activity: undefined, }); } this._emitAction({ type: ActionType.SessionTurnComplete, - session: this._protocolSession(), turnId: this._turnId, }); })); @@ -1619,10 +1598,8 @@ export class CopilotAgentSession extends Disposable { this._register(wrapper.onSkillInvoked(e => { this._logService.info(`[Copilot:${sessionId}] Skill invoked: ${e.data.name} (${e.data.path})`); const synth = synthesizeSkillToolCall(e.data, e.id); - const protocolSession = this._protocolSession(); this._emitAction({ type: ActionType.SessionToolCallStart, - session: protocolSession, turnId: this._turnId, toolCallId: synth.toolCallId, toolName: synth.toolName, @@ -1630,7 +1607,6 @@ export class CopilotAgentSession extends Disposable { }); this._emitAction({ type: ActionType.SessionToolCallReady, - session: protocolSession, turnId: this._turnId, toolCallId: synth.toolCallId, invocationMessage: synth.invocationMessage, @@ -1638,7 +1614,6 @@ export class CopilotAgentSession extends Disposable { }); this._emitAction({ type: ActionType.SessionToolCallComplete, - session: protocolSession, turnId: this._turnId, toolCallId: synth.toolCallId, result: { @@ -1667,7 +1642,6 @@ export class CopilotAgentSession extends Disposable { this._logService.error(`[Copilot:${sessionId}] Session error: ${e.data.errorType} - ${e.data.message}`); this._emitAction({ type: ActionType.SessionError, - session: this._protocolSession(), turnId: this._turnId, error: { errorType: e.data.errorType, @@ -1699,7 +1673,6 @@ export class CopilotAgentSession extends Disposable { }; this._emitAction({ type: ActionType.SessionUsage, - session: this._protocolSession(), turnId: this._turnId, usage, }); @@ -1840,7 +1813,6 @@ export class CopilotAgentSession extends Disposable { session: this.sessionUri, action: { type: ActionType.SessionInputRequested, - session: this.sessionUri.toString(), request: inputRequest, } }); diff --git a/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts b/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts index 2d91a04ac49..d78215194e5 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts @@ -191,7 +191,7 @@ export class ShellManager extends Disposable { const executable = await this.getResolvedExecutable(); await this._terminalManager.createTerminal({ - terminal: terminalUri, + channel: terminalUri, claim, name: shellDisplayName, cwd: cwd ?? this._workingDirectory?.fsPath, diff --git a/src/vs/platform/agentHost/node/copilot/copilotSlashCommandCompletionProvider.ts b/src/vs/platform/agentHost/node/copilot/copilotSlashCommandCompletionProvider.ts index 017e256cd1f..dff508494c6 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotSlashCommandCompletionProvider.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotSlashCommandCompletionProvider.ts @@ -80,7 +80,7 @@ export class CopilotSlashCommandCompletionProvider implements IAgentHostCompleti constructor(private readonly copilotcliId: string, private readonly _sessionInfo?: ICopilotSlashCommandSessionInfo) { } async provideCompletionItems(params: CompletionsParams, _token: CancellationToken): Promise { - if (AgentSession.provider(params.session) !== this.copilotcliId) { + if (AgentSession.provider(params.channel) !== this.copilotcliId) { return []; } const leading = extractLeadingSlashToken(params.text, params.offset); @@ -89,7 +89,7 @@ export class CopilotSlashCommandCompletionProvider implements IAgentHostCompleti } // Raw session id is the URI path without the leading slash. - const sessionId = AgentSession.id(params.session); + const sessionId = AgentSession.id(params.channel); const hasHistory = this._sessionInfo?.hasHistory(sessionId) ?? true; // `/abc` → typed = 'abc'; empty after just '/' → typed = ''. diff --git a/src/vs/platform/agentHost/node/protocolServerHandler.ts b/src/vs/platform/agentHost/node/protocolServerHandler.ts index a19af7b6b9a..31ac8dfb105 100644 --- a/src/vs/platform/agentHost/node/protocolServerHandler.ts +++ b/src/vs/platform/agentHost/node/protocolServerHandler.ts @@ -12,7 +12,7 @@ import { ILogService } from '../../log/common/log.js'; import { AHPFileSystemProvider } from '../common/agentHostFileSystemProvider.js'; import { AgentSession, type IAgentService } from '../common/agentService.js'; import type { CommandMap } from '../common/state/protocol/messages.js'; -import { ActionEnvelope, ActionType, INotification, isChangesetAction, isSessionAction, isTerminalAction, type SessionAction, type TerminalAction, type IRootConfigChangedAction } from '../common/state/sessionActions.js'; +import { ActionEnvelope, ActionType, INotification, isSessionAction, isTerminalAction, type SessionAction, type TerminalAction, type IRootConfigChangedAction } from '../common/state/sessionActions.js'; import { PROTOCOL_VERSION } from '../common/state/protocol/version/registry.js'; import { negotiateProtocolVersion } from '../common/state/protocol/version/negotiation.js'; import { VSCODE_UPGRADE_METHOD, type UnsupportedProtocolVersionErrorDataEx } from '../common/state/protocolUpgrade.js'; @@ -34,7 +34,7 @@ import { type ReconnectParams, type IStateSnapshot, } from '../common/state/sessionProtocol.js'; -import { ChangesetOperationScope, ChangesetOperationTargetKind, ResponsePartKind, ROOT_STATE_URI, SessionStatus, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, type SessionState } from '../common/state/sessionState.js'; +import { ChangesetOperationScope, ChangesetOperationTargetKind, isAhpRootChannel, ResponsePartKind, SessionStatus, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, type SessionState } from '../common/state/sessionState.js'; import type { IProtocolServer, IProtocolTransport } from '../common/state/sessionTransport.js'; import { AgentHostStateManager } from './agentHostStateManager.js'; @@ -211,9 +211,9 @@ export class ProtocolServerHandler extends Disposable { switch (msg.method) { case 'unsubscribe': if (client) { - const resource = msg.params.resource; - if (client.subscriptions.delete(resource)) { - this._agentService.unsubscribe(URI.parse(resource), client.clientId); + const channel = msg.params.channel; + if (client.subscriptions.delete(channel)) { + this._agentService.unsubscribe(URI.parse(channel), client.clientId); } } break; @@ -221,8 +221,9 @@ export class ProtocolServerHandler extends Disposable { if (client) { this._logService.trace(`[ProtocolServer] dispatchAction: ${JSON.stringify(msg.params.action.type)}`); const action = msg.params.action as SessionAction | TerminalAction | IRootConfigChangedAction; + const channel = msg.params.channel; if (isSessionAction(action) || isTerminalAction(action) || action.type === ActionType.RootConfigChanged) { - this._agentService.dispatchAction(action, client.clientId, msg.params.clientSeq); + this._agentService.dispatchAction(channel, action, client.clientId, msg.params.clientSeq); } } break; @@ -444,9 +445,8 @@ export class ProtocolServerHandler extends Disposable { const state = this._stateManager.getSessionState(session); const ownsPendingToolCall = state ? this._hasPendingClientToolCall(state, clientId) : false; if (state?.activeClient?.clientId === clientId) { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(session, { type: ActionType.SessionActiveClientChanged, - session, activeClient: null, }); } @@ -509,18 +509,16 @@ export class ProtocolServerHandler extends Disposable { if (toolCall.toolClientId === clientId && (toolCall.status === ToolCallStatus.Streaming || toolCall.status === ToolCallStatus.Running || toolCall.status === ToolCallStatus.PendingConfirmation)) { const mayRetryWithReplacementClient = this._hasReplacementActiveClientTool(state, clientId, toolCall.toolName); if (toolCall.status === ToolCallStatus.Streaming) { - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(session, { type: ActionType.SessionToolCallReady, - session, turnId: activeTurn.id, toolCallId: toolCall.toolCallId, invocationMessage: toolCall.invocationMessage ?? toolCall.displayName, confirmed: ToolCallConfirmationReason.NotNeeded, }); } - this._stateManager.dispatchServerAction({ + this._stateManager.dispatchServerAction(session, { type: ActionType.SessionToolCallComplete, - session, turnId: activeTurn.id, toolCallId: toolCall.toolCallId, result: { @@ -543,15 +541,15 @@ export class ProtocolServerHandler extends Disposable { private readonly _requestHandlers: RequestHandlerMap = { subscribe: async (client, params) => { try { - const snapshot = await this._agentService.subscribe(URI.parse(params.resource), client.clientId); - client.subscriptions.add(params.resource); - this._clearClientToolCallDisconnectTimeout(client.clientId, params.resource); + const snapshot = await this._agentService.subscribe(URI.parse(params.channel), client.clientId); + client.subscriptions.add(params.channel); + this._clearClientToolCallDisconnectTimeout(client.clientId, params.channel); return { snapshot }; } catch (err) { if (err instanceof ProtocolError) { throw err; } - throw new ProtocolError(AHP_SESSION_NOT_FOUND, `Resource not found: ${params.resource}`); + throw new ProtocolError(AHP_SESSION_NOT_FOUND, `Resource not found: ${params.channel}`); } }, createSession: async (_client, params) => { @@ -580,7 +578,7 @@ export class ProtocolServerHandler extends Disposable { provider: params.provider, model: params.model, workingDirectory: params.workingDirectory ? URI.parse(params.workingDirectory) : undefined, - session: URI.parse(params.session), + session: URI.parse(params.channel), fork, config: params.config, activeClient: params.activeClient, @@ -592,13 +590,13 @@ export class ProtocolServerHandler extends Disposable { throw new ProtocolError(AHP_PROVIDER_NOT_FOUND, err instanceof Error ? err.message : String(err)); } // Verify the provider honored the client-chosen session URI per the protocol contract - if (createdSession.toString() !== URI.parse(params.session).toString()) { - this._logService.warn(`[ProtocolServer] createSession: provider returned URI ${createdSession.toString()} but client requested ${params.session}`); + if (createdSession.toString() !== URI.parse(params.channel).toString()) { + this._logService.warn(`[ProtocolServer] createSession: provider returned URI ${createdSession.toString()} but client requested ${params.channel}`); } return null; }, disposeSession: async (_client, params) => { - await this._agentService.disposeSession(URI.parse(params.session)); + await this._agentService.disposeSession(URI.parse(params.channel)); return null; }, resourceWrite: async (_client, params) => { @@ -655,9 +653,9 @@ export class ProtocolServerHandler extends Disposable { return this._agentService.completions(params); }, fetchTurns: async (_client, params) => { - const state = this._stateManager.getSessionState(params.session); + const state = this._stateManager.getSessionState(params.channel); if (!state) { - throw new ProtocolError(AHP_SESSION_NOT_FOUND, `Session not found: ${params.session}`); + throw new ProtocolError(AHP_SESSION_NOT_FOUND, `Session not found: ${params.channel}`); } const turns = state.turns; const limit = Math.min(params.limit ?? 50, 100); @@ -709,7 +707,7 @@ export class ProtocolServerHandler extends Disposable { return null; }, disposeTerminal: async (_client, params) => { - await this._agentService.disposeTerminal(URI.parse(params.terminal)); + await this._agentService.disposeTerminal(URI.parse(params.channel)); return null; }, invokeChangesetOperation: async (_client, params) => { @@ -720,13 +718,13 @@ export class ProtocolServerHandler extends Disposable { // boilerplate, then rejects the request with a JSON-RPC error // for the "no handler" case. See the Changesets spec section // "Changeset Operations" for the contract. - const state = this._stateManager.getChangesetState(params.changeset); + const state = this._stateManager.getChangesetState(params.channel); if (!state) { - throw new ProtocolError(AHP_SESSION_NOT_FOUND, `Changeset not found: ${params.changeset}`); + throw new ProtocolError(AHP_SESSION_NOT_FOUND, `Changeset not found: ${params.channel}`); } const op = state.operations?.find(o => o.id === params.operationId); if (!op) { - throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, `Unknown operation '${params.operationId}' on changeset ${params.changeset}`); + throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, `Unknown operation '${params.operationId}' on changeset ${params.channel}`); } const targetKind: ChangesetOperationScope = params.target?.kind === ChangesetOperationTargetKind.Resource ? ChangesetOperationScope.Resource @@ -736,7 +734,7 @@ export class ProtocolServerHandler extends Disposable { if (!op.scopes.includes(targetKind)) { throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, `Operation '${params.operationId}' does not support scope '${targetKind}' (allowed: ${op.scopes.join(', ')})`); } - throw new ProtocolError(JsonRpcErrorCodes.InternalError, `No operation handler registered for '${params.operationId}' on changeset ${params.changeset}`); + throw new ProtocolError(JsonRpcErrorCodes.InternalError, `No operation handler registered for '${params.operationId}' on changeset ${params.channel}`); }, }; @@ -831,27 +829,32 @@ export class ProtocolServerHandler extends Disposable { } private _broadcastNotification(notification: INotification): void { - const msg: AhpServerNotification<'notification'> = { jsonrpc: '2.0', method: 'notification', params: { notification } }; + // Each protocol notification now ships as its own top-level method. The + // `type` discriminant on our local {@link ProtocolNotification} union is + // the wire-level method name, so we can route it directly. + const { type, ...params } = notification; + // eslint-disable-next-line local/code-no-dangerous-type-assertions + const msg = { jsonrpc: '2.0', method: type, params } as AhpServerNotification; for (const client of this._clients.values()) { client.transport.send(msg); } } private _isRelevantToClient(client: IConnectedClient, envelope: ActionEnvelope): boolean { - const action = envelope.action; - if (action.type.startsWith('root/')) { - return client.subscriptions.has(ROOT_STATE_URI); + // The root channel has two equivalent string forms (`ahp-root://` and + // the URI-roundtripped `ahp-root:`). Treat them interchangeably so a + // client that subscribed with either form receives root broadcasts + // regardless of which form the envelope carries. See + // {@link isAhpRootChannel}. + if (isAhpRootChannel(envelope.channel)) { + for (const sub of client.subscriptions) { + if (isAhpRootChannel(sub)) { + return true; + } + } + return false; } - if (isSessionAction(action)) { - return client.subscriptions.has(action.session); - } - if (isChangesetAction(action)) { - return client.subscriptions.has(action.changeset); - } - if (isTerminalAction(action)) { - return client.subscriptions.has(action.terminal); - } - return false; + return client.subscriptions.has(envelope.channel); } override dispose(): void { diff --git a/src/vs/platform/agentHost/node/sessionPermissions.ts b/src/vs/platform/agentHost/node/sessionPermissions.ts index 254776accfc..da867afaed3 100644 --- a/src/vs/platform/agentHost/node/sessionPermissions.ts +++ b/src/vs/platform/agentHost/node/sessionPermissions.ts @@ -172,12 +172,11 @@ export class SessionPermissionManager extends Disposable { * (the protocol state carries `confirmationTitle`), the standard * confirmation options are baked in so clients can render them directly. */ - createToolReadyAction(e: IAgentToolPendingConfirmationSignal, sessionKey: ProtocolURI, turnId: string): IToolCallReadyAction { + createToolReadyAction(e: IAgentToolPendingConfirmationSignal, _sessionKey: ProtocolURI, turnId: string): IToolCallReadyAction { const state = e.state; if (state.confirmationTitle) { return { type: ActionType.SessionToolCallReady, - session: sessionKey, turnId, toolCallId: state.toolCallId, invocationMessage: state.invocationMessage, @@ -193,7 +192,6 @@ export class SessionPermissionManager extends Disposable { } return { type: ActionType.SessionToolCallReady, - session: sessionKey, turnId, toolCallId: state.toolCallId, invocationMessage: state.invocationMessage, diff --git a/src/vs/platform/agentHost/test/common/agentHostFileSystemProvider.test.ts b/src/vs/platform/agentHost/test/common/agentHostFileSystemProvider.test.ts index 2e08090a4d6..addd578e3ea 100644 --- a/src/vs/platform/agentHost/test/common/agentHostFileSystemProvider.test.ts +++ b/src/vs/platform/agentHost/test/common/agentHostFileSystemProvider.test.ts @@ -13,6 +13,7 @@ import { AGENT_HOST_LABEL_FORMATTER, AGENT_HOST_SCHEME, agentHostAuthority, from import { ContentEncoding, type ResourceListResult, type ResourceReadResult, type ResourceRequestParams, type ResourceRequestResult } from '../../common/state/protocol/commands.js'; import { AhpErrorCodes } from '../../common/state/protocol/errors.js'; import { ProtocolError } from '../../common/state/sessionProtocol.js'; +import { ROOT_STATE_URI } from '../../common/state/sessionState.js'; suite('AgentHostFileSystemProvider - URI helpers', () => { @@ -425,7 +426,7 @@ suite('AgentHostFileSystemProvider - permission errors and requestResourceAccess await provider.requestResourceAccess(wrapped, { read: true, write: true }); assert.deepStrictEqual(connection.requestCalls, [ - { uri: URI.file('/etc/foo').toString(), read: true, write: true }, + { channel: ROOT_STATE_URI, uri: URI.file('/etc/foo').toString(), read: true, write: true }, ]); }); diff --git a/src/vs/platform/agentHost/test/common/agentSubscription.test.ts b/src/vs/platform/agentHost/test/common/agentSubscription.test.ts index ccd2015f100..96865293c4c 100644 --- a/src/vs/platform/agentHost/test/common/agentSubscription.test.ts +++ b/src/vs/platform/agentHost/test/common/agentSubscription.test.ts @@ -9,7 +9,7 @@ import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { ActionType, type ActionEnvelope } from '../../common/state/sessionActions.js'; import { SessionLifecycle, SessionStatus, TerminalClaimKind, type RootState, type SessionState, type TerminalState } from '../../common/state/protocol/state.js'; -import { StateComponents } from '../../common/state/sessionState.js'; +import { ROOT_STATE_URI, StateComponents } from '../../common/state/sessionState.js'; import { AgentSubscriptionManager, RootStateSubscription, SessionStateSubscription, TerminalStateSubscription } from '../../common/state/agentSubscription.js'; // Helpers @@ -49,13 +49,20 @@ function makeTerminalState(overrides?: Partial): TerminalState { }; } -function makeEnvelope(action: ActionEnvelope['action'], serverSeq: number, origin?: ActionEnvelope['origin'], rejectionReason?: string): ActionEnvelope { - return { action, serverSeq, origin, rejectionReason }; +function makeEnvelope(action: ActionEnvelope['action'], serverSeq: number, origin?: ActionEnvelope['origin'], rejectionReason?: string, channel?: string): ActionEnvelope { + const resolvedChannel = channel ?? ( + action.type.startsWith('root/') ? ROOT_STATE_URI + : action.type.startsWith('terminal/') ? terminalUri + : action.type.startsWith('changeset/') ? changesetUri + : sessionUri + ); + return { channel: resolvedChannel, action, serverSeq, origin, rejectionReason }; } const noop = () => { }; const sessionUri = URI.from({ scheme: 'copilot', path: '/test-session' }).toString(); const terminalUri = URI.from({ scheme: 'agenthost-terminal', path: '/term1' }).toString(); +const changesetUri = `${sessionUri}/changeset/session`; // RootStateSubscription @@ -110,7 +117,7 @@ suite('RootStateSubscription', () => { const state = makeRootState(); sub.handleSnapshot(state, 0); sub.receiveEnvelope(makeEnvelope( - { type: ActionType.SessionReady, session: sessionUri }, + { type: ActionType.SessionReady, }, 1, )); assert.deepStrictEqual(sub.value, state); @@ -207,7 +214,6 @@ suite('SessionStateSubscription', () => { const clientSeq = sub.applyOptimistic({ type: ActionType.SessionTitleChanged, - session: sessionUri, title: 'Optimistic', }); @@ -223,13 +229,12 @@ suite('SessionStateSubscription', () => { const clientSeq = sub.applyOptimistic({ type: ActionType.SessionTitleChanged, - session: sessionUri, title: 'Optimistic', }); // Server confirms the action sub.receiveEnvelope(makeEnvelope( - { type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Optimistic' }, + { type: ActionType.SessionTitleChanged, title: 'Optimistic' }, 1, { clientId: 'c1', clientSeq }, )); @@ -246,13 +251,12 @@ suite('SessionStateSubscription', () => { const clientSeq = sub.applyOptimistic({ type: ActionType.SessionTitleChanged, - session: sessionUri, title: 'Optimistic', }); // Server rejects the action sub.receiveEnvelope(makeEnvelope( - { type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Optimistic' }, + { type: ActionType.SessionTitleChanged, title: 'Optimistic' }, 1, { clientId: 'c1', clientSeq }, 'denied', @@ -271,13 +275,12 @@ suite('SessionStateSubscription', () => { // Local optimistic action sub.applyOptimistic({ type: ActionType.SessionTitleChanged, - session: sessionUri, title: 'Local', }); // Foreign action arrives sub.receiveEnvelope(makeEnvelope( - { type: ActionType.SessionReady, session: sessionUri }, + { type: ActionType.SessionReady, }, 1, { clientId: 'other-client', clientSeq: 1 }, )); @@ -294,13 +297,12 @@ suite('SessionStateSubscription', () => { const clientSeq = sub.applyOptimistic({ type: ActionType.SessionTitleChanged, - session: sessionUri, title: 'Temp', }); // Confirm the pending action sub.receiveEnvelope(makeEnvelope( - { type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Temp' }, + { type: ActionType.SessionTitleChanged, title: 'Temp' }, 1, { clientId: 'c1', clientSeq }, )); @@ -315,7 +317,6 @@ suite('SessionStateSubscription', () => { sub.applyOptimistic({ type: ActionType.SessionTitleChanged, - session: sessionUri, title: 'Pending', }); @@ -332,8 +333,11 @@ suite('SessionStateSubscription', () => { sub.handleSnapshot(makeSessionState(sessionUri), 0); sub.receiveEnvelope(makeEnvelope( - { type: ActionType.SessionTitleChanged, session: 'copilot:///other', title: 'Other' }, + { type: ActionType.SessionTitleChanged, title: 'Other' }, 1, + undefined, + undefined, + 'copilot:/other-session', )); assert.strictEqual((sub.value as SessionState).summary.title, 'Test'); @@ -343,7 +347,7 @@ suite('SessionStateSubscription', () => { const sub = createSub(); sub.receiveEnvelope(makeEnvelope( - { type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Buffered' }, + { type: ActionType.SessionTitleChanged, title: 'Buffered' }, 2, )); @@ -363,7 +367,6 @@ suite('SessionStateSubscription', () => { sub.applyOptimistic({ type: ActionType.SessionTitleChanged, - session: sessionUri, title: 'Changed', }); @@ -393,7 +396,7 @@ suite('TerminalStateSubscription', () => { sub.handleSnapshot(makeTerminalState(), 0); sub.receiveEnvelope(makeEnvelope( - { type: ActionType.TerminalData, terminal: terminalUri, data: 'hello' }, + { type: ActionType.TerminalData, data: 'hello' }, 1, )); @@ -407,8 +410,11 @@ suite('TerminalStateSubscription', () => { sub.handleSnapshot(makeTerminalState(), 0); sub.receiveEnvelope(makeEnvelope( - { type: ActionType.TerminalData, terminal: 'agenthost-terminal:///other', data: 'nope' }, + { type: ActionType.TerminalData, data: 'nope' }, 1, + undefined, + undefined, + 'agenthost-terminal:/other-term', )); assert.deepStrictEqual((sub.value as TerminalState).content, []); @@ -551,7 +557,7 @@ suite('AgentSubscriptionManager', () => { // Send a session action mgr.receiveEnvelope(makeEnvelope( - { type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Routed' }, + { type: ActionType.SessionTitleChanged, title: 'Routed' }, 2, )); assert.strictEqual((ref.object.value as SessionState).summary.title, 'Routed'); @@ -589,9 +595,8 @@ suite('AgentSubscriptionManager', () => { const ref = mgr.getSubscription(StateComponents.Session, uri); await new Promise(r => setTimeout(r, 0)); - const clientSeq = mgr.dispatchOptimistic({ + const clientSeq = mgr.dispatchOptimistic(uri.toString(), { type: ActionType.SessionTitleChanged, - session: sessionUri, title: 'Dispatched', }); diff --git a/src/vs/platform/agentHost/test/electron-browser/localAhpJsonlLogging.test.ts b/src/vs/platform/agentHost/test/electron-browser/localAhpJsonlLogging.test.ts index 1204395df5b..92af1eca9a9 100644 --- a/src/vs/platform/agentHost/test/electron-browser/localAhpJsonlLogging.test.ts +++ b/src/vs/platform/agentHost/test/electron-browser/localAhpJsonlLogging.test.ts @@ -45,7 +45,7 @@ suite('localAhpJsonlLogging', () => { async createSession(config: unknown) { return URI.parse(`agent-host://session/1?cfg=${JSON.stringify(config)}`); }, async disposeTerminal() { return undefined; }, async resourceRead(uri: URI) { throw new Error('boom: ' + uri.toString()); }, - dispatchAction(action: unknown, clientId: string, clientSeq: number) { void action; void clientId; void clientSeq; }, + dispatchAction(channel: URI, action: unknown, clientId: string, clientSeq: number) { void channel; void action; void clientId; void clientSeq; }, get onDidAction() { return () => ({ dispose() { } }); }, } as unknown as IAgentService; @@ -56,7 +56,7 @@ suite('localAhpJsonlLogging', () => { await wrapped.createSession({ kind: 'demo' } as never); await wrapped.disposeTerminal(URI.parse('agent-host://terminal/1')); await assert.rejects(() => wrapped.resourceRead(URI.parse('agent-host://x/y'))); - wrapped.dispatchAction({ type: 'noop' } as never, 'client-1', 7); + wrapped.dispatchAction('agent-host://session/1', { type: 'noop' } as never, 'client-1', 7); // Event accessors must pass through untouched (no log emitted, no wrapping). const eventFn = wrapped.onDidAction; diff --git a/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts b/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts index 2214e5df51a..3fb95f24283 100644 --- a/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts +++ b/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts @@ -117,7 +117,7 @@ suite('RemoteAgentHostProtocolClient', () => { jsonrpc: '2.0', id: 1, method: 'resourceList', - params: { uri: URI.file('/workspace').toString() }, + params: { channel: 'ahp-root://', uri: URI.file('/workspace').toString() }, }); transport.fireMessage({ jsonrpc: '2.0', id: 1, result: { entries: [] } }); @@ -321,13 +321,12 @@ suite('RemoteAgentHostProtocolClient', () => { // Late notification — must not fan out as an action event. const lateAction: SessionActiveClientChangedAction = { type: ActionType.SessionActiveClientChanged, - session: 'session://test/late', activeClient: null, }; transport.fireMessage({ jsonrpc: '2.0', method: 'action', - params: { action: lateAction, serverSeq: 1, origin: undefined } + params: { channel: 'ahp-session:/test', action: lateAction, serverSeq: 1, origin: undefined } }); assert.strictEqual(actionCount, 0, 'late action notifications must be ignored after close'); @@ -389,6 +388,7 @@ suite('RemoteAgentHostProtocolClient', () => { jsonrpc: '2.0', method: 'dispatchAction', params: { + channel: ROOT_STATE_URI, clientSeq: 0, action: { type: ActionType.RootConfigChanged, @@ -477,7 +477,7 @@ suite('RemoteAgentHostProtocolClient', () => { const { transport } = createClient(undefined, createPermissionService(false)); const uri = URI.file('/etc/passwd').toString(); - transport.fireMessage({ jsonrpc: '2.0', id: 42, method: 'resourceRead', params: { uri } }); + transport.fireMessage({ jsonrpc: '2.0', id: 42, method: 'resourceRead', params: { channel: 'ahp-root://', uri } }); await new Promise(resolve => setTimeout(resolve, 0)); assert.deepStrictEqual(transport.sentMessages.pop(), { @@ -486,7 +486,7 @@ suite('RemoteAgentHostProtocolClient', () => { error: { code: AhpErrorCodes.PermissionDenied, message: `Access to ${uri} is not granted.`, - data: { request: { uri, read: true } }, + data: { request: { channel: ROOT_STATE_URI, uri, read: true } }, }, }); }); @@ -495,7 +495,7 @@ suite('RemoteAgentHostProtocolClient', () => { const { transport } = createClient(undefined, createPermissionService(false)); const uri = URI.file('/etc/passwd').toString(); - transport.fireMessage({ jsonrpc: '2.0', id: 7, method: 'resourceWrite', params: { uri, data: 'aGVsbG8=', encoding: ContentEncoding.Base64 } }); + transport.fireMessage({ jsonrpc: '2.0', id: 7, method: 'resourceWrite', params: { channel: 'ahp-root://', uri, data: 'aGVsbG8=', encoding: ContentEncoding.Base64 } }); await new Promise(resolve => setTimeout(resolve, 0)); assert.deepStrictEqual(transport.sentMessages.pop(), { @@ -504,7 +504,7 @@ suite('RemoteAgentHostProtocolClient', () => { error: { code: AhpErrorCodes.PermissionDenied, message: `Access to ${uri} is not granted.`, - data: { request: { uri, write: true } }, + data: { request: { channel: ROOT_STATE_URI, uri, write: true } }, }, }); }); @@ -513,7 +513,7 @@ suite('RemoteAgentHostProtocolClient', () => { const { transport } = createClient(undefined, createPermissionService(false)); const uri = URI.file('/etc').toString(); - transport.fireMessage({ jsonrpc: '2.0', id: 5, method: 'resourceList', params: { uri } }); + transport.fireMessage({ jsonrpc: '2.0', id: 5, method: 'resourceList', params: { channel: 'ahp-root://', uri } }); await new Promise(resolve => setTimeout(resolve, 0)); assert.deepStrictEqual(transport.sentMessages.pop(), { @@ -522,7 +522,7 @@ suite('RemoteAgentHostProtocolClient', () => { error: { code: AhpErrorCodes.PermissionDenied, message: `Access to ${uri} is not granted.`, - data: { request: { uri, read: true } }, + data: { request: { channel: ROOT_STATE_URI, uri, read: true } }, }, }); }); @@ -531,7 +531,7 @@ suite('RemoteAgentHostProtocolClient', () => { const { transport } = createClient(undefined, createPermissionService(false)); const uri = URI.file('/etc/passwd').toString(); - transport.fireMessage({ jsonrpc: '2.0', id: 8, method: 'resourceDelete', params: { uri } }); + transport.fireMessage({ jsonrpc: '2.0', id: 8, method: 'resourceDelete', params: { channel: 'ahp-root://', uri } }); await new Promise(resolve => setTimeout(resolve, 0)); assert.deepStrictEqual(transport.sentMessages.pop(), { @@ -540,7 +540,7 @@ suite('RemoteAgentHostProtocolClient', () => { error: { code: AhpErrorCodes.PermissionDenied, message: `Access to ${uri} is not granted.`, - data: { request: { uri, write: true } }, + data: { request: { channel: ROOT_STATE_URI, uri, write: true } }, }, }); }); @@ -554,7 +554,7 @@ suite('RemoteAgentHostProtocolClient', () => { }; const { transport } = createClient(undefined, stub); - transport.fireMessage({ jsonrpc: '2.0', id: 9, method: 'resourceMove', params: { source: sourceUri, destination: destUri } }); + transport.fireMessage({ jsonrpc: '2.0', id: 9, method: 'resourceMove', params: { channel: 'ahp-root://', source: sourceUri, destination: destUri } }); await new Promise(resolve => setTimeout(resolve, 0)); assert.deepStrictEqual(transport.sentMessages.pop(), { @@ -563,13 +563,13 @@ suite('RemoteAgentHostProtocolClient', () => { error: { code: AhpErrorCodes.PermissionDenied, message: `Access to ${destUri} is not granted.`, - data: { request: { uri: destUri, write: true } }, + data: { request: { channel: ROOT_STATE_URI, uri: destUri, write: true } }, }, }); }); test('reverse resourceRequest delegates to permission service and replies with empty result', async () => { - let lastRequest: { address: string; params: { uri: string; read?: boolean; write?: boolean } } | undefined; + let lastRequest: { address: string; params: { channel: 'ahp-root://'; uri: string; read?: boolean; write?: boolean } } | undefined; const stub: ReturnType = { ...createPermissionService(false), request: async (address, params) => { lastRequest = { address, params }; }, @@ -577,12 +577,12 @@ suite('RemoteAgentHostProtocolClient', () => { const { transport } = createClient(undefined, stub); const uri = URI.file('/etc/foo').toString(); - transport.fireMessage({ jsonrpc: '2.0', id: 11, method: 'resourceRequest', params: { uri, read: true } }); + transport.fireMessage({ jsonrpc: '2.0', id: 11, method: 'resourceRequest', params: { channel: 'ahp-root://', uri, read: true } }); // Allow the awaited request promise to resolve. await new Promise(resolve => setTimeout(resolve, 0)); - assert.deepStrictEqual(lastRequest, { address: 'test.example:1234', params: { uri, read: true } }); + assert.deepStrictEqual(lastRequest, { address: 'test.example:1234', params: { channel: 'ahp-root://', uri, read: true } }); assert.deepStrictEqual(transport.sentMessages.pop(), { jsonrpc: '2.0', id: 11, result: {} }); }); @@ -594,7 +594,7 @@ suite('RemoteAgentHostProtocolClient', () => { const { transport } = createClient(undefined, stub); const uri = URI.file('/etc/foo').toString(); - transport.fireMessage({ jsonrpc: '2.0', id: 12, method: 'resourceRequest', params: { uri, read: true } }); + transport.fireMessage({ jsonrpc: '2.0', id: 12, method: 'resourceRequest', params: { channel: 'ahp-root://', uri, read: true } }); await new Promise(resolve => setTimeout(resolve, 0)); @@ -634,17 +634,17 @@ suite('RemoteAgentHostProtocolClient', () => { test('SessionActiveClientChanged dispatches implicit reads for each customization', () => { const { service, calls } = createCapturingPermissionService(); const { client } = createClient(undefined, service); + const sessionUri = URI.parse('ahp-session:/test'); - client.dispatch({ + client.dispatch(sessionUri.toString(), { type: ActionType.SessionActiveClientChanged, - session: 'session://test/1', activeClient: { clientId: 'c1', tools: [], customizations: [ { uri: 'file:///plugins/foo', displayName: 'Foo' }, { uri: 'file:///plugins/bar', displayName: 'Bar' }, - ], + ] }, }); @@ -660,21 +660,21 @@ suite('RemoteAgentHostProtocolClient', () => { test('repeat dispatch dedupes per URI', () => { const { service, calls } = createCapturingPermissionService(); const { client } = createClient(undefined, service); + const sessionUri = URI.parse('ahp-session:/test'); const action: SessionActiveClientChangedAction = { type: ActionType.SessionActiveClientChanged, - session: 'session://test/1', activeClient: { clientId: 'c1', tools: [], customizations: [ { uri: 'file:///plugins/foo', displayName: 'Foo' }, - ], + ] }, }; - client.dispatch(action); - client.dispatch(action); + client.dispatch(sessionUri.toString(), action); + client.dispatch(sessionUri.toString(), action); assert.strictEqual(calls.length, 1); }); @@ -682,10 +682,10 @@ suite('RemoteAgentHostProtocolClient', () => { test('null activeClient does not crash', () => { const { service, calls } = createCapturingPermissionService(); const { client } = createClient(undefined, service); + const sessionUri = URI.parse('ahp-session:/test'); - client.dispatch({ + client.dispatch(sessionUri.toString(), { type: ActionType.SessionActiveClientChanged, - session: 'session://test/1', activeClient: null, }); @@ -861,10 +861,9 @@ suite('RemoteAgentHostProtocolClient', () => { // Dispatch an optimistic action right before the transport drops. const action: SessionTitleChangedAction = { type: ActionType.SessionTitleChanged, - session: sessionUri.toString(), title: 'Renamed by user', }; - client.dispatch(action); + client.dispatch(sessionUri.toString(), action); const initialDispatch = findDispatchAction(transports[0], ActionType.SessionTitleChanged); assert.ok(initialDispatch, 'optimistic dispatch should reach the original transport'); const initialSeq = (initialDispatch.params as { clientSeq: number }).clientSeq; @@ -909,10 +908,9 @@ suite('RemoteAgentHostProtocolClient', () => { const action: SessionTitleChangedAction = { type: ActionType.SessionTitleChanged, - session: sessionUri.toString(), title: 'Echoed back', }; - client.dispatch(action); + client.dispatch(sessionUri.toString(), action); const initialDispatch = findDispatchAction(transports[0], ActionType.SessionTitleChanged)!; const initialSeq = (initialDispatch.params as { clientSeq: number }).clientSeq; @@ -928,6 +926,7 @@ suite('RemoteAgentHostProtocolClient', () => { result: { type: ReconnectResultType.Replay, actions: [{ + channel: sessionUri.toString(), action, serverSeq: 6, origin: { clientId: client.clientId, clientSeq: initialSeq }, @@ -999,10 +998,9 @@ suite('RemoteAgentHostProtocolClient', () => { const action: SessionTitleChangedAction = { type: ActionType.SessionTitleChanged, - session: sessionUri.toString(), title: 'Rejected change', }; - client.dispatch(action); + client.dispatch(sessionUri.toString(), action); const initialDispatch = findDispatchAction(transports[0], ActionType.SessionTitleChanged)!; const initialSeq = (initialDispatch.params as { clientSeq: number }).clientSeq; @@ -1018,6 +1016,7 @@ suite('RemoteAgentHostProtocolClient', () => { result: { type: ReconnectResultType.Replay, actions: [{ + channel: sessionUri.toString(), action, serverSeq: 6, origin: { clientId: client.clientId, clientSeq: initialSeq }, @@ -1121,9 +1120,8 @@ suite('RemoteAgentHostProtocolClient', () => { // optimistic replay path for terminal/root actions; the only way // these reach the server is via the notification gate. const terminalUri = URI.parse('agenthost-terminal:/term-1'); - client.dispatch({ + client.dispatch(terminalUri.toString(), { type: ActionType.TerminalInput, - terminal: terminalUri.toString(), data: 'echo hello\n', }); diff --git a/src/vs/platform/agentHost/test/node/agentHostChangesetService.test.ts b/src/vs/platform/agentHost/test/node/agentHostChangesetService.test.ts index 4d10d3d30c5..79129e3ee28 100644 --- a/src/vs/platform/agentHost/test/node/agentHostChangesetService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostChangesetService.test.ts @@ -40,7 +40,7 @@ suite('AgentHostChangesetService', () => { workingDirectory, changesets: buildDefaultChangesetCatalogue(sessionUri.toString()), }); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri.toString() }); + stateManager.dispatchServerAction(sessionUri.toString(), { type: ActionType.SessionReady, }); } setup(() => { @@ -289,12 +289,11 @@ suite('AgentHostChangesetService', () => { // Walk the captured stream and reconstruct the per-changeset // file lists to assert each matches the git service output. const fileSets = envelopes - .map(e => e.action) - .filter(a => a.type === ActionType.ChangesetFileSet) as Array<{ changeset: string; file: { edit: unknown } }>; - const sessionFileSets = fileSets.filter(a => a.changeset === `${sessionUri.toString()}/changeset/session`); - const uncommittedFileSets = fileSets.filter(a => a.changeset === `${sessionUri.toString()}/changeset/uncommitted`); - assert.deepStrictEqual(sessionFileSets.map(a => a.file.edit), gitDiffs); - assert.deepStrictEqual(uncommittedFileSets.map(a => a.file.edit), gitDiffs); + .filter(e => e.action.type === ActionType.ChangesetFileSet) as Array<{ channel: string; action: { file: { edit: unknown } } }>; + const sessionFileSets = fileSets.filter(e => e.channel === `${sessionUri.toString()}/changeset/session`); + const uncommittedFileSets = fileSets.filter(e => e.channel === `${sessionUri.toString()}/changeset/uncommitted`); + assert.deepStrictEqual(sessionFileSets.map(e => e.action.file.edit), gitDiffs); + assert.deepStrictEqual(uncommittedFileSets.map(e => e.action.file.edit), gitDiffs); // The compute pass also persists the file list under the // legacy `'diffs'` slot so it survives restarts. The write @@ -418,8 +417,7 @@ suite('AgentHostChangesetService', () => { // were emitted. const uncommittedUri = `${sessionStr}/changeset/uncommitted`; const removed = envelopes - .map(e => e.action) - .filter(a => a.type === ActionType.ChangesetFileRemoved && a.changeset === uncommittedUri); + .filter(e => e.action.type === ActionType.ChangesetFileRemoved && e.channel === uncommittedUri); assert.deepStrictEqual(removed, [], 'no files should be removed when the git path is unavailable'); // 2) The persisted DB blob is unchanged (compute did not overwrite it). @@ -657,8 +655,7 @@ suite('AgentHostChangesetService', () => { // per-turn state at status: ready with an empty file list. await svc.computeTurnChangeset(sessionUri.toString(), 'turn-1'); const statusReady = envelopes - .map(e => e.action) - .find(a => a.type === ActionType.ChangesetStatusChanged && a.changeset === turnUri); + .find(e => e.action.type === ActionType.ChangesetStatusChanged && e.channel === turnUri); assert.ok(statusReady, 'first per-turn compute must transition the URI to ready'); // Subsequent recomputes are observable via `_publishChangesetDiffs` @@ -667,7 +664,7 @@ suite('AgentHostChangesetService', () => { // `computeTurnChangeset` invocation through the sequencer. envelopes.length = 0; svc.onTurnComplete(sessionUri.toString(), 'turn-1'); - for (let i = 0; i < 100 && !envelopes.some(e => e.action.type === ActionType.ChangesetStatusChanged && e.action.changeset === `${sessionUri.toString()}/changeset/session`); i++) { + for (let i = 0; i < 100 && !envelopes.some(e => e.action.type === ActionType.ChangesetStatusChanged && e.channel === `${sessionUri.toString()}/changeset/session`); i++) { await timeout(2); } // Per-turn recompute was scheduled — at minimum its presence is diff --git a/src/vs/platform/agentHost/test/node/agentHostFileCompletionProvider.test.ts b/src/vs/platform/agentHost/test/node/agentHostFileCompletionProvider.test.ts index 2378ce0a9ea..3c9f988403c 100644 --- a/src/vs/platform/agentHost/test/node/agentHostFileCompletionProvider.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostFileCompletionProvider.test.ts @@ -114,7 +114,7 @@ suite('AgentHostFileCompletionProvider', () => { test('returns [] when session has no working directory', async () => { const { sessionUri, provider } = setup({}); const result = await provider.provideCompletionItems( - { kind: CompletionItemKind.UserMessage, session: sessionUri, text: '@', offset: 1 }, + { kind: CompletionItemKind.UserMessage, channel: sessionUri, text: '@', offset: 1 }, CancellationToken.None, ); assert.deepStrictEqual(result, []); @@ -123,7 +123,7 @@ suite('AgentHostFileCompletionProvider', () => { test('returns [] for non-file working directory', async () => { const { sessionUri, provider } = setup({ workingDirectory: URI.parse('vscode-vfs://github/foo/bar') }); const result = await provider.provideCompletionItems( - { kind: CompletionItemKind.UserMessage, session: sessionUri, text: '@', offset: 1 }, + { kind: CompletionItemKind.UserMessage, channel: sessionUri, text: '@', offset: 1 }, CancellationToken.None, ); assert.deepStrictEqual(result, []); @@ -134,7 +134,7 @@ suite('AgentHostFileCompletionProvider', () => { const files = [URI.joinPath(wd, 'foo.ts')]; const { sessionUri, provider } = setup({ workingDirectory: wd, files }); const result = await provider.provideCompletionItems( - { kind: CompletionItemKind.UserMessage, session: sessionUri, text: 'hello world', offset: 5 }, + { kind: CompletionItemKind.UserMessage, channel: sessionUri, text: 'hello world', offset: 5 }, CancellationToken.None, ); assert.deepStrictEqual(result, []); @@ -149,7 +149,7 @@ suite('AgentHostFileCompletionProvider', () => { ]; const { sessionUri, provider } = setup({ workingDirectory: wd, files }); const result = await provider.provideCompletionItems( - { kind: CompletionItemKind.UserMessage, session: sessionUri, text: 'see @util', offset: 9 }, + { kind: CompletionItemKind.UserMessage, channel: sessionUri, text: 'see @util', offset: 9 }, CancellationToken.None, ); assert.strictEqual(result.length, 1); @@ -171,7 +171,7 @@ suite('AgentHostFileCompletionProvider', () => { const files = Array.from({ length: 100 }, (_, i) => URI.joinPath(wd, `file${i}.ts`)); const { sessionUri, provider } = setup({ workingDirectory: wd, files }); const result = await provider.provideCompletionItems( - { kind: CompletionItemKind.UserMessage, session: sessionUri, text: '@', offset: 1 }, + { kind: CompletionItemKind.UserMessage, channel: sessionUri, text: '@', offset: 1 }, CancellationToken.None, ); assert.strictEqual(result.length, 50); diff --git a/src/vs/platform/agentHost/test/node/agentHostSkillCompletionProvider.test.ts b/src/vs/platform/agentHost/test/node/agentHostSkillCompletionProvider.test.ts index e2815eeae24..aebdafbc3fb 100644 --- a/src/vs/platform/agentHost/test/node/agentHostSkillCompletionProvider.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostSkillCompletionProvider.test.ts @@ -64,7 +64,7 @@ suite('AgentHostSkillCompletionProvider', () => { } async function run(provider: AgentHostSkillCompletionProvider, text: string, offset = text.length) { - return provider.provideCompletionItems({ kind: CompletionItemKind.UserMessage, session: 'mock:/session', text, offset }, CancellationToken.None); + return provider.provideCompletionItems({ kind: CompletionItemKind.UserMessage, channel: 'mock:/session', text, offset }, CancellationToken.None); } test('announces slash as a trigger character', () => { diff --git a/src/vs/platform/agentHost/test/node/agentHostStateManager.test.ts b/src/vs/platform/agentHost/test/node/agentHostStateManager.test.ts index 673d9ea4016..06d2602bbc5 100644 --- a/src/vs/platform/agentHost/test/node/agentHostStateManager.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostStateManager.test.ts @@ -11,7 +11,7 @@ import { runWithFakedTimers } from '../../../../base/test/common/timeTravelSched import { NullLogService } from '../../../log/common/log.js'; import { ActionType, NotificationType, type ActionEnvelope, type INotification } from '../../common/state/sessionActions.js'; import { SessionSummary, ResponsePartKind, ROOT_STATE_URI, SessionLifecycle, SessionStatus, TurnState, buildSubagentSessionUri, buildSubagentSessionUriPrefix, isSubagentSession, parseSubagentSessionUri, type MarkdownResponsePart, type SessionState } from '../../common/state/sessionState.js'; -import { type SessionSummaryChangedNotification } from '../../common/state/protocol/notifications.js'; +import { type SessionSummaryChangedParams } from '../../common/state/protocol/notifications.js'; import { AgentHostStateManager } from '../../node/agentHostStateManager.js'; import { buildChangesetUri, buildSessionChangesetUri } from '../../common/changesetUri.js'; @@ -83,9 +83,8 @@ suite('AgentHostStateManager', () => { const envelopes: ActionEnvelope[] = []; disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e))); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, - session: sessionUri, }); const state = manager.getSessionState(sessionUri); @@ -104,8 +103,8 @@ suite('AgentHostStateManager', () => { const envelopes: ActionEnvelope[] = []; disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e))); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); - manager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Updated' }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTitleChanged, title: 'Updated' }); assert.strictEqual(envelopes.length, 2); assert.strictEqual(envelopes[0].serverSeq, 1); @@ -120,8 +119,7 @@ suite('AgentHostStateManager', () => { disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e))); const origin = { clientId: 'renderer-1', clientSeq: 42 }; - manager.dispatchClientAction( - { type: ActionType.SessionReady, session: sessionUri }, + manager.dispatchClientAction(sessionUri, { type: ActionType.SessionReady, }, origin, ); @@ -134,7 +132,7 @@ suite('AgentHostStateManager', () => { disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e))); // First dispatch: introduces a new value, should emit. - manager.dispatchServerAction({ + manager.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, config: { 'my.setting': 'value-a' }, }); @@ -142,7 +140,7 @@ suite('AgentHostStateManager', () => { assert.strictEqual(manager.serverSeq, 1); // Second dispatch with the same value: should be deduped and not emit. - manager.dispatchServerAction({ + manager.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, config: { 'my.setting': 'value-a' }, }); @@ -151,13 +149,13 @@ suite('AgentHostStateManager', () => { // Third dispatch with a deeply-equal but newly allocated object value: // should also be deduped. - manager.dispatchServerAction({ + manager.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, config: { 'my.nested': { allow: ['x'], deny: [] } }, }); assert.strictEqual(envelopes.length, 2); assert.strictEqual(manager.serverSeq, 2); - manager.dispatchServerAction({ + manager.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, config: { 'my.nested': { allow: ['x'], deny: [] } }, }); @@ -165,7 +163,7 @@ suite('AgentHostStateManager', () => { assert.strictEqual(manager.serverSeq, 2, 'serverSeq must not advance on a no-op'); // Real change still emits. - manager.dispatchServerAction({ + manager.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, config: { 'my.setting': 'value-b' }, }); @@ -212,13 +210,12 @@ suite('AgentHostStateManager', () => { test('getActiveTurnId returns active turn id after turnStarted', () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); assert.strictEqual(manager.getActiveTurnId(sessionUri), undefined); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'hello' }, }); @@ -236,14 +233,13 @@ suite('AgentHostStateManager', () => { test('turnStarted dispatches root/activeSessionsChanged with correct count', () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); const envelopes: ActionEnvelope[] = []; disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e))); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'hello' }, }); @@ -256,10 +252,9 @@ suite('AgentHostStateManager', () => { test('turnComplete dispatches root/activeSessionsChanged back to 0', () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'hello' }, }); @@ -267,9 +262,8 @@ suite('AgentHostStateManager', () => { const envelopes: ActionEnvelope[] = []; disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e))); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnComplete, - session: sessionUri, turnId: 'turn-1', }); @@ -283,33 +277,29 @@ suite('AgentHostStateManager', () => { const session2Uri = URI.from({ scheme: 'copilot', path: '/test-session-2' }).toString(); manager.createSession(makeSessionSummary(sessionUri)); manager.createSession(makeSessionSummary(session2Uri)); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: session2Uri }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); + manager.dispatchServerAction(session2Uri, { type: ActionType.SessionReady, }); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'a' }, }); - manager.dispatchServerAction({ + manager.dispatchServerAction(session2Uri, { type: ActionType.SessionTurnStarted, - session: session2Uri, turnId: 'turn-2', userMessage: { text: 'b' }, }); assert.strictEqual(manager.rootState.activeSessions, 2); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnComplete, - session: sessionUri, turnId: 'turn-1', }); assert.strictEqual(manager.rootState.activeSessions, 1); - manager.dispatchServerAction({ + manager.dispatchServerAction(session2Uri, { type: ActionType.SessionTurnComplete, - session: session2Uri, turnId: 'turn-2', }); assert.strictEqual(manager.rootState.activeSessions, 0); @@ -317,10 +307,9 @@ suite('AgentHostStateManager', () => { test('removeSession decrements active sessions when an active turn is stranded', () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'hello' }, }); @@ -342,7 +331,7 @@ suite('AgentHostStateManager', () => { test('removeSession does not dispatch active-sessions change when no turn is active', () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); const envelopes: ActionEnvelope[] = []; disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e))); @@ -359,18 +348,16 @@ suite('AgentHostStateManager', () => { // the lifetime tracker doesn't release its hold while a turn is still // genuinely running. manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'hello' }, }); assert.strictEqual(manager.rootState.activeSessions, 1); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnComplete, - session: sessionUri, turnId: 'stale-turn', }); @@ -383,25 +370,22 @@ suite('AgentHostStateManager', () => { // without an intervening complete still represent a single active turn // from state's point of view. The count must mirror that. manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'a' }, }); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-2', userMessage: { text: 'b' }, }); assert.strictEqual(manager.rootState.activeSessions, 1); - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnComplete, - session: sessionUri, turnId: 'turn-2', }); @@ -447,12 +431,12 @@ suite('AgentHostStateManager', () => { test('emits sessionSummaryChanged when summary changes', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); const notifications: INotification[] = []; disposables.add(manager.onDidEmitNotification(n => notifications.push(n))); - manager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'New Title' }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTitleChanged, title: 'New Title' }); // Should not fire synchronously (debounced) assert.strictEqual(notifications.filter(n => n.type === NotificationType.SessionSummaryChanged).length, 0); @@ -462,7 +446,7 @@ suite('AgentHostStateManager', () => { const changed = notifications.filter(n => n.type === NotificationType.SessionSummaryChanged); assert.strictEqual(changed.length, 1); - const notification = changed[0] as SessionSummaryChangedNotification; + const notification = changed[0] as SessionSummaryChangedParams; assert.strictEqual(notification.session, sessionUri); assert.strictEqual(notification.changes.title, 'New Title'); assert.strictEqual(notification.changes.status, undefined, 'unchanged fields should be omitted'); @@ -472,26 +456,26 @@ suite('AgentHostStateManager', () => { test('coalesces multiple summary changes into one notification', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); const notifications: INotification[] = []; disposables.add(manager.onDidEmitNotification(n => notifications.push(n))); - manager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'First' }); - manager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Second' }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTitleChanged, title: 'First' }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTitleChanged, title: 'Second' }); await new Promise(r => setTimeout(r, 150)); const changed = notifications.filter(n => n.type === NotificationType.SessionSummaryChanged); assert.strictEqual(changed.length, 1, 'should coalesce into one notification'); - assert.strictEqual((changed[0] as SessionSummaryChangedNotification).changes.title, 'Second'); + assert.strictEqual((changed[0] as SessionSummaryChangedParams).changes.title, 'Second'); }); }); test('does not emit sessionSummaryChanged when summary is unchanged', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); const notifications: INotification[] = []; disposables.add(manager.onDidEmitNotification(n => notifications.push(n))); @@ -507,12 +491,12 @@ suite('AgentHostStateManager', () => { test('does not emit sessionSummaryChanged for deleted session', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); const notifications: INotification[] = []; disposables.add(manager.onDidEmitNotification(n => notifications.push(n))); - manager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'New Title' }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTitleChanged, title: 'New Title' }); manager.deleteSession(sessionUri); await new Promise(r => setTimeout(r, 150)); @@ -534,12 +518,11 @@ suite('AgentHostStateManager', () => { // races with that 100 ms window the flush must happen synchronously. return runWithFakedTimers({ useFakeTimers: true }, async () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); // Start a turn → status becomes InProgress. - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'hello' }, }); @@ -552,16 +535,15 @@ suite('AgentHostStateManager', () => { // Turn completes — status flips back to Idle. This schedules a summary // flush 100 ms later but we will call removeSession before it fires. - manager.dispatchServerAction({ + manager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnComplete, - session: sessionUri, turnId: 'turn-1', }); // Simulate eviction within the 100 ms debounce window. manager.removeSession(sessionUri); - const changed = notifications.filter(n => n.type === NotificationType.SessionSummaryChanged) as SessionSummaryChangedNotification[]; + const changed = notifications.filter(n => n.type === NotificationType.SessionSummaryChanged) as SessionSummaryChangedParams[]; assert.strictEqual(changed.length, 1, 'should emit SessionSummaryChanged synchronously in removeSession'); assert.strictEqual(changed[0].changes.status, SessionStatus.Idle, 'status should be Idle so the spinner clears'); }); @@ -577,16 +559,15 @@ suite('AgentHostStateManager', () => { const cleared = envelopes.filter(e => e.action.type === ActionType.ChangesetCleared); assert.strictEqual(cleared.length, 1, 'expected exactly one cleared envelope'); - assert.strictEqual((cleared[0].action as { changeset: string }).changeset, changeset); + assert.strictEqual(cleared[0].channel, changeset); assert.strictEqual(manager.getChangesetState(changeset), undefined, 'state should be deleted'); }); test('producer-emitted ChangesetCleared keeps the state alive (recompute path)', () => { manager.createSession(makeSessionSummary()); const changeset = manager.registerChangeset(buildSessionChangesetUri(sessionUri)); - manager.dispatchServerAction({ + manager.dispatchServerAction(changeset, { type: ActionType.ChangesetFileSet, - changeset, file: { id: 'file:///a.ts', edit: { after: { uri: 'file:///a.ts', content: { uri: 'file:///a.ts' } }, diff: { added: 1, removed: 0 } }, @@ -594,9 +575,8 @@ suite('AgentHostStateManager', () => { }); assert.strictEqual(manager.getChangesetState(changeset)?.files.length, 1); - manager.dispatchServerAction({ + manager.dispatchServerAction(changeset, { type: ActionType.ChangesetCleared, - changeset, }); const after = manager.getChangesetState(changeset); @@ -615,9 +595,8 @@ suite('AgentHostStateManager', () => { // the changeset. manager.createSession(makeSessionSummary()); const changeset = manager.registerChangeset(buildSessionChangesetUri(sessionUri)); - manager.dispatchServerAction({ + manager.dispatchServerAction(changeset, { type: ActionType.ChangesetFileSet, - changeset, file: { id: 'file:///a.ts', edit: { after: { uri: 'file:///a.ts', content: { uri: 'file:///a.ts' } }, diff: { added: 1, removed: 0 } }, @@ -637,9 +616,8 @@ suite('AgentHostStateManager', () => { test('deleteSession disposes per-session changesets before emitting SessionRemoved', () => { manager.createSession(makeSessionSummary()); const changeset = manager.registerChangeset(buildSessionChangesetUri(sessionUri)); - manager.dispatchServerAction({ + manager.dispatchServerAction(changeset, { type: ActionType.ChangesetFileSet, - changeset, file: { id: 'file:///a.ts', edit: { after: { uri: 'file:///a.ts', content: { uri: 'file:///a.ts' } }, diff: { added: 1, removed: 0 } }, @@ -668,12 +646,11 @@ suite('AgentHostStateManager', () => { disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e))); const seqBefore = manager.serverSeq; - manager.dispatchServerAction({ + manager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetFileSet, - changeset: changesetUri, file: { id: 'file:///x.ts', - edit: { after: { uri: 'file:///x.ts', content: { uri: 'file:///x.ts' } }, diff: { added: 1, removed: 0 } }, + edit: { after: { uri: 'file:///x.ts', content: { uri: 'file:///x.ts' } }, diff: { added: 1, removed: 0 } } }, }); @@ -695,12 +672,11 @@ suite('AgentHostStateManager', () => { // break valid changesets. const registered = manager.registerChangeset(buildChangesetUri(sessionUri, 'missing')); assert.strictEqual(registered, changesetUri); - manager.dispatchServerAction({ + manager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetFileSet, - changeset: changesetUri, file: { id: 'file:///x.ts', - edit: { after: { uri: 'file:///x.ts', content: { uri: 'file:///x.ts' } }, diff: { added: 1, removed: 0 } }, + edit: { after: { uri: 'file:///x.ts', content: { uri: 'file:///x.ts' } }, diff: { added: 1, removed: 0 } } }, }); assert.strictEqual(envelopes.length, 1, 'registered changeset action should emit an envelope'); diff --git a/src/vs/platform/agentHost/test/node/agentHostTerminalManager.test.ts b/src/vs/platform/agentHost/test/node/agentHostTerminalManager.test.ts index b0ca0ee5d17..433e9a8085b 100644 --- a/src/vs/platform/agentHost/test/node/agentHostTerminalManager.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostTerminalManager.test.ts @@ -75,7 +75,6 @@ class TestTerminalDataHandler { this.tracker.detectionAvailableEmitted = true; this.dispatched.push({ type: ActionType.TerminalCommandDetectionAvailable, - terminal: this.uri, }); } @@ -105,7 +104,6 @@ class TestTerminalDataHandler { this.dispatched.push({ type: ActionType.TerminalCommandExecuted, - terminal: this.uri, commandId, commandLine, timestamp, @@ -135,7 +133,6 @@ class TestTerminalDataHandler { this.dispatched.push({ type: ActionType.TerminalCommandFinished, - terminal: this.uri, commandId: finishedCommandId, exitCode: event.exitCode, durationMs, @@ -147,7 +144,6 @@ class TestTerminalDataHandler { this.cwd = event.value; this.dispatched.push({ type: ActionType.TerminalCwdChanged, - terminal: this.uri, cwd: event.value, }); } @@ -274,7 +270,7 @@ suite('AgentHostTerminalManager – command detection integration', () => { const manager = disposables.add(new TestAgentHostTerminalManager(stateManager, logService, productService, configurationService, pty)); const createTerminal = manager.createTerminal({ - terminal: 'agenthost-terminal://test/command-input', + channel: 'agenthost-terminal://test/command-input', claim: { kind: TerminalClaimKind.Client, clientId: 'test-client' }, cwd: process.cwd(), cols: 80, @@ -299,7 +295,7 @@ suite('AgentHostTerminalManager – command detection integration', () => { const manager = disposables.add(new TestAgentHostTerminalManager(stateManager, logService, productService, configurationService, pty)); const createTerminal = manager.createTerminal({ - terminal: 'agenthost-terminal://test/bracketed-paste', + channel: 'agenthost-terminal://test/bracketed-paste', claim: { kind: TerminalClaimKind.Client, clientId: 'test-client' }, cwd: process.cwd(), cols: 80, @@ -324,7 +320,7 @@ suite('AgentHostTerminalManager – command detection integration', () => { const manager = disposables.add(new TestAgentHostTerminalManager(stateManager, logService, productService, configurationService, pty)); const createTerminal = manager.createTerminal({ - terminal: 'agenthost-terminal://test/bracketed-paste-disabled', + channel: 'agenthost-terminal://test/bracketed-paste-disabled', claim: { kind: TerminalClaimKind.Client, clientId: 'test-client' }, cwd: process.cwd(), cols: 80, @@ -355,7 +351,7 @@ suite('AgentHostTerminalManager – command detection integration', () => { const pty = new TestPty(); const manager = disposables.add(new TestAgentHostTerminalManager(stateManager, logService, productService, configurationService, pty)); const createTerminal = manager.createTerminal({ - terminal: `agenthost-terminal://test/${id}`, + channel: `agenthost-terminal://test/${id}`, claim, cwd: process.cwd(), cols: 80, @@ -401,7 +397,7 @@ suite('AgentHostTerminalManager – command detection integration', () => { const manager = disposables.add(new TestAgentHostTerminalManager(stateManager, logService, productService, configurationService, pty)); const createTerminal = manager.createTerminal({ - terminal: 'agenthost-terminal://test/dsr', + channel: 'agenthost-terminal://test/dsr', claim: { kind: TerminalClaimKind.Client, clientId: 'test-client' }, cwd: process.cwd(), cols: 80, @@ -426,7 +422,7 @@ suite('AgentHostTerminalManager – command detection integration', () => { const uri = 'agenthost-terminal://test/alt-buffer'; const createTerminal = manager.createTerminal({ - terminal: uri, + channel: uri, claim: { kind: TerminalClaimKind.Client, clientId: 'test-client' }, cwd: process.cwd(), cols: 80, @@ -455,7 +451,7 @@ suite('AgentHostTerminalManager – command detection integration', () => { const uri = 'agenthost-terminal://test/alt-buffer-disposed'; const createTerminal = manager.createTerminal({ - terminal: uri, + channel: uri, claim: { kind: TerminalClaimKind.Client, clientId: 'test-client' }, cwd: process.cwd(), cols: 80, diff --git a/src/vs/platform/agentHost/test/node/agentService.test.ts b/src/vs/platform/agentHost/test/node/agentService.test.ts index 8f169b3302c..1f043bbcda7 100644 --- a/src/vs/platform/agentHost/test/node/agentService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentService.test.ts @@ -23,7 +23,7 @@ import { AgentSession } from '../../common/agentService.js'; import { ISessionDatabase, ISessionDataService } from '../../common/sessionDataService.js'; import { SessionDatabase } from '../../node/sessionDatabase.js'; import { ActionType, ActionEnvelope } from '../../common/state/sessionActions.js'; -import { ChangesetStatus, MessageAttachmentKind, SessionActiveClient, ResponsePartKind, SessionLifecycle, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, buildSubagentSessionUri, type ChangesetState, type MarkdownResponsePart, type ToolCallCompletedState, type ToolCallResponsePart } from '../../common/state/sessionState.js'; +import { ChangesetStatus, MessageAttachmentKind, SessionActiveClient, ResponsePartKind, ROOT_STATE_URI, SessionLifecycle, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, buildSubagentSessionUri, type ChangesetState, type MarkdownResponsePart, type ToolCallCompletedState, type ToolCallResponsePart } from '../../common/state/sessionState.js'; import { type MessageResourceAttachment } from '../../common/state/protocol/state.js'; import { IProductService } from '../../../product/common/productService.js'; import { AgentService } from '../../node/agentService.js'; @@ -114,7 +114,8 @@ suite('AgentService (node dispatcher)', () => { // Start a turn so there's an active turn to map events to service.dispatchAction( - { type: ActionType.SessionTurnStarted, session: session.toString(), turnId: 'turn-1', userMessage: { text: 'hello' } }, + session.toString(), + { type: ActionType.SessionTurnStarted, turnId: 'turn-1', userMessage: { text: 'hello' } }, 'test-client', 1, ); @@ -123,7 +124,7 @@ suite('AgentService (node dispatcher)', () => { copilotAgent.fireProgress({ kind: 'action', session, - action: { type: ActionType.SessionResponsePart, session: session.toString(), turnId: 'turn-1', part: { kind: ResponsePartKind.Markdown, id: 'msg-1', content: 'hello' } }, + action: { type: ActionType.SessionResponsePart, turnId: 'turn-1', part: { kind: ResponsePartKind.Markdown, id: 'msg-1', content: 'hello' } }, }); assert.ok(envelopes.some(e => e.action.type === ActionType.SessionResponsePart)); }); @@ -143,7 +144,7 @@ suite('AgentService (node dispatcher)', () => { svc.registerProvider(agent); const customization = { uri: 'file:///plugin-a', displayName: 'Plugin A' }; - svc.dispatchAction({ + svc.dispatchAction(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, config: { customizations: [customization] }, }, 'test-client', 1); @@ -212,9 +213,9 @@ suite('AgentService (node dispatcher)', () => { async function dispatchTurnAndWait(svc: AgentService, agent: MockAgent, session: URI, attachments: MessageResourceAttachment[] | { type: MessageAttachmentKind.EmbeddedResource; label: string; data: string; contentType: string; displayKind?: string }[]): Promise { svc.dispatchAction( + session.toString(), { type: ActionType.SessionTurnStarted, - session: session.toString(), turnId: 'turn-1', userMessage: { text: 'hello', attachments: attachments as never }, }, @@ -676,17 +677,15 @@ suite('AgentService (node dispatcher)', () => { // Seed live changeset state directly: a single file with // different counts than the stale persisted blob. const changesetUri = svc.stateManager.registerChangeset(buildSessionChangesetUri(sessionUri.toString())); - svc.stateManager.dispatchServerAction({ + svc.stateManager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetFileSet, - changeset: changesetUri, file: { id: 'file:///wd/live.ts', - edit: { after: { uri: 'file:///wd/live.ts', content: { uri: 'file:///wd/live.ts' } }, diff: { added: 1, removed: 0 } }, + edit: { after: { uri: 'file:///wd/live.ts', content: { uri: 'file:///wd/live.ts' } }, diff: { added: 1, removed: 0 } } }, }); - svc.stateManager.dispatchServerAction({ + svc.stateManager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetStatusChanged, - changeset: changesetUri, status: ChangesetStatus.Ready, }); @@ -751,9 +750,8 @@ suite('AgentService (node dispatcher)', () => { // must be authoritative enough to suppress the persisted-diffs // read. const changesetUri = svc.stateManager.registerChangeset(buildSessionChangesetUri(sessionUri.toString())); - svc.stateManager.dispatchServerAction({ + svc.stateManager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetStatusChanged, - changeset: changesetUri, status: ChangesetStatus.Ready, }); @@ -821,9 +819,8 @@ suite('AgentService (node dispatcher)', () => { const session = await service.createSession({ provider: 'copilot' }); // Simulate immediate title change via state manager - service.stateManager.dispatchServerAction({ + service.stateManager.dispatchServerAction(session.toString(), { type: ActionType.SessionTitleChanged, - session: session.toString(), title: 'User first message', }); @@ -1519,7 +1516,8 @@ suite('AgentService (node dispatcher)', () => { // when the refcount reaches zero, otherwise we'd drop live state // mid-response. service.dispatchAction( - { type: ActionType.SessionTurnStarted, session: sessionResource.toString(), turnId: 'turn-1', userMessage: { text: 'hello' } }, + sessionResource.toString(), + { type: ActionType.SessionTurnStarted, turnId: 'turn-1', userMessage: { text: 'hello' } }, 'client-1', 1, ); @@ -1826,11 +1824,13 @@ suite('AgentService (node dispatcher)', () => { const sessionResource = await service.createSession({ provider: 'copilot' }); service.addSubscriber(sessionResource, 'client-1'); service.dispatchAction( - { type: ActionType.SessionTurnStarted, session: sessionResource.toString(), turnId: 'turn-1', userMessage: { text: 'hello' } }, + sessionResource.toString(), + { type: ActionType.SessionTurnStarted, turnId: 'turn-1', userMessage: { text: 'hello' } }, 'client-1', 1, ); service.dispatchAction( - { type: ActionType.SessionTurnComplete, session: sessionResource.toString(), turnId: 'turn-1' }, + sessionResource.toString(), + { type: ActionType.SessionTurnComplete, turnId: 'turn-1' }, 'client-1', 2, ); diff --git a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts index 311cfc5fb63..e3ac514a0ba 100644 --- a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts +++ b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts @@ -141,12 +141,11 @@ suite('AgentSideEffects', () => { workingDirectory, changesets: buildDefaultChangesetCatalogue(sessionUri.toString()), }); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri.toString() }); + stateManager.dispatchServerAction(sessionUri.toString(), { type: ActionType.SessionReady, }); } function startTurn(turnId: string): void { - stateManager.dispatchClientAction( - { type: ActionType.SessionTurnStarted, session: sessionUri.toString(), turnId, userMessage: { text: 'hello' } }, + stateManager.dispatchClientAction(sessionUri.toString(), { type: ActionType.SessionTurnStarted, turnId, userMessage: { text: 'hello' } }, { clientId: 'test', clientSeq: 1 }, ); } @@ -187,11 +186,10 @@ suite('AgentSideEffects', () => { setupSession(); const action: SessionAction = { type: ActionType.SessionTurnStarted, - session: sessionUri.toString(), turnId: 'turn-1', userMessage: { text: 'hello world' }, }; - sideEffects.handleAction(action); + sideEffects.handleAction(sessionUri.toString(), action); // sendMessage is async but fire-and-forget; wait a tick await new Promise(r => setTimeout(r, 10)); @@ -203,19 +201,17 @@ suite('AgentSideEffects', () => { setupSession(); const activeClientAction: SessionAction = { type: ActionType.SessionActiveClientChanged, - session: sessionUri.toString(), activeClient: { clientId: 'test-client', tools: [{ name: 'testTool', inputSchema: { type: 'object' } }], - customizations: [{ uri: 'file:///customizations/SKILL.md', displayName: 'Test Skill' }], + customizations: [{ uri: 'file:///customizations/SKILL.md', displayName: 'Test Skill' }] }, }; - stateManager.dispatchClientAction(activeClientAction, { clientId: 'test', clientSeq: 1 }); - sideEffects.handleAction(activeClientAction); + stateManager.dispatchClientAction(sessionUri.toString(), activeClientAction, { clientId: 'test', clientSeq: 1 }); + sideEffects.handleAction(sessionUri.toString(), activeClientAction); const fileUri = URI.file('/workspace/direct.ts'); - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionTurnStarted, - session: sessionUri.toString(), turnId: 'turn-1', userMessage: { text: 'hello world', attachments: [{ type: MessageAttachmentKind.Resource, uri: fileUri.toString(), label: 'direct.ts', displayKind: 'document' }] }, }); @@ -241,12 +237,11 @@ suite('AgentSideEffects', () => { const fileUri = URI.file('/workspace/test.ts'); const action: SessionAction = { type: ActionType.SessionTurnStarted, - session: sessionUri.toString(), turnId: 'turn-1', userMessage: { text: 'hello world', attachments: [{ type: MessageAttachmentKind.Resource, uri: fileUri.toString(), label: 'test.ts', displayKind: 'document' }] }, }; - sideEffects.handleAction(action); + sideEffects.handleAction(sessionUri.toString(), action); assert.deepStrictEqual(agent.sendMessageCalls, [{ session: URI.parse(sessionUri.toString()), @@ -260,7 +255,6 @@ suite('AgentSideEffects', () => { const fileUri = URI.file('/workspace/selection.ts'); const action: SessionAction = { type: ActionType.SessionTurnStarted, - session: sessionUri.toString(), turnId: 'turn-1', userMessage: { text: 'hello world', @@ -272,14 +266,14 @@ suite('AgentSideEffects', () => { selection: { range: { start: { line: 2, character: 3 }, - end: { line: 4, character: 5 }, - }, - }, - }], + end: { line: 4, character: 5 } + } + } + }] }, }; - sideEffects.handleAction(action); + sideEffects.handleAction(sessionUri.toString(), action); assert.deepStrictEqual(agent.sendMessageCalls, [{ session: URI.parse(sessionUri.toString()), @@ -312,9 +306,8 @@ suite('AgentSideEffects', () => { const envelopes: ActionEnvelope[] = []; disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e))); - noAgentSideEffects.handleAction({ + noAgentSideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionTurnStarted, - session: sessionUri.toString(), turnId: 'turn-1', userMessage: { text: 'hello' }, }); @@ -338,7 +331,7 @@ suite('AgentSideEffects', () => { modifiedAt: Date.now(), project: { uri: 'file:///test-project', displayName: 'Test Project' }, }); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri.toString() }); + stateManager.dispatchServerAction(sessionUri.toString(), { type: ActionType.SessionReady, }); } test('dispatches titleChanged with user message on first turn', () => { @@ -347,9 +340,8 @@ suite('AgentSideEffects', () => { const envelopes: ActionEnvelope[] = []; disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e))); - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionTurnStarted, - session: sessionUri.toString(), turnId: 'turn-1', userMessage: { text: 'Fix the login bug' }, }); @@ -367,9 +359,8 @@ suite('AgentSideEffects', () => { const envelopes: ActionEnvelope[] = []; disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e))); - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionTurnStarted, - session: sessionUri.toString(), turnId: 'turn-1', userMessage: { text: ' ' }, }); @@ -385,9 +376,8 @@ suite('AgentSideEffects', () => { disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e))); const longMessage = 'Fix the bug\nin the login\tpage please ' + 'a'.repeat(250); - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionTurnStarted, - session: sessionUri.toString(), turnId: 'turn-1', userMessage: { text: longMessage }, }); @@ -407,18 +397,16 @@ suite('AgentSideEffects', () => { startTurn('turn-1'); // Complete the first turn so turns.length becomes 1. - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri.toString(), { type: ActionType.SessionTurnComplete, - session: sessionUri.toString(), turnId: 'turn-1', }); const envelopes: ActionEnvelope[] = []; disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e))); - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionTurnStarted, - session: sessionUri.toString(), turnId: 'turn-2', userMessage: { text: 'second message' }, }); @@ -438,14 +426,13 @@ suite('AgentSideEffects', () => { modifiedAt: Date.now(), project: { uri: 'file:///test-project', displayName: 'Test Project' }, }); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri.toString() }); + stateManager.dispatchServerAction(sessionUri.toString(), { type: ActionType.SessionReady, }); const envelopes: ActionEnvelope[] = []; disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e))); - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionTurnStarted, - session: sessionUri.toString(), turnId: 'turn-1', userMessage: { text: 'hello' }, }); @@ -459,9 +446,8 @@ suite('AgentSideEffects', () => { test('calls abortSession on the agent', async () => { setupSession(); - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionTurnCancelled, - session: sessionUri.toString(), turnId: 'turn-1', }); @@ -477,9 +463,8 @@ suite('AgentSideEffects', () => { test('calls changeModel on the agent', async () => { setupSession(); - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionModelChanged, - session: sessionUri.toString(), model: { id: 'gpt-5' }, }); @@ -503,7 +488,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, - action: { type: ActionType.SessionResponsePart, session: sessionUri.toString(), turnId: 'turn-1', part: { kind: ResponsePartKind.Markdown, id: 'msg-1', content: 'hi' } }, + action: { type: ActionType.SessionResponsePart, turnId: 'turn-1', part: { kind: ResponsePartKind.Markdown, id: 'msg-1', content: 'hi' } }, }); // First delta creates a response part (not a delta action) @@ -520,14 +505,14 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, - action: { type: ActionType.SessionResponsePart, session: sessionUri.toString(), turnId: 'turn-1', part: { kind: ResponsePartKind.Markdown, id: 'msg-1', content: 'before' } }, + action: { type: ActionType.SessionResponsePart, turnId: 'turn-1', part: { kind: ResponsePartKind.Markdown, id: 'msg-1', content: 'before' } }, }); assert.strictEqual(envelopes.filter(e => e.action.type === ActionType.SessionResponsePart).length, 1); listener.dispose(); agent.fireProgress({ kind: 'action', session: sessionUri, - action: { type: ActionType.SessionResponsePart, session: sessionUri.toString(), turnId: 'turn-1', part: { kind: ResponsePartKind.Markdown, id: 'msg-2', content: 'after' } }, + action: { type: ActionType.SessionResponsePart, turnId: 'turn-1', part: { kind: ResponsePartKind.Markdown, id: 'msg-2', content: 'after' } }, }); assert.strictEqual(envelopes.filter(e => e.action.type === ActionType.SessionResponsePart).length, 1); }); @@ -626,13 +611,12 @@ suite('AgentSideEffects', () => { const action = { type: ActionType.SessionPendingMessageSet as const, - session: sessionUri.toString(), kind: PendingMessageKind.Steering, id: 'steer-1', userMessage: { text: 'focus on tests' }, }; - stateManager.dispatchClientAction(action, { clientId: 'test', clientSeq: 1 }); - sideEffects.handleAction(action); + stateManager.dispatchClientAction(sessionUri.toString(), action, { clientId: 'test', clientSeq: 1 }); + sideEffects.handleAction(sessionUri.toString(), action); assert.strictEqual(agent.setPendingMessagesCalls.length, 1); assert.deepStrictEqual(agent.setPendingMessagesCalls[0].steeringMessage, { id: 'steer-1', userMessage: { text: 'focus on tests' } }); @@ -644,13 +628,12 @@ suite('AgentSideEffects', () => { const action = { type: ActionType.SessionPendingMessageSet as const, - session: sessionUri.toString(), kind: PendingMessageKind.Queued, id: 'q-1', userMessage: { text: 'queued message' }, }; - stateManager.dispatchClientAction(action, { clientId: 'test', clientSeq: 1 }); - sideEffects.handleAction(action); + stateManager.dispatchClientAction(sessionUri.toString(), action, { clientId: 'test', clientSeq: 1 }); + sideEffects.handleAction(sessionUri.toString(), action); // Queued messages are not forwarded to the agent; the server controls consumption assert.strictEqual(agent.setPendingMessagesCalls.length, 1); @@ -667,14 +650,13 @@ suite('AgentSideEffects', () => { const fileUri = URI.file('/workspace/queued.ts'); const action: SessionAction = { type: ActionType.SessionPendingMessageSet as const, - session: sessionUri.toString(), kind: PendingMessageKind.Queued, id: 'q-uri', userMessage: { text: 'queued message', attachments: [{ type: MessageAttachmentKind.Resource, uri: fileUri.toString(), label: 'queued.ts', displayKind: 'document' }] }, }; - stateManager.dispatchClientAction(action, { clientId: 'test', clientSeq: 1 }); - sideEffects.handleAction(action); + stateManager.dispatchClientAction(sessionUri.toString(), action, { clientId: 'test', clientSeq: 1 }); + sideEffects.handleAction(sessionUri.toString(), action); assert.deepStrictEqual(agent.sendMessageCalls, [{ session: URI.parse(sessionUri.toString()), @@ -688,13 +670,12 @@ suite('AgentSideEffects', () => { const action = { type: ActionType.SessionPendingMessageSet as const, - session: sessionUri.toString(), kind: PendingMessageKind.Queued, id: 'q-telemetry', userMessage: { text: 'queued message' }, }; - stateManager.dispatchClientAction(action, { clientId: 'test', clientSeq: 1 }); - sideEffects.handleAction(action); + stateManager.dispatchClientAction(sessionUri.toString(), action, { clientId: 'test', clientSeq: 1 }); + sideEffects.handleAction(sessionUri.toString(), action); assert.deepStrictEqual(telemetryService.events, [{ eventName: 'agentHost.userMessageSent', @@ -715,25 +696,23 @@ suite('AgentSideEffects', () => { // Add a queued message const setAction = { type: ActionType.SessionPendingMessageSet as const, - session: sessionUri.toString(), kind: PendingMessageKind.Queued, id: 'q-rm', userMessage: { text: 'will be removed' }, }; - stateManager.dispatchClientAction(setAction, { clientId: 'test', clientSeq: 1 }); - sideEffects.handleAction(setAction); + stateManager.dispatchClientAction(sessionUri.toString(), setAction, { clientId: 'test', clientSeq: 1 }); + sideEffects.handleAction(sessionUri.toString(), setAction); agent.setPendingMessagesCalls.length = 0; // Remove const removeAction = { type: ActionType.SessionPendingMessageRemoved as const, - session: sessionUri.toString(), kind: PendingMessageKind.Queued, id: 'q-rm', }; - stateManager.dispatchClientAction(removeAction, { clientId: 'test', clientSeq: 2 }); - sideEffects.handleAction(removeAction); + stateManager.dispatchClientAction(sessionUri.toString(), removeAction, { clientId: 'test', clientSeq: 2 }); + sideEffects.handleAction(sessionUri.toString(), removeAction); assert.strictEqual(agent.setPendingMessagesCalls.length, 1); assert.deepStrictEqual(agent.setPendingMessagesCalls[0].queuedMessages, []); @@ -743,20 +722,20 @@ suite('AgentSideEffects', () => { setupSession(); // Add two queued messages - const setA = { type: ActionType.SessionPendingMessageSet as const, session: sessionUri.toString(), kind: PendingMessageKind.Queued, id: 'q-a', userMessage: { text: 'A' } }; - stateManager.dispatchClientAction(setA, { clientId: 'test', clientSeq: 1 }); - sideEffects.handleAction(setA); + const setA = { type: ActionType.SessionPendingMessageSet as const, kind: PendingMessageKind.Queued, id: 'q-a', userMessage: { text: 'A' } }; + stateManager.dispatchClientAction(sessionUri.toString(), setA, { clientId: 'test', clientSeq: 1 }); + sideEffects.handleAction(sessionUri.toString(), setA); - const setB = { type: ActionType.SessionPendingMessageSet as const, session: sessionUri.toString(), kind: PendingMessageKind.Queued, id: 'q-b', userMessage: { text: 'B' } }; - stateManager.dispatchClientAction(setB, { clientId: 'test', clientSeq: 2 }); - sideEffects.handleAction(setB); + const setB = { type: ActionType.SessionPendingMessageSet as const, kind: PendingMessageKind.Queued, id: 'q-b', userMessage: { text: 'B' } }; + stateManager.dispatchClientAction(sessionUri.toString(), setB, { clientId: 'test', clientSeq: 2 }); + sideEffects.handleAction(sessionUri.toString(), setB); agent.setPendingMessagesCalls.length = 0; // Reorder - const reorderAction = { type: ActionType.SessionQueuedMessagesReordered as const, session: sessionUri.toString(), order: ['q-b', 'q-a'] }; - stateManager.dispatchClientAction(reorderAction, { clientId: 'test', clientSeq: 3 }); - sideEffects.handleAction(reorderAction); + const reorderAction = { type: ActionType.SessionQueuedMessagesReordered as const, order: ['q-b', 'q-a'] }; + stateManager.dispatchClientAction(sessionUri.toString(), reorderAction, { clientId: 'test', clientSeq: 3 }); + sideEffects.handleAction(sessionUri.toString(), reorderAction); assert.strictEqual(agent.setPendingMessagesCalls.length, 1); assert.deepStrictEqual(agent.setPendingMessagesCalls[0].queuedMessages, []); @@ -775,13 +754,12 @@ suite('AgentSideEffects', () => { startTurn('turn-1'); const setAction = { type: ActionType.SessionPendingMessageSet as const, - session: sessionUri.toString(), kind: PendingMessageKind.Queued, id: 'q-auto', userMessage: { text: 'auto queued' }, }; - stateManager.dispatchClientAction(setAction, { clientId: 'test', clientSeq: 1 }); - sideEffects.handleAction(setAction); + stateManager.dispatchClientAction(sessionUri.toString(), setAction, { clientId: 'test', clientSeq: 1 }); + sideEffects.handleAction(sessionUri.toString(), setAction); // Message should NOT be consumed yet (turn is active) assert.strictEqual(agent.sendMessageCalls.length, 0); @@ -792,7 +770,7 @@ suite('AgentSideEffects', () => { // Fire idle → turn completes → queued message should be consumed agent.fireProgress({ kind: 'action', session: sessionUri, - action: { type: ActionType.SessionTurnComplete, session: sessionUri.toString(), turnId: 'turn-1' }, + action: { type: ActionType.SessionTurnComplete, turnId: 'turn-1' }, }); const turnComplete = envelopes.find(e => e.action.type === ActionType.SessionTurnComplete); @@ -819,13 +797,12 @@ suite('AgentSideEffects', () => { const setAction = { type: ActionType.SessionPendingMessageSet as const, - session: sessionUri.toString(), kind: PendingMessageKind.Queued, id: 'q-wait', userMessage: { text: 'should wait' }, }; - stateManager.dispatchClientAction(setAction, { clientId: 'test', clientSeq: 1 }); - sideEffects.handleAction(setAction); + stateManager.dispatchClientAction(sessionUri.toString(), setAction, { clientId: 'test', clientSeq: 1 }); + sideEffects.handleAction(sessionUri.toString(), setAction); // No turn started for the queued message const turnStarted = envelopes.find(e => e.action.type === ActionType.SessionTurnStarted); @@ -847,13 +824,12 @@ suite('AgentSideEffects', () => { const action = { type: ActionType.SessionPendingMessageSet as const, - session: sessionUri.toString(), kind: PendingMessageKind.Steering, id: 'steer-rm', userMessage: { text: 'steer me' }, }; - stateManager.dispatchClientAction(action, { clientId: 'test', clientSeq: 1 }); - sideEffects.handleAction(action); + stateManager.dispatchClientAction(sessionUri.toString(), action, { clientId: 'test', clientSeq: 1 }); + sideEffects.handleAction(sessionUri.toString(), action); // Removal is not dispatched synchronously; it waits for the agent let removal = envelopes.find(e => @@ -906,17 +882,16 @@ suite('AgentSideEffects', () => { const action: SessionAction = { type: ActionType.SessionActiveClientChanged, - session: sessionUri.toString(), activeClient: { clientId: 'test-client', tools: [], customizations: [ { uri: 'file:///plugin-a', displayName: 'Plugin A' }, { uri: 'file:///plugin-b', displayName: 'Plugin B' }, - ], + ] }, }; - sideEffects.handleAction(action); + sideEffects.handleAction(sessionUri.toString(), action); // Wait for async setClientCustomizations await new Promise(r => setTimeout(r, 50)); @@ -942,13 +917,12 @@ suite('AgentSideEffects', () => { const action: SessionAction = { type: ActionType.SessionActiveClientChanged, - session: sessionUri.toString(), activeClient: { clientId: 'test-client', - tools: [], + tools: [] }, }; - sideEffects.handleAction(action); + sideEffects.handleAction(sessionUri.toString(), action); assert.deepStrictEqual(agent.setClientCustomizationsCalls, [{ clientId: 'test-client', @@ -964,10 +938,9 @@ suite('AgentSideEffects', () => { const action: SessionAction = { type: ActionType.SessionActiveClientChanged, - session: sessionUri.toString(), activeClient: null, }; - sideEffects.handleAction(action); + sideEffects.handleAction(sessionUri.toString(), action); assert.deepStrictEqual(agent.setClientCustomizationsCalls, [{ clientId: '', @@ -998,8 +971,8 @@ suite('AgentSideEffects', () => { config: { customizations: [customization] }, }; - stateManager.dispatchServerAction(action); - sideEffects.handleAction(action); + stateManager.dispatchServerAction(sessionUri.toString(), action); + sideEffects.handleAction(sessionUri.toString(), action); await new Promise(resolve => setTimeout(resolve, 10)); const agentInfoAction = envelopes.filter(e => e.action.type === ActionType.RootAgentsChanged).at(-1); @@ -1022,10 +995,9 @@ suite('AgentSideEffects', () => { config: { [AgentHostTelemetryLevelConfigKey]: telemetryLevelToAgentHostConfigValue(TelemetryLevel.NONE) }, }; - sideEffects.handleAction(action); - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), action); + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionTurnStarted, - session: sessionUri.toString(), turnId: 'turn-1', userMessage: { text: 'hello world' }, }); @@ -1099,11 +1071,10 @@ suite('AgentSideEffects', () => { const action: SessionAction = { type: ActionType.SessionCustomizationToggled, - session: sessionUri.toString(), uri: 'file:///plugin-a', enabled: false, }; - sideEffects.handleAction(action); + sideEffects.handleAction(sessionUri.toString(), action); assert.deepStrictEqual(agent.setCustomizationEnabledCalls, [ { uri: 'file:///plugin-a', enabled: false }, @@ -1124,7 +1095,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-conf-1', toolName: 'read', displayName: 'Read File', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1132,7 +1103,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-conf-1', invocationMessage: 'Reading file', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1151,9 +1122,8 @@ suite('AgentSideEffects', () => { }); // Now confirm the tool call - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionToolCallConfirmed, - session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-conf-1', approved: true, @@ -1173,7 +1143,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-deny-1', toolName: 'shell', displayName: 'Shell', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1181,15 +1151,14 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-deny-1', invocationMessage: 'Running command', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, }); - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionToolCallConfirmed, - session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-deny-1', approved: false, @@ -1215,7 +1184,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-ready-1', toolName: 'runTask', displayName: 'Run Task', toolClientId: 'test-client', _meta: { toolKind: undefined, language: undefined }, }, @@ -1254,7 +1223,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-perm-1', toolName: 'write', displayName: 'Write File', toolClientId: 'test-client', _meta: { toolKind: undefined, language: undefined }, }, @@ -1294,7 +1263,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-parent', toolName: 'runSubagent', displayName: 'Run Subagent', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1302,7 +1271,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-parent', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1313,7 +1282,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-inner', toolName: 'problems', displayName: 'Problems', toolClientId: 'client-tools', _meta: { toolKind: undefined, language: undefined }, }, @@ -1393,7 +1362,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-bypass-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1401,7 +1370,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-bypass-1', invocationMessage: 'Write .env', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1432,7 +1401,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-ap-shell-1', toolName: 'shell', displayName: 'Shell', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1440,7 +1409,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-ap-shell-1', invocationMessage: 'Run rm -rf /', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1471,7 +1440,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-default-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1479,7 +1448,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-default-1', invocationMessage: 'Write .env', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1506,16 +1475,15 @@ suite('AgentSideEffects', () => { disposables.add(sideEffects.registerProgressListener(agent)); // Change to bypass mid-session - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri.toString(), { type: ActionType.SessionConfigChanged, - session: sessionUri.toString(), config: { autoApprove: 'autoApprove' }, }); agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-mid-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1523,7 +1491,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-mid-1', invocationMessage: 'Write .env', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1559,7 +1527,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-auto-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1567,7 +1535,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-auto-1', invocationMessage: 'Write file', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1601,7 +1569,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-env-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1609,7 +1577,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-env-1', invocationMessage: 'Write .env', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1642,7 +1610,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-pkg-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1650,7 +1618,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-pkg-1', invocationMessage: 'Write package.json', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1678,7 +1646,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-lock-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1686,7 +1654,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-lock-1', invocationMessage: 'Write yarn.lock', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1714,7 +1682,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-git-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1722,7 +1690,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-git-1', invocationMessage: 'Write .git/config', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1755,7 +1723,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-read-1', toolName: 'read', displayName: 'Read', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1763,7 +1731,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-read-1', invocationMessage: 'Read file', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1796,7 +1764,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-read-2', toolName: 'read', displayName: 'Read', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1804,7 +1772,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-read-2', invocationMessage: 'Read file', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -1864,9 +1832,8 @@ suite('AgentSideEffects', () => { project: { uri: 'file:///test-project', displayName: 'Test Project' }, }); - localSideEffects.handleAction({ + localSideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionTitleChanged, - session: sessionUri.toString(), title: 'Custom Title', }); @@ -1948,14 +1915,12 @@ suite('AgentSideEffects', () => { session.config = { schema: { type: 'object', properties: {} }, values: { autoApprove: 'default' } }; // Mid-session change merges new values into existing. - localStateManager.dispatchClientAction({ + localStateManager.dispatchClientAction(sessionUri.toString(), { type: ActionType.SessionConfigChanged, - session: sessionUri.toString(), config: { autoApprove: 'autoApprove' }, }, { clientId: 'test-client', clientSeq: 1 }); - localSideEffects.handleAction({ + localSideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionConfigChanged, - session: sessionUri.toString(), config: { autoApprove: 'autoApprove' }, }); @@ -1980,7 +1945,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -1988,7 +1953,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating task...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -2029,15 +1994,15 @@ suite('AgentSideEffects', () => { disposables.add(sideEffects.registerProgressListener(agent)); // Start parent tool + subagent - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); agent.fireProgress({ kind: 'subagent_started', session: sessionUri, toolCallId: 'tc-1', agentName: 'helper', agentDisplayName: 'Helper', agentDescription: 'Helps' }); // Fire an inner tool start with parentToolCallId agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-1', action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'inner-tc-1', toolName: 'readFile', displayName: 'Read File', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -2045,7 +2010,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-1', action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'inner-tc-1', invocationMessage: 'Reading file...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -2078,14 +2043,14 @@ suite('AgentSideEffects', () => { startTurn('turn-1'); disposables.add(sideEffects.registerProgressListener(agent)); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); // Inner event arrives but `subagent_started` never does. agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-1', action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'inner-1', toolName: 'read', displayName: 'Read', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -2093,7 +2058,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-1', action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'inner-1', invocationMessage: 'Reading...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -2103,7 +2068,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallComplete, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallComplete, turnId: 'turn-1', toolCallId: 'tc-1', result: { success: false, pastTenseMessage: 'Failed' }, }, @@ -2129,8 +2094,8 @@ suite('AgentSideEffects', () => { disposables.add(sideEffects.registerProgressListener(agent)); // Start parent tool + subagent - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); agent.fireProgress({ kind: 'subagent_started', session: sessionUri, toolCallId: 'tc-1', agentName: 'helper', agentDisplayName: 'Helper', agentDescription: 'Helps' }); // Completing the parent tool call must NOT tear down the @@ -2139,7 +2104,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallComplete, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallComplete, turnId: 'turn-1', toolCallId: 'tc-1', result: { success: true, pastTenseMessage: 'Started in background' }, }, @@ -2165,18 +2130,17 @@ suite('AgentSideEffects', () => { disposables.add(sideEffects.registerProgressListener(agent)); // Start two parent tool calls with subagents - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Sub 1', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating 1...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Sub 1', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating 1...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); agent.fireProgress({ kind: 'subagent_started', session: sessionUri, toolCallId: 'tc-1', agentName: 'sub1', agentDisplayName: 'Sub 1', agentDescription: 'First' }); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-2', toolName: 'runSubagent', displayName: 'Sub 2', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-2', invocationMessage: 'Delegating 2...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-2', toolName: 'runSubagent', displayName: 'Sub 2', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-2', invocationMessage: 'Delegating 2...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); agent.fireProgress({ kind: 'subagent_started', session: sessionUri, toolCallId: 'tc-2', agentName: 'sub2', agentDisplayName: 'Sub 2', agentDescription: 'Second' }); // Cancel via parent turn cancellation - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionTurnCancelled, - session: sessionUri.toString(), turnId: 'turn-1', }); @@ -2192,8 +2156,8 @@ suite('AgentSideEffects', () => { startTurn('turn-1'); disposables.add(sideEffects.registerProgressListener(agent)); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Sub 1', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Sub 1', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); agent.fireProgress({ kind: 'subagent_started', session: sessionUri, toolCallId: 'tc-1', agentName: 'sub', agentDisplayName: 'Sub', agentDescription: 'Has subagent' }); const subagentUri = `${sessionUri.toString()}/subagent/tc-1`; @@ -2209,14 +2173,14 @@ suite('AgentSideEffects', () => { startTurn('turn-1'); disposables.add(sideEffects.registerProgressListener(agent)); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); agent.fireProgress({ kind: 'subagent_started', session: sessionUri, toolCallId: 'tc-1', agentName: 'helper', agentDisplayName: 'Helper', agentDescription: 'Helps' }); // Fire a delta with parentToolCallId agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-1', - action: { type: ActionType.SessionResponsePart, session: sessionUri.toString(), turnId: 'turn-1', part: { kind: ResponsePartKind.Markdown, id: 'msg-sub', content: 'thinking...' } }, + action: { type: ActionType.SessionResponsePart, turnId: 'turn-1', part: { kind: ResponsePartKind.Markdown, id: 'msg-sub', content: 'thinking...' } }, }); // Verify the delta went to the subagent session @@ -2234,8 +2198,8 @@ suite('AgentSideEffects', () => { startTurn('turn-1'); disposables.add(sideEffects.registerProgressListener(agent)); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'task', displayName: 'Task', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'task', displayName: 'Task', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); agent.fireProgress({ kind: 'subagent_started', session: sessionUri, toolCallId: 'tc-1', agentName: 'explore', agentDisplayName: 'Explore', agentDescription: 'Explores' }); // Verify subagent content is on the running tool @@ -2250,7 +2214,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallComplete, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallComplete, turnId: 'turn-1', toolCallId: 'tc-1', result: { success: true, pastTenseMessage: 'Delegated', content: [{ type: ToolResultContentType.Text, text: 'Done' }] }, }, @@ -2279,14 +2243,14 @@ suite('AgentSideEffects', () => { disposables.add(sideEffects.registerProgressListener(agent)); // 1. Parent tool starts (the `task` invocation). - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-parent', toolName: 'task', displayName: 'Task', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-parent', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-parent', toolName: 'task', displayName: 'Task', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-parent', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); // 2. Inner tool fires BEFORE subagent_started (race condition). agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'inner-tc-1', toolName: 'readFile', displayName: 'Read File', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -2294,7 +2258,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'inner-tc-1', invocationMessage: 'Reading file...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -2330,8 +2294,8 @@ suite('AgentSideEffects', () => { disposables.add(sideEffects.registerProgressListener(agent)); // Parent task tool spawns a subagent. - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-parent', toolName: 'task', displayName: 'Task', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-parent', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-parent', toolName: 'task', displayName: 'Task', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-parent', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); agent.fireProgress({ kind: 'subagent_started', session: sessionUri, toolCallId: 'tc-parent', agentName: 'helper', agentDisplayName: 'Helper', agentDescription: 'Helps' }); // Inner tool inside the subagent requests permission to read a file @@ -2339,7 +2303,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'inner-read-1', toolName: 'read', displayName: 'Read', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -2347,7 +2311,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'inner-read-1', invocationMessage: 'Read file', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -2393,8 +2357,8 @@ suite('AgentSideEffects', () => { }; } - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-parent', toolName: 'task', displayName: 'Task', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); - agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-parent', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-parent', toolName: 'task', displayName: 'Task', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined } } }); + agent.fireProgress({ kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-parent', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded } }); agent.fireProgress({ kind: 'subagent_started', session: sessionUri, toolCallId: 'tc-parent', agentName: 'helper', agentDisplayName: 'Helper', agentDescription: 'Helps' }); // Inner write outside the workspace would normally NOT auto-approve, @@ -2402,7 +2366,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'inner-write-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -2410,7 +2374,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'inner-write-1', invocationMessage: 'Write file', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -2444,7 +2408,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-perm-1', toolName: 'CustomTool', displayName: 'Custom Tool', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -2452,7 +2416,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-perm-1', invocationMessage: 'Running custom tool', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -2494,7 +2458,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-perm-2', toolName: 'CustomTool', displayName: 'Custom Tool', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -2502,7 +2466,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-perm-2', invocationMessage: 'Running custom tool', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -2519,9 +2483,8 @@ suite('AgentSideEffects', () => { permissionKind: 'custom-tool', permissionPath: undefined, }); - sideEffects.handleAction({ + sideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionToolCallConfirmed, - session: sessionUri.toString(), turnId: 'turn-1', toolCallId: 'tc-perm-2', approved: true, @@ -2551,7 +2514,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-perm-3', toolName: 'CustomTool', displayName: 'Custom Tool', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -2559,7 +2522,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-perm-3', invocationMessage: 'Running custom tool', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -2596,7 +2559,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-parent', toolName: 'task', displayName: 'Task', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -2604,7 +2567,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-parent', invocationMessage: 'Delegating...', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -2620,7 +2583,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'inner-perm-1', toolName: 'CustomTool', displayName: 'Custom Tool', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -2628,7 +2591,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'inner-perm-1', invocationMessage: 'Running custom tool', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -2673,7 +2636,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallStart, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-edit-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, _meta: { toolKind: undefined, language: undefined }, }, @@ -2681,7 +2644,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallReady, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallReady, turnId: 'turn-1', toolCallId: 'tc-edit-1', invocationMessage: 'Write file', toolInput: undefined, confirmed: ToolCallConfirmationReason.NotNeeded, }, @@ -2689,7 +2652,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, action: { - type: ActionType.SessionToolCallComplete, session: sessionUri.toString(), turnId: 'turn-1', + type: ActionType.SessionToolCallComplete, turnId: 'turn-1', toolCallId: 'tc-edit-1', result: { success: true, @@ -2697,8 +2660,8 @@ suite('AgentSideEffects', () => { content: [{ type: ToolResultContentType.FileEdit, after: { uri: 'file:///wd/a.ts', content: { uri: 'file:///wd/a.ts' } }, - diff: { added: 1, removed: 0 }, - }], + diff: { added: 1, removed: 0 } + }] }, }, }); @@ -2721,7 +2684,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ kind: 'action', session: sessionUri, - action: { type: ActionType.SessionTurnComplete, session: sessionUri.toString(), turnId: 'turn-1' }, + action: { type: ActionType.SessionTurnComplete, turnId: 'turn-1' }, }); assert.deepStrictEqual(changesets.turnCompletes, [{ session: sessionUri.toString(), turnId: 'turn-1' }]); @@ -2738,9 +2701,8 @@ suite('AgentSideEffects', () => { onTurnComplete: () => { }, }, undefined, NullTelemetryService, changesets); - localSideEffects.handleAction({ + localSideEffects.handleAction(sessionUri.toString(), { type: ActionType.SessionTruncated, - session: sessionUri.toString(), turnId: 'turn-1', }); diff --git a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts index d45901a897e..122c1250d09 100644 --- a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts @@ -1338,7 +1338,7 @@ suite('ClaudeAgent', () => { partIdsMatch: part.part.id === firstDelta.partId && part.part.id === secondDelta.partId, turnId: part.turnId, deltaTexts: [firstDelta.content, secondDelta.content], - session: part.session.toString(), + session: partActions[0].s.kind === 'action' ? partActions[0].s.session.toString() : undefined, }, { partKindIsMarkdown: true, partPrecedesDelta: true, diff --git a/src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts b/src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts index be0356c16c1..859528b12d3 100644 --- a/src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts @@ -97,7 +97,7 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { assert.strictEqual(out.length, 3); const start = out[0]; assert.ok(start.kind === 'action' && start.action.type === ActionType.SessionResponsePart); - assert.strictEqual(start.action.session, SESSION_STR); + assert.strictEqual(start.session.toString(), SESSION_STR); assert.strictEqual(start.action.turnId, TURN_ID); assert.strictEqual(start.action.part.kind, ResponsePartKind.Markdown); const partId = start.action.part.id; @@ -109,7 +109,6 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { session: SESSION, action: { type: ActionType.SessionDelta, - session: SESSION_STR, turnId: TURN_ID, partId, content: 'Hello, ', @@ -120,7 +119,6 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { session: SESSION, action: { type: ActionType.SessionDelta, - session: SESSION_STR, turnId: TURN_ID, partId, content: 'world!', @@ -161,7 +159,6 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { session: SESSION, action: { type: ActionType.SessionReasoning, - session: SESSION_STR, turnId: TURN_ID, partId, content: 'pondering', @@ -190,7 +187,6 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { session: SESSION, action: { type: ActionType.SessionToolCallStart, - session: SESSION_STR, turnId: TURN_ID, toolCallId: 'tu_1', toolName: 'Read', @@ -222,7 +218,6 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { session: SESSION, action: { type: ActionType.SessionToolCallDelta, - session: SESSION_STR, turnId: TURN_ID, toolCallId: 'tu_1', content: '{"file_pa', @@ -250,7 +245,6 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { session: SESSION, action: { type: ActionType.SessionToolCallReady, - session: SESSION_STR, turnId: TURN_ID, toolCallId: 'tu_b', invocationMessage: { markdown: 'Running `git status`' }, @@ -289,7 +283,6 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { session: SESSION, action: { type: ActionType.SessionToolCallComplete, - session: SESSION_STR, turnId: TURN_ID, toolCallId: 'tu_1', result: { @@ -518,13 +511,12 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { session: SESSION, action: { type: ActionType.SessionUsage, - session: SESSION_STR, turnId: TURN_ID, usage: { inputTokens: 12, outputTokens: 34, cacheReadTokens: 5, - model: 'claude-test', + model: 'claude-test' }, }, }, diff --git a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts index b559a712ac4..4391fbc4c49 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts @@ -287,7 +287,6 @@ class TestableCopilotAgent extends CopilotAgent { session: sessionUri, action: { type: ActionType.SessionResponsePart, - session: sessionUri.toString(), turnId, part: { kind: ResponsePartKind.Markdown, id: `synth-${Date.now()}`, content }, }, diff --git a/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts b/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts index be8a09657cc..a49d0acf5c1 100644 --- a/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts @@ -127,7 +127,7 @@ suite('CopilotShellTools', () => { function markCreatedTerminalsExist(terminalManager: TestAgentHostTerminalManager): void { for (const created of terminalManager.created) { - terminalManager.existingTerminalUris.add(created.params.terminal); + terminalManager.existingTerminalUris.add(created.params.channel); } } diff --git a/src/vs/platform/agentHost/test/node/copilotSlashCommandCompletionProvider.test.ts b/src/vs/platform/agentHost/test/node/copilotSlashCommandCompletionProvider.test.ts index c74b6601871..855270c6a99 100644 --- a/src/vs/platform/agentHost/test/node/copilotSlashCommandCompletionProvider.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotSlashCommandCompletionProvider.test.ts @@ -53,13 +53,13 @@ suite('CopilotSlashCommandCompletionProvider', () => { const session = 'copilotcli:/abc'; async function run(text: string, offset = text.length) { - return provider.provideCompletionItems({ kind: CompletionItemKind.UserMessage, session, text, offset }, CancellationToken.None); + return provider.provideCompletionItems({ kind: CompletionItemKind.UserMessage, channel: session, text, offset }, CancellationToken.None); } test('returns nothing for non-copilotcli scheme', async () => { const items = await provider.provideCompletionItems({ kind: CompletionItemKind.UserMessage, - session: 'claude:/abc', + channel: 'claude:/abc', text: '/', offset: 1, }, CancellationToken.None); @@ -127,7 +127,7 @@ suite('CopilotSlashCommandCompletionProvider', () => { test('omits /compact when session has no history', async () => { const gated = new CopilotSlashCommandCompletionProvider('copilotcli', { hasHistory: () => false }); const items = await gated.provideCompletionItems({ - kind: CompletionItemKind.UserMessage, session, text: '/', offset: 1, + kind: CompletionItemKind.UserMessage, channel: session, text: '/', offset: 1, }, CancellationToken.None); assert.deepStrictEqual(items.map(i => i.insertText), ['/plan ']); }); @@ -138,7 +138,7 @@ suite('CopilotSlashCommandCompletionProvider', () => { hasHistory: (id: string) => { seen = id; return true; }, }); await gated.provideCompletionItems({ - kind: CompletionItemKind.UserMessage, session: 'copilotcli:/abc', text: '/', offset: 1, + kind: CompletionItemKind.UserMessage, channel: 'copilotcli:/abc', text: '/', offset: 1, }, CancellationToken.None); assert.strictEqual(seen, 'abc'); }); diff --git a/src/vs/platform/agentHost/test/node/mockAgent.ts b/src/vs/platform/agentHost/test/node/mockAgent.ts index 249aa4bcc5d..d009f28aa45 100644 --- a/src/vs/platform/agentHost/test/node/mockAgent.ts +++ b/src/vs/platform/agentHost/test/node/mockAgent.ts @@ -502,7 +502,6 @@ export class ScriptedMockAgent implements IAgent { initialReasoning, _action(session, { type: ActionType.SessionReasoning, - session: sessionStr, turnId: tid, partId, content: ' about this...', @@ -544,7 +543,6 @@ export class ScriptedMockAgent implements IAgent { // Client tools don't get auto-ready — toolStart with toolClientId only emits tool_start this._onDidSessionProgress.fire(_action(session, { type: ActionType.SessionToolCallStart, - session: sessionStr, turnId: tid, toolCallId: 'tc-client-1', toolName: 'runTests', @@ -571,7 +569,6 @@ export class ScriptedMockAgent implements IAgent { await timeout(10); this._onDidSessionProgress.fire(_action(session, { type: ActionType.SessionToolCallStart, - session: sessionStr, turnId: tid, toolCallId: 'tc-client-perm-1', toolName: 'runTests', @@ -775,7 +772,6 @@ function _action(session: URI, action: import('../../common/state/sessionActions function _markdown(session: URI, sessionStr: string, turnId: string, content: string, parentToolCallId?: string): IAgentActionSignal { return _action(session, { type: ActionType.SessionResponsePart, - session: sessionStr, turnId, part: { kind: ResponsePartKind.Markdown, id: `mock-md-${++_mockPartIdCounter}`, content }, }, parentToolCallId); @@ -785,7 +781,6 @@ function _markdown(session: URI, sessionStr: string, turnId: string, content: st function _reasoning(session: URI, sessionStr: string, turnId: string, content: string): IAgentActionSignal { return _action(session, { type: ActionType.SessionResponsePart, - session: sessionStr, turnId, part: { kind: ResponsePartKind.Reasoning, id: `mock-rs-${++_mockPartIdCounter}`, content }, }); @@ -793,22 +788,22 @@ function _reasoning(session: URI, sessionStr: string, turnId: string, content: s /** Creates a {@link ActionType.SessionTurnComplete} signal. */ function _idle(session: URI, sessionStr: string, turnId: string): IAgentActionSignal { - return _action(session, { type: ActionType.SessionTurnComplete, session: sessionStr, turnId }); + return _action(session, { type: ActionType.SessionTurnComplete, turnId }); } /** Creates a {@link ActionType.SessionError} signal. */ function _error(session: URI, sessionStr: string, turnId: string, errorType: string, message: string, stack?: string): IAgentActionSignal { - return _action(session, { type: ActionType.SessionError, session: sessionStr, turnId, error: { errorType, message, stack } }); + return _action(session, { type: ActionType.SessionError, turnId, error: { errorType, message, stack } }); } /** Creates a {@link ActionType.SessionTitleChanged} signal. */ function _titleChanged(session: URI, sessionStr: string, title: string): IAgentActionSignal { - return _action(session, { type: ActionType.SessionTitleChanged, session: sessionStr, title }); + return _action(session, { type: ActionType.SessionTitleChanged, title }); } /** Creates a {@link ActionType.SessionUsage} signal. */ function _usage(session: URI, sessionStr: string, turnId: string, usage: UsageInfo): IAgentActionSignal { - return _action(session, { type: ActionType.SessionUsage, session: sessionStr, turnId, usage }); + return _action(session, { type: ActionType.SessionUsage, turnId, usage }); } /** @@ -835,7 +830,6 @@ function _toolStart(session: URI, sessionStr: string, turnId: string, toolCallId } const signals: IAgentActionSignal[] = [_action(session, { type: ActionType.SessionToolCallStart, - session: sessionStr, turnId, toolCallId, toolName, @@ -846,7 +840,6 @@ function _toolStart(session: URI, sessionStr: string, turnId: string, toolCallId if (!opts?.toolClientId) { signals.push(_action(session, { type: ActionType.SessionToolCallReady, - session: sessionStr, turnId, toolCallId, invocationMessage, @@ -859,7 +852,7 @@ function _toolStart(session: URI, sessionStr: string, turnId: string, toolCallId /** Creates a {@link ActionType.SessionToolCallComplete} signal. */ function _toolComplete(session: URI, sessionStr: string, turnId: string, toolCallId: string, result: ToolCallResult, parentToolCallId?: string): IAgentActionSignal { - return _action(session, { type: ActionType.SessionToolCallComplete, session: sessionStr, turnId, toolCallId, result }, parentToolCallId); + return _action(session, { type: ActionType.SessionToolCallComplete, turnId, toolCallId, result }, parentToolCallId); } /** Creates a {@link IAgentToolPendingConfirmationSignal}. */ diff --git a/src/vs/platform/agentHost/test/node/protocol/clientTools.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocol/clientTools.integrationTest.ts index 2849e35f563..65a959a439c 100644 --- a/src/vs/platform/agentHost/test/node/protocol/clientTools.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/clientTools.integrationTest.ts @@ -82,9 +82,9 @@ suite('Protocol WebSocket — Client Tools', function () { // Complete the client tool call client.notify('dispatchAction', { clientSeq: 2, + channel: sessionUri, action: { type: 'session/toolCallComplete', - session: sessionUri, turnId: 'turn-ct', toolCallId: 'tc-client-1', result: { @@ -137,9 +137,9 @@ suite('Protocol WebSocket — Client Tools', function () { // Approve the permission client.notify('dispatchAction', { clientSeq: 2, + channel: sessionUri, action: { type: 'session/toolCallConfirmed', - session: sessionUri, turnId: 'turn-cp', toolCallId: 'tc-client-perm-1', approved: true, @@ -170,9 +170,9 @@ suite('Protocol WebSocket — Client Tools', function () { // tool_ready that was generated by the event mapper. client.notify('dispatchAction', { clientSeq: 2, + channel: sessionUri, action: { type: 'session/toolCallComplete', - session: sessionUri, turnId: 'turn-ra', toolCallId: 'tc-client-1', result: { diff --git a/src/vs/platform/agentHost/test/node/protocol/copilotRealSdk.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocol/copilotRealSdk.integrationTest.ts index 3bf3e0a7f87..dd12d6b023b 100644 --- a/src/vs/platform/agentHost/test/node/protocol/copilotRealSdk.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/copilotRealSdk.integrationTest.ts @@ -98,8 +98,9 @@ defineSharedRealSdkTests(COPILOT_CONFIG); dispatchTurn(client, sessionUri, 'turn-usage', 'Reply with exactly "usage-ok" and do not use tools.', 1); const usageNotif = await client.waitForNotification(n => isActionNotification(n, 'session/usage'), 90_000); - const usageAction = getActionEnvelope(usageNotif).action as SessionUsageAction; - assert.strictEqual(usageAction.session, sessionUri); + const usageEnvelope = getActionEnvelope(usageNotif); + const usageAction = usageEnvelope.action as SessionUsageAction; + assert.strictEqual(usageEnvelope.channel, sessionUri); assert.strictEqual(usageAction.turnId, 'turn-usage'); assert.strictEqual(typeof usageAction.usage.model, 'string'); assert.ok(usageAction.usage.model); @@ -113,8 +114,8 @@ defineSharedRealSdkTests(COPILOT_CONFIG); assert.ok(cost > 0, `expected usage._meta.cost to be positive: ${JSON.stringify(usageAction.usage)}`); await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'), 90_000); - const snapshot = await client.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; const turn = state.turns.find(t => t.id === 'turn-usage'); assert.strictEqual(turn?.usage?._meta?.cost, cost); }); @@ -188,13 +189,14 @@ defineSharedRealSdkTests(COPILOT_CONFIG); } const envelope = getActionEnvelope(next); seenSeqs.add(envelope.serverSeq); - const action = envelope.action as { session: string; turnId: string; toolCallId: string; confirmed?: string }; + const action = envelope.action as { turnId: string; toolCallId: string; confirmed?: string }; if (!action.confirmed) { client.notify('dispatchAction', { + channel: envelope.channel, clientSeq: ++teardownSeq, action: { type: 'session/toolCallConfirmed', - session: action.session, turnId: action.turnId, + turnId: action.turnId, toolCallId: action.toolCallId, approved: true, }, }); diff --git a/src/vs/platform/agentHost/test/node/protocol/handshake.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocol/handshake.integrationTest.ts index 8c4e307fe42..fe37e9f93a6 100644 --- a/src/vs/platform/agentHost/test/node/protocol/handshake.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/handshake.integrationTest.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { URI } from '../../../../../base/common/uri.js'; import { PROTOCOL_VERSION } from '../../../common/state/protocol/version/registry.js'; import { JSON_RPC_PARSE_ERROR, type InitializeResult, type JsonRpcErrorResponse, } from '../../../common/state/sessionProtocol.js'; +import { ROOT_STATE_URI } from '../../../common/state/sessionState.js'; import { IServerHandle, nextSessionUri, startServer, TestProtocolClient } from './testHelpers.js'; suite('Protocol WebSocket — Handshake & Errors', function () { @@ -43,7 +43,7 @@ suite('Protocol WebSocket — Handshake & Errors', function () { const result = await client.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-handshake', - initialSubscriptions: [URI.from({ scheme: 'agenthost', path: '/root' }).toString()], + initialSubscriptions: [ROOT_STATE_URI], }); assert.strictEqual(result.protocolVersion, PROTOCOL_VERSION); @@ -75,16 +75,16 @@ suite('Protocol WebSocket — Handshake & Errors', function () { let gotError = false; try { - await client.call('createSession', { session: nextSessionUri(), provider: 'nonexistent' }); + await client.call('createSession', { channel: nextSessionUri(), provider: 'nonexistent' }); } catch { gotError = true; } assert.ok(gotError, 'should have received an error for invalid provider'); // Server should still be functional - await client.call('createSession', { session: nextSessionUri(), provider: 'mock' }); + await client.call('createSession', { channel: nextSessionUri(), provider: 'mock' }); const notif = await client.waitForNotification(n => - n.method === 'notification' && (n.params as { notification: { type: string } }).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); assert.ok(notif); }); diff --git a/src/vs/platform/agentHost/test/node/protocol/multiClient.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocol/multiClient.integrationTest.ts index ff7bd2c202a..2e6600410e2 100644 --- a/src/vs/platform/agentHost/test/node/protocol/multiClient.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/multiClient.integrationTest.ts @@ -5,10 +5,10 @@ import assert from 'assert'; import { SubscribeResult } from '../../../common/state/protocol/commands.js'; -import type { SessionAddedNotification, SessionRemovedNotification } from '../../../common/state/sessionActions.js'; +import type { SessionAddedParams, SessionRemovedParams } from '../../../common/state/protocol/notifications.js'; import { PROTOCOL_VERSION } from '../../../common/state/protocol/version/registry.js'; -import type { INotificationBroadcastParams, ReconnectResult } from '../../../common/state/sessionProtocol.js'; -import type { SessionState } from '../../../common/state/sessionState.js'; +import type { ReconnectResult } from '../../../common/state/sessionProtocol.js'; +import { ROOT_STATE_URI, type SessionState } from '../../../common/state/sessionState.js'; import { createAndSubscribeSession, dispatchTurnStarted, @@ -47,28 +47,28 @@ suite('Protocol WebSocket — Multi-Client', function () { test('sessionAdded notification is broadcast to all connected clients', async function () { this.timeout(10_000); - await client.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-broadcast-add-1' }); + await client.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-broadcast-add-1' }); const client2 = new TestProtocolClient(server.port); await client2.connect(); - await client2.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-broadcast-add-2' }); + await client2.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-broadcast-add-2' }); client.clearReceived(); client2.clearReceived(); - await client.call('createSession', { session: nextSessionUri(), provider: 'mock' }); + await client.call('createSession', { channel: nextSessionUri(), provider: 'mock' }); const n1 = await client.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); const n2 = await client2.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); assert.ok(n1, 'client 1 should receive sessionAdded'); assert.ok(n2, 'client 2 should receive sessionAdded'); - const uri1 = ((n1.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource; - const uri2 = ((n2.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource; + const uri1 = (n1.params as SessionAddedParams).summary.resource; + const uri2 = (n2.params as SessionAddedParams).summary.resource; assert.strictEqual(uri1, uri2, 'both clients should see the same session URI'); client2.close(); @@ -81,22 +81,22 @@ suite('Protocol WebSocket — Multi-Client', function () { const client2 = new TestProtocolClient(server.port); await client2.connect(); - await client2.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-broadcast-remove-2' }); + await client2.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-broadcast-remove-2' }); client2.clearReceived(); - await client.call('disposeSession', { session: sessionUri }); + await client.call('disposeSession', { channel: sessionUri }); const n1 = await client.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionRemoved' + n.method === 'root/sessionRemoved' ); const n2 = await client2.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionRemoved' + n.method === 'root/sessionRemoved' ); assert.ok(n1, 'client 1 should receive sessionRemoved'); assert.ok(n2, 'client 2 should receive sessionRemoved even without subscribing'); - const removed1 = (n1.params as INotificationBroadcastParams).notification as SessionRemovedNotification; - const removed2 = (n2.params as INotificationBroadcastParams).notification as SessionRemovedNotification; + const removed1 = n1.params as SessionRemovedParams; + const removed2 = n2.params as SessionRemovedParams; assert.strictEqual(removed1.session.toString(), sessionUri.toString()); assert.strictEqual(removed2.session.toString(), sessionUri.toString()); @@ -110,8 +110,8 @@ suite('Protocol WebSocket — Multi-Client', function () { const client2 = new TestProtocolClient(server.port); await client2.connect(); - await client2.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-multi-client-2' }); - await client2.call('subscribe', { resource: sessionUri }); + await client2.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-multi-client-2' }); + await client2.call('subscribe', { channel: sessionUri }); client2.clearReceived(); dispatchTurnStarted(client, sessionUri, 'turn-mc', 'hello', 1); @@ -134,8 +134,8 @@ suite('Protocol WebSocket — Multi-Client', function () { const client2 = new TestProtocolClient(server.port); await client2.connect(); - await client2.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-cross-msg-2' }); - await client2.call('subscribe', { resource: sessionUri }); + await client2.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-cross-msg-2' }); + await client2.call('subscribe', { channel: sessionUri }); client.clearReceived(); client2.clearReceived(); @@ -160,8 +160,8 @@ suite('Protocol WebSocket — Multi-Client', function () { const client2 = new TestProtocolClient(server.port); await client2.connect(); - await client2.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-tool-progress-2' }); - await client2.call('subscribe', { resource: sessionUri }); + await client2.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-tool-progress-2' }); + await client2.call('subscribe', { channel: sessionUri }); client.clearReceived(); client2.clearReceived(); @@ -183,14 +183,14 @@ suite('Protocol WebSocket — Multi-Client', function () { this.timeout(10_000); const sessionUri = await createAndSubscribeSession(client, 'test-unsubscribe'); - client.notify('unsubscribe', { resource: sessionUri }); + client.notify('unsubscribe', { channel: sessionUri }); await new Promise(resolve => setTimeout(resolve, 100)); client.clearReceived(); const client2 = new TestProtocolClient(server.port); await client2.connect(); - await client2.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-unsub-helper' }); - await client2.call('subscribe', { resource: sessionUri }); + await client2.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-unsub-helper' }); + await client2.call('subscribe', { channel: sessionUri }); dispatchTurnStarted(client2, sessionUri, 'turn-unsub', 'hello', 1); await client2.waitForNotification(n => isActionNotification(n, 'session/turnComplete')); @@ -209,7 +209,7 @@ suite('Protocol WebSocket — Multi-Client', function () { const client2 = new TestProtocolClient(server.port); await client2.connect(); - await client2.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-scoping-2' }); + await client2.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-scoping-2' }); // Client 2 does NOT subscribe to the session client2.clearReceived(); @@ -223,10 +223,10 @@ suite('Protocol WebSocket — Multi-Client', function () { // But disposing the session should still broadcast a notification client2.clearReceived(); - await client.call('disposeSession', { session: sessionUri }); + await client.call('disposeSession', { channel: sessionUri }); const removed = await client2.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionRemoved' + n.method === 'root/sessionRemoved' ); assert.ok(removed, 'unsubscribed client should still receive sessionRemoved notification'); @@ -243,10 +243,10 @@ suite('Protocol WebSocket — Multi-Client', function () { // Client 2 joins after the turn has completed const client2 = new TestProtocolClient(server.port); await client2.connect(); - await client2.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-late-sub-2' }); + await client2.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-late-sub-2' }); - const result = await client2.call('subscribe', { resource: sessionUri }); - const state = result.snapshot.state as SessionState; + const result = await client2.call('subscribe', { channel: sessionUri }); + const state = result.snapshot!.state as SessionState; assert.ok(state.turns.length >= 1, `late subscriber should see completed turn, got ${state.turns.length}`); assert.strictEqual(state.turns[0].id, 'turn-late'); assert.strictEqual(state.turns[0].state, 'complete'); @@ -261,8 +261,8 @@ suite('Protocol WebSocket — Multi-Client', function () { const client2 = new TestProtocolClient(server.port); await client2.connect(); - await client2.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-cross-perm-2' }); - await client2.call('subscribe', { resource: sessionUri }); + await client2.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-cross-perm-2' }); + await client2.call('subscribe', { channel: sessionUri }); client.clearReceived(); client2.clearReceived(); @@ -277,10 +277,10 @@ suite('Protocol WebSocket — Multi-Client', function () { // Client B confirms the tool call client2.notify('dispatchAction', { + channel: sessionUri, clientSeq: 1, action: { type: 'session/toolCallConfirmed', - session: sessionUri, turnId: 'turn-cross-perm', toolCallId: 'tc-perm-1', approved: true, diff --git a/src/vs/platform/agentHost/test/node/protocol/realSdkTestHelpers.ts b/src/vs/platform/agentHost/test/node/protocol/realSdkTestHelpers.ts index 565400d9ed0..589e3715930 100644 --- a/src/vs/platform/agentHost/test/node/protocol/realSdkTestHelpers.ts +++ b/src/vs/platform/agentHost/test/node/protocol/realSdkTestHelpers.ts @@ -21,7 +21,6 @@ import { tmpdir } from 'os'; import { removeAnsiEscapeCodes } from '../../../../../base/common/strings.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; -import { NotificationType } from '../../../common/state/protocol/notifications.js'; import { SubscribeResult } from '../../../common/state/protocol/commands.js'; import { PROTOCOL_VERSION } from '../../../common/state/protocol/version/registry.js'; import { @@ -31,12 +30,13 @@ import { type ToolResultContent, type ToolResultSubagentContent, } from '../../../common/state/sessionState.js'; import type { RootState } from '../../../common/state/protocol/state.js'; -import type { - RootAgentsChangedAction, - SessionAddedNotification, SessionInputRequestedAction, SessionToolCallReadyAction, - SessionToolCallStartAction, +import { + NotificationType, + type RootAgentsChangedAction, + type SessionInputRequestedAction, type SessionToolCallReadyAction, + type SessionToolCallStartAction, } from '../../../common/state/sessionActions.js'; -import type { INotificationBroadcastParams } from '../../../common/state/sessionProtocol.js'; +import type { SessionAddedParams } from '../../../common/state/protocol/notifications.js'; import { getActionEnvelope, isActionNotification, IServerHandle, startRealServer, TestProtocolClient, } from './testHelpers.js'; @@ -133,8 +133,8 @@ export async function createRealSession( trackingList: string[], workingDirectory?: string, ): Promise { - await c.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId }, 30_000); - await c.call('authenticate', { resource: 'https://api.github.com', token: resolveGitHubToken() }, 30_000); + await c.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId }, 30_000); + await c.call('authenticate', { channel: ROOT_STATE_URI, resource: 'https://api.github.com', token: resolveGitHubToken() }, 30_000); const sessionUri = URI.from({ scheme: config.scheme, path: `/${generateUuid()}` }).toString(); // Default to `folder` isolation so the agent runs in the directory the @@ -142,7 +142,7 @@ export async function createRealSession( // silently relocate the agent into `.worktrees/...` // and break tests that assert on filesystem state in the original dir. await c.call('createSession', { - session: sessionUri, + channel: sessionUri, provider: config.provider, workingDirectory, config: workingDirectory ? { isolation: 'folder' } : undefined, @@ -153,8 +153,8 @@ export async function createRealSession( // directly without waiting for the notification. trackingList.push(sessionUri); - const subscribeResult = await c.call('subscribe', { resource: sessionUri }); - void (subscribeResult.snapshot.state as SessionState); + const subscribeResult = await c.call('subscribe', { channel: sessionUri }); + void (subscribeResult.snapshot!.state as SessionState); c.clearReceived(); return sessionUri; @@ -163,10 +163,10 @@ export async function createRealSession( /** Dispatch a turn with the given user message text. */ export function dispatchTurn(c: TestProtocolClient, session: string, turnId: string, text: string, clientSeq: number): void { c.notify('dispatchAction', { + channel: session, clientSeq, action: { type: 'session/turnStarted', - session, turnId, userMessage: { text }, }, @@ -372,7 +372,7 @@ export function startBackgroundApprovalLoop(c: TestProtocolClient, options: IBac }, 2_000); const envelope = getActionEnvelope(ready); processedSeqs.add(envelope.serverSeq); - const action = envelope.action as SessionToolCallReadyAction & { session: string; turnId: string }; + const action = envelope.action as SessionToolCallReadyAction; if (action.confirmed) { continue; } @@ -388,10 +388,11 @@ export function startBackgroundApprovalLoop(c: TestProtocolClient, options: IBac if (!matchingRule) { errors.push(`unexpected tool call: toolName=${toolName ?? ''} input=${JSON.stringify(action.toolInput)}`); c.notify('dispatchAction', { + channel: envelope.channel, clientSeq: ++approvalSeq, action: { type: 'session/toolCallConfirmed', - session: action.session, turnId: action.turnId, + turnId: action.turnId, toolCallId: action.toolCallId, approved: false, }, }); @@ -402,10 +403,11 @@ export function startBackgroundApprovalLoop(c: TestProtocolClient, options: IBac approvedToolNames.add(matchingRule.toolName); c.notify('dispatchAction', { + channel: envelope.channel, clientSeq: ++approvalSeq, action: { type: 'session/toolCallConfirmed', - session: action.session, turnId: action.turnId, + turnId: action.turnId, toolCallId: action.toolCallId, approved: true, }, }); @@ -513,16 +515,16 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { test('listModels returns well-shaped model entries after authenticate', async function () { this.timeout(60_000); - await client.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: `real-sdk-list-models-${config.provider}` }, 30_000); + await client.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: `real-sdk-list-models-${config.provider}` }, 30_000); // Subscribe to root state *before* authenticating so we can observe // the agentsChanged action that carries the populated model list. - const rootResult = await client.call('subscribe', { resource: ROOT_STATE_URI }, 30_000); - const initial = rootResult.snapshot.state as RootState; + const rootResult = await client.call('subscribe', { channel: ROOT_STATE_URI }, 30_000); + const initial = rootResult.snapshot!.state as RootState; const providerAgent = initial.agents.find(a => a.provider === config.provider); assert.ok(providerAgent, `Expected ${config.provider} agent in root state, got: ${initial.agents.map(a => a.provider).join(', ')}`); - await client.call('authenticate', { resource: 'https://api.github.com', token: resolveGitHubToken() }, 30_000); + await client.call('authenticate', { channel: ROOT_STATE_URI, resource: 'https://api.github.com', token: resolveGitHubToken() }, 30_000); // Models load asynchronously after the *first* authenticate against // the shared server. If a sibling test already authenticated, the @@ -605,10 +607,11 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { } const action = getActionEnvelope(next).action as { toolCallId: string }; client.notify('dispatchAction', { + channel: sessionUri, clientSeq: nextSeq++, action: { type: 'session/toolCallConfirmed', - session: sessionUri, turnId: 'turn-perm', + turnId: 'turn-perm', toolCallId: action.toolCallId, approved: true, }, }); @@ -626,8 +629,9 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { const sessionUri = await createRealSession(client, config, `real-sdk-plan-mode-${config.provider}`, createdSessions, URI.file(tempDir).toString()); client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 1, - action: { type: 'session/configChanged', session: sessionUri, config: { mode: 'plan' } }, + action: { type: 'session/configChanged', config: { mode: 'plan' } }, }); await client.waitForNotification(n => isActionNotification(n, 'session/configChanged')); @@ -637,15 +641,15 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { assert.ok(planTurn.sawInputRequest, `should reach the ${config.exitPlanModeToolName} question so the test can continue the same session`); const extraSessionNotificationsAfterPlan = client.receivedNotifications(n => - n.method === 'notification' && - (n.params as INotificationBroadcastParams).notification.type === NotificationType.SessionAdded && - ((n.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource !== sessionUri, + n.method === NotificationType.SessionAdded && + (n.params as SessionAddedParams).summary.resource !== sessionUri, ); assert.strictEqual(extraSessionNotificationsAfterPlan.length, 0, 'should not create a second session while answering the plan-mode question'); client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 50, - action: { type: 'session/configChanged', session: sessionUri, config: { mode: 'interactive' } }, + action: { type: 'session/configChanged', config: { mode: 'interactive' } }, }); await client.waitForNotification(n => isActionNotification(n, 'session/configChanged')); @@ -655,14 +659,13 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { assert.match(followupTurn.responseText, /hello world/i, 'follow-up turn should retain the original plan context'); const extraSessionNotificationsAfterFollowup = client.receivedNotifications(n => - n.method === 'notification' && - (n.params as INotificationBroadcastParams).notification.type === NotificationType.SessionAdded && - ((n.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource !== sessionUri, + n.method === NotificationType.SessionAdded && + (n.params as SessionAddedParams).summary.resource !== sessionUri, ); assert.strictEqual(extraSessionNotificationsAfterFollowup.length, 0, 'sending another message should stay on the same session instead of forking'); - const resubscribeResult = await client.call('subscribe', { resource: sessionUri }); - const finalSnapshot = resubscribeResult.snapshot.state as SessionState; + const resubscribeResult = await client.call('subscribe', { channel: sessionUri }); + const finalSnapshot = resubscribeResult.snapshot!.state as SessionState; assert.strictEqual(finalSnapshot.summary.resource, sessionUri, 'follow-up turn should keep the original session resource'); }); @@ -678,8 +681,9 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { ); client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 2, - action: { type: 'session/abortTurn', session: sessionUri }, + action: { type: 'session/abortTurn' }, }); await client.waitForNotification(n => isActionNotification(n, 'session/abortTurn'), 10_000); @@ -692,15 +696,15 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { tempDirs.push(tempDir); const workingDirUri = URI.file(tempDir).toString(); - await client.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: `real-sdk-workdir-${config.provider}` }); - await client.call('authenticate', { resource: 'https://api.github.com', token: resolveGitHubToken() }); + await client.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: `real-sdk-workdir-${config.provider}` }); + await client.call('authenticate', { channel: ROOT_STATE_URI, resource: 'https://api.github.com', token: resolveGitHubToken() }); const sessionUri = URI.from({ scheme: config.scheme, path: `/${generateUuid()}` }).toString(); - await client.call('createSession', { session: sessionUri, provider: config.provider, workingDirectory: workingDirUri }); + await client.call('createSession', { channel: sessionUri, provider: config.provider, workingDirectory: workingDirUri }); createdSessions.push(sessionUri); - const subscribeResult = await client.call('subscribe', { resource: sessionUri }); - const sessionState = subscribeResult.snapshot.state as SessionState; + const subscribeResult = await client.call('subscribe', { channel: sessionUri }); + const sessionState = subscribeResult.snapshot!.state as SessionState; assert.strictEqual(sessionState.summary.workingDirectory, workingDirUri, `subscribe snapshot summary should carry the requested working directory`); }); @@ -717,23 +721,23 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { const defaultBranch = execSync('git branch --show-current', { cwd: tempDir, encoding: 'utf-8' }).trim(); const workingDirUri = URI.file(tempDir).toString(); - await client.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: `real-sdk-worktree-${config.provider}` }); - await client.call('authenticate', { resource: 'https://api.github.com', token: resolveGitHubToken() }); + await client.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: `real-sdk-worktree-${config.provider}` }); + await client.call('authenticate', { channel: ROOT_STATE_URI, resource: 'https://api.github.com', token: resolveGitHubToken() }); const sessionUri = URI.from({ scheme: config.scheme, path: `/${generateUuid()}` }).toString(); await client.call('createSession', { - session: sessionUri, provider: config.provider, workingDirectory: workingDirUri, + channel: sessionUri, provider: config.provider, workingDirectory: workingDirUri, config: { isolation: 'worktree', branch: defaultBranch }, }); createdSessions.push(sessionUri); - await client.call('subscribe', { resource: sessionUri }); + await client.call('subscribe', { channel: sessionUri }); client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 1, action: { type: 'session/activeClientChanged', - session: sessionUri, activeClient: { clientId: `real-sdk-worktree-${config.provider}`, displayName: 'Test Client', @@ -751,10 +755,10 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { 'What is your current working directory? Reply with just the absolute path and nothing else.', 2); const addedNotif = await client.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === NotificationType.SessionAdded, + n.method === NotificationType.SessionAdded, 60_000, ); - const addedSummary = ((addedNotif.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary; + const addedSummary = (addedNotif.params as SessionAddedParams).summary; assert.ok(addedSummary.workingDirectory, 'sessionAdded notification should have a workingDirectory'); assert.ok(addedSummary.workingDirectory!.includes('.worktrees'), @@ -785,10 +789,11 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { const toolReadyAction = getActionEnvelope(toolReadyNotif).action as { confirmed?: string }; if (!toolReadyAction.confirmed) { client.notify('dispatchAction', { + channel: addedSummary.resource, clientSeq: 4, action: { type: 'session/toolCallConfirmed', - session: addedSummary.resource, turnId: 'turn-wt-terminal', + turnId: 'turn-wt-terminal', toolCallId: toolStartAction.toolCallId, approved: true, }, }); @@ -805,13 +810,13 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { const terminalUri = terminalResourceFromContent(terminalContentAction.content); assert.ok(terminalUri, 'shell tool should expose its terminal resource'); - const terminalSubscribeResult = await client.call('subscribe', { resource: terminalUri }); - const initialTerminalState = terminalSubscribeResult.snapshot.state as TerminalState; + const terminalSubscribeResult = await client.call('subscribe', { channel: terminalUri }); + const initialTerminalState = terminalSubscribeResult.snapshot!.state as TerminalState; assert.strictEqual(initialTerminalState.cwd, resolvedWorkingDirectoryPath, 'terminal should be created in the resolved worktree directory'); await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'), 90_000); - const terminalSnapshot = await client.call('subscribe', { resource: terminalUri }); - const terminalState = terminalSnapshot.snapshot.state as TerminalState; + const terminalSnapshot = await client.call('subscribe', { channel: terminalUri }); + const terminalState = terminalSnapshot.snapshot!.state as TerminalState; assert.ok(terminalText(terminalState).includes(resolvedWorkingDirectoryPath), `pwd output should include the resolved worktree path ${resolvedWorkingDirectoryPath}`); }); @@ -843,13 +848,14 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { const envelope = getActionEnvelope(ready); if (!processedSeqs.has(envelope.serverSeq)) { processedSeqs.add(envelope.serverSeq); - const action = envelope.action as { session: string; turnId: string; toolCallId: string; confirmed?: string }; + const action = envelope.action as { turnId: string; toolCallId: string; confirmed?: string }; if (!action.confirmed) { client.notify('dispatchAction', { + channel: envelope.channel, clientSeq: ++approvalSeq, action: { type: 'session/toolCallConfirmed', - session: action.session, turnId: action.turnId, + turnId: action.turnId, toolCallId: action.toolCallId, approved: true, }, }); @@ -869,8 +875,9 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { if (!isActionNotification(n, 'session/toolCallContentChanged')) { return false; } - const action = getActionEnvelope(n).action as { session: string; content: readonly ToolResultContent[] }; - return action.session === sessionUri && action.content.some(c => c.type === ToolResultContentType.Subagent); + const envelope = getActionEnvelope(n); + const action = envelope.action as { content: readonly ToolResultContent[] }; + return envelope.channel === sessionUri && action.content.some(c => c.type === ToolResultContentType.Subagent); }, 120_000); const parentContent = (getActionEnvelope(subagentContentNotif).action as { content: readonly ToolResultContent[] }).content; @@ -879,23 +886,23 @@ export function defineSharedRealSdkTests(config: IRealSdkProviderConfig): void { assert.ok(typeof subagentSessionUri === 'string' && isSubagentSession(subagentSessionUri), `subagent session URI should be subagent-shaped, got: ${JSON.stringify(subagentSessionUri)}`); - await client.call('subscribe', { resource: subagentSessionUri }); + await client.call('subscribe', { channel: subagentSessionUri }); await client.waitForNotification(n => { if (!isActionNotification(n, 'session/turnComplete')) { return false; } - return (getActionEnvelope(n).action as { session: string }).session === sessionUri; + return getActionEnvelope(n).channel === sessionUri; }, 150_000); approvalsActive = false; await approvalLoop; const toolStarts = client.receivedNotifications(n => isActionNotification(n, 'session/toolCallStart')) - .map(n => getActionEnvelope(n).action as SessionToolCallStartAction); + .map(n => ({ channel: getActionEnvelope(n).channel, action: getActionEnvelope(n).action as SessionToolCallStartAction })); - const parentStarts = toolStarts.filter(a => (a.session as unknown as string) === sessionUri); - const subagentStarts = toolStarts.filter(a => (a.session as unknown as string) === subagentSessionUri); + const parentStarts = toolStarts.filter(t => t.channel === sessionUri).map(t => t.action); + const subagentStarts = toolStarts.filter(t => t.channel === subagentSessionUri).map(t => t.action); const parentNonTaskStarts = parentStarts.filter(a => a.toolName !== config.subagentToolName); assert.deepStrictEqual(parentNonTaskStarts.map(a => a.toolName), [], diff --git a/src/vs/platform/agentHost/test/node/protocol/sessionConfig.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocol/sessionConfig.integrationTest.ts index 10abcde3876..8016c5cf01d 100644 --- a/src/vs/platform/agentHost/test/node/protocol/sessionConfig.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/sessionConfig.integrationTest.ts @@ -8,10 +8,9 @@ import { mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import { URI } from '../../../../../base/common/uri.js'; import type { ResolveSessionConfigResult, SessionConfigCompletionsResult, SubscribeResult } from '../../../common/state/protocol/commands.js'; -import { ActionType, type SessionAddedNotification } from '../../../common/state/sessionActions.js'; +import { ActionType, type SessionAddedParams } from '../../../common/state/sessionActions.js'; import { PROTOCOL_VERSION } from '../../../common/state/protocol/version/registry.js'; -import type { INotificationBroadcastParams } from '../../../common/state/sessionProtocol.js'; -import type { SessionState } from '../../../common/state/sessionState.js'; +import { ROOT_STATE_URI, type SessionState } from '../../../common/state/sessionState.js'; import { getActionEnvelope, isActionNotification, @@ -39,7 +38,7 @@ suite('Protocol WebSocket - Session Config', function () { this.timeout(10_000); client = new TestProtocolClient(server.port); await client.connect(); - await client.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-session-config' }); + await client.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-session-config' }); }); teardown(function () { @@ -51,6 +50,7 @@ suite('Protocol WebSocket - Session Config', function () { const workingDirectory = URI.file('/mock/workspace').toString(); const initial = await client.call('resolveSessionConfig', { + channel: ROOT_STATE_URI, provider: 'mock', workingDirectory, }); @@ -62,6 +62,7 @@ suite('Protocol WebSocket - Session Config', function () { assert.strictEqual(initial.schema.properties.branch.readOnly, false); const folder = await client.call('resolveSessionConfig', { + channel: ROOT_STATE_URI, provider: 'mock', workingDirectory, config: { isolation: 'folder', branch: 'feature/config' }, @@ -76,6 +77,7 @@ suite('Protocol WebSocket - Session Config', function () { this.timeout(10_000); const result = await client.call('sessionConfigCompletions', { + channel: ROOT_STATE_URI, provider: 'mock', workingDirectory: URI.file('/mock/workspace').toString(), config: { isolation: 'worktree' }, @@ -93,20 +95,20 @@ suite('Protocol WebSocket - Session Config', function () { const config = { isolation: 'worktree', branch: 'feature/config' }; await client.call('createSession', { - session: nextSessionUri(), + channel: nextSessionUri(), provider: 'mock', workingDirectory: URI.file('/mock/workspace').toString(), config, }); const notif = await client.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); - const notification = (notif.params as INotificationBroadcastParams).notification as SessionAddedNotification; + const notification = notif.params as SessionAddedParams; assert.strictEqual(Object.hasOwn(notification.summary, 'config'), false); - const snapshot = await client.call('subscribe', { resource: notification.summary.resource }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: notification.summary.resource }); + const state = snapshot.snapshot!.state as SessionState; assert.deepStrictEqual(state.config?.values, config); assert.deepStrictEqual(Object.keys(state.config?.schema.properties ?? {}), ['isolation', 'branch']); }); @@ -115,23 +117,23 @@ suite('Protocol WebSocket - Session Config', function () { this.timeout(10_000); await client.call('createSession', { - session: nextSessionUri(), + channel: nextSessionUri(), provider: 'mock', config: { isolation: 'folder', branch: 'main' }, }); const notif = await client.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); - const session = ((notif.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource; - await client.call('subscribe', { resource: session }); + const session = (notif.params as SessionAddedParams).summary.resource; + await client.call('subscribe', { channel: session }); client.clearReceived(); client.notify('dispatchAction', { + channel: session, clientSeq: 1, action: { type: ActionType.SessionConfigChanged, - session, config: { branch: 'release' }, }, }); @@ -139,8 +141,8 @@ suite('Protocol WebSocket - Session Config', function () { const configChanged = await client.waitForNotification(n => isActionNotification(n, ActionType.SessionConfigChanged)); assert.strictEqual(getActionEnvelope(configChanged).action.type, ActionType.SessionConfigChanged); - const snapshot = await client.call('subscribe', { resource: session }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: session }); + const state = snapshot.snapshot!.state as SessionState; assert.deepStrictEqual(state.config?.values, { isolation: 'folder', branch: 'release' }); }); }); @@ -173,28 +175,28 @@ suite('Protocol WebSocket - Session Config persistence across restarts', functio try { const client1 = new TestProtocolClient(server1.port); await client1.connect(); - await client1.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-config-restore-1' }); + await client1.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-config-restore-1' }); await client1.call('createSession', { - session: nextSessionUri(), + channel: nextSessionUri(), provider: 'mock', workingDirectory: URI.file('/mock/workspace').toString(), config: initialConfig, }); const addedNotif = await client1.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); // The mock agent assigns its own URI rather than honoring the // requested one, so capture the real URI from the notification. - sessionUri = ((addedNotif.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource; + sessionUri = (addedNotif.params as SessionAddedParams).summary.resource; - await client1.call('subscribe', { resource: sessionUri }); + await client1.call('subscribe', { channel: sessionUri }); client1.notify('dispatchAction', { + channel: sessionUri, clientSeq: 1, action: { type: ActionType.SessionConfigChanged, - session: sessionUri, config: { branch: updatedBranch }, }, }); @@ -224,13 +226,13 @@ suite('Protocol WebSocket - Session Config persistence across restarts', functio try { const client2 = new TestProtocolClient(server2.port); await client2.connect(); - await client2.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-config-restore-2' }); + await client2.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-config-restore-2' }); // Subscribing triggers the restore-on-subscribe path on the server, // which reads `configValues` from the per-session DB and overlays // them on the freshly-resolved schema. - const snapshot = await client2.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client2.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; assert.ok(state.config, 'restored session should have state.config populated'); // Schema is re-resolved by the provider (worktree-mode mock returns diff --git a/src/vs/platform/agentHost/test/node/protocol/sessionDiffs.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocol/sessionDiffs.integrationTest.ts index d345f4da14d..ffc49427a49 100644 --- a/src/vs/platform/agentHost/test/node/protocol/sessionDiffs.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/sessionDiffs.integrationTest.ts @@ -10,9 +10,8 @@ import { tmpdir } from 'os'; import { join } from '../../../../../base/common/path.js'; import { URI } from '../../../../../base/common/uri.js'; import { SubscribeResult } from '../../../common/state/protocol/commands.js'; -import type { ChangesetFileSetAction, SessionAddedNotification } from '../../../common/state/sessionActions.js'; +import type { ChangesetFileSetAction, SessionAddedParams } from '../../../common/state/sessionActions.js'; import { PROTOCOL_VERSION } from '../../../common/state/protocol/version/registry.js'; -import type { INotificationBroadcastParams } from '../../../common/state/sessionProtocol.js'; import { dispatchTurnStarted, getActionEnvelope, @@ -81,19 +80,19 @@ const hasGit = (() => { await client.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-git-diffs' }); const workingDirectory = URI.file(tmpRoot).toString(); - await client.call('createSession', { session: nextSessionUri(), provider: 'mock', workingDirectory }); + await client.call('createSession', { channel: nextSessionUri(), provider: 'mock', workingDirectory }); const addedNotif = await client.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); - const sessionUri = ((addedNotif.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource; + const sessionUri = (addedNotif.params as SessionAddedParams).summary.resource; - await client.call('subscribe', { resource: sessionUri }); + await client.call('subscribe', { channel: sessionUri }); // Also subscribe to the session changeset URI: `changeset/*` envelopes // are scoped to the changeset URI by `_isRelevantToClient`, so a // session-only subscription will not receive them. const sessionChangesetUri = `${sessionUri}/changeset/session`; - await client.call('subscribe', { resource: sessionChangesetUri }); + await client.call('subscribe', { channel: sessionChangesetUri }); client.clearReceived(); // Fire a turn that runs the `terminal-edit:` mock prompt. The mock diff --git a/src/vs/platform/agentHost/test/node/protocol/sessionFeatures.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocol/sessionFeatures.integrationTest.ts index f6bd8b9d5be..6ae9b010c6e 100644 --- a/src/vs/platform/agentHost/test/node/protocol/sessionFeatures.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/sessionFeatures.integrationTest.ts @@ -6,10 +6,10 @@ import assert from 'assert'; import { timeout } from '../../../../../base/common/async.js'; import { SubscribeResult } from '../../../common/state/protocol/commands.js'; -import type { IModelChangedAction, IResponsePartAction, SessionAddedNotification, ITitleChangedAction } from '../../../common/state/sessionActions.js'; +import type { IModelChangedAction, IResponsePartAction, SessionAddedParams, ITitleChangedAction } from '../../../common/state/sessionActions.js'; import { PROTOCOL_VERSION } from '../../../common/state/protocol/version/registry.js'; -import type { ListSessionsResult, INotificationBroadcastParams } from '../../../common/state/sessionProtocol.js'; -import { PendingMessageKind, ResponsePartKind, type SessionState } from '../../../common/state/sessionState.js'; +import type { ListSessionsResult } from '../../../common/state/sessionProtocol.js'; +import { PendingMessageKind, ResponsePartKind, ROOT_STATE_URI, type SessionState } from '../../../common/state/sessionState.js'; import { MOCK_AUTO_TITLE } from '../mockAgent.js'; import { createAndSubscribeSession, @@ -54,10 +54,10 @@ suite('Protocol WebSocket — Session Features', function () { const sessionUri = await createAndSubscribeSession(client, 'test-titleChanged'); client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 1, action: { type: 'session/titleChanged', - session: sessionUri, title: 'My Custom Title', }, }); @@ -66,8 +66,8 @@ suite('Protocol WebSocket — Session Features', function () { const titleAction = getActionEnvelope(titleNotif).action as ITitleChangedAction; assert.strictEqual(titleAction.title, 'My Custom Title'); - const snapshot = await client.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; assert.strictEqual(state.summary.title, 'My Custom Title'); }); @@ -91,8 +91,8 @@ suite('Protocol WebSocket — Session Features', function () { await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete')); - const snapshot = await client.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; assert.strictEqual(state.summary.title, MOCK_AUTO_TITLE); }); @@ -102,8 +102,8 @@ suite('Protocol WebSocket — Session Features', function () { const sessionUri = await createAndSubscribeSession(client, 'test-immediate-title'); // Verify the session starts with the default placeholder title - const before = await client.call('subscribe', { resource: sessionUri }); - assert.strictEqual((before.snapshot.state as SessionState).summary.title, ''); + const before = await client.call('subscribe', { channel: sessionUri }); + assert.strictEqual((before.snapshot!.state as SessionState).summary.title, ''); // Send first turn — side effects should dispatch an immediate titleChanged // with the user's message text before the agent produces its own title. @@ -115,7 +115,7 @@ suite('Protocol WebSocket — Session Features', function () { assert.strictEqual(titleAction.title, 'Fix the login bug'); // listSessions should also reflect the updated title - const result = await client.call('listSessions'); + const result = await client.call('listSessions', { channel: ROOT_STATE_URI }); const session = result.items.find(s => s.resource === sessionUri); assert.ok(session, 'session should appear in listSessions'); assert.strictEqual(session.title, 'Fix the login bug'); @@ -127,10 +127,10 @@ suite('Protocol WebSocket — Session Features', function () { const sessionUri = await createAndSubscribeSession(client, 'test-title-list'); client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 1, action: { type: 'session/titleChanged', - session: sessionUri, title: 'Persisted Title', }, }); @@ -140,7 +140,7 @@ suite('Protocol WebSocket — Session Features', function () { // Poll listSessions until the persisted title appears (async DB write) let session: { title: string } | undefined; for (let i = 0; i < 20; i++) { - const result = await client.call('listSessions'); + const result = await client.call('listSessions', { channel: ROOT_STATE_URI }); session = result.items.find(s => s.resource === sessionUri); if (session?.title === 'Persisted Title') { break; @@ -156,30 +156,30 @@ suite('Protocol WebSocket — Session Features', function () { test('session model flows through create, subscribe, listSessions, and modelChanged', async function () { this.timeout(10_000); - await client.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-model-summary' }); + await client.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-model-summary' }); const sessionUri = nextSessionUri(); - await client.call('createSession', { session: sessionUri, provider: 'mock', model: { id: 'mock-model' } }); + await client.call('createSession', { channel: sessionUri, provider: 'mock', model: { id: 'mock-model' } }); const addedNotif = await client.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); - const addedSession = (addedNotif.params as INotificationBroadcastParams).notification as SessionAddedNotification; + const addedSession = addedNotif.params as SessionAddedParams; assert.deepStrictEqual(addedSession.summary.model, { id: 'mock-model' }); const createdSessionUri = addedSession.summary.resource; - const initialSnapshot = await client.call('subscribe', { resource: createdSessionUri }); - const initialState = initialSnapshot.snapshot.state as SessionState; + const initialSnapshot = await client.call('subscribe', { channel: createdSessionUri }); + const initialState = initialSnapshot.snapshot!.state as SessionState; assert.deepStrictEqual(initialState.summary.model, { id: 'mock-model' }); - const initialList = await client.call('listSessions'); + const initialList = await client.call('listSessions', { channel: ROOT_STATE_URI }); assert.deepStrictEqual(initialList.items.find(s => s.resource === createdSessionUri)?.model, { id: 'mock-model' }); client.notify('dispatchAction', { + channel: createdSessionUri, clientSeq: 1, action: { type: 'session/modelChanged', - session: createdSessionUri, model: { id: 'mock-model-2' }, }, }); @@ -188,11 +188,11 @@ suite('Protocol WebSocket — Session Features', function () { const modelAction = getActionEnvelope(modelNotif).action as IModelChangedAction; assert.deepStrictEqual(modelAction.model, { id: 'mock-model-2' }); - const updatedSnapshot = await client.call('subscribe', { resource: createdSessionUri }); - const updatedState = updatedSnapshot.snapshot.state as SessionState; + const updatedSnapshot = await client.call('subscribe', { channel: createdSessionUri }); + const updatedState = updatedSnapshot.snapshot!.state as SessionState; assert.deepStrictEqual(updatedState.summary.model, { id: 'mock-model-2' }); - const updatedList = await client.call('listSessions'); + const updatedList = await client.call('listSessions', { channel: ROOT_STATE_URI }); assert.deepStrictEqual(updatedList.items.find(s => s.resource === createdSessionUri)?.model, { id: 'mock-model-2' }); }); @@ -246,10 +246,10 @@ suite('Protocol WebSocket — Session Features', function () { // Queue a message when the session is idle — server should immediately consume it client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 1, action: { type: 'session/pendingMessageSet', - session: sessionUri, kind: PendingMessageKind.Queued, id: 'q-1', userMessage: { text: 'hello' }, @@ -262,8 +262,8 @@ suite('Protocol WebSocket — Session Features', function () { await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete')); // Verify the turn was created from the queued message - const snapshot = await client.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; assert.ok(state.turns.length >= 1); assert.strictEqual(state.turns[state.turns.length - 1].userMessage.text, 'hello'); // Queue should be empty after consumption @@ -283,10 +283,10 @@ suite('Protocol WebSocket — Session Features', function () { // Queue a message while the turn is in progress client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 2, action: { type: 'session/pendingMessageSet', - session: sessionUri, kind: PendingMessageKind.Queued, id: 'q-wait-1', userMessage: { text: 'hello' }, @@ -313,8 +313,8 @@ suite('Protocol WebSocket — Session Features', function () { }); assert.ok(secondComplete, 'should receive a second turnComplete from the queued message'); - const snapshot = await client.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; assert.ok(state.turns.length >= 2, `expected >= 2 turns but got ${state.turns.length}`); }); @@ -330,10 +330,10 @@ suite('Protocol WebSocket — Session Features', function () { // Set a steering message while the turn is in progress client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 2, action: { type: 'session/pendingMessageSet', - session: sessionUri, kind: PendingMessageKind.Steering, id: 'steer-1', userMessage: { text: 'Please be concise' }, @@ -352,8 +352,8 @@ suite('Protocol WebSocket — Session Features', function () { await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete')); // Steering should be cleared from state - const snapshot = await client.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; assert.ok(!state.steeringMessage, 'steering message should be cleared after consumption'); }); @@ -373,22 +373,23 @@ suite('Protocol WebSocket — Session Features', function () { await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete') && (getActionEnvelope(n).action as { turnId: string }).turnId === 'turn-t2'); // Verify 2 turns exist - let snapshot = await client.call('subscribe', { resource: sessionUri }); - let state = snapshot.snapshot.state as SessionState; + let snapshot = await client.call('subscribe', { channel: sessionUri }); + let state = snapshot.snapshot!.state as SessionState; assert.strictEqual(state.turns.length, 2); client.clearReceived(); // Truncate: keep only turn-t1 client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 3, - action: { type: 'session/truncated', session: sessionUri, turnId: 'turn-t1' }, + action: { type: 'session/truncated', turnId: 'turn-t1' }, }); await client.waitForNotification(n => isActionNotification(n, 'session/truncated')); - snapshot = await client.call('subscribe', { resource: sessionUri }); - state = snapshot.snapshot.state as SessionState; + snapshot = await client.call('subscribe', { channel: sessionUri }); + state = snapshot.snapshot!.state as SessionState; assert.strictEqual(state.turns.length, 1); assert.strictEqual(state.turns[0].id, 'turn-t1'); }); @@ -405,14 +406,15 @@ suite('Protocol WebSocket — Session Features', function () { // Truncate all (no turnId) client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 2, - action: { type: 'session/truncated', session: sessionUri }, + action: { type: 'session/truncated' }, }); await client.waitForNotification(n => isActionNotification(n, 'session/truncated')); - const snapshot = await client.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; assert.strictEqual(state.turns.length, 0); }); @@ -432,8 +434,9 @@ suite('Protocol WebSocket — Session Features', function () { // Truncate to turn-tr1 client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 3, - action: { type: 'session/truncated', session: sessionUri, turnId: 'turn-tr1' }, + action: { type: 'session/truncated', turnId: 'turn-tr1' }, }); await client.waitForNotification(n => isActionNotification(n, 'session/truncated')); @@ -442,8 +445,8 @@ suite('Protocol WebSocket — Session Features', function () { dispatchTurnStarted(client, sessionUri, 'turn-tr3', 'hello', 4); await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete')); - const snapshot = await client.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; assert.strictEqual(state.turns.length, 2); assert.strictEqual(state.turns[0].id, 'turn-tr1'); assert.strictEqual(state.turns[1].id, 'turn-tr3'); @@ -469,25 +472,25 @@ suite('Protocol WebSocket — Session Features', function () { // Fork at turn-f1 (keep turns up to and including turn-f1) const forkedSessionUri = nextSessionUri(); await client.call('createSession', { - session: forkedSessionUri, + channel: forkedSessionUri, provider: 'mock', fork: { session: sessionUri, turnId: 'turn-f1' }, }); const addedNotif = await client.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); - const addedSession = (addedNotif.params as INotificationBroadcastParams).notification as SessionAddedNotification; + const addedSession = addedNotif.params as SessionAddedParams; // Subscribe — forked session should have 1 turn - const snapshot = await client.call('subscribe', { resource: addedSession.summary.resource }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: addedSession.summary.resource }); + const state = snapshot.snapshot!.state as SessionState; assert.strictEqual(state.lifecycle, 'ready'); assert.strictEqual(state.turns.length, 1, 'forked session should have 1 turn'); // Source session should be unaffected - const sourceSnapshot = await client.call('subscribe', { resource: sessionUri }); - const sourceState = sourceSnapshot.snapshot.state as SessionState; + const sourceSnapshot = await client.call('subscribe', { channel: sessionUri }); + const sourceState = sourceSnapshot.snapshot!.state as SessionState; assert.strictEqual(sourceState.turns.length, 2); }); @@ -499,7 +502,7 @@ suite('Protocol WebSocket — Session Features', function () { let gotError = false; try { await client.call('createSession', { - session: nextSessionUri(), + channel: nextSessionUri(), provider: 'mock', fork: { session: sessionUri, turnId: 'nonexistent-turn' }, }); @@ -512,12 +515,12 @@ suite('Protocol WebSocket — Session Features', function () { test('fork with invalid source session returns error', async function () { this.timeout(10_000); - await client.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-fork-no-source' }); + await client.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-fork-no-source' }); let gotError = false; try { await client.call('createSession', { - session: nextSessionUri(), + channel: nextSessionUri(), provider: 'mock', fork: { session: 'mock://nonexistent-session', turnId: 'turn-1' }, }); diff --git a/src/vs/platform/agentHost/test/node/protocol/sessionLifecycle.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocol/sessionLifecycle.integrationTest.ts index c84c6ec015e..b2f634c956c 100644 --- a/src/vs/platform/agentHost/test/node/protocol/sessionLifecycle.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/sessionLifecycle.integrationTest.ts @@ -7,10 +7,10 @@ import assert from 'assert'; import { timeout } from '../../../../../base/common/async.js'; import { URI } from '../../../../../base/common/uri.js'; import { SubscribeResult } from '../../../common/state/protocol/commands.js'; -import type { SessionAddedNotification, SessionRemovedNotification } from '../../../common/state/sessionActions.js'; +import type { SessionAddedParams, SessionRemovedParams } from '../../../common/state/protocol/notifications.js'; import { PROTOCOL_VERSION } from '../../../common/state/protocol/version/registry.js'; -import type { ListSessionsResult, INotificationBroadcastParams } from '../../../common/state/sessionProtocol.js'; -import { ResponsePartKind, SessionStatus, type MarkdownResponsePart, type SessionState, type ToolCallResponsePart } from '../../../common/state/sessionState.js'; +import type { ListSessionsResult } from '../../../common/state/sessionProtocol.js'; +import { ResponsePartKind, ROOT_STATE_URI, SessionStatus, type MarkdownResponsePart, type SessionState, type ToolCallResponsePart } from '../../../common/state/sessionState.js'; import { PRE_EXISTING_SESSION_URI } from '../mockAgent.js'; import { createAndSubscribeSession, @@ -48,14 +48,14 @@ suite('Protocol WebSocket — Session Lifecycle', function () { test('create session triggers sessionAdded notification', async function () { this.timeout(10_000); - await client.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-create-session' }); + await client.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-create-session' }); - await client.call('createSession', { session: nextSessionUri(), provider: 'mock' }); + await client.call('createSession', { channel: nextSessionUri(), provider: 'mock' }); const notif = await client.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); - const notification = (notif.params as INotificationBroadcastParams).notification as SessionAddedNotification; + const notification = notif.params as SessionAddedParams; assert.strictEqual(URI.parse(notification.summary.resource).scheme, 'mock'); assert.strictEqual(notification.summary.provider, 'mock'); }); @@ -63,14 +63,14 @@ suite('Protocol WebSocket — Session Lifecycle', function () { test('listSessions returns sessions', async function () { this.timeout(10_000); - await client.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-list-sessions' }); + await client.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-list-sessions' }); - await client.call('createSession', { session: nextSessionUri(), provider: 'mock' }); + await client.call('createSession', { channel: nextSessionUri(), provider: 'mock' }); await client.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); - const result = await client.call('listSessions'); + const result = await client.call('listSessions', { channel: ROOT_STATE_URI }); assert.ok(Array.isArray(result.items)); assert.ok(result.items.length >= 1, 'should have at least one session'); }); @@ -79,25 +79,25 @@ suite('Protocol WebSocket — Session Lifecycle', function () { this.timeout(10_000); const sessionUri = await createAndSubscribeSession(client, 'test-dispose'); - await client.call('disposeSession', { session: sessionUri }); + await client.call('disposeSession', { channel: sessionUri }); const notif = await client.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionRemoved' + n.method === 'root/sessionRemoved' ); - const removed = (notif.params as INotificationBroadcastParams).notification as SessionRemovedNotification; + const removed = notif.params as SessionRemovedParams; assert.strictEqual(removed.session.toString(), sessionUri.toString()); }); test('subscribe to a pre-existing session restores turns from agent history', async function () { this.timeout(10_000); - await client.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-restore' }); + await client.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-restore' }); // The mock agent seeds a pre-existing session that was never created // through the server's handleCreateSession -- simulating a session // from a previous server lifetime. const preExistingUri = PRE_EXISTING_SESSION_URI.toString(); - const list = await client.call('listSessions'); + const list = await client.call('listSessions', { channel: ROOT_STATE_URI }); const preExisting = list.items.find(s => s.resource === preExistingUri); assert.ok(preExisting, 'listSessions should include the pre-existing session'); @@ -106,8 +106,8 @@ suite('Protocol WebSocket — Session Lifecycle', function () { // Subscribing to this session should trigger the restore path: the // server fetches message history from the agent and reconstructs turns. - const result = await client.call('subscribe', { resource: preExistingUri }); - const state = result.snapshot.state as SessionState; + const result = await client.call('subscribe', { channel: preExistingUri }); + const state = result.snapshot!.state as SessionState; assert.strictEqual(state.lifecycle, 'ready', 'restored session should be in ready state'); assert.ok(state.turns.length >= 1, `expected at least 1 restored turn but got ${state.turns.length}`); @@ -125,7 +125,7 @@ suite('Protocol WebSocket — Session Lifecycle', function () { // (the session is already known to clients via listSessions). await new Promise(resolve => setTimeout(resolve, 200)); const sessionAddedNotifs = client.receivedNotifications(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); assert.strictEqual(sessionAddedNotifs.length, 0, 'restore should not emit sessionAdded'); }); @@ -137,10 +137,10 @@ suite('Protocol WebSocket — Session Lifecycle', function () { // Dispatch isArchived=true client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 1, action: { type: 'session/isArchivedChanged', - session: sessionUri, isArchived: true, }, }); @@ -149,10 +149,10 @@ suite('Protocol WebSocket — Session Lifecycle', function () { // Dispatch isRead=true client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 2, action: { type: 'session/isReadChanged', - session: sessionUri, isRead: true, }, }); @@ -160,8 +160,8 @@ suite('Protocol WebSocket — Session Lifecycle', function () { await client.waitForNotification(n => isActionNotification(n, 'session/isReadChanged')); // Verify the flags are reflected in the subscribed session state - const snapshot = await client.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; assert.ok(state.summary.status & SessionStatus.IsArchived, 'IsArchived flag should be set in snapshot'); assert.ok(state.summary.status & SessionStatus.IsRead, 'IsRead flag should be set in snapshot'); @@ -169,11 +169,11 @@ suite('Protocol WebSocket — Session Lifecycle', function () { client.close(); const client2 = new TestProtocolClient(server.port); await client2.connect(); - await client2.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-read-archived-flags-2' }); + await client2.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-read-archived-flags-2' }); let session: ListSessionsResult['items'][0] | undefined; for (let i = 0; i < 20; i++) { - const result = await client2.call('listSessions'); + const result = await client2.call('listSessions', { channel: ROOT_STATE_URI }); session = result.items.find(s => s.resource === sessionUri); if (session && (session.status & SessionStatus.IsArchived) && (session.status & SessionStatus.IsRead)) { break; @@ -196,10 +196,10 @@ suite('Protocol WebSocket — Session Lifecycle', function () { // isRead=false should persist the value so that listSessions // returns an explicit `false` rather than omitting the field. client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 1, action: { type: 'session/isReadChanged', - session: sessionUri, isRead: false, }, }); @@ -209,11 +209,11 @@ suite('Protocol WebSocket — Session Lifecycle', function () { client.close(); const client2 = new TestProtocolClient(server.port); await client2.connect(); - await client2.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId: 'test-isread-false-2' }); + await client2.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId: 'test-isread-false-2' }); let session: ListSessionsResult['items'][0] | undefined; for (let i = 0; i < 20; i++) { - const result = await client2.call('listSessions'); + const result = await client2.call('listSessions', { channel: ROOT_STATE_URI }); session = result.items.find(s => s.resource === sessionUri); if (session && !(session.status & SessionStatus.IsRead)) { break; diff --git a/src/vs/platform/agentHost/test/node/protocol/testHelpers.ts b/src/vs/platform/agentHost/test/node/protocol/testHelpers.ts index 8aa93d6d9dc..982c9d67155 100644 --- a/src/vs/platform/agentHost/test/node/protocol/testHelpers.ts +++ b/src/vs/platform/agentHost/test/node/protocol/testHelpers.ts @@ -8,7 +8,8 @@ import { fileURLToPath } from 'url'; import { WebSocket } from 'ws'; import { URI } from '../../../../../base/common/uri.js'; import { SubscribeResult } from '../../../common/state/protocol/commands.js'; -import type { ActionEnvelope, SessionAddedNotification } from '../../../common/state/sessionActions.js'; +import type { ActionEnvelope } from '../../../common/state/sessionActions.js'; +import type { SessionAddedParams } from '../../../common/state/protocol/notifications.js'; import { PROTOCOL_VERSION } from '../../../common/state/protocol/version/registry.js'; import { isJsonRpcNotification, @@ -16,7 +17,6 @@ import { type AhpNotification, type JsonRpcErrorResponse, type JsonRpcSuccessResponse, - type INotificationBroadcastParams, type ProtocolMessage, } from '../../../common/state/sessionProtocol.js'; @@ -291,16 +291,16 @@ export function getActionEnvelope(n: AhpNotification): ActionEnvelope { /** Perform handshake, create a session, subscribe, and return its URI. */ export async function createAndSubscribeSession(c: TestProtocolClient, clientId: string, workingDirectory?: string): Promise { - await c.call('initialize', { protocolVersions: [PROTOCOL_VERSION], clientId }); + await c.call('initialize', { channel: 'ahp-root://', protocolVersions: [PROTOCOL_VERSION], clientId }); - await c.call('createSession', { session: nextSessionUri(), provider: 'mock', workingDirectory }); + await c.call('createSession', { channel: nextSessionUri(), provider: 'mock', workingDirectory }); const notif = await c.waitForNotification(n => - n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded' + n.method === 'root/sessionAdded' ); - const realSessionUri = ((notif.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource; + const realSessionUri = (notif.params as SessionAddedParams).summary.resource; - await c.call('subscribe', { resource: realSessionUri }); + await c.call('subscribe', { channel: realSessionUri }); c.clearReceived(); return realSessionUri; @@ -308,10 +308,10 @@ export async function createAndSubscribeSession(c: TestProtocolClient, clientId: export function dispatchTurnStarted(c: TestProtocolClient, session: string, turnId: string, text: string, clientSeq: number): void { c.notify('dispatchAction', { + channel: session, clientSeq, action: { type: 'session/turnStarted', - session, turnId, userMessage: { text }, }, diff --git a/src/vs/platform/agentHost/test/node/protocol/toolApproval.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocol/toolApproval.integrationTest.ts index 13a2bc6cf49..f8840f24dcc 100644 --- a/src/vs/platform/agentHost/test/node/protocol/toolApproval.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/toolApproval.integrationTest.ts @@ -55,9 +55,9 @@ suite('Protocol WebSocket — Permissions & Auto-Approve', function () { // Confirm the tool call client.notify('dispatchAction', { clientSeq: 2, + channel: sessionUri, action: { type: 'session/toolCallConfirmed', - session: sessionUri, turnId: 'turn-perm', toolCallId: 'tc-perm-1', approved: true, @@ -116,9 +116,9 @@ suite('Protocol WebSocket — Permissions & Auto-Approve', function () { // Confirm it manually to let the turn complete client.notify('dispatchAction', { clientSeq: 2, + channel: sessionUri, action: { type: 'session/toolCallConfirmed', - session: sessionUri, turnId: 'turn-deny', toolCallId: 'tc-write-env-1', approved: true, @@ -173,9 +173,9 @@ suite('Protocol WebSocket — Permissions & Auto-Approve', function () { // Confirm it manually to let the turn complete client.notify('dispatchAction', { clientSeq: 2, + channel: sessionUri, action: { type: 'session/toolCallConfirmed', - session: sessionUri, turnId: 'turn-shell-deny', toolCallId: 'tc-shell-deny-1', approved: true, diff --git a/src/vs/platform/agentHost/test/node/protocol/turnExecution.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocol/turnExecution.integrationTest.ts index b94d94bf413..bfa29b125a4 100644 --- a/src/vs/platform/agentHost/test/node/protocol/turnExecution.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/turnExecution.integrationTest.ts @@ -93,14 +93,15 @@ suite('Protocol WebSocket — Turn Execution', function () { dispatchTurnStarted(client, sessionUri, 'turn-cancel', 'slow', 1); client.notify('dispatchAction', { + channel: sessionUri, clientSeq: 2, - action: { type: 'session/turnCancelled', session: sessionUri, turnId: 'turn-cancel' }, + action: { type: 'session/turnCancelled', turnId: 'turn-cancel' }, }); await client.waitForNotification(n => isActionNotification(n, 'session/turnCancelled')); - const snapshot = await client.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; assert.ok(state.turns.length >= 1); assert.strictEqual(state.turns[state.turns.length - 1].state, 'cancelled'); }); @@ -117,8 +118,8 @@ suite('Protocol WebSocket — Turn Execution', function () { await new Promise(resolve => setTimeout(resolve, 200)); await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete')); - const snapshot = await client.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; assert.ok(state.turns.length >= 2, `expected >= 2 turns but got ${state.turns.length}`); assert.strictEqual(state.turns[0].id, 'turn-m1'); assert.strictEqual(state.turns[1].id, 'turn-m2'); @@ -136,7 +137,7 @@ suite('Protocol WebSocket — Turn Execution', function () { await new Promise(resolve => setTimeout(resolve, 200)); await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete')); - const result = await client.call('fetchTurns', { session: sessionUri, limit: 10 }); + const result = await client.call('fetchTurns', { channel: sessionUri, limit: 10 }); assert.ok(result.turns.length >= 2); assert.strictEqual(typeof result.hasMore, 'boolean'); }); @@ -155,8 +156,8 @@ suite('Protocol WebSocket — Turn Execution', function () { await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete')); - const snapshot = await client.call('subscribe', { resource: sessionUri }); - const state = snapshot.snapshot.state as SessionState; + const snapshot = await client.call('subscribe', { channel: sessionUri }); + const state = snapshot.snapshot!.state as SessionState; assert.ok(state.turns.length >= 1); const turn = state.turns[state.turns.length - 1]; assert.ok(turn.usage); @@ -170,16 +171,16 @@ suite('Protocol WebSocket — Turn Execution', function () { const sessionUri = await createAndSubscribeSession(client, 'test-modifiedAt'); - const initialSnapshot = await client.call('subscribe', { resource: sessionUri }); - const initialModifiedAt = (initialSnapshot.snapshot.state as SessionState).summary.modifiedAt; + const initialSnapshot = await client.call('subscribe', { channel: sessionUri }); + const initialModifiedAt = (initialSnapshot.snapshot!.state as SessionState).summary.modifiedAt; await new Promise(resolve => setTimeout(resolve, 50)); dispatchTurnStarted(client, sessionUri, 'turn-mod', 'hello', 1); await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete')); - const updatedSnapshot = await client.call('subscribe', { resource: sessionUri }); - const updatedModifiedAt = (updatedSnapshot.snapshot.state as SessionState).summary.modifiedAt; + const updatedSnapshot = await client.call('subscribe', { channel: sessionUri }); + const updatedModifiedAt = (updatedSnapshot.snapshot!.state as SessionState).summary.modifiedAt; assert.ok(updatedModifiedAt >= initialModifiedAt); }); @@ -196,10 +197,10 @@ suite('Protocol WebSocket — Turn Execution', function () { // the parent session URI + parent toolCallId. const childUri = buildSubagentSessionUri(sessionUri, 'tc-task-1'); - const parentSnapshot = await client.call('subscribe', { resource: sessionUri }); - const parentState = parentSnapshot.snapshot.state as SessionState; - const childSnapshot = await client.call('subscribe', { resource: childUri }); - const childState = childSnapshot.snapshot.state as SessionState; + const parentSnapshot = await client.call('subscribe', { channel: sessionUri }); + const parentState = parentSnapshot.snapshot!.state as SessionState; + const childSnapshot = await client.call('subscribe', { channel: childUri }); + const childState = childSnapshot.snapshot!.state as SessionState; // Parent turn should contain the `task` tool call but NOT the inner one. const parentTurn = parentState.turns[parentState.turns.length - 1]; diff --git a/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts b/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts index 9bd68256d41..6871cd10ca2 100644 --- a/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts +++ b/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts @@ -16,6 +16,7 @@ import { ActionType, type IRootConfigChangedAction, type SessionAction, type Ter import { PROTOCOL_VERSION } from '../../common/state/protocol/version/registry.js'; import { isJsonRpcNotification, isJsonRpcResponse, JSON_RPC_INTERNAL_ERROR, ProtocolError, AHP_UNSUPPORTED_PROTOCOL_VERSION, type AhpNotification, type InitializeResult, type ProtocolMessage, type ReconnectResult, type ResourceListResult, type ResourceWriteParams, type ResourceWriteResult, type IStateSnapshot } from '../../common/state/sessionProtocol.js'; import { ResponsePartKind, SessionStatus, ChangesetStatus, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, type SessionSummary } from '../../common/state/sessionState.js'; +import type { SessionAddedParams } from '../../common/state/protocol/notifications.js'; import type { IProtocolServer, IProtocolTransport } from '../../common/state/sessionTransport.js'; import { ProtocolServerHandler } from '../../node/protocolServerHandler.js'; import { AgentHostStateManager } from '../../node/agentHostStateManager.js'; @@ -87,10 +88,10 @@ class MockAgentService implements IAgentService { this._stateManager = sm; } - dispatchAction(action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { + dispatchAction(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { this.handledActions.push(action); const origin = { clientId, clientSeq }; - this._stateManager.dispatchClientAction(action, origin); + this._stateManager.dispatchClientAction(channel, action, origin); } async createSession(config?: IAgentCreateSessionConfig): Promise { this.createSessionConfigs.push(config); @@ -373,7 +374,7 @@ suite('ProtocolServerHandler', () => { transport.sent.length = 0; const responsePromise = waitForResponse(transport, 1); - transport.simulateMessage(request(1, 'subscribe', { resource: sessionUri })); + transport.simulateMessage(request(1, 'subscribe', { channel: sessionUri })); const resp = await responsePromise; assert.ok(resp, 'should have sent response'); @@ -383,16 +384,16 @@ suite('ProtocolServerHandler', () => { test('client action is dispatched and echoed', () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); const transport = connectClient('client-1', [sessionUri]); transport.sent.length = 0; transport.simulateMessage(notification('dispatchAction', { + channel: sessionUri, clientSeq: 1, action: { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'hello' }, }, @@ -411,7 +412,7 @@ suite('ProtocolServerHandler', () => { test('actions are scoped to subscribed sessions', () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); const transportA = connectClient('client-a', [sessionUri]); const transportB = connectClient('client-b'); @@ -419,9 +420,8 @@ suite('ProtocolServerHandler', () => { transportA.sent.length = 0; transportB.sent.length = 0; - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionTitleChanged, - session: sessionUri, title: 'New Title', }); @@ -432,7 +432,7 @@ suite('ProtocolServerHandler', () => { test('changeset actions are scoped to subscribed changeset URIs', () => { const changesetUri = `${sessionUri}/changeset/session`; stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); stateManager.registerChangeset(changesetUri); const transportA = connectClient('client-a-cs', [changesetUri]); @@ -442,15 +442,14 @@ suite('ProtocolServerHandler', () => { transportA.sent.length = 0; transportB.sent.length = 0; - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetFileSet, - changeset: changesetUri, file: { id: 'file:///test/changed.ts', edit: { after: { uri: 'file:///test/changed.ts', content: { uri: 'file:///test/changed.ts' } }, - diff: { added: 1, removed: 0 }, - }, + diff: { added: 1, removed: 0 } + } }, }); @@ -459,25 +458,24 @@ suite('ProtocolServerHandler', () => { assert.strictEqual(aActions.length, 1, 'changeset subscriber should receive 1 envelope'); assert.strictEqual(bActions.length, 0, 'session-only subscriber should receive 0 changeset envelopes'); - const params = aActions[0].params as { action: { type: string; changeset: string } }; + const params = aActions[0].params as { channel: string; action: { type: string } }; assert.deepStrictEqual( - { type: params.action.type, changeset: params.action.changeset }, - { type: ActionType.ChangesetFileSet, changeset: changesetUri }, + { type: params.action.type, channel: params.channel }, + { type: ActionType.ChangesetFileSet, channel: changesetUri }, ); }); test('changeset/cleared reaches changeset subscribers', () => { const changesetUri = `${sessionUri}/changeset/session`; stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); stateManager.registerChangeset(changesetUri); const transport = connectClient('client-clear', [changesetUri]); transport.sent.length = 0; - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetCleared, - changeset: changesetUri, }); const actions = findNotifications(transport.sent, 'action'); @@ -495,8 +493,8 @@ suite('ProtocolServerHandler', () => { stateManager.createSession(makeSessionSummary()); - assert.strictEqual(findNotifications(transportA.sent, 'notification').length, 1); - assert.strictEqual(findNotifications(transportB.sent, 'notification').length, 1); + assert.strictEqual(findNotifications(transportA.sent, 'root/sessionAdded').length, 1); + assert.strictEqual(findNotifications(transportB.sent, 'root/sessionAdded').length, 1); }); test('listSessions includes project metadata', async () => { @@ -580,16 +578,13 @@ suite('ProtocolServerHandler', () => { const responsePromise = waitForResponse(transport, 2); const newSession = URI.parse('copilot:///created-session').toString(); - transport.simulateMessage(request(2, 'createSession', { session: newSession })); + transport.simulateMessage(request(2, 'createSession', { channel: newSession })); const resp = await responsePromise; - const added = findNotifications(transport.sent, 'notification').find(message => { - const params = message.params as { notification: { type: string } }; - return params.notification.type === 'notify/sessionAdded'; - }); + const added = findNotifications(transport.sent, 'root/sessionAdded')[0]; assert.deepStrictEqual({ result: (resp as { result: null }).result, - project: (added!.params as { notification: { summary: SessionSummary } }).notification.summary.project, + project: (added!.params as SessionAddedParams).summary.project, }, { result: null, project: { uri: 'file:///created-project', displayName: 'Created Project' }, @@ -598,15 +593,15 @@ suite('ProtocolServerHandler', () => { test('reconnect replays missed actions', async () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); const transport1 = connectClient('client-r', [sessionUri]); const resp = findResponse(transport1.sent, 1); const initSeq = (resp as { result: InitializeResult }).result.serverSeq; transport1.simulateClose(); - stateManager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Title A' }); - stateManager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Title B' }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionTitleChanged, title: 'Title A' }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionTitleChanged, title: 'Title B' }); const transport2 = new MockProtocolTransport(); server.simulateConnection(transport2); @@ -628,7 +623,7 @@ suite('ProtocolServerHandler', () => { test('reconnect replays missed changeset actions to changeset subscribers', async () => { const changesetUri = `${sessionUri}/changeset/session`; stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); // Register the changeset before the first connection so the initial // subscription succeeds. stateManager.registerChangeset(changesetUri); @@ -639,20 +634,18 @@ suite('ProtocolServerHandler', () => { transport1.simulateClose(); // Dispatch two changeset actions while client is disconnected. - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetFileSet, - changeset: changesetUri, file: { id: 'file:///a.ts', edit: { after: { uri: 'file:///a.ts', content: { uri: 'file:///a.ts' } }, - diff: { added: 2, removed: 0 }, - }, + diff: { added: 2, removed: 0 } + } }, }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetStatusChanged, - changeset: changesetUri, status: ChangesetStatus.Ready, }); @@ -678,13 +671,13 @@ suite('ProtocolServerHandler', () => { test('reconnect sends fresh snapshots when gap too large', async () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); const transport1 = connectClient('client-g', [sessionUri]); transport1.simulateClose(); for (let i = 0; i < 1100; i++) { - stateManager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: `Title ${i}` }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionTitleChanged, title: `Title ${i}` }); } const transport2 = new MockProtocolTransport(); @@ -706,7 +699,7 @@ suite('ProtocolServerHandler', () => { test('reconnect rehydrates server-side state that was evicted while disconnected', async () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); // MockAgentService.subscribe normally just returns the existing snapshot. // Override it so a missing session is restored on subscribe — this is the @@ -749,14 +742,14 @@ suite('ProtocolServerHandler', () => { test('client disconnect cleans up', () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); const transport = connectClient('client-d', [sessionUri]); transport.sent.length = 0; transport.simulateClose(); - stateManager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'After Disconnect' }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionTitleChanged, title: 'After Disconnect' }); assert.strictEqual(transport.sent.length, 0); }); @@ -764,33 +757,29 @@ suite('ProtocolServerHandler', () => { test('client disconnect clears active client and fails owned tool calls after grace period', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionActiveClientChanged, - session: sessionUri, activeClient: { clientId: 'client-tools', - tools: [{ name: 'runTask', description: 'Runs a task' }], + tools: [{ name: 'runTask', description: 'Runs a task' }] }, }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'run it' }, }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallStart, - session: sessionUri, turnId: 'turn-1', toolCallId: 'tool-1', toolName: 'runTask', displayName: 'Run Task', toolClientId: 'client-tools', }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallReady, - session: sessionUri, turnId: 'turn-1', toolCallId: 'tool-1', invocationMessage: 'Run Task', @@ -825,24 +814,21 @@ suite('ProtocolServerHandler', () => { test('client disconnect fails owned streaming tool calls after grace period', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionActiveClientChanged, - session: sessionUri, activeClient: { clientId: 'client-tools', - tools: [{ name: 'runTask', description: 'Runs a task' }], + tools: [{ name: 'runTask', description: 'Runs a task' }] }, }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'run it' }, }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallStart, - session: sessionUri, turnId: 'turn-1', toolCallId: 'tool-1', toolName: 'runTask', @@ -876,33 +862,29 @@ suite('ProtocolServerHandler', () => { test('client reconnect without session subscription does not clear tool call disconnect timeout', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionActiveClientChanged, - session: sessionUri, activeClient: { clientId: 'client-tools', - tools: [{ name: 'runTask', description: 'Runs a task' }], + tools: [{ name: 'runTask', description: 'Runs a task' }] }, }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'run it' }, }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallStart, - session: sessionUri, turnId: 'turn-1', toolCallId: 'tool-1', toolName: 'runTask', displayName: 'Run Task', toolClientId: 'client-tools', }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallReady, - session: sessionUri, turnId: 'turn-1', toolCallId: 'tool-1', invocationMessage: 'Run Task', @@ -938,33 +920,29 @@ suite('ProtocolServerHandler', () => { test('client reconnect with session subscription clears tool call disconnect timeout for that session', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionActiveClientChanged, - session: sessionUri, activeClient: { clientId: 'client-tools', - tools: [{ name: 'runTask', description: 'Runs a task' }], + tools: [{ name: 'runTask', description: 'Runs a task' }] }, }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'run it' }, }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallStart, - session: sessionUri, turnId: 'turn-1', toolCallId: 'tool-1', toolName: 'runTask', displayName: 'Run Task', toolClientId: 'client-tools', }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallReady, - session: sessionUri, turnId: 'turn-1', toolCallId: 'tool-1', invocationMessage: 'Run Task', @@ -994,33 +972,29 @@ suite('ProtocolServerHandler', () => { test('client tool timeout tells model it may retry when replacement active client provides the tool', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady, }); + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionActiveClientChanged, - session: sessionUri, activeClient: { clientId: 'client-tools', - tools: [{ name: 'runTask', description: 'Runs a task' }], + tools: [{ name: 'runTask', description: 'Runs a task' }] }, }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionTurnStarted, - session: sessionUri, turnId: 'turn-1', userMessage: { text: 'run it' }, }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallStart, - session: sessionUri, turnId: 'turn-1', toolCallId: 'tool-1', toolName: 'runTask', displayName: 'Run Task', toolClientId: 'client-tools', }); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallReady, - session: sessionUri, turnId: 'turn-1', toolCallId: 'tool-1', invocationMessage: 'Run Task', @@ -1030,12 +1004,11 @@ suite('ProtocolServerHandler', () => { const transport = connectClient('client-tools', [sessionUri]); transport.simulateClose(); - stateManager.dispatchServerAction({ + stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionActiveClientChanged, - session: sessionUri, activeClient: { clientId: 'client-replacement', - tools: [{ name: 'runTask', description: 'Runs a task' }], + tools: [{ name: 'runTask', description: 'Runs a task' }] }, }); diff --git a/src/vs/platform/agentHost/test/node/reducers.test.ts b/src/vs/platform/agentHost/test/node/reducers.test.ts index a5f7f123d13..27f9a64b522 100644 --- a/src/vs/platform/agentHost/test/node/reducers.test.ts +++ b/src/vs/platform/agentHost/test/node/reducers.test.ts @@ -28,13 +28,11 @@ function makeSession(): SessionState { function withActiveTurnAndToolCall(state: SessionState): SessionState { state = sessionReducer(state, { type: ActionType.SessionTurnStarted, - session: 'copilot:/test', turnId: 'turn-1', userMessage: { text: 'hello' }, }); state = sessionReducer(state, { type: ActionType.SessionToolCallStart, - session: 'copilot:/test', turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'readFile', @@ -53,7 +51,6 @@ suite('sessionReducer – summaryStatus with tool call confirmations and input r // Transition to PendingConfirmation (no `confirmed` field) state = sessionReducer(state, { type: ActionType.SessionToolCallReady, - session: 'copilot:/test', turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Read file?', @@ -69,7 +66,6 @@ suite('sessionReducer – summaryStatus with tool call confirmations and input r // Transition to Running first state = sessionReducer(state, { type: ActionType.SessionToolCallReady, - session: 'copilot:/test', turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Read file', @@ -80,13 +76,12 @@ suite('sessionReducer – summaryStatus with tool call confirmations and input r // Then complete with requiresResultConfirmation state = sessionReducer(state, { type: ActionType.SessionToolCallComplete, - session: 'copilot:/test', turnId: 'turn-1', toolCallId: 'tc-1', requiresResultConfirmation: true, result: { success: true, - pastTenseMessage: 'Read file', + pastTenseMessage: 'Read file' }, }); @@ -99,7 +94,6 @@ suite('sessionReducer – summaryStatus with tool call confirmations and input r // Transition to PendingConfirmation state = sessionReducer(state, { type: ActionType.SessionToolCallReady, - session: 'copilot:/test', turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Read file?', @@ -110,7 +104,6 @@ suite('sessionReducer – summaryStatus with tool call confirmations and input r // Confirm it state = sessionReducer(state, { type: ActionType.SessionToolCallConfirmed, - session: 'copilot:/test', turnId: 'turn-1', toolCallId: 'tc-1', approved: true, @@ -125,7 +118,6 @@ suite('sessionReducer – summaryStatus with tool call confirmations and input r state = sessionReducer(state, { type: ActionType.SessionInputRequested, - session: 'copilot:/test', request: { id: 'req-1', message: 'What is your name?', @@ -133,8 +125,8 @@ suite('sessionReducer – summaryStatus with tool call confirmations and input r kind: SessionInputQuestionKind.Text, id: 'q-1', message: 'What is your name?', - required: true, - }], + required: true + }] }, }); @@ -147,7 +139,6 @@ suite('sessionReducer – summaryStatus with tool call confirmations and input r // Add an input request state = sessionReducer(state, { type: ActionType.SessionInputRequested, - session: 'copilot:/test', request: { id: 'req-1', message: 'What is your name?', @@ -155,8 +146,8 @@ suite('sessionReducer – summaryStatus with tool call confirmations and input r kind: SessionInputQuestionKind.Text, id: 'q-1', message: 'What is your name?', - required: true, - }], + required: true + }] }, }); assert.strictEqual(state.summary.status, SessionStatus.InputNeeded); @@ -164,7 +155,6 @@ suite('sessionReducer – summaryStatus with tool call confirmations and input r // Complete the input request state = sessionReducer(state, { type: ActionType.SessionInputCompleted, - session: 'copilot:/test', requestId: 'req-1', response: SessionInputResponseKind.Accept, answers: { 'q-1': { state: SessionInputAnswerState.Submitted, value: { kind: SessionInputAnswerValueKind.Text, value: 'Alice' } } }, @@ -182,7 +172,6 @@ suite('sessionReducer – summaryStatus with tool call confirmations and input r // Transition to PendingConfirmation via SessionToolCallReady (no confirmed) state = sessionReducer(state, { type: ActionType.SessionToolCallReady, - session: 'copilot:/test', turnId: 'turn-1', toolCallId: 'tc-1', invocationMessage: 'Read file?', @@ -202,60 +191,60 @@ suite('changesetReducer', () => { const fileARenamed = { id: 'file:///a.ts', edit: { after: { uri: 'file:///a.ts', content: { uri: 'file:///a.ts' } }, diff: { added: 5, removed: 0 } } }; test('ChangesetFileSet appends a new file', () => { - const next = changesetReducer(ready, { type: ActionType.ChangesetFileSet, changeset: 'cs', file: fileA }); + const next = changesetReducer(ready, { type: ActionType.ChangesetFileSet, file: fileA }); assert.deepStrictEqual(next.files, [fileA]); }); test('ChangesetFileSet replaces an existing file by id (upsert)', () => { - const seeded = changesetReducer(ready, { type: ActionType.ChangesetFileSet, changeset: 'cs', file: fileA }); - const next = changesetReducer(seeded, { type: ActionType.ChangesetFileSet, changeset: 'cs', file: fileARenamed }); + const seeded = changesetReducer(ready, { type: ActionType.ChangesetFileSet, file: fileA }); + const next = changesetReducer(seeded, { type: ActionType.ChangesetFileSet, file: fileARenamed }); assert.deepStrictEqual(next.files, [fileARenamed]); }); test('ChangesetFileRemoved removes by id', () => { - const seeded = changesetReducer(ready, { type: ActionType.ChangesetFileSet, changeset: 'cs', file: fileA }); - const next = changesetReducer(seeded, { type: ActionType.ChangesetFileRemoved, changeset: 'cs', fileId: fileA.id }); + const seeded = changesetReducer(ready, { type: ActionType.ChangesetFileSet, file: fileA }); + const next = changesetReducer(seeded, { type: ActionType.ChangesetFileRemoved, fileId: fileA.id }); assert.deepStrictEqual(next.files, []); }); test('ChangesetFileRemoved is a no-op for an unknown id', () => { - const seeded = changesetReducer(ready, { type: ActionType.ChangesetFileSet, changeset: 'cs', file: fileA }); - const next = changesetReducer(seeded, { type: ActionType.ChangesetFileRemoved, changeset: 'cs', fileId: 'file:///nope.ts' }); + const seeded = changesetReducer(ready, { type: ActionType.ChangesetFileSet, file: fileA }); + const next = changesetReducer(seeded, { type: ActionType.ChangesetFileRemoved, fileId: 'file:///nope.ts' }); assert.strictEqual(next, seeded); }); test('ChangesetStatusChanged → Error attaches the error', () => { const err = { errorType: 'computeFailed', message: 'boom' }; - const next = changesetReducer(ready, { type: ActionType.ChangesetStatusChanged, changeset: 'cs', status: ChangesetStatus.Error, error: err }); + const next = changesetReducer(ready, { type: ActionType.ChangesetStatusChanged, status: ChangesetStatus.Error, error: err }); assert.deepStrictEqual({ status: next.status, error: next.error }, { status: ChangesetStatus.Error, error: err }); }); test('ChangesetStatusChanged → Ready strips a previous error', () => { const errored: ChangesetState = { status: ChangesetStatus.Error, error: { errorType: 'x', message: 'y' }, files: [fileA] }; - const next = changesetReducer(errored, { type: ActionType.ChangesetStatusChanged, changeset: 'cs', status: ChangesetStatus.Ready }); + const next = changesetReducer(errored, { type: ActionType.ChangesetStatusChanged, status: ChangesetStatus.Ready }); assert.deepStrictEqual({ status: next.status, error: next.error, files: next.files }, { status: ChangesetStatus.Ready, error: undefined, files: [fileA] }); }); test('ChangesetOperationsChanged with array replaces operations', () => { const ops = [{ id: 'stage', label: 'Stage', scopes: [] }]; - const next = changesetReducer(ready, { type: ActionType.ChangesetOperationsChanged, changeset: 'cs', operations: ops }); + const next = changesetReducer(ready, { type: ActionType.ChangesetOperationsChanged, operations: ops }); assert.deepStrictEqual(next.operations, ops); }); test('ChangesetOperationsChanged with undefined strips operations', () => { - const seeded = changesetReducer(ready, { type: ActionType.ChangesetOperationsChanged, changeset: 'cs', operations: [{ id: 'stage', label: 'Stage', scopes: [] }] }); - const next = changesetReducer(seeded, { type: ActionType.ChangesetOperationsChanged, changeset: 'cs', operations: undefined }); + const seeded = changesetReducer(ready, { type: ActionType.ChangesetOperationsChanged, operations: [{ id: 'stage', label: 'Stage', scopes: [] }] }); + const next = changesetReducer(seeded, { type: ActionType.ChangesetOperationsChanged, operations: undefined }); assert.strictEqual(next.operations, undefined); }); test('ChangesetCleared empties files', () => { - const seeded = changesetReducer(ready, { type: ActionType.ChangesetFileSet, changeset: 'cs', file: fileA }); - const next = changesetReducer(seeded, { type: ActionType.ChangesetCleared, changeset: 'cs' }); + const seeded = changesetReducer(ready, { type: ActionType.ChangesetFileSet, file: fileA }); + const next = changesetReducer(seeded, { type: ActionType.ChangesetCleared, }); assert.deepStrictEqual(next.files, []); }); test('ChangesetCleared is a no-op when files are already empty', () => { - const next = changesetReducer(ready, { type: ActionType.ChangesetCleared, changeset: 'cs' }); + const next = changesetReducer(ready, { type: ActionType.ChangesetCleared, }); assert.strictEqual(next, ready); }); }); diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts index 58f541d4df4..299743f86a2 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -20,10 +20,9 @@ import { AgentSession, IAgentConnection, IAgentSessionMetadata } from '../../../ import { buildUncommittedChangesetUri } from '../../../../../platform/agentHost/common/changesetUri.js'; import { KNOWN_AUTO_APPROVE_VALUES, SessionConfigKey } from '../../../../../platform/agentHost/common/sessionConfigKeys.js'; import { ResolveSessionConfigResult } from '../../../../../platform/agentHost/common/state/protocol/commands.js'; -import { NotificationType } from '../../../../../platform/agentHost/common/state/protocol/notifications.js'; import { ModelSelection, SessionStatus as ProtocolSessionStatus, RootConfigState, RootState, SessionState, SessionSummary, type ChangesetSummary } from '../../../../../platform/agentHost/common/state/protocol/state.js'; -import { ActionType, isSessionAction } from '../../../../../platform/agentHost/common/state/sessionActions.js'; -import { readSessionGitState, SessionMeta, StateComponents, type ISessionGitState } from '../../../../../platform/agentHost/common/state/sessionState.js'; +import { ActionType, isSessionAction, NotificationType } from '../../../../../platform/agentHost/common/state/sessionActions.js'; +import { readSessionGitState, ROOT_STATE_URI, SessionMeta, StateComponents, type ISessionGitState } from '../../../../../platform/agentHost/common/state/sessionState.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; @@ -1190,8 +1189,9 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement const rawId = this._rawIdFromChatId(sessionId); const cached = rawId ? this._sessionCache.get(rawId) : undefined; if (cached && rawId) { - const action = { type: ActionType.SessionConfigChanged as const, session: AgentSession.uri(cached.agentProvider, rawId).toString(), config: { [property]: value } }; - connection.dispatch(action); + const sessionUri = AgentSession.uri(cached.agentProvider, rawId); + const action = { type: ActionType.SessionConfigChanged as const, config: { [property]: value } }; + connection.dispatch(sessionUri.toString(), action); } } @@ -1234,13 +1234,13 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement const rawId = this._rawIdFromChatId(sessionId); const cached = rawId ? this._sessionCache.get(rawId) : undefined; if (cached && rawId) { + const sessionUri = AgentSession.uri(cached.agentProvider, rawId); const action = { type: ActionType.SessionConfigChanged as const, - session: AgentSession.uri(cached.agentProvider, rawId).toString(), config: nextValues, replace: true, }; - connection.dispatch(action); + connection.dispatch(sessionUri.toString(), action); } } @@ -1292,7 +1292,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement type: ActionType.RootConfigChanged as const, config: { [property]: value }, }; - connection.dispatch(action); + connection.dispatch(ROOT_STATE_URI, action); } async replaceRootConfig(values: Record): Promise { @@ -1323,7 +1323,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement config: nextValues, replace: true, }; - connection.dispatch(action); + connection.dispatch(ROOT_STATE_URI, action); } // -- Model selection ------------------------------------------------------ @@ -1343,8 +1343,9 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement const resourceScheme = cached.resource.scheme; const rawModelId = modelId.startsWith(`${resourceScheme}:`) ? modelId.substring(resourceScheme.length + 1) : modelId; const model = cached.modelSelection?.id === rawModelId ? cached.modelSelection : { id: rawModelId }; - const action = { type: ActionType.SessionModelChanged as const, session: AgentSession.uri(cached.agentProvider, rawId).toString(), model }; - connection.dispatch(action); + const sessionUri = AgentSession.uri(cached.agentProvider, rawId); + const action = { type: ActionType.SessionModelChanged as const, model }; + connection.dispatch(sessionUri.toString(), action); } } @@ -1358,8 +1359,9 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement this._onDidChangeSessions.fire({ added: [], removed: [], changed: [cached] }); const connection = this.connection; if (connection) { - const action = { type: ActionType.SessionIsArchivedChanged as const, session: AgentSession.uri(cached.agentProvider, rawId).toString(), isArchived: true }; - connection.dispatch(action); + const sessionUri = AgentSession.uri(cached.agentProvider, rawId); + const action = { type: ActionType.SessionIsArchivedChanged as const, isArchived: true }; + connection.dispatch(sessionUri.toString(), action); } } } @@ -1372,8 +1374,9 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement this._onDidChangeSessions.fire({ added: [], removed: [], changed: [cached] }); const connection = this.connection; if (connection) { - const action = { type: ActionType.SessionIsArchivedChanged as const, session: AgentSession.uri(cached.agentProvider, rawId).toString(), isArchived: false }; - connection.dispatch(action); + const sessionUri = AgentSession.uri(cached.agentProvider, rawId); + const action = { type: ActionType.SessionIsArchivedChanged as const, isArchived: false }; + connection.dispatch(sessionUri.toString(), action); } } } @@ -1397,8 +1400,9 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement if (cached && rawId && connection) { cached.title.set(title, undefined); this._onDidChangeSessions.fire({ added: [], removed: [], changed: [cached] }); - const action = { type: ActionType.SessionTitleChanged as const, session: AgentSession.uri(cached.agentProvider, rawId).toString(), title }; - connection.dispatch(action); + const sessionUri = AgentSession.uri(cached.agentProvider, rawId); + const action = { type: ActionType.SessionTitleChanged as const, title }; + connection.dispatch(sessionUri.toString(), action); } } @@ -1788,13 +1792,13 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement if (e.action.type === ActionType.SessionTurnComplete && isSessionAction(e.action)) { this._refreshSessions(); } else if (e.action.type === ActionType.SessionTitleChanged && isSessionAction(e.action)) { - this._handleTitleChanged(e.action.session, e.action.title); + this._handleTitleChanged(e.channel, e.action.title); } else if (e.action.type === ActionType.SessionModelChanged && isSessionAction(e.action)) { - this._handleModelChanged(e.action.session, e.action.model); + this._handleModelChanged(e.channel, e.action.model); } else if (e.action.type === ActionType.SessionIsArchivedChanged && isSessionAction(e.action)) { - this._handleIsArchivedChanged(e.action.session, e.action.isArchived); + this._handleIsArchivedChanged(e.channel, e.action.isArchived); } else if (e.action.type === ActionType.SessionConfigChanged && isSessionAction(e.action)) { - this._handleConfigChanged(e.action.session, e.action.config, e.action.replace === true); + this._handleConfigChanged(e.channel, e.action.config, e.action.replace === true); } })); } diff --git a/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts b/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts index ba945c7cae2..f6c9eab8b0f 100644 --- a/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts @@ -15,10 +15,9 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/ import { AgentSession, IAgentHostService, type IAgentSessionMetadata } from '../../../../../../platform/agentHost/common/agentService.js'; import type { IAgentSubscription } from '../../../../../../platform/agentHost/common/state/agentSubscription.js'; import type { ResolveSessionConfigResult } from '../../../../../../platform/agentHost/common/state/protocol/commands.js'; -import { NotificationType } from '../../../../../../platform/agentHost/common/state/protocol/notifications.js'; import { SessionLifecycle, type AgentInfo, type ModelSelection, type RootState, type SessionConfigState, type SessionState } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; import { SessionStatus as ProtocolSessionStatus, StateComponents } from '../../../../../../platform/agentHost/common/state/sessionState.js'; -import { ActionType, type ActionEnvelope, type IRootConfigChangedAction, type SessionAction, type TerminalAction, type INotification } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; +import { ActionType, NotificationType, type ActionEnvelope, type IRootConfigChangedAction, type SessionAction, type TerminalAction, type INotification } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IFileDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; @@ -51,7 +50,7 @@ class MockAgentHostService extends mock() { override readonly clientId = 'test-local-client'; private readonly _sessions = new Map(); public disposedSessions: URI[] = []; - public dispatchedActions: { action: SessionAction | TerminalAction | IRootConfigChangedAction; clientId: string; clientSeq: number }[] = []; + public dispatchedActions: { channel: string; action: SessionAction | TerminalAction | IRootConfigChangedAction; clientId: string; clientSeq: number }[] = []; public failResolveSessionConfig = false; public resolveSessionConfigResult: ResolveSessionConfigResult = { schema: { type: 'object', properties: {} }, values: { isolation: 'worktree' } }; public resolveSessionConfigRequests: { config?: Record }[] = []; @@ -125,12 +124,12 @@ class MockAgentHostService extends mock() { return this.resolveSessionConfigResult; } - dispatchAction(action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { - this.dispatchedActions.push({ action, clientId, clientSeq }); + dispatchAction(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { + this.dispatchedActions.push({ channel, action, clientId, clientSeq }); } - override dispatch(action: SessionAction | TerminalAction | IRootConfigChangedAction): void { - this.dispatchedActions.push({ action, clientId: this.clientId, clientSeq: this._nextSeq++ }); + override dispatch(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): void { + this.dispatchedActions.push({ channel, action, clientId: this.clientId, clientSeq: this._nextSeq++ }); } // Test helpers @@ -279,6 +278,7 @@ function fireSessionAdded(agentHost: MockAgentHostService, rawId: string, opts?: const provider = opts?.provider ?? 'copilotcli'; const sessionUri = AgentSession.uri(provider, rawId); agentHost.fireNotification({ + channel: 'ahp-root://', type: NotificationType.SessionAdded, summary: { resource: sessionUri.toString(), @@ -297,6 +297,7 @@ function fireSessionAdded(agentHost: MockAgentHostService, rawId: string, opts?: function fireSessionRemoved(agentHost: MockAgentHostService, rawId: string, provider = 'copilotcli'): void { const sessionUri = AgentSession.uri(provider, rawId); agentHost.fireNotification({ + channel: 'ahp-root://', type: NotificationType.SessionRemoved, session: sessionUri.toString(), }); @@ -615,7 +616,6 @@ suite('LocalAgentHostSessionsProvider', () => { assert.strictEqual(session!.modelId.get(), 'agent-host-copilotcli:new-model'); assert.deepStrictEqual(agentHost.dispatchedActions.at(-1)?.action, { type: ActionType.SessionModelChanged, - session: AgentSession.uri('copilotcli', 'set-model').toString(), model: { id: 'new-model' }, }); }); @@ -631,7 +631,6 @@ suite('LocalAgentHostSessionsProvider', () => { assert.deepStrictEqual(agentHost.dispatchedActions.at(-1)?.action, { type: ActionType.SessionModelChanged, - session: AgentSession.uri('copilotcli', 'set-model-config').toString(), model: { id: 'configured-model', config: { thinkingLevel: 'high' } }, }); }); @@ -897,7 +896,7 @@ suite('LocalAgentHostSessionsProvider', () => { const dispatched = agentHost.dispatchedActions[0]; assert.strictEqual(dispatched.action.type, ActionType.SessionTitleChanged); assert.strictEqual((dispatched.action as { title: string }).title, 'New Title'); - const actionSession = (dispatched.action as { session: string }).session; + const actionSession = dispatched.channel.toString(); assert.strictEqual(AgentSession.provider(actionSession), 'copilotcli'); assert.strictEqual(AgentSession.id(actionSession), 'rename-sess'); assert.strictEqual(dispatched.clientId, 'test-local-client'); @@ -936,9 +935,9 @@ suite('LocalAgentHostSessionsProvider', () => { disposables.add(provider.onDidChangeSessions(e => changes.push(e))); agentHost.fireAction({ + channel: AgentSession.uri('copilotcli', 'echo-sess').toString(), action: { type: ActionType.SessionTitleChanged, - session: AgentSession.uri('copilotcli', 'echo-sess').toString(), title: 'Server Title', }, serverSeq: 1, @@ -961,9 +960,9 @@ suite('LocalAgentHostSessionsProvider', () => { disposables.add(provider.onDidChangeSessions(e => changes.push(e))); agentHost.fireAction({ + channel: AgentSession.uri('copilotcli', 'model-change').toString(), action: { type: ActionType.SessionModelChanged, - session: AgentSession.uri('copilotcli', 'model-change').toString(), model: { id: 'new-model' } satisfies ModelSelection, }, serverSeq: 1, @@ -991,9 +990,9 @@ suite('LocalAgentHostSessionsProvider', () => { disposables.add(provider.onDidChangeSessions(e => changes.push(e))); agentHost.fireAction({ + channel: AgentSession.uri('copilotcli', 'turn-sess').toString(), action: { type: 'session/turnComplete', - session: AgentSession.uri('copilotcli', 'turn-sess').toString(), }, serverSeq: 1, origin: undefined, @@ -1264,11 +1263,10 @@ suite('LocalAgentHostSessionsProvider', () => { }); const sessionUri = AgentSession.uri('copilotcli', 'rep-1').toString(); - const configChanged = agentHost.dispatchedActions.find(d => d.action.type === ActionType.SessionConfigChanged && (d.action as { session: string }).session === sessionUri); + const configChanged = agentHost.dispatchedActions.find(d => d.action.type === ActionType.SessionConfigChanged && d.channel === sessionUri); assert.ok(configChanged, 'a SessionConfigChanged action should be dispatched'); assert.deepStrictEqual(configChanged.action, { type: ActionType.SessionConfigChanged, - session: sessionUri, config: { autoApprove: 'autoApprove', isolation: 'worktree', branch: 'main' }, replace: true, }); @@ -1340,9 +1338,9 @@ suite('LocalAgentHostSessionsProvider', () => { await waitForSessionConfig(provider, session!.sessionId, c => c?.values.autoApprove === 'default'); agentHost.fireAction({ + channel: AgentSession.uri('copilotcli', 'cfg-merge').toString(), action: { type: ActionType.SessionConfigChanged, - session: AgentSession.uri('copilotcli', 'cfg-merge').toString(), config: { autoApprove: 'autoApprove' }, }, serverSeq: 1, @@ -1381,9 +1379,9 @@ suite('LocalAgentHostSessionsProvider', () => { await waitForSessionConfig(provider, session!.sessionId, c => c?.values.autoApprove === 'default'); agentHost.fireAction({ + channel: AgentSession.uri('copilotcli', 'cfg-replace').toString(), action: { type: ActionType.SessionConfigChanged, - session: AgentSession.uri('copilotcli', 'cfg-replace').toString(), config: { autoApprove: 'autoApprove', isolation: 'worktree' }, replace: true, }, diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts index baa4776bd6c..22091f6a46b 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts @@ -17,7 +17,7 @@ import { ILogService } from '../../../../../platform/log/common/log.js'; import { AGENT_HOST_SCHEME, fromAgentHostUri } from '../../../../../platform/agentHost/common/agentHostUri.js'; import type { IAgentConnection } from '../../../../../platform/agentHost/common/agentService.js'; import { ActionType } from '../../../../../platform/agentHost/common/state/sessionActions.js'; -import { type AgentInfo, type CustomizationRef } from '../../../../../platform/agentHost/common/state/sessionState.js'; +import { ROOT_STATE_URI, type AgentInfo, type CustomizationRef } from '../../../../../platform/agentHost/common/state/sessionState.js'; import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { INotificationService } from '../../../../../platform/notification/common/notification.js'; import { AICustomizationManagementSection, IAICustomizationWorkspaceService, type IStorageSourceFilter } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; @@ -75,7 +75,7 @@ export class RemoteAgentPluginController extends Disposable { } private dispatchCustomizations(customizations: readonly CustomizationRef[]): void { - this._connection.dispatch({ + this._connection.dispatch(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, config: { [AgentHostConfigKey.Customizations]: [...customizations], diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.ts index 04f6b6c3254..90f3d576f7a 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.ts @@ -38,7 +38,7 @@ class MockAgentConnection extends mock() { private _rootStateValue: RootState = { agents: [] }; override readonly rootState; - readonly dispatchedActions: StateAction[] = []; + readonly dispatchedActions: { channel: string; action: StateAction }[] = []; constructor() { super(); @@ -56,8 +56,8 @@ class MockAgentConnection extends mock() { this._rootStateValue = rootState; } - override dispatch(action: StateAction): void { - this.dispatchedActions.push(action); + override dispatch(channel: string, action: StateAction): void { + this.dispatchedActions.push({ channel, action }); } fireAction(envelope: ActionEnvelope): void { @@ -123,9 +123,12 @@ suite('RemoteAgentHostCustomizationHarness', () => { await controller.removeConfiguredPlugin(pluginA); assert.deepStrictEqual(connection.dispatchedActions, [{ - type: ActionType.RootConfigChanged, - config: { - customizations: [pluginB], + channel: 'ahp-root://', + action: { + type: ActionType.RootConfigChanged, + config: { + customizations: [pluginB], + }, }, }]); }); @@ -202,11 +205,11 @@ suite('RemoteAgentHostCustomizationHarness', () => { )); connection.fireAction({ + channel: agentHostSessionId, serverSeq: 1, origin: undefined, action: { type: ActionType.SessionCustomizationsChanged, - session: agentHostSessionId, customizations: [synced], }, }); @@ -253,11 +256,11 @@ suite('RemoteAgentHostCustomizationHarness', () => { )); connection.fireAction({ + channel: agentHostSessionId, serverSeq: 1, origin: undefined, action: { type: ActionType.SessionCustomizationsChanged, - session: agentHostSessionId, customizations: [synced], }, }); @@ -351,11 +354,11 @@ suite('RemoteAgentHostCustomizationHarness', () => { )); connection.fireAction({ + channel: agentHostSessionId, serverSeq: 1, origin: undefined, action: { type: ActionType.SessionCustomizationsChanged, - session: agentHostSessionId, customizations: [synced], }, }); @@ -405,11 +408,11 @@ suite('RemoteAgentHostCustomizationHarness', () => { )); connection.fireAction({ + channel: agentHostSessionId, serverSeq: 1, origin: undefined, action: { type: ActionType.SessionCustomizationsChanged, - session: agentHostSessionId, customizations: [synced], }, }); @@ -459,11 +462,11 @@ suite('RemoteAgentHostCustomizationHarness', () => { )); connection.fireAction({ + channel: agentHostSessionId, serverSeq: 1, origin: undefined, action: { type: ActionType.SessionCustomizationsChanged, - session: agentHostSessionId, customizations: [sessionCustomization], }, }); @@ -508,14 +511,14 @@ suite('RemoteAgentHostCustomizationHarness', () => { disposables.add(provider.onDidChange(() => changeCount++)); connection.fireAction({ + channel: agentHostSessionId, serverSeq: 1, origin: undefined, action: { type: ActionType.SessionCustomizationsChanged, - session: agentHostSessionId, customizations: [{ customization: pluginRef, - enabled: true, + enabled: true }], }, }); @@ -554,15 +557,15 @@ suite('RemoteAgentHostCustomizationHarness', () => { )); connection.fireAction({ + channel: agentHostSessionId, serverSeq: 1, origin: undefined, action: { type: ActionType.SessionCustomizationsChanged, - session: agentHostSessionId, customizations: [{ customization: clientPlugin, clientId: 'test-client', - enabled: true, + enabled: true }], }, }); @@ -604,9 +607,12 @@ suite('RemoteAgentHostCustomizationHarness', () => { assert.strictEqual(connection.dispatchedActions.length, 1); assert.deepStrictEqual(connection.dispatchedActions[0], { - type: ActionType.RootConfigChanged, - config: { - customizations: [pluginA, pluginC], + channel: 'ahp-root://', + action: { + type: ActionType.RootConfigChanged, + config: { + customizations: [pluginA, pluginC], + }, }, }); }); @@ -642,11 +648,11 @@ suite('RemoteAgentHostCustomizationHarness', () => { )); connection.fireAction({ + channel: agentHostSessionId, serverSeq: 1, origin: undefined, action: { type: ActionType.SessionCustomizationsChanged, - session: agentHostSessionId, customizations: [ { customization: clientA, clientId: 'test-client', enabled: true }, { customization: clientB, clientId: 'test-client', enabled: true }, diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts index 973cfc0a285..d836398c17e 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts @@ -13,9 +13,8 @@ import { runWithFakedTimers } from '../../../../../../base/test/common/timeTrave import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { AgentSession, type IAgentConnection, type IAgentSessionMetadata } from '../../../../../../platform/agentHost/common/agentService.js'; import type { ResolveSessionConfigResult } from '../../../../../../platform/agentHost/common/state/protocol/commands.js'; -import { NotificationType } from '../../../../../../platform/agentHost/common/state/protocol/notifications.js'; import { SessionLifecycle, type AgentInfo, type ModelSelection, type RootState, type SessionConfigState, type SessionState } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; -import { ActionType, type ActionEnvelope, type IRootConfigChangedAction, type SessionAction, type TerminalAction, type INotification } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; +import { ActionType, NotificationType, type ActionEnvelope, type IRootConfigChangedAction, type SessionAction, type TerminalAction, type INotification } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; import { SessionStatus as ProtocolSessionStatus, StateComponents } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import type { IAgentSubscription } from '../../../../../../platform/agentHost/common/state/agentSubscription.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; @@ -55,7 +54,7 @@ class MockAgentConnection extends mock() { override readonly clientId = 'test-client-1'; private readonly _sessions = new Map(); public disposedSessions: URI[] = []; - public dispatchedActions: { action: SessionAction | TerminalAction | IRootConfigChangedAction; clientId: string; clientSeq: number }[] = []; + public dispatchedActions: { channel: string; action: SessionAction | TerminalAction | IRootConfigChangedAction; clientId: string; clientSeq: number }[] = []; public failResolveSessionConfig = false; public resolveSessionConfigResult: ResolveSessionConfigResult = { schema: { type: 'object', properties: {} }, values: { isolation: 'worktree' } }; @@ -102,12 +101,12 @@ class MockAgentConnection extends mock() { return this.resolveSessionConfigResult; } - dispatchAction(action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { - this.dispatchedActions.push({ action, clientId, clientSeq }); + dispatchAction(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { + this.dispatchedActions.push({ channel, action, clientId, clientSeq }); } - override dispatch(action: SessionAction | TerminalAction | IRootConfigChangedAction): void { - this.dispatchedActions.push({ action, clientId: this.clientId, clientSeq: this._nextSeq++ }); + override dispatch(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): void { + this.dispatchedActions.push({ channel, action, clientId: this.clientId, clientSeq: this._nextSeq++ }); } // Test helpers @@ -258,6 +257,7 @@ function fireSessionAdded(connection: MockAgentConnection, rawId: string, opts?: const provider = opts?.provider ?? 'copilotcli'; const sessionUri = AgentSession.uri(provider, rawId); connection.fireNotification({ + channel: 'ahp-root://', type: NotificationType.SessionAdded, summary: { resource: sessionUri.toString(), @@ -276,6 +276,7 @@ function fireSessionAdded(connection: MockAgentConnection, rawId: string, opts?: function fireSessionRemoved(connection: MockAgentConnection, rawId: string, provider = 'copilotcli'): void { const sessionUri = AgentSession.uri(provider, rawId); connection.fireNotification({ + channel: 'ahp-root://', type: NotificationType.SessionRemoved, session: sessionUri.toString(), }); @@ -553,7 +554,6 @@ suite('RemoteAgentHostSessionsProvider', () => { assert.strictEqual(session!.modelId.get(), 'remote-localhost__4321-copilotcli:new-model'); assert.deepStrictEqual(connection.dispatchedActions.at(-1)?.action, { type: ActionType.SessionModelChanged, - session: AgentSession.uri('copilotcli', 'set-model').toString(), model: { id: 'new-model' }, }); }); @@ -569,7 +569,6 @@ suite('RemoteAgentHostSessionsProvider', () => { assert.deepStrictEqual(connection.dispatchedActions.at(-1)?.action, { type: ActionType.SessionModelChanged, - session: AgentSession.uri('copilotcli', 'set-model-config').toString(), model: { id: 'configured-model', config: { thinkingLevel: 'high' } }, }); }); @@ -662,7 +661,7 @@ suite('RemoteAgentHostSessionsProvider', () => { assert.strictEqual(dispatched.action.type, ActionType.SessionTitleChanged); assert.strictEqual((dispatched.action as { title: string }).title, 'New Title'); // The session URI in the action must be the backend agent session URI - const actionSession = (dispatched.action as { session: string }).session; + const actionSession = dispatched.channel.toString(); assert.strictEqual(AgentSession.provider(actionSession), 'copilotcli'); assert.strictEqual(AgentSession.id(actionSession), 'rename-sess'); assert.strictEqual(dispatched.clientId, 'test-client-1'); @@ -719,9 +718,9 @@ suite('RemoteAgentHostSessionsProvider', () => { // Simulate the server echoing a title change (from auto-generation or another client) connection.fireAction({ + channel: AgentSession.uri('copilotcli', 'echo-sess').toString(), action: { type: ActionType.SessionTitleChanged, - session: AgentSession.uri('copilotcli', 'echo-sess').toString(), title: 'Server Title', }, serverSeq: 1, @@ -744,9 +743,9 @@ suite('RemoteAgentHostSessionsProvider', () => { disposables.add(provider.onDidChangeSessions((e: ISessionChangeEvent) => changes.push(e))); connection.fireAction({ + channel: AgentSession.uri('copilotcli', 'model-change').toString(), action: { type: ActionType.SessionModelChanged, - session: AgentSession.uri('copilotcli', 'model-change').toString(), model: { id: 'new-model' } satisfies ModelSelection, }, serverSeq: 1, @@ -776,9 +775,9 @@ suite('RemoteAgentHostSessionsProvider', () => { // Trigger refresh via turnComplete action (simulates what happens on reload) connection.fireAction({ + channel: AgentSession.uri('copilotcli', 'persist-sess').toString(), action: { type: 'session/turnComplete', - session: AgentSession.uri('copilotcli', 'persist-sess').toString(), }, serverSeq: 1, origin: undefined, @@ -966,9 +965,9 @@ suite('RemoteAgentHostSessionsProvider', () => { disposables.add(provider.onDidChangeSessions((e: ISessionChangeEvent) => changes.push(e))); connection.fireAction({ + channel: AgentSession.uri('copilotcli', 'turn-sess').toString(), action: { type: 'session/turnComplete', - session: AgentSession.uri('copilotcli', 'turn-sess').toString(), }, serverSeq: 1, origin: undefined, diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts index e361da26a31..f9aec3f9b12 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts @@ -61,7 +61,7 @@ export class AgentCustomizationItemProvider extends Disposable implements ICusto this._register(this._connection.onDidAction(envelope => { if (envelope.action.type === ActionType.SessionCustomizationsChanged) { - this._sessionCustomizationsCache.set(envelope.action.session, envelope.action.customizations); + this._sessionCustomizationsCache.set(envelope.channel, envelope.action.customizations); this._onDidChange.fire(); } })); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatInputPicker.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatInputPicker.ts index 40f6de44315..f664f9741fb 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatInputPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatInputPicker.ts @@ -574,9 +574,8 @@ export class AgentHostChatInputPicker extends Disposable { return; } - this._agentHostService.dispatch({ + this._agentHostService.dispatch(backendSession.toString(), { type: ActionType.SessionConfigChanged, - session: backendSession.toString(), config: partial, }); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts index 745e72c6ff5..2c74654a7bb 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts @@ -439,9 +439,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC const backendSession = this._resolveSessionUri(sessionResource); const state = this._getSessionState(backendSession.toString()); if (state?.activeClient?.clientId === this._config.connection.clientId) { - this._dispatchAction({ + this._dispatchAction(backendSession, { type: ActionType.SessionActiveClientToolsChanged, - session: backendSession.toString(), tools: defs, }); } @@ -457,9 +456,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC return; } this._logService.info(`[AgentHost] Continue in background: terminal=${parsed.terminal}, session=${parsed.session}`); - this._config.connection.dispatch({ + this._config.connection.dispatch(parsed.terminal, { type: ActionType.TerminalClaimed, - terminal: parsed.terminal, claim: { kind: TerminalClaimKind.Session, session: parsed.session, @@ -509,7 +507,7 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC // result if the user kept typing while the request was in flight. const result = await this._config.connection.completions({ kind: AhpCompletionItemKind.UserMessage, - session: backendSession.toString(), + channel: backendSession.toString(), text: params.text, offset: params.offset, }); @@ -689,9 +687,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC return true; } this._logService.info(`[AgentHost] Cancellation requested for ${sessionKey}, dispatching turnCancelled`); - this._config.connection.dispatch({ + this._config.connection.dispatch(resolvedSession.toString(), { type: ActionType.SessionTurnCancelled, - session: sessionKey, turnId, }); return true; @@ -810,9 +807,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC // already live in `existingState.config?.values` and don't need to // be re-dispatched. if (request.agentHostSessionConfig && Object.keys(request.agentHostSessionConfig).length > 0) { - this._dispatchAction({ + this._dispatchAction(resolvedSession, { type: ActionType.SessionConfigChanged, - session: sessionKey, config: request.agentHostSessionConfig, }); } @@ -884,18 +880,16 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC // --- Steering --- if (currentSteering) { if (currentSteering.id !== prevSteering?.id || currentSteering.text !== prevSteering.userMessage.text) { - this._dispatchAction({ + this._dispatchAction(backendSession, { type: ActionType.SessionPendingMessageSet, - session, kind: PendingMessageKind.Steering, id: currentSteering.id, userMessage: { text: currentSteering.text, attachments: currentSteering.attachments }, }); } } else if (prevSteering) { - this._dispatchAction({ + this._dispatchAction(backendSession, { type: ActionType.SessionPendingMessageRemoved, - session, kind: PendingMessageKind.Steering, id: prevSteering.id, }); @@ -905,9 +899,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC const currentQueuedIds = new Set(currentQueued.map(q => q.id)); for (const prev of prevQueued) { if (!currentQueuedIds.has(prev.id)) { - this._dispatchAction({ + this._dispatchAction(backendSession, { type: ActionType.SessionPendingMessageRemoved, - session, kind: PendingMessageKind.Queued, id: prev.id, }); @@ -919,9 +912,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC for (const q of currentQueued) { const prev = prevQueuedById.get(q.id); if (!prev || q.text !== prev.userMessage.text) { - this._dispatchAction({ + this._dispatchAction(backendSession, { type: ActionType.SessionPendingMessageSet, - session, kind: PendingMessageKind.Queued, id: q.id, userMessage: { text: q.text, attachments: q.attachments }, @@ -937,17 +929,16 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC if (updatedQueued.length > 1 && currentQueued.length === updatedQueued.length) { const needsReorder = currentQueued.some((q, i) => q.id !== updatedQueued[i].id); if (needsReorder) { - this._dispatchAction({ + this._dispatchAction(backendSession, { type: ActionType.SessionQueuedMessagesReordered, - session, order: currentQueued.map(q => q.id), }); } } } - private _dispatchAction(action: ClientSessionAction): void { - this._config.connection.dispatch(action); + private _dispatchAction(channel: URI, action: ClientSessionAction): void { + this._config.connection.dispatch(channel.toString(), action); } /** @@ -956,9 +947,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC * client-provided tools. */ private _dispatchActiveClient(backendSession: URI, customizations: CustomizationRef[]): void { - this._dispatchAction({ + this._dispatchAction(backendSession, { type: ActionType.SessionActiveClientChanged, - session: backendSession.toString(), activeClient: { clientId: this._config.connection.clientId, tools: this._clientToolsObs.get().map(toolDataToDefinition), @@ -1103,9 +1093,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC if (selectedModel) { const currentModel = this._getSessionState(session.toString())?.summary.model; if (!this._modelSelectionsEqual(currentModel, selectedModel)) { - this._config.connection.dispatch({ + this._config.connection.dispatch(session.toString(), { type: ActionType.SessionModelChanged, - session: session.toString(), model: selectedModel, }); } @@ -1123,18 +1112,16 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC if (!previousRequest && protocolState.turns.length > 0) { const truncateAction: SessionTruncatedAction = { type: ActionType.SessionTruncated, - session: session.toString(), }; - this._config.connection.dispatch(truncateAction); + this._config.connection.dispatch(session.toString(), truncateAction); } else { const seenAtIndex = protocolState.turns.findIndex(t => t.id === previousRequest!.id); if (seenAtIndex !== -1 && seenAtIndex < protocolState.turns.length - 1) { const truncateAction: SessionTruncatedAction = { type: ActionType.SessionTruncated, - session: session.toString(), turnId: previousRequest!.id, }; - this._config.connection.dispatch(truncateAction); + this._config.connection.dispatch(session.toString(), truncateAction); } } } @@ -1143,14 +1130,13 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC // the provider as a side effect. const turnAction: SessionTurnStartedAction = { type: ActionType.SessionTurnStarted, - session: session.toString(), turnId, userMessage: { text: request.message, attachments: messageAttachments.length > 0 ? messageAttachments : undefined, }, }; - this._config.connection.dispatch(turnAction); + this._config.connection.dispatch(session.toString(), turnAction); // Ensure the editing session records a sentinel checkpoint for this // request so it appears in requestDisablement even if the turn @@ -1168,9 +1154,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC const cancelSub = store.add(cancellationToken.onCancellationRequested(() => { cancelSub.dispose(); this._logService.info(`[AgentHost] Cancellation requested for ${session.toString()}, dispatching turnCancelled`); - this._config.connection.dispatch({ + this._config.connection.dispatch(session.toString(), { type: ActionType.SessionTurnCancelled, - session: session.toString(), turnId, }); })); @@ -1226,9 +1211,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC this._logService.info(`[AgentHost] Tool confirmation: toolCallId=${toolCallId}, approved=${approved}, selectedOptionId=${selectedOption?.id}`); if (approved) { - this._config.connection.dispatch({ + this._config.connection.dispatch(session.toString(), { type: ActionType.SessionToolCallConfirmed, - session: session.toString(), turnId, toolCallId, approved: true, @@ -1236,9 +1220,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC ...(selectedOption ? { selectedOptionId: selectedOption.id } : {}), }); } else { - this._config.connection.dispatch({ + this._config.connection.dispatch(session.toString(), { type: ActionType.SessionToolCallConfirmed, - session: session.toString(), turnId, toolCallId, approved: false, @@ -1617,9 +1600,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC const toolData = this._toolsService.getToolByName(toolName); if (!toolData) { this._logService.warn(`[AgentHost] Client tool call for unknown tool: ${toolName}`); - this._dispatchAction({ + this._dispatchAction(opts.backendSession, { type: ActionType.SessionToolCallComplete, - session: opts.backendSession.toString(), turnId: opts.turnId, toolCallId, result: { @@ -1640,9 +1622,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC if (!invocation) { this._logService.warn(`[AgentHost] Failed to begin client tool invocation: ${toolName}`); - this._dispatchAction({ + this._dispatchAction(opts.backendSession, { type: ActionType.SessionToolCallComplete, - session: opts.backendSession.toString(), turnId: opts.turnId, toolCallId, result: { @@ -1675,9 +1656,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC return; } approvedDispatched = true; - this._dispatchAction({ + this._dispatchAction(opts.backendSession, { type: ActionType.SessionToolCallConfirmed, - session: opts.backendSession.toString(), turnId: opts.turnId, toolCallId, approved: true, @@ -1691,9 +1671,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC if (cts.token.isCancellationRequested) { return; } - this._dispatchAction({ + this._dispatchAction(opts.backendSession, { type: ActionType.SessionToolCallConfirmed, - session: opts.backendSession.toString(), turnId: opts.turnId, toolCallId, approved: false, @@ -1719,9 +1698,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC const message = err instanceof Error ? err.message : String(err); result = { content: [], toolResultError: message }; } - this._dispatchAction({ + this._dispatchAction(opts.backendSession, { type: ActionType.SessionToolCallComplete, - session: opts.backendSession.toString(), turnId: opts.turnId, toolCallId, result: toolResultToProtocol(result ?? { content: [] }, toolName), @@ -1770,9 +1748,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC parameters = parsed as Record; } catch { this._logService.warn(`[AgentHost] Failed to parse tool input for ${toolName}`); - this._dispatchAction({ + this._dispatchAction(opts.backendSession, { type: ActionType.SessionToolCallComplete, - session: opts.backendSession.toString(), turnId: opts.turnId, toolCallId, result: { @@ -1909,17 +1886,15 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC return; } if (!result.answers) { - this._config.connection.dispatch({ + this._config.connection.dispatch(opts.backendSession.toString(), { type: ActionType.SessionInputCompleted, - session: opts.backendSession.toString(), requestId: inputReq.id, response: SessionInputResponseKind.Cancel, }); } else { const answers = convertCarouselAnswers(result.answers); - this._config.connection.dispatch({ + this._config.connection.dispatch(opts.backendSession.toString(), { type: ActionType.SessionInputCompleted, - session: opts.backendSession.toString(), requestId: inputReq.id, response: SessionInputResponseKind.Accept, answers, @@ -1974,9 +1949,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC return; } settled = true; - this._config.connection.dispatch({ + this._config.connection.dispatch(opts.backendSession.toString(), { type: ActionType.SessionInputCompleted, - session: opts.backendSession.toString(), requestId: inputReq.id, response, }); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionListController.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionListController.ts index b548fdea2ef..25bc923ed43 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionListController.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionListController.ts @@ -104,7 +104,7 @@ export class AgentHostSessionListController extends Disposable implements IChatS // React to protocol notifications for session list changes this._register(this._connection.onDidNotification(n => { - if (n.type === 'notify/sessionAdded' && n.summary.provider === this._provider) { + if (n.type === 'root/sessionAdded' && n.summary.provider === this._provider) { const rawId = AgentSession.id(n.summary.resource); this._pendingNewSessions.delete(rawId); this._cachedSummaries.set(rawId, n.summary); @@ -116,7 +116,7 @@ export class AgentHostSessionListController extends Disposable implements IChatS this._items.push(item); } this._onDidChangeChatSessionItems.fire({ addedOrUpdated: [item] }); - } else if (n.type === 'notify/sessionRemoved' && AgentSession.provider(n.session) === this._provider) { + } else if (n.type === 'root/sessionRemoved' && AgentSession.provider(n.session) === this._provider) { const removedId = AgentSession.id(n.session); this._pendingNewSessions.delete(removedId); const idx = this._items.findIndex(item => item.resource.path === `/${removedId}`); @@ -125,7 +125,7 @@ export class AgentHostSessionListController extends Disposable implements IChatS this._cachedSummaries.delete(removedId); this._onDidChangeChatSessionItems.fire({ removed: [removed.resource] }); } - } else if (n.type === 'notify/sessionSummaryChanged' && AgentSession.provider(n.session) === this._provider) { + } else if (n.type === 'root/sessionSummaryChanged' && AgentSession.provider(n.session) === this._provider) { const rawId = AgentSession.id(n.session); const cached = this._cachedSummaries.get(rawId); if (!cached) { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostTerminalContribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostTerminalContribution.ts index ca0ed66d24c..56e48f4c634 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostTerminalContribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostTerminalContribution.ts @@ -9,6 +9,7 @@ import { localize } from '../../../../../../nls.js'; import { AgentHostCustomTerminalToolEnabledSettingId, AgentHostEnabledSettingId, IAgentHostService } from '../../../../../../platform/agentHost/common/agentService.js'; import { AgentHostConfigKey } from '../../../../../../platform/agentHost/common/agentHostCustomizationConfig.js'; import { ActionType } from '../../../../../../platform/agentHost/common/state/protocol/actions.js'; +import { ROOT_STATE_URI } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { TerminalSettingId } from '../../../../../../platform/terminal/common/terminal.js'; @@ -160,7 +161,7 @@ export class AgentHostTerminalContribution extends Disposable implements IWorkbe return; } - this._agentHostService.dispatch({ + this._agentHostService.dispatch(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, config: { [AgentHostConfigKey.DefaultShell]: profile.path }, }); @@ -180,7 +181,7 @@ export class AgentHostTerminalContribution extends Disposable implements IWorkbe return; } - this._agentHostService.dispatch({ + this._agentHostService.dispatch(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, config: { [AgentHostConfigKey.DisableCustomTerminalTool]: disableCustomTerminalTool }, }); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostUntitledProvisionalSessionService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostUntitledProvisionalSessionService.ts index d88fe54d002..5a14e59582c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostUntitledProvisionalSessionService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostUntitledProvisionalSessionService.ts @@ -420,9 +420,8 @@ export class AgentHostUntitledProvisionalSessionService extends Disposable imple Object.assign(entry.config, partial); } } - this._agentHostService.dispatch({ + this._agentHostService.dispatch(backend.toString(), { type: ActionType.SessionConfigChanged, - session: backend.toString(), config: partial, }); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.ts index 1d53184f247..41187069636 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.ts @@ -224,9 +224,9 @@ export class LoggingAgentConnection extends Disposable implements IAgentConnecti return this._inner.getSubscriptionUnmanaged(kind, resource); } - dispatch(action: SessionAction | TerminalAction | IRootConfigChangedAction): void { - this._log('>>', 'dispatch', action); - this._inner.dispatch(action); + dispatch(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): void { + this._log('>>', 'dispatch', { channel, action }); + this._inner.dispatch(channel, action); } async resourceList(uri: URI): Promise { diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts index a6f98b45c33..177f458b463 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts @@ -21,7 +21,7 @@ import { AgentFeedbackAttachmentDisplayKind, AgentFeedbackAttachmentMetadataKey import { ActionType, isSessionAction, type ActionEnvelope, type IRootConfigChangedAction, type SessionAction, type TerminalAction, type INotification, type IToolCallConfirmedAction, type ITurnStartedAction } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; import type { IStateSnapshot } from '../../../../../../platform/agentHost/common/state/sessionProtocol.js'; import type { CustomizationRef } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; -import { SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, SessionLifecycle, SessionStatus, TurnState, ToolCallStatus, ToolCallConfirmationReason, createSessionState, createActiveTurn, ROOT_STATE_URI, PolicyState, ResponsePartKind, StateComponents, buildSubagentSessionUri, ToolResultContentType, MessageAttachmentKind, type SessionState, type SessionSummary, RootState, type ToolCallState, type AgentInfo } from '../../../../../../platform/agentHost/common/state/sessionState.js'; +import { SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, SessionLifecycle, SessionStatus, TurnState, ToolCallStatus, ToolCallConfirmationReason, createSessionState, createActiveTurn, isAhpRootChannel, PolicyState, ResponsePartKind, StateComponents, buildSubagentSessionUri, ToolResultContentType, MessageAttachmentKind, type SessionState, type SessionSummary, RootState, type ToolCallState, type AgentInfo } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { CompletionItemKind as AhpCompletionItemKind, type CompletionsParams, type CompletionsResult } from '../../../../../../platform/agentHost/common/state/protocol/commands.js'; import { sessionReducer } from '../../../../../../platform/agentHost/common/state/sessionReducers.js'; import { IDefaultAccountService } from '../../../../../../platform/defaultAccount/common/defaultAccount.js'; @@ -143,7 +143,7 @@ class MockAgentHostService extends mock() { // Protocol methods public override readonly clientId = 'test-window-1'; - public dispatchedActions: { action: SessionAction | TerminalAction | IRootConfigChangedAction; clientId: string; clientSeq: number }[] = []; + public dispatchedActions: { channel: string; action: SessionAction | TerminalAction | IRootConfigChangedAction; clientId: string; clientSeq: number }[] = []; /** Returns dispatched actions filtered to turn-related types only * (excludes lifecycle actions like activeClientChanged). */ @@ -158,7 +158,7 @@ class MockAgentHostService extends mock() { return { resource: resourceStr, state: existingState, fromSeq: 0 }; } // Root state subscription - if (resourceStr === ROOT_STATE_URI) { + if (isAhpRootChannel(resourceStr)) { return { resource: resourceStr, state: { @@ -183,8 +183,8 @@ class MockAgentHostService extends mock() { }; } unsubscribe(_resource: URI): void { } - dispatchAction(action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { - this.dispatchedActions.push({ action, clientId, clientSeq }); + dispatchAction(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction, clientId: string, clientSeq: number): void { + this.dispatchedActions.push({ channel, action, clientId, clientSeq }); } private _nextSeq = 1; nextClientSeq(): number { @@ -294,14 +294,14 @@ class MockAgentHostService extends mock() { onDidApplyAction: entry.onDidApply.event, } satisfies IAgentSubscription; } - override dispatch(action: SessionAction | TerminalAction | IRootConfigChangedAction): void { - this.dispatchedActions.push({ action, clientId: this.clientId, clientSeq: this._nextSeq++ }); + override dispatch(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): void { + this.dispatchedActions.push({ channel, action, clientId: this.clientId, clientSeq: this._nextSeq++ }); // Apply state-management actions optimistically so state-dependent // logic (e.g. customization re-dispatch) sees the correct activeClient. // Turn lifecycle actions (turnStarted, toolCallConfirmed, etc.) are applied // later via fireAction when the server echoes them back. if (isSessionAction(action) && action.type === 'session/activeClientChanged') { - const entry = this._liveSubscriptions.get(action.session); + const entry = this._liveSubscriptions.get(channel.toString()); if (entry) { const noop = () => { }; entry.state = sessionReducer(entry.state, action as Parameters[1], noop); @@ -315,7 +315,7 @@ class MockAgentHostService extends mock() { this._onDidAction.fire(envelope); // Route action to matching live subscriptions if (isSessionAction(envelope.action)) { - const sessionUri = envelope.action.session; + const sessionUri = envelope.channel; const entry = this._liveSubscriptions.get(sessionUri); if (entry) { const noop = () => { }; @@ -595,11 +595,11 @@ async function startTurn( // Filter for turn-related dispatches only (skip activeClientChanged etc.) const turnDispatches = agentHostService.dispatchedActions.filter(d => d.action.type === 'session/turnStarted'); const lastDispatch = turnDispatches[turnDispatches.length - 1] ?? agentHostService.dispatchedActions[agentHostService.dispatchedActions.length - 1]; - const session = (lastDispatch?.action as ITurnStartedAction)?.session; + const session = lastDispatch?.channel.toString(); const turnId = (lastDispatch?.action as ITurnStartedAction)?.turnId; const fire = (action: SessionAction) => { - agentHostService.fireAction({ action, serverSeq: seq.v++, origin: undefined }); + agentHostService.fireAction({ channel: session!, action, serverSeq: seq.v++, origin: undefined }); }; // Echo the turnStarted action to clear the pending write-ahead entry. @@ -607,6 +607,7 @@ async function startTurn( // the server's turnComplete clears it, preventing the turn from finishing. if (lastDispatch) { agentHostService.fireAction({ + channel: lastDispatch.channel.toString(), action: lastDispatch.action, serverSeq: seq.v++, origin: { clientId: agentHostService.clientId, clientSeq: lastDispatch.clientSeq }, @@ -654,14 +655,15 @@ async function startDynamicAgentTurn( const turnDispatches = agentHostService.dispatchedActions.filter(d => d.action.type === 'session/turnStarted'); const lastDispatch = turnDispatches[turnDispatches.length - 1] ?? agentHostService.dispatchedActions[agentHostService.dispatchedActions.length - 1]; - const session = (lastDispatch?.action as ITurnStartedAction)?.session; + const session = lastDispatch?.channel.toString(); const turnId = (lastDispatch?.action as ITurnStartedAction)?.turnId; const fire = (action: SessionAction) => { - agentHostService.fireAction({ action, serverSeq: seq.v++, origin: undefined }); + agentHostService.fireAction({ channel: session!, action, serverSeq: seq.v++, origin: undefined }); }; if (lastDispatch) { agentHostService.fireAction({ + channel: lastDispatch.channel.toString(), action: lastDispatch.action, serverSeq: seq.v++, origin: { clientId: agentHostService.clientId, clientSeq: lastDispatch.clientSeq }, @@ -962,8 +964,8 @@ suite('AgentHostChatContribution', () => { const dispatch1 = agentHostService.turnActions[0]; const action1 = dispatch1.action as ITurnStartedAction; // Echo the turnStarted to clear pending write-ahead - agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); - agentHostService.fireAction({ action: { type: 'session/turnComplete', session: action1.session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); + agentHostService.fireAction({ channel: dispatch1.channel.toString(), action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); + agentHostService.fireAction({ channel: dispatch1.channel.toString(), action: { type: 'session/turnComplete', turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); await turn1Promise; // Second turn @@ -974,14 +976,14 @@ suite('AgentHostChatContribution', () => { await timeout(10); const dispatch2 = agentHostService.turnActions[1]; const action2 = dispatch2.action as ITurnStartedAction; - agentHostService.fireAction({ action: dispatch2.action, serverSeq: 3, origin: { clientId: agentHostService.clientId, clientSeq: dispatch2.clientSeq } }); - agentHostService.fireAction({ action: { type: 'session/turnComplete', session: action2.session, turnId: action2.turnId } as SessionAction, serverSeq: 4, origin: undefined }); + agentHostService.fireAction({ channel: dispatch2.channel.toString(), action: dispatch2.action, serverSeq: 3, origin: { clientId: agentHostService.clientId, clientSeq: dispatch2.clientSeq } }); + agentHostService.fireAction({ channel: dispatch2.channel.toString(), action: { type: 'session/turnComplete', turnId: action2.turnId } as SessionAction, serverSeq: 4, origin: undefined }); await turn2Promise; assert.strictEqual(agentHostService.turnActions.length, 2); assert.strictEqual( - (agentHostService.turnActions[0].action as ITurnStartedAction).session.toString(), - (agentHostService.turnActions[1].action as ITurnStartedAction).session.toString(), + agentHostService.turnActions[0].channel.toString(), + agentHostService.turnActions[1].channel.toString(), ); })); @@ -1017,8 +1019,8 @@ suite('AgentHostChatContribution', () => { const dispatch = agentHostService.turnActions[0]; const action = dispatch.action as ITurnStartedAction; - agentHostService.fireAction({ action: dispatch.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch.clientSeq } }); - agentHostService.fireAction({ action: { type: 'session/turnComplete', session: action.session, turnId: action.turnId } as SessionAction, serverSeq: 2, origin: undefined }); + agentHostService.fireAction({ channel: dispatch.channel.toString(), action: dispatch.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch.clientSeq } }); + agentHostService.fireAction({ channel: dispatch.channel.toString(), action: { type: 'session/turnComplete', turnId: action.turnId } as SessionAction, serverSeq: 2, origin: undefined }); await turnPromise; assert.deepStrictEqual(agentHostService.turnActions.map(d => (d.action as ITurnStartedAction).userMessage.text), ['Recovered']); @@ -1248,16 +1250,17 @@ suite('AgentHostChatContribution', () => { test('events from other sessions are ignored', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const { sessionHandler, agentHostService, chatAgentService } = createContribution(disposables); - const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); + const { turnPromise, collected, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); // Delta from a different session — will be ignored (session not subscribed) agentHostService.fireAction({ - action: { type: 'session/delta', session: AgentSession.uri('copilot', 'other-session').toString(), turnId, partId: 'md-other', content: 'wrong' } as SessionAction, + channel: AgentSession.uri('copilot', 'other-session').toString(), + action: { type: 'session/delta', turnId, partId: 'md-other', content: 'wrong' } as SessionAction, serverSeq: 100, origin: undefined, }); - fire({ type: 'session/responsePart', session, turnId, part: { kind: 'markdown', id: 'md-1', content: 'right' } } as SessionAction); - fire({ type: 'session/turnComplete', session, turnId } as SessionAction); + fire({ type: 'session/responsePart', turnId, part: { kind: 'markdown', id: 'md-1', content: 'right' } } as SessionAction); + fire({ type: 'session/turnComplete', turnId } as SessionAction); await turnPromise; @@ -1270,11 +1273,10 @@ suite('AgentHostChatContribution', () => { const sessionResource = URI.from({ scheme: 'agent-host-copilot', path: '/new-input-request-test' }); chatWidgetService.setWidgetForSession(sessionResource); - const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables, { sessionResource }); + const { turnPromise, collected, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables, { sessionResource }); fire({ type: ActionType.SessionInputRequested, - session, request: { id: 'input-1', message: 'Need more information', @@ -1300,7 +1302,6 @@ suite('AgentHostChatContribution', () => { agentHostService.dispatchedActions.length = 0; fire({ type: ActionType.SessionInputCompleted, - session, requestId: 'input-1', response: SessionInputResponseKind.Accept, answers: { @@ -1331,7 +1332,7 @@ suite('AgentHostChatContribution', () => { }); assert.strictEqual(agentHostService.dispatchedActions.some(dispatched => dispatched.action.type === ActionType.SessionInputCompleted), false); - fire({ type: ActionType.SessionTurnComplete, session, turnId }); + fire({ type: ActionType.SessionTurnComplete, turnId }); await turnPromise; })); @@ -1340,11 +1341,10 @@ suite('AgentHostChatContribution', () => { const sessionResource = URI.from({ scheme: 'agent-host-copilot', path: '/new-local-input-request-test' }); chatWidgetService.setWidgetForSession(sessionResource); - const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables, { sessionResource }); + const { turnPromise, collected, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables, { sessionResource }); fire({ type: ActionType.SessionInputRequested, - session, request: { id: 'input-1', message: 'Need more information', @@ -1371,7 +1371,6 @@ suite('AgentHostChatContribution', () => { agentHostService.dispatchedActions.length = 0; fire({ type: ActionType.SessionInputCompleted, - session, requestId: 'input-1', response: SessionInputResponseKind.Accept, answers: { @@ -1387,7 +1386,7 @@ suite('AgentHostChatContribution', () => { assert.deepStrictEqual(chatWidgetService.clearQuestionCarouselCalls, []); assert.strictEqual(agentHostService.dispatchedActions.some(dispatched => dispatched.action.type === ActionType.SessionInputCompleted), false); - fire({ type: ActionType.SessionTurnComplete, session, turnId }); + fire({ type: ActionType.SessionTurnComplete, turnId }); await turnPromise; })); @@ -1396,11 +1395,10 @@ suite('AgentHostChatContribution', () => { const sessionResource = URI.from({ scheme: 'agent-host-copilot', path: '/new-cancelled-input-request-test' }); chatWidgetService.setWidgetForSession(sessionResource); - const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables, { sessionResource }); + const { turnPromise, collected, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables, { sessionResource }); fire({ type: ActionType.SessionInputRequested, - session, request: { id: 'input-1', message: 'Need more information', @@ -1426,7 +1424,6 @@ suite('AgentHostChatContribution', () => { agentHostService.dispatchedActions.length = 0; fire({ type: ActionType.SessionInputCompleted, - session, requestId: 'input-1', response: SessionInputResponseKind.Cancel, }); @@ -1441,18 +1438,17 @@ suite('AgentHostChatContribution', () => { ]); assert.strictEqual(agentHostService.dispatchedActions.some(dispatched => dispatched.action.type === ActionType.SessionInputCompleted), false); - fire({ type: ActionType.SessionTurnComplete, session, turnId }); + fire({ type: ActionType.SessionTurnComplete, turnId }); await turnPromise; })); test('url-style input request renders an elicitation part with the URL', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const { sessionHandler, agentHostService, chatAgentService } = createContribution(disposables); - const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); + const { turnPromise, collected, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); fire({ type: ActionType.SessionInputRequested, - session, request: { id: 'url-1', message: 'Please authorize', @@ -1472,18 +1468,17 @@ suite('AgentHostChatContribution', () => { assert.ok(part.acceptButtonLabel.includes('example.com'), 'accept button should reference the URL authority'); assert.strictEqual(collected.flat().some(p => p.kind === 'questionCarousel'), false, 'url-style requests must not also render a question carousel'); - fire({ type: ActionType.SessionTurnComplete, session, turnId }); + fire({ type: ActionType.SessionTurnComplete, turnId }); await turnPromise; })); test('url input request accept opens URL and dispatches Accept', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const { sessionHandler, agentHostService, chatAgentService, openerService } = createContribution(disposables); - const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); + const { turnPromise, collected, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); fire({ type: ActionType.SessionInputRequested, - session, request: { id: 'url-1', url: 'https://example.com/auth', @@ -1507,18 +1502,17 @@ suite('AgentHostChatContribution', () => { response: (completions[0].action as { response: SessionInputResponseKind }).response, }, { requestId: 'url-1', response: SessionInputResponseKind.Accept }); - fire({ type: ActionType.SessionTurnComplete, session, turnId }); + fire({ type: ActionType.SessionTurnComplete, turnId }); await turnPromise; })); test('url input request decline dispatches Decline', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const { sessionHandler, agentHostService, chatAgentService } = createContribution(disposables); - const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); + const { turnPromise, collected, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); fire({ type: ActionType.SessionInputRequested, - session, request: { id: 'url-1', url: 'https://example.com/auth', @@ -1538,7 +1532,7 @@ suite('AgentHostChatContribution', () => { assert.strictEqual((completions[0].action as { response: SessionInputResponseKind }).response, SessionInputResponseKind.Decline); assert.strictEqual(part.state.get(), ElicitationState.Rejected); - fire({ type: ActionType.SessionTurnComplete, session, turnId }); + fire({ type: ActionType.SessionTurnComplete, turnId }); await turnPromise; })); @@ -1546,11 +1540,10 @@ suite('AgentHostChatContribution', () => { const { sessionHandler, agentHostService, chatAgentService, openerService } = createContribution(disposables); openerService.openShouldFail = true; - const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); + const { turnPromise, collected, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); fire({ type: ActionType.SessionInputRequested, - session, request: { id: 'url-1', url: 'https://example.com/auth' }, }); await timeout(10); @@ -1567,7 +1560,7 @@ suite('AgentHostChatContribution', () => { assert.strictEqual((completions[0].action as { response: SessionInputResponseKind }).response, SessionInputResponseKind.Decline); assert.strictEqual(part.state.get(), ElicitationState.Rejected); - fire({ type: ActionType.SessionTurnComplete, session, turnId }); + fire({ type: ActionType.SessionTurnComplete, turnId }); await turnPromise; })); @@ -1575,11 +1568,10 @@ suite('AgentHostChatContribution', () => { const { sessionHandler, agentHostService, chatAgentService, openerService } = createContribution(disposables); openerService.openResult = false; - const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); + const { turnPromise, collected, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); fire({ type: ActionType.SessionInputRequested, - session, request: { id: 'url-1', url: 'https://example.com/auth' }, }); await timeout(10); @@ -1596,24 +1588,23 @@ suite('AgentHostChatContribution', () => { assert.strictEqual((completions[0].action as { response: SessionInputResponseKind }).response, SessionInputResponseKind.Decline); assert.strictEqual(part.state.get(), ElicitationState.Rejected); - fire({ type: ActionType.SessionTurnComplete, session, turnId }); + fire({ type: ActionType.SessionTurnComplete, turnId }); await turnPromise; })); test('url input request abandoned at turn end dispatches Cancel', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const { sessionHandler, agentHostService, chatAgentService } = createContribution(disposables); - const { turnPromise, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); + const { turnPromise, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); fire({ type: ActionType.SessionInputRequested, - session, request: { id: 'url-1', url: 'https://example.com/auth' }, }); await timeout(10); agentHostService.dispatchedActions.length = 0; - fire({ type: ActionType.SessionTurnComplete, session, turnId }); + fire({ type: ActionType.SessionTurnComplete, turnId }); await turnPromise; const completions = agentHostService.dispatchedActions.filter(d => d.action.type === ActionType.SessionInputCompleted); @@ -1627,11 +1618,10 @@ suite('AgentHostChatContribution', () => { test('url input request completion from another client does not redispatch', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const { sessionHandler, agentHostService, chatAgentService } = createContribution(disposables); - const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); + const { turnPromise, collected, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); fire({ type: ActionType.SessionInputRequested, - session, request: { id: 'url-1', url: 'https://example.com/auth' }, }); await timeout(10); @@ -1642,7 +1632,6 @@ suite('AgentHostChatContribution', () => { agentHostService.dispatchedActions.length = 0; fire({ type: ActionType.SessionInputCompleted, - session, requestId: 'url-1', response: SessionInputResponseKind.Accept, }); @@ -1650,7 +1639,7 @@ suite('AgentHostChatContribution', () => { assert.strictEqual(part.state.get(), ElicitationState.Accepted); - fire({ type: ActionType.SessionTurnComplete, session, turnId }); + fire({ type: ActionType.SessionTurnComplete, turnId }); await turnPromise; assert.strictEqual(agentHostService.dispatchedActions.some(d => d.action.type === ActionType.SessionInputCompleted), false); @@ -1659,11 +1648,10 @@ suite('AgentHostChatContribution', () => { test('url input request server-side dismissal rejects the part and does not redispatch', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const { sessionHandler, agentHostService, chatAgentService } = createContribution(disposables); - const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); + const { turnPromise, collected, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); fire({ type: ActionType.SessionInputRequested, - session, request: { id: 'url-1', url: 'https://example.com/auth' }, }); await timeout(10); @@ -1674,7 +1662,6 @@ suite('AgentHostChatContribution', () => { agentHostService.dispatchedActions.length = 0; fire({ type: ActionType.SessionInputCompleted, - session, requestId: 'url-1', response: SessionInputResponseKind.Cancel, }); @@ -1682,7 +1669,7 @@ suite('AgentHostChatContribution', () => { assert.strictEqual(part.state.get(), ElicitationState.Rejected); - fire({ type: ActionType.SessionTurnComplete, session, turnId }); + fire({ type: ActionType.SessionTurnComplete, turnId }); await turnPromise; assert.strictEqual(agentHostService.dispatchedActions.some(d => d.action.type === ActionType.SessionInputCompleted), false); @@ -1800,9 +1787,9 @@ suite('AgentHostChatContribution', () => { const { turnPromise, collected, session, turnId } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); agentHostService.fireAction({ + channel: session, action: { type: 'session/error', - session, turnId, error: { errorType: 'test_error', message: 'Something went wrong' }, } as SessionAction, @@ -2002,6 +1989,7 @@ suite('AgentHostChatContribution', () => { // Echo the confirmation so the reducer transitions tc → Running, // then complete the turn cleanly. agentHostService.fireAction({ + channel: confirmedDispatches[0].channel.toString(), action: confirmedDispatches[0].action, serverSeq: 100, origin: { clientId: agentHostService.clientId, clientSeq: confirmedDispatches[0].clientSeq }, @@ -2051,6 +2039,7 @@ suite('AgentHostChatContribution', () => { return (a.action as IToolCallConfirmedAction).toolCallId === 'tc-recon'; })!; agentHostService.fireAction({ + channel: firstConfirm.channel.toString(), action: firstConfirm.action, serverSeq: 100, origin: { clientId: agentHostService.clientId, clientSeq: firstConfirm.clientSeq }, @@ -2589,9 +2578,9 @@ suite('AgentHostChatContribution', () => { // Simulate a server-side error (e.g. sendMessage failure on the server) agentHostService.fireAction({ + channel: session, action: { type: 'session/error', - session, turnId, error: { errorType: 'connection_error', message: 'connection lost' }, } as SessionAction, @@ -3289,8 +3278,8 @@ suite('AgentHostChatContribution', () => { await timeout(10); const turnDispatch = agentHostService.turnActions[0]; const turnAction = turnDispatch.action as ITurnStartedAction; - agentHostService.fireAction({ action: turnDispatch.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: turnDispatch.clientSeq } }); - agentHostService.fireAction({ action: { type: 'session/turnComplete', session: turnAction.session, turnId: turnAction.turnId } as SessionAction, serverSeq: 2, origin: undefined }); + agentHostService.fireAction({ channel: turnDispatch.channel.toString(), action: turnDispatch.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: turnDispatch.clientSeq } }); + agentHostService.fireAction({ channel: turnDispatch.channel.toString(), action: { type: 'session/turnComplete', turnId: turnAction.turnId } as SessionAction, serverSeq: 2, origin: undefined }); await turnPromise; const configChanged = agentHostService.dispatchedActions.find(d => d.action.type === ActionType.SessionConfigChanged); @@ -3337,8 +3326,8 @@ suite('AgentHostChatContribution', () => { await timeout(10); const turnDispatch = agentHostService.turnActions[0]; const turnAction = turnDispatch.action as ITurnStartedAction; - agentHostService.fireAction({ action: turnDispatch.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: turnDispatch.clientSeq } }); - agentHostService.fireAction({ action: { type: 'session/turnComplete', session: turnAction.session, turnId: turnAction.turnId } as SessionAction, serverSeq: 2, origin: undefined }); + agentHostService.fireAction({ channel: turnDispatch.channel.toString(), action: turnDispatch.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: turnDispatch.clientSeq } }); + agentHostService.fireAction({ channel: turnDispatch.channel.toString(), action: { type: 'session/turnComplete', turnId: turnAction.turnId } as SessionAction, serverSeq: 2, origin: undefined }); await turnPromise; const configChanged = agentHostService.dispatchedActions.find(d => d.action.type === ActionType.SessionConfigChanged) as { action: { config: Record; replace?: boolean } } | undefined; @@ -3472,13 +3461,13 @@ suite('AgentHostChatContribution', () => { assert.ok(chatAgentService.registeredAgents.has('connection-test')); // Verify it can run a turn through the IAgentConnection path - const { turnPromise, session, turnId, fire } = await startTurn(handler, agentHostService, chatAgentService, disposables, { + const { turnPromise, turnId, fire } = await startTurn(handler, agentHostService, chatAgentService, disposables, { message: 'Test message', agentId: 'connection-test', }); - fire({ type: 'session/delta', session, turnId, content: 'Response' } as SessionAction); - fire({ type: 'session/turnComplete', session, turnId } as SessionAction); + fire({ type: 'session/delta', turnId, content: 'Response' } as SessionAction); + fire({ type: 'session/turnComplete', turnId } as SessionAction); await turnPromise; // Turn dispatched via connection.dispatchAction @@ -3616,7 +3605,7 @@ suite('AgentHostChatContribution', () => { // Fire a delta action to simulate the server streaming more text agentHostService.fireAction({ - action: { type: 'session/delta', session: sessionUri.toString(), turnId: 'turn-active', partId: 'md-active', content: ' and more' } as SessionAction, + channel: sessionUri.toString(), action: { type: 'session/delta', turnId: 'turn-active', partId: 'md-active', content: ' and more' } as SessionAction, serverSeq: 1, origin: undefined, }); @@ -3645,7 +3634,7 @@ suite('AgentHostChatContribution', () => { // Fire turnComplete to finish the active turn agentHostService.fireAction({ - action: { type: 'session/turnComplete', session: sessionUri.toString(), turnId: 'turn-active' } as SessionAction, + channel: sessionUri.toString(), action: { type: 'session/turnComplete', turnId: 'turn-active' } as SessionAction, serverSeq: 1, origin: undefined, }); @@ -3713,7 +3702,7 @@ suite('AgentHostChatContribution', () => { // Complete the turn so the awaitConfirmation promise and its internal // DisposableStore are cleaned up before test teardown. agentHostService.fireAction({ - action: { type: 'session/turnComplete', session: sessionUri.toString(), turnId: 'turn-active' } as SessionAction, + channel: sessionUri.toString(), action: { type: 'session/turnComplete', turnId: 'turn-active' } as SessionAction, serverSeq: 1, origin: undefined, }); @@ -3817,7 +3806,6 @@ suite('AgentHostChatContribution', () => { assert.ok(action, 'queued message should be dispatched to the agent host'); assert.deepStrictEqual(action, { type: ActionType.SessionPendingMessageSet, - session: backendSession.toString(), kind: 'queued', id: 'queued-request-1', userMessage: { text, attachments: undefined }, @@ -3859,7 +3847,6 @@ suite('AgentHostChatContribution', () => { assert.ok(action, 'queued message text update should be dispatched to the agent host'); assert.deepStrictEqual(action, { type: ActionType.SessionPendingMessageSet, - session: backendSession.toString(), kind: 'queued', id: 'queued-request-1', userMessage: { text, attachments: undefined }, @@ -3887,10 +3874,10 @@ suite('AgentHostChatContribution', () => { await timeout(10); const dispatch1 = agentHostService.turnActions[0]; const action1 = dispatch1.action as ITurnStartedAction; - const session = action1.session; + const session = dispatch1.channel.toString(); // Echo + complete the first turn - agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); - agentHostService.fireAction({ action: { type: 'session/turnComplete', session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); + agentHostService.fireAction({ channel: dispatch1.channel.toString(), action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); + agentHostService.fireAction({ channel: session, action: { type: 'session/turnComplete', session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); await turn1Promise; // Now simulate a server-initiated turn (e.g. from a consumed queued message) @@ -3899,9 +3886,9 @@ suite('AgentHostChatContribution', () => { disposables.add(chatSession.onDidStartServerRequest!(e => serverRequestEvents.push(e))); agentHostService.fireAction({ + channel: session, action: { type: 'session/turnStarted', - session, turnId: serverTurnId, userMessage: { text: 'queued message text' }, } as SessionAction, @@ -3939,14 +3926,15 @@ suite('AgentHostChatContribution', () => { await timeout(10); const dispatch1 = agentHostService.turnActions[0]; const action1 = dispatch1.action as ITurnStartedAction; - const session = action1.session; - agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); - agentHostService.fireAction({ action: { type: 'session/turnComplete', session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); + const session = dispatch1.channel.toString(); + agentHostService.fireAction({ channel: dispatch1.channel.toString(), action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); + agentHostService.fireAction({ channel: session, action: { type: 'session/turnComplete', session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); await turn1Promise; // Server-initiated turn const serverTurnId = 'server-turn-progress'; agentHostService.fireAction({ + channel: session, action: { type: 'session/turnStarted', session, turnId: serverTurnId, userMessage: { text: 'auto queued' } } as SessionAction, serverSeq: 3, origin: undefined, }); @@ -3954,10 +3942,12 @@ suite('AgentHostChatContribution', () => { // Stream a response part + delta agentHostService.fireAction({ + channel: session, action: { type: 'session/responsePart', session, turnId: serverTurnId, part: { kind: 'markdown', id: 'md-srv', content: 'Hello ' } } as SessionAction, serverSeq: 4, origin: undefined, }); agentHostService.fireAction({ + channel: session, action: { type: 'session/delta', session, turnId: serverTurnId, partId: 'md-srv', content: 'world' } as SessionAction, serverSeq: 5, origin: undefined, }); @@ -3971,6 +3961,7 @@ suite('AgentHostChatContribution', () => { // Complete the turn agentHostService.fireAction({ + channel: session, action: { type: 'session/turnComplete', session, turnId: serverTurnId } as SessionAction, serverSeq: 6, origin: undefined, }); @@ -4016,8 +4007,8 @@ suite('AgentHostChatContribution', () => { await timeout(10); const dispatch = agentHostService.turnActions[0]; const action = dispatch.action as ITurnStartedAction; - agentHostService.fireAction({ action: dispatch.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch.clientSeq } }); - agentHostService.fireAction({ action: { type: 'session/turnComplete', session: action.session, turnId: action.turnId } as SessionAction, serverSeq: 2, origin: undefined }); + agentHostService.fireAction({ channel: dispatch.channel.toString(), action: dispatch.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch.clientSeq } }); + agentHostService.fireAction({ channel: dispatch.channel.toString(), action: { type: 'session/turnComplete', turnId: action.turnId } as SessionAction, serverSeq: 2, origin: undefined }); await turnPromise; assert.strictEqual(serverRequestEvents.length, 0, 'Client-dispatched turns should not trigger onDidStartServerRequest'); @@ -4043,14 +4034,15 @@ suite('AgentHostChatContribution', () => { await timeout(10); const dispatch1 = agentHostService.turnActions[0]; const action1 = dispatch1.action as ITurnStartedAction; - const session = action1.session; - agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); - agentHostService.fireAction({ action: { type: 'session/turnComplete', session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); + const session = dispatch1.channel.toString(); + agentHostService.fireAction({ channel: dispatch1.channel.toString(), action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); + agentHostService.fireAction({ channel: session, action: { type: 'session/turnComplete', session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); await turn1Promise; // Server-initiated turn const serverTurnId = 'server-turn-tool-dedup'; agentHostService.fireAction({ + channel: session, action: { type: 'session/turnStarted', session, turnId: serverTurnId, userMessage: { text: 'queued' } } as SessionAction, serverSeq: 3, origin: undefined, }); @@ -4058,10 +4050,12 @@ suite('AgentHostChatContribution', () => { // Tool start + ready (auto-confirmed) agentHostService.fireAction({ + channel: session, action: { type: 'session/toolCallStart', session, turnId: serverTurnId, toolCallId: 'tc-srv-1', toolName: 'bash', displayName: 'Bash' } as SessionAction, serverSeq: 4, origin: undefined, }); agentHostService.fireAction({ + channel: session, action: { type: 'session/toolCallReady', session, turnId: serverTurnId, toolCallId: 'tc-srv-1', invocationMessage: 'Running Bash', confirmed: 'not-needed' } as SessionAction, serverSeq: 5, origin: undefined, }); @@ -4069,6 +4063,7 @@ suite('AgentHostChatContribution', () => { // Tool complete agentHostService.fireAction({ + channel: session, action: { type: 'session/toolCallComplete', session, turnId: serverTurnId, toolCallId: 'tc-srv-1', result: { success: true, pastTenseMessage: 'Ran Bash' } } as SessionAction, serverSeq: 6, origin: undefined, }); @@ -4076,10 +4071,12 @@ suite('AgentHostChatContribution', () => { // Fire additional state changes that might cause re-processing agentHostService.fireAction({ + channel: session, action: { type: 'session/responsePart', session, turnId: serverTurnId, part: { kind: 'markdown', id: 'md-after', content: 'Done.' } } as SessionAction, serverSeq: 7, origin: undefined, }); agentHostService.fireAction({ + channel: session, action: { type: 'session/turnComplete', session, turnId: serverTurnId } as SessionAction, serverSeq: 8, origin: undefined, }); @@ -4111,9 +4108,9 @@ suite('AgentHostChatContribution', () => { await timeout(10); const dispatch1 = agentHostService.turnActions[0]; const action1 = dispatch1.action as ITurnStartedAction; - const session = action1.session; - agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); - agentHostService.fireAction({ action: { type: 'session/turnComplete', session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); + const session = dispatch1.channel.toString(); + agentHostService.fireAction({ channel: dispatch1.channel.toString(), action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); + agentHostService.fireAction({ channel: session, action: { type: 'session/turnComplete', session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); await turn1Promise; // Fire turnStarted followed immediately by a response part. @@ -4123,10 +4120,12 @@ suite('AgentHostChatContribution', () => { // is not missed. const serverTurnId = 'server-turn-md-initial'; agentHostService.fireAction({ + channel: session, action: { type: 'session/turnStarted', session, turnId: serverTurnId, userMessage: { text: 'queued' } } as SessionAction, serverSeq: 3, origin: undefined, }); agentHostService.fireAction({ + channel: session, action: { type: 'session/responsePart', session, turnId: serverTurnId, part: { kind: 'markdown', id: 'md-init', content: 'Initial text' } } as SessionAction, serverSeq: 4, origin: undefined, }); @@ -4140,6 +4139,7 @@ suite('AgentHostChatContribution', () => { // Complete the turn agentHostService.fireAction({ + channel: session, action: { type: 'session/turnComplete', session, turnId: serverTurnId } as SessionAction, serverSeq: 5, origin: undefined, }); @@ -4166,13 +4166,14 @@ suite('AgentHostChatContribution', () => { await timeout(10); const dispatch1 = agentHostService.turnActions[0]; const action1 = dispatch1.action as ITurnStartedAction; - const session = action1.session; - agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); - agentHostService.fireAction({ action: { type: 'session/turnComplete', session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); + const session = dispatch1.channel.toString(); + agentHostService.fireAction({ channel: dispatch1.channel.toString(), action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); + agentHostService.fireAction({ channel: session, action: { type: 'session/turnComplete', session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); await turn1Promise; // Add a queued message to the protocol state so it's tracked. agentHostService.fireAction({ + channel: session, action: { type: 'session/pendingMessageSet', session, kind: 'queued', id: 'q-1', userMessage: { text: 'will be consumed' } } as SessionAction, serverSeq: 3, origin: undefined, }); @@ -4182,6 +4183,7 @@ suite('AgentHostChatContribution', () => { // and the queued message disappears in the same state change. chatService.removePendingRequestCalls.length = 0; agentHostService.fireAction({ + channel: session, action: { type: 'session/turnStarted', session, turnId: 'server-turn-q', userMessage: { text: 'will be consumed' }, queuedMessageId: 'q-1' } as SessionAction, serverSeq: 4, origin: undefined, }); @@ -4210,13 +4212,14 @@ suite('AgentHostChatContribution', () => { await timeout(10); const dispatch1 = agentHostService.turnActions[0]; const action1 = dispatch1.action as ITurnStartedAction; - const session = action1.session; - agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); - agentHostService.fireAction({ action: { type: 'session/turnComplete', session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); + const session = dispatch1.channel.toString(); + agentHostService.fireAction({ channel: dispatch1.channel.toString(), action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); + agentHostService.fireAction({ channel: session, action: { type: 'session/turnComplete', session, turnId: action1.turnId } as SessionAction, serverSeq: 2, origin: undefined }); await turn1Promise; // Set a steering message on the protocol state. agentHostService.fireAction({ + channel: session, action: { type: 'session/pendingMessageSet', session, kind: 'steering', id: 'steer-1', userMessage: { text: 'be more careful' } } as SessionAction, serverSeq: 3, origin: undefined, }); @@ -4225,6 +4228,7 @@ suite('AgentHostChatContribution', () => { // Steering message is consumed by the agent. agentHostService.fireAction({ + channel: session, action: { type: 'session/pendingMessageRemoved', session, kind: 'steering', id: 'steer-1' } as SessionAction, serverSeq: 4, origin: undefined, }); @@ -4416,11 +4420,10 @@ suite('AgentHostChatContribution', () => { const childTurnId = 'child-turn-1'; const childToolCallId = 'tc-child-1'; const fireChild = (action: SessionAction) => { - agentHostService.fireAction({ action, serverSeq: 1000, origin: undefined }); + agentHostService.fireAction({ channel: childSessionUri, action, serverSeq: 1000, origin: undefined }); }; fireChild({ type: 'session/turnStarted', - session: childSessionUri, turnId: childTurnId, userMessage: { text: '' }, } as SessionAction); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostClientTools.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostClientTools.test.ts index d3540dd8ba5..c4bf83290a5 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostClientTools.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostClientTools.test.ts @@ -313,17 +313,18 @@ suite('AgentHostClientTools', () => { override readonly onAgentHostStart = Event.None; private readonly _liveSubscriptions = new Map }>(); - public dispatchedActions: (SessionAction | TerminalAction | IRootConfigChangedAction)[] = []; + public dispatchedActions: { channel: string; action: SessionAction | TerminalAction | IRootConfigChangedAction }[] = []; - override dispatch(action: SessionAction | TerminalAction | IRootConfigChangedAction): void { - this.dispatchedActions.push(action); + override dispatch(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): void { + this.dispatchedActions.push({ channel, action }); if (isSessionAction(action)) { - this.applySessionAction(action); + this.applySessionAction(channel, action); } } - applySessionAction(action: SessionAction): void { - const entry = this._ensureLiveSubscription(action.session); + applySessionAction(channel: string | URI, action: SessionAction): void { + const channelStr = typeof channel === 'string' ? channel : channel.toString(); + const entry = this._ensureLiveSubscription(channelStr); entry.state = sessionReducer(entry.state, action as Parameters[1], () => { }); entry.emitter.fire(entry.state); } @@ -536,7 +537,7 @@ suite('AgentHostClientTools', () => { // no activeClientToolsChanged should be dispatched. // But the observable should now reflect the new tools. const toolsChangedActions = connection.dispatchedActions.filter( - a => isSessionAction(a) && a.type === 'session/activeClientToolsChanged' + a => isSessionAction(a.action) && a.action.type === 'session/activeClientToolsChanged' ); // No sessions active = no dispatches assert.strictEqual(toolsChangedActions.length, 0); @@ -558,24 +559,21 @@ suite('AgentHostClientTools', () => { const sessionResource = URI.parse('agent-host-copilot:/session-1'); const backendSession = AgentSession.uri('copilot', 'session-1').toString(); - connection.applySessionAction({ + connection.applySessionAction(URI.parse(backendSession), { type: ActionType.SessionTurnStarted, - session: backendSession, turnId: 'turn-1', userMessage: { text: 'run the task' }, } as SessionAction); - connection.applySessionAction({ + connection.applySessionAction(URI.parse(backendSession), { type: ActionType.SessionToolCallStart, - session: backendSession, turnId: 'turn-1', toolCallId: 'tool-call-1', toolName: 'runTask', displayName: 'Run Task', toolClientId: connection.clientId, } as SessionAction); - connection.applySessionAction({ + connection.applySessionAction(URI.parse(backendSession), { type: ActionType.SessionToolCallReady, - session: backendSession, turnId: 'turn-1', toolCallId: 'tool-call-1', invocationMessage: 'Run Task', @@ -598,9 +596,9 @@ suite('AgentHostClientTools', () => { parameters: { task: 'build' }, chatStreamToolCallId: 'tool-call-1', }]); - assert.ok(connection.dispatchedActions.some(action => isSessionAction(action) - && action.type === ActionType.SessionToolCallComplete - && action.toolCallId === 'tool-call-1')); + assert.ok(connection.dispatchedActions.some(entry => isSessionAction(entry.action) + && entry.action.type === ActionType.SessionToolCallComplete + && entry.action.toolCallId === 'tool-call-1')); }); test('reconnecting to an active turn with owned client tool completes the initial snapshot invocation', async () => { @@ -608,24 +606,21 @@ suite('AgentHostClientTools', () => { const sessionResource = URI.parse('agent-host-copilot:/session-1'); const backendSession = AgentSession.uri('copilot', 'session-1').toString(); - connection.applySessionAction({ + connection.applySessionAction(URI.parse(backendSession), { type: ActionType.SessionTurnStarted, - session: backendSession, turnId: 'turn-1', userMessage: { text: 'run the task' }, } as SessionAction); - connection.applySessionAction({ + connection.applySessionAction(URI.parse(backendSession), { type: ActionType.SessionToolCallStart, - session: backendSession, turnId: 'turn-1', toolCallId: 'tool-call-1', toolName: 'runTask', displayName: 'Run Task', toolClientId: connection.clientId, } as SessionAction); - connection.applySessionAction({ + connection.applySessionAction(URI.parse(backendSession), { type: ActionType.SessionToolCallReady, - session: backendSession, turnId: 'turn-1', toolCallId: 'tool-call-1', invocationMessage: 'Run Task', @@ -670,24 +665,21 @@ suite('AgentHostClientTools', () => { const subagentBackendSession = buildSubagentSessionUri(backendSession, parentToolCallId); // Parent turn with a `task` tool that spawns a subagent. - connection.applySessionAction({ + connection.applySessionAction(URI.parse(backendSession), { type: ActionType.SessionTurnStarted, - session: backendSession, turnId: 'turn-1', userMessage: { text: 'do work' }, }); - connection.applySessionAction({ + connection.applySessionAction(URI.parse(backendSession), { type: ActionType.SessionToolCallStart, - session: backendSession, turnId: 'turn-1', toolCallId: parentToolCallId, toolName: 'task', displayName: 'Task', _meta: { toolKind: 'subagent' }, }); - connection.applySessionAction({ + connection.applySessionAction(URI.parse(backendSession), { type: ActionType.SessionToolCallReady, - session: backendSession, turnId: 'turn-1', toolCallId: parentToolCallId, invocationMessage: 'Spawning subagent', @@ -698,24 +690,21 @@ suite('AgentHostClientTools', () => { // Subagent turn carrying a client-provided tool call (toolClientId // matches the renderer's clientId so the renderer owns the // invocation). - connection.applySessionAction({ + connection.applySessionAction(URI.parse(subagentBackendSession), { type: ActionType.SessionTurnStarted, - session: subagentBackendSession, turnId: 'sub-turn-1', userMessage: { text: '' }, }); - connection.applySessionAction({ + connection.applySessionAction(URI.parse(subagentBackendSession), { type: ActionType.SessionToolCallStart, - session: subagentBackendSession, turnId: 'sub-turn-1', toolCallId: 'inner-tool-call-1', toolName: 'runTask', displayName: 'Run Task', toolClientId: connection.clientId, }); - connection.applySessionAction({ + connection.applySessionAction(URI.parse(subagentBackendSession), { type: ActionType.SessionToolCallReady, - session: subagentBackendSession, turnId: 'sub-turn-1', toolCallId: 'inner-tool-call-1', invocationMessage: 'Run Task', @@ -737,14 +726,14 @@ suite('AgentHostClientTools', () => { // The completion must be dispatched against the subagent session // URI (the agent will then resolve it to the parent session that // owns the SDK deferred). - const completion = connection.dispatchedActions.find(action => - isSessionAction(action) - && action.type === ActionType.SessionToolCallComplete - && action.toolCallId === 'inner-tool-call-1' + const completionEntry = connection.dispatchedActions.find(entry => + isSessionAction(entry.action) + && entry.action.type === ActionType.SessionToolCallComplete + && entry.action.toolCallId === 'inner-tool-call-1' ); - assert.ok(completion, 'completion for the inner client tool should be dispatched'); + assert.ok(completionEntry, 'completion for the inner client tool should be dispatched'); assert.strictEqual( - isSessionAction(completion!) ? completion.session : undefined, + completionEntry.channel.toString(), subagentBackendSession, 'completion should target the subagent session URI' ); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostTerminalContribution.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostTerminalContribution.test.ts index f2935721575..51873fecd77 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostTerminalContribution.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostTerminalContribution.test.ts @@ -40,10 +40,10 @@ class MockAgentHostService extends mock() { private readonly _onDidNotification = new Emitter(); override readonly onDidNotification = this._onDidNotification.event; - public dispatchedActions: (SessionAction | TerminalAction | IRootConfigChangedAction)[] = []; + public dispatchedActions: { channel: string; action: SessionAction | TerminalAction | IRootConfigChangedAction }[] = []; - override dispatch(action: SessionAction | TerminalAction | IRootConfigChangedAction): void { - this.dispatchedActions.push(action); + override dispatch(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): void { + this.dispatchedActions.push({ channel, action }); } private _rootStateValue: RootState | undefined = undefined; @@ -241,7 +241,7 @@ suite('AgentHostTerminalContribution', () => { // The host-start fire from setRootState's onDidChange listener should // have produced exactly one dispatch with the resolved path. assert.strictEqual(agentHostService.dispatchedActions.length, 1); - const action = agentHostService.dispatchedActions[0]; + const action = agentHostService.dispatchedActions[0].action; assert.strictEqual(action.type, ActionType.RootConfigChanged); assert.deepStrictEqual((action as IRootConfigChangedAction).config, { [AgentHostConfigKey.DefaultShell]: '/usr/bin/bash', @@ -285,7 +285,7 @@ suite('AgentHostTerminalContribution', () => { await flush(); assert.strictEqual(agentHostService.dispatchedActions.length, initialCount + 1); - const last = agentHostService.dispatchedActions[agentHostService.dispatchedActions.length - 1]; + const last = agentHostService.dispatchedActions[agentHostService.dispatchedActions.length - 1].action; assert.deepStrictEqual((last as IRootConfigChangedAction).config, { [AgentHostConfigKey.DefaultShell]: '/usr/bin/pwsh', }); @@ -341,7 +341,7 @@ suite('AgentHostTerminalContribution', () => { await flush(); assert.strictEqual(agentHostService.dispatchedActions.length, 1); - assert.deepStrictEqual((agentHostService.dispatchedActions[0] as IRootConfigChangedAction).config, { + assert.deepStrictEqual((agentHostService.dispatchedActions[0].action as IRootConfigChangedAction).config, { [AgentHostConfigKey.DisableCustomTerminalTool]: true, }); }); @@ -353,7 +353,7 @@ suite('AgentHostTerminalContribution', () => { await flush(); assert.strictEqual(agentHostService.dispatchedActions.length, 1); - assert.deepStrictEqual((agentHostService.dispatchedActions[0] as IRootConfigChangedAction).config, { + assert.deepStrictEqual((agentHostService.dispatchedActions[0].action as IRootConfigChangedAction).config, { [AgentHostConfigKey.DisableCustomTerminalTool]: false, }); }); @@ -364,7 +364,7 @@ suite('AgentHostTerminalContribution', () => { rootState.config!.values[AgentHostConfigKey.DisableCustomTerminalTool] = false; agentHostService.setRootState(rootState); await flush(); - assert.deepStrictEqual(agentHostService.dispatchedActions, []); + assert.deepStrictEqual(agentHostService.dispatchedActions as readonly unknown[], []); configurationService.setUserConfiguration(AgentHostCustomTerminalToolEnabledSettingId, false); configurationService.onDidChangeConfigurationEmitter.fire({ @@ -375,8 +375,9 @@ suite('AgentHostTerminalContribution', () => { }); assert.strictEqual(agentHostService.dispatchedActions.length, 1); - assert.deepStrictEqual((agentHostService.dispatchedActions[0] as IRootConfigChangedAction).config, { + assert.deepStrictEqual((agentHostService.dispatchedActions[0].action as IRootConfigChangedAction).config, { [AgentHostConfigKey.DisableCustomTerminalTool]: true, }); }); }); + diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostUntitledProvisionalSessionService.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostUntitledProvisionalSessionService.test.ts index 974c5473ace..c740e1f412b 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostUntitledProvisionalSessionService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostUntitledProvisionalSessionService.test.ts @@ -25,8 +25,8 @@ import { AgentHostUntitledProvisionalSessionService, IAgentHostUntitledProvision // ---- Mocks ----------------------------------------------------------------- interface IDispatchedAction { + readonly channel: string; readonly type: string; - readonly session: string; readonly config: Record; } @@ -53,8 +53,8 @@ class MockAgentHostService extends mock() { this.disposed.push(session); } - override dispatch(action: Parameters[0]): void { - this.dispatched.push(action as IDispatchedAction); + override dispatch(channel: Parameters[0], action: Parameters[1]): void { + this.dispatched.push({ channel, ...action } as IDispatchedAction); } override async resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise { @@ -156,7 +156,7 @@ suite('AgentHostUntitledProvisionalSessionService', () => { assert.strictEqual(agentHost.dispatched.length, 1, 'dispatched before re-resolve await'); assert.strictEqual(agentHost.dispatched[0].type, ActionType.SessionConfigChanged); assert.deepStrictEqual(agentHost.dispatched[0].config, { isolation: 'worktree' }); - assert.strictEqual(agentHost.dispatched[0].session, expectedBackendUri('b').toString()); + assert.strictEqual(agentHost.dispatched[0].channel, expectedBackendUri('b').toString()); // Unblock so the queued re-resolve completes and the outer promise settles. blocked.complete({ schema: makeSchema(false), values: { isolation: 'worktree' } }); diff --git a/src/vs/workbench/contrib/terminal/browser/agentHostPty.ts b/src/vs/workbench/contrib/terminal/browser/agentHostPty.ts index 0daf8a902e1..261c643d267 100644 --- a/src/vs/workbench/contrib/terminal/browser/agentHostPty.ts +++ b/src/vs/workbench/contrib/terminal/browser/agentHostPty.ts @@ -133,7 +133,7 @@ export class AgentHostPty extends BasePty implements ITerminalChildProcess { // where the terminal already exists, e.g. created by a tool) if (!this._options?.attachOnly) { await this._connection.createTerminal({ - terminal: this._terminalUri.toString(), + channel: this._terminalUri.toString(), claim: { kind: TerminalClaimKind.Client, clientId: this._connection.clientId }, name: this._options?.name, cwd: this._resolveCwdForProtocol(this._options?.cwd), @@ -306,7 +306,8 @@ export class AgentHostPty extends BasePty implements ITerminalChildProcess { } this._startBarrier.wait().then(() => { this._connection.dispatch( - { type: ActionType.TerminalInput, terminal: this._terminalUri.toString(), data }, + this._terminalUri.toString(), + { type: ActionType.TerminalInput, data }, ); }); } @@ -319,7 +320,8 @@ export class AgentHostPty extends BasePty implements ITerminalChildProcess { this._lastDimensions.rows = rows; this._startBarrier.wait().then(() => { this._connection.dispatch( - { type: ActionType.TerminalResized, terminal: this._terminalUri.toString(), cols, rows }, + this._terminalUri.toString(), + { type: ActionType.TerminalResized, cols, rows }, ); }); } @@ -349,7 +351,8 @@ export class AgentHostPty extends BasePty implements ITerminalChildProcess { async clearBuffer(): Promise { // Send a clear action to the agent host this._connection.dispatch( - { type: ActionType.TerminalCleared, terminal: this._terminalUri.toString() }, + this._terminalUri.toString(), + { type: ActionType.TerminalCleared }, ); } diff --git a/src/vs/workbench/contrib/terminal/test/browser/agentHostPty.test.ts b/src/vs/workbench/contrib/terminal/test/browser/agentHostPty.test.ts index d7571d3396e..d1d619e8107 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/agentHostPty.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/agentHostPty.test.ts @@ -18,8 +18,6 @@ import type { ResourceCopyParams, ResourceCopyResult, ResourceDeleteParams, Reso import { AgentHostPty } from '../../browser/agentHostPty.js'; import { IAgentSubscription } from '../../../../../platform/agentHost/common/state/agentSubscription.js'; import { StateComponents } from '../../../../../platform/agentHost/common/state/sessionState.js'; -import { hasKey } from '../../../../../base/common/types.js'; - // ---- Mock IAgentConnection -------------------------------------------------- class MockAgentConnection implements IAgentConnection { @@ -32,15 +30,13 @@ class MockAgentConnection implements IAgentConnection { private readonly _onDidNotification = new Emitter(); readonly onDidNotification: Event = this._onDidNotification.event; - readonly dispatchedActions: (SessionAction | TerminalAction | IRootConfigChangedAction)[] = []; + readonly dispatchedActions: { channel: string; action: SessionAction | TerminalAction | IRootConfigChangedAction }[] = []; readonly createdTerminals: CreateTerminalParams[] = []; readonly disposedTerminals: URI[] = []; readonly subscribedResources: URI[] = []; private _terminalState: TerminalState = { - title: 'Test Terminal', - content: [], - claim: { kind: TerminalClaimKind.Client, clientId: 'test-client' }, + title: 'Test Terminal', content: [], claim: { kind: TerminalClaimKind.Client, clientId: 'test-client' }, }; constructor(initialState?: Partial) { @@ -62,8 +58,8 @@ class MockAgentConnection implements IAgentConnection { } /** Simulate the server sending an action to the client */ - fireAction(action: StateAction, serverSeq = 1): void { - this._onDidAction.fire({ action, serverSeq, origin: { clientId: 'server', clientSeq: 0 } }); + fireAction(channel: URI, action: StateAction, serverSeq = 1): void { + this._onDidAction.fire({ channel: channel.toString(), action, serverSeq, origin: { clientId: 'server', clientSeq: 0 } }); } // ---- Unused IAgentService methods (stubs) ----- @@ -85,40 +81,31 @@ class MockAgentConnection implements IAgentConnection { // ---- IAgentConnection new API (stubs for tests) ----- readonly rootState: IAgentSubscription = { - value: undefined, - verifiedValue: undefined, - onDidChange: Event.None, - onWillApplyAction: Event.None, - onDidApplyAction: Event.None, + value: undefined, verifiedValue: undefined, onDidChange: Event.None, onWillApplyAction: Event.None, onDidApplyAction: Event.None, }; getSubscription(_kind: StateComponents, _resource: URI): IReference> { const onDidChange = new Emitter(); const onWillApplyAction = new Emitter(); const onDidApplyAction = new Emitter(); const sub: IAgentSubscription = { - value: this._terminalState, - verifiedValue: this._terminalState, - onDidChange: onDidChange.event, - onWillApplyAction: onWillApplyAction.event, - onDidApplyAction: onDidApplyAction.event, + value: this._terminalState, verifiedValue: this._terminalState, onDidChange: onDidChange.event, onWillApplyAction: onWillApplyAction.event, onDidApplyAction: onDidApplyAction.event, }; // Wire onDidAction to the subscription's events const listener = this._onDidAction.event(envelope => { - if (hasKey(envelope.action, { terminal: true }) && (envelope.action as { terminal: string }).terminal === _resource.toString()) { + if (envelope.channel === _resource.toString()) { onWillApplyAction.fire(envelope); onDidApplyAction.fire(envelope); } }); return { - object: sub as IAgentSubscription, - dispose: () => { listener.dispose(); onDidChange.dispose(); onWillApplyAction.dispose(); onDidApplyAction.dispose(); }, + object: sub as IAgentSubscription, dispose: () => { listener.dispose(); onDidChange.dispose(); onWillApplyAction.dispose(); onDidApplyAction.dispose(); }, }; } getSubscriptionUnmanaged(_kind: StateComponents, _resource: URI): IAgentSubscription | undefined { return undefined; } - dispatch(action: SessionAction | TerminalAction | IRootConfigChangedAction): void { - this.dispatchedActions.push(action); + dispatch(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): void { + this.dispatchedActions.push({ channel, action }); } dispose(): void { @@ -153,7 +140,7 @@ suite('AgentHostPty', () => { assert.strictEqual(result, undefined, 'start() should succeed'); assert.strictEqual(conn.createdTerminals.length, 1); - assert.strictEqual(conn.createdTerminals[0].terminal, terminalUri.toString()); + assert.strictEqual(conn.createdTerminals[0].channel, terminalUri.toString()); assert.strictEqual(conn.createdTerminals[0].name, 'test'); assert.deepStrictEqual(conn.createdTerminals[0].claim, { kind: TerminalClaimKind.Client, clientId: 'test-client' }); }); @@ -195,9 +182,9 @@ suite('AgentHostPty', () => { // Wait for the async barrier await new Promise(resolve => setTimeout(resolve, 10)); - const inputActions = conn.dispatchedActions.filter(a => a.type === ActionType.TerminalInput); + const inputActions = conn.dispatchedActions.filter(a => a.action.type === ActionType.TerminalInput); assert.strictEqual(inputActions.length, 1); - assert.strictEqual((inputActions[0] as { data: string }).data, 'hello'); + assert.strictEqual((inputActions[0].action as { data: string }).data, 'hello'); }); test('resize() dispatches terminal/resized action', async () => { @@ -210,10 +197,10 @@ suite('AgentHostPty', () => { await new Promise(resolve => setTimeout(resolve, 10)); - const resizeActions = conn.dispatchedActions.filter(a => a.type === ActionType.TerminalResized); + const resizeActions = conn.dispatchedActions.filter(a => a.action.type === ActionType.TerminalResized); assert.strictEqual(resizeActions.length, 1); - assert.strictEqual((resizeActions[0] as { cols: number; rows: number }).cols, 120); - assert.strictEqual((resizeActions[0] as { cols: number; rows: number }).rows, 40); + assert.strictEqual((resizeActions[0].action as { cols: number; rows: number }).cols, 120); + assert.strictEqual((resizeActions[0].action as { cols: number; rows: number }).rows, 40); }); test('resize() skips duplicate dimensions', async () => { @@ -227,7 +214,7 @@ suite('AgentHostPty', () => { await new Promise(resolve => setTimeout(resolve, 10)); - const resizeActions = conn.dispatchedActions.filter(a => a.type === ActionType.TerminalResized); + const resizeActions = conn.dispatchedActions.filter(a => a.action.type === ActionType.TerminalResized); assert.strictEqual(resizeActions.length, 1); }); @@ -242,7 +229,7 @@ suite('AgentHostPty', () => { })); await pty.start(); - conn.fireAction({ type: ActionType.TerminalData, terminal: terminalUri.toString(), data: 'hello world\r\n' }); + conn.fireAction(terminalUri, { type: ActionType.TerminalData, data: 'hello world\r\n' }); assert.deepStrictEqual(dataReceived, ['existing output\n' /* skip replay since content is '' */, 'hello world\r\n'].filter(x => x !== 'existing output\n')); // Since initial content is empty, only the streamed data should be received @@ -258,7 +245,7 @@ suite('AgentHostPty', () => { disposables.add(pty.onProcessExit!(e => { exitCode = e; })); await pty.start(); - conn.fireAction({ type: ActionType.TerminalExited, terminal: terminalUri.toString(), exitCode: 42 }); + conn.fireAction(terminalUri, { type: ActionType.TerminalExited, exitCode: 42 }); assert.strictEqual(exitCode, 42); }); @@ -269,7 +256,7 @@ suite('AgentHostPty', () => { const pty = disposables.add(new AgentHostPty(1, conn, terminalUri)); await pty.start(); - conn.fireAction({ type: ActionType.TerminalCwdChanged, terminal: terminalUri.toString(), cwd: '/home/user/project' }); + conn.fireAction(terminalUri, { type: ActionType.TerminalCwdChanged, cwd: '/home/user/project' }); const cwd = await pty.getCwd(); assert.strictEqual(cwd, '/home/user/project'); @@ -288,7 +275,7 @@ suite('AgentHostPty', () => { })); await pty.start(); - conn.fireAction({ type: ActionType.TerminalTitleChanged, terminal: terminalUri.toString(), title: 'npm test' }); + conn.fireAction(terminalUri, { type: ActionType.TerminalTitleChanged, title: 'npm test' }); assert.strictEqual(changedTitle, 'npm test'); }); @@ -304,7 +291,7 @@ suite('AgentHostPty', () => { })); await pty.start(); - conn.fireAction({ type: ActionType.TerminalData, terminal: 'agenthost-terminal:///other', data: 'should not appear' }); + conn.fireAction(URI.parse('agenthost-terminal:///other'), { type: ActionType.TerminalData, data: 'should not appear' }); assert.deepStrictEqual(dataReceived, []); }); @@ -353,9 +340,7 @@ suite('AgentHostPty', () => { // Create a new connection with different content (simulating server-side changes during disconnect) const conn2 = new MockAgentConnection({ - content: [{ type: 'unclassified', value: 'old output\nnew output after reconnect\n' }], - cwd: '/home/reconnected', - title: 'Reconnected Terminal', + content: [{ type: 'unclassified', value: 'old output\nnew output after reconnect\n' }], cwd: '/home/reconnected', title: 'Reconnected Terminal', }); disposables.add(conn2); @@ -393,12 +378,12 @@ suite('AgentHostPty', () => { dataReceived.length = 0; // clear replay data // New actions from conn2 should be received - conn2.fireAction({ type: ActionType.TerminalData, terminal: terminalUri.toString(), data: 'post-reconnect data' }); + conn2.fireAction(terminalUri, { type: ActionType.TerminalData, data: 'post-reconnect data' }); assert.deepStrictEqual(dataReceived, ['post-reconnect data']); // Old connection actions should NOT be received - conn1.fireAction({ type: ActionType.TerminalData, terminal: terminalUri.toString(), data: 'stale data' }); + conn1.fireAction(terminalUri, { type: ActionType.TerminalData, data: 'stale data' }); assert.deepStrictEqual(dataReceived, ['post-reconnect data']); }); @@ -419,14 +404,10 @@ suite('AgentHostPty', () => { disposables.add(onDidApplyAction); const sub: IAgentSubscription = { value: undefined, // never hydrated - verifiedValue: undefined, - onDidChange: onDidChange.event, - onWillApplyAction: Event.None, - onDidApplyAction: onDidApplyAction.event, + verifiedValue: undefined, onDidChange: onDidChange.event, onWillApplyAction: Event.None, onDidApplyAction: onDidApplyAction.event, }; return { - object: sub as IAgentSubscription, - dispose: () => { onDidChange.dispose(); onDidApplyAction.dispose(); }, + object: sub as IAgentSubscription, dispose: () => { onDidChange.dispose(); onDidApplyAction.dispose(); }, }; }; @@ -454,12 +435,12 @@ suite('AgentHostPty', () => { pty.input('after reconnect'); await new Promise(resolve => setTimeout(resolve, 10)); - const inputActions = conn2.dispatchedActions.filter(a => a.type === ActionType.TerminalInput); + const inputActions = conn2.dispatchedActions.filter(a => a.action.type === ActionType.TerminalInput); assert.strictEqual(inputActions.length, 1); - assert.strictEqual((inputActions[0] as { data: string }).data, 'after reconnect'); + assert.strictEqual((inputActions[0].action as { data: string }).data, 'after reconnect'); // conn1 should not have received the input - const oldInputActions = conn1.dispatchedActions.filter(a => a.type === ActionType.TerminalInput); + const oldInputActions = conn1.dispatchedActions.filter(a => a.action.type === ActionType.TerminalInput); assert.strictEqual(oldInputActions.length, 0); }); }); diff --git a/src/vs/workbench/services/agentHost/browser/editorRemoteAgentHostServiceClient.ts b/src/vs/workbench/services/agentHost/browser/editorRemoteAgentHostServiceClient.ts index 4b38e833eee..2da8c4c7182 100644 --- a/src/vs/workbench/services/agentHost/browser/editorRemoteAgentHostServiceClient.ts +++ b/src/vs/workbench/services/agentHost/browser/editorRemoteAgentHostServiceClient.ts @@ -164,8 +164,8 @@ export class EditorRemoteAgentHostServiceClient extends Disposable implements IA return this._protocolClient?.getSubscriptionUnmanaged(kind, resource); } - dispatch(action: SessionAction | TerminalAction | IRootConfigChangedAction): void { - this._protocolClient?.dispatch(action); + dispatch(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): void { + this._protocolClient?.dispatch(channel, action); } authenticate(params: AuthenticateParams): Promise { diff --git a/src/vs/workbench/services/agentHost/test/common/agentHostPermissionService.test.ts b/src/vs/workbench/services/agentHost/test/common/agentHostPermissionService.test.ts index 5935e4b2817..4062127dbf9 100644 --- a/src/vs/workbench/services/agentHost/test/common/agentHostPermissionService.test.ts +++ b/src/vs/workbench/services/agentHost/test/common/agentHostPermissionService.test.ts @@ -178,14 +178,14 @@ suite('AgentHostPermissionService', () => { test('request resolves immediately when already granted', async () => { const { service } = createService(); disposables.add(service.grantImplicitRead('host', URI.file('/plugins/foo'))); - await service.request('host', { uri: URI.file('/plugins/foo/x.md').toString(), read: true }); + await service.request('host', { channel: 'ahp-root://', uri: URI.file('/plugins/foo/x.md').toString(), read: true }); assert.strictEqual(service.allPending.get().length, 0); }); test('allow grants in-memory until connection closes', async () => { const { service, config } = createService(); const uri = URI.file('/etc/foo'); - const promise = service.request('host', { uri: uri.toString(), read: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: uri.toString(), read: true }); // Wait for canonicalization + enqueue. await new Promise(resolve => setTimeout(resolve, 0)); @@ -207,7 +207,7 @@ suite('AgentHostPermissionService', () => { test('allow for write also covers read on the same URI', async () => { const { service } = createService(); - const promise = service.request('host', { uri: URI.file('/etc/foo').toString(), write: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), write: true }); await new Promise(resolve => setTimeout(resolve, 0)); const pending = service.allPending.get(); @@ -224,7 +224,7 @@ suite('AgentHostPermissionService', () => { test('allow for read does not grant write', async () => { const { service } = createService(); - const promise = service.request('host', { uri: URI.file('/etc/foo').toString(), read: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), read: true }); await new Promise(resolve => setTimeout(resolve, 0)); service.allPending.get()[0].allow(); @@ -236,7 +236,7 @@ suite('AgentHostPermissionService', () => { test('request rejects with CancellationError on deny', async () => { const { service } = createService(); - const promise = service.request('host', { uri: URI.file('/etc/foo').toString(), read: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), read: true }); await new Promise(resolve => setTimeout(resolve, 0)); service.allPending.get()[0].deny(); await assert.rejects(promise, (err: unknown) => err instanceof CancellationError); @@ -244,7 +244,7 @@ suite('AgentHostPermissionService', () => { test('allowAlways persists the grant', async () => { const { service, config } = createService(); - const promise = service.request('host', { uri: URI.file('/etc/foo').toString(), read: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), read: true }); await new Promise(resolve => setTimeout(resolve, 0)); service.allPending.get()[0].allowAlways(); await promise; @@ -265,7 +265,7 @@ suite('AgentHostPermissionService', () => { const service = disposables.add(new AgentHostPermissionService(config, createStubFileService(), new NullLogService())); const uri = URI.file('/etc/foo'); - const promise = service.request('host', { uri: uri.toString(), read: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: uri.toString(), read: true }); await new Promise(resolve => setTimeout(resolve, 0)); service.allPending.get()[0].allowAlways(); await promise; @@ -276,7 +276,7 @@ suite('AgentHostPermissionService', () => { test('allowAlways for write persists rw', async () => { const { service, config } = createService(); - const promise = service.request('host', { uri: URI.file('/etc/foo').toString(), write: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), write: true }); await new Promise(resolve => setTimeout(resolve, 0)); service.allPending.get()[0].allowAlways(); await promise; @@ -292,14 +292,14 @@ suite('AgentHostPermissionService', () => { }, }); // Already covered by parent — request resolves without prompting. - await service.request('host', { uri: URI.file('/etc/foo').toString(), read: true }); + await service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), read: true }); assert.strictEqual(config.lastUpdate, undefined); }); test('concurrent identical requests share one pending entry', async () => { const { service } = createService(); - const a = service.request('host', { uri: URI.file('/etc/foo').toString(), read: true }); - const b = service.request('host', { uri: URI.file('/etc/foo').toString(), read: true }); + const a = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), read: true }); + const b = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), read: true }); await new Promise(resolve => setTimeout(resolve, 0)); assert.strictEqual(service.allPending.get().length, 1); @@ -313,7 +313,7 @@ suite('AgentHostPermissionService', () => { [URI.file('/etc/foo').toString()]: AgentHostAccessMode.Read, }, }); - const promise = service.request('host', { uri: URI.file('/etc/foo').toString(), write: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), write: true }); await new Promise(resolve => setTimeout(resolve, 0)); assert.strictEqual(service.allPending.get().length, 1); assert.strictEqual(service.allPending.get()[0].mode, AgentHostPermissionMode.Write); @@ -324,7 +324,7 @@ suite('AgentHostPermissionService', () => { test('connectionClosed rejects pending and clears the queue', async () => { const { service } = createService(); - const promise = service.request('host', { uri: URI.file('/etc/foo').toString(), read: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), read: true }); await new Promise(resolve => setTimeout(resolve, 0)); service.connectionClosed('host'); await assert.rejects(promise, (err: unknown) => err instanceof CancellationError); @@ -351,7 +351,7 @@ suite('AgentHostPermissionService', () => { test('findPending returns the pending request by id', async () => { const { service } = createService(); - const promise = service.request('host', { uri: URI.file('/etc/foo').toString(), read: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), read: true }); await new Promise(resolve => setTimeout(resolve, 0)); const [pending] = service.allPending.get(); assert.strictEqual(service.findPending(pending.id), pending); @@ -382,8 +382,8 @@ suite('AgentHostPermissionService', () => { disposables.add(service.grantImplicitRead('host-a', URI.file('/plugins/a'))); disposables.add(service.grantImplicitRead('host-b', URI.file('/plugins/b'))); - const pendingA = service.request('host-a', { uri: URI.file('/etc/a').toString(), read: true }); - const pendingB = service.request('host-b', { uri: URI.file('/etc/b').toString(), read: true }); + const pendingA = service.request('host-a', { channel: 'ahp-root://', uri: URI.file('/etc/a').toString(), read: true }); + const pendingB = service.request('host-b', { channel: 'ahp-root://', uri: URI.file('/etc/b').toString(), read: true }); await new Promise(resolve => setTimeout(resolve, 0)); service.connectionClosed('host-a'); @@ -402,8 +402,8 @@ suite('AgentHostPermissionService', () => { test('pendingFor returns only this host\'s requests, with normalized address', async () => { const { service } = createService(); - const a = service.request('host-a', { uri: URI.file('/etc/a').toString(), read: true }); - const b = service.request('ws://host-b', { uri: URI.file('/etc/b').toString(), read: true }); + const a = service.request('host-a', { channel: 'ahp-root://', uri: URI.file('/etc/a').toString(), read: true }); + const b = service.request('ws://host-b', { channel: 'ahp-root://', uri: URI.file('/etc/b').toString(), read: true }); await new Promise(resolve => setTimeout(resolve, 0)); assert.strictEqual(service.pendingFor('host-a').get().length, 1); @@ -423,7 +423,7 @@ suite('AgentHostPermissionService', () => { // for the smaller scope first lets the user decline the dangerous // part without ever seeing it. const { service } = createService(); - const promise = service.request('host', { uri: URI.file('/etc/foo').toString(), read: true, write: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), read: true, write: true }); await new Promise(resolve => setTimeout(resolve, 0)); let pending = service.allPending.get(); @@ -462,7 +462,7 @@ suite('AgentHostPermissionService', () => { test('allowAlways defaults to APPLICATION scope when no value is configured anywhere', async () => { const { service, config } = createService(); - const promise = service.request('host', { uri: URI.file('/etc/foo').toString(), read: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), read: true }); await new Promise(resolve => setTimeout(resolve, 0)); service.allPending.get()[0].allowAlways(); await promise; @@ -496,7 +496,7 @@ suite('AgentHostPermissionService', () => { }; const service = disposables.add(new AgentHostPermissionService(config, createStubFileService(), new NullLogService())); - const promise = service.request('host', { uri: URI.file('/etc/foo').toString(), read: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), read: true }); await new Promise(resolve => setTimeout(resolve, 0)); service.allPending.get()[0].allowAlways(); await promise; @@ -527,7 +527,7 @@ suite('AgentHostPermissionService', () => { }; const service = disposables.add(new AgentHostPermissionService(config, createStubFileService(), new NullLogService())); - const promise = service.request('host', { uri: URI.file('/etc/foo').toString(), read: true }); + const promise = service.request('host', { channel: 'ahp-root://', uri: URI.file('/etc/foo').toString(), read: true }); await new Promise(resolve => setTimeout(resolve, 0)); service.allPending.get()[0].allowAlways(); await promise;