agentHost: configure plugins for remote agent hosts#312225
agentHost: configure plugins for remote agent hosts#312225joshspicer wants to merge 15 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enables remote Agent Host Protocol (AHP) servers to own and persist plugin customization configuration (rather than relying on client-side sync), and wires those host-owned plugins through the agent host state/actions pipeline into the Sessions AI customization UI.
Changes:
- Persist host-owned plugin customizations in agent host root config (
agent-host-config.json) and plumbroot/configChangedthrough server dispatch + side effects to republish live customization state. - Extend customization harness descriptors and the Plugins UI to support remote plugin items plus harness-provided toolbar/actions (add/remove/manage flows for remote hosts).
- Update protocol/state typings and add targeted agentHost + sessions tests for root config and customization republishing behavior.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/contrib/chat/common/customizationHarnessService.ts | Adds pluginActions, per-item actions, and itemKey to support remote/plugin management UI. |
| src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts | Renders remote plugin entries, groups them, and allows harness-driven toolbar + item context actions. |
| src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.ts | Updates dispatch typing to StateAction for broader action plumbing. |
| src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.ts | New browser tests for remote host plugin item identity and removal behavior. |
| src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts | Implements remote host plugin controller + provider; maps root/session state to plugin list items & actions. |
| src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts | Wires remote harness registration with plugin controller, provider, and required services. |
| src/vs/sessions/AI_CUSTOMIZATIONS.md | Documents external harnesses + pluginActions and how plugins are surfaced for remote harnesses. |
| src/vs/platform/agentHost/test/node/mockAgent.ts | Extends mock agent to support host/session customization APIs used by new side effects + tests. |
| src/vs/platform/agentHost/test/node/agentSideEffects.test.ts | Adds coverage for republishing customizations on root config changes + clearing client customizations. |
| src/vs/platform/agentHost/test/node/agentService.test.ts | Adds coverage ensuring root config changes are persisted to agent-host-config.json. |
| src/vs/platform/agentHost/node/protocolServerHandler.ts | Validates client-dispatchable actions (incl root actions) before forwarding to agent service. |
| src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts | Adds customizationDirectory so workspace-scoped host plugins resolve against the intended root. |
| src/vs/platform/agentHost/node/copilot/copilotAgent.ts | Implements host-owned + session-effective customization resolution and plugin parsing flow updates. |
| src/vs/platform/agentHost/node/agentSideEffects.ts | Applies host config to agents and republishes agent/session customizations when root config changes. |
| src/vs/platform/agentHost/node/agentService.ts | Accepts root config resource, persists root config on RootConfigChanged, and initializes providers with host customizations. |
| src/vs/platform/agentHost/node/agentHostStateManager.ts | Broadens client dispatch to StateAction and exposes session URIs for republish flows. |
| src/vs/platform/agentHost/node/agentHostServerMain.ts | Provides default persisted root-config path when starting standalone agent host server. |
| src/vs/platform/agentHost/node/agentHostMain.ts | Provides default persisted root-config path when starting agent host utility process. |
| src/vs/platform/agentHost/node/agentConfigurationService.ts | Introduces root config load/validate/persist support and initializes root config state. |
| src/vs/platform/agentHost/electron-browser/agentHostService.ts | Updates action dispatch typing to StateAction. |
| src/vs/platform/agentHost/common/state/protocol/version/registry.ts | Registers new action version mapping (incl SessionActivityChanged). |
| src/vs/platform/agentHost/common/state/protocol/state.ts | Adds customization scoping + source metadata and session activity field. |
| src/vs/platform/agentHost/common/state/protocol/reducers.ts | Updates reducer and isClientDispatchable to cover root actions; adds session activity reducer case. |
| src/vs/platform/agentHost/common/state/protocol/actions.ts | Adds SessionActivityChanged action and extends StateAction union. |
| src/vs/platform/agentHost/common/state/protocol/action-origin.generated.ts | Regenerates action origin unions and dispatchability map for new root/session actions. |
| src/vs/platform/agentHost/common/state/protocol/.ahp-version | Bumps synced protocol version hash. |
| src/vs/platform/agentHost/common/state/agentSubscription.ts | Broadens optimistic dispatch signature to StateAction. |
| src/vs/platform/agentHost/common/agentService.ts | Extends agent interfaces for host/session customizations and updates dispatch typing to StateAction. |
| src/vs/platform/agentHost/common/agentHostCustomizationConfig.ts | New schema + helpers for persisted host customization config and scope matching. |
| src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts | Updates dispatch typing to StateAction. |
| src/vs/platform/agentHost/browser/nullAgentHostService.ts | Updates dispatch typing to StateAction. |
Copilot's findings
Comments suppressed due to low confidence (1)
src/vs/platform/agentHost/common/state/agentSubscription.ts:464
dispatchOptimisticnow acceptsStateAction(so clients can send root actions likeroot/configChanged), but it still only applies optimistic updates for session actions. This means root-config updates won’t be reflected locally until the server echo arrives, and it also conflicts with the comment that root state is “server-only mutations”. Either implement write-ahead/reconciliation for client-dispatchable root actions (at leastRootConfigChanged) or explicitly document/handle why root actions are excluded here.
dispatchOptimistic(action: StateAction): number {
if (isSessionAction(action)) {
const entry = this._subscriptions.get(URI.parse(action.session));
if (entry && entry.sub instanceof SessionStateSubscription) {
return entry.sub.applyOptimistic(action);
}
}
return this._seqAllocator();
}
- Files reviewed: 31/31 changed files
- Comments generated: 1
| private async filterPlugins(): Promise<void> { | ||
| const query = this.searchQuery.toLowerCase().trim(); | ||
| const allPlugins = this.agentPluginService.plugins.get(); | ||
| this.remoteItems = [...await this.getRemotePluginItems(query)]; | ||
|
|
There was a problem hiding this comment.
filterPlugins is now async and awaits remote provider results, but there’s no concurrency guard. Multiple calls (search typing, autorun refreshes, toggleGroup, etc.) can overlap and apply results out of order, leading to stale list contents. Consider tracking a request id / CancellationTokenSource per filter run and only applying results for the latest invocation; also ensure callers (e.g. refresh) await/return the promise or explicitly void it to avoid unhandled rejections.
655622b to
5266a5c
Compare
Sync CustomizationRef.scope, SessionCustomization.source, and RootConfigChanged action from the agent-host-protocol spec branch (8d19730). Add agentHostCustomizationConfig.ts with the JSON schema and validation helpers for host-owned plugin configuration. Co-authored-by: Copilot <[email protected]>
Add AgentConfigurationService for root config read/write/persist to agent-host-config.json. Wire root action dispatch and side effects so that host plugin changes are persisted and republished as session customizations. Implement PluginController in copilotAgent.ts to merge host-owned and client-synced plugins, resolve them into IParsedPlugin snapshots, and feed them into the SDK via _buildSessionConfig(). Co-authored-by: Copilot <[email protected]>
Add RemoteAgentPluginController for add/remove plugin commands and RemoteAgentCustomizationItemProvider that surfaces host-owned and session-synced plugins. Expand each plugin directory via IFileService to discover individual skills, agents, instructions, and prompts for per-type sections. Extend pluginListWidget with remote item rendering, toolbar actions, group headers, and live refresh on itemProvider changes. Co-authored-by: Copilot <[email protected]>
Add tests for host config dispatch, side effect republishing, scope filtering, and the remote harness item provider (distinct item keys for scope-distinct plugins, client-synced vs host-owned separation). Co-authored-by: Copilot <[email protected]>
Add ICustomizationDisableProvider interface (opt-out model) alongside the existing ICustomizationSyncProvider. Add AgentCustomizationDisableProvider implementation and LocalAgentHostCustomizationItemProvider with shared enumerateLocalCustomizationsForHarness() and resolveCustomizationRefs() helpers. Co-authored-by: Copilot <[email protected]>
Replace ICustomizationSyncProvider usage with ICustomizationDisableProvider across all consumers. Simplify ProviderCustomizationItemSource to a single required itemProvider (no more dual local/remote path). Update harness descriptors to use disableProvider. Listen to both disable provider and prompts service change events for auto-sync. Add throttling for bundler re-resolves. Co-authored-by: Copilot <[email protected]>
Delete AgentCustomizationSyncProvider and its tests. The opt-out ICustomizationDisableProvider is now the only sync model. Co-authored-by: Copilot <[email protected]>
- Fix synthetic bundle URI scheme: synced-customization:// URIs must not be wrapped as agent-host:// since the server lacks that scheme; expansion now reads the client in-memory FS directly - Suppress client-synced plugin entries from the Plugins list (they are already shown under 'Enabled Locally'); their contents still expand into per-type tabs (Skills, Agents, etc.) - Propagate childGroupKey through expansion so host-originated items land in the 'Remote' group and client-synced items land in 'Client' group - Rename groups to 'Remote' / 'Client' in both the Plugins view and the per-type tabs - Remove redundant 'Remote Host' badge from host-configured plugin items - Add dotfile filter (.DS_Store etc.) in _collectFromTypeDir - Include PromptsStorage.extension in SYNCABLE_STORAGE_SOURCES so built-in extension skills reach the agent host SDK - Remove checkbox column from Agent Customizations list widget - Enhance debug report with installed harnesses and plugins (Stages 5-6) - Fix test: add IPromptsService stub to agentHostChatContribution test Co-authored-by: Copilot <[email protected]>
5266a5c to
b55a44d
Compare
Skills are conventionally directories containing SKILL.md, so the file
locator returns URIs like `/skills/skill-a/SKILL.md`. The bundler used
`basename(uri)` for the destination filename, causing every skill to
overwrite the same `skills/SKILL. only the last one survived.md`
Fix: detect SKILL.md filenames and preserve the parent directory
structure (`skills/{skillName}/SKILL.md`), matching the Open Plugin
convention that the expansion code already handles.
Co-authored-by: Copilot <[email protected]>
Reverts the broad StateAction widening on the dispatchAction/dispatch interface boundary. Instead of accepting any StateAction (which includes server-only emissions like SessionReady, SessionDelta, etc.), the API now accepts the precise ClientAction union: ClientAction = ClientRootAction | ClientSessionAction | ClientTerminalAction This restores compile-time safety: passing a server-only action to dispatch() is now a type error. The new type is exported from sessionActions.ts alongside the existing SessionAction/TerminalAction aliases. Internal server-side state management (reducers, subscriptions, dispatchServerAction, dispatchClientAction) continues to use the broad StateAction type as appropriate. The one root action clients legitimately dispatch (RootConfigChanged, used to push plugin configuration changes) is included in ClientRootAction, so the remote harness's dispatchCustomizations() call compiles correctly. Co-authored-by: Copilot <[email protected]>
The provideChatSessionCustomizations method was skipping client-synced
items from the top-level items map with the comment that the local
'Enabled Locally' section already shows them. This is wrong for the
remote harness there is no local section, and client-syncedcontext
plugins should appear as distinct 'Client' group entries alongside
host-owned 'Remote' group entries.
Fixes the failing test:
RemoteAgentHostCustomizationHarness
provider keeps client-synced entries distinct from host-owned entries
Co-authored-by: Copilot <[email protected]>
Covers: - Client-synced vs host-owned group assignment (remote-client/remote-host) - Synthetic bundle hidden from top-level but expanded for children - Synced-customization scheme URI preserved (not wrapped as agent-host://) - Status/statusMessage propagation from session customizations - Change event firing on SessionCustomizationsChanged action - Remove action only on host items, not client-synced items - Multi-entry remove dispatch - Multiple client-synced entries with distinct keys - SKILL.md collision prevention with subdirectory layout - Mixed SKILL.md and non-SKILL.md skill coexistence - Nonce includes subdirectory path for SKILL.md files - Rebundle clears previous tree - Bundle description includes file count Co-authored-by: Copilot <[email protected]>
Dynamic groups (e.g. remote-host, remote-client) were appended after the built-in group, causing built-in to appear above them. Insert dynamic groups before the built-in entry so it always stays last. Co-authored-by: Copilot <[email protected]>
Remove the 'Add Workspace Plugin' button and its backing methods (addPluginForWorkspace, getWorkspaceScope). Plugins are now always added at the host scope. Simplifies addConfiguredPlugin accordingly. Co-authored-by: Copilot <[email protected]>
The 'Client' group header already communicates that entries are synced from the client. The full-width 'Synced' badge was redundant and visually inconsistent with inline badges elsewhere. Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (1)
src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts:963
filterPluginsis nowasync(awaiting remote provider results). Please ensure all call sites treat it as such (useawait/return/voidconsistently). At leastrefresh()currently calls it without awaiting, which can leave a floating promise and make refresh complete before the list state is updated.
- Files reviewed: 49/49 changed files
- Comments generated: 2
| * `SKILL.md`, but the local-sync bundler writes them as flat files; | ||
| * both layouts are accepted. |
There was a problem hiding this comment.
The comment says “local-sync bundler writes [skills] as flat files”, but SyncedCustomizationBundler now preserves per-skill subdirectories for SKILL.md. Updating this comment to reflect the current on-disk layout (and/or clarifying that legacy bundles or non-SKILL.md skill files may still be flat) would prevent confusion when debugging expansion behavior.
| * `SKILL.md`, but the local-sync bundler writes them as flat files; | |
| * both layouts are accepted. | |
| * `SKILL.md`, and synced bundles may preserve that per-skill | |
| * directory layout. Flat skill files can still appear for legacy | |
| * bundles or non-`SKILL.md` skill files, so both layouts are | |
| * accepted. |
| if (itemProvider) { | ||
| itemProviderChangeDisposable.value = itemProvider.onDidChange(() => { | ||
| if (!this.browseMode) { | ||
| this.refresh(); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Inside the itemProvider onDidChange handler, this.refresh() is called without await/void, which leaves a floating promise (and can drop errors if refresh ever starts throwing). Use void this.refresh() (or make the callback async and await).
This issue also appears on line 959 of the same file.
Summary
Why
When a user connects from a browser or phone, they may not have any local customization state to sync into the remote host. This change lets the remote AHP host configure plugins directly, then feeds those resolved plugins into the Copilot-backed runtime so sessions can use them without relying on local client state.
Protocol / Spec
Paired draft PR: microsoft/agent-host-protocol#77
Notes for reviewers
agent-host-config.jsonvia AHP root configValidation
npm run compile-check-ts-nativenode ./build/hygiene.ts $(git diff --name-only)npm run valid-layers-checknpm run transpile-clientnpm run test-node -- --run src/vs/platform/agentHost/test/node/agentService.test.js src/vs/platform/agentHost/test/node/agentSideEffects.test.js src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.js./scripts/test.shstill does not boot Electron correctly in this shell (appis undefined during startup), so the focused node runner remains the reliable validation path here.