Objective
The naive state schema, and why it breaks under parallelism
from typing_extensions import TypedDict
class ResearchState(TypedDict):
research_question: str
sub_tasks: list
web_findings: str
technical_findings: str
market_findings: str
final_report: str
citations: list # looks reasonable... until 3 agents each append to it
Notice web_findings, technical_findings, and market_findings are separate fields -- each written by exactly ONE specialist, so no conflict there. citations, though, is different: all 3 specialists append their own sources to that SAME shared list in the SAME parallel step (Lesson 5's fan-out). A plain-typed field has no defined merge behavior for 2 simultaneous writes, and current LangGraph refuses to guess: it raises InvalidUpdateError the instant Web Researcher and Technical Analyst both return citation updates in the same step, and your graph run dies before a report is ever produced.
This is the exact bug the SPEC's hotspot list warns about
"Students who don't understand state reducers will get silent data loss" is not a hypothetical — it is the single most commonly cited real bug when people first build parallel LangGraph systems. The fix is one line per field, but you have to know to add it; LangGraph will not tell you it's missing.
The fix: annotated reducers
import operator
from typing import Annotated
from typing_extensions import TypedDict
class ResearchState(TypedDict):
research_question: str
sub_tasks: list
web_findings: str
technical_findings: str
market_findings: str
final_report: str
citations: Annotated[list[str], operator.add] # concatenates, doesn't overwrite
status: str
Annotated[list[str], operator.add] tells LangGraph exactly what to do when multiple nodes update citations in the same parallel step: concatenate the lists (operator.add on two lists is list concatenation) rather than picking one and discarding the rest. web_findings/technical_findings/market_findings stay plain str fields with default overwrite behavior, because each has exactly one writer -- reducers are specifically for fields more than one node might update in the same parallel step, not a blanket rule to apply everywhere.
Your research_question field is set once by the initial input and never modified by any node afterward. Does it need an Annotated[..., operator.add] reducer?
Reducers solve a specific problem: multiple concurrent writes to the same field. A field that's set once at the start and never touched by a node again has no concurrent-write scenario to resolve, so a plain (overwrite) type is correct and clearer -- adding reducers everywhere "to be safe" adds noise without preventing any real bug, since the bug only exists where genuine concurrent writes happen.
What you're building
research_state.py: the full ResearchState schema with every field the 5 agents need (research_question, sub_tasks, web_findings, technical_findings, market_findings, final_report, citations, status) — the citations field carries the operator.add reducer since all 3 specialists append to it in the same parallel step, the per-specialist findings fields stay plain since each has exactly one writer — verified by simulating 3 concurrent citation-appending nodes and confirming all 3 contributions survive, not just the last one.