agentHost: surface client custom agents when opening an existing session#324715
agentHost: surface client custom agents when opening an existing session#324715sandy081 wants to merge 1 commit into
Conversation
The Agents Window agent picker lists custom agents from `getEffectiveAgents(SessionState.customizations)` — the host's merged view of host-configured, session-discovered, and client-contributed plugins. Client-installed VS Code plugins reach the host only via `session/activeClientSet` (push-only; there is no host pull API for them). `provideChatSessionContent` intentionally does not register the active client on open (to avoid changing tool-dedup ownership for another client's in-flight turn), and the host drops active clients on disconnect. So a session created in this process shows the client's custom agents (createSession carries `activeClient`), but a reopened existing session — or one created before the plugins were installed — shows none. Publish a customizations-only active-client entry (`tools: []`) on open, and extend the customization-sync autorun to also publish for an opened existing session when the client's customizations resolve after open. Tools stay deferred to the first turn (`_ensureActiveClientForMessage`), so viewing a session never changes tool ownership for another client's in-flight turn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a bug in the Agents Window where opening an existing agent-host session (e.g. Copilot CLI) showed no client-contributed custom agents/plugins in the agent picker, while creating a new session showed them correctly. The agent picker derives its list from getEffectiveAgents(SessionState.customizations), and client-installed plugins only reach the host via a push-only session/activeClientSet message. Because provideChatSessionContent deliberately avoided registering the active client on open (to preserve tool-dedup ownership for another client's in-flight turn), a reopened session had no client customizations. The fix decouples "publishing customizations" from "claiming tool ownership": it publishes a customizations-only (tools: []) active-client entry on open, while still deferring tools to the first turn.
Changes:
- Add
_publishClientCustomizationsOnOpen(replacing_dispatchActiveClient) and call it fromprovideChatSessionContentfor existing sessions; no-op if this client is already active so tool contributions are never downgraded. - Extend the customization-sync autorun to preserve an existing entry's
tools({ ...existing, customizations }) and to publish customizations-only for an opened existing session whose customizations resolve after open. - Add a unit test and document the design decision in the sessions skill file.
Show a summary per file
| File | Description |
|---|---|
src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts |
Introduces customizations-only publish on open, reworks the customization-sync autorun to preserve existing tools and cover resolve-after-open, and removes the tools-pulling _dispatchActiveClient. |
src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostClientTools.test.ts |
Adds a test asserting provideChatSessionContent on an existing session dispatches a SessionActiveClientSet with tools: [] and the client's customizations; adds a mock active-client service. |
.github/skills/sessions/SKILL.md |
Documents the "publish customizations on open, defer tools to first turn" rule. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Medium
| // client yet. Publish customizations-only now (tools stay | ||
| // deferred to the first turn) so the picker still lists them. | ||
| // New sessions are skipped: they publish via `createSession`. | ||
| this._dispatchAction(backendSession, { | ||
| type: ActionType.SessionActiveClientSet, | ||
| activeClient: { clientId, tools: [], customizations: [...refs] }, |
There was a problem hiding this comment.
Tools should be readily accessible here via this._activeClientService.getClientTools(this._config.sessionType)
| this._dispatchAction(backendSession, { | ||
| type: ActionType.SessionActiveClientSet, | ||
| activeClient: { ...current, customizations }, | ||
| activeClient: { clientId, tools: [], customizations: [...customizations] }, |
There was a problem hiding this comment.
Likewise here.
I feel like we can probably dedupe this method, and the two autoruns where we call getClientTools and getCustomizations
Problem
In the Agents Window, opening an existing agent-host session (e.g. Copilot CLI) shows no custom agents in the agent picker — only "Agent" — even though the user has installed plugins/custom agents in VS Code. Creating a new session shows them correctly.
Root cause / AH design
The agent picker's list comes from
getEffectiveAgents(SessionState.customizations)— the host's merged view of three customization sources:AgentInfo.customizationsat root).Client-contributed customizations are push-only, client → host, via
session/activeClientSet. There is no host pull API for them: the host cannot enumerate the VS Code client's installed plugins; it only learns the specific refs the client publishes and then reads/expands those files.The client publishes in exactly two places today:
createSession(...)for a new session (carriesactiveClient), and_ensureActiveClientForMessage).provideChatSessionContentdeliberately does not register the active client on open (see comment added ine719715c9f6/ "support multiple active clients per session"), to avoid changing tool-dedup ownership for another client's in-flight turn. The host also drops active clients on disconnect. Result: a reopened existing session (or one created before the plugins were installed) has no client customizations in itsSessionState, so the picker is empty.Fix
Decouple "here are my customizations" from "I'm the client driving this turn (my tools win dedup)":
tools: []) via_publishClientCustomizationsOnOpen. No-op if this client is already an active client, so a real turn's tool contribution is never downgraded.!_isNewSessionResource) session that isn't yet an active client when the client's customizations resolve after open._ensureActiveClientForMessage), so merely viewing a session never changes tool ownership for another client's in-flight turn — honoring the original multi-active-client design intent.The customization-sync autorun now preserves the existing entry's
tools({ ...existing, customizations }) instead of pulling fresh tools, mirroring the sibling tools autorun ({ ...existing, tools }).Why not read local prompt/agent files in the picker?
That's what the regular editor does for its local sessions, but for agent-host sessions it would bypass the host's authoritative enabled/expanded/dedup view (children parsing, disabled state, session-type filtering). Keeping the host as the single source of truth is the robust design.
For discussion (@connor4312)
This intentionally relaxes the "don't register the active client on open" rule — but only for customizations, never tools. Does publishing a
tools: []active-client entry on open raise any concerns on the host side (e.g.requiresRestarton a live session, or active-client churn while another client is mid-turn)? The restart check is onsendMessagefrom an active client, so an on-open customizations-only publish should not restart another client's in-flight turn — but I'd like your read before we land this.Testing
agentHostClientTools.test.ts: added a test asserting that opening an existing session dispatches a singleSessionActiveClientSetwithtools: []and the client's customizations.npm run typecheck-clientis clean. (The full electron unit-test harness needs a local build; CI runs it.)