feat(todo): make the parallel in_progress policy configurable
Whether concurrent active tasks are legitimate depends on runtime concurrency the tool cannot observe, but whether a deployment's agents ever fan out is knowable at composition time. `allowParallelInProgress` (default true) therefore replaces the hardcoded policy: the flag moves the model-facing instruction and the accepted input together, so a deployment running strictly sequential agents can restore the single-active discipline from cordis.yml. The durable-log invariant does not follow the flag. A log written while parallel work was allowed must still replay after a deployment tightens the policy, so the invariant stays silent on the active count.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-26-todo-parallel-in-progress.md
|
||||
2026-07-26-todo-parallel-in-progress.md: 61bfe4bdf03441fa2369683060efcbc35493a95f
|
||||
2026-07-26-todo-parallel-in-progress.zh.md: e4098cd0c3151867c788b15b3226cff866f6c23d
|
||||
2026-07-26-todo-parallel-in-progress.md: 24803da46f12f4fd6e8d62f493ea097dcd099718
|
||||
2026-07-26-todo-parallel-in-progress.zh.md: d44492dbfdbe986985fa17cc7b9b975dc1ddcd9c
|
||||
|
||||
@@ -10,11 +10,10 @@ The [original `todo_write` design](2026-06-29-todo-write-tool.md) enforced at mo
|
||||
|
||||
## Decision
|
||||
|
||||
Remove the single-`in_progress` cap everywhere it was enforced and let any number of tasks be `in_progress`:
|
||||
Make the single-`in_progress` cap a deployment policy instead of a fixed rule, defaulting to allowing several:
|
||||
|
||||
- `execute` in `packages/todo/tool-todo/src/index.ts` no longer counts `in_progress` items; the `at most one task may be in_progress` error is gone from the tool's stable failure set.
|
||||
- The durable-log invariant in `packages/todo/tool-todo/src/invariant.ts` no longer rejects snapshots with several active items, so previously-persisted logs are unaffected and parallel snapshots replay cleanly.
|
||||
- The tool description now instructs the model to mark every actively-worked task `in_progress` — several during parallel work, one for sequential work — and to keep at least one while work remains.
|
||||
- `packages/todo/tool-todo/src/index.ts` gains `Config.allowParallelInProgress` (default `true`). At `true`, `execute` accepts any number of active items and the description instructs the model to mark every actively-worked task — several during parallel work, one for sequential work — keeping at least one while work remains. At `false`, the description asks for exactly one and `execute` rejects a call marking more.
|
||||
- The durable-log invariant in `packages/todo/tool-todo/src/invariant.ts` no longer rejects snapshots with several active items, and does not follow the config, so previously-persisted logs are unaffected and parallel snapshots replay cleanly under either policy.
|
||||
|
||||
The remaining coded invariants are unchanged: non-empty trimmed unique `content`, valid status enum. This supersedes the "at most one active" clause of the [original design's validation decision](2026-06-29-todo-write-tool.md); the rest of that Agent Note (whole-list replace, log-backed state, single owner) stands.
|
||||
|
||||
@@ -22,10 +21,19 @@ The remaining coded invariants are unchanged: non-empty trimmed unique `content`
|
||||
|
||||
A coded invariant can only see the list, not the runtime: whether two `in_progress` items are legitimate depends on whether work is actually running concurrently, which the tool cannot observe. Enforcing a cap was therefore wrong in exactly the cases parallelism made it matter, and any replacement (for example, capping active items at the live subagent count) would couple the tool to runtimes it deliberately knows nothing about. The discipline of matching `in_progress` marks to genuinely concurrent work moves to the tool description, the same place ordering and list freshness already live.
|
||||
|
||||
## The policy is a deployment choice
|
||||
|
||||
Whether concurrent active tasks are legitimate depends on runtime concurrency the tool cannot observe — but whether a deployment's agents ever run work concurrently is knowable at composition time. That makes the policy a `Config` field rather than a constant: `allowParallelInProgress` (default `true`) is set from cordis.yml, and a deployment whose agents never fan out can restore the single-active discipline.
|
||||
|
||||
The flag moves the model-facing instruction and the accepted input together. Splitting them would be the bug: a description asking for one active task while `execute` accepts several teaches the model a rule the tool does not hold, and the reverse rejects calls the description invited. Only the active-status clause of the description varies, because that is the only instruction the policy changes.
|
||||
|
||||
The durable-log invariant deliberately does NOT follow the flag. A log written while parallel work was allowed must still replay after a deployment tightens the policy, so tying `invariant.ts` to the current config would reject history that was valid when it was written. The invariant stays silent on the active count; the tool is where the policy applies, at the moment of the write.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep the cap and add an explicit parallel opt-in flag** — an extra argument on every call to serve the common case; the flag would be noise for sequential work and still unverifiable.
|
||||
- **Cap active items at a configured maximum** — any fixed number is arbitrary, and a deployment-varying tunable for list coherence has no principled value.
|
||||
- **Cap active items at a configured maximum** — any fixed number is arbitrary. This is why the config field is a boolean policy switch and not a count: "may several tasks be active" is a property of the deployment, while "at most N" invents a threshold nothing can justify.
|
||||
- **Hardcode the parallel policy** — the first revision of this branch did, which is what made `allowParallelInProgress` necessary: a deployment running strictly sequential agents had no way back to the discipline it wanted.
|
||||
|
||||
## The display surfaces are part of the change
|
||||
|
||||
@@ -39,4 +47,4 @@ Splitting the count into its own span puts it outside the `.summary` rule, so it
|
||||
|
||||
## Consequences
|
||||
|
||||
A todo list can now faithfully mirror parallel execution, and every UI renders several active markers at once: the TUI's per-status prefix needed no change, the plan strip's header counts the active items, and the row needed the derivation above. The tool no longer rejects a formerly-invalid snapshot shape, so the change is compatible with every previously valid call; only the error path was removed. The model-facing description changed, which re-recorded the tool-catalog page and every `tool-schemas.expected.json` sidecar carrying the todo schema (seven of the eight in the tree). Scenarios composing an identical header share one sidecar through `toolSchemasSource` rather than each keeping a copy, so the count tracks distinct header compositions, not scenarios; a branch changing the tool description still has to refresh whichever sidecars landed after it branched — `pnpm run test:snapshot:refresh` does it keylessly. The web fixture's todo sample now runs two items `in_progress`, so the assembled web transcript replays a parallel plan and would fail again if either surface returned to single-active derivation.
|
||||
A todo list can now faithfully mirror parallel execution, and every UI renders several active markers at once: the TUI's per-status prefix needed no change, the plan strip's header counts the active items, and the row needed the derivation above. Under the default policy the tool no longer rejects a formerly-invalid snapshot shape, so the change is compatible with every previously valid call; a deployment that sets `allowParallelInProgress: false` keeps the old rejection, and the durable-log invariant accepts both. The model-facing description changed, which re-recorded the tool-catalog page and every `tool-schemas.expected.json` sidecar carrying the todo schema (seven of the eight in the tree). Scenarios composing an identical header share one sidecar through `toolSchemasSource` rather than each keeping a copy, so the count tracks distinct header compositions, not scenarios; a branch changing the tool description still has to refresh whichever sidecars landed after it branched — `pnpm run test:snapshot:refresh` does it keylessly. The web fixture's todo sample now runs two items `in_progress`, so the assembled web transcript replays a parallel plan and would fail again if either surface returned to single-active derivation.
|
||||
|
||||
@@ -10,11 +10,10 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
在所有强制它的位置移除单一 `in_progress` 上限,允许任意数量的任务处于 `in_progress`:
|
||||
把单一 `in_progress` 上限从固定规则改为部署策略,默认允许多个:
|
||||
|
||||
- `packages/todo/tool-todo/src/index.ts` 中的 `execute` 不再统计 `in_progress` 条目;`at most one task may be in_progress` 错误已从工具稳定的失败集合中移除。
|
||||
- `packages/todo/tool-todo/src/invariant.ts` 中的持久日志不变式不再拒绝含多个活跃条目的快照,因此此前持久化的日志不受影响,并行快照也能干净回放。
|
||||
- 工具描述现在指示模型把每个正在处理的任务标记为 `in_progress`(并行工作时可以有多个,顺序工作时只有一个),并在仍有工作未完成时至少保留一个。
|
||||
- `packages/todo/tool-todo/src/index.ts` 新增 `Config.allowParallelInProgress`(默认 `true`)。为 `true` 时,`execute` 接受任意数量的活跃条目,描述指示模型把每个正在处理的任务标记为 `in_progress`(并行工作时可以有多个,顺序工作时只有一个),并在仍有工作未完成时至少保留一个;为 `false` 时,描述要求恰好一个,`execute` 拒绝标记更多的调用。
|
||||
- `packages/todo/tool-todo/src/invariant.ts` 中的持久日志不变式不再拒绝含多个活跃条目的快照,且不跟随该配置,因此此前持久化的日志不受影响,并行快照在任一策略下都能干净回放。
|
||||
|
||||
其余编码的不变式保持不变:`content` 去除首尾空白后非空且唯一、status 为合法枚举值。本决定取代[原始设计的校验决策](2026-06-29-todo-write-tool.md)中「至多一个活跃」的条款;该 Agent Note 的其余部分(整列表替换、日志支撑的状态、单一所有者)依然成立。
|
||||
|
||||
@@ -22,10 +21,19 @@ Status: implemented
|
||||
|
||||
编码的不变式只能看到列表,看不到运行时:两个 `in_progress` 条目是否合理,取决于工作是否真的在并发运行,而这一点工具无法观测。因此,恰恰在并行让上限变得重要的场景里,强制上限反而是错的;任何替代方案(例如把活跃条目数限制为在线 subagent 的数量)都会把工具耦合到它有意一无所知的运行时上。把 `in_progress` 标记与真正并发的工作对应起来这一纪律,转移到工具描述中,也就是排序与列表新鲜度已经所在的地方。
|
||||
|
||||
## 该策略是部署层的选择
|
||||
|
||||
并发的活跃任务是否合理,取决于工具无法观测的运行时并发情况——但一个部署的 agent 是否会并发展开工作,在组装期就是可知的。因此该策略是 `Config` 字段而非常量:`allowParallelInProgress`(默认 `true`)从 cordis.yml 设置,agent 从不并行展开的部署可以恢复单活跃项纪律。
|
||||
|
||||
该开关会同时改变面向模型的指令与接受的输入。把两者拆开才是 bug:描述要求只保留一个活跃任务、而 `execute` 却接受多个,等于教给模型一条工具并不遵守的规则;反过来则会拒绝描述所邀请的调用。描述中只有活跃状态那一句会变化,因为这是该策略唯一改变的指令。
|
||||
|
||||
持久日志不变式刻意**不**跟随该开关。在允许并行时写下的日志,在部署收紧策略之后仍必须可回放,因此把 `invariant.ts` 绑定到当前配置会拒绝在写入当时合法的历史。不变式对活跃数量保持沉默;策略生效之处是工具,时机是写入的那一刻。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **保留上限并增加一个显式的并行 opt-in 标志**——为服务常见场景而给每次调用增加一个额外参数;这个标志对顺序工作而言只是噪声,而且仍然无法验证。
|
||||
- **把活跃条目限制在一个可配置的上限内**——任何固定数字都是任意的,而为列表连贯性设一个随部署变化的可调参数没有原则性价值。
|
||||
- **把活跃条目限制在一个可配置的上限内**——任何固定数字都是任意的。这正是该配置字段是布尔策略开关而非数量的原因:「是否允许多个任务同时活跃」是部署的属性,而「最多 N 个」凭空发明了一个无从论证的阈值。
|
||||
- **把并行策略硬编码**——本分支的第一版就是如此,这也是 `allowParallelInProgress` 之所以必要的原因:运行严格顺序 agent 的部署没有任何办法回到它想要的纪律。
|
||||
|
||||
## 展示面是本次改动的一部分
|
||||
|
||||
@@ -39,4 +47,4 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
现在 todo 列表可以忠实反映并行执行,并且每个 UI 都能一次渲染多个活跃标记:TUI 按状态区分的前缀无需改动,计划横条的表头会计数活跃条目,工具行则需要上述推导。工具不再拒绝一种此前无效的快照形状,因此该改动兼容此前所有合法的调用;被移除的只是错误路径。面向模型的描述发生了变化,这重新记录了 tool-catalog 页面以及每个带有 todo schema 的 `tool-schemas.expected.json` sidecar(树中八个里有七个)。组合出相同 header 的场景通过 `toolSchemasSource` 共用同一份 sidecar,而非各自保留副本,因此这个数量对应的是不同的 header 组合,而不是场景数;改动工具描述的分支仍须刷新它分叉之后落地的那些 sidecar —— `pnpm run test:snapshot:refresh` 可以无 key 完成。web fixture 的 todo 样本现在有两个条目处于 `in_progress`,因此组装后的 web transcript 回放的是一个并行计划;若任一展示面退回单活跃项推导,它会再次失败。
|
||||
现在 todo 列表可以忠实反映并行执行,并且每个 UI 都能一次渲染多个活跃标记:TUI 按状态区分的前缀无需改动,计划横条的表头会计数活跃条目,工具行则需要上述推导。在默认策略下,工具不再拒绝一种此前无效的快照形状,因此该改动兼容此前所有合法的调用;设置了 `allowParallelInProgress: false` 的部署仍保留旧的拒绝行为,而持久日志不变式两者都接受。面向模型的描述发生了变化,这重新记录了 tool-catalog 页面以及每个带有 todo schema 的 `tool-schemas.expected.json` sidecar(树中八个里有七个)。组合出相同 header 的场景通过 `toolSchemasSource` 共用同一份 sidecar,而非各自保留副本,因此这个数量对应的是不同的 header 组合,而不是场景数;改动工具描述的分支仍须刷新它分叉之后落地的那些 sidecar —— `pnpm run test:snapshot:refresh` 可以无 key 完成。web fixture 的 todo 样本现在有两个条目处于 `in_progress`,因此组装后的 web transcript 回放的是一个并行计划;若任一展示面退回单活跃项推导,它会再次失败。
|
||||
|
||||
Reference in New Issue
Block a user