docs: correct the implicit output-key section — the completion binding is transient
The completion is bound to `output` only while state_updates templates render, then restored; an explicit `output` key in state_updates is now honored; contention on `output` at a parallel join occurs only when the key was absent before the super-step (every llm/agent/rag branch with state_updates then leaves `output: null`). Recommends the named-key pattern for map branches and fixes the reducer example and the Branch-isolation bullet to match.
1 file changed
+43
-17
+43
-17
@@ -1141,15 +1141,35 @@ validator does not model. The most important of these is the implicit
|
|||||||
|
|
||||||
### The implicit `output` key (gotcha)
|
### The implicit `output` key (gotcha)
|
||||||
|
|
||||||
Every LLM (and agent) node stores its raw completion in the `output` state
|
`{{output}}` in a `state_updates` template refers to the node's completion
|
||||||
key. That is what `{{output}}` in `state_updates` templates against. In a
|
(the raw text, or the extracted object when `output_schema` is set). The
|
||||||
parallel super-step this counts as a write from every LLM/agent branch, so
|
binding is **transient**: the executor sets `output` to the completion,
|
||||||
a fan-out of **two or more LLM/agent nodes always contends on `output`**,
|
renders every `state_updates` template, then puts `output` back to whatever
|
||||||
even when every declared `state_updates` key is distinct.
|
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
|
```yaml
|
||||||
(`state_updates` ∪ `output_schema`), this conflict surfaces at **runtime,
|
state_updates:
|
||||||
at the join**, after the branches have already executed:
|
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
|
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.
|
rename one writer.
|
||||||
```
|
```
|
||||||
|
|
||||||
The fix is one declaration at the graph root. When (as is typical) each
|
When `output` already existed before the super-step, every branch restores
|
||||||
branch captures its real result into its own key via `state_updates` and
|
the same prior value, the state diff sees no write, and there is no
|
||||||
nothing reads `output` after the join, `overwrite` is the idiomatic
|
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:
|
choice:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
reducers:
|
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
|
Merge order is deterministic (branches sorted by
|
||||||
`(node_id, invocation_index)`), so even `overwrite` reproducibly keeps the
|
`(node_id, invocation_index)`), so `overwrite` is reproducible across runs.
|
||||||
last-sorted branch's completion across runs.
|
|
||||||
|
|
||||||
Rule of thumb: **any parallel super-step containing 2+ LLM/agent nodes
|
Rule of thumb: **a parallel super-step containing 2+ LLM/agent/rag nodes
|
||||||
needs a reducer for `output`.**
|
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
|
## Branch isolation
|
||||||
|
|
||||||
@@ -1186,7 +1211,8 @@ This means:
|
|||||||
|
|
||||||
- Branches can freely mutate state **during** the super-step without
|
- Branches can freely mutate state **during** the super-step without
|
||||||
stepping on siblings — but every key a branch wrote participates in the
|
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
|
any key written by 2+ branches needs a declared reducer (see
|
||||||
[the implicit `output` key](#the-implicit-output-key-gotcha))
|
[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
|
||||||
|
|||||||
Reference in new issue
Block a user