DSH 独有的工具面:terminal / lsp / jobs
别家没有的几个工具各解决什么问题
本页解决的问题
先给结论「DSH 独有的工具面:terminal / lsp / jobs」要解决的关键问题是什么?
别家没有的几个工具各解决什么问题
让这个结论先证明自己值得留下。 把这一页当成决策工具,而不是需要背下来的定义。把概念连到一个真实任务、一个可观察结果,以及一个能改变你判断的失败上。
写下一个问题:试完这个方法后,你能用什么证据回答它?
结论听起来很完整,却没有检查最关键的假设。
同一个任务:起一个 Python REPL,分三步调试一段代码。情景 A 左边只给 bash 单工具,右边给 terminal 六件套,直接看轮数和重复劳动的差距。情景 B 演示 jobs 面板:三种完全不同的后台任务,怎么被同一张列表管起来。
docs/tool-catalog.zh.md 中 bash 与 terminal_* 的真实 schema;情景 B 的任务形态对应 @deepseek-ai/dsh-tool-jobs 的三个工具。SEND_ACTIVE 报错行为对应 packages/terminal/terminal/src/index.ts 第 246 行。- 六个工具:
terminal_open / send / read / signal / close / list,背后是持久 PTY 会话。 - REPL、gdb、ssh 这类有状态程序,跨调用活着。
- 每个会话归属确切的 Agent 实例,别的 agent 拿到 id 也操作不了。
- 恰好四类查询:跳定义、找引用、跳实现、hover,闭合联合,加一类就是编译期大改。
- 故意不开通用 JSON-RPC 逃生口,模型玩不出协议花活。
- 没有 provider 时 schema 不变,返回结构化
LSP_UNAVAILABLE。
- 后台 bash、后台 PTY 发送、后台 subagent,全部注册成
<kind>-N任务。 - 统一
job_list / job_output / job_kill三个工具管一切。 - 授权看所有者会话,id 可预测也无所谓;每个 owner 默认最多 10 个并发。
先看最容易被低估的 terminal。一次性 bash 的问题演示里已经看到了:REPL 的变量活不过一次调用,模型只能把旧代码全部重发一遍,轮数和 token 双倍烧。DSH 的解法是把 PTY 会话做成一等资源:terminal_open 建会话拿 id,之后的 send、read、signal、close 全按 id 操作,会话在后端和工具插件热重载期间照样活着(docs/subsystems/terminal.zh.md「归属与持久性」一节)。
工具描述本身就是提示词工程的范本,1878 行的工具目录里 terminal_send 这条把等待语义一句话说清:
「向持久终端发送文本。默认会提交 Enter,并等待提示符、stdin 等待、输出静默、超时或会话退出。后台模式会返回供 job_output/job_kill 使用的 job id。」
然后是并发纪律:一个 PTY 会话同一时刻只接受一个活动 send。两个调用抢同一个终端,第二个直接吃结构化报错,谁都别想把字符插进别人的命令中间。
看 startSend() 的把关顺序,三道检查一道都省不掉。进门第一件事是 expectOwned(owner, id):授权比较的是拥有会话的确切 Agent 实例,id 不是秘密,边界靠归属,别的 agent 就算猜中了 id 也过不了这一关。第二道看会话是否正在关闭,关到一半的会话拒收任何新输入。第三道看 record.active:上一条 send 还没结算,新来的这条直接抛带 SEND_ACTIVE 错误码的 TerminalError,模型看到的是一条结构化失败,等上一条结算完再来就行。检查全过之后,本次操作被登记为这个会话唯一的活动 send,等它的 done 承诺结算(成功失败都算),登记才清空,下一条 send 才有资格进门。归属即边界这条纪律,待会在 jobs 里还会再见一次。
出处:packages/terminal/terminal/src/index.ts 第 243 至 254 行(startSend()),核对日期 2026-08-13。
lsp 工具把语言服务器的能力收窄到四类语义查询。为什么不直接把 LSP 协议整个透出去?因为那样模型面对的是一个无底洞 schema,provider 换一家行为就漂一次。DSH 的做法是闭合:seam、提供方、工具三层共享同一个四操作联合,加第五个操作会让编译失败,直到三层都改完。选路也简单粗暴,按文件扩展名找 provider,一个扩展名只属于一家:
async query(request: LspQueryRequest, signal?: AbortSignal): Promise<LspQueryResult> {
const route = this.routes.get(finalExtension(request.filePath))
if (route === undefined) {
throw new LspError(`no LSP provider handles "${request.filePath}"`, 'LSP_UNAVAILABLE')
}
return route.provider.query({ ...request, languageId: route.languageId }, signal)
}
packages/lsp/lsp/src/index.ts,核对日期 2026-08-13。代码块保留源码原文。重点在没有 provider 的时候:lsp 工具照常挂在目录里,schema 一个字不变,调用返回带 LSP_UNAVAILABLE 错误码的结构化失败。模型学到的是这个项目没配语言服务器,不用去猜工具怎么消失了。降级要结构化、词汇表要稳定,这个思路和 bash 报 [sandbox: …] 一脉相承。
最后是 jobs。后台 bash 命令、terminal_send 的后台模式、后台 subagent,三种生产方形态完全不同,DSH 让它们全部注册进同一个 ctx.jobs 注册表,领到 bash-1、subagent-2 这种按种类加序号发的 id,然后模型用同一组 job_list / job_output / job_kill 通吃。生产方拥有执行资源,注册表拥有身份、访问权限和生命周期状态(docs/subsystems/jobs.zh.md)。
授权不靠 id 保密id 按 <kind>-N 顺序发放,完全可预测。防线是所有者授权:读、杀、等,全都校验调用方的会话与任务 owner 是否一致,别人的任务连 label 都看不到。
done 等的是资源释放生产方的 done 承诺在资源释放后才 resolve,工作干完了还不算数。owner 被销毁时注册表取消并等待任务,不留孤儿进程。
完成通知不重复打扰某个接口已经交付过终止状态时,reported 标记会抑制重复的完成通知,避免一次任务结束让模型收两遍消息、开两个 turn 白烧请求。
Grok Build 在 PTY 这件事上和 DSH 想到了一块:仓库里有独立的 ptyctl crate(crates/codegen/ptyctl/,内含 pty、session、server、term、wait 等模块,另配 ptyctl-cli),把终端控制做成了可复用的基础设施。两家都认为有状态终端值得一等公民待遇,差异在组合方式:Grok 是编译期链接的 Rust crate,DSH 是运行时挂载的插件加六个面向模型的工具。
Claude Code 的还原源码工具清单里(书稿第 2 章)没有一等的 PTY 会话工具,也没有 LSP 工具,这一条基于已公开的还原源码证据。后台能力它有:bash 带 run_in_background,agent 任务还能按耗时自动转后台(tengu_auto_background_agents 开关,书稿第 2 章第 448 至 458 行引文),但那是围绕 bash 和 subagent 各自做的,没有跨种类的统一任务注册表。这个差距不是谁偷懒:CC 是产品,交互式调试有 IDE 兜底,语义导航有编辑器兜底;DSH 是运行时,要在无头环境里独自把这些能力供给模型。产品可以不做的事,运行时值得做。顺带点破一句:这三套能力都不在 agent loop 主干上,全是可选插件,minimal 预设一个都不挂,照样是完整的 coding agent。
推演两个边界场景
场景一:agent 在同一个 terminal 会话上先发了一条 run_in_background: true 的 send,紧接着又发一条前台 send。第二条的下场是什么,错误码是哪个?job 面板里此时能看到什么?场景二:项目没配任何语言服务器,模型调了一次 lsp 工具查 a.py 的定义。写出模型收到的结果形态,并说明为什么这比直接把 lsp 工具从目录里摘掉对模型更友好。
「交互演示 · 工具面对照间」的能力藏在每次交接里
「同一个任务:起一个 Python REPL,分三步调试一段代码。情景 A 左边只给 bash 单工具,右边给 terminal 六件套,直接看轮数和重复劳动的差距。情景 B 演示 jobs 面板:三种完全不同的后台任务,怎么被同一张列表管起来」说明,Agent 的表现不只由模型决定。模型、上下文、工具、状态、权限和人之间的每次交接,都会改变任务能否继续以及出了问题能否恢复。
先写清状态,再增加能力
从「先看最容易被低估的 terminal。一次性 bash 的问题演示里已经看到了:REPL 的变量活不过一次调用,模型只能把旧代码全部重发一遍,轮数和 token 双倍烧。DSH 的解法是把 PTY 会话做成一等资源: terminal_open 建会话拿 id,之后的 send、read、signal、close 全按 id 操作,会话在后端和工具插件热重载…」出发,可以把流程拆成起始状态、下一步动作、工具返回、状态更新和停止条件。这样调试时找的是第一处丢失信息或权限的位置,而不是笼统地说“模型变笨了”。
- 六个工具: terminal_open / send / read / signal / close / list ,背后是持久 PTY 会话
- REPL、gdb、ssh 这类有状态程序,跨调用活着
- 每个会话归属确切的 Agent 实例,别的 agent 拿到 id 也操作不了
成功路径不能代表系统可靠
用「场景一:agent 在同一个 terminal 会话上先发了一条 run_in_background: true 的 send,紧接着又发一条前台 send。第二条的下场是什么,错误码是哪个?job 面板里此时能看到什么?场景二:项目没配任何语言服务器,模型调了一次 lsp 工具查 a.py 的定义。写出模型收到的结果形态,并说明为什么这比直接把 lsp 工…」重放一次成功和一次失败,记录每一轮真正传入的上下文、工具结果和负责人;只要第二个人能复述这条链,系统才有可维护性。
从「交互演示 · 工具面对照间」走到「三套能力各治一种病」
「交互演示 · 工具面对照间」先把问题落在「同一个任务:起一个 Python REPL,分三步调试一段代码。情景 A 左边只给 bash 单工具,右边给 terminal 六件套,直接看轮数和重复劳动的差距。情景 B 演示 jobs 面板:三种完全不同的后台任务,怎么被同一张列表管起来」上;到了「三套能力各治一种病」,讨论继续推进到「terminal · 治状态失忆 六个工具: terminal_open / send / read / signal / close / list ,背后是持久 PTY 会话。 REPL、gdb、ssh 这类有状态程序,跨调用活着。 每个会话归属确切的 Agent 实例,别的 agent 拿到 id 也操作不了。 lsp · 治文本搜索没有语义 恰好四类查询:跳定义、找引用、跳实现、hover,闭合联合,加一类就是…」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。
把这条判断带到下一个场景
分析 Agent 时,沿着状态、动作、工具结果和下一步的顺序走一遍;每次交接都要能说明信息从哪里来、由谁确认、失败时停在哪里。
- 「交互演示 · 工具面对照间」:同一个任务:起一个 Python REPL,分三步调试一段代码。情景 A 左边只给 bash 单工具,右边给 terminal 六件套,直接看轮数和重复劳动的差距。情景 B 演示 jobs 面板:三种完全不同的后台任务,怎么被同一张列表管起来
- 「三套能力各治一种病」:terminal · 治状态失忆 六个工具: terminal_open / send / read / signal / close / list ,背后是持久 PTY 会话。 REPL、gdb、ssh 这类有状态程序,跨调用活着。 每个会话归属确切的 Agent 实例,别的 agent 拿到 id 也操作不了。 lsp · 治文本搜索没有语义 恰好四类查询:跳定义、找引用、跳实现、hover,闭合联合,加一类就是…
- 「最后的要点」:故意不开通用 JSON-RPC 逃生口,模型玩不出协议花活
最后的「最后的要点」把讨论落到「故意不开通用 JSON-RPC 逃生口,模型玩不出协议花活」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。
我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。
读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。
如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。
还没有这篇文章的讨论。