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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -911,11 +911,11 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
/**
* Inspect an outgoing client-dispatched action and grant implicit reads
* for any customization URIs it carries. Today this covers
* `SessionActiveClientChanged`, which is the only client-dispatched
* `SessionActiveClientSet`, which is the only client-dispatched
* action that ships customization URIs to the host.
*/
private _grantImplicitReadsForOutgoingAction(action: SessionAction | ChatAction | TerminalAction | ClientAnnotationsAction | IRootConfigChangedAction): void {
if (action.type === ActionType.SessionActiveClientChanged && action.activeClient?.customizations) {
if (action.type === ActionType.SessionActiveClientSet && action.activeClient.customizations) {
this._grantImplicitReadsForCustomizations(action.activeClient.customizations);
}
}
Expand Down
56 changes: 39 additions & 17 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type { IObservable } from '../../../base/common/observable.js';
import { URI } from '../../../base/common/uri.js';
import type { IConfigurationService } from '../../configuration/common/configuration.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import type { ISyncedCustomization } from './agentPluginManager.js';
import type { IAgentServerToolHost } from './agentServerTools.js';
import type { IActiveSubscriptionInfo, IAgentSubscription } from './state/agentSubscription.js';
import type { IRemoteWatchHandle } from './agentHostFileSystemProvider.js';
Expand Down Expand Up @@ -636,7 +635,7 @@ export interface IAgentCreateSessionConfig {
/**
* Eagerly claim the active client role for the new session. When provided,
* the server initializes the session with this client as the active
* client, equivalent to dispatching a `session/activeClientChanged`
* client, equivalent to dispatching a `session/activeClientSet`
* action immediately after creation. The `clientId` MUST match the
* connection's own `clientId`.
*/
Expand Down Expand Up @@ -841,6 +840,30 @@ export interface IMcpNotification {
readonly params?: Record<string, unknown>;
}

/**
* A per-session handle for one active client's contributions (tools and
* plugin customizations) to an agent session, obtained via
* {@link IAgent.getOrCreateActiveClient}.
*
* `tools` and `customizations` are mutable accessor properties: assigning a
* new array replaces this client's contribution wholesale and triggers the
* agent's internal reaction (refreshing the merged tool set exposed to the
* model, or kicking off an asynchronous customization sync). The arrays are
* `readonly` so callers cannot mutate them in place and silently bypass the
* setter. The agent merges the contributions of all active clients on a
* session, deduplicating as needed.
*/
export interface IActiveClient {
/** Client identifier (matches `clientId` from `initialize`). */
readonly clientId: string;
/** Human-readable client name (e.g. `"VS Code"`), if provided. */
readonly displayName: string | undefined;
/** This client's tools. Assigning replaces the set (full replacement). */
tools: readonly ToolDefinition[];
/** This client's plugin customizations. Assigning replaces the set and starts an internal sync. */
customizations: readonly ClientPluginCustomization[];
}

/**
* Implemented by each agent backend (e.g. Copilot SDK).
* The {@link IAgentService} dispatches to the appropriate agent based on
Expand Down Expand Up @@ -1016,27 +1039,26 @@ export interface IAgent {
onArchivedChanged?(session: URI, isArchived: boolean): Promise<void>;

/**
* Receives client-provided customization refs for a session and syncs them
* (e.g. copies plugin files to local storage). The agent publishes
* customization state actions as the sync progresses.
* Get (or lazily create) the per-session handle for an active client,
* identified by `clientId`. Mutating the returned {@link IActiveClient}'s
* `tools` / `customizations` updates only that client's contribution; the
* agent merges the contributions of all active clients when exposing them
* to the model. A session MAY have several active clients at once.
*
* The agent MAY defer a client restart until all active sessions are idle.
* @param session The session URI this client contributes to.
* @param client The client's `clientId` and optional human-readable name.
*/
setClientCustomizations(session: URI, clientId: string, customizations: ClientPluginCustomization[]): Promise<ISyncedCustomization[]>;
getOrCreateActiveClient(session: URI, client: { readonly clientId: string; readonly displayName?: string }): IActiveClient;

/**
* Receives client-provided tool definitions to make available in a
* specific session. The agent registers these as custom tools so the
* LLM can call them; execution is routed back to the owning client.
*
* Always called on `activeClientChanged`, even with an empty array,
* to clear a previous client's tools.
* Remove an active client from a session, clearing its tool and
* customization contributions. No-op when no active client matches
* `clientId`.
*
* @param session The session URI this tool set applies to.
* @param clientId The client that owns these tools.
* @param tools The tool definitions (full replacement).
* @param session The session the client is leaving.
* @param clientId The client to remove.
*/
setClientTools(session: URI, clientId: string | undefined, tools: ToolDefinition[]): void;
removeActiveClient(session: URI, clientId: string): void;

/**
* Called when a client completes a client-provided tool call.
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
77c6312
0259a7e
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// Generated from types/actions.ts — do not edit
// Run `npm run generate` to regenerate.

import { ActionType, type StateAction, type RootAgentsChangedAction, type RootActiveSessionsChangedAction, type RootTerminalsChangedAction, type RootConfigChangedAction, type SessionReadyAction, type SessionCreationFailedAction, type SessionChatAddedAction, type SessionChatRemovedAction, type SessionChatUpdatedAction, type SessionDefaultChatChangedAction, type SessionTitleChangedAction, type SessionModelChangedAction, type SessionAgentChangedAction, type SessionServerToolsChangedAction, type SessionActiveClientChangedAction, type SessionActiveClientToolsChangedAction, type SessionCustomizationsChangedAction, type SessionCustomizationToggledAction, type SessionCustomizationUpdatedAction, type SessionCustomizationRemovedAction, type SessionMcpServerStateChangedAction, type SessionIsReadChangedAction, type SessionIsArchivedChangedAction, type SessionActivityChangedAction, type SessionChangesetsChangedAction, type SessionConfigChangedAction, type SessionMetaChangedAction, type ChatTurnStartedAction, type ChatDeltaAction, type ChatResponsePartAction, type ChatToolCallStartAction, type ChatToolCallDeltaAction, type ChatToolCallReadyAction, type ChatToolCallConfirmedAction, type ChatToolCallCompleteAction, type ChatToolCallResultConfirmedAction, type ChatToolCallContentChangedAction, type ChatTurnCompleteAction, type ChatTurnCancelledAction, type ChatErrorAction, type ChatUsageAction, type ChatReasoningAction, type ChatPendingMessageSetAction, type ChatPendingMessageRemovedAction, type ChatQueuedMessagesReorderedAction, type ChatInputRequestedAction, type ChatInputAnswerChangedAction, type ChatInputCompletedAction, type ChatTruncatedAction, type ChangesetStatusChangedAction, type ChangesetFileSetAction, type ChangesetFileRemovedAction, type ChangesetContentChangedAction, type ChangesetOperationsChangedAction, type ChangesetOperationStatusChangedAction, type ChangesetClearedAction, type AnnotationsSetAction, type AnnotationsUpdatedAction, type AnnotationsRemovedAction, type AnnotationsEntrySetAction, type AnnotationsEntryRemovedAction, type TerminalDataAction, type TerminalInputAction, type TerminalResizedAction, type TerminalClaimedAction, type TerminalTitleChangedAction, type TerminalCwdChangedAction, type TerminalExitedAction, type TerminalClearedAction, type TerminalCommandDetectionAvailableAction, type TerminalCommandExecutedAction, type TerminalCommandFinishedAction, type ResourceWatchChangedAction } from './actions.js';
import { ActionType, type StateAction, type RootAgentsChangedAction, type RootActiveSessionsChangedAction, type RootTerminalsChangedAction, type RootConfigChangedAction, type SessionReadyAction, type SessionCreationFailedAction, type SessionChatAddedAction, type SessionChatRemovedAction, type SessionChatUpdatedAction, type SessionDefaultChatChangedAction, type SessionTitleChangedAction, type SessionModelChangedAction, type SessionAgentChangedAction, type SessionServerToolsChangedAction, type SessionActiveClientSetAction, type SessionActiveClientRemovedAction, type SessionActiveClientToolsChangedAction, type SessionCustomizationsChangedAction, type SessionCustomizationToggledAction, type SessionCustomizationUpdatedAction, type SessionCustomizationRemovedAction, type SessionMcpServerStateChangedAction, type SessionIsReadChangedAction, type SessionIsArchivedChangedAction, type SessionActivityChangedAction, type SessionChangesetsChangedAction, type SessionConfigChangedAction, type SessionMetaChangedAction, type ChatTurnStartedAction, type ChatDeltaAction, type ChatResponsePartAction, type ChatToolCallStartAction, type ChatToolCallDeltaAction, type ChatToolCallReadyAction, type ChatToolCallConfirmedAction, type ChatToolCallCompleteAction, type ChatToolCallResultConfirmedAction, type ChatToolCallContentChangedAction, type ChatTurnCompleteAction, type ChatTurnCancelledAction, type ChatErrorAction, type ChatUsageAction, type ChatReasoningAction, type ChatPendingMessageSetAction, type ChatPendingMessageRemovedAction, type ChatQueuedMessagesReorderedAction, type ChatInputRequestedAction, type ChatInputAnswerChangedAction, type ChatInputCompletedAction, type ChatTruncatedAction, type ChangesetStatusChangedAction, type ChangesetFileSetAction, type ChangesetFileRemovedAction, type ChangesetContentChangedAction, type ChangesetOperationsChangedAction, type ChangesetOperationStatusChangedAction, type ChangesetClearedAction, type AnnotationsSetAction, type AnnotationsUpdatedAction, type AnnotationsRemovedAction, type AnnotationsEntrySetAction, type AnnotationsEntryRemovedAction, type TerminalDataAction, type TerminalInputAction, type TerminalResizedAction, type TerminalClaimedAction, type TerminalTitleChangedAction, type TerminalCwdChangedAction, type TerminalExitedAction, type TerminalClearedAction, type TerminalCommandDetectionAvailableAction, type TerminalCommandExecutedAction, type TerminalCommandFinishedAction, type ResourceWatchChangedAction } from './actions.js';


// ─── Root vs Session vs Chat vs Terminal vs Changeset Action Unions ─────────────────
Expand Down Expand Up @@ -46,7 +46,8 @@ export type SessionAction =
| SessionModelChangedAction
| SessionAgentChangedAction
| SessionServerToolsChangedAction
| SessionActiveClientChangedAction
| SessionActiveClientSetAction
| SessionActiveClientRemovedAction
| SessionActiveClientToolsChangedAction
| SessionCustomizationsChangedAction
| SessionCustomizationToggledAction
Expand All @@ -66,7 +67,8 @@ export type ClientSessionAction =
| SessionTitleChangedAction
| SessionModelChangedAction
| SessionAgentChangedAction
| SessionActiveClientChangedAction
| SessionActiveClientSetAction
| SessionActiveClientRemovedAction
| SessionActiveClientToolsChangedAction
| SessionCustomizationToggledAction
| SessionIsReadChangedAction
Expand Down Expand Up @@ -268,7 +270,8 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool
[ActionType.SessionModelChanged]: true,
[ActionType.SessionAgentChanged]: true,
[ActionType.SessionServerToolsChanged]: false,
[ActionType.SessionActiveClientChanged]: true,
[ActionType.SessionActiveClientSet]: true,
[ActionType.SessionActiveClientRemoved]: true,
[ActionType.SessionActiveClientToolsChanged]: true,
[ActionType.SessionCustomizationsChanged]: false,
[ActionType.SessionCustomizationToggled]: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,36 +238,59 @@ export interface SessionServerToolsChangedAction {
}

/**
* The active client for this session has changed.
* An active client for this session was added or updated.
*
* A client dispatches this action with its own `SessionActiveClient` to claim
* the active role, or with `null` to release it. The server SHOULD reject if
* another client is already active. The server SHOULD automatically dispatch
* this action with `activeClient: null` when the active client disconnects.
* Upsert semantics keyed by {@link SessionActiveClient.clientId | `clientId`}:
* a client dispatches this action with its own `SessionActiveClient` to claim
* the active role or refresh its entry, replacing any existing entry that has
* the same `clientId`. Multiple clients may be active at once. Use
* {@link SessionActiveClientRemovedAction | `session/activeClientRemoved`} to
* release the role. The server SHOULD automatically dispatch that removal when
* an active client disconnects.
*
* @category Session Actions
* @version 1
* @clientDispatchable
*/
export interface SessionActiveClientChangedAction {
type: ActionType.SessionActiveClientChanged;
/** The new active client, or `null` to unset */
activeClient: SessionActiveClient | null;
export interface SessionActiveClientSetAction {
type: ActionType.SessionActiveClientSet;
/** The active client to add or update, matched by `clientId`. */
activeClient: SessionActiveClient;
}

/**
* The active client's tool list has changed.
* An active client was removed from this session.
*
* Full-replacement semantics: the `tools` array replaces the active client's
* previous tools entirely. The server SHOULD reject if the dispatching client
* is not the current active client.
* Releases the active role for the client identified by `clientId`. No-op when
* no active client matches. The server SHOULD dispatch this automatically when
* an active client disconnects.
*
* @category Session Actions
* @version 1
* @clientDispatchable
*/
export interface SessionActiveClientRemovedAction {
type: ActionType.SessionActiveClientRemoved;
/** The `clientId` of the active client to remove. */
clientId: string;
}

/**
* An active client's tool list has changed.
*
* Full-replacement semantics: the `tools` array replaces the named active
* client's previous tools entirely. The active client is identified by
* `clientId`; the action is a no-op when no active client matches. The server
* SHOULD reject if the dispatching client is not the named active client.
*
* @category Session Actions
* @version 1
* @clientDispatchable
*/
export interface SessionActiveClientToolsChangedAction {
type: ActionType.SessionActiveClientToolsChanged;
/** The `clientId` of the active client whose tools changed. */
clientId: string;
/** Updated client tools list (full replacement) */
tools: ToolDefinition[];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export interface CreateSessionParams extends BaseParams {
* Eagerly claim the active client role for the new session.
*
* When provided, the server initializes the session with this client as the
* active client, equivalent to dispatching a `session/activeClientChanged`
* active client, equivalent to dispatching a `session/activeClientSet`
* action immediately after creation. The `clientId` MUST match the
* `clientId` the creating client supplied in `initialize`.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,20 +153,38 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?:
case ActionType.SessionServerToolsChanged:
return { ...state, serverTools: action.tools };

case ActionType.SessionActiveClientChanged:
return {
...state,
activeClient: action.activeClient ?? undefined,
};
case ActionType.SessionActiveClientSet: {
const list = state.activeClients;
const idx = list.findIndex(c => c.clientId === action.activeClient.clientId);
if (idx < 0) {
return { ...state, activeClients: [...list, action.activeClient] };
}
const updated = list.slice();
updated[idx] = action.activeClient;
return { ...state, activeClients: updated };
}

case ActionType.SessionActiveClientToolsChanged:
if (!state.activeClient) {
case ActionType.SessionActiveClientRemoved: {
const list = state.activeClients;
const idx = list.findIndex(c => c.clientId === action.clientId);
if (idx < 0) {
return state;
}
return {
...state,
activeClient: { ...state.activeClient, tools: action.tools },
};
const updated = list.slice();
updated.splice(idx, 1);
return { ...state, activeClients: updated };
}

case ActionType.SessionActiveClientToolsChanged: {
const list = state.activeClients;
const idx = list.findIndex(c => c.clientId === action.clientId);
if (idx < 0) {
return state;
}
const updated = list.slice();
updated[idx] = { ...updated[idx], tools: action.tools };
return { ...state, activeClients: updated };
}

// ── Customizations ──────────────────────────────────────────────────

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ export interface SessionState {
creationError?: ErrorInfo;
/** Tools provided by the server (agent host) for this session */
serverTools?: ToolDefinition[];
/** The client currently providing tools and interactive capabilities to this session */
activeClient?: SessionActiveClient;
/**
* The clients currently providing tools and interactive capabilities to this
* session. If multiple tools or customizations are provided by the same
* active client, an agent host MAY deduplicate them when exposed to a model,
* with a preference given to the client that started the turn.
*/
activeClients: SessionActiveClient[];
/** Catalog of chats in this session. */
chats: ChatSummary[];
/**
Expand All @@ -91,7 +96,7 @@ export interface SessionState {
* also appear as children of a container.
*
* Client-published plugins arrive via
* {@link SessionActiveClient.customizations | `activeClient.customizations`}
* {@link SessionActiveClient.customizations | `activeClients[].customizations`}
* and the host propagates them into this list (typically with the
* container's `clientId` set and `children` populated). Clients
* publish in container shape only; bare MCP servers at the top level
Expand All @@ -117,10 +122,11 @@ export interface SessionState {
}

/**
* The client currently providing tools and interactive capabilities to a session.
* A client currently providing tools and interactive capabilities to a session.
*
* Only one client may be active per session at a time. The server SHOULD
* automatically unset the active client if that client disconnects.
* A session MAY have several active clients at once; entries in
* {@link SessionState.activeClients} are keyed by `clientId`. The server SHOULD
* automatically remove an active client when that client disconnects.
*
* @category Session State
*/
Expand Down
Loading
Loading