/agentsop-state-reducer
Tool skill for declaring reducers on LangGraph state keys so parallel writes merge instead of crashing. Activates whenever a coder agent designs a StateGraph with parallel branches, fan-out via Send, multi-agent topologies, or whenever a run raises `InvalidUpdateError: At key
$ npx -y skills add agentsope/SkillAlchemy --skill agentsop-state-reducer --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/agentsop-state-reducer
Context preview
The summary Claude sees to decide when to auto-load this skill.
Tool skill for declaring reducers on LangGraph state keys so parallel writes merge instead of crashing. Activates whenever a coder agent designs a StateGraph with parallel branches, fan-out via Send, multi-agent topologies, or whenever a run raises `InvalidUpdateError: At key
SKILL.md
agentsop-state-reducer.SKILL.mdname: agentsop-state-reducer
description: |
Tool skill for declaring reducers on LangGraph state keys so parallel writes
merge instead of crashing. Activates whenever a coder agent designs a
StateGraph with parallel branches, fan-out via Send, multi-agent topologies,
or whenever a run raises `InvalidUpdateError: At key '<k>': Can receive only
one value per step`. Encodes the rule "every state key is single-writer or
has a reducer — nothing in between."
version: 0.1.0
State Reducer · Tool Skill
> Scope: a single decision — for each state key, declare a reducer or > guarantee single-writer. Out of scope: checkpointers, HITL, supervisor vs. > swarm — see `langgraph-sop` for those.
---
1. 何时激活 (Activation Rules)
Activate when **any** trigger fires:
- The task involves a LangGraph `StateGraph`, `TypedDict`/`BaseModel` schema,
or `Annotated[..., <reducer>]` typing.
- The graph has ≥1 of: parallel branches via static fan-out, `Send` API,
multiple agents writing shared state, or a supervisor pattern where workers return concurrently.
- The run raises `langgraph.errors.InvalidUpdateError` — message looks like
`At key 'messages': Can receive only one value per step. Use an Annotated key to handle multiple values.`
- The user pastes a state schema and asks "why does this crash on parallel?"
or "do I need a reducer here?".
- The user is migrating a linear chain to a fan-out / map-reduce shape.
Do **not** activate if every key is written by exactly one node per superstep (see §6: over-reducing single-writer keys is an anti-pattern).
---
2. 核心心智模型 (Core Mental Model)
**LangGraph state is either single-writer or has a reducer. Nothing in between.**
When a node returns `{"k": v}`, LangGraph must decide how to merge `v` into existing `state["k"]`. There are exactly two legal regimes:
1. **Single-writer / "set" semantics** (default, no `Annotated`). At most one node writes the key per superstep. The new value *replaces* the old. Two concurrent writers → `InvalidUpdateError`. 2. **Reducer / "merge" semantics** (`Annotated[T, reducer_fn]`). Any number of writers may write per superstep; LangGraph folds them via `reducer_fn(current, new)`.
The reducer is *commutative-enough* algebra that lets the engine schedule parallel writes without you reasoning about interleavings. Three canonical reducers cover ~90% of real graphs:
| Reducer | Type | Behaviour | |---|---|---| | `add_messages` (from `langgraph.graph.message`) | `list[BaseMessage]` | Append; **dedupe-and-update by message `id`** (in-place edit when IDs match) | | `operator.add` | `list`, `int`, `float`, `str` | List concat / numeric sum | | Custom `(curr, new) -> merged` | anything | Domain-specific merge (keep-latest, dedupe-by-id, LLM-summarise) |
> "Reducers are mandatory, not optional, for parallel execution." > — `[cheatsheet/gotchas]`
The `add_messages` quirk that beginners miss: it is **not** plain append. If a new message shares an `id` with one in state, it overwrites in place. This is what makes HITL `interrupt() + edit-state` work — a human can edit the last AI turn and resume.
The decision is per-key, not per-schema. A schema can mix freely: one key single-writer, another with `add_messages`, another with `operator.add`.
---
3. SOP 工作流 (Agentic Protocol)
Run top-down for every new or modified state schema.
Step 1 · Enumerate writers per key
For each key `k` in the state schema, list every node that returns `k` in its update dict. Be paranoid: include nodes spawned via `Send`, subgraphs whose output schema overlaps the parent, and any conditional branches.
Step 2 · Classify each key
- **Single-writer-per-superstep** → leave un-annotated. Plain `k: T`.
- **Multi-writer in the same superstep** (parallel branches, fan-out workers,
swarm handoffs) → **must** declare a reducer. Plain `k: T` will crash.
- **Sequential multi-writer** (different supersteps) — a reducer is
*optional*: without one, each later write overwrites; with one, each write folds in. Choose based on intent.
> Decision gate: if you cannot answer "which nodes write this key?" in one > sentence per key, stop and redraw the graph before continuing.
Step 3 · Pick the reducer
Use the OP table in §4. Default ladder:
1. List of `BaseMessage` → `add_messages`. 2. Append-only list of anything else → `operator.add`. 3. Counter / accumulator → `operator.add`. 4. Anything weirder → write a custom reducer (§4 OP-5).
Step 4 · Probe-test the parallel update
Before shipping, write a unit test that invokes the graph along the parallel path with deterministic node outputs and asserts state matches expectation. If the reducer is wrong (e.g., `operator.add` on dicts), this is where you find out — not in production.
def test_parallel_merge():
out = graph.invoke({"items": []})
assert sorted(out["items"]) == ["a", "b"] # both workers' contributions presentStep 5 · Document the contract
In a docstring on the TypedDict / Pydantic model, note for each non-default key *why* it has a reducer. Future maintainers will thank you when they refactor.
---
4. 操作模型 (Operation Models)
Each op: **Trigger → Action → Output**.
OP-1 · Declare `add_messages` for a chat history key
- **Trigger**: state has a `messages: list[BaseMessage]` field; ≥1 of (HITL
edit-state, multi-agent that all append turns, ReAct loop).
- **Action**:
from typing import Annotated, TypedDict
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
class S(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]- **Output**: Concurrent message writes append; same-id writes overwrite
in-place (enables HITL state edits).
OP-2 · Declare `operator.add` for an accumulating list
- **Trigger**: parallel workers each emit zero or more items into a shared
list (e.g., one chunk per worker in a map step).
- **Action**:
Read more
name: agentsop-state-reducer description: | Tool skill for declaring reducers on LangGraph state keys so parallel writes merge instead of crashing. Activates whenever a coder agent designs a StateGraph with parallel branches, fan-out via Send, multi-agent topologies, or whenever a run raises `InvalidUpdateError: At key '<k>': Can receive only one value per step`. Encodes the rule "every state key is single-writer or has a reducer — nothing in between." version: 0.1.0
State Reducer · Tool Skill
> Scope: a single decision — for each state key, declare a reducer or > guarantee single-writer. Out of scope: checkpointers, HITL, supervisor vs. > swarm — see `langgraph-sop` for those.
---
1. 何时激活 (Activation Rules)
Activate when **any** trigger fires:
- The task involves a LangGraph `StateGraph`, `TypedDict`/`BaseModel` schema,
or `Annotated[..., <reducer>]` typing.
- The graph has ≥1 of: parallel branches via static fan-out, `Send` API,
multiple agents writing shared state, or a supervisor pattern where workers return concurrently.
- The run raises `langgraph.errors.InvalidUpdateError` — message looks like
`At key 'messages': Can receive only one value per step. Use an Annotated key to handle multiple values.`
- The user pastes a state schema and asks "why does this crash on parallel?"
or "do I need a reducer here?".
- The user is migrating a linear chain to a fan-out / map-reduce shape.
Do **not** activate if every key is written by exactly one node per superstep (see §6: over-reducing single-writer keys is an anti-pattern).
---
2. 核心心智模型 (Core Mental Model)
**LangGraph state is either single-writer or has a reducer. Nothing in between.**
When a node returns `{"k": v}`, LangGraph must decide how to merge `v` into existing `state["k"]`. There are exactly two legal regimes:
1. **Single-writer / "set" semantics** (default, no `Annotated`). At most one node writes the key per superstep. The new value *replaces* the old. Two concurrent writers → `InvalidUpdateError`. 2. **Reducer / "merge" semantics** (`Annotated[T, reducer_fn]`). Any number of writers may write per superstep; LangGraph folds them via `reducer_fn(current, new)`.
The reducer is *commutative-enough* algebra that lets the engine schedule parallel writes without you reasoning about interleavings. Three canonical reducers cover ~90% of real graphs:
| Reducer | Type | Behaviour | |---|---|---| | `add_messages` (from `langgraph.graph.message`) | `list[BaseMessage]` | Append; **dedupe-and-update by message `id`** (in-place edit when IDs match) | | `operator.add` | `list`, `int`, `float`, `str` | List concat / numeric sum | | Custom `(curr, new) -> merged` | anything | Domain-specific merge (keep-latest, dedupe-by-id, LLM-summarise) |
> "Reducers are mandatory, not optional, for parallel execution." > — `[cheatsheet/gotchas]`
The `add_messages` quirk that beginners miss: it is **not** plain append. If a new message shares an `id` with one in state, it overwrites in place. This is what makes HITL `interrupt() + edit-state` work — a human can edit the last AI turn and resume.
The decision is per-key, not per-schema. A schema can mix freely: one key single-writer, another with `add_messages`, another with `operator.add`.
---
3. SOP 工作流 (Agentic Protocol)
Run top-down for every new or modified state schema.
Step 1 · Enumerate writers per key
For each key `k` in the state schema, list every node that returns `k` in its update dict. Be paranoid: include nodes spawned via `Send`, subgraphs whose output schema overlaps the parent, and any conditional branches.
Step 2 · Classify each key
- **Single-writer-per-superstep** → leave un-annotated. Plain `k: T`.
- **Multi-writer in the same superstep** (parallel branches, fan-out workers,
swarm handoffs) → **must** declare a reducer. Plain `k: T` will crash.
- **Sequential multi-writer** (different supersteps) — a reducer is
*optional*: without one, each later write overwrites; with one, each write folds in. Choose based on intent.
> Decision gate: if you cannot answer "which nodes write this key?" in one > sentence per key, stop and redraw the graph before continuing.
Step 3 · Pick the reducer
Use the OP table in §4. Default ladder:
1. List of `BaseMessage` → `add_messages`. 2. Append-only list of anything else → `operator.add`. 3. Counter / accumulator → `operator.add`. 4. Anything weirder → write a custom reducer (§4 OP-5).
Step 4 · Probe-test the parallel update
Before shipping, write a unit test that invokes the graph along the parallel path with deterministic node outputs and asserts state matches expectation. If the reducer is wrong (e.g., `operator.add` on dicts), this is where you find out — not in production.
def test_parallel_merge():
out = graph.invoke({"items": []})
assert sorted(out["items"]) == ["a", "b"] # both workers' contributions presentStep 5 · Document the contract
In a docstring on the TypedDict / Pydantic model, note for each non-default key *why* it has a reducer. Future maintainers will thank you when they refactor.
---
4. 操作模型 (Operation Models)
Each op: **Trigger → Action → Output**.
OP-1 · Declare `add_messages` for a chat history key
- **Trigger**: state has a `messages: list[BaseMessage]` field; ≥1 of (HITL
edit-state, multi-agent that all append turns, ReAct loop).
- **Action**:
from typing import Annotated, TypedDict
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
class S(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]- **Output**: Concurrent message writes append; same-id writes overwrite
in-place (enables HITL state edits).
OP-2 · Declare `operator.add` for an accumulating list
- **Trigger**: parallel workers each emit zero or more items into a shared
list (e.g., one chunk per worker in a map step).
- **Action**:
Other skills on skillalchemy.
- /LEAP
LEAP — 落地执行引擎。内含两条管线:A 分支蒸馏(从 raw data 提取 skill)、 B 分支融合(多 skill 编织为一个)。被 SkillAlchemy 编排器调用。 Use when 编排器判断需要蒸馏或融合时。
Open skill - /Lens
Lens — 给你的问题加一层认知镜片。输入任意任务描述,输出增强版 description, 发现「你不知道自己不知道」的隐性维度、前置条件和认知路线。 Use when 用户说「帮我想想」「分析一下」「生成 skill」「蒸馏」「融合」 或输入看起来太简单需要展开。
Open skill - /agentsop-agent-topology-selection
Cross-framework enhancement overlay for choosing a multi-agent topology BEFORE writing any agent. A binary-question rubric — is single-agent + tools enough? do agents need to know about each other? does the output need one voice? — maps the answer to single-agent / supervisor /
Open skill - /agentsop-aider
SOP for terminal-based, git-native AI pair programming with Aider (git work-tree + tree-sitter repo-map + edit-format + human-in-loop REPL). Use when editing code in an existing git repo via an LLM, when you need to converge a change to 2-5 files, pick an edit format that fits
Open skill - /agentsop-bio-fraud-forensics
Screens biomedical / life-science papers for signs of data fabrication, image manipulation, and statistical anomalies, using the detection techniques distilled from the field's canonical exposure platforms (PubPeer, Data Colada, Science Integrity Digest, For Better Science) and
Open skill - /agentsop-bounded-loop
Universal discipline for any LM-driven loop — agent retries, plan-act-observe, multi-agent handoffs, optimiser passes, test-fix cycles. Encodes the one rule every framework documents quietly and every team relearns expensively: the LM in the loop is NEVER a reliable terminator.
Open skill

