Repository navigation
Conversation
Hand-written subset of the Agent Client Protocol v1 wire types (schema-v1.25.0) and an NDJSON stdio JSON-RPC 2.0 client modelled on codexAppServerClient. Groundwork for a generic ACP agent host provider (microsoft#340860). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AcpTurnMapper turns the session/update stream of one ACP prompt turn into agent host chat actions: markdown/reasoning parts and deltas, the tool call start/ready/complete lifecycle (leaving ready to the host when the agent asks for permission) and the terminal turn action derived from the ACP stop reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AcpAgent implements IAgent for any agent that speaks the Agent Client Protocol (e.g. `qwen --acp`, `opencode acp`). One lazily started process serves all chats of a configured agent; chats map to ACP sessions (session/new, resume or load on restore, prompt, cancel, close), model selection uses the `model` config option, permission requests flow through the host confirmation path, and fs/* requests are confined to the session working directory. Agents are configured with the experimental, application-scoped and restricted `chat.agentHost.acpAgents` setting, mirrored to the agent host as the `acpAgents` root config key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each well-formed `chat.agentHost.acpAgents` entry registers an AcpAgent as `acp-<id>`; entries added later register on root config change. Like the Codex provider, registration is one-way, so removing an entry takes effect after an agent host restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A missing or non-executable command makes `spawn` emit `error` without `exit`, which would crash the agent host. Treat it as an exit and report "Failed to start <agent> (`<command>`)" from the failing chat operation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ACP agents report models only per session, so the Agents window listed ACP harnesses as "No models available" and disabled them. Publish an "Agent Default" model up front and add the agent's own models once a session reports them; selecting the default (or a model the session does not offer) leaves the agent's choice unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Qwen Code routes reads and writes of its own state (memories, session records under ~/.qwen) through fs/* when the client offers it, so confining fs/* to the session working directory broke the agent. The agent process already runs with the user's permissions, so the confinement protected nothing. Advertise no fs/* (or terminal/*) capability and let agents use their own file access, as they do from a shell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use hasKey instead of the `in` operator, throw Error objects in tests and drop `any` from the fake ACP agent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
🟡 Changes recommended
Managed-policy bypass and cross-platform and lifecycle defects must be addressed.
4 open findings
What changed in this PR
Adds an experimental Agent Client Protocol provider to run user-configured ACP agents through Agent Host.
Changes:
- Adds ACP configuration, validation, and provider registration.
- Implements JSON-RPC transport, session lifecycle, permissions, models, and turn mapping.
- Adds unit coverage for the client, agent, registration, and turn mapping.
| File | Description |
|---|---|
src/vs/platform/agentHost/test/node/acp/acpTurnMapper.test.ts |
Tests ACP response mapping. |
src/vs/platform/agentHost/test/node/acp/acpTestUtils.ts |
Provides an in-memory ACP process. |
src/vs/platform/agentHost/test/node/acp/acpClient.test.ts |
Tests JSON-RPC transport behavior. |
src/vs/platform/agentHost/test/node/acp/acpAgentRegistration.test.ts |
Tests configuration validation and registration. |
src/vs/platform/agentHost/test/node/acp/acpAgent.test.ts |
Tests ACP agent lifecycle and permissions. |
src/vs/platform/agentHost/node/agentHostServerMain.ts |
Registers ACP providers in server hosts. |
src/vs/platform/agentHost/node/agentHostMain.ts |
Registers ACP providers locally. |
src/vs/platform/agentHost/node/acp/acpTurnMapper.ts |
Maps ACP updates to chat actions. |
src/vs/platform/agentHost/node/acp/acpProtocol.ts |
Defines the used ACP wire types. |
src/vs/platform/agentHost/node/acp/acpClient.ts |
Implements ACP JSON-RPC transport. |
src/vs/platform/agentHost/node/acp/acpAgentRegistration.ts |
Validates and registers configured agents. |
src/vs/platform/agentHost/node/acp/acpAgent.ts |
Implements the ACP-backed agent provider. |
src/vs/platform/agentHost/common/agentService.ts |
Defines the ACP setting identifier. |
src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts |
Registers the user-facing setting. |
src/vs/platform/agentHost/common/agentHostSchema.ts |
Adds ACP root configuration schema. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| scope: ConfigurationScope.APPLICATION, | ||
| restricted: true, | ||
| tags: ['experimental'], | ||
| agentHost: { key: AgentHostAcpAgentsConfigKey }, |
There was a problem hiding this comment.
Fixed in 2176b0d: new chat.agentHost.acpAgent.enabled owned by the AcpAgents3PIntegration policy (reuses thirdPartyAgentEnabledValue). The host registers ACP providers only while the mirrored acpAgentEnabled root key is true, and the workbench hides acp-* providers otherwise. Note: export-policy-data needs the private distro product.json, so policyData.jsonc contains only the generated entry for this policy — a maintainer may want to re-run the export.
| let msg: IWireMessage; | ||
| try { | ||
| msg = JSON.parse(line); | ||
| } catch { | ||
| // Agents sometimes print diagnostics on stdout; never echo the line, it may contain user content. | ||
| this._log('warn', `ignoring non-JSON line from agent (${line.length} chars)`); | ||
| continue; | ||
| } | ||
| this._dispatch(msg); |
There was a problem hiding this comment.
Fixed in 2176b0d: non-object JSON (null, numbers, strings, arrays) is now ignored with a warning; covered by a test.
| const child = spawn(config.command, [...(config.args ?? [])], { | ||
| env: { ...process.env, ...config.env }, | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| }); |
There was a problem hiding this comment.
Fixed in 2176b0d: the launcher now goes through formatSubprocessArguments, moved from extHostMcpNode to base/node/processes so MCP stdio servers and ACP agents share the same CVE-2024-27980 handling. I couldn't test on Windows locally.
| const state = this._track(chat, result.sessionId, cwd, connection); | ||
| this._applySetup(state, result); | ||
| if (options?.model) { | ||
| await this._changeModel(chat, options.model); | ||
| } |
There was a problem hiding this comment.
Fixed in 2176b0d: a failure after session/new closes the native session and untracks the chat; a failed restore untracks it so it can be retried. Both covered by tests.
- Gate ACP providers on a new `chat.agentHost.acpAgent.enabled` setting owned by the `AcpAgents3PIntegration` policy, which reuses thirdPartyAgentEnabledValue so managed settings and disabled preview features turn ACP agents off like the Claude and Codex harnesses. The host registers providers only while the mirrored `acpAgentEnabled` root key is true, and the workbench hides `acp-*` providers otherwise. policyData.jsonc carries only the generated entry for the new policy (export-policy-data needs the private distro product.json). - Ignore valid JSON lines that are not objects instead of throwing out of the stdout listener. - Close the native session when setup after session/new fails, and drop the tracked chat when a restore fails so it can be retried. - Launch Windows `.cmd`/`.bat` shims (npm-installed agents) through formatSubprocessArguments, moved from extHostMcpNode to base/node/processes so MCP stdio servers and ACP agents share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note for maintainers: the |


Part of #340860 (ACP provider proposal; follow-up to the locked #265496).
Adds an experimental, off-by-default Agent Host provider for agents that speak the Agent Client Protocol v1, so tools such as Qwen Code (
qwen --acp) and OpenCode (opencode acp) can run in the Agents window next to Copilot, Claude and Codex.What changes
chat.agentHost.acpAgent.enabled(defaulttrue), owned by the newAcpAgents3PIntegrationpolicy. It reusesthirdPartyAgentEnabledValue, so managed settings or disabled preview features turn ACP agents off like the Claude and Codex harnesses. The host registers ACP providers only while the mirroredacpAgentEnabledroot key istrue, and the workbench hidesacp-*providers otherwise.policyData.jsonccontains only the generated entry for this policy, becauseexport-policy-dataneeds the private distro product.json; a maintainer may want to re-run the export.chat.agentHost.acpAgents(experimental,APPLICATIONscope,restricted, default[]): a list of{ id, displayName?, command, args?, env? }, mirrored to the host as theacpAgentsroot config key. Workspace settings cannot spawn commands. No built-in presets; the description gives Qwen Code and OpenCode as examples.node/acp/acpClient.ts,acpProtocol.ts: an in-house JSON-RPC 2.0 / NDJSON stdio client modeled oncodexAppServerClient.ts, plus hand-written types for the subset of the ACP v1 schema (schema-v1.25.0) that is used. No new npm dependency.node/acp/acpAgent.ts:AcpAgent implements IAgent, one instance per configured agent with provider idacp-<id>. A single, lazily started process serves all chats.createChat→session/new. The native session id is persisted inproviderData.materializeChat→session/resume, falling back tosession/load.sendMessage→session/prompt, which resolves at the end of the turn; progress arrives assession/update.abort→session/cancel; dispose/release →session/close.modelconfig option. Agents only report models per session, so an "Agent Default" model is published before the first session.node/acp/acpTurnMapper.ts: mapssession/updateto chat actions: markdown/reasoning parts and deltas, the tool call start → ready → complete lifecycle, and the terminal turn action from the ACP stop reason.session/request_permissiongoes through the existingpending_confirmationflow, and the host's answer picks the matching ACP permission option..cmdfiles, so the launcher goes throughformatSubprocessArguments, moved fromextHostMcpNode.tstobase/node/processes.ts. MCP stdio servers and ACP agents now share the same CVE-2024-27980 handling. Not tested on Windows by me.node/acp/acpAgentRegistration.ts: validates the entries and registers providers inagentHostMain.tsandagentHostServerMain.ts. Registration is one-way, like Codex'sregisterCodexIfEnabled: entries added later register on root-config change, while removals take effect after an agent host restart (IAgentHostProviderServicehas no unregister).Deliberately out of scope
fs/*andterminal/*client capabilities are not advertised. Agents keep using their own file access and terminals, as they do from a shell. Qwen Code routes reads and writes of its own state (~/.qwen/...) throughfs/*when offered, so confiningfs/*to the session folder broke it. Tracking agent edits as VS Code edits can follow separately.session/list→onDidDiscoverChats) and a dynamic Harness filter insessionsListFilters.tsare left for a follow-up PR.mcpServers: []).How to test
Verified manually in a dev build (
./scripts/code.sh --agents) with Qwen Code 0.25.0 and OpenCode 1.18.34: both providers register, appear in the harness picker, stream a reply and complete the turn. A command that does not exist reports "Failed to start …" instead of crashing the agent host.Unit tests are in
src/vs/platform/agentHost/test/node/acp/(35 tests: client, turn mapper, agent against an in-memory scripted ACP agent, registration).eslintandhygienepass on the touched files. The existing agent host config, schema, policy and provider service tests, the MCP helper tests and the build policy tests still pass.🤖 Generated with Claude Code