Skip to content

LSP(语言服务器协议)集成让模型获得原始文本 grep 无法提供的语义代码导航:由真正理解语言(而非磁盘上的字节)的语言服务器解析出的精确定义、引用、实现与悬停文档。dsh 通过一个能力接缝 ctx.lsp 暴露它,按标准的"接缝 + provider + 消费者"结构拆成三个包。

角色ctx key
packages/lsp/lsp服务定义LspService + provider 注册表 + 封闭结果类型ctx.lsp
packages/lsp/lsp-stdio服务provider:通用 stdio 语言服务器宿主ctx.lsp 上注册 provider
packages/lsp/tool-lsp消费者:面向模型的 lsp 工具 + 系统提示词指导ctx.tools 上注册

收益:provider 更换不会改变模型请求导航的方式,因为工具 schema 在接缝后面保持稳定。

Agent 框架为什么需要 LSP

基于文本的导航有歧义——符号 List 出现在许多文件中,但只有一个是编译器认识的定义。语言服务器提供:

  • goToDefinition——符号在哪里声明;
  • findReferences——所有使用处(总是包含声明);
  • goToImplementation——接口/抽象符号的具体实现;
  • hover——某位置的类型签名与文档注释。

因为这些操作精确且正确实现的成本低,系统提示词把 LSP 定位为 search/read 的补充:普通导航用 search/read;文本匹配有歧义,或在改动前需要精确定义、实现或引用时用 lsp。

接缝:ctx.lsp 与契约

LspServicepackages/lsp/lsp/src/types.ts)只暴露四个操作,构成一个封闭联合,因此新增一个操作是在接缝、provider 与工具之间由编译强制推动的变更:

ts
type LspOperation = 'goToDefinition' | 'findReferences' | 'goToImplementation' | 'hover'

位置与范围是从零开始的 UTF-16,与 LSP 线上约定一致;面向模型的工具拥有从一开始的光标约定,并在进出时转换。

ts
interface LspQueryRequest {
  operation: LspOperation
  filePath: string        // 源文件,相对于 workspaceRoot 或绝对路径
  position: LspPosition   // { line, character } 从零开始的 UTF-16
  workspaceRoot: string   // 必填,绝不默认
}

结果同样是第二个封闭判别联合——消费者按 kindswitch,新增分支在未处理前无法编译:

ts
type LspQueryResult =
  | { kind: 'locations'; locations: readonly LspLocation[]; resolvedWorkspaceUri: string }
  | { kind: 'hover';      hover: LspHover | null }

resolvedWorkspaceUri 是 provider 对 workspace 根的标准 file: URI;调用方若要相对化 location URI,必须使用这个坐标,而不是用 host 平台规则去解析可能经过符号链接的请求路径。

Provider 与选择

LspProvider 拥有一个稳定的品牌化 id 和一张独占的小写前导点扩展名映射(如 { '.ts': 'typescript' })。registerProvider 原子地保留 id 与每个扩展名——无效或冲突的注册不发布任何东西——其 disposer 释放所有保留。选择按查询进行、与顺序无关;无匹配时抛出带稳定错误码的 LspError,如 LSP_UNAVAILABLELSP_INVALID_PROVIDERLSP_CONFLICTLSP_DISPOSEDLSP_UNSUPPORTED_OPERATIONLSP_MALFORMED_RESPONSE。接缝不暴露任何协议类型、进程/文档控制或通用 JSON-RPC 逃生口——调用方只按四个操作思考。

findReferences 总是包含声明——provider 在内部强制这一点,调用方拿不到任何开关。

Provider:dsh-lsp-stdio

lsp-stdiopackages/lsp/lsp-stdio/src/index.ts)是通用 stdio 后端。一个插件实例配置一张服务器命令表,并为每个条目注册一个隔离的 provider。每个 provider:

  • 对每个标准 workspace 目标惰性单飞(single-flight)一个服务器进程(源码经 ctx.fs 读取、经 ctx.subprocess 启动,因此本地与远程实现共享同一个宿主);
  • 通过它服务瞬态打开的查询;
  • 下一次只读查询之前或期间替换失败的被选传输。

