State and reducers¶
State schemas, channels, and the built-in reducers. See the State and reducers guide for the concepts.
agentflow.state
¶
State model: channels, reducers, and update merging.
A workflow's state is a set of named channels. Each channel has a
reducer: a pure function (current, update) -> new that folds a node's
partial update into the current value. Reducers are what make concurrent
node updates in a single super-step merge deterministically instead of
racing on last-writer-wins.
Schemas are declared as a TypedDict subclass of :class:State, with each
field optionally annotated with its reducer::
from typing import Annotated
from agentflow.state import State, append, add, last
class ChatState(State):
messages: Annotated[list, append] # concatenate
tokens: Annotated[int, add] # sum
best: Annotated[float, last] # overwrite (the default)
A field without an explicit reducer uses :func:last (last-value-wins).
This module is pure data: no async, no I/O, no engine imports.
State
¶
Bases: TypedDict
Base class for a workflow state schema.
Subclass with Annotated[type, reducer] fields. Being total=False
means every channel is optional at the type level — nodes emit partial
updates, and a channel simply has no value until first written.
Source code in agentflow/state.py
Channel
¶
last
¶
append
¶
Concatenate list updates onto a list channel.
current of None is treated as an empty list. A non-list update
is appended as a single element; a list update is extended in order.
Source code in agentflow/state.py
add
¶
merge
¶
Shallow dict merge; keys in update win. None current is {}.
by_key
¶
Build a reducer for a dict channel that folds each key with its own reducer.
Where :func:merge applies one policy (last-wins) to a whole dict, this
lets a single structured channel hold keys that accumulate differently —
for example an intents list that grows with :func:append beside an
events list that is replaced with :func:last and an incumbent that
is overwritten whole::
search = by_key({
"intents": append, # grow
"candidates": append, # grow (concurrent workers each add one)
"verified": append, # grow
"events": last, # replace (planner clears what it consumed)
"incumbent": last, # overwrite the current best, whole
})
class OptState(State):
search: Annotated[dict, search]
Folding rules, given current (None is treated as {}) and an
update dict:
- Only keys present in
updateare folded; a key absent fromupdatekeeps itscurrentvalue untouched. - Each folded key
kis combined aspolicy[k](current.get(k), update[k])— so{"events": []}genuinely clearsevents(that is alastof an empty list, not a no-op), while{}orNoneis a no-op returning a copy ofcurrent. - A key in
updatethat is not inpolicyraises :class:KeyError. Writing an undeclared field is a bug (usually a typo), mirroring how the engine rejects writes to undeclared top-level channels. - A key in
currentthat is not inpolicyis passed through unchanged; the policy only governs keys an update actually writes.
The value under each key is combined whole by that key's reducer; this is one level of structure, not a recursive deep-merge. It is for distinct keys that accumulate differently, not for concurrent partial patches of the same nested object.
Source code in agentflow/state.py
union
¶
Set union. None current is the empty set.