11 KiB
ReducerRef Structural Capability 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: Move reducer references from ambiguous dotted strings toward structural capability refs while preserving string reducer names as parse-only shorthand.
Architecture: Reducers are source-owned capabilities, not graph paths. ReducerRef should carry a structural CapabilityRef plus config, while old name strings continue to validate at compatibility boundaries. Runtime reducer lookup can keep using display names temporarily through a compatibility property; artifact dependency extraction should stop reparsing dotted reducer names manually.
Tech Stack: Python 3.14, Pydantic v2, wf_platform.refs.CapabilityRef, wf_core.models.reducers.ReducerRef, wf_artifacts.factory, pytest, basedpyright, ruff.
Current State
src/wf_core/models/reducers.py:
class ReducerRef(BaseModel):
name: str
config: dict[str, Any] = Field(default_factory=dict)
src/wf_artifacts/factory.py extracts reducer dependencies by reparsing the display name:
reducer_ref = CapabilityRef.parse(reducer.name)
requirements[reducer.name] = RequiredCapability(ref=reducer_ref, kind="reducer")
This is the same separator problem in another domain. wf.std.add is a capability ref, not a graph path.
Canonical Shape
New canonical reducer ref:
{
"ref": { "source": "wf.std", "capability_key": "add" },
"config": {}
}
Compatibility inputs:
"wf.std.add"
{ "name": "wf.std.add", "config": { "modulus": 10 } }
For now, ReducerRef.name remains available as a display/registry key compatibility property. Runtime reducer registries are still keyed by strings such as wf.std.add.
File Structure
-
Modify:
src/wf_core/models/reducers.py- Add
ref: CapabilityRef - Keep
nameas computed/display compatibility property - Parse old string and old
nameobject shapes - Dump canonical
refshape in JSON/Python model dumps
- Add
-
Modify:
src/wf_artifacts/factory.py- Use
reducer.reffor required capabilities - Keep dependency key as
reducer.namefor now
- Use
-
Modify tests:
tests/core/test_nested_state_paths.pytests/core/test_schema_validation.pytests/artifacts/test_factory.pytests/artifacts/test_validation.pyif needed
-
Modify docs:
docs/structural_refs.mddocs/core_state_mapping_and_merge.md
Task 1: Pin ReducerRef Compatibility and Canonical Dump
Files:
-
Modify:
tests/core/test_nested_state_paths.py -
Step 1: Add reducer ref tests
Add:
from wf_platform import CapabilityRef
def test_reducer_ref_accepts_string_shorthand_and_dumps_structural_ref() -> None:
reducer = ReducerRef.model_validate("wf.std.add")
assert reducer.ref == CapabilityRef(source="wf.std", capability_key="add")
assert reducer.name == "wf.std.add"
assert reducer.model_dump(mode="json") == {
"ref": {"source": "wf.std", "capability_key": "add"},
"config": {},
}
def test_reducer_ref_accepts_legacy_name_object_with_config() -> None:
reducer = ReducerRef.model_validate({
"name": "wf.std.modulo_add",
"config": {"modulus": 10},
})
assert reducer.ref == CapabilityRef(source="wf.std", capability_key="modulo_add")
assert reducer.name == "wf.std.modulo_add"
assert reducer.config == {"modulus": 10}
def test_reducer_ref_accepts_canonical_ref_object() -> None:
reducer = ReducerRef.model_validate({
"ref": {"source": "wf.std", "capability_key": "append"},
})
assert reducer.name == "wf.std.append"
- Step 2: Run focused tests to verify red
uv run --with pytest pytest tests/core/test_nested_state_paths.py::test_reducer_ref_accepts_string_shorthand_and_dumps_structural_ref tests/core/test_nested_state_paths.py::test_reducer_ref_accepts_legacy_name_object_with_config tests/core/test_nested_state_paths.py::test_reducer_ref_accepts_canonical_ref_object -q
Expected: fail because ReducerRef does not parse strings and has no ref.
Task 2: Implement Structural ReducerRef
Files:
-
Modify:
src/wf_core/models/reducers.py -
Step 1: Update imports
Add:
from collections.abc import Mapping
from pydantic import computed_field, model_validator
from wf_platform import CapabilityRef
- Step 2: Change model fields
Change ReducerRef to:
class ReducerRef(BaseModel):
"""Reference to one reducer capability plus JSON-compatible configuration."""
ref: CapabilityRef
config: dict[str, Any] = Field(default_factory=dict)
- Step 3: Add compatibility validator
Add:
@model_validator(mode="before")
@classmethod
def _coerce_legacy_shapes(cls, value: object) -> object:
if isinstance(value, str):
return {"ref": CapabilityRef.parse(value)}
if not isinstance(value, Mapping):
return value
data = dict(value)
if "ref" not in data and "name" in data:
data["ref"] = CapabilityRef.parse(str(data.pop("name")))
return data
Do not parse arbitrary dotted strings anywhere else.
- Step 4: Add name compatibility property
Add:
@computed_field
@property
def name(self) -> str:
"""Display/registry compatibility key for existing reducer catalogs."""
return str(self.ref)
If CapabilityRef.__str__ does not produce source.capability_key, use its display helper or add one there.
- Step 5: Run reducer ref tests
uv run --with pytest pytest tests/core/test_nested_state_paths.py::test_reducer_ref_accepts_string_shorthand_and_dumps_structural_ref tests/core/test_nested_state_paths.py::test_reducer_ref_accepts_legacy_name_object_with_config tests/core/test_nested_state_paths.py::test_reducer_ref_accepts_canonical_ref_object -q
Expected: pass.
Task 3: Update Reducer Field Serializers and Existing Expectations
Files:
-
Modify:
src/wf_core/models/schemas.py -
Modify tests that assert reducer dumps
-
Step 1: Inspect current reducer dump helper
Current helper:
def _dump_reducer_keyword(reducer: ReducerRef) -> str | dict[str, Any]:
if not reducer.config:
return reducer.name
return reducer.model_dump(mode="json")
Decide canonical output:
- For no-config reducers, keep string shorthand in JSON Schema
reducerkeyword for readability. - For configured reducers, dump canonical object:
{
"ref": { "source": "wf.std", "capability_key": "modulo_add" },
"config": { "modulus": 10 }
}
This keeps common schema compact while avoiding string parsing for config objects.
- Step 2: Update helper
Use:
def _dump_reducer_keyword(reducer: ReducerRef) -> str | dict[str, Any]:
if not reducer.config:
return reducer.name
return reducer.model_dump(mode="json")
This may already work after ReducerRef.model_dump() changes. Keep the helper but update tests.
- Step 3: Run schema tests
uv run --with pytest pytest tests/core/test_schema_validation.py tests/core/test_nested_state_paths.py -q
Expected: pass after updating expectations for configured reducer dumps if needed.
Task 4: Update Artifact Reducer Dependency Extraction
Files:
-
Modify:
src/wf_artifacts/factory.py -
Modify:
tests/artifacts/test_factory.py -
Step 1: Add/adjust artifact test
In tests/artifacts/test_factory.py, ensure reducer dependencies assert structural refs:
def test_create_workflow_artifact_from_plan_adds_reducer_dependencies() -> None:
...
reducer = artifact.required_capability_map()["wf.std.max"]
assert reducer.ref.source == "wf.std"
assert reducer.ref.capability_key == "max"
assert reducer.logical_source == "wf.std"
assert reducer.capability_name == "max"
assert reducer.kind == "reducer"
Add a configured reducer payload test:
def test_create_workflow_artifact_from_plan_accepts_structural_reducer_ref() -> None:
plan = minimal_plan()
plan["state_schema"] = {
"type": "object",
"properties": {
"score": {
"type": "integer",
"reducer": {
"ref": {"source": "wf.std", "capability_key": "max"},
"config": {},
},
}
},
}
artifact = create_workflow_artifact_from_plan(...)
assert "wf.std.max" in artifact.required_capability_map()
- Step 2: Update extraction
Change:
reducer_ref = CapabilityRef.parse(reducer.name)
requirements[reducer.name] = RequiredCapability(ref=reducer_ref, kind="reducer")
to:
requirements[reducer.name] = RequiredCapability(ref=reducer.ref, kind="reducer")
- Step 3: Run artifact tests
uv run --with pytest pytest tests/artifacts/test_factory.py tests/artifacts/test_validation.py -q
Expected: pass.
Task 5: Runtime Compatibility Check
Files:
-
Tests only unless failures require runtime changes
-
Step 1: Run reducer runtime tests
uv run --with pytest pytest tests/core/test_nested_state_paths.py tests/core/test_atomic_state_patches.py -q
Expected: pass because reducer.name remains a compatibility registry key.
- Step 2: If runtime fails
Only if needed, update lookup code to use reducer.name as the compatibility string key. Do not make runtime registries structural in this pass.
Task 6: Docs
Files:
-
Modify:
docs/structural_refs.md -
Modify:
docs/core_state_mapping_and_merge.md -
Step 1: Update reducer docs
In docs/structural_refs.md, replace temporary wording with:
Canonical configured reducer refs use `ref`:
{
"ref": {"source": "wf.std", "capability_key": "modulo_add"},
"config": {"modulus": 10}
}
String reducer names such as `wf.std.add` remain shorthand for unconfigured
reducers and compatibility display.
- Step 2: Update merge docs
In docs/core_state_mapping_and_merge.md, update examples with both compact and configured forms:
"reducer": "wf.std.add"
and:
"reducer": {
"ref": {"source": "wf.std", "capability_key": "modulo_add"},
"config": {"modulus": 10}
}
Task 7: Verification
- Step 1: Focused tests
uv run --with pytest pytest tests/core/test_nested_state_paths.py tests/core/test_schema_validation.py tests/artifacts/test_factory.py tests/artifacts/test_validation.py -q
- Step 2: Full tests
uv run --with pytest pytest -q
- Step 3: Static checks
uvx ruff check src/wf_core src/wf_artifacts tests/core tests/artifacts
uvx ruff format --check src/wf_core src/wf_artifacts tests/core tests/artifacts
uv run basedpyright --level error src/wf_core src/wf_artifacts tests/core tests/artifacts
Expected:
- tests pass
- ruff passes
- basedpyright reports
0 errors
Self-Review Checklist
ReducerRefcanonical shape hasref, not onlyname.- String shorthand still parses.
- Legacy
{name, config}still parses. - Runtime reducer lookup still works through
reducer.name. - Artifact dependency extraction uses
reducer.ref. - No graph path parser is used for reducer refs.