配置形态——servers 是 provider id → 本地服务器配置的映射:

yaml
plugins:
  lsp-stdio:
    servers:
      typescript:
        command: typescript-language-server
        args: ['--stdio']
        extensionToLanguage: { '.ts': 'typescript', '.tsx': 'typescript' }
LspLocalServerConfig key默认说明
command可执行文件(绝对路径,或在加载时按 PATH 解析)
extensionToLanguage必填映射,小写前导点 key
args / env[] / {}不经 shell;env 在清洗后的环境之上合并
initializationOptions / configurationnull静态 initialize 选项 / 每个 workspace/configuration 的应答
maxMessageBytes16 000 000从服务器接受的最大单帧消息
maxStderrBytes1 000 000为诊断保留的 stderr 尾部
maxDocumentBytes4 000 000宿主打开的最大源文件
shutdownTimeoutMs5 000优雅 shutdown/exit 预算,超时后升级
killGraceMs2 000请求取消与 SIGTERM→SIGKILL 的宽限

在发布任何 provider 之前,插件会在加载时(凭据清洗之后)解析每个可执行文件;每个进程随后在其首个匹配查询时惰性启动。生命周期受效果作用域约束:销毁时从 ctx.lsp 注销并拆除每个存活服务器。支撑模块包括 framing.ts(JSON-RPC 消息编解码)、protocol.tstranslate.ts(规范化、位置编码协商、supportsOperation)、instance.tsconnection.ts

消费者:dsh-tool-lsp

tool-lspctx.lsp 之上注册唯一的只读 lsp 工具:

参数说明
operation必填:goToDefinition | findReferences | goToImplementation | hover
file_path源文件,相对于 workspace 或绝对路径
line / character从一开始的 UTF-16 光标坐标(工具转换为接缝的从零开始位置)

它要求会话 workspace 的 cwd 且无回退(否则为 LSP_WORKSPACE_REQUIRED),对结果设上限并渲染,并挂一个可配置的超时预算(timeoutMs,默认 60 000,≤ MAX_TIMER_DELAY_MS),由 dsh-tool-call-timeout-policy 在排队的 open/query/close 生命周期上强制。

ts
interface Config {
  maxLocations?: number      // 默认 100 —— 在省略标记之前设上限
  maxResultChars?: number    // 默认 16000 —— 最大渲染结果
  timeoutMs?: number         // 默认 60000 —— 工具调用超时预算
}

没有任何注册 provider 时,查询返回结构化的 LSP_UNAVAILABLE 错误而不是改变 schema——跨组合,模型可见的契约保持稳定。

生命周期草图(ASCII)

model: lsp({ operation: "goToDefinition", file_path, line, character })
   │   (1-based → 0-based UTF-16)

ctx.lsp.query(LspQueryRequest)         // 按 .ext → language id 选择 provider

lsp-stdio provider(为 workspace 惰性启动的服务器)
   │   瞬态打开文档,执行请求,关闭

LspQueryResult = { kind: 'locations', locations, resolvedWorkspaceUri }
             |  { kind: 'hover', hover }
   │   (provider 规范化;工具按上限重新渲染)

模型看到格式化后的 locations / hover 文本

@deepseek-ai/dsh-lsp
@deepseek-ai/dsh-lsp-stdio
@deepseek-ai/dsh-tool-lsp

延伸阅读

  • 代码运行时——lspsearch/read 导航并列之处。
  • 工具呈现——timeoutMs 与工具调用超时策略。
  • 沙箱与子进程——ctx.subprocess 如何启动语言服务器。
  • docs/subsystems/lsp.md——官方 LSP 导航参考。
  • packages/lsp/lsp/src/types.ts——LspOperationLspQueryRequestLspQueryResult
  • packages/lsp/lsp-stdio/src/index.ts——stdio 服务器托管与配置默认值。