-
Notifications
You must be signed in to change notification settings - Fork 11
feat: VS Code Language Model Provider (experimental) #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
nikomatsakis
merged 62 commits into
symposium-dev:main
from
nikomatsakis:vscodelm-prototype
Jan 7, 2026
Merged
feat: VS Code Language Model Provider (experimental) #95
nikomatsakis
merged 62 commits into
symposium-dev:main
from
nikomatsakis:vscodelm-prototype
Jan 7, 2026
+5,303
−46,121
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Adds support for exposing Symposium as a VS Code Language Model via the LanguageModelChatProvider API (VS Code 1.104+). This allows Symposium to appear in VS Code's model picker and be used by Copilot and other extensions that consume the Language Model API. Components: - Rust: vscodelm subcommand in symposium-acp-agent that speaks JSON-RPC - TypeScript: LanguageModelChatProvider that bridges to the Rust process - Design doc: md/design/vscode-extension/lm-provider.md The prototype uses Eliza for responses to validate the plumbing before integrating with real ACP agents. Co-authored-by: Claude <[email protected]>
Replace hand-rolled JSON-RPC handling with sacp's typed message infrastructure: - Define VsCodePeer/LmBackendPeer peers and corresponding links - Define message types using JrRequest/JrNotification derives - Use MatchMessage for request handling - Use sacp's connection builder to serve on stdio This aligns with how the rest of Symposium handles JSON-RPC and provides better type safety. Co-authored-by: Claude <[email protected]>
Eliza is owned by a single handler instance, so no synchronization needed. Split MatchMessage handling to allow &mut self access for the response handler. Co-authored-by: Claude <[email protected]>
- LmBackend now implements Component<LmBackendToVsCode> - Add test_provide_info: verifies model info response - Add test_provide_token_count: verifies token counting heuristic - Add test_chat_response: verifies streaming response with notifications Tests use sacp's Channel and into_server() for in-process testing, avoiding the need for manual JSON-RPC message construction. Co-authored-by: Claude <[email protected]>
- Add expect-test to workspace dependencies - Refactor tests to use expect! snapshots instead of assertions - Add LmBackend::new_deterministic() for reproducible test results Co-authored-by: Claude <[email protected]>
- Add proper instanceof checks for LanguageModelTextPart, LanguageModelToolCallPart, and LanguageModelToolResultPart - Create MessageContentPart discriminated union type - Skip known-but-undocumented VS Code/Copilot internal parts (cache_control, stateful_marker) with debug logging - Log truly unknown part types as errors with full JSON - Fixes [object Object] corruption in message serialization This enables clean message history comparison for session matching. Co-authored-by: Claude <[email protected]>
Implements session management for the VS Code Language Model Provider using the Tokio actor pattern. This enables session continuity across multiple requests from VS Code, which sends full message history with each request. Key changes: - Add session_actor.rs with SessionActor handle and run loop - Refactor LmBackendHandler to maintain Vec<SessionActor> - Session matching via message history prefix comparison - Clean cancellation via channel closure (drop reply_rx) - Actor streams response parts back through one-shot reply channel The session manager finds the session with the longest matching prefix, computes new messages, and only sends those to the actor. This avoids reprocessing the entire conversation on each request. Co-authored-by: Claude <[email protected]>
Each session actor now has a unique UUID for tracing. Logs include: - Session creation with UUID - Incoming message counts per request - Eliza responses with user input - Cancellation events - Session shutdown Co-authored-by: Claude <[email protected]>
- Add McpServer field to ProvideResponseRequest params - Introduce AgentDefinition enum (Eliza for testing, McpServer for real agents) - Session actor now spawns ACP agent using sacp Component infrastructure - Replace old Eliza-based session loop with ACP session management - Remove deterministic Eliza tests (require different test infrastructure now) Co-authored-by: Claude <[email protected]>
The Stdio variant of McpServer uses #[serde(untagged)] so it serializes as a flat object without a wrapper. Updated TypeScript interface to match. Also added a test to verify the serialization format. Co-authored-by: Claude <[email protected]>
- Document the McpServerStdio format for agent configuration - Update implementation status to reflect completed work - Remove 'Full ACP agent integration' from future work (now done) Co-authored-by: Claude <[email protected]>
The agent field now accepts either:
- { eliza: { deterministic?: boolean } } - for in-process testing
- { mcp_server: { name, command, args, env } } - for external ACP agents
This allows the Language Model API to use both the built-in Eliza agent
for testing and real external agents configured in VS Code settings.
Co-authored-by: Claude <[email protected]>
Instead of sending the 'eliza' variant to the backend, the TypeScript extension now always sends 'mcp_server' with the resolved binary path. For symposium builtins like Eliza, this uses getConductorCommand() to get the embedded binary path and adds the subcommand as the first arg. The Rust protocol still supports the 'eliza' variant for direct testing. Co-authored-by: Claude <[email protected]>
- New chapter: lm-tool-bridging.md covering: - VS Code-provided tools via synthetic MCP server - Agent-internal tools with permission flow - Handle state machine (Idle, AwaitingToolPermission, AwaitingToolResult) - History matching for session continuity - Cancellation handling - New references: - vscode-lm-tool-api.md: Tool registration, call flow, confirmation UI - vscode-lm-tool-rejection.md: Rejection behavior and limitations - Updated lm-provider.md to link to new tool bridging chapter Co-authored-by: Claude <[email protected]>
- Add lm/cancel notification from TypeScript to Rust - Refactor handler to use SessionData with SessionState enum (Idle/Streaming) - Make request handler non-blocking by spawning streaming task - Use futures-concurrency Race to handle cancellation during streaming - TypeScript sends lm/cancel on CancellationToken fire - Convert cancellation errors to vscode.CancellationError - Auto-cancel previous streaming when new request arrives on same session Co-authored-by: Claude <[email protected]>
…lation - Switch from tokio::sync channels to futures::channel for cleaner stream-based cancellation handling - Replace race-in-loop pattern with futures-concurrency Merge for combining response parts and cancellation signals - Use unbounded_send() for futures mpsc channels - Use StreamExt::next() instead of recv() for futures channels - Pin merged stream for iteration Co-authored-by: Claude <[email protected]>
Adds support for ACP agents to request permission for internal tools (like bash, file_edit) through VS Code's Language Model Tool API. TypeScript side: - Register symposium-agent-action tool in package.json - Add AgentActionTool class with prepareInvocation for confirmation UI - Extend ResponsePart type to include tool_call variant - Update pendingRequests to handle LanguageModelResponsePart Rust side: - Extend ContentPart to support tool_call and tool_result variants - Add message helpers for checking tool call/result presence - Handle session/request_permission from agents in session_actor - Emit ToolCall response parts for symposium-agent-action - Add AwaitingPermission marker in response stream - Implement history matching to detect approval/rejection - Continue streaming after permission approval The flow: 1. Agent sends session/request_permission 2. Rust emits LanguageModelToolCallPart for symposium-agent-action 3. VS Code shows confirmation dialog via prepareInvocation 4. On next request, history matching detects approval (tool result present) 5. Permission decision sent to actor, streaming continues Co-authored-by: Claude <[email protected]>
Simplifies the message handling by using MatchMessage pattern instead of manual match + parse_message calls. This also removes the need for manual JSON serialization since MatchMessage provides typed request context. Co-authored-by: Claude <[email protected]>
Work in progress - does not build. Changes: - Remove ResponsePart, use ContentPart for both messages and response streaming - Unify field names to match VS Code API (toolCallId, toolName, parameters) - Add send_tool_use with peek/consume pattern (incomplete) - Document committed/provisional session model in lm-provider.md - Document tool bridging flows in lm-tool-bridging.md Next: implement committed/provisional tracking in SessionActor handle Co-authored-by: Claude <[email protected]>
Refactor the vscodelm module to use an actor-based architecture where: - HistoryActor owns all session state (committed/provisional history) - SessionActors send parts back to HistoryActor via mailbox - JrConnectionCx handler forwards requests to HistoryActor This solves the problem of record_part needing &mut SessionData access from within spawned tasks. The HistoryActor processes messages in its central mailbox loop, providing proper &mut access to session state. Key changes: - Add history_actor.rs with HistoryActor, HistoryActorHandle, message types - Rewrite session_actor.rs to use HistoryActorHandle instead of JrConnectionCx - Simplify mod.rs handler to forward to HistoryActor (lazy initialized) - Make role constants pub(crate) for submodule access - Temporarily disable request_response.rs (needs update for new architecture) Co-authored-by: Claude <[email protected]>
- Add ToolDefinition, ToolMode, and ChatRequestOptions types on both sides - TypeScript: properly type options parameter in provideLanguageModelChatResponse - Rust: add options field to ProvideResponseRequest - Track has_internal_tool flag per-session to auto-deny permission requests when symposium-agent-action tool is not available - Bundle cancel_rx and has_internal_tool into RequestState struct to ensure correct values are used when requests change mid-turn - Add RequestState::cancellation() helper for cleaner async patterns - Fix eslint warnings in languageModelProvider.ts and agentRegistry.ts Co-authored-by: Claude <[email protected]>
Co-authored-by: Claude <[email protected]>
The toolCallId is provided by VS Code through the LanguageModelToolCallPart structure, not as part of the tool input parameters. Removing it from the required fields fixes the validation error. Also improved the confirmation UI to show the description from raw_input if available (e.g., Claude Code's bash tool includes this). Co-authored-by: Claude <[email protected]>
Add normalize_messages() to coalesce consecutive Text parts in messages. This ensures our provisional history matches VS Code's format when comparing. - Normalize incoming messages in handle_vscode_request - Normalize provisional messages on Complete - Add debug logging for history match mismatches Co-authored-by: Claude <[email protected]>
- Fetch registry agents alongside availability checks - Merge uninstalled registry agents into the agent list - Auto-install registry agent to settings when selected - Remove separate 'Add agent from registry' link (no longer needed) Co-authored-by: Claude <[email protected]>
These are now available from the ACP registry and will be shown automatically alongside other registry agents. Co-authored-by: Claude <[email protected]>
Sort by display name in both the settings panel and LM provider. Co-authored-by: Claude <[email protected]>
Instead of returning the receiver from new(), callers now create the channel and pass in the sender. Co-authored-by: Claude <[email protected]>
Use (fut1, fut2, fut3).race() pattern instead of tokio::select! to avoid subtle cancellation bugs that select can introduce. Co-authored-by: Claude <[email protected]>
- Take ownership of request_state and return Result<RequestState, Canceled> - Take ownership of ToolInvocation including the result_tx - Send result via result_tx inside the function rather than at call site - Add Canceled marker type for consistency with other similar functions Co-authored-by: Claude <[email protected]>
Now properly uses the request_state.cancel_rx to abort early if cancellation occurs while waiting for the tool result from VS Code. Co-authored-by: Claude <[email protected]>
- Add cancel_tool_invocation() helper to reduce boilerplate - Destructure ToolInvocation at start of handle_vscode_tool_invocation - Use Race::race((a, b)).await style for cleaner formatting Co-authored-by: Claude <[email protected]>
Simplifies the cancellation pattern in race calls from:
async { let _ = (&mut request_state.cancel_rx).await; Event::Canceled }
to:
request_state.on_cancel(Event::Canceled)
Co-authored-by: Claude <[email protected]>
The session actor was connecting directly to the agent, which bypassed MCP-over-ACP negotiation. This meant the synthetic VscodeToolsMcpServer was never discovered by the agent. Now we wrap the agent in a Conductor using AgentOnly, which enables proper MCP-over-ACP negotiation so the agent can discover and use our VS Code tools MCP server. Co-authored-by: Claude <[email protected]>
The MCP server needs to be provided by a proxy component in the Conductor's chain, not by the client connecting to the Conductor. Previously, we were adding the MCP server via build_session().with_mcp_server() on the ClientToAgent connection, but the Conductor's MCP bridging happens between the proxy chain and the agent. This commit: - Creates VscodeToolsProxy, a Component<ProxyToConductor> that wraps the VscodeToolsMcpServer and provides it via ProxyToConductor::builder() - Updates run_with_agent to include VscodeToolsProxy in the Conductor's proxy chain using the closure-based instantiator - Moves the MCP server creation to run_with_agent and passes the tools_handle and invocation_rx to run_with_cx The proxy sits in the Conductor's chain between client and agent, making the synthetic MCP server available to the agent via MCP-over-ACP bridging. Co-authored-by: Claude <[email protected]>
This reverts commit bad5f12.
Changes the --log CLI argument from tracing::Level to String, allowing flexible filter expressions like 'sacp=debug,symposium=trace' in addition to simple levels like 'debug'. This makes it easier to do targeted debugging via the VS Code symposium.agentLogLevel setting. Co-authored-by: Claude <[email protected]>
Fixes a race condition where the agent would call tools/list before VS Code had reported its available tools, resulting in an empty tool list. Changes: - Peek at first request to get initial tools before creating session - Add set_initial_tools() to populate tools without sending notification - Only send tools/list_changed notification when tools actually change - Extract peek() helper for cleaner stream peeking Co-authored-by: Claude <[email protected]>
Add in-process integration tests for vscodelm that simulate VS Code talking to elizacp without spawning binaries: - test_simple_chat_request: basic chat flow - test_chat_request_with_tools: tools passed in request - test_multi_turn_conversation: multi-turn with history - test_mcp_list_tools: elizacp 'list tools from vscode-tools' command - test_mcp_invoke_tool: elizacp 'use tool' command triggers ToolCall Uses expect_test for snapshot-style assertions. Co-authored-by: Claude <[email protected]>
The assistant message in the conversation history should match what Eliza actually said in the first turn, not a made-up response. Co-authored-by: Claude <[email protected]>
When handle_vscode_tool_invocation sends Complete to signal VS Code to invoke the tool, the history actor drops the old cancel_tx. This caused the old cancel_rx to fire immediately, incorrectly canceling the tool invocation. The fix removes the cancellation race from the peek() call - after sending Complete, the old cancel channel is meaningless. The new request will bring its own cancel_rx. Also adds test_mcp_tool_result_flow to verify the full tool call round-trip: VS Code triggers tool call, provides result, elizacp echoes it back. Co-authored-by: Claude <[email protected]>
The integration tests in vscodelm_mcp_bridging.rs were slow, flaky (timeouts, signal 101), and didn't add meaningful coverage beyond what the unit tests already provide. The unit tests exercise the same vscodelm → conductor → elizacp path including MCP bridge setup, just with in-process channels instead of stdio. Co-authored-by: Claude <[email protected]>
Extract tests from mod.rs into tests.rs for better organization. Co-authored-by: Claude <[email protected]>
When handle_vscode_tool_invocation sends Complete, the history actor drops the streaming state including cancel_tx. This causes cancel_rx to resolve, which was incorrectly winning the race against peek() and returning Canceled. The fix is to not race against cancel_rx after sending Complete, since it's stale at that point. Real cancellation is detected via the next_request.canceled flag, which is set when VS Code sends a request with mismatched history. Also adds test_mcp_invoke_tool test that verifies the full tool invocation flow: sending a tool call to VS Code, receiving a tool result back, and Eliza continuing with the response. Co-authored-by: Claude <[email protected]>
Add an interactive CLI that acts as a fake VS Code to debug the vscodelm tool invocation flow with real Claude Code. Changes: - Add ClaudeCode variant to AgentDefinition - Create lib.rs to expose vscodelm module for examples - Add vscodelm_cli example with 'average' tool for testing Usage: cargo run --example vscodelm_cli Co-authored-by: Claude <[email protected]>
Allows writing traces to a file for debugging: cargo run --example vscodelm_cli -- --log-file /tmp/vscodelm.log Then tail -f the log in another terminal while interacting. Co-authored-by: Claude <[email protected]>
Claude Code prefixes MCP tool names with `mcp__<server>__<tool>` when invoking tools. Our VscodeToolsMcpServer was comparing against unprefixed names stored in the tool list, causing 'tool not found' errors. Now we strip the `mcp__vscode_tools__` prefix before looking up and invoking tools. Co-authored-by: Claude <[email protected]>
- Add experimental banner and current status section to lm-provider.md - Add Language Model Provider section to implementation-status.md - Document known issue: multiple VS Code-provided tools cause invocation failures - Document open question: competing context injection between VS Code LM consumers and ACP agents Co-authored-by: Claude <[email protected]>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds experimental support for exposing ACP agents as VS Code Language Models via the
LanguageModelChatProviderAPI (VS Code 1.104+). This allows ACP agents to appear in VS Code's model picker and be used by extensions like GitHub Copilot.Status: Experimental, disabled by default. Enable with
symposium.enableExperimentalLM: true.What's Included
Core Language Model Provider
vscodelmsubcommand): Handles session management, message history, and agent communicationTool Bridging
symposium-agent-actiontoolAgent Registry Integration
Other Changes
vscodelm_cliexample for debugging tool invocationKnown Issues
Multiple VS Code tools cause invocation failures: A single isolated tool works, but when multiple VS Code-provided tools are bridged, the model doesn't invoke them properly. Root cause unknown.
Context collision: VS Code LM consumers inject their own context (project details, editor state). ACP agents like Claude Code also inject context. These competing layers may confuse the model.
Open Question
The LM API may not be the right abstraction for wrapping full ACP agents. It seems better suited for raw model access rather than agents with their own context management and tool orchestration.
Documentation