reducer replaces merge_strategy.

This commit is contained in:
lda
2026-05-17 16:23:51 +07:00 Verified
parent c5b43c7dbe
commit 86ffb17ec6
14 changed files with 348 additions and 91 deletions
@@ -0,0 +1,78 @@
# Reducer Capabilities Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace `merge_strategy` with named pure reducer references across core and authoring, using `wf.std.replace` as the default reducer.
**Architecture:** Introduce a small reducer registry in `wf_core`, register the current three built-ins as reducers, and have state writes resolve every declared reducer name through that registry. Keep undeclared state paths using default replace semantics. Migrate `wf_authoring.state_field()` and all state metadata/tests/docs to reducer names in the same pass so there is one merge concept in the codebase.
**Tech Stack:** Python, Pydantic, pytest, existing `wf_core` runtime and `wf_authoring` schema projection.
---
## File Structure
- Modify `src/wf_core/models/schemas.py`
- replace `merge_strategy` with `reducer`
- Replace/refactor `src/wf_core/runtime/ops/merges.py`
- reducer callable type
- built-in reducer functions
- default reducer registry
- reducer application helper
- Modify `src/wf_core/runtime/ops/state.py`
- resolve reducer names from state fields
- use default replace reducer for undeclared paths
- Modify `src/wf_authoring/schemas.py`
- expose `state_field(reducer=...)`
- project reducer metadata through flattened state paths
- Modify tests under `tests/core/`, `tests/authoring/`, and `tests/rewrite/`
- migrate old metadata
- add unknown reducer coverage
- Update docs mentioning `merge_strategy`
## Tasks
### Task 1: Pin Reducer Semantics
- [ ] Add tests proving:
- `StateField(type="string")` defaults to `wf.std.replace`
- `wf.std.append` preserves append behavior
- `wf.std.merge_object` preserves shallow object merge behavior
- unknown reducer names fail clearly
- exact nested state paths still use their own reducer
- [ ] Run focused core tests and confirm failure before implementation.
### Task 2: Replace Core Merge Strategy With Reducers
- [ ] Replace `merge_strategy` on `StateField` with `reducer`.
- [ ] Add reducer functions for `wf.std.replace`, `wf.std.append`, and `wf.std.merge_object`.
- [ ] Add a registry lookup path that raises for unknown reducer names.
- [ ] Update `write_state_value()` to resolve declared reducers and use `wf.std.replace` for undeclared paths.
- [ ] Run focused core tests and confirm reducer behavior is green.
### Task 3: Migrate Authoring
- [ ] Change `StateFieldMetadata` and `state_field()` to use `reducer`.
- [ ] Preserve nested metadata projection under reducer names.
- [ ] Update authoring/rewrite fixtures from `merge_strategy=` to `reducer=`.
- [ ] Run focused authoring tests and confirm they pass.
### Task 4: Update Docs
- [ ] Replace docs that describe `merge_strategy` with reducer terminology.
- [ ] Update examples to show reducer names, including the default replace reducer.
- [ ] Keep the design point that reducers are pure and source-owned.
### Task 5: Verify
- [ ] Run `uv run --with pytest pytest tests/core tests/authoring tests/rewrite -q`
- [ ] Run `uv run --with pytest pytest -q`
- [ ] Run `uv run basedpyright --level error`
## Non-Goals
- custom user-authored reducer registration through MCP/platform sources
- reducer parameters/configuration
- async reducers
- parallel foreach
- compatibility shims for `merge_strategy`
@@ -0,0 +1,109 @@
# Reducer Capabilities Design
## Goal
Make state merging a single capability system instead of keeping a built-in
`merge_strategy` path beside future custom reducers.
## Decision
`StateField` should reference exactly one reducer:
```python
class StateField(BaseModel):
type: str
reducer: str = "wf.std.replace"
trace: bool = True
default: Any = None
```
The current built-ins become the first reducer library:
- `wf.std.replace`
- `wf.std.append`
- `wf.std.merge_object`
There is no separate `merge_strategy` field after this migration.
## Reducer Contract
Reducers are pure merge functions:
```text
current_value, incoming_value -> merged_value
```
They do not receive node ids, frame ids, paths, timestamps, or other execution
context. If behavior needs workflow context, it belongs in nodes or graph
structure instead.
Reducers are named and resolved at runtime from a registry. Workflow artifacts
store the reducer name, not a Python callable.
## Runtime Model
`wf_core` owns:
- a reducer callable protocol/type
- a reducer registry
- default registration of the three built-ins
- lookup and execution during state writes
Missing reducer names are execution errors. Reducer failures are wrapped with
the destination path so the failing state write is obvious.
## Authoring Model
`wf_authoring.state_field()` changes from:
```python
state_field(merge_strategy="append")
```
to:
```python
state_field(reducer="wf.std.append")
```
Nested authored state projection continues to flatten exact state paths and now
copies reducer references onto those flattened fields.
## Why Reducer-Only
Keeping both `merge_strategy` and `reducer` would create two concepts for the
same job. Turning the current built-ins into reducers gives us:
- one merge abstraction
- source-owned reusable behavior
- inspectable future reducer libraries
- a direct path to custom reducers such as `wf.std.max`,
`wf.std.set_union`, or user-authored reducers
## Error Handling
- unknown reducer name: execution error before the state write commits
- reducer rejects a value shape: execution error from that reducer
- reducers remain pure, so there is no side-effect rollback problem
## Compatibility
This is an intentional model migration:
- core `StateField.merge_strategy` is removed
- authoring `state_field(merge_strategy=...)` is removed
- docs and tests migrate to reducer references
The project is still early enough that keeping both public shapes would create
more confusion than value.
## Testing
Tests should prove:
- `wf.std.replace` preserves current replace behavior
- `wf.std.append` preserves current append behavior
- `wf.std.merge_object` preserves current shallow object merge behavior
- exact nested state paths still select their own reducer
- unknown reducers fail clearly
- authoring metadata projects reducer names through nested state schemas