Skip to content

Commit 0e86585

Browse files
authored
Merge pull request #95 from nikomatsakis/vscodelm-prototype
feat: VS Code Language Model Provider (experimental)
2 parents 32dca20 + 2dfcfae commit 0e86585

File tree

227 files changed

+5303
-46121
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

227 files changed

+5303
-46121
lines changed

Cargo.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,6 @@ clap = { version = "4.0", features = ["derive"] }
5656
which = "6.0"
5757
home = "0.5"
5858
fxhash = "0.2.1"
59+
60+
# Testing
61+
expect-test = "1.5"

md/SUMMARY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
- [Testing Implementation](./design/vscode-extension/testing-implementation.md)
3434
- [Packaging](./design/vscode-extension/packaging.md)
3535
- [Agent Registry](./design/vscode-extension/agent-registry.md)
36+
- [Language Model Provider](./design/vscode-extension/lm-provider.md)
37+
- [Language Model Tool Bridging](./design/vscode-extension/lm-tool-bridging.md)
3638
- [Implementation Status](./design/vscode-extension/implementation-status.md)
3739

3840
# References
@@ -45,6 +47,8 @@
4547

4648
- [MynahUI GUI Capabilities](./references/mynah-ui-guide.md)
4749
- [VSCode Webview Lifecycle](./references/vscode-webview-lifecycle.md)
50+
- [VSCode Language Model Tool API](./references/vscode-lm-tool-api.md)
51+
- [VSCode Language Model Tool Rejection](./references/vscode-lm-tool-rejection.md)
4852
- [Language Server Protocol Overview](./research/lsp-overview/README.md)
4953
- [Base Protocol](./research/lsp-overview/base-protocol.md)
5054
- [Language Features](./research/lsp-overview/language-features.md)

