DeepSeek Harness 里的 TurnTrigger:文档还留着,packages 里搜不到

2026-08-16

顺着文档读源码,最容易踩的一种坑是:文档给你一个类型名,你照着这个名字去代码里找实现,找了半天什么都没有,于是开始怀疑自己 grep 的姿势不对、怀疑是不是被打包进 dist 了、怀疑是不是某个 .d.ts 里的声明合并没被检索到。

TurnTrigger 在 DeepSeek Harness 里就是这样一个名字。

先把限定说在前面:本文全部依据仓库快照 47f9438 的静态阅读,核对日 2026-08-16。该仓库建立于 2026-08-13,版本号是 0.1.0-rc.5,README 自述处于开发者预览阶段并明写未来会出现破坏兼容性的变更——下面提到的每一个类型名、文件路径、行号都随时可能变,读到本文时请以仓库当前内容为准。

一条命令就能确认的现象

在快照根目录下执行:

grep -rn "TurnTrigger" --include="*.ts" packages/ | wc -l

我们得到的结果是 0

同一个名字在 docs/ 下是有的。我们核到三处:

  • docs/subsystems/README.md:17 的子系统索引表里,session.md 那一行的职责描述写着它覆盖 TurnTrigger/TurnEndReason
  • docs/subsystems/core.md:248 的正文里,同样把「the TurnTrigger/TurnEndReason reasons」指到 session.md
  • docs/subsystems/core.md:290 的表格里还有独立的一行:TurnTriggerMap | dsh-session | TurnTrigger | session.md

生成脚本里也有。scripts/gen-cordis-catalog.ts:364scripts/gen-persistence-catalog.ts:48 都留着一条 TurnTrigger: 'session.md' 的映射项。注意这两个脚本在 scripts/ 下,不在 packages/ 下,所以上面那条限定了 packages/ 的 grep 不会命中它们——如果你用的是不带路径限定的全仓搜索,看到的命中数当然不是 0,这一点先分清楚。

更有意思的是被指向的那一页自己怎么写的。docs/subsystems/session.md:542 的正文明写着一句:turn/start has no trigger field.(turn/start 没有 trigger 字段)。而 turn/start 的 payload 在源码里就是 { turn: number }

也就是说:索引表和 core.mdTurnTrigger 指向 session.mdsession.md 这一页正文明写 turn/start 上没有 trigger 字段,packages/ 下的 TypeScript 文件里搜不到这个标识符。三处位置的口径就是这样,我们只陈述这个差异,不推断哪一处「才是对的」,也不推断这个类型是被删掉了还是从来没合入——我们没有依据回答后面这个问题。

别把一对名字当成一个东西

TurnTriggerTurnEndReason 在文档里几乎总是连着写成 TurnTrigger/TurnEndReason,很容易让人以为它俩是同生共死的一对。实际情况是后者活得好好的。

TurnEndReasonMap 的成员在 docs/subsystems/session.md:553-572 有完整列举,共六种:completedabortedblockederrormax-tokensinterrupted。它有明确的语义规则可查,比如一个 turn 里只要有任一 step 撞到输出 token 上限,整个 turn 的结束原因就是 max-tokens 而不是 completedsession.md:575);再比如 interrupted 是唯一一个 agent loop 永远不会写入的原因,它由崩溃恢复合成——读到只有 turn/start 没有 turn/end 的日志时,补一条 turn/end { reason: { kind: 'interrupted' } }docs/subsystems/persistence.md:15)。

所以你在文档里读到这对名字时,正确的做法是分别去代码里核一遍,而不是因为一半能对上就默认另一半也在。

同一仓库里还有几处同类落差

顺着这条线核下去,会发现类型词汇表与文档描述对不上的地方不止一处。下面几条都在同一批文件里,核对方法和上面完全一样:先在 docs/ 里找到那句表述,再回 packages/ 下打开源码那一行。

「十二个事件变体」与源码的 13 个。 docs/subsystems/core.md:248 逐个列举了十二种事件变体,其中含 steering/message。而源码里 packages/core/session/src/types.ts:236 起的 SessionEventMap 接口,我们数出的核心成员是 13 个,不含 steering/message,且多出 request/contextsession/end-seed 两个。

steering/message 在源码里并非完全不存在,但它出现的位置很特别:packages/session/session-persistence/src/coordinator.ts:328 有一行 const legacySteeringType: string = 'steering/message':343-365 是把它升级成 user/message 的迁移函数,错误文案里称它为 pre-react-loop steering/message。换句话说,它在快照里是持久化迁移路径上的一个遗留类型字符串,不是当前事件词汇表的成员。

