From 2ff6981866ddb39794215c1f76620b3563e87271 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Mon, 31 Aug 2026 16:41:34 -0600 Subject: [PATCH] docs: corrected the graph-agent parallel reducer gotcha --- Graph-Agents.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/Graph-Agents.md b/Graph-Agents.md index ffcfcad..5b857cf 100644 --- a/Graph-Agents.md +++ b/Graph-Agents.md @@ -1065,6 +1065,48 @@ parallel super-step but no reducer is declared for 'summary'. Add 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 Each parallel branch runs against an **independent state fork**. Writes @@ -1075,8 +1117,11 @@ merges them via the reducer pipeline. This means: -- Branches can freely mutate `output`, internal counters, etc. without - worrying about siblings +- Branches can freely mutate state **during** the super-step without + 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 the parallel phase - Branches cannot communicate with each other mid-super-step. If you