md/design/vscode-extension/implementation-status.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,27 @@ These allow protocol extensions beyond the ACP specification. Not currently need
9090
- [ ] Session restoration after VSCode restart
9191
- [ ] Workspace-specific state persistence
9292
- [ ] Tab history and conversation export
93+
94+
## Language Model Provider (Experimental)
95+
96+
> Set `symposium.enableExperimentalLM: true` in VS Code settings to enable.
97+
98+
This feature exposes ACP agents via VS Code's `LanguageModelChatProvider` API, allowing them to appear in the model picker for use by Copilot and other extensions.
99+
100+
**Status:** Experimental, disabled by default. May not be the right approach.
101+
102+
- [x] TypeScript: LanguageModelChatProvider registration
103+
- [x] TypeScript: JSON-RPC client over stdio
104+
- [x] TypeScript: Progress callback integration
105+
- [x] Rust: `vscodelm` subcommand
106+
- [x] Rust: Session actor with history management
107+
- [x] Rust: Tool bridging (symposium-agent-action for permissions)
108+
- [x] Rust: VS Code tools via synthetic MCP server
109+
- [x] Feature flag gating (`symposium.enableExperimentalLM`)
110+
- [ ] Fix: Multiple MCP tools cause invocation failures
111+
112+
**Known issue:** Tool invocation works with a single isolated tool but fails when multiple VS Code-provided tools are bridged. Root cause unknown.
113+
114+
**Open question:** VS Code LM consumers inject their own context (project details, editor state, etc.) into requests. ACP agents like Claude Code also inject context. These competing context layers may confuse the model, making the LM API better suited for raw model access than wrapping full agents.
115+
116+
See [Language Model Provider](./lm-provider.md) and [Tool Bridging](./lm-tool-bridging.md) for architecture details.
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
# Language Model Provider
2+
3+
> **Experimental:** This feature is disabled by default. Set `symposium.enableExperimentalLM: true` in VS Code settings to enable it.
4+
5+
This chapter describes the architecture for exposing ACP agents as VS Code Language Models via the `LanguageModelChatProvider` API (introduced in VS Code 1.104). This allows ACP agents to appear in VS Code's model picker and be used by any extension that consumes the Language Model API.
6+
7+
## Current Status
8+
9+
The Language Model Provider is experimental and may not be the right approach for Symposium.
10+
11+
**What works:**
12+
- Basic message flow between VS Code LM API and ACP agents
13+
- Session management with committed/provisional history model
14+
- Tool bridging architecture (both directions)
15+
16+
**Known issues:**
17+
- Tool invocation fails when multiple VS Code-provided tools are bridged to the agent. A single isolated tool works correctly, but when multiple tools are available, the model doesn't invoke them properly. The root cause is not yet understood.
18+
19+
**Open question:** VS Code LM consumers (like GitHub Copilot) inject their own context into requests - project details, file contents, editor state, etc. ACP agents like Claude Code also inject their own context. When both layers add context, they may "fight" each other, confusing the model. The LM API may be better suited for raw model access rather than wrapping agents that have their own context management.
20+
21+
## Overview
22+
23+
The Language Model Provider bridges VS Code's stateless Language Model API to ACP's stateful session model. When users select "Symposium" in the model picker, requests are routed through Symposium to the configured ACP agent.
24+
25+
```
26+
┌─────────────────────────────────────────────────────────────────┐
27+
│ VS Code │
28+
│ ┌───────────────────────────────────────────────────────────┐ │
29+
│ │ Language Model Consumer │ │
30+
│ │ (Copilot, other extensions, etc.) │ │
31+
│ └─────────────────────────┬─────────────────────────────────┘ │
32+
│ │ │
33+
│ ▼ │
34+
│ ┌───────────────────────────────────────────────────────────┐ │
35+
│ │ LanguageModelChatProvider (TypeScript) │ │
36+
│ │ │ │
37+
│ │ - Thin adapter layer │ │
38+
│ │ - Serializes VS Code API calls to JSON-RPC │ │
39+
│ │ - Forwards to Rust process │ │
40+
│ │ - Deserializes responses, streams back via progress │ │
41+
│ └─────────────────────────┬─────────────────────────────────┘ │
42+
└────────────────────────────┼────────────────────────────────────┘
43+
│ JSON-RPC (stdio)
44+
45+
┌─────────────────────────────────────────────────────────────────┐
46+
│ symposium-acp-agent vscodelm │
47+
│ │
48+
│ - Receives serialized VS Code LM API calls │
49+
│ - Manages session state │
50+
│ - Routes to ACP agent (or Eliza for prototype) │
51+
│ - Streams responses back │
52+
└─────────────────────────────────────────────────────────────────┘
53+
```
54+
55+
## Design Decisions
56+
57+
### TypeScript/Rust Split
58+
59+
The TypeScript extension is a thin adapter:
60+
- Registers as `LanguageModelChatProvider`
61+
- Serializes `provideLanguageModelChatResponse` calls to JSON-RPC
62+
- Sends to Rust process over stdio
63+
- Deserializes responses and streams back via `progress` callback
64+
65+
The Rust process handles all logic:
66+
- Session management
67+
- Message history tracking
68+
- ACP protocol (future)
69+
- Response streaming
70+
71+
This keeps the interesting logic in Rust where it's testable and maintainable.
72+
73+
### Session Management
74+
75+
VS Code's Language Model API is stateless: each request includes the full message history. ACP sessions are stateful. The Rust backend bridges this gap using a **History Actor** that tracks session state.
76+
77+
#### Architecture
78+
79+
```mermaid
80+
graph LR
81+
VSCode[VS Code] <--> HA[History Actor]
82+
HA <--> SA[Session Actor]
83+
SA <--> Agent[ACP Agent]
84+
```
85+
86+
- **History Actor**: Receives requests from VS Code, tracks message history, identifies new messages
87+
- **Session Actor**: Manages the ACP agent connection, handles streaming responses
88+
89+
#### Committed and Provisional History
90+
91+
The History Actor maintains two pieces of state:
92+
93+
- **Committed**: Complete `(User, Assistant)*` message pairs that VS Code has acknowledged. Always ends with an assistant message (or is empty).
94+
- **Provisional**: The current in-flight exchange: one user message `U` and the assistant response parts `A` we've sent so far (possibly empty).
95+
96+
#### Commit Flow
97+
98+
When we receive a new request, we compare its history against `committed + provisional`:
99+
100+
```mermaid
101+
sequenceDiagram
102+
participant VSCode as VS Code
103+
participant HA as History Actor
104+
participant SA as Session Actor
105+
106+
Note over HA: committed = [], provisional = (U1, [])
107+
108+
SA->>HA: stream parts P1, P2, P3
109+
Note over HA: provisional = (U1, [P1, P2, P3])
110+
HA->>VSCode: stream P1, P2, P3
111+
112+
SA->>HA: done streaming
113+
HA->>VSCode: response complete
114+
115+
VSCode->>HA: new request with history [U1, A1, U2]
116+
Note over HA: matches committed + provisional + new user msg
117+
Note over HA: commit: committed = [U1, A1]
118+
Note over HA: provisional = (U2, [])
119+
HA->>SA: new_messages = [U2], canceled = false
120+
```
121+
122+
The new user message `U2` confirms that VS Code received and accepted our assistant response `A1`. We commit the exchange and start fresh with `U2`.
123+
124+
#### Cancellation via History Mismatch
125+
126+
If VS Code sends a request that doesn't include our provisional content, the provisional work was rejected:
127+
128+
```mermaid
129+
sequenceDiagram
130+
participant VSCode as VS Code
131+
participant HA as History Actor
132+
participant SA as Session Actor
133+
134+
Note over HA: committed = [U1, A1], provisional = (U2, [P1, P2])
135+
136+
VSCode->>HA: new request with history [U1, A1, U3]
137+
Note over HA: doesn't match committed + provisional
138+
Note over HA: discard provisional
139+
Note over HA: provisional = (U3, [])
140+
HA->>SA: new_messages = [U3], canceled = true
141+
142+
SA->>SA: cancel downstream agent
143+
```
144+
145+
This happens when:
146+
- User cancels the chat in VS Code
147+
- User rejects a tool confirmation
148+
- User sends a different message while we were responding
149+
150+
The Session Actor receives `canceled = true` and propagates cancellation to the downstream ACP agent.
151+
152+
### Agent Configuration
153+
154+
The agent to use is specified per-request via the `agent` field in the JSON-RPC protocol. This is an `AgentDefinition` enum:
155+
156+
```typescript
157+
type AgentDefinition =
158+
| { eliza: { deterministic?: boolean } }
159+
| { mcp_server: McpServerStdio };
160+
161+
interface McpServerStdio {
162+
name: string;
163+
command: string;
164+
args: string[];
165+
env: Array<{ name: string; value: string }>;
166+
}
167+
```
168+
169+
The TypeScript extension reads the agent configuration from VS Code settings via the agent registry, resolves the distribution to get the actual command, and includes it in each request. The Rust backend dispatches based on the variant:
170+
171+
- **`eliza`**: Uses the in-process Eliza chatbot (useful for testing)
172+
- **`mcp_server`**: Spawns an external ACP agent process and manages sessions
173+
174+
## JSON-RPC Protocol
175+
176+
The protocol between TypeScript and Rust mirrors the `LanguageModelChatProvider` interface.
177+
178+
### Requests (TypeScript → Rust)
179+
180+
**`lm/provideLanguageModelChatResponse`**
181+
182+
Each request includes the agent configuration via the `agent` field, which is an `AgentDefinition` enum with two variants:
183+
184+
**External ACP agent (mcp_server)**:
185+
```json
186+
{
187+
"jsonrpc": "2.0",
188+
"id": 1,
189+
"method": "lm/provideLanguageModelChatResponse",
190+
"params": {
191+
"modelId": "symposium",
192+
"messages": [
193+
{ "role": "user", "content": [{ "type": "text", "value": "Hello" }] }
194+
],
195+
"agent": {
196+
"mcp_server": {
197+
"name": "my-agent",
198+
"command": "/path/to/agent",
199+
"args": ["--flag"],
200+
"env": [{ "name": "KEY", "value": "value" }]
201+
}
202+
}
203+
}
204+
}
205+
```
206+
207+
**Built-in Eliza (for testing)**:
208+
```json
209+
{
210+
"jsonrpc": "2.0",
211+
"id": 1,
212+
"method": "lm/provideLanguageModelChatResponse",
213+
"params": {
214+
"modelId": "symposium-eliza",
215+
"messages": [
216+
{ "role": "user", "content": [{ "type": "text", "value": "Hello" }] }
217+
],
218+
"agent": {
219+
"eliza": { "deterministic": true }
220+
}
221+
}
222+
}
223+
```
224+
225+
The Rust backend dispatches based on the variant - spawning an external process for `mcp_server` or using the in-process Eliza for `eliza`.
226+
227+
### Notifications (Rust → TypeScript)
228+
229+
**`lm/responsePart`** - Streams response chunks
230+
```json
231+
{
232+
"jsonrpc": "2.0",
233+
"method": "lm/responsePart",
234+
"params": {
235+
"requestId": 1,
236+
"part": { "type": "text", "value": "How " }
237+
}
238+
}
239+
```
240+
241+
**`lm/responseComplete`** - Signals end of response
242+
```json
243+
{
244+
"jsonrpc": "2.0",
245+
"method": "lm/responseComplete",
246+
"params": {
247+
"requestId": 1
248+
}
249+
}
250+
```
251+
252+
### Response
253+
254+
After all parts are streamed, the request completes:
255+
```json
256+
{
257+
"jsonrpc": "2.0",
258+
"id": 1,
259+
"result": {}
260+
}
261+
```
262+
263+
## Implementation Status
264+
265+
- [x] Rust: `vscodelm` subcommand in symposium-acp-agent
266+
- [x] Rust: JSON-RPC message parsing
267+
- [x] Rust: Eliza integration for testing
268+
- [x] Rust: Response streaming
269+
- [x] Rust: Configurable agent backend (McpServer support)
270+
- [x] Rust: Session actor with ACP session management
271+
- [x] TypeScript: LanguageModelChatProvider registration
272+
- [x] TypeScript: JSON-RPC client over stdio
273+
- [x] TypeScript: Progress callback integration
274+
- [x] TypeScript: Agent configuration from settings
275+
- [ ] End-to-end test with real ACP agent
276+
277+
## Tool Bridging
278+
279+
See [Language Model Tool Bridging](./lm-tool-bridging.md) for the design of how tools flow between VS Code and ACP agents. This covers:
280+
281+
- VS Code-provided tools (shuttled to agent via synthetic MCP server)
282+
- Agent-internal tools (permission requests surfaced via `symposium-agent-action`)
283+
- Handle state management across requests
284+
- Cancellation and history matching
285+
286+
## Future Work
287+
288+
- Session caching with message history diffing
289+
- Token counting heuristics
290+
- Model metadata from agent capabilities

0 commit comments

Comments
 (0)