docs: corrected the graph-agent parallel reducer gotcha
+47
-2
@@ -1065,6 +1065,48 @@ parallel super-step but no reducer is declared for 'summary'. Add
|
|||||||
node's output.
|
node's output.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The load-time check covers **declared** writes only. The runtime applies the
|
||||||
|
same contention rule to every key a branch actually wrote (writes are
|
||||||
|
extracted by diffing branch state; see
|
||||||
|
[Branch isolation](#branch-isolation)), including keys the load-time
|
||||||
|
validator does not model. The most important of these is the implicit
|
||||||
|
`output` key:
|
||||||
|
|
||||||
|
### The implicit `output` key (gotcha)
|
||||||
|
|
||||||
|
Every LLM (and agent) node stores its raw completion in the `output` state
|
||||||
|
key. That is what `{{output}}` in `state_updates` templates against. In a
|
||||||
|
parallel super-step this counts as a write from every LLM/agent branch, so
|
||||||
|
a fan-out of **two or more LLM/agent nodes always contends on `output`**,
|
||||||
|
even when every declared `state_updates` key is distinct.
|
||||||
|
|
||||||
|
Because the load-time validator only computes declared write sets
|
||||||
|
(`state_updates` ∪ `output_schema`), this conflict surfaces at **runtime,
|
||||||
|
at the join**, after the branches have already executed:
|
||||||
|
|
||||||
|
```
|
||||||
|
Key 'output' was written by 5 parallel branches but has no reducer
|
||||||
|
declared. Add a reducer for 'output' to the graph's `reducers:` block, or
|
||||||
|
rename one writer.
|
||||||
|
```
|
||||||
|
|
||||||
|
The fix is one declaration at the graph root. When (as is typical) each
|
||||||
|
branch captures its real result into its own key via `state_updates` and
|
||||||
|
nothing reads `output` after the join, `overwrite` is the idiomatic
|
||||||
|
choice:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
reducers:
|
||||||
|
output: overwrite # implicit LLM completion key; unused after the join
|
||||||
|
```
|
||||||
|
|
||||||
|
Merge order is deterministic (branches sorted by
|
||||||
|
`(node_id, invocation_index)`), so even `overwrite` reproducibly keeps the
|
||||||
|
last-sorted branch's completion across runs.
|
||||||
|
|
||||||
|
Rule of thumb: **any parallel super-step containing 2+ LLM/agent nodes
|
||||||
|
needs a reducer for `output`.**
|
||||||
|
|
||||||
## Branch isolation
|
## Branch isolation
|
||||||
|
|
||||||
Each parallel branch runs against an **independent state fork**. Writes
|
Each parallel branch runs against an **independent state fork**. Writes
|
||||||
@@ -1075,8 +1117,11 @@ merges them via the reducer pipeline.
|
|||||||
|
|
||||||
This means:
|
This means:
|
||||||
|
|
||||||
- Branches can freely mutate `output`, internal counters, etc. without
|
- Branches can freely mutate state **during** the super-step without
|
||||||
worrying about siblings
|
stepping on siblings — but every key a branch wrote participates in the
|
||||||
|
join, including the implicit `output` key every LLM/agent node sets, so
|
||||||
|
any key written by 2+ branches needs a declared reducer (see
|
||||||
|
[the implicit `output` key](#the-implicit-output-key-gotcha))
|
||||||
- Race conditions are impossible since there is no shared mutable state during
|
- Race conditions are impossible since there is no shared mutable state during
|
||||||
the parallel phase
|
the parallel phase
|
||||||
- Branches cannot communicate with each other mid-super-step. If you
|
- Branches cannot communicate with each other mid-super-step. If you
|
||||||
|
|||||||
Reference in New Issue
Block a user