DeepSeek Harness 里的 TurnTrigger:文档还留着,packages 里搜不到
顺着文档读源码,最容易踩的一种坑是:文档给你一个类型名,你照着这个名字去代码里找实现,找了半天什么都没有,于是开始怀疑自己 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的正文里,同样把「theTurnTrigger/TurnEndReasonreasons」指到session.md;docs/subsystems/core.md:290的表格里还有独立的一行:TurnTriggerMap | dsh-session | TurnTrigger | session.md。
生成脚本里也有。scripts/gen-cordis-catalog.ts:364 与 scripts/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.md 把 TurnTrigger 指向 session.md,session.md 这一页正文明写 turn/start 上没有 trigger 字段,packages/ 下的 TypeScript 文件里搜不到这个标识符。三处位置的口径就是这样,我们只陈述这个差异,不推断哪一处「才是对的」,也不推断这个类型是被删掉了还是从来没合入——我们没有依据回答后面这个问题。
别把一对名字当成一个东西
TurnTrigger 和 TurnEndReason 在文档里几乎总是连着写成 TurnTrigger/TurnEndReason,很容易让人以为它俩是同生共死的一对。实际情况是后者活得好好的。
TurnEndReasonMap 的成员在 docs/subsystems/session.md:553-572 有完整列举,共六种:completed、aborted、blocked、error、max-tokens、interrupted。它有明确的语义规则可查,比如一个 turn 里只要有任一 step 撞到输出 token 上限,整个 turn 的结束原因就是 max-tokens 而不是 completed(session.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/context 与 session/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/start、compaction/summary、compaction/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:224 的 pruneSession 签名注释里,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:364、gen-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。这是一条位置事实,我们只记录它出现在这个路径下,不据此推断任何结论。
你自己怎么判断一个名字是不是「幽灵」
把上面的过程收成一个可执行的动作,大致是四步:
- 限定搜索范围再搜。 全仓搜
TurnTrigger会同时命中docs/、scripts/、.agents/notes/,看着像「有」。要判断它是不是当前代码词汇表的一员,得把范围收到实现代码所在的目录,也就是packages/下的.ts。 - 找到文档指向的那一页,读它自己怎么说。 索引表和跨页引用往往滞后于正文。本例里
session.md那一页正文自己就写了turn/start没有 trigger 字段。 - 回到定义源。 事件词汇表的定义源是
packages/core/session/src/types.ts里的SessionEventMap,全仓已知事件类型的汇总在packages/core/session/src/known-event-types.ts(我们数出 44 个,其中docs/persistence-catalog.md里标surface的只有 3 个:assistant/message、tool/result、user/message)。类型名对不上时,以你实读的源码为准。 - 确认这不是别的原因。 如果你搜到的命中全在
.zh.md、.agents/notes/或scripts/里,那说明你搜的范围包含了非实现代码;如果你其实搜的是TurnEndReason,那它在源码里是实打实存在的,别一起归到「查无此物」里;如果你在的是另一个提交而不是47f9438,那结果不同是正常的——这个仓库我们采集时距离建仓只有三天,README 自述会有破坏性变更。
Windows 侧如果没有 grep,可以用一段自己写的 Python 遍历 packages/ 下的 .ts 文件、按行匹配标识符来做同样的判断——这段检索脚本是读者自己的核查工具,不属于该仓库的内容,也不需要安装或运行这个项目本身。本文所有结论都只来自对文件内容的静态阅读。
最后重复一遍分寸:本文列出的每一处差异,都是「A 处写的是这样、B 处写的是那样」的并列陈述。我们没有安装、没有运行过这个项目,也没有依据去判断哪一侧更准确、为什么会出现这种差异,或者这说明了什么。你要做的也只是:读文档时留个心眼,遇到对不上的名字,回源码数一遍。
延伸阅读
- 从头读起:DeepSeek Harness 是什么:建仓三天、13 万 star 的 Agent 框架
- 本专题共 45 篇,完整分组目录见专题页
- DeepSeek Harness 的 spill:上下文放不下时溢出到哪、边界在哪
- DeepSeek Harness 的会话生命周期:事件、投影与持久化三层
本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的
架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-16,对应仓库快照 47f9438(版本 0.1.0-rc.5)。
本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目,
因此不涉及界面外观、操作手感与运行速度的任何描述。
该仓库建立于 2026-08-13,README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更,
文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。