续跑
taskflow 运行如何跨会话存活。
你那二十分钟的审计在最后一步因为速率限制失败了。十九分钟的子代理工作成功了;一个阶段绊在一个瞬时错误上,运行停了下来。没有续跑,你会从零开始——重新读取每个文件、重新运行每条审查、为这一切再付一遍钱。
taskflow 不这样工作。一次运行不绑定到你的会话。每个完成的阶段在它完成的那一刻就被写入磁盘,所以一个失败、被中止或只是比它的终端活得更久的运行,可以稍后被捡起来——而且只有没完成的工作会被重做。
续跑如何工作
当你续跑一次运行时,运行时遍历 DAG,对每个阶段问一个问题:你依赖的东西,自你上次运行以来变了没?
加载持久化的运行状态。 每个阶段上一次的结果——它的输出、它的输入、它完成的时刻——已经在磁盘上了。
为每个阶段计算输入哈希。 这个哈希把所有实质上影响子代理会产出什么的东西折在一起。
跳过哈希没动的阶段。 如果一个阶段已经完成且它的输入完全相同,它的结果被复用。不花任何 token。
重跑变了或从未完成的阶段。 只有这些花 token。它们的新结果级联到依赖者,依赖者按同样的方式重新评估。
/tf resume nightly-audit-abc123什么算作"变了"
一个阶段的输入哈希就是全部。它包括:
- 解析后的 task 文本——在插值之后,所以如果它插值的某个上游输出变了,哈希就变。
- 解析后的
over条目,对map阶段而言。 - 解析后的
context文件——它们预读的内容是身份的一部分。 - 传给运行的解析后的
args。 - flow 名称、阶段的结构指纹,以及任何
thinking或tools覆盖。
关键洞察是第一条。因为 task 文本是在哈希之前插值的,一个读取 {steps.audit.output} 的阶段会在 audit 的输出每次变化时产生不同的哈希。依赖关系自动传播——你不用声明"audit 变了就重跑我",插值替你做了。
如果上游阶段产出的东西什么都没变,下游哈希就相同,下游阶段被跳过。
一个具体例子
会话 1。 你启动一次夜间审计。它发现了十个文件,审查了每一个,正要总结时 provider 返回了一个 429:
✓ discover (10 files found)
✓ audit (map over 10 files — all done)
✗ review (rate limit — run paused)运行以这个精确状态被持久化。discover 和 audit 是 done;review 从未完成。
会话 2。 第二天早上,从一个全新的终端,你续跑:
/tf resume nightly-audit-abc123✓ discover (skipped — cache: run-only)
✓ audit (skipped — cache: run-only)
◐ review (re-running)
✓ report (ran, because review finished)discover 和 audit 被免费复用——它们带一个 cache: run-only 标记,花了零新 token。review 运行,因为它从未完成。report 运行,因为它依赖 review,而 review 的输出现在和"什么都没有"不同了。失败点之后的每个阶段都重新评估,只有那些输入真的动了的阶段花 token。
这是运行内续跑。它不需要任何配置就能启用——它是默认行为。每次运行从它启动的那一刻起就是可续跑的。
跨运行记忆化
运行内续跑复用的是同一次运行的结果。但有时你上周做的工作和今天要做的工作完全一样——一次不同的运行,同样的输入。taskflow 也能复用那个。
用一个 cache 字段把一个阶段 opt in 到跨运行缓存:
{
"id": "analyze-auth",
"agent": "analyst",
"task": "Analyze the auth layer in src/auth/.",
"cache": {
"scope": "cross-run",
"fingerprint": ["git:HEAD", "glob:src/auth/**/*.ts"],
"ttl": "7d"
}
}现在,在这个阶段运行之前,运行时会检查一个持久化存储,键由阶段的输入哈希加上它的指纹组成。如果任何先前的运行用相同的输入计算过这个阶段——同样的 task、同样的文件、同样的 git 提交——存储的结果以 $0.00 被恢复,子代理根本不会被 spawn。
指纹
fingerprint 列表是你告诉缓存"世界变了"的方式。每一条目都是一个折进键里的新鲜度信号:
git:HEAD—— 当前提交。新提交意味着新键,意味着缓存未命中。glob:src/auth/**/*.ts—— 每个匹配文件的大小和 mtime。glob!:src/auth/**/*.ts—— 每个匹配文件的内容哈希。捕获不碰 mtime 的变更。file:package.json—— 一个特定文件的内容哈希。env:CI—— 一个环境变量的值。
如果任何指纹条目变了,缓存键就变,阶段重新运行。输入哈希和指纹的组合正是让一次缓存命中可靠的原因——你只会在它依赖的一切都没动时复用一个结果。
跨运行何时被禁用
有几种阶段类型永远不能被跨运行缓存,因为它们每次运行都必须产生一个新鲜的结果:
gate和approval—— 上周做的决定不是今天的决定。loop和tournament—— 它们的结果取决于迭代动态。script—— shell 命令往往有副作用。
一个标记为 idempotent: false 的阶段也被排除——它有不可逆副作用(一次部署、一个 webhook、一次数据库写入),绝不能从缓存重放。在这些上把 cache.scope 设成 cross-run 是一个校验错误,由 /tf verify 捕获。
全 flow 启用
如果一个 flow 的大部分都能安全缓存,在顶层设 incremental: true 把每个阶段默认成 cross-run:
{
"name": "nightly-audit",
"incremental": true,
"phases": [/* ... */]
}重新运行一个增量 flow 会跨运行和会话复用未变阶段。上面被阻止的类型仍然优先——即使在 incremental 下,gate 和 approval 也总是新鲜运行,所以安全性永远不会为速度让步。
跨运行缓存住在你的工作目录下的磁盘上,上限 1000 条目,90 天硬性年龄限制。驱逐是自动的、尽力而为的——一个满了的缓存永远不会破坏一次运行。
续跑,端到端
把碎片拼起来,一次 taskflow 运行是一个持久的人造物,不是一个短暂的脚本:
- 每个阶段在它完成时就被持久化,所以一次崩溃或一个关掉的终端永远不会丢失已完成的工作。
- 运行内续跑免费复用已完成阶段,只重跑失败或从未运行的部分。
- 跨运行记忆化把这种复用延伸到完全独立的运行之间,由指纹把关,所以变了的世界总是正确地失效。
- 最终输出仍然遵守上下文隔离——只有结果到达你的对话,不管复用了多少阶段来产生它。
结果是:你周一运行的 flow 和周二重新运行的 flow,成本大约是周一和周二之间的差异——而不是全价付两次。
下一步
Last updated on