Skip to content

feat(lsp): add agent-callable LSP query tools - #55532

Open
lyx516 wants to merge 1 commit into
NousResearch:mainfrom
lyx516:feat/lsp-query-tools-v2
Open

feat(lsp): add agent-callable LSP query tools#55532
lyx516 wants to merge 1 commit into
NousResearch:mainfrom
lyx516:feat/lsp-query-tools-v2

Conversation

@lyx516

@lyx516 lyx516 commented Jun 30, 2026

Copy link
Copy Markdown

Summary

Implements Phase 3 of Issue #516 — active LSP code intelligence tools, building on the existing passive diagnostic pipeline (Phase 2, PR #24168).

Adds 5 new agent-callable tools to the coding toolset (and hermes-acp/hermes-api-server):

  • lsp_go_to_definition — find where a symbol is defined
  • lsp_find_references — find all usages of a symbol
  • lsp_hover — get type information and documentation
  • lsp_document_symbols — list all symbols in a file
  • lsp_workspace_symbols — search symbols across the project

Architecture

The implementation follows the existing LSP infrastructure patterns:

Layer File What it does
Async LSP client agent/lsp/client.py 5 new async methods (go_to_definition(), find_references(), hover(), document_symbols(), workspace_symbols()) using _send_request_with_retry() — no new dependencies
Sync bridge agent/lsp/manager.py query_lsp_sync() wraps _query_async() through the existing _BackgroundLoop; auto-opens files on the server when needed
Agent tools tools/lsp_tools.py 5 tools registered via registry.register(), each returning structured JSON output
Toolset registration toolsets.py Added to coding, hermes-acp, and hermes-api-server toolsets
Tests tests/agent/lsp/test_query_tools.py 14 new tests covering all query methods + tool handlers + graceful degradation

Design decisions

  • No new dependencies — pure JSON-RPC via existing _send_request_with_retry()
  • Graceful degradation — tools return clear error messages when LSP is unavailable (disabled, no git workspace, no matching server)
  • Auto-openquery_lsp_sync() opens the file on the server if not already open, so single-shot queries work without a preceding write
  • Route-specific dispatch_query_async() maps LSP method names to the matching LSPClient method, with a fallback to _send_request_with_retry() for arbitrary methods
  • Workspace/symbol pass-throughworkspaceSymbol doesn't need a file path; the file_path parameter is used for workspace detection

Testing

  • 159 LSP tests all pass (14 new + 145 existing)
  • New tests cover: each query method end-to-end via mock LSP server, each tool handler with formatted JSON output, missing parameter validation, and LSP-unavailable scenarios (no git workspace, disabled in config)
  • Mock LSP server extended with a "query" script mode that responds to all 5 query methods

…-references, hover, document-symbols, workspace-symbols)

Implements the active query phase of Issue NousResearch#516 (Phase 3), building on
the existing passive diagnostic pipeline (Phase 2, PR NousResearch#24168).

Adds 5 new LSP query methods to LSPClient:
- go_to_definition()
- find_references()
- hover()
- document_symbols()
- workspace_symbols()

Each uses the existing _send_request_with_retry() infrastructure and
requires no new dependencies.

Adds LSPService.query_lsp_sync() as the synchronous bridge from the
agent-tool layer into the async LSP client, including automatic file
opening via open_file() when the file isn't already open on the server.

Registers 5 new Hermes agent tools under the 'coding' toolset:
- lsp_go_to_definition
- lsp_find_references
- lsp_hover
- lsp_document_symbols
- lsp_workspace_symbols

Also adds these tools to hermes-acp and hermes-api-server toolsets.

All tools return structured JSON with 'error' and 'result' fields for
robust model consumption.  Graceful degradation when LSP is unavailable.

Tests: 14 new tests (159 total LSP tests, all passing).
@alt-glitch alt-glitch added type/feature New feature or request comp/lsp Language Server Protocol integration (P2 policy) P3 Low — cosmetic, nice to have labels Jun 30, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: LGTM

New LSP query tools (6 files, +1360 additions). Adds agent-callable LSP methods: go_to_definition, find_references, hover, document_symbols, workspace_symbols. Well-structured with proper timeout handling and result formatting.

Looks Good

  • Clean API design with consistent parameter patterns (path, line, character)
  • Proper timeout handling via _send_request_with_retry with LSP_QUERY_TIMEOUT
  • Good result formatting: _format_location and _format_symbol handle both Location and LocationLink formats
  • DocumentSymbol tree flattening is correct (recursive with indent)
  • Symbol kind names cover all LSP standard kinds
  • Export includes LSP_QUERY_TIMEOUT in __all__

Note

  • The _SYMBOL_KIND_NAMES dict could be made a ClassVar or module constant for slightly cleaner access, but this is minor

Reviewed by Hermes Agent

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for extending the existing LSP transport rather than adding a dependency. Current main still exposes LSP only through post-write diagnostics (website/docs/user-guide/features/lsp.md:9-15), so the feature remains needed.

Problems

  • tools/lsp_tools.py:389 passes a directory to query_lsp_sync. That path is gated by LSPService.enabled_for() (agent/lsp/manager.py:248-279), which selects a server with extension/basename matching (agent/lsp/servers.py:150-188). A workspace directory selects no server, so lsp_workspace_symbols returns an empty success result. The test at tests/agent/lsp/test_query_tools.py:292-302 does not catch this because it only asserts error == "".
  • tools/lsp_tools.py:177-181 ignores targetUri and targetRange, although LSPClient.go_to_definition() accepts LocationLink results (agent/lsp/client.py:900-919). Those valid responses render as ? rather than the definition target.

Suggested changes

  • Select a concrete server for workspace-symbol queries, then assert a returned mock symbol in an end-to-end test.
  • Format both Location and LocationLink results, with a LocationLink regression test.

Automated hermes-sweeper review.

Comment thread tools/lsp_tools.py
# workspace/symbol doesn't need a file path; pass cwd for workspace detection
cwd = __import__("os").getcwd()
result = svc.query_lsp_sync(
cwd,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

query_lsp_sync() first calls enabled_for(file_path), which selects a server via extension/basename matching. cwd is normally a directory, so it has no matching server and this always becomes an empty successful result. Pass a representative source path/language or add workspace-level client selection, and assert the mock symbol result in this handler test.

Comment thread tools/lsp_tools.py
for loc in locations:
from agent.lsp.client import uri_to_path

uri = loc.get("uri", "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go_to_definition() permits compliant LocationLink results, but those use targetUri/targetRange, not uri/range; this formatter will emit ? at 1:1. Handle both response shapes (or reuse a shared formatter) and add a LocationLink regression test.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/lsp Language Server Protocol integration (P2 policy) P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants