taskflow

续跑

taskflow 运行如何跨会话存活。

你那二十分钟的审计在最后一步因为速率限制失败了。十九分钟的子代理工作成功了;一个阶段绊在一个瞬时错误上,运行停了下来。没有续跑,你会从零开始——重新读取每个文件、重新运行每条审查、为这一切再付一遍钱。

taskflow 不这样工作。一次运行不绑定到你的会话。每个完成的阶段在它完成的那一刻就被写入磁盘,所以一个失败、被中止或只是比它的终端活得更久的运行,可以稍后被捡起来——而且只有没完成的工作会被重做。

续跑如何工作

当你续跑一次运行时,运行时遍历 DAG,对每个阶段问一个问题:你依赖的东西,自你上次运行以来变了没?

加载持久化的运行状态。 每个阶段上一次的结果——它的输出、它的输入、它完成的时刻——已经在磁盘上了。

为每个阶段计算输入哈希。 这个哈希把所有实质上影响子代理会产出什么的东西折在一起。

跳过哈希没动的阶段。 如果一个阶段已经完成且它的输入完全相同,它的结果被复用。不花任何 token。

重跑变了或从未完成的阶段。 只有这些花 token。它们的新结果级联到依赖者,依赖者按同样的方式重新评估。

续跑一次运行
/tf resume nightly-audit-abc123

什么算作"变了"

一个阶段的输入哈希就是全部。它包括:

  • 解析后的 task 文本——在插值之后,所以如果它插值的某个上游输出变了,哈希就变。
  • 解析后的 over 条目,对 map 阶段而言。
  • 解析后的 context 文件——它们预读的内容是身份的一部分。
  • 传给运行的解析后的 args
  • flow 名称、阶段的结构指纹,以及任何 thinkingtools 覆盖。

关键洞察是第一条。因为 task 文本是在哈希之前插值的,一个读取 {steps.audit.output} 的阶段会在 audit 的输出每次变化时产生不同的哈希。依赖关系自动传播——你不用声明"audit 变了就重跑我",插值替你做了。

如果上游阶段产出的东西什么都没变,下游哈希就相同,下游阶段被跳过。

一个具体例子

会话 1。 你启动一次夜间审计。它发现了十个文件,审查了每一个,正要总结时 provider 返回了一个 429:

会话 1 —— 运行停下
✓ discover      (10 files found)
✓ audit         (map over 10 files — all done)
✗ review        (rate limit — run paused)

运行以这个精确状态被持久化。discoverauditdonereview 从未完成。

会话 2。 第二天早上,从一个全新的终端,你续跑:

会话 2 —— 从停下的地方接上
/tf resume nightly-audit-abc123
会话 2 —— 只填补空缺
✓ discover      (skipped — cache: run-only)
✓ audit         (skipped — cache: run-only)
◐ review        (re-running)
✓ report        (ran, because review finished)

discoveraudit 被免费复用——它们带一个 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 —— 一个环境变量的值。

如果任何指纹条目变了,缓存键就变,阶段重新运行。输入哈希和指纹的组合正是让一次缓存命中可靠的原因——你只会在它依赖的一切都没动时复用一个结果。

跨运行何时被禁用

有几种阶段类型永远不能被跨运行缓存,因为它们每次运行都必须产生一个新鲜的结果:

  • gateapproval —— 上周做的决定不是今天的决定。
  • looptournament —— 它们的结果取决于迭代动态。
  • script —— shell 命令往往有副作用。

一个标记为 idempotent: false 的阶段也被排除——它有不可逆副作用(一次部署、一个 webhook、一次数据库写入),绝不能从缓存重放。在这些上把 cache.scope 设成 cross-run 是一个校验错误,由 /tf verify 捕获。

全 flow 启用

如果一个 flow 的大部分都能安全缓存,在顶层设 incremental: true 把每个阶段默认成 cross-run

增量 flow
{
  "name": "nightly-audit",
  "incremental": true,
  "phases": [/* ... */]
}

重新运行一个增量 flow 会跨运行和会话复用未变阶段。上面被阻止的类型仍然优先——即使在 incremental 下,gate 和 approval 也总是新鲜运行,所以安全性永远不会为速度让步。

跨运行缓存住在你的工作目录下的磁盘上,上限 1000 条目,90 天硬性年龄限制。驱逐是自动的、尽力而为的——一个满了的缓存永远不会破坏一次运行。

续跑,端到端

把碎片拼起来,一次 taskflow 运行是一个持久的人造物,不是一个短暂的脚本:

  • 每个阶段在它完成时就被持久化,所以一次崩溃或一个关掉的终端永远不会丢失已完成的工作。
  • 运行内续跑免费复用已完成阶段,只重跑失败或从未运行的部分。
  • 跨运行记忆化把这种复用延伸到完全独立的运行之间,由指纹把关,所以变了的世界总是正确地失效。
  • 最终输出仍然遵守上下文隔离——只有结果到达你的对话,不管复用了多少阶段来产生它。

结果是:你周一运行的 flow 和周二重新运行的 flow,成本大约是周一和周二之间的差异——而不是全价付两次。

下一步

Last updated on

这页内容对你有帮助吗?

帮助我们改进文档,或在社区中提问。

On this page