diff --git a/src/vs/platform/agentHost/common/agentHostCustomizationConfig.ts b/src/vs/platform/agentHost/common/agentHostCustomizationConfig.ts index 52eeb108be1d3..24c945d31f597 100644 --- a/src/vs/platform/agentHost/common/agentHostCustomizationConfig.ts +++ b/src/vs/platform/agentHost/common/agentHostCustomizationConfig.ts @@ -5,7 +5,7 @@ import { localize } from '../../../nls.js'; import { createSchema, schemaProperty } from './agentHostSchema.js'; -import { CustomizationType, type Customization } from './state/protocol/state.js'; +import { CustomizationType, type Customization, type PluginCustomization } from './state/protocol/state.js'; import { customizationId } from './state/sessionState.js'; /** @@ -105,7 +105,7 @@ export function getAgentHostConfiguredCustomizations(values: Record('agentPluginManager'); @@ -14,7 +14,7 @@ export const IAgentPluginManager = createDecorator('agentPl */ export interface ISyncedCustomization { /** The session customization with loading/error status. */ - readonly customization: Customization; + readonly customization: PluginCustomization; /** Local plugin directory URI, defined when the sync was successful. */ readonly pluginDir?: URI; } @@ -51,5 +51,5 @@ export interface IAgentPluginManager { * @returns Final status for every customization, with `pluginDir` * defined when the sync was successful. */ - syncCustomizations(clientId: string, customizations: ClientPluginCustomization[], progress?: (status: Customization) => void): Promise; + syncCustomizations(clientId: string, customizations: ClientPluginCustomization[], progress?: (status: PluginCustomization) => void): Promise; } diff --git a/src/vs/platform/agentHost/common/customAgents.ts b/src/vs/platform/agentHost/common/customAgents.ts index c52a8eaa6b914..a770131e7f3c4 100644 --- a/src/vs/platform/agentHost/common/customAgents.ts +++ b/src/vs/platform/agentHost/common/customAgents.ts @@ -26,6 +26,9 @@ export function getEffectiveAgents( const seen = new Map(); if (sessionCustomizations) { for (const container of sessionCustomizations) { + if (container.type === CustomizationType.McpServer) { + continue; + } if (container.enabled === false || !container.children) { continue; } diff --git a/src/vs/platform/agentHost/common/state/protocol/.ahp-version b/src/vs/platform/agentHost/common/state/protocol/.ahp-version index 847391941ffb7..97efb749b1e40 100644 --- a/src/vs/platform/agentHost/common/state/protocol/.ahp-version +++ b/src/vs/platform/agentHost/common/state/protocol/.ahp-version @@ -1 +1 @@ -eafb1d7 +740f6cf diff --git a/src/vs/platform/agentHost/common/state/protocol/action-origin.generated.ts b/src/vs/platform/agentHost/common/state/protocol/action-origin.generated.ts index 7661ca26888ae..e8254fb5bc3f0 100644 --- a/src/vs/platform/agentHost/common/state/protocol/action-origin.generated.ts +++ b/src/vs/platform/agentHost/common/state/protocol/action-origin.generated.ts @@ -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 SessionTurnStartedAction, type SessionDeltaAction, type SessionResponsePartAction, type SessionToolCallStartAction, type SessionToolCallDeltaAction, type SessionToolCallReadyAction, type SessionToolCallConfirmedAction, type SessionToolCallCompleteAction, type SessionToolCallResultConfirmedAction, type SessionToolCallContentChangedAction, type SessionTurnCompleteAction, type SessionTurnCancelledAction, type SessionErrorAction, type SessionTitleChangedAction, type SessionUsageAction, type SessionReasoningAction, type SessionModelChangedAction, type SessionAgentChangedAction, type SessionServerToolsChangedAction, type SessionActiveClientChangedAction, type SessionActiveClientToolsChangedAction, type SessionPendingMessageSetAction, type SessionPendingMessageRemovedAction, type SessionQueuedMessagesReorderedAction, type SessionInputRequestedAction, type SessionInputAnswerChangedAction, type SessionInputCompletedAction, type SessionCustomizationsChangedAction, type SessionCustomizationToggledAction, type SessionCustomizationUpdatedAction, type SessionCustomizationRemovedAction, type SessionTruncatedAction, type SessionIsReadChangedAction, type SessionIsArchivedChangedAction, type SessionActivityChangedAction, type SessionChangesetsChangedAction, type SessionConfigChangedAction, type SessionMetaChangedAction, type ChangesetStatusChangedAction, type ChangesetFileSetAction, type ChangesetFileRemovedAction, type ChangesetOperationsChangedAction, type ChangesetClearedAction, 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 SessionTurnStartedAction, type SessionDeltaAction, type SessionResponsePartAction, type SessionToolCallStartAction, type SessionToolCallDeltaAction, type SessionToolCallReadyAction, type SessionToolCallConfirmedAction, type SessionToolCallCompleteAction, type SessionToolCallResultConfirmedAction, type SessionToolCallContentChangedAction, type SessionTurnCompleteAction, type SessionTurnCancelledAction, type SessionErrorAction, type SessionTitleChangedAction, type SessionUsageAction, type SessionReasoningAction, type SessionModelChangedAction, type SessionAgentChangedAction, type SessionServerToolsChangedAction, type SessionActiveClientChangedAction, type SessionActiveClientToolsChangedAction, type SessionPendingMessageSetAction, type SessionPendingMessageRemovedAction, type SessionQueuedMessagesReorderedAction, type SessionInputRequestedAction, type SessionInputAnswerChangedAction, type SessionInputCompletedAction, type SessionCustomizationsChangedAction, type SessionCustomizationToggledAction, type SessionCustomizationUpdatedAction, type SessionCustomizationRemovedAction, type SessionMcpServerStateChangedAction, type SessionTruncatedAction, type SessionIsReadChangedAction, type SessionIsArchivedChangedAction, type SessionActivityChangedAction, type SessionChangesetsChangedAction, type SessionConfigChangedAction, type SessionMetaChangedAction, type ChangesetStatusChangedAction, type ChangesetFileSetAction, type ChangesetFileRemovedAction, type ChangesetOperationsChangedAction, type ChangesetOperationStatusChangedAction, type ChangesetClearedAction, 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 Terminal vs Changeset Action Unions ───────────────── @@ -69,6 +69,7 @@ export type SessionAction = | SessionCustomizationToggledAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction + | SessionMcpServerStateChangedAction | SessionTruncatedAction | SessionIsReadChangedAction | SessionIsArchivedChangedAction @@ -121,6 +122,7 @@ export type ServerSessionAction = | SessionCustomizationsChangedAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction + | SessionMcpServerStateChangedAction | SessionActivityChangedAction | SessionChangesetsChangedAction | SessionMetaChangedAction @@ -166,6 +168,7 @@ export type ChangesetAction = | ChangesetFileSetAction | ChangesetFileRemovedAction | ChangesetOperationsChangedAction + | ChangesetOperationStatusChangedAction | ChangesetClearedAction ; @@ -180,6 +183,7 @@ export type ServerChangesetAction = | ChangesetFileSetAction | ChangesetFileRemovedAction | ChangesetOperationsChangedAction + | ChangesetOperationStatusChangedAction | ChangesetClearedAction ; @@ -242,6 +246,7 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.SessionCustomizationToggled]: true, [ActionType.SessionCustomizationUpdated]: false, [ActionType.SessionCustomizationRemoved]: false, + [ActionType.SessionMcpServerStateChanged]: false, [ActionType.SessionTruncated]: true, [ActionType.SessionIsReadChanged]: true, [ActionType.SessionIsArchivedChanged]: true, @@ -253,6 +258,7 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.ChangesetFileSet]: false, [ActionType.ChangesetFileRemoved]: false, [ActionType.ChangesetOperationsChanged]: false, + [ActionType.ChangesetOperationStatusChanged]: false, [ActionType.ChangesetCleared]: false, [ActionType.TerminalData]: false, [ActionType.TerminalInput]: true, diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-changeset/actions.ts b/src/vs/platform/agentHost/common/state/protocol/channels-changeset/actions.ts index 449dbc483917c..8564894e958a1 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-changeset/actions.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-changeset/actions.ts @@ -8,7 +8,7 @@ import { ActionType } from '../common/actions.js'; import type { ErrorInfo } from '../common/state.js'; -import { ChangesetStatus, type ChangesetFile, type ChangesetOperation } from './state.js'; +import { ChangesetStatus, type ChangesetFile, type ChangesetOperation, type ChangesetOperationStatus } from './state.js'; // ─── Changeset Actions ─────────────────────────────────────────────────────── @@ -70,6 +70,31 @@ export interface ChangesetOperationsChangedAction { operations: ChangesetOperation[] | undefined; } +/** + * The {@link ChangesetOperation.status} for a single operation transitioned + * (e.g. `idle → running → idle`, or `running → error`). The error payload + * is set together with `status` whenever it transitions to + * {@link ChangesetOperationStatus.Error | Error}, and cleared on any other + * transition. + * + * Targets one operation by its {@link ChangesetOperation.id}. If no + * operation with that id is currently present in the changeset, the action + * is a no-op. Use {@link ChangesetOperationsChangedAction} to add, remove, + * or otherwise replace the operation list itself. + * + * @category Changeset Actions + * @version 3 + */ +export interface ChangesetOperationStatusChangedAction { + type: ActionType.ChangesetOperationStatusChanged; + /** The {@link ChangesetOperation.id} whose status changed. */ + operationId: string; + /** New execution status. */ + status: ChangesetOperationStatus; + /** Cause when `status === ChangesetOperationStatus.Error`; otherwise omitted. */ + error?: ErrorInfo; +} + /** * Drop every file from the changeset. * diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-changeset/reducer.ts b/src/vs/platform/agentHost/common/state/protocol/channels-changeset/reducer.ts index aabb0db471aa9..d93f2c1f418b4 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-changeset/reducer.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-changeset/reducer.ts @@ -7,7 +7,7 @@ // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts import { ActionType } from '../common/actions.js'; -import { ChangesetStatus, type ChangesetState, type ChangesetFile } from './state.js'; +import { ChangesetStatus, ChangesetOperationStatus, type ChangesetState, type ChangesetFile, type ChangesetOperation } from './state.js'; import type { ChangesetAction } from '../action-origin.generated.js'; import { softAssertNever } from '../common/reducer-helpers.js'; @@ -60,6 +60,29 @@ export function changesetReducer(state: ChangesetState, action: ChangesetAction, return { ...state, operations: action.operations }; } + case ActionType.ChangesetOperationStatusChanged: { + if (state.operations === undefined) { + return state; + } + const idx = state.operations.findIndex(o => o.id === action.operationId); + if (idx < 0) { + return state; + } + const current = state.operations[idx]; + // Carry `error` only when the new status is `Error` so we don't leave + // a stale error on an operation that recovered or started running. + let nextOp: ChangesetOperation; + if (action.status === ChangesetOperationStatus.Error) { + nextOp = { ...current, status: action.status, error: action.error }; + } else { + const { error: _ignored, ...rest } = current; + nextOp = { ...rest, status: action.status }; + } + const next: ChangesetOperation[] = [...state.operations]; + next[idx] = nextOp; + return { ...state, operations: next }; + } + case ActionType.ChangesetCleared: if (state.files.length === 0) { return state; diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-changeset/state.ts b/src/vs/platform/agentHost/common/state/protocol/channels-changeset/state.ts index 698484c20867e..f22feab892bba 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-changeset/state.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-changeset/state.ts @@ -117,6 +117,31 @@ export interface ChangesetFile { _meta?: Record; } +/** + * Execution lifecycle of a {@link ChangesetOperation}. + * + * An operation is invoked imperatively via `invokeChangesetOperation`, but + * its progress and outcome are reflected back into changeset state so that + * every subscriber observes a consistent view (e.g. a spinner on a "Create + * Pull Request" button, or an inline error after a failed "revert"). + * + * @category Changesets + */ +export const enum ChangesetOperationStatus { + /** + * The operation is ready to be invoked. This is the default when + * {@link ChangesetOperation.status} is omitted. + */ + Idle = 'idle', + /** An invocation of this operation is currently in flight. */ + Running = 'running', + /** + * The most recent invocation failed. The cause is described by + * {@link ChangesetOperation.error}. + */ + Error = 'error', +} + /** * Where a {@link ChangesetOperation} can be invoked. * @@ -161,4 +186,21 @@ export interface ChangesetOperation { confirmation?: StringOrMarkdown; /** Optional generic icon hint, e.g. `"check"`, `"trash"`. */ icon?: string; + /** + * Current execution status. The server sets + * {@link ChangesetOperationStatus.Running | Running} while an invocation + * is in flight, {@link ChangesetOperationStatus.Error | Error} when the + * most recent invocation failed, and + * {@link ChangesetOperationStatus.Idle | Idle} otherwise. + * + * Clients SHOULD reflect this state in the UI — e.g. disabling the + * control or showing a spinner while `Running`, and surfacing + * {@link error} while `Error`. + */ + status: ChangesetOperationStatus; + /** + * Cause of failure. Present iff + * `status === ChangesetOperationStatus.Error`; otherwise omitted. + */ + error?: ErrorInfo; } diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-root/state.ts b/src/vs/platform/agentHost/common/state/protocol/channels-root/state.ts index db7e3b2c328d4..90c87aa4e4ba1 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-root/state.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-root/state.ts @@ -66,13 +66,15 @@ export interface AgentInfo { /** * Customizations associated with this agent. * - * Always container customizations — + * Either container customizations — * {@link PluginCustomization | `PluginCustomization`} entries the agent * bundles, plus {@link DirectoryCustomization | `DirectoryCustomization`} - * entries it watches in any workspace it's used with. When a session is - * created with this agent, these entries are augmented (e.g. directory - * URIs are resolved against the workspace, children are parsed) and - * propagated into the session's `customizations` list. + * entries it watches in any workspace it's used with — or top-level + * {@link McpServerCustomization | `McpServerCustomization`} entries + * the agent host declares directly. When a session is created with + * this agent, these entries are augmented (e.g. directory URIs are + * resolved against the workspace, children are parsed) and propagated + * into the session's `customizations` list. */ customizations?: Customization[]; } diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-session/actions.ts b/src/vs/platform/agentHost/common/state/protocol/channels-session/actions.ts index 945984d503b45..08e3d4bd18bf9 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-session/actions.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-session/actions.ts @@ -7,8 +7,8 @@ // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts import { ActionType } from '../common/actions.js'; -import type { StringOrMarkdown, ErrorInfo, FileEdit, UsageInfo } from '../common/state.js'; -import { ToolCallConfirmationReason, ToolCallCancellationReason, PendingMessageKind, type Message, type ResponsePart, type ToolCallResult, type ToolResultContent, type ToolDefinition, type SessionActiveClient, type Customization, type SessionInputAnswer, type SessionInputRequest, type SessionInputResponseKind, type ConfirmationOption, type AgentSelection } from './state.js'; +import type { StringOrMarkdown, ErrorInfo, FileEdit, UsageInfo, URI } from '../common/state.js'; +import { ToolCallConfirmationReason, ToolCallCancellationReason, PendingMessageKind, type Message, type ResponsePart, type ToolCallResult, type ToolResultContent, type ToolDefinition, type SessionActiveClient, type Customization, type McpServerState, type SessionInputAnswer, type SessionInputRequest, type SessionInputResponseKind, type ConfirmationOption, type AgentSelection, type ToolCallContributor } from './state.js'; import type { ModelSelection } from '../channels-root/state.js'; import type { ChangesetSummary } from '../channels-changeset/state.js'; @@ -116,9 +116,11 @@ export interface SessionResponsePartAction { /** * A tool call begins — parameters are streaming from the LM. * - * For client-provided tools, the server sets `toolClientId` to identify the - * owning client. That client is responsible for executing the tool once it - * reaches the `running` state and dispatching `session/toolCallComplete`. + * The server sets {@link ToolCallContributor | `contributor`} to identify + * the origin of the tool. For client-provided tools, the named client is + * responsible for executing the tool once it reaches the `running` state + * and dispatching `session/toolCallComplete`. For MCP-served tools, the + * server executes the call against the named `McpServerCustomization`. * * @category Session Actions * @version 1 @@ -130,10 +132,10 @@ export interface SessionToolCallStartAction extends ToolCallActionBase { /** Human-readable tool name */ displayName: string; /** - * If this tool is provided by a client, the `clientId` of the owning client. - * Absent for server-side tools. + * Reference to the contributor of the tool being called. Absent for + * server-side tools that are not contributed by a client or MCP server. */ - toolClientId?: string; + contributor?: ToolCallContributor; } /** @@ -617,6 +619,45 @@ export interface SessionCustomizationRemovedAction { id: string; } +/** + * Updates the runtime fields of an existing + * {@link McpServerCustomization} — narrow alternative to + * {@link SessionCustomizationUpdatedAction} for the high-frequency + * `starting` ↔ `ready` ↔ `authRequired` transitions. + * + * Locates the target entry by `id`, searching both the top-level + * customization list and the `children` array of every container. + * Replaces the entry's {@link McpServerCustomization.state | `state`} + * and {@link McpServerCustomization.channel | `channel`} + * (full-replacement semantics: omit `channel` to clear an existing + * channel URI). Other fields of the customization are preserved. + * + * Is a no-op when no matching `McpServerCustomization` is found. To + * update any other field (name, icons, `mcpApp` capabilities, etc.) use + * {@link SessionCustomizationUpdatedAction} instead. + * + * When the transition is to {@link McpServerStatus.AuthRequired} + * because of a request issued mid-turn, the host SHOULD also raise + * {@link SessionStatus.InputNeeded} on the session — see + * {@link McpServerAuthRequiredState} for the rationale. + * + * @category Session Actions + * @version 1 + */ +export interface SessionMcpServerStateChangedAction { + type: ActionType.SessionMcpServerStateChanged; + /** The id of the {@link McpServerCustomization} to update. */ + id: string; + /** The new lifecycle state. */ + state: McpServerState; + /** + * Updated `mcp://` side-channel URI. Full-replacement: omit to clear + * an existing channel (typical when leaving + * {@link McpServerStatus.Ready | `Ready`}). + */ + channel?: URI; +} + // ─── Config Actions ────────────────────────────────────────────────────────── /** diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-session/reducer.ts b/src/vs/platform/agentHost/common/state/protocol/channels-session/reducer.ts index 3bf0d2e9d09eb..32053159ad8b1 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-session/reducer.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-session/reducer.ts @@ -7,7 +7,7 @@ // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts import { ActionType } from '../common/actions.js'; -import { SessionLifecycle, SessionStatus, TurnState, ToolCallStatus, ToolCallConfirmationReason, ToolCallCancellationReason, ResponsePartKind, PendingMessageKind, type SessionInputRequest, type SessionState, type ToolCallState, type ResponsePart, type ToolCallResponsePart, type Turn, type PendingMessage, type ConfirmationOption } from './state.js'; +import { SessionLifecycle, SessionStatus, TurnState, ToolCallStatus, ToolCallConfirmationReason, ToolCallCancellationReason, ResponsePartKind, PendingMessageKind, CustomizationType, type SessionInputRequest, type SessionState, type ToolCallState, type ResponsePart, type ToolCallResponsePart, type Turn, type PendingMessage, type ConfirmationOption, type McpServerCustomization } from './state.js'; import type { SessionAction } from '../action-origin.generated.js'; import { softAssertNever } from '../common/reducer-helpers.js'; @@ -19,7 +19,7 @@ function tcBase(tc: ToolCallState) { toolCallId: tc.toolCallId, toolName: tc.toolName, displayName: tc.displayName, - toolClientId: tc.toolClientId, + contributor: tc.contributor, _meta: tc._meta, }; } @@ -340,7 +340,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: toolCallId: action.toolCallId, toolName: action.toolName, displayName: action.displayName, - toolClientId: action.toolClientId, + contributor: action.contributor, _meta: action._meta, status: ToolCallStatus.Streaming, }, @@ -636,6 +636,9 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: } let changed = false; const updated = list.map(container => { + if (container.type === CustomizationType.McpServer) { + return container; + } const children = container.children; if (!children) { return container; @@ -655,6 +658,59 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: return { ...state, customizations: updated }; } + case ActionType.SessionMcpServerStateChanged: { + const list = state.customizations; + if (!list) { + return state; + } + const topIdx = list.findIndex(c => c.id === action.id); + if (topIdx >= 0) { + const entry = list[topIdx]; + if (entry.type !== CustomizationType.McpServer) { + return state; + } + const updatedEntry: McpServerCustomization = { + ...entry, + state: action.state, + channel: action.channel, + }; + const updated = list.slice(); + updated[topIdx] = updatedEntry; + return { ...state, customizations: updated }; + } + let changed = false; + const updated = list.map(container => { + if (container.type === CustomizationType.McpServer) { + return container; + } + const children = container.children; + if (!children) { + return container; + } + const childIdx = children.findIndex(c => c.id === action.id); + if (childIdx < 0) { + return container; + } + const child = children[childIdx]; + if (child.type !== CustomizationType.McpServer) { + return container; + } + changed = true; + const updatedChild: McpServerCustomization = { + ...child, + state: action.state, + channel: action.channel, + }; + const newChildren = children.slice(); + newChildren[childIdx] = updatedChild; + return { ...container, children: newChildren }; + }); + if (!changed) { + return state; + } + return { ...state, customizations: updated }; + } + // ── Truncation ──────────────────────────────────────────────────────── case ActionType.SessionTruncated: { diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-session/state.ts b/src/vs/platform/agentHost/common/state/protocol/channels-session/state.ts index b983170dd120a..1e4d9ee71761a 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-session/state.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-session/state.ts @@ -6,9 +6,9 @@ // allow-any-unicode-comment-file // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts -import type { URI, StringOrMarkdown, Icon, ConfigPropertySchema, ContentRef, ErrorInfo, FileEdit, TextRange, TextSelection, UsageInfo } from '../common/state.js'; import type { ChangesetSummary } from '../channels-changeset/state.js'; import type { ModelSelection } from '../channels-root/state.js'; +import type { ConfigPropertySchema, ContentRef, ErrorInfo, FileEdit, Icon, ProtectedResourceMetadata, StringOrMarkdown, TextRange, TextSelection, URI, UsageInfo } from '../common/state.js'; // ─── Pending Message Types ─────────────────────────────────────────────────── @@ -108,15 +108,23 @@ export interface SessionState { /** * Top-level customizations active in this session. * - * Always container customizations — {@link PluginCustomization} or - * {@link DirectoryCustomization}. Children (agents, skills, prompts, - * rules, hooks, MCP servers) live in each container's - * {@link ContainerCustomizationBase.children | `children`} array. + * Always one of the {@link Customization} variants: + * + * - Container customizations ({@link PluginCustomization}, + * {@link DirectoryCustomization}) whose children — agents, skills, + * prompts, rules, hooks, MCP servers — live in each container's + * {@link ContainerCustomizationBase.children | `children`} array. + * - Top-level {@link McpServerCustomization} entries the host + * surfaces directly (for example a globally-configured MCP server + * that isn't bundled in a plugin or directory). MCP servers may + * also appear as children of a container. * * Client-published plugins arrive via * {@link SessionActiveClient.customizations | `activeClient.customizations`} * and the host propagates them into this list (typically with the - * container's `clientId` set and `children` populated). + * container's `clientId` set and `children` populated). Clients + * publish in container shape only; bare MCP servers at the top level + * are server-originated. */ customizations?: Customization[]; /** @@ -912,6 +920,33 @@ export interface ConfirmationOption { group?: number; } +export const enum ToolCallContributorKind { + Client = 'client', + MCP = 'mcp', +} + +export interface ToolCallClientContributor { + kind: ToolCallContributorKind.Client; + /** + * If this tool is provided by a client, the `clientId` of the owning client. + * Absent for server-side tools. + * + * When set, the identified client is responsible for executing the tool and + * dispatching `session/toolCallComplete` with the result. + */ + clientId: string; +} + +export interface ToolCallMcpContributor { + kind: ToolCallContributorKind.MCP; + /** + * Customization ID of the corresponding MCP server in {@link SessionState.customizations}. + */ + customizationId: string; +} + +export type ToolCallContributor = ToolCallClientContributor | ToolCallMcpContributor; + /** * Metadata common to all tool call states. * @@ -930,20 +965,15 @@ interface ToolCallBase { /** Human-readable tool name */ displayName: string; /** - * If this tool is provided by a client, the `clientId` of the owning client. - * Absent for server-side tools. - * - * When set, the identified client is responsible for executing the tool and - * dispatching `session/toolCallComplete` with the result. + * Reference to the contributor of the tool being called. */ - toolClientId?: string; + contributor?: ToolCallContributor; /** * Additional provider-specific metadata for this tool call. * - * Clients MAY look for well-known keys here to provide enhanced UI. - * For example, a `ptyTerminal` key with `{ input: string; output: string }` - * indicates the tool operated on a terminal (both `input` and `output` may - * contain escape sequences). + * This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + * `McpUiToolMeta` found in MCP tool calls, which may be used in combination + * with the {@link contributor} to serve MCP Apps. */ _meta?: Record; } @@ -1292,10 +1322,12 @@ export type ToolResultContent = * Discriminant for the kind of customization. * * Top-level entries in {@link SessionState.customizations} and - * {@link AgentInfo.customizations} are always - * {@link CustomizationType.Plugin | `Plugin`} or - * {@link CustomizationType.Directory | `Directory`}; the remaining - * types appear only as children of those containers. + * {@link AgentInfo.customizations} are either container customizations + * ({@link CustomizationType.Plugin | `Plugin`} or + * {@link CustomizationType.Directory | `Directory`}) or + * {@link CustomizationType.McpServer | `McpServer`} entries surfaced + * directly by the host. The remaining types appear only as children of + * a container. * * @category Customization Types */ @@ -1515,6 +1547,12 @@ export interface AgentCustomization extends CustomizationBase { * invoke it. Sourced from the agent file's frontmatter `description`. */ description?: string; + /** + * Additional provider-specific metadata for this custom agent. + * + * Mirrors the MCP `_meta` convention. + */ + _meta?: Record; } /** @@ -1595,17 +1633,129 @@ export interface HookCustomization extends CustomizationBase { } /** - * An MCP manifest contributed by a plugin or directory. + * An MCP server contributed by a plugin or directory. * * When the server is declared inline in the containing plugin manifest, * `uri` points at the manifest file and * {@link CustomizationBase.range | `range`} narrows it to the * declaration's span. * + * The MCP server customization also reflects its current status. + * * @category Customization Types */ export interface McpServerCustomization extends CustomizationBase { type: CustomizationType.McpServer; + /** + * Whether this MCP server is currently enabled. + */ + enabled: boolean; + /** + * Current lifecycle state of the MCP server. + */ + state: McpServerState; + /** + * An `mcp://`-protocol channel the client uses to side-channel traffic + * into the upstream MCP server itself. The channel is NOT a fresh raw MCP + * connection: it piggybacks on the AHP transport + * and skips the MCP `initialize` sequence. + * + * The agent host MAY only serve a subset of MCP on this + * channel; the served subset is described by domain-specific + * capabilities such as those in + * {@link McpServerCustomizationApps.capabilities}. + * + * The channel URI SHOULD be stable across the server's lifetime, but + * the agent host MAY change it (for example across a restart) and + * MAY only expose it while the server is in + * {@link McpServerStatus.Ready | `Ready`}. Absence means no + * side-channel is currently available. + */ + channel?: URI; + /** + * MCP App support. This property SHOULD be advertised for MCP servers + * which support apps. + */ + mcpApp?: McpServerCustomizationApps; +} + +/** + * Information from the agent host needed to render MCP Apps served + * by this MCP server. + * + * @category MCP Server State + */ +export interface McpServerCustomizationApps { + /** + * The subset of MCP App + * [`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) + * the AHP host can satisfy for Views backed by this server. The + * client feeds these straight through into the `hostCapabilities` of + * the `ui/initialize` response delivered to the View. + */ + capabilities: AhpMcpUiHostCapabilities; +} + +/** + * The subset of MCP App + * [`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) + * an AHP host can derive from the upstream MCP server (and from AHP's own + * forwarding plumbing). Advertised on + * {@link McpServerCustomizationApps.capabilities} so clients can pass it + * through into the `hostCapabilities` of the `ui/initialize` response + * delivered to an MCP App View. + * + * Field names mirror the MCP Apps spec exactly, so the AHP-side producer + * can pass them straight through into the `hostCapabilities` of the + * `ui/initialize` response delivered to the View. + * + * Capabilities outside this set (`openLinks`, `downloadFile`, `sandbox`, + * `experimental`) are decided locally by whichever AHP client renders the + * View and are NOT part of this AHP-level advertisement — only the + * server-derived subset is. + * + * An agent host MUST only advertise a capability when it actually accepts the + * corresponding methods/notifications on the `mcp://` channel: + * + * - {@link serverTools}: host proxies `tools/list` and `tools/call` to + * the MCP server. When `listChanged` is `true`, the host also forwards + * `notifications/tools/list_changed`. + * - {@link serverResources}: host proxies `resources/read`, + * `resources/list`, and `resources/templates/list` to the MCP server. + * When `listChanged` is `true`, the host also forwards + * `notifications/resources/list_changed`. + * - {@link logging}: host accepts `notifications/message` log entries + * from the App and forwards them via `mcpNotification` (and forwards + * `logging/setLevel` calls to the server). + * - {@link sampling}: host serves `sampling/createMessage` via + * `mcpMethodCall`. When `sampling.tools` is present, the host also + * accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks + * inside `CreateMessageRequest`. + * + * @category MCP Server State + * @see {@link https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx | MCP Apps spec (SEP-1865)} + */ +export interface AhpMcpUiHostCapabilities { + /** Producer proxies the MCP `tools/*` methods to the upstream server. */ + serverTools?: { + /** Producer forwards `notifications/tools/list_changed` from the server. */ + listChanged?: boolean; + }; + /** Producer proxies the MCP `resources/*` methods to the upstream server. */ + serverResources?: { + /** Producer forwards `notifications/resources/list_changed` from the server. */ + listChanged?: boolean; + }; + /** Producer accepts `notifications/message` log entries from the App via `mcpNotification`. */ + logging?: Record; + /** Producer serves `sampling/createMessage` via `mcpMethodCall`. */ + sampling?: { + /** + * Producer accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content + * blocks inside `CreateMessageRequest`. + */ + tools?: Record; + }; } /** @@ -1623,11 +1773,177 @@ export type ChildCustomization = | McpServerCustomization; /** - * A top-level customization active in a session. Always a container - * ({@link PluginCustomization} or {@link DirectoryCustomization}); the - * remaining customization types appear inside the container's - * {@link ContainerCustomizationBase.children | `children`} array. + * A top-level customization active in a session. Either a container + * ({@link PluginCustomization} or {@link DirectoryCustomization}) whose + * leaf customizations live in its + * {@link ContainerCustomizationBase.children | `children`} array, or a + * bare {@link McpServerCustomization} surfaced directly by the host. * * @category Customization Types */ -export type Customization = PluginCustomization | DirectoryCustomization; +export type Customization = + | PluginCustomization + | DirectoryCustomization + | McpServerCustomization; + + +// ─── MCP Server State ──────────────────────────────────────────────────────── + +/** + * Discriminant for the {@link McpServerState} union. + * + * @category MCP Server State + */ +export const enum McpServerStatus { + /** Server has been registered but is not yet running. */ + Starting = 'starting', + /** Server is running and serving requests. */ + Ready = 'ready', + /** + * Server is reachable but requires additional authentication before it + * can start, or before it can serve a particular request. Carries the + * RFC 9728 Protected Resource Metadata the client needs to obtain a + * token; the client then pushes the token via the existing + * `authenticate` command. + */ + AuthRequired = 'authRequired', + /** Server failed to start, crashed, or otherwise transitioned to a fatal error. */ + Error = 'error', + /** Server has been shut down. */ + Stopped = 'stopped', +} + +/** + * Why an MCP server is currently in the {@link McpServerStatus.AuthRequired} + * state. Mirrors the three failure modes defined by the + * [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md). + * + * @category MCP Server State + */ +export const enum McpAuthRequiredReason { + /** No token has been provided yet (HTTP 401, no prior token). */ + Required = 'required', + /** A previously valid token expired or was revoked (HTTP 401). */ + Expired = 'expired', + /** + * Step-up auth: a token is present but its scopes are insufficient for + * the requested operation (HTTP 403 with + * `WWW-Authenticate: Bearer error="insufficient_scope"`). + * + * Unlike {@link Required} and {@link Expired} — which typically surface + * before any tool work is in flight — `InsufficientScope` is almost + * always triggered by an MCP request issued mid-turn (a `tools/call`, + * `resources/read`, etc.). The host SHOULD pair the + * {@link McpServerAuthRequiredState} transition with + * {@link SessionStatus.InputNeeded} on + * {@link SessionSummary.status | the session} so the activity becomes + * visible at the session-summary level, and clients SHOULD watch for + * this kind on any + * {@link McpServerCustomization | MCP server} backing a running tool + * call so they can present an explicit "grant more access" affordance + * tied to the blocked tool call. + */ + InsufficientScope = 'insufficientScope', +} + +/** + * Server is registered with the host but has not yet started. + * + * @category MCP Server State + */ +export interface McpServerStartingState { + kind: McpServerStatus.Starting; +} + +/** + * Server is running and serving requests. + * + * @category MCP Server State + */ +export interface McpServerReadyState { + kind: McpServerStatus.Ready; +} + +/** + * Server is reachable but cannot serve requests until the client + * authenticates. Mirrors the discovery flow defined by + * [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) + * (Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge + * semantics required by the MCP authorization spec. + * + * Clients react to this state by calling the existing `authenticate` + * command with the {@link ProtectedResourceMetadata.resource | resource} + * carried here. There is **no** `notify/authRequired` notification for + * MCP servers — the action stream is the single source of truth. + * + * When the transition is triggered by a request issued during a turn + * — most commonly + * {@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`} + * surfacing mid-tool-call — the host SHOULD also raise + * {@link SessionStatus.InputNeeded} on the session so the block is + * visible at the summary level. Clients SHOULD watch this status on + * any MCP server backing a running tool call and surface an explicit + * affordance (e.g. a "grant additional access" prompt) tied to that + * tool call, rather than relying on the user to notice the + * customization’s status badge. + * + * @category MCP Server State + */ +export interface McpServerAuthRequiredState { + kind: McpServerStatus.AuthRequired; + /** Why authentication is required. */ + reason: McpAuthRequiredReason; + /** + * RFC 9728 Protected Resource Metadata. The `resource` field is the + * canonical MCP server URI per RFC 8707, used as the OAuth `resource` + * indicator. `authorization_servers` is REQUIRED by the MCP + * authorization spec. + */ + resource: ProtectedResourceMetadata; + /** + * Scopes required for the current challenge, parsed from the + * `WWW-Authenticate: Bearer scope="…"` header (or `scopes_supported` + * fallback). Authoritative for the next authorization request — clients + * MUST NOT assume any subset/superset relationship to + * `resource.scopes_supported`. + */ + requiredScopes?: string[]; + /** Human-readable hint, typically from the OAuth `error_description`. */ + description?: string; +} + +/** + * Server failed to start, crashed, or otherwise transitioned to a + * non-recoverable error. Use {@link McpServerStatus.AuthRequired} + * for authentication failures. + * + * @category MCP Server State + */ +export interface McpServerErrorState { + kind: McpServerStatus.Error; + /** Error details. */ + error: ErrorInfo; +} + +/** + * Server has been shut down. The host MAY remove the server from the + * session entirely shortly after this state. + * + * @category MCP Server State + */ +export interface McpServerStoppedState { + kind: McpServerStatus.Stopped; +} + +/** + * Discriminated union of all MCP server lifecycle states. + * Discriminated by `kind` (a {@link McpServerStatus} value). + * + * @category MCP Server State + */ +export type McpServerState = + | McpServerStartingState + | McpServerReadyState + | McpServerAuthRequiredState + | McpServerErrorState + | McpServerStoppedState; diff --git a/src/vs/platform/agentHost/common/state/protocol/common/actions.ts b/src/vs/platform/agentHost/common/state/protocol/common/actions.ts index df506e331c66f..2eeccf45dd347 100644 --- a/src/vs/platform/agentHost/common/state/protocol/common/actions.ts +++ b/src/vs/platform/agentHost/common/state/protocol/common/actions.ts @@ -10,9 +10,9 @@ import type { URI } from './state.js'; import type { RootAgentsChangedAction, RootActiveSessionsChangedAction, RootTerminalsChangedAction, RootConfigChangedAction } from '../channels-root/actions.js'; -import type { SessionReadyAction, SessionCreationFailedAction, SessionTurnStartedAction, SessionDeltaAction, SessionResponsePartAction, SessionToolCallStartAction, SessionToolCallDeltaAction, SessionToolCallReadyAction, SessionToolCallConfirmedAction, SessionToolCallCompleteAction, SessionToolCallResultConfirmedAction, SessionToolCallContentChangedAction, SessionTurnCompleteAction, SessionTurnCancelledAction, SessionErrorAction, SessionTitleChangedAction, SessionUsageAction, SessionReasoningAction, SessionModelChangedAction, SessionAgentChangedAction, SessionServerToolsChangedAction, SessionActiveClientChangedAction, SessionActiveClientToolsChangedAction, SessionPendingMessageSetAction, SessionPendingMessageRemovedAction, SessionQueuedMessagesReorderedAction, SessionInputRequestedAction, SessionInputAnswerChangedAction, SessionInputCompletedAction, SessionCustomizationsChangedAction, SessionCustomizationToggledAction, SessionCustomizationUpdatedAction, SessionCustomizationRemovedAction, SessionTruncatedAction, SessionIsReadChangedAction, SessionIsArchivedChangedAction, SessionActivityChangedAction, SessionChangesetsChangedAction, SessionConfigChangedAction, SessionMetaChangedAction } from '../channels-session/actions.js'; +import type { SessionReadyAction, SessionCreationFailedAction, SessionTurnStartedAction, SessionDeltaAction, SessionResponsePartAction, SessionToolCallStartAction, SessionToolCallDeltaAction, SessionToolCallReadyAction, SessionToolCallConfirmedAction, SessionToolCallCompleteAction, SessionToolCallResultConfirmedAction, SessionToolCallContentChangedAction, SessionTurnCompleteAction, SessionTurnCancelledAction, SessionErrorAction, SessionTitleChangedAction, SessionUsageAction, SessionReasoningAction, SessionModelChangedAction, SessionAgentChangedAction, SessionServerToolsChangedAction, SessionActiveClientChangedAction, SessionActiveClientToolsChangedAction, SessionPendingMessageSetAction, SessionPendingMessageRemovedAction, SessionQueuedMessagesReorderedAction, SessionInputRequestedAction, SessionInputAnswerChangedAction, SessionInputCompletedAction, SessionCustomizationsChangedAction, SessionCustomizationToggledAction, SessionCustomizationUpdatedAction, SessionCustomizationRemovedAction, SessionMcpServerStateChangedAction, SessionTruncatedAction, SessionIsReadChangedAction, SessionIsArchivedChangedAction, SessionActivityChangedAction, SessionChangesetsChangedAction, SessionConfigChangedAction, SessionMetaChangedAction } from '../channels-session/actions.js'; -import type { ChangesetStatusChangedAction, ChangesetFileSetAction, ChangesetFileRemovedAction, ChangesetOperationsChangedAction, ChangesetClearedAction } from '../channels-changeset/actions.js'; +import type { ChangesetStatusChangedAction, ChangesetFileSetAction, ChangesetFileRemovedAction, ChangesetOperationsChangedAction, ChangesetOperationStatusChangedAction, ChangesetClearedAction } from '../channels-changeset/actions.js'; import type { TerminalDataAction, TerminalInputAction, TerminalResizedAction, TerminalClaimedAction, TerminalTitleChangedAction, TerminalCwdChangedAction, TerminalExitedAction, TerminalClearedAction, TerminalCommandDetectionAvailableAction, TerminalCommandExecutedAction, TerminalCommandFinishedAction } from '../channels-terminal/actions.js'; @@ -61,6 +61,7 @@ export const enum ActionType { SessionCustomizationToggled = 'session/customizationToggled', SessionCustomizationUpdated = 'session/customizationUpdated', SessionCustomizationRemoved = 'session/customizationRemoved', + SessionMcpServerStateChanged = 'session/mcpServerStateChanged', SessionTruncated = 'session/truncated', SessionIsReadChanged = 'session/isReadChanged', SessionIsArchivedChanged = 'session/isArchivedChanged', @@ -72,6 +73,7 @@ export const enum ActionType { ChangesetFileSet = 'changeset/fileSet', ChangesetFileRemoved = 'changeset/fileRemoved', ChangesetOperationsChanged = 'changeset/operationsChanged', + ChangesetOperationStatusChanged = 'changeset/operationStatusChanged', ChangesetCleared = 'changeset/cleared', RootTerminalsChanged = 'root/terminalsChanged', RootConfigChanged = 'root/configChanged', @@ -160,6 +162,7 @@ export type StateAction = | SessionCustomizationToggledAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction + | SessionMcpServerStateChangedAction | SessionTruncatedAction | SessionIsReadChangedAction | SessionIsArchivedChangedAction @@ -171,6 +174,7 @@ export type StateAction = | ChangesetFileSetAction | ChangesetFileRemovedAction | ChangesetOperationsChangedAction + | ChangesetOperationStatusChangedAction | ChangesetClearedAction | TerminalDataAction | TerminalInputAction diff --git a/src/vs/platform/agentHost/common/state/protocol/common/commands.ts b/src/vs/platform/agentHost/common/state/protocol/common/commands.ts index 1b9f65f25cd26..03ffb40c48ba7 100644 --- a/src/vs/platform/agentHost/common/state/protocol/common/commands.ts +++ b/src/vs/platform/agentHost/common/state/protocol/common/commands.ts @@ -71,6 +71,40 @@ export interface InitializeParams extends BaseParams { * user-facing strings such as confirmation option labels. */ locale?: string; + /** + * Optional client capability declarations. + * + * Servers SHOULD only advertise features whose corresponding client + * capability is set here. Absent means "not declared" — the server + * MUST assume the client does not support the feature. + */ + capabilities?: ClientCapabilities; +} + +/** + * Optional capabilities a client declares during `initialize`. + * + * Each field is a presence flag: an empty object `{}` means "supported", + * absence means "not supported". Sub-fields on individual capabilities + * are reserved for future per-capability options. + * + * @category Commands + */ +export interface ClientCapabilities { + /** + * Client can render + * [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e. + * it can host the View sandbox, run the `ui/*` protocol against it, + * and forward `mcp://`-channel traffic on the App's behalf. + * + * Hosts SHOULD only populate + * {@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`} + * (and expose the corresponding + * {@link McpServerCustomization.channel | `mcp://` channel}) when this + * capability is declared. Clients that omit it MUST treat + * App-bearing tool calls as ordinary MCP tool calls. + */ + mcpApps?: Record; } /** 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 f0d191172c633..a3706e6501ab0 100644 --- a/src/vs/platform/agentHost/common/state/protocol/version/registry.ts +++ b/src/vs/platform/agentHost/common/state/protocol/version/registry.ts @@ -111,6 +111,7 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.SessionCustomizationToggled]: '0.1.0', [ActionType.SessionCustomizationUpdated]: '0.1.0', [ActionType.SessionCustomizationRemoved]: '0.2.0', + [ActionType.SessionMcpServerStateChanged]: '0.3.0', [ActionType.SessionTruncated]: '0.1.0', [ActionType.SessionIsReadChanged]: '0.1.0', [ActionType.SessionIsArchivedChanged]: '0.1.0', @@ -122,6 +123,7 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.ChangesetFileSet]: '0.2.0', [ActionType.ChangesetFileRemoved]: '0.2.0', [ActionType.ChangesetOperationsChanged]: '0.2.0', + [ActionType.ChangesetOperationStatusChanged]: '0.3.0', [ActionType.ChangesetCleared]: '0.2.0', [ActionType.RootTerminalsChanged]: '0.1.0', [ActionType.RootConfigChanged]: '0.1.0', diff --git a/src/vs/platform/agentHost/common/state/sessionState.ts b/src/vs/platform/agentHost/common/state/sessionState.ts index 19d9b2427b391..7cd72764c78e4 100644 --- a/src/vs/platform/agentHost/common/state/sessionState.ts +++ b/src/vs/platform/agentHost/common/state/sessionState.ts @@ -37,7 +37,7 @@ import { // Re-export everything from the protocol state module export { - ChangesetOperationScope, ChangesetStatus, CustomizationLoadStatus, + ChangesetOperationScope, ChangesetOperationStatus, ChangesetStatus, CustomizationLoadStatus, CustomizationType, MessageAttachmentKind, MessageKind, PendingMessageKind, PolicyState, @@ -47,7 +47,7 @@ export { SessionInputQuestionKind, SessionInputResponseKind, SessionLifecycle, - SessionStatus, ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallStatus, + SessionStatus, ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, TurnState, type ActiveTurn, type AgentCustomization, type AgentInfo, type AgentSelection, type ChangesetFile, type ChangesetOperation, type ChangesetState, type ChangesetSummary, type ChildCustomization, type ClientPluginCustomization, type ConfigPropertySchema, @@ -71,6 +71,7 @@ export { type ToolCallRunningState, type ToolCallState, type ToolCallStreamingState, + type ToolCallContributor, type ToolDefinition, type ToolResultContent, type ToolResultFileEditContent, type ToolResultSubagentContent, diff --git a/src/vs/platform/agentHost/node/agentHostCommitOperationProvider.ts b/src/vs/platform/agentHost/node/agentHostCommitOperationProvider.ts index e2799e567b40c..9856a2cc16327 100644 --- a/src/vs/platform/agentHost/node/agentHostCommitOperationProvider.ts +++ b/src/vs/platform/agentHost/node/agentHostCommitOperationProvider.ts @@ -8,7 +8,7 @@ import { localize } from '../../../nls.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; import { ChangesetKind } from '../common/changesetUri.js'; import type { IChangesetOperationContribution, IChangesetOperationContext, IChangesetOperationRegistry } from '../common/changesetOperation.js'; -import { ChangesetOperationScope, type ChangesetOperation } from '../common/state/sessionState.js'; +import { ChangesetOperationScope, ChangesetOperationStatus, type ChangesetOperation } from '../common/state/sessionState.js'; import { AgentHostCommitOperationHandler } from './agentHostCommitOperationHandler.js'; import { AgentHostStateManager } from './agentHostStateManager.js'; @@ -43,6 +43,7 @@ export class AgentHostCommitOperationContribution extends Disposable implements label: localize('agentHost.changeset.commit', "Commit"), scopes: [ChangesetOperationScope.Changeset], icon: 'git-commit', + status: ChangesetOperationStatus.Idle, }]; } diff --git a/src/vs/platform/agentHost/node/agentHostSkillCompletionProvider.ts b/src/vs/platform/agentHost/node/agentHostSkillCompletionProvider.ts index 29d480af8a0bb..a8092f0b9bb46 100644 --- a/src/vs/platform/agentHost/node/agentHostSkillCompletionProvider.ts +++ b/src/vs/platform/agentHost/node/agentHostSkillCompletionProvider.ts @@ -78,8 +78,17 @@ export class AgentHostSkillCompletionProvider extends Disposable implements IAge return []; } const customizations = await agent.getSessionCustomizations(session); - return customizations - .filter(c => c.enabled) - .flatMap(c => (c.children ?? []).filter(child => child.type === CustomizationType.Skill)); + const result: SkillCustomization[] = []; + for (const c of customizations) { + if (c.type === CustomizationType.McpServer || !c.enabled || !c.children) { + continue; + } + for (const child of c.children) { + if (child.type === CustomizationType.Skill) { + result.push(child); + } + } + } + return result; } } diff --git a/src/vs/platform/agentHost/node/agentPluginManager.ts b/src/vs/platform/agentHost/node/agentPluginManager.ts index eca2380d985cb..8e7589ff23304 100644 --- a/src/vs/platform/agentHost/node/agentPluginManager.ts +++ b/src/vs/platform/agentHost/node/agentPluginManager.ts @@ -9,7 +9,7 @@ import { URI } from '../../../base/common/uri.js'; import { IFileService } from '../../files/common/files.js'; import { ILogService } from '../../log/common/log.js'; import { IAgentPluginManager, type ISyncedCustomization } from '../common/agentPluginManager.js'; -import { CustomizationLoadStatus, type ClientPluginCustomization, type Customization } from '../common/state/sessionState.js'; +import { CustomizationLoadStatus, type ClientPluginCustomization, type PluginCustomization } from '../common/state/sessionState.js'; import { toAgentClientUri } from '../common/agentClientUri.js'; const DEFAULT_MAX_PLUGINS = 20; @@ -68,7 +68,7 @@ export class AgentPluginManager implements IAgentPluginManager { async syncCustomizations( clientId: string, customizations: ClientPluginCustomization[], - progress?: (status: Customization) => void, + progress?: (status: PluginCustomization) => void, ): Promise { await this._ensureCacheLoaded(); @@ -77,13 +77,13 @@ export class AgentPluginManager implements IAgentPluginManager { this._sequencer.queue(ref.uri, async (): Promise => { try { const pluginDir = await this._syncPlugin(clientId, ref); - const customization: Customization = { ...ref, load: { kind: CustomizationLoadStatus.Loaded } }; + const customization: PluginCustomization = { ...ref, load: { kind: CustomizationLoadStatus.Loaded } }; progress?.(customization); return { customization, pluginDir }; } catch (err) { const message = err instanceof Error ? err.message : String(err); this._logService.error(`[AgentPluginManager] Failed to sync plugin ${ref.uri}: ${message}`); - const customization: Customization = { ...ref, load: { kind: CustomizationLoadStatus.Error, message } }; + const customization: PluginCustomization = { ...ref, load: { kind: CustomizationLoadStatus.Error, message } }; progress?.(customization); return { customization }; } diff --git a/src/vs/platform/agentHost/node/claude/customizations/claudeSdkCustomizationBundler.ts b/src/vs/platform/agentHost/node/claude/customizations/claudeSdkCustomizationBundler.ts index de04e169794e7..050b8ef4ac326 100644 --- a/src/vs/platform/agentHost/node/claude/customizations/claudeSdkCustomizationBundler.ts +++ b/src/vs/platform/agentHost/node/claude/customizations/claudeSdkCustomizationBundler.ts @@ -10,7 +10,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { IFileService } from '../../../../files/common/files.js'; import { IAgentPluginManager } from '../../../common/agentPluginManager.js'; -import { CustomizationLoadStatus, CustomizationType, customizationId, type AgentCustomization, type Customization, type SkillCustomization } from '../../../common/state/sessionState.js'; +import { CustomizationLoadStatus, CustomizationType, customizationId, type AgentCustomization, type PluginCustomization, type SkillCustomization } from '../../../common/state/sessionState.js'; import type { ISdkResolvedCustomizations } from '../claudeSdkPipeline.js'; const PLUGIN_NAME = 'claude-discovered'; @@ -58,7 +58,7 @@ export class ClaudeSdkCustomizationBundler extends Disposable { this._rootUri = URI.joinPath(pluginManager.basePath, DISCOVERED_DIR, authority); } - async bundle(snapshot: ISdkResolvedCustomizations): Promise { + async bundle(snapshot: ISdkResolvedCustomizations): Promise { if (snapshot.commands.length === 0 && snapshot.agents.length === 0) { return undefined; } diff --git a/src/vs/platform/agentHost/node/claude/customizations/claudeSessionClientCustomizationsModel.ts b/src/vs/platform/agentHost/node/claude/customizations/claudeSessionClientCustomizationsModel.ts index de48a23c1d070..ace08dfade4cb 100644 --- a/src/vs/platform/agentHost/node/claude/customizations/claudeSessionClientCustomizationsModel.ts +++ b/src/vs/platform/agentHost/node/claude/customizations/claudeSessionClientCustomizationsModel.ts @@ -9,6 +9,7 @@ import { equals as arraysEqual } from '../../../../../base/common/arrays.js'; import { URI } from '../../../../../base/common/uri.js'; import { autorun, derivedOpts, IObservable, ISettableObservable, observableValueOpts } from '../../../../../base/common/observable.js'; import type { ISyncedCustomization } from '../../../common/agentPluginManager.js'; +import type { ClientPluginCustomization } from '../../../common/state/sessionState.js'; /** * Per-session **client-pushed** customization snapshot + enablement @@ -197,10 +198,10 @@ function syncedListEqual(a: readonly ISyncedCustomization[], b: readonly ISynced if (ai.id !== bi.id) { return false; } - if (ai.uri.toString() !== bi.uri.toString()) { + if (ai.uri !== bi.uri) { return false; } - if ((ai as { nonce?: string }).nonce !== (bi as { nonce?: string }).nonce) { + if ((ai as ClientPluginCustomization).nonce !== (bi as ClientPluginCustomization).nonce) { return false; } if (ai.name !== bi.name) { diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index 2ed43d7c71865..aa8bd0a6d5922 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -34,7 +34,7 @@ import type { ResolveSessionConfigResult, SessionConfigCompletionsResult } from import { ProtectedResourceMetadata, type ChildCustomizationType, type ConfigSchema, type ModelSelection, type AgentSelection, type ToolDefinition } from '../../common/state/protocol/state.js'; import { ActionType, type SessionAction } from '../../common/state/sessionActions.js'; import { AHP_AUTH_REQUIRED, ProtocolError } from '../../common/state/sessionProtocol.js'; -import { CustomizationLoadStatus, CustomizationType, ResponsePartKind, SessionInputResponseKind, customizationId, parseSubagentSessionUri, type ChildCustomization, type ClientPluginCustomization, type Customization, type DirectoryCustomization, type MessageAttachment, type PendingMessage, type PolicyState, type ResponsePart, type SessionInputAnswer, type ToolCallResult, type Turn } from '../../common/state/sessionState.js'; +import { CustomizationLoadStatus, CustomizationType, ResponsePartKind, SessionInputResponseKind, customizationId, parseSubagentSessionUri, type ChildCustomization, type ClientPluginCustomization, type Customization, type DirectoryCustomization, type MessageAttachment, type PendingMessage, type PluginCustomization, type PolicyState, type ResponsePart, type SessionInputAnswer, type ToolCallResult, type Turn } from '../../common/state/sessionState.js'; import { IAgentConfigurationService } from '../agentConfigurationService.js'; import { IAgentHostOTelService } from '../../common/otel/agentHostOTelService.js'; import { IAgentHostCompletions } from '../agentHostCompletions.js'; @@ -1949,7 +1949,7 @@ export class CopilotAgent extends Disposable implements IAgent { } interface IResolvedCustomization { - readonly customization: Customization; + readonly customization: PluginCustomization; readonly pluginDir?: URI; readonly plugin?: IParsedPlugin; } @@ -2332,12 +2332,12 @@ class PluginController extends Disposable { return this._enablement.get(customization.uri) ?? customization.enabled; } - private _applyEnablement(customization: Customization): Customization { + private _applyEnablement(customization: T): T { const enabled = this._isEnabled(customization); return customization.enabled === enabled ? customization : { ...customization, enabled }; } - private async _resolveConfiguredCustomization(customization: Customization): Promise { + private async _resolveConfiguredCustomization(customization: PluginCustomization): Promise { const pluginDir = URI.parse(customization.uri); const parsed = await this._tryParsePlugin(pluginDir); if (!parsed) { @@ -2361,7 +2361,7 @@ class PluginController extends Disposable { } private async _resolveSyncedCustomization(item: ISyncedCustomization, clientId: string): Promise { - const baseCustomization: Customization = { ...item.customization, clientId }; + const baseCustomization: PluginCustomization = { ...item.customization, clientId }; if (!item.pluginDir) { return { customization: baseCustomization }; } diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts index 7496cfd895f21..29e3e9c62d4c9 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts @@ -29,7 +29,7 @@ import { stripRedundantCdPrefix } from '../../common/commandLineHelpers.js'; import type { LanguageModelToolInvokedClassification, LanguageModelToolInvokedEvent } from '../../../telemetry/common/languageModelToolTelemetry.js'; 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 { MessageAttachmentKind, ToolCallContributorKind, type FileEdit, type MessageAttachment, type ToolDefinition } from '../../common/state/protocol/state.js'; import { ActionType, type SessionAction } from '../../common/state/sessionActions.js'; import { MessageKind, 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'; @@ -1755,6 +1755,7 @@ export class CopilotAgentSession extends Disposable { const toolKind = getToolKind(e.data.toolName); const subagentMeta = toolKind === 'subagent' ? getSubagentMetadata(parameters) : undefined; const toolClientId = this._clientToolNames.has(e.data.toolName) ? this._appliedSnapshot.clientId : undefined; + const contributor = toolClientId ? { kind: ToolCallContributorKind.Client, clientId: toolClientId } as const : undefined; // A new tool call invalidates the current markdown and reasoning // parts so the next text/reasoning delta after the tool call @@ -1787,7 +1788,7 @@ export class CopilotAgentSession extends Disposable { toolCallId: e.data.toolCallId, toolName: e.data.toolName, displayName, - toolClientId, + contributor, _meta: meta, }, parentToolCallId); diff --git a/src/vs/platform/agentHost/node/protocolServerHandler.ts b/src/vs/platform/agentHost/node/protocolServerHandler.ts index fa94bcdd1825a..96d98ce07a581 100644 --- a/src/vs/platform/agentHost/node/protocolServerHandler.ts +++ b/src/vs/platform/agentHost/node/protocolServerHandler.ts @@ -34,7 +34,7 @@ import { type ReconnectParams, type IStateSnapshot, } from '../common/state/sessionProtocol.js'; -import { isAhpResourceWatchChannel, isAhpRootChannel, ResponsePartKind, SessionStatus, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, type SessionState } from '../common/state/sessionState.js'; +import { isAhpResourceWatchChannel, isAhpRootChannel, ResponsePartKind, SessionStatus, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, type SessionState } from '../common/state/sessionState.js'; import type { IProtocolServer, IProtocolTransport } from '../common/state/sessionTransport.js'; import { AgentHostStateManager } from './agentHostStateManager.js'; import { @@ -651,7 +651,8 @@ export class ProtocolServerHandler extends Disposable { return false; } return activeTurn.responseParts.some(part => part.kind === ResponsePartKind.ToolCall - && part.toolCall.toolClientId === clientId + && part.toolCall.contributor?.kind === ToolCallContributorKind.Client + && part.toolCall.contributor.clientId === clientId && (part.toolCall.status === ToolCallStatus.Streaming || part.toolCall.status === ToolCallStatus.Running || part.toolCall.status === ToolCallStatus.PendingConfirmation)); } @@ -695,7 +696,8 @@ export class ProtocolServerHandler extends Disposable { continue; } const toolCall = part.toolCall; - if (toolCall.toolClientId === clientId && (toolCall.status === ToolCallStatus.Streaming || toolCall.status === ToolCallStatus.Running || toolCall.status === ToolCallStatus.PendingConfirmation)) { + const toolContributor = toolCall.contributor; + if (toolContributor?.kind === ToolCallContributorKind.Client && toolContributor.clientId === 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(session, { diff --git a/src/vs/platform/agentHost/test/node/agentHostChangesetOperationContributionService.test.ts b/src/vs/platform/agentHost/test/node/agentHostChangesetOperationContributionService.test.ts index 762d185428ef1..32b544d1ab833 100644 --- a/src/vs/platform/agentHost/test/node/agentHostChangesetOperationContributionService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostChangesetOperationContributionService.test.ts @@ -12,7 +12,7 @@ import type { IChangesetOperationContribution, IChangesetOperationContext, IChan import { buildUncommittedChangesetUri } from '../../common/changesetUri.js'; import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } from '../../common/state/protocol/channels-changeset/commands.js'; import { ActionType } from '../../common/state/sessionActions.js'; -import { ChangesetOperationScope, type ChangesetOperation } from '../../common/state/sessionState.js'; +import { ChangesetOperationScope, ChangesetOperationStatus, type ChangesetOperation } from '../../common/state/sessionState.js'; import { AgentHostChangesetOperationContributionService } from '../../node/agentHostChangesetOperationContributionService.js'; import { AgentHostStateManager } from '../../node/agentHostStateManager.js'; import type { AgentHostSessionGitStateService } from '../../node/agentHostSessionGitStateService.js'; @@ -58,7 +58,7 @@ suite('AgentHostChangesetOperationContributionService', () => { stateManager.registerChangeset(changesetUri); stateManager.dispatchServerAction(changesetUri, { type: ActionType.ChangesetOperationsChanged, - operations: [{ id: 'commit', label: 'Commit', scopes: [ChangesetOperationScope.Changeset] }], + operations: [{ id: 'commit', label: 'Commit', scopes: [ChangesetOperationScope.Changeset], status: ChangesetOperationStatus.Idle }], }); const service = disposables.add(new AgentHostChangesetOperationContributionService( diff --git a/src/vs/platform/agentHost/test/node/agentPluginManager.test.ts b/src/vs/platform/agentHost/test/node/agentPluginManager.test.ts index 5a94df8b59555..0504b44e37fdf 100644 --- a/src/vs/platform/agentHost/test/node/agentPluginManager.test.ts +++ b/src/vs/platform/agentHost/test/node/agentPluginManager.test.ts @@ -13,7 +13,7 @@ import { FileService } from '../../../files/common/fileService.js'; import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js'; import { NullLogService } from '../../../log/common/log.js'; import { AGENT_CLIENT_SCHEME, toAgentClientUri } from '../../common/agentClientUri.js'; -import { customizationId, type ClientPluginCustomization, type Customization } from '../../common/state/sessionState.js'; +import { customizationId, type ClientPluginCustomization, type PluginCustomization } from '../../common/state/sessionState.js'; import { CustomizationType } from '../../common/state/protocol/state.js'; import { AgentPluginManager } from '../../node/agentPluginManager.js'; @@ -100,7 +100,7 @@ suite('AgentPluginManager', () => { test('fires progress callback with changed customization status', async () => { await seedPluginDir('prog', { 'index.js': 'content' }); - const progressCalls: Customization[] = []; + const progressCalls: PluginCustomization[] = []; await manager.syncCustomizations('test-client', [makeRef('prog', 'n1')], status => { progressCalls.push(status); }); @@ -174,7 +174,7 @@ suite('AgentPluginManager', () => { const result = await manager2.syncCustomizations('test-client', [ref]); // Should be loaded from cache (nonce match), not error - assert.strictEqual(result[0].customization.load?.kind, 'loaded'); + assert.strictEqual((result[0].customization as PluginCustomization).load?.kind, 'loaded'); assert.ok(result[0].pluginDir); }); }); diff --git a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts index 617b66f54afdb..66e0342075bfe 100644 --- a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts +++ b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts @@ -23,7 +23,7 @@ import { ISessionDataService } from '../../common/sessionDataService.js'; import type { RootConfigChangedAction } from '../../common/state/protocol/actions.js'; import { CustomizationType } from '../../common/state/protocol/state.js'; import { ActionType, ActionEnvelope, SessionAction } from '../../common/state/sessionActions.js'; -import { buildSubagentSessionUri, CustomizationLoadStatus, MessageAttachmentKind, MessageKind, PendingMessageKind, ResponsePartKind, SessionStatus, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, customizationId, type ClientPluginCustomization, type Customization } from '../../common/state/sessionState.js'; +import { buildSubagentSessionUri, CustomizationLoadStatus, MessageAttachmentKind, MessageKind, PendingMessageKind, ResponsePartKind, SessionStatus, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, customizationId, type ClientPluginCustomization, type Customization, type PluginCustomization } from '../../common/state/sessionState.js'; import { IProductService } from '../../../product/common/productService.js'; import { ITelemetryService, TelemetryLevel } from '../../../telemetry/common/telemetry.js'; import { NullTelemetryService } from '../../../telemetry/common/telemetryUtils.js'; @@ -1039,7 +1039,7 @@ suite('AgentSideEffects', () => { agent.getSessionCustomizations = async () => currentCustomizations; agent.setClientCustomizations = async (session, clientId, customizations) => { agent.setClientCustomizationsCalls.push({ clientId, customizations }); - const loading: Customization = { ...pluginAClient, load: { kind: CustomizationLoadStatus.Loading } }; + const loading: PluginCustomization = { ...pluginAClient, load: { kind: CustomizationLoadStatus.Loading } }; currentCustomizations = [loading]; agent.fireProgress({ kind: 'action', @@ -1050,7 +1050,7 @@ suite('AgentSideEffects', () => { }, }); await new Promise(resolve => setTimeout(resolve, 0)); - const loaded: Customization = { ...pluginAClient, load: { kind: CustomizationLoadStatus.Loaded } }; + const loaded: PluginCustomization = { ...pluginAClient, load: { kind: CustomizationLoadStatus.Loaded } }; currentCustomizations = [loaded]; agent.fireProgress({ kind: 'action', @@ -1060,7 +1060,7 @@ suite('AgentSideEffects', () => { customization: loaded, }, }); - return currentCustomizations.map(customization => ({ customization })); + return currentCustomizations.map(customization => ({ customization: customization as PluginCustomization })); }; const envelopes: ActionEnvelope[] = []; @@ -1267,7 +1267,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-conf-1', toolName: 'read', displayName: 'Read File', toolClientId: undefined, + toolCallId: 'tc-conf-1', toolName: 'read', displayName: 'Read File', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1315,7 +1315,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-deny-1', toolName: 'shell', displayName: 'Shell', toolClientId: undefined, + toolCallId: 'tc-deny-1', toolName: 'shell', displayName: 'Shell', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1356,7 +1356,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-ready-1', toolName: 'runTask', displayName: 'Run Task', toolClientId: 'test-client', + toolCallId: 'tc-ready-1', toolName: 'runTask', displayName: 'Run Task', contributor: { kind: ToolCallContributorKind.Client, clientId: 'test-client' }, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1395,7 +1395,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-perm-1', toolName: 'write', displayName: 'Write File', toolClientId: 'test-client', + toolCallId: 'tc-perm-1', toolName: 'write', displayName: 'Write File', contributor: { kind: ToolCallContributorKind.Client, clientId: 'test-client' }, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1435,7 +1435,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-parent', toolName: 'runSubagent', displayName: 'Run Subagent', toolClientId: undefined, + toolCallId: 'tc-parent', toolName: 'runSubagent', displayName: 'Run Subagent', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1454,7 +1454,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-inner', toolName: 'problems', displayName: 'Problems', toolClientId: 'client-tools', + toolCallId: 'tc-inner', toolName: 'problems', displayName: 'Problems', contributor: { kind: ToolCallContributorKind.Client, clientId: 'client-tools' }, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1534,7 +1534,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-bypass-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, + toolCallId: 'tc-bypass-1', toolName: 'write', displayName: 'Write', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1573,7 +1573,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-ap-shell-1', toolName: 'shell', displayName: 'Shell', toolClientId: undefined, + toolCallId: 'tc-ap-shell-1', toolName: 'shell', displayName: 'Shell', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1612,7 +1612,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-default-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, + toolCallId: 'tc-default-1', toolName: 'write', displayName: 'Write', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1655,7 +1655,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-mid-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, + toolCallId: 'tc-mid-1', toolName: 'write', displayName: 'Write', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1699,7 +1699,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-auto-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, + toolCallId: 'tc-auto-1', toolName: 'write', displayName: 'Write', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1741,7 +1741,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-env-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, + toolCallId: 'tc-env-1', toolName: 'write', displayName: 'Write', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1782,7 +1782,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-pkg-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, + toolCallId: 'tc-pkg-1', toolName: 'write', displayName: 'Write', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1818,7 +1818,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-lock-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, + toolCallId: 'tc-lock-1', toolName: 'write', displayName: 'Write', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1854,7 +1854,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-git-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, + toolCallId: 'tc-git-1', toolName: 'write', displayName: 'Write', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1895,7 +1895,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-read-1', toolName: 'read', displayName: 'Read', toolClientId: undefined, + toolCallId: 'tc-read-1', toolName: 'read', displayName: 'Read', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -1936,7 +1936,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-read-2', toolName: 'read', displayName: 'Read', toolClientId: undefined, + toolCallId: 'tc-read-2', toolName: 'read', displayName: 'Read', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -2117,7 +2117,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', toolClientId: undefined, + toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -2165,7 +2165,7 @@ suite('AgentSideEffects', () => { disposables.add(sideEffects.registerProgressListener(agent)); // Start parent tool + subagent - 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.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', contributor: 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' }); @@ -2174,7 +2174,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, parentToolCallId: 'tc-1', action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'inner-tc-1', toolName: 'readFile', displayName: 'Read File', toolClientId: undefined, + toolCallId: 'inner-tc-1', toolName: 'readFile', displayName: 'Read File', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -2214,7 +2214,7 @@ suite('AgentSideEffects', () => { startTurn('turn-1'); disposables.add(sideEffects.registerProgressListener(agent)); - 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.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', contributor: 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. @@ -2222,7 +2222,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, parentToolCallId: 'tc-1', action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'inner-1', toolName: 'read', displayName: 'Read', toolClientId: undefined, + toolCallId: 'inner-1', toolName: 'read', displayName: 'Read', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -2265,7 +2265,7 @@ suite('AgentSideEffects', () => { disposables.add(sideEffects.registerProgressListener(agent)); // Start parent tool + subagent - 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.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', contributor: 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' }); @@ -2301,11 +2301,11 @@ suite('AgentSideEffects', () => { disposables.add(sideEffects.registerProgressListener(agent)); // Start two parent tool calls with subagents - 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.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Sub 1', contributor: 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, 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.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-2', toolName: 'runSubagent', displayName: 'Sub 2', contributor: 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' }); @@ -2327,7 +2327,7 @@ suite('AgentSideEffects', () => { startTurn('turn-1'); disposables.add(sideEffects.registerProgressListener(agent)); - 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.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Sub 1', contributor: 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' }); @@ -2344,7 +2344,7 @@ suite('AgentSideEffects', () => { startTurn('turn-1'); disposables.add(sideEffects.registerProgressListener(agent)); - 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.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'runSubagent', displayName: 'Run Subagent', contributor: 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' }); @@ -2369,7 +2369,7 @@ suite('AgentSideEffects', () => { startTurn('turn-1'); disposables.add(sideEffects.registerProgressListener(agent)); - 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.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-1', toolName: 'task', displayName: 'Task', contributor: 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' }); @@ -2414,7 +2414,7 @@ suite('AgentSideEffects', () => { disposables.add(sideEffects.registerProgressListener(agent)); // 1. Parent tool starts (the `task` invocation). - 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.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-parent', toolName: 'task', displayName: 'Task', contributor: 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). @@ -2422,7 +2422,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'inner-tc-1', toolName: 'readFile', displayName: 'Read File', toolClientId: undefined, + toolCallId: 'inner-tc-1', toolName: 'readFile', displayName: 'Read File', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -2465,7 +2465,7 @@ suite('AgentSideEffects', () => { disposables.add(sideEffects.registerProgressListener(agent)); // Parent task tool spawns a subagent. - 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.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-parent', toolName: 'task', displayName: 'Task', contributor: 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' }); @@ -2475,7 +2475,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'inner-read-1', toolName: 'read', displayName: 'Read', toolClientId: undefined, + toolCallId: 'inner-read-1', toolName: 'read', displayName: 'Read', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -2528,7 +2528,7 @@ suite('AgentSideEffects', () => { }; } - 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.SessionToolCallStart, turnId: 'turn-1', toolCallId: 'tc-parent', toolName: 'task', displayName: 'Task', contributor: 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' }); @@ -2538,7 +2538,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'inner-write-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, + toolCallId: 'inner-write-1', toolName: 'write', displayName: 'Write', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -2580,7 +2580,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-perm-1', toolName: 'CustomTool', displayName: 'Custom Tool', toolClientId: undefined, + toolCallId: 'tc-perm-1', toolName: 'CustomTool', displayName: 'Custom Tool', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -2630,7 +2630,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-perm-2', toolName: 'CustomTool', displayName: 'Custom Tool', toolClientId: undefined, + toolCallId: 'tc-perm-2', toolName: 'CustomTool', displayName: 'Custom Tool', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -2686,7 +2686,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-perm-3', toolName: 'CustomTool', displayName: 'Custom Tool', toolClientId: undefined, + toolCallId: 'tc-perm-3', toolName: 'CustomTool', displayName: 'Custom Tool', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -2731,7 +2731,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-parent', toolName: 'task', displayName: 'Task', toolClientId: undefined, + toolCallId: 'tc-parent', toolName: 'task', displayName: 'Task', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -2755,7 +2755,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, parentToolCallId: 'tc-parent', action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'inner-perm-1', toolName: 'CustomTool', displayName: 'Custom Tool', toolClientId: undefined, + toolCallId: 'inner-perm-1', toolName: 'CustomTool', displayName: 'Custom Tool', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); @@ -2808,7 +2808,7 @@ suite('AgentSideEffects', () => { kind: 'action', session: sessionUri, action: { type: ActionType.SessionToolCallStart, turnId: 'turn-1', - toolCallId: 'tc-edit-1', toolName: 'write', displayName: 'Write', toolClientId: undefined, + toolCallId: 'tc-edit-1', toolName: 'write', displayName: 'Write', contributor: undefined, _meta: { toolKind: undefined, language: undefined }, }, }); diff --git a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts index 90fcd5c29e876..41664ae2219e0 100644 --- a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts @@ -38,7 +38,7 @@ import { FileService } from '../../../files/common/fileService.js'; import { IAgentMaterializeSessionEvent, AgentSession, AgentSignal, GITHUB_COPILOT_PROTECTED_RESOURCE } from '../../common/agentService.js'; import { AgentFeedbackAttachmentDisplayKind } from '../../common/agentFeedbackAttachments.js'; import { ActionType } from '../../common/state/sessionActions.js'; -import { CustomizationLoadStatus, CustomizationType, MessageAttachmentKind, MessageKind, ResponsePartKind, SessionInputResponseKind, SessionStatus, ToolResultContentType, buildSubagentSessionUri, customizationId, type ClientPluginCustomization, type Customization } from '../../common/state/sessionState.js'; +import { CustomizationLoadStatus, CustomizationType, MessageAttachmentKind, MessageKind, ResponsePartKind, SessionInputResponseKind, SessionStatus, ToolResultContentType, buildSubagentSessionUri, customizationId, type ClientPluginCustomization, type PluginCustomization } from '../../common/state/sessionState.js'; import { ISessionDataService } from '../../common/sessionDataService.js'; import { AHP_AUTH_REQUIRED, ProtocolError } from '../../common/state/sessionProtocol.js'; import { ProtectedResourceMetadata, SessionInputAnswerState, SessionInputAnswerValueKind, ToolCallStatus, type SessionConfigState, type SessionInputRequest, type ToolDefinition } from '../../common/state/protocol/state.js'; @@ -73,7 +73,7 @@ class FakeAgentPluginManager implements IAgentPluginManager { async syncCustomizations( clientId: string, customizations: ClientPluginCustomization[], - progress?: (status: Customization) => void, + progress?: (status: PluginCustomization) => void, ): Promise { this.syncCalls.push({ clientId, customizations: [...customizations] }); if (this.syncResult) { diff --git a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts index 94f8c73dded69..7c047a3140cbb 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts @@ -31,7 +31,7 @@ import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPlu import { AgentSession, type AgentSignal, type IAgentActionSignal, type IAgentSessionMetadata } from '../../common/agentService.js'; import { ISessionDataService } from '../../common/sessionDataService.js'; import { AHP_AUTH_REQUIRED, ProtocolError } from '../../common/state/sessionProtocol.js'; -import { buildSubagentSessionUri, CustomizationLoadStatus, MessageKind, ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, TurnState, customizationId, type ClientPluginCustomization, type Customization, type MarkdownResponsePart, type ToolCallResult, type Turn, RuleCustomization } from '../../common/state/sessionState.js'; +import { buildSubagentSessionUri, CustomizationLoadStatus, MessageKind, ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, TurnState, customizationId, type ClientPluginCustomization, type MarkdownResponsePart, type PluginCustomization, type ToolCallResult, type Turn, RuleCustomization } from '../../common/state/sessionState.js'; import { CustomizationType } from '../../common/state/protocol/state.js'; import { ActionType, type IDeltaAction, type SessionAction } from '../../common/state/sessionActions.js'; @@ -54,7 +54,7 @@ class TestAgentPluginManager implements IAgentPluginManager { readonly basePath = URI.from({ scheme: 'inmemory', path: '/agentPlugins' }); - async syncCustomizations(_clientId: string, _customizations: ClientPluginCustomization[], _progress?: (status: Customization) => void): Promise { + async syncCustomizations(_clientId: string, _customizations: ClientPluginCustomization[], _progress?: (status: PluginCustomization) => void): Promise { return []; } } @@ -681,7 +681,7 @@ suite('CopilotAgent', () => { class SpyingPluginManager extends TestAgentPluginManager { public readonly calls: { clientId: string; customizations: ClientPluginCustomization[] }[] = []; - override async syncCustomizations(clientId: string, customizations: ClientPluginCustomization[], _progress?: (status: Customization) => void): Promise { + override async syncCustomizations(clientId: string, customizations: ClientPluginCustomization[], _progress?: (status: PluginCustomization) => void): Promise { this.calls.push({ clientId, customizations: [...customizations] }); return []; } @@ -784,10 +784,10 @@ suite('CopilotAgent', () => { const updatesWithChildren = actions .filter(a => a.type === ActionType.SessionCustomizationUpdated) .filter((a): a is Extract => true) - .filter(a => a.customization.children !== undefined); + .filter(a => (a.customization as PluginCustomization).children !== undefined); assert.strictEqual(updatesWithChildren.length > 0, true, 'expected SessionCustomizationUpdated to carry parsed children'); - const agentChildren = updatesWithChildren.at(-1)!.customization.children!.filter(c => c.type === CustomizationType.Agent); + const agentChildren = (updatesWithChildren.at(-1)!.customization as PluginCustomization).children!.filter(c => c.type === CustomizationType.Agent); assert.deepStrictEqual(agentChildren, [{ type: CustomizationType.Agent, id: customizationId(URI.joinPath(pluginDir, 'agents', 'helper.md').toString()), diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index 4c76885da9328..d5a90f2140df8 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -24,7 +24,7 @@ import { AgentSession, type AgentSignal, type IAgentActionSignal, type IAgentToo import { IDiffComputeService } from '../../common/diffComputeService.js'; import { ISessionDataService } from '../../common/sessionDataService.js'; import { ActionType, type SessionDeltaAction, type SessionErrorAction, type SessionInputRequestedAction, type SessionResponsePartAction, type SessionToolCallCompleteAction, type SessionToolCallReadyAction, type SessionToolCallStartAction } from '../../common/state/sessionActions.js'; -import { MessageAttachmentKind, MessageKind, ResponsePartKind, SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, ToolCallStatus, ToolResultContentType, type ToolResultFileEditContent } from '../../common/state/sessionState.js'; +import { MessageAttachmentKind, MessageKind, ResponsePartKind, SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, type ToolResultFileEditContent } from '../../common/state/sessionState.js'; import { CopilotAgentSession, IActiveClientSnapshot, SessionWrapperFactory } from '../../node/copilot/copilotAgentSession.js'; import { CopilotSessionWrapper } from '../../node/copilot/copilotSessionWrapper.js'; import { buildCopilotSystemNotification } from '../../node/copilot/copilotSystemNotification.js'; @@ -2428,7 +2428,7 @@ suite('CopilotAgentSession', () => { const startSignal = signals.find(s => isAction(s, ActionType.SessionToolCallStart)); assert.ok(startSignal && isAction(startSignal, ActionType.SessionToolCallStart)); if (isAction(startSignal!, ActionType.SessionToolCallStart)) { - assert.strictEqual((startSignal.action as SessionToolCallStartAction).toolClientId, 'test-client'); + assert.deepStrictEqual((startSignal.action as SessionToolCallStartAction).contributor, { kind: ToolCallContributorKind.Client, clientId: 'test-client' }); } // SDK invokes the handler — it creates a deferred and waits, diff --git a/src/vs/platform/agentHost/test/node/copilotPluginConverters.test.ts b/src/vs/platform/agentHost/test/node/copilotPluginConverters.test.ts index b64fea2e2bdca..6c51573d1ffe7 100644 --- a/src/vs/platform/agentHost/test/node/copilotPluginConverters.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotPluginConverters.test.ts @@ -17,10 +17,10 @@ import { NullLogService } from '../../../log/common/log.js'; import { McpServerType } from '../../../mcp/common/mcpPlatformTypes.js'; import { toSdkInstructionDirectories, toSdkMcpServers, toSdkCustomAgents, toSdkSkillDirectories, parsedPluginsEqual, toSdkHooks } from '../../node/copilot/copilotPluginConverters.js'; import type { IMcpServerDefinition, INamedPluginResource, IParsedHookGroup, IParsedPlugin, IParsedSkill } from '../../../agentPlugins/common/pluginParsers.js'; -import { CustomizationType, type HookCustomization, type McpServerCustomization, type SkillCustomization } from '../../common/state/protocol/state.js'; +import { CustomizationType, McpServerStatus, type HookCustomization, type McpServerCustomization, type SkillCustomization } from '../../common/state/protocol/state.js'; function stubMcpCustomization(name = 'test'): McpServerCustomization { - return { type: CustomizationType.McpServer, id: `mcp:${name}`, uri: 'file:///plugin', name }; + return { type: CustomizationType.McpServer, id: `mcp:${name}`, uri: 'file:///plugin', name, enabled: true, state: { kind: McpServerStatus.Starting } }; } function stubHookCustomization(type: string): HookCustomization { return { type: CustomizationType.Hook, id: `hook:${type}`, uri: 'file:///plugin/hooks.json', name: 'hooks.json' }; diff --git a/src/vs/platform/agentHost/test/node/mockAgent.ts b/src/vs/platform/agentHost/test/node/mockAgent.ts index 4ff0a010b7a70..269b2113f5f4f 100644 --- a/src/vs/platform/agentHost/test/node/mockAgent.ts +++ b/src/vs/platform/agentHost/test/node/mockAgent.ts @@ -11,7 +11,7 @@ import { URI } from '../../../../base/common/uri.js'; import { type ISyncedCustomization } from '../../common/agentPluginManager.js'; import { AgentSession, type AgentProvider, type AgentSignal, type IAgent, type IAgentActionSignal, type IAgentCreateSessionConfig, type IAgentCreateSessionResult, type IAgentDescriptor, type IAgentModelInfo, type IAgentResolveSessionConfigParams, type IAgentSessionConfigCompletionsParams, type IAgentSessionMetadata, type IAgentToolPendingConfirmationSignal } from '../../common/agentService.js'; import { buildSubagentTurnsFromHistory, buildTurnsFromHistory, type IHistoryRecord } from './historyRecordFixtures.js'; -import { ProtectedResourceMetadata, type MessageAttachment, type ModelSelection } from '../../common/state/protocol/state.js'; +import { ProtectedResourceMetadata, ToolCallContributorKind, type MessageAttachment, type ModelSelection } from '../../common/state/protocol/state.js'; import type { ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../../common/state/protocol/commands.js'; import { ActionType } from '../../common/state/sessionActions.js'; import { ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, CustomizationLoadStatus, parseSubagentSessionUri, type ClientPluginCustomization, type Customization, type PendingMessage, type StringOrMarkdown, type ToolCallResult, type Turn, type UsageInfo } from '../../common/state/sessionState.js'; @@ -553,7 +553,7 @@ export class ScriptedMockAgent implements IAgent { toolCallId: 'tc-client-1', toolName: 'runTests', displayName: 'Run Tests', - toolClientId: 'test-client-tool', + contributor: { kind: ToolCallContributorKind.Client, clientId: 'test-client-tool' }, })); await timeout(5); this._onDidSessionProgress.fire(_pendingConfirmation(session, 'tc-client-1', 'Running tests...', { toolInput: '{}' })); @@ -579,7 +579,7 @@ export class ScriptedMockAgent implements IAgent { toolCallId: 'tc-client-perm-1', toolName: 'runTests', displayName: 'Run Tests', - toolClientId: 'test-client-tool', + contributor: { kind: ToolCallContributorKind.Client, clientId: 'test-client-tool' }, })); await timeout(5); this._onDidSessionProgress.fire(_pendingConfirmation(session, 'tc-client-perm-1', 'Run tests on project', { confirmationTitle: 'Allow Run Tests?' })); @@ -840,7 +840,7 @@ function _toolStart(session: URI, sessionStr: string, turnId: string, toolCallId toolCallId, toolName, displayName, - toolClientId: opts?.toolClientId, + contributor: opts?.toolClientId ? { kind: ToolCallContributorKind.Client, clientId: opts.toolClientId } : undefined, _meta: Object.keys(meta).length ? meta : undefined, }, opts?.parentToolCallId)]; if (!opts?.toolClientId) { 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 65a959a439ca9..9dae0001f2850 100644 --- a/src/vs/platform/agentHost/test/node/protocol/clientTools.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/clientTools.integrationTest.ts @@ -7,7 +7,7 @@ * Integration tests for client-provided tool handling through the protocol layer. * * These tests verify that: - * - tool_start with toolClientId emits only a toolCallStart (no auto-ready) + * - tool_start with client contributor emits only a toolCallStart (no auto-ready) * - tool_ready without confirmationTitle transitions to Running (auto-confirmed) * - tool_ready with confirmationTitle transitions to PendingConfirmation * - toolCallComplete dispatched by the client flows through to the agent @@ -17,7 +17,7 @@ */ import assert from 'assert'; -import { ToolResultContentType } from '../../../common/state/sessionState.js'; +import { ToolCallContributorKind, ToolResultContentType, type ToolCallContributor } from '../../../common/state/sessionState.js'; import { createAndSubscribeSession, dispatchTurnStarted, @@ -52,7 +52,7 @@ suite('Protocol WebSocket — Client Tools', function () { client.close(); }); - // ---- Client tool: tool_start with toolClientId -------------------------- + // ---- Client tool: tool_start with client contributor -------------------- test('client tool_start emits toolCallStart then toolCallReady (auto-confirmed)', async function () { this.timeout(10_000); @@ -67,10 +67,10 @@ suite('Protocol WebSocket — Client Tools', function () { ]); const toolStartAction = getActionEnvelope(toolStartNotif).action as { toolCallId: string; - toolClientId?: string; + contributor?: ToolCallContributor; }; assert.strictEqual(toolStartAction.toolCallId, 'tc-client-1'); - assert.strictEqual(toolStartAction.toolClientId, 'test-client-tool'); + assert.deepStrictEqual(toolStartAction.contributor, { kind: ToolCallContributorKind.Client, clientId: 'test-client-tool' }); const toolReadyAction = getActionEnvelope(toolReadyNotif).action as { toolCallId: string; @@ -109,16 +109,16 @@ suite('Protocol WebSocket — Client Tools', function () { const sessionUri = await createAndSubscribeSession(client, 'test-client-perm'); dispatchTurnStarted(client, sessionUri, 'turn-cp', 'client-tool-with-permission', 1); - // Wait for toolCallStart (should have toolClientId) + // Wait for toolCallStart (should have client contributor) const toolStartNotif = await client.waitForNotification( n => isActionNotification(n, 'session/toolCallStart'), ); const toolStartAction = getActionEnvelope(toolStartNotif).action as { toolCallId: string; - toolClientId?: string; + contributor?: ToolCallContributor; }; assert.strictEqual(toolStartAction.toolCallId, 'tc-client-perm-1'); - assert.strictEqual(toolStartAction.toolClientId, 'test-client-tool'); + assert.deepStrictEqual(toolStartAction.contributor, { kind: ToolCallContributorKind.Client, clientId: 'test-client-tool' }); // Wait for toolCallReady with confirmationTitle (permission flow) const toolReadyNotif = await client.waitForNotification( diff --git a/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts b/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts index 9f3e87d631e27..e784b039e6834 100644 --- a/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts +++ b/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts @@ -16,7 +16,7 @@ import { CompletionsParams, CompletionsResult, ListSessionsResult, ResourceReadR import { ActionType, type IRootConfigChangedAction, type SessionAction, type TerminalAction } from '../../common/state/sessionActions.js'; import { PROTOCOL_VERSION } from '../../common/state/protocol/version/registry.js'; import { isJsonRpcNotification, isJsonRpcRequest, 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 { MessageKind, ResponsePartKind, SessionStatus, ChangesetStatus, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, type SessionSummary } from '../../common/state/sessionState.js'; +import { MessageKind, ResponsePartKind, SessionStatus, ChangesetStatus, ToolCallConfirmationReason, ToolCallContributorKind, 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'; @@ -835,7 +835,7 @@ suite('ProtocolServerHandler', () => { toolCallId: 'tool-1', toolName: 'runTask', displayName: 'Run Task', - toolClientId: 'client-tools', + contributor: { kind: ToolCallContributorKind.Client, clientId: 'client-tools' }, }); stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallReady, @@ -892,7 +892,7 @@ suite('ProtocolServerHandler', () => { toolCallId: 'tool-1', toolName: 'runTask', displayName: 'Run Task', - toolClientId: 'client-tools', + contributor: { kind: ToolCallContributorKind.Client, clientId: 'client-tools' }, }); const transport = connectClient('client-tools', [sessionUri]); @@ -940,7 +940,7 @@ suite('ProtocolServerHandler', () => { toolCallId: 'tool-1', toolName: 'runTask', displayName: 'Run Task', - toolClientId: 'client-tools', + contributor: { kind: ToolCallContributorKind.Client, clientId: 'client-tools' }, }); stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallReady, @@ -998,7 +998,7 @@ suite('ProtocolServerHandler', () => { toolCallId: 'tool-1', toolName: 'runTask', displayName: 'Run Task', - toolClientId: 'client-tools', + contributor: { kind: ToolCallContributorKind.Client, clientId: 'client-tools' }, }); stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallReady, @@ -1050,7 +1050,7 @@ suite('ProtocolServerHandler', () => { toolCallId: 'tool-1', toolName: 'runTask', displayName: 'Run Task', - toolClientId: 'client-tools', + contributor: { kind: ToolCallContributorKind.Client, clientId: 'client-tools' }, }); stateManager.dispatchServerAction(sessionUri, { type: ActionType.SessionToolCallReady, diff --git a/src/vs/platform/agentHost/test/node/reducers.test.ts b/src/vs/platform/agentHost/test/node/reducers.test.ts index 58489d64538bb..2144ef144cbc2 100644 --- a/src/vs/platform/agentHost/test/node/reducers.test.ts +++ b/src/vs/platform/agentHost/test/node/reducers.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { changesetReducer, sessionReducer } from '../../common/state/protocol/reducers.js'; import { ActionType } from '../../common/state/sessionActions.js'; -import { ChangesetStatus, CustomizationLoadStatus, MessageKind, SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, SessionLifecycle, SessionStatus, ToolCallConfirmationReason, type AgentCustomization, type ChangesetState, type Customization, type PluginCustomization, type SessionState } from '../../common/state/sessionState.js'; +import { ChangesetStatus, ChangesetOperationStatus, CustomizationLoadStatus, MessageKind, SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, SessionLifecycle, SessionStatus, ToolCallConfirmationReason, type AgentCustomization, type ChangesetState, type Customization, type PluginCustomization, type SessionState } from '../../common/state/sessionState.js'; import { CustomizationType } from '../../common/state/protocol/state.js'; function makeSession(): SessionState { @@ -227,13 +227,13 @@ suite('changesetReducer', () => { }); test('ChangesetOperationsChanged with array replaces operations', () => { - const ops = [{ id: 'stage', label: 'Stage', scopes: [] }]; + const ops = [{ id: 'stage', label: 'Stage', scopes: [], status: ChangesetOperationStatus.Idle }]; 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, operations: [{ id: 'stage', label: 'Stage', scopes: [] }] }); + const seeded = changesetReducer(ready, { type: ActionType.ChangesetOperationsChanged, operations: [{ id: 'stage', label: 'Stage', scopes: [], status: ChangesetOperationStatus.Idle }] }); const next = changesetReducer(seeded, { type: ActionType.ChangesetOperationsChanged, operations: undefined }); assert.strictEqual(next.operations, undefined); }); diff --git a/src/vs/platform/agentPlugins/common/pluginParsers.ts b/src/vs/platform/agentPlugins/common/pluginParsers.ts index a7c70a5b0b30d..6f27a16be942d 100644 --- a/src/vs/platform/agentPlugins/common/pluginParsers.ts +++ b/src/vs/platform/agentPlugins/common/pluginParsers.ts @@ -14,7 +14,7 @@ import { URI } from '../../../base/common/uri.js'; import { IFileService } from '../../files/common/files.js'; import { parseFrontMatter } from '../../../base/common/yaml.js'; import { IMcpRemoteServerConfiguration, IMcpServerConfiguration, IMcpStdioServerConfiguration, McpServerType } from '../../mcp/common/mcpPlatformTypes.js'; -import { CustomizationType, type AgentCustomization, type HookCustomization, type McpServerCustomization, type RuleCustomization, type SkillCustomization } from '../../agentHost/common/state/protocol/state.js'; +import { CustomizationType, McpServerStatus, type AgentCustomization, type HookCustomization, type McpServerCustomization, type RuleCustomization, type SkillCustomization } from '../../agentHost/common/state/protocol/state.js'; import { customizationId } from '../../agentHost/common/state/sessionState.js'; // --------------------------------------------------------------------------- @@ -238,6 +238,8 @@ function makeMcpServerCustomization(definitionUri: URI, name: string): McpServer id: buildChildId(definitionUri, `mcp=${encodeURIComponent(name)}`), uri: definitionUri.toString(), name, + enabled: true, + state: { kind: McpServerStatus.Starting }, }; } diff --git a/src/vs/platform/agentPlugins/test/common/pluginParsers.test.ts b/src/vs/platform/agentPlugins/test/common/pluginParsers.test.ts index f238130cb8ef6..7d7eda096448b 100644 --- a/src/vs/platform/agentPlugins/test/common/pluginParsers.test.ts +++ b/src/vs/platform/agentPlugins/test/common/pluginParsers.test.ts @@ -7,10 +7,10 @@ import assert from 'assert'; import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { McpServerType } from '../../../mcp/common/mcpPlatformTypes.js'; -import { CustomizationType, type McpServerCustomization } from '../../../agentHost/common/state/protocol/state.js'; +import { CustomizationType, McpServerStatus, type McpServerCustomization } from '../../../agentHost/common/state/protocol/state.js'; function stubMcpCustomization(): McpServerCustomization { - return { type: CustomizationType.McpServer, id: 'stub', uri: 'file:///plugin', name: 'test' }; + return { type: CustomizationType.McpServer, id: 'stub', uri: 'file:///plugin', name: 'test', enabled: true, state: { kind: McpServerStatus.Starting } }; } import { parseComponentPathConfig, 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 b39b70b124935..c7fd1b6d2dea0 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 @@ -1050,7 +1050,7 @@ suite('LocalAgentHostSessionsProvider', () => { agentHost.setSessionState(rawId, sessionTypeId, { ...state, customizations: [{ - ...customizations[0], + ...(customizations[0] as Extract), children: [{ type: CustomizationType.Agent, id: 'agent://only', uri: 'agent://only', name: 'only' }], }], }); 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 eeab0f9015b2e..c96090e3ceebf 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts @@ -152,6 +152,9 @@ export class AgentCustomizationItemProvider extends Disposable implements ICusto for (const sessionCustomization of customizations) { if (isDirectoryCustomization(sessionCustomization)) { directoryCustomizations.push(sessionCustomization); + } else if (sessionCustomization.type === CustomizationType.McpServer) { + // Bare MCP server entries aren't shown as plugin items in this view. + continue; } else { const isBundleItem = isSyntheticBundle(sessionCustomization); const isClientSynced = sessionCustomization.clientId !== undefined; 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 0ee4efb47f9fd..9b3caafbc9e0a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts @@ -23,7 +23,7 @@ import { AgentProvider, AgentSession, type IAgentConnection } from '../../../../ import { IAgentSubscription, observableFromSubscription } from '../../../../../../platform/agentHost/common/state/agentSubscription.js'; import { SessionTruncatedAction } from '../../../../../../platform/agentHost/common/state/protocol/actions.js'; import { CompletionItemKind as AhpCompletionItemKind, type CompletionItem as AhpCompletionItem } from '../../../../../../platform/agentHost/common/state/protocol/commands.js'; -import { ConfirmationOptionKind, TerminalClaimKind, ToolResultContentType, type ConfirmationOption, type ProtectedResourceMetadata, type SessionActiveClient } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; +import { ConfirmationOptionKind, TerminalClaimKind, ToolCallContributorKind, ToolResultContentType, type ConfirmationOption, type ProtectedResourceMetadata, type SessionActiveClient } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; import { ActionType, SessionTurnStartedAction, type ClientSessionAction, type SessionAction, type SessionInputCompletedAction } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; import { AHP_AUTH_REQUIRED, ProtocolError } from '../../../../../../platform/agentHost/common/state/sessionProtocol.js'; import { buildSubagentSessionUri, getToolSubagentContent, MessageAttachmentKind, MessageKind, PendingMessageKind, ResponsePartKind, SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, StateComponents, ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallStatus, TurnState, type ClientPluginCustomization, type ICompletedToolCall, type MarkdownResponsePart, type Message, type MessageAttachment, type ModelSelection, type ReasoningResponsePart, type RootState, type SessionInputAnswer, type SessionInputRequest, type SessionState, type ToolCallResponsePart, type ToolCallState, type Turn, type UsageInfo } from '../../../../../../platform/agentHost/common/state/sessionState.js'; @@ -1496,7 +1496,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC observedSubagentToolIds: Set, ): void { const initial = part$.get().toolCall; - if (initial.toolClientId === this._config.connection.clientId) { + const contributor = initial.contributor; + if (contributor?.kind === ToolCallContributorKind.Client && contributor.clientId === this._config.connection.clientId) { this._setupClientToolCall(initial, part$, store, opts); } else { this._setupServerToolCall(initial, part$, store, opts, observedSubagentToolIds); 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 2da289e845e35..de7c1cb501c58 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 @@ -20,7 +20,7 @@ import { isSessionAction, type ActionEnvelope, type IRootConfigChangedAction, ty import { buildSubagentSessionUri, MessageKind, SessionLifecycle, SessionStatus, createSessionState, StateComponents, type SessionState, type SessionSummary, type RootState } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { sessionReducer } from '../../../../../../platform/agentHost/common/state/sessionReducers.js'; import { ActionType } from '../../../../../../platform/agentHost/common/state/protocol/actions.js'; -import { ToolCallConfirmationReason, ToolResultContentType } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; +import { ToolCallConfirmationReason, ToolCallContributorKind, ToolResultContentType } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; import { IChatAgentService } from '../../../common/participants/chatAgents.js'; import { IChatProgress, IChatService, IChatToolInvocation, ToolConfirmKind } from '../../../common/chatService/chatService.js'; import { IChatEditingService } from '../../../common/editing/chatEditingService.js'; @@ -587,7 +587,7 @@ suite('AgentHostClientTools', () => { toolCallId: 'tool-call-1', toolName: 'runTask', displayName: 'Run Task', - toolClientId: connection.clientId, + contributor: { kind: ToolCallContributorKind.Client, clientId: connection.clientId }, } as SessionAction); connection.applySessionAction(URI.parse(backendSession), { type: ActionType.SessionToolCallReady, @@ -634,7 +634,7 @@ suite('AgentHostClientTools', () => { toolCallId: 'tool-call-1', toolName: 'runTask', displayName: 'Run Task', - toolClientId: connection.clientId, + contributor: { kind: ToolCallContributorKind.Client, clientId: connection.clientId }, } as SessionAction); connection.applySessionAction(URI.parse(backendSession), { type: ActionType.SessionToolCallReady, @@ -718,7 +718,7 @@ suite('AgentHostClientTools', () => { toolCallId: 'inner-tool-call-1', toolName: 'runTask', displayName: 'Run Task', - toolClientId: connection.clientId, + contributor: { kind: ToolCallContributorKind.Client, clientId: connection.clientId }, }); connection.applySessionAction(URI.parse(subagentBackendSession), { type: ActionType.SessionToolCallReady, diff --git a/src/vs/workbench/contrib/chat/test/common/plugins/convertBareEnvVarsToVsCodeSyntax.test.ts b/src/vs/workbench/contrib/chat/test/common/plugins/convertBareEnvVarsToVsCodeSyntax.test.ts index 36adcf7faba1e..3bcad8b03762b 100644 --- a/src/vs/workbench/contrib/chat/test/common/plugins/convertBareEnvVarsToVsCodeSyntax.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/plugins/convertBareEnvVarsToVsCodeSyntax.test.ts @@ -8,11 +8,11 @@ import { URI } from '../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { IMcpRemoteServerConfiguration, IMcpStdioServerConfiguration, McpServerType } from '../../../../../../platform/mcp/common/mcpPlatformTypes.js'; import { convertBareEnvVarsToVsCodeSyntax as convertBareEnvVarsToVsCodeSyntaxRaw } from '../../../common/plugins/agentPluginServiceImpl.js'; -import { CustomizationType, type McpServerCustomization } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; +import { CustomizationType, McpServerStatus, type McpServerCustomization } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; import type { IMcpServerDefinition } from '../../../../../../platform/agentPlugins/common/pluginParsers.js'; function stubMcpCustomization(): McpServerCustomization { - return { type: CustomizationType.McpServer, id: 'stub', uri: 'file:///test', name: 'test' }; + return { type: CustomizationType.McpServer, id: 'stub', uri: 'file:///test', name: 'test', enabled: true, state: { kind: McpServerStatus.Starting } }; } /**