reducer path to strengthen too

This commit is contained in:
lda
2026-05-21 04:19:50 +07:00 Verified
parent 444b5735c1
commit d089e28c15
9 changed files with 201 additions and 38 deletions
+7 -4
View File
@@ -249,14 +249,17 @@ Reducers are a capability family, similar to reusable node specs:
- dependency-trackable
State fields reference reducers declaratively. String reducer names are accepted
as shorthand for unconfigured reducers; configured reducers use a `name` plus
JSON-compatible `config`. Workflow artifacts do not embed arbitrary Python
callables.
as shorthand for unconfigured reducers; configured reducers use a structural
`ref` plus JSON-compatible `config`. Workflow artifacts do not embed arbitrary
Python callables.
```python
StateField(
type="integer",
reducer={"name": "wf.std.modulo_add", "config": {"modulus": 10}},
reducer={
"ref": {"source": "wf.std", "capability_key": "modulo_add"},
"config": {"modulus": 10},
},
)
```
+6 -6
View File
@@ -159,16 +159,16 @@ being split again.
Reducer refs are capability refs, not graph paths. `wf.std.add` is shorthand for
source `wf.std` and capability key `add`.
The reducer cleanup should move `ReducerRef` toward structural `CapabilityRef`
while keeping string reducer names as parse-only shorthand. Reducer config stays
part of the reducer reference payload:
Configured reducer refs now use a structural `CapabilityRef` while keeping
string reducer names as parse-only shorthand and display keys. Reducer config
stays part of the reducer reference payload:
```json
{
"name": "wf.std.modulo_add",
"ref": {"source": "wf.std", "capability_key": "modulo_add"},
"config": {"modulus": 10}
}
```
That future cleanup must not reuse graph path parsing rules. Reducer names live
in the capability/source domain.
That shape must not reuse graph path parsing rules. Reducer names live in the
capability/source domain.
+2 -1
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from wf_authoring import WorkflowBuilder
from wf_authoring.dsl import PathExpr
from wf_core import JoinNode, Workflow
from wf_core.paths import GraphSourcePath
from .models import (
DraftChooseStep,
@@ -81,7 +82,7 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
).entry
if isinstance(step, DraftMatchStep):
return builder.match(
PathExpr(step.match.value),
PathExpr(GraphSourcePath.parse(step.match.value)),
{case.equals: case.then for case in step.match.cases},
id=step_id,
default=step.match.default,
+2 -6
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from collections.abc import Mapping
from wf_core import ReducerRef, Workflow
from wf_platform import CapabilityRef, NodeSpecInventory
from wf_platform import NodeSpecInventory
from .models import ArtifactKind, JsonObject, RequiredCapability, WorkflowArtifact
from .references import normalize_plan_node_refs
@@ -99,12 +99,8 @@ def _required_reducers_from_plan(plan: JsonObject) -> dict[str, RequiredCapabili
reducer = ReducerRef.model_validate(reducer_payload)
except ValueError:
continue
try:
reducer_ref = CapabilityRef.parse(reducer.name)
except ValueError:
continue
requirements[reducer.name] = RequiredCapability(
ref=reducer_ref,
ref=reducer.ref,
kind="reducer",
)
return requirements
+46 -3
View File
@@ -1,16 +1,59 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, model_validator
from wf_platform.refs import CapabilityRef
class ReducerRef(BaseModel):
"""Reference to one reducer plus JSON-compatible configuration."""
"""Reference to one reducer capability plus JSON-compatible configuration."""
name: str
ref: CapabilityRef
config: dict[str, Any] = Field(default_factory=dict)
def __init__(
self,
*,
ref: CapabilityRef | str | Mapping[str, Any] | None = None,
name: str | None = None,
config: dict[str, Any] | None = None,
**extra: object,
) -> None:
"""Accept legacy `name=` construction while storing canonical `ref`.
Existing authoring and runtime code still constructs reducers with
`ReducerRef(name="wf.std.add")`. That remains source-compatible, but
the model state is now the structural capability reference.
"""
payload: dict[str, object] = {"config": config or {}}
if ref is not None:
payload["ref"] = ref
if name is not None:
payload["name"] = name
payload.update(extra)
super().__init__(**payload)
@model_validator(mode="before")
@classmethod
def _coerce_legacy_shapes(cls, value: object) -> object:
"""Accept old reducer strings/`name` objects as parse-only shorthand."""
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
@property
def name(self) -> str:
"""Display/registry compatibility key for existing reducer catalogs."""
return str(self.ref)
class ReducerSpec(BaseModel):
"""Inspectable metadata for one named pure state reducer."""
+45 -1
View File
@@ -1,7 +1,15 @@
from .docs import DocumentationPrompt, DocumentationResource, build_documentation_source
from typing import TYPE_CHECKING
from .refs import CapabilityRef, SourceRef
from .paging import Page, page_items
from .schema_hashes import hash_json_schema
if TYPE_CHECKING:
from .docs import (
DocumentationPrompt,
DocumentationResource,
build_documentation_source,
)
from .sources import (
CapabilityBuckets,
CapabilitySource,
@@ -17,6 +25,24 @@ from .sources import (
SourceVisibilitySnapshot,
)
_LAZY_EXPORTS = {
"CapabilityBuckets": ".sources",
"CapabilitySource": ".sources",
"DocumentationPrompt": ".docs",
"DocumentationResource": ".docs",
"NodeSpecInventory": ".sources",
"ReducerInventory": ".sources",
"SourceCapabilityInventory": ".sources",
"SourceInventory": ".sources",
"SourceKind": ".sources",
"SourcePermissions": ".sources",
"SourcePermissionsSnapshot": ".sources",
"SourceStatus": ".sources",
"SourceVisibility": ".sources",
"SourceVisibilitySnapshot": ".sources",
"build_documentation_source": ".docs",
}
__all__ = [
"CapabilityBuckets",
"CapabilitySource",
@@ -39,3 +65,21 @@ __all__ = [
"hash_json_schema",
"page_items",
]
def __getattr__(name: str) -> object:
"""Load platform inventory exports lazily to keep core-safe refs importable.
`wf_core` depends on `wf_platform.refs` for structural reducer references.
Eagerly importing source inventory here would pull in authoring/core again
and create an import cycle, so only foundational refs/helpers are eager.
"""
import importlib
module_name = _LAZY_EXPORTS.get(name)
if module_name is None:
raise AttributeError(f"module 'wf_platform' has no attribute {name!r}")
module = importlib.import_module(module_name, __name__)
value = getattr(module, name)
globals()[name] = value
return value
+9 -3
View File
@@ -46,6 +46,12 @@ class SourceRef:
return value
if isinstance(value, str):
return cls.parse(value)
if isinstance(value, Mapping):
parts = value.get("parts")
if isinstance(parts, list | tuple) and all(
isinstance(part, str) for part in parts
):
return cls(tuple(parts))
raise TypeError("source ref must be a string")
@@ -101,9 +107,9 @@ class CapabilityRef:
return cls.parse(value)
if isinstance(value, dict):
source = value.get("source")
name = value.get("capability_key")
if isinstance(source, str) and isinstance(name, str):
return cls(source=SourceRef.parse(source), name=name)
name = value.get("capability_key", value.get("name"))
if isinstance(name, str):
return cls(source=SourceRef._validate(source), name=name)
raise TypeError(
"capability ref must be a string or {'source': str, 'capability_key': str}"
)
+31
View File
@@ -50,6 +50,37 @@ def test_create_workflow_artifact_from_plan_adds_reducer_dependencies() -> None:
outcomes=("done",),
)
reducer = artifact.required_capability_map()["wf.std.max"]
assert str(reducer.capability_ref().source) == "wf.std"
assert reducer.capability_ref().name == "max"
assert reducer.logical_source == "wf.std"
assert reducer.capability_name == "max"
assert reducer.kind == "reducer"
def test_create_workflow_artifact_from_plan_accepts_structural_reducer_ref() -> None:
plan = _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(
artifact_id="score",
version=1,
title="Score",
plan=plan,
outcomes=("done",),
)
reducer = artifact.required_capability_map()["wf.std.max"]
assert reducer.logical_source == "wf.std"
assert reducer.capability_name == "max"
+39
View File
@@ -13,6 +13,7 @@ from wf_core.paths import StatePath
from wf_core.runtime.ops.merges import ReducerDefinition, apply_reducer
from wf_core.runtime.ops.runs import create_run_state
from wf_core.runtime.ops.state import write_state_value
from wf_platform import CapabilityRef
def test_exact_nested_state_path_uses_declared_reducer() -> None:
@@ -313,6 +314,44 @@ def test_state_field_accepts_configured_reducer_reference() -> None:
# PYLINT!!!! what is u on ts is so clear
assert field.reducer.name == "wf.std.max" # pylint: disable=no-member
assert field.reducer.config == {"sample": True} # pylint: disable=no-member
assert field.model_dump(mode="json")["reducer"] == {
"ref": {"source": "wf.std", "capability_key": "max"},
"config": {"sample": True},
}
def test_reducer_ref_accepts_string_shorthand_and_dumps_structural_ref() -> None:
reducer = ReducerRef.model_validate("wf.std.add")
assert reducer.ref == CapabilityRef.parse("wf.std.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.parse("wf.std.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"
def test_unknown_state_reducer_fails_clearly() -> None: