reducer replaces merge_strategy.
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user