压缩事件是三个还是四个。 docs/subsystems/compaction.md:11 原文写压缩通过声明合并向 SessionEventMap 加入 three event types,紧接着 :13-17 的表格列了 compaction/startcompaction/summarycompaction/end 三个。源码 packages/compaction/compaction/src/types.ts 的同一个 declare module 块里声明的是四个,多一个 compaction/prune:81)。生成的目录侧也是四个:packages/core/session/src/known-event-types.ts:29-32,以及 docs/persistence-catalog.md:299#### compaction/prune — log-only。同一页文档下方 compaction.md:224pruneSession 签名注释里,compaction/prune 自己就出现了。

baseline 是两种还是三种。 docs/subsystems/token-meter.md:29 只解释了 baseline.kind === 'usage'estimated 两种。源码 packages/llm/token-meter/src/types.ts:15-18 声明的是三元联合,多一个 { kind:'none'; tokens: 0 }packages/llm/token-meter/src/index.ts:128-130 在「没有可复用锚点、header 为 undefined、surface tokens 为 0」的条件下确实返回这一种。

这几条我们同样只陈述差异、标明两侧位置,不延伸。

生成的文档要和手写的分开看

有一点值得单独拎出来:这个仓库里相当一部分文档是脚本生成的,文件头会写明这件事。比如 docs/agent-lifecycle.md 开头 1-2 行注明由 scripts/gen-doc-graphs.ts 生成、不要手改;packages/core/session/src/known-event-types.ts:1-6 注明由 scripts/gen-persistence-catalog.ts 生成、不要手改。

TurnTrigger 恰好出现在两个生成脚本的映射表里(gen-cordis-catalog.ts:364gen-persistence-catalog.ts:48)。所以当你核对一处「文档说 A、代码是 B」时,先看清这份文档是手写的还是生成的、生成它的脚本读的是哪份输入——这决定了你该去哪里核第二遍。这里我们只陈述文件头的这些标注和映射表里那两行的存在,不去推断生成链路的具体行为。

顺带一提,TurnTrigger 这个名字在快照里还出现在 .agents/notes/archived/proposed/ 目录下的旧笔记中,例如 .agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md:14 记录了 TurnTriggerMap.continuation。这是一条位置事实,我们只记录它出现在这个路径下,不据此推断任何结论。

你自己怎么判断一个名字是不是「幽灵」

把上面的过程收成一个可执行的动作,大致是四步:

  1. 限定搜索范围再搜。 全仓搜 TurnTrigger 会同时命中 docs/scripts/.agents/notes/,看着像「有」。要判断它是不是当前代码词汇表的一员,得把范围收到实现代码所在的目录,也就是 packages/ 下的 .ts
  2. 找到文档指向的那一页,读它自己怎么说。 索引表和跨页引用往往滞后于正文。本例里 session.md 那一页正文自己就写了 turn/start 没有 trigger 字段。
  3. 回到定义源。 事件词汇表的定义源是 packages/core/session/src/types.ts 里的 SessionEventMap,全仓已知事件类型的汇总在 packages/core/session/src/known-event-types.ts(我们数出 44 个,其中 docs/persistence-catalog.md 里标 surface 的只有 3 个:assistant/messagetool/resultuser/message)。类型名对不上时,以你实读的源码为准。
  4. 确认这不是别的原因。 如果你搜到的命中全在 .zh.md.agents/notes/scripts/ 里,那说明你搜的范围包含了非实现代码;如果你其实搜的是 TurnEndReason,那它在源码里是实打实存在的,别一起归到「查无此物」里;如果你在的是另一个提交而不是 47f9438,那结果不同是正常的——这个仓库我们采集时距离建仓只有三天,README 自述会有破坏性变更。

Windows 侧如果没有 grep,可以用一段自己写的 Python 遍历 packages/ 下的 .ts 文件、按行匹配标识符来做同样的判断——这段检索脚本是读者自己的核查工具,不属于该仓库的内容,也不需要安装或运行这个项目本身。本文所有结论都只来自对文件内容的静态阅读。

最后重复一遍分寸:本文列出的每一处差异,都是「A 处写的是这样、B 处写的是那样」的并列陈述。我们没有安装、没有运行过这个项目,也没有依据去判断哪一侧更准确、为什么会出现这种差异,或者这说明了什么。你要做的也只是:读文档时留个心眼,遇到对不上的名字,回源码数一遍。

延伸阅读


本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的 架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-16,对应仓库快照 47f9438(版本 0.1.0-rc.5)。 本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目, 因此不涉及界面外观、操作手感与运行速度的任何描述。 该仓库建立于 2026-08-13,README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更, 文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。