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 与契约
LspService(packages/lsp/lsp/src/types.ts)只暴露四个操作,构成一个封闭联合,因此新增一个操作是在接缝、provider 与工具之间由编译强制推动的变更:
type LspOperation = 'goToDefinition' | 'findReferences' | 'goToImplementation' | 'hover'位置与范围是从零开始的 UTF-16,与 LSP 线上约定一致;面向模型的工具拥有从一开始的光标约定,并在进出时转换。
interface LspQueryRequest {
operation: LspOperation
filePath: string // 源文件,相对于 workspaceRoot 或绝对路径
position: LspPosition // { line, character } 从零开始的 UTF-16
workspaceRoot: string // 必填,绝不默认
}结果同样是第二个封闭判别联合——消费者按 kind 做 switch,新增分支在未处理前无法编译:
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_UNAVAILABLE、LSP_INVALID_PROVIDER、LSP_CONFLICT、LSP_DISPOSED、LSP_UNSUPPORTED_OPERATION 或 LSP_MALFORMED_RESPONSE。接缝不暴露任何协议类型、进程/文档控制或通用 JSON-RPC 逃生口——调用方只按四个操作思考。
findReferences 总是包含声明——provider 在内部强制这一点,调用方拿不到任何开关。
Provider:dsh-lsp-stdio
lsp-stdio(packages/lsp/lsp-stdio/src/index.ts)是通用 stdio 后端。一个插件实例配置一张服务器命令表,并为每个条目注册一个隔离的 provider。每个 provider:
- 对每个标准 workspace 目标惰性单飞(single-flight)一个服务器进程(源码经
ctx.fs读取、经ctx.subprocess启动,因此本地与远程实现共享同一个宿主); - 通过它服务瞬态打开的查询;
- 在下一次只读查询之前或期间替换失败的被选传输。
配置形态——servers 是 provider id → 本地服务器配置的映射:
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 / configuration | null | 静态 initialize 选项 / 每个 workspace/configuration 的应答 |
maxMessageBytes | 16 000 000 | 从服务器接受的最大单帧消息 |
maxStderrBytes | 1 000 000 | 为诊断保留的 stderr 尾部 |
maxDocumentBytes | 4 000 000 | 宿主打开的最大源文件 |
shutdownTimeoutMs | 5 000 | 优雅 shutdown/exit 预算,超时后升级 |
killGraceMs | 2 000 | 请求取消与 SIGTERM→SIGKILL 的宽限 |
在发布任何 provider 之前,插件会在加载时(凭据清洗之后)解析每个可执行文件;每个进程随后在其首个匹配查询时惰性启动。生命周期受效果作用域约束:销毁时从 ctx.lsp 注销并拆除每个存活服务器。支撑模块包括 framing.ts(JSON-RPC 消息编解码)、protocol.ts、translate.ts(规范化、位置编码协商、supportsOperation)、instance.ts 与 connection.ts。
消费者:dsh-tool-lsp
tool-lsp 在 ctx.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 生命周期上强制。
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 |