diff --git a/Graph-Agents.md b/Graph-Agents.md index 5fb4df6..7a40346 100644 --- a/Graph-Agents.md +++ b/Graph-Agents.md @@ -1141,15 +1141,35 @@ validator does not model. The most important of these is the implicit ### 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. +`{{output}}` in a `state_updates` template refers to the node's completion +(the raw text, or the extracted object when `output_schema` is set). The +binding is **transient**: the executor sets `output` to the completion, +renders every `state_updates` template, then puts `output` back to whatever +it was before the node ran — or to `null` if the key did not exist. An +LLM/agent/rag node therefore **never persists its completion under +`output`** on its own. If you want the completion to survive, name a key: -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: +```yaml +state_updates: + finding: "{{output}}" +``` + +Writing `output` explicitly also works — `state_updates: { output: +"{{output}}" }` leaves `output` equal to the completion — but a named key +is the recommended pattern, especially for map branches (pair it with the +map's `output_key`, see [`map` nodes](#map)). + +Two consequences of the restore step that bite in parallel super-steps: + +- A node with **no `state_updates`** writes nothing at all. A map branch + that omits `state_updates` produces whatever `output` the fork inherited + (usually `null`), not the completion. Always declare what the branch + writes. +- When `output` was **absent** from state before the super-step, every + LLM/agent/rag branch that has `state_updates` leaves `output: null` + behind (the restore falls back to `null`). Two or more such branches + then contend on `output` at the join, even though every declared key is + distinct: ``` Key 'output' was written by 5 parallel branches but has no reducer @@ -1157,22 +1177,27 @@ 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 +When `output` already existed before the super-step, every branch restores +the same prior value, the state diff sees no write, and there is no +contention. The load-time validator only models declared write sets +(`state_updates` ∪ `output_schema`), so the `null` case surfaces at +**runtime, at the join**. The fix is one declaration at the graph root; +since the value being reduced is always `null`, `overwrite` is the right choice: ```yaml reducers: - output: overwrite # implicit LLM completion key; unused after the join + output: overwrite # transient completion binding; always null 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. +`(node_id, invocation_index)`), so `overwrite` is reproducible across runs. -Rule of thumb: **any parallel super-step containing 2+ LLM/agent nodes -needs a reducer for `output`.** +Rule of thumb: **a parallel super-step containing 2+ LLM/agent/rag nodes +with `state_updates` needs a reducer for `output` only if `output` may be +absent from state when the super-step starts.** Seeding `output` in +`initial_state` (or having any earlier node with `state_updates` run +first, which leaves `output: null` behind) makes the reducer unnecessary. ## Branch isolation @@ -1186,7 +1211,8 @@ This means: - 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 + join, including the `output: null` residual an LLM/agent/rag node with + `state_updates` leaves behind when `output` was previously absent, 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