Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ export function isAgentHostEnabled(configurationService: IConfigurationService):
return !isWeb && !!configurationService.getValue<boolean>(AgentHostEnabledSettingId);
}

/** Configuration key that controls whether per-host IPC traffic output channels are created. */
export const AgentHostIpcLoggingSettingId = 'chat.agentHost.ipcLoggingEnabled';

/** Configuration key that controls whether AHP JSONL logs are written for agent host transports. */
export const AgentHostAhpJsonlLoggingSettingId = 'chat.agentHost.ahpJsonlLoggingEnabled';
Comment on lines 51 to 52

Expand Down
4 changes: 4 additions & 0 deletions src/vs/platform/agentHost/common/remoteAgentHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ export function getEntryAddress(entry: IRemoteAgentHostEntry): string {
}
}

export function remoteAgentHostLogOutputChannelId(address: string): string {
return `agentHost.otlp.${address}`;
}

export const enum RemoteAgentHostInputValidationError {
Empty = 'empty',
Invalid = 'invalid',
Expand Down
2 changes: 0 additions & 2 deletions src/vs/sessions/common/agentHostSessionsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ export interface IAgentHostSessionsProvider extends ISessionsProvider {
readonly connectionStatus?: IObservable<RemoteAgentHostConnectionStatus>;
/** Remote address string, present on remote providers. */
readonly remoteAddress?: string;
/** Output channel ID for remote provider logs. */
outputChannelId?: string;
/**
* Establish (or re-establish) the connection for this host on demand.
* Tears down any existing connection first. Present on remote providers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,11 @@ Decoupling these allows copilot sessions from different providers (local CLI, re

## Connection Management

- `setConnection(connection, defaultDirectory?)` — Wires a live agent host connection; dynamically discovers session types from the host's root state agents
- `setConnection(connection, defaultDirectory?)` — Wires a live agent host connection directly; dynamically discovers session types from the host's root state agents
- `clearConnection()` — Clears the connection when the host disconnects
- Handles session notifications (`notify/sessionAdded`, `notify/sessionRemoved`) and state changes
- Fires `onDidChangeSessionTypes` when the host's agent list changes
- Remote-host management options do not expose an IPC output channel; remote diagnostics use the host's forwarded logs when available.
- SSH connection progress notifications are closed when the connect promise settles; keyboard-interactive prompt cancellation rejects the connect promise as cancellation and does not show an error notification.
- SSH config host connections use resolved `IdentityFile` and `IdentityAgent` values from `ssh -G`; encrypted private keys are prompted for a passphrase through the same quick-input bridge as keyboard-interactive auth.
- Startup SSH auto-reconnect treats keyboard-interactive cancellation as an intentional pause and does not schedule another reconnect attempt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { authenticateProtectedResources, AgentHostAuthTokenCache, resolveAuthent
import { AgentHostLanguageModelProvider } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLanguageModelProvider.js';
import { AgentHostSessionHandler } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.js';
import { IAgentHostActiveClientService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostActiveClientService.js';
import { LoggingAgentConnection } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.js';
import { IChatSessionsService } from '../../../../../workbench/contrib/chat/common/chatSessionsService.js';
import { ICustomizationHarnessService } from '../../../../../workbench/contrib/chat/common/customizationHarnessService.js';
import { ILanguageModelsService } from '../../../../../workbench/contrib/chat/common/languageModels.js';
Expand Down Expand Up @@ -163,19 +162,14 @@ class ConnectionState extends Disposable {
readonly store = this._register(new DisposableStore());
readonly agents = this._register(new DisposableMap<AgentProvider, DisposableStore>());
readonly modelProviders = new Map<AgentProvider, AgentHostLanguageModelProvider>();
readonly loggedConnection: LoggingAgentConnection;
/** Dedupes redundant `authenticate` RPCs when the resolved token hasn't changed. */
readonly authTokenCache = new AgentHostAuthTokenCache();

constructor(
readonly name: string | undefined,
connection: IAgentConnection,
channelId: string,
channelLabel: string,
@IInstantiationService instantiationService: IInstantiationService,
readonly connection: IAgentConnection,
) {
super();
this.loggedConnection = this._register(instantiationService.createInstance(LoggingAgentConnection, connection, channelId, channelLabel));
}
}

Expand Down Expand Up @@ -267,17 +261,14 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
this._reconcileConnections();
this._reconnectSSHEntries();

// Ensure every live connection is wired to its provider.
// This covers the case where a provider was recreated (e.g. name
// change) while a connection for that address already existed —
// we need to re-expose both the connection and the output channel,
// otherwise `Show Output` on the recreated provider would break.
// Ensure every live connection is wired to its provider. This covers
// the case where a provider was recreated (e.g. name change) while a
// connection for that address already existed.
for (const [address, connState] of this._connections) {
const connectionInfo = this._remoteAgentHostService.connections.find(c => c.address === address);
const provider = this._providerInstances.get(address);
if (provider) {
provider.setConnection(connState.loggedConnection, connectionInfo?.defaultDirectory);
provider.setOutputChannelId(connState.loggedConnection.channelId);
provider.setConnection(connState.connection, connectionInfo?.defaultDirectory);
}
}

Expand Down Expand Up @@ -591,12 +582,12 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
const existing = this._connections.get(connectionInfo.address);
if (existing) {
const nameChanged = existing.name !== connectionInfo.name;
const clientIdChanged = existing.loggedConnection.clientId !== connectionInfo.clientId;
const clientIdChanged = existing.connection.clientId !== connectionInfo.clientId;

// If the name or clientId changed, tear down and re-register
if (nameChanged || clientIdChanged) {
this._logService.info(`[RemoteAgentHost] Reconnecting contribution for ${connectionInfo.address}: oldClientId=${existing.loggedConnection.clientId}, newClientId=${connectionInfo.clientId}, nameChanged=${nameChanged}`);
const oldClientId = existing.loggedConnection.clientId;
this._logService.info(`[RemoteAgentHost] Reconnecting contribution for ${connectionInfo.address}: oldClientId=${existing.connection.clientId}, newClientId=${connectionInfo.clientId}, nameChanged=${nameChanged}`);
const oldClientId = existing.connection.clientId;
this._connections.deleteAndDispose(connectionInfo.address);
this._setupConnection(connectionInfo);

Expand Down Expand Up @@ -632,9 +623,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
}

const { address, name } = connectionInfo;
const channelLabel = `Agent Host IPC (${name || address})`;
const connState = this._instantiationService.createInstance(ConnectionState, name, connection, `agenthost.${connection.clientId}`, channelLabel);
const loggedConnection = connState.loggedConnection;
const connState = this._instantiationService.createInstance(ConnectionState, name, connection);
this._connections.set(address, connState);
const store = connState.store;

Expand All @@ -657,26 +646,24 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
store.add(this._agentHostFileSystemService.registerAuthority(authority, connection));

// React to root state changes (agent discovery)
store.add(loggedConnection.rootState.onDidChange(rootState => {
this._handleRootStateChange(address, loggedConnection, rootState);
store.add(connection.rootState.onDidChange(rootState => {
this._handleRootStateChange(address, connection, rootState);
}));

// If root state is already available, process it immediately
const initialRootState = loggedConnection.rootState.value;
const initialRootState = connection.rootState.value;
if (initialRootState && !(initialRootState instanceof Error)) {
this._handleRootStateChange(address, loggedConnection, initialRootState);
this._handleRootStateChange(address, connection, initialRootState);
}

// Wire connection to existing sessions provider
const provider = this._providerInstances.get(address);
if (provider) {
provider.setConnection(loggedConnection, connectionInfo.defaultDirectory);
// Expose the output channel ID so the workspace picker can offer "Show Output"
provider.setOutputChannelId(loggedConnection.channelId);
provider.setConnection(connection, connectionInfo.defaultDirectory);
}
}

private _handleRootStateChange(address: string, loggedConnection: LoggingAgentConnection, rootState: RootState): void {
private _handleRootStateChange(address: string, connection: IAgentConnection, rootState: RootState): void {
const connState = this._connections.get(address);
if (!connState) {
return;
Expand All @@ -693,21 +680,21 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
}

// Authenticate using protectedResources from agent info
this._authenticateWithConnection(address, loggedConnection, rootState.agents)
this._authenticateWithConnection(address, connection, rootState.agents)
.catch(() => { /* best-effort */ });

// Register new agents, push model updates to existing ones
for (const agent of rootState.agents) {
if (!connState.agents.has(agent.provider)) {
this._registerAgent(address, loggedConnection, agent, connState.name);
this._registerAgent(address, connection, agent, connState.name);
} else {
const modelProvider = connState.modelProviders.get(agent.provider);
modelProvider?.updateModels(agent.models);
}
}
}

private _registerAgent(address: string, loggedConnection: LoggingAgentConnection, agent: AgentInfo, configuredName: string | undefined): void {
private _registerAgent(address: string, connection: IAgentConnection, agent: AgentInfo, configuredName: string | undefined): void {
const connState = this._connections.get(address);
if (!connState) {
return;
Expand Down Expand Up @@ -776,7 +763,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
const pluginController = agentStore.add(this._instantiationService.createInstance(RemoteAgentPluginController,
hostLabel,
sanitized,
loggedConnection,
connection,
));
const itemProvider = agentStore.add(this._instantiationService.createInstance(AgentCustomizationItemProvider,
sanitized,
Expand Down Expand Up @@ -808,13 +795,13 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
sessionType,
fullName: displayName,
description: agent.description,
connection: loggedConnection,
connection,
connectionAuthority: sanitized,
extensionId: 'vscode.remote-agent-host',
extensionDisplayName: 'Remote Agent Host',
resolveWorkingDirectory,
isNewSession,
resolveAuthentication: (resources) => this._resolveAuthenticationInteractively(address, loggedConnection, resources),
resolveAuthentication: (resources) => this._resolveAuthenticationInteractively(address, connection, resources),
}));
agentStore.add(this._chatSessionsService.registerChatSessionContentProvider(sessionType, sessionHandler));

Expand All @@ -835,9 +822,9 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc

private _authenticateAllConnections(): void {
for (const [address, connState] of this._connections) {
const rootState = connState.loggedConnection.rootState.value;
const rootState = connState.connection.rootState.value;
if (rootState && !(rootState instanceof Error)) {
this._authenticateWithConnection(address, connState.loggedConnection, rootState.agents).catch(() => { /* best-effort */ });
this._authenticateWithConnection(address, connState.connection, rootState.agents).catch(() => { /* best-effort */ });
}
}
}
Expand All @@ -849,7 +836,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
* Marks the matching provider's `authenticationPending` observable while
* the auth pass is in flight so that sessions surface as still loading.
*/
private async _authenticateWithConnection(address: string, loggedConnection: LoggingAgentConnection, agents: readonly AgentInfo[]): Promise<void> {
private async _authenticateWithConnection(address: string, connection: IAgentConnection, agents: readonly AgentInfo[]): Promise<void> {
const providerId = `agenthost-${agentHostAuthority(address)}`;
const provider = this._sessionsProvidersService.getProvider<RemoteAgentHostSessionsProvider>(providerId);
const authTokenCache = this._connections.get(address)?.authTokenCache;
Expand All @@ -860,11 +847,10 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
authenticationService: this._authenticationService,
logPrefix: '[RemoteAgentHost]',
logService: this._logService,
authenticate: request => loggedConnection.authenticate(request),
authenticate: request => connection.authenticate(request),
});
} catch (err) {
this._logService.error('[RemoteAgentHost] Failed to authenticate with connection', err);
loggedConnection.logError('authenticateWithConnection', err);
} finally {
provider?.setAuthenticationPending(false);
}
Expand All @@ -874,19 +860,18 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
* Interactively prompt the user to authenticate when the server requires it.
* Returns true if authentication succeeded.
*/
private async _resolveAuthenticationInteractively(address: string, loggedConnection: LoggingAgentConnection, protectedResources: readonly ProtectedResourceMetadata[]): Promise<boolean> {
private async _resolveAuthenticationInteractively(address: string, connection: IAgentConnection, protectedResources: readonly ProtectedResourceMetadata[]): Promise<boolean> {
const authTokenCache = this._connections.get(address)?.authTokenCache;
try {
return await resolveAuthenticationInteractively(protectedResources, {
authTokenCache,
authenticationService: this._authenticationService,
logPrefix: '[RemoteAgentHost]',
logService: this._logService,
authenticate: request => loggedConnection.authenticate(request),
authenticate: request => connection.authenticate(request),
});
} catch (err) {
this._logService.error('[RemoteAgentHost] Interactive authentication failed', err);
loggedConnection.logError('resolveAuthenticationInteractively', err);
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js
import { Registry } from '../../../../../platform/registry/common/platform.js';
import { iterateOtlpLogRecords, logLevelToOtlpLevelName, severityNumberToLogLevel, type IOtlpLogRecord, type OtlpLogLevelName } from '../../../../../platform/agentHost/common/otlp/otlpLogEmitter.js';
import { AgentHostClientState, type RemoteAgentHostProtocolClient } from '../../../../../platform/agentHost/browser/remoteAgentHostProtocolClient.js';
import { remoteAgentHostLogOutputChannelId } from '../../../../../platform/agentHost/common/remoteAgentHostService.js';
import { Extensions, IOutputChannel, IOutputChannelRegistry, IOutputService } from '../../../../../workbench/services/output/common/output.js';

/**
Expand Down Expand Up @@ -57,7 +58,7 @@ export class RemoteAgentHostLogForwarder extends Disposable {
) {
super();

this._channelId = `agentHost.otlp.${address}`;
this._channelId = remoteAgentHostLogOutputChannelId(address);
this._channelLabel = `Agent Host (${displayName})`;

// Wire up subscribe/teardown around the client's connection state.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,6 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid
readonly remoteAddress: string;
readonly browseActions: readonly ISessionWorkspaceBrowseAction[];

private _outputChannelId: string | undefined;
get outputChannelId(): string | undefined { return this._outputChannelId; }

private readonly _connectionStatus = observableValue<RemoteAgentHostConnectionStatus>('connectionStatus', RemoteAgentHostConnectionStatus.disconnected);
readonly connectionStatus: IObservable<RemoteAgentHostConnectionStatus> = this._connectionStatus;

Expand Down Expand Up @@ -353,11 +350,6 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid
this._connectionStatus.set(status, undefined);
}

/** Set the output channel ID for this provider's IPC log. */
setOutputChannelId(id: string): void {
this._outputChannelId = id;
}

setAuthenticationPending(pending: boolean): void {
// Sticky: once the first authentication pass settles, never surface
// pending again. Subsequent re-auths happen silently in the background.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
*--------------------------------------------------------------------------------------------*/

import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js';
import { localize } from '../../../../../nls.js';
import { IRemoteAgentHostService, RemoteAgentHostConnectionStatus } from '../../../../../platform/agentHost/common/remoteAgentHostService.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../workbench/common/contributions.js';
import { LoggingAgentConnection } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.js';
import { IAgentHostTerminalService } from '../../../../../workbench/contrib/terminal/browser/agentHostTerminalService.js';

/**
Expand All @@ -21,7 +18,6 @@ class RemoteAgentHostTerminalContribution extends Disposable {
constructor(
@IRemoteAgentHostService private readonly _remoteAgentHostService: IRemoteAgentHostService,
@IAgentHostTerminalService private readonly _agentHostTerminalService: IAgentHostTerminalService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super();

Expand All @@ -45,12 +41,7 @@ class RemoteAgentHostTerminalContribution extends Disposable {
this._remoteEntries.set(info.address, this._agentHostTerminalService.registerEntry({
name: info.name || info.address,
address: info.address,
getConnection: () => this._instantiationService.createInstance(
LoggingAgentConnection,
connection,
`agenthost.${connection.clientId}`,
localize('agentHostTerminal.channelRemote', "Agent Host Terminal ({0})", info.address),
),
getConnection: () => connection,
}));
}
}
Expand Down
Loading
Loading