reducerspec

This commit is contained in:
lda
2026-05-17 16:35:17 +07:00 Verified
parent 86ffb17ec6
commit 281229a78f
9 changed files with 130 additions and 3 deletions
@@ -0,0 +1,64 @@
# Reducer Source Inventory 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:** Represent reducers as first-class source-owned capabilities and expose the built-in reducer catalog through existing source inventory.
**Architecture:** Add a small immutable `ReducerSpec` model in `wf_core`, keep runtime reducer execution where it is, and extend `wf_mcp` capability buckets with reducer ownership metadata. Register the three built-in reducer specs under `wf.std` so inventory becomes honest without adding reducer-authoring UX yet.
**Tech Stack:** Python, dataclasses/Pydantic, pytest, existing capability source inventory.
---
## File Structure
- Create `src/wf_core/models/reducers.py`
- reducer capability metadata
- Modify `src/wf_core/models/__init__.py` and `src/wf_core/__init__.py`
- export `ReducerSpec`
- Modify `src/wf_mcp/broker/service/capability_sources.py`
- add reducer bucket, counts, and inventory listing
- Modify `src/wf_mcp/broker/service/builtins.py`
- define/register built-in reducer specs under `wf.std`
- Modify `tests/wf_mcp/test_service.py`
- assert reducer inventory
- Update `docs/wf_mcp_capability_sources.md`
- document reducers under `wf.std`
- Update `docs/workflow_capabilities.md`
- name reducers as workflow-facing capabilities
## Tasks
### Task 1: Pin Inventory Behavior
- [ ] Add failing tests proving:
- `wf.std` owns built-in reducers
- source status exposes `reducer_count`
- source inventory exposes reducer names
- [ ] Run focused service tests and confirm failure before implementation.
### Task 2: Add ReducerSpec and Source Buckets
- [ ] Add `ReducerSpec` with `name`, `description`, and optional value-shape notes.
- [ ] Export `ReducerSpec` from core.
- [ ] Extend `CapabilityBuckets`, `as_status()`, and `as_inventory()` with reducers.
- [ ] Register `wf.std.replace`, `wf.std.append`, and `wf.std.merge_object` in built-ins.
- [ ] Run focused service tests and confirm they pass.
### Task 3: Update Docs
- [ ] Add reducers to the source vocabulary docs.
- [ ] Clarify that reducers are selected from sources; LLMs are not expected to author reducer code.
### Task 4: Verify
- [ ] Run `uv run --with pytest pytest tests/wf_mcp -q`
- [ ] Run `uv run --with pytest pytest -q`
- [ ] Run `uv run basedpyright --level error`
## Non-Goals
- reducer decorators
- reducer runtime dependency resolution from external sources
- reducer MCP tools
- non-built-in reducer libraries
+3
View File
@@ -30,6 +30,7 @@ CapabilitySource
capabilities: capabilities:
tools tools
node_specs node_specs
reducers
prompts prompts
resources resources
``` ```
@@ -60,6 +61,7 @@ Expected capabilities:
`wf.std.truthy`, `wf.std.first_item`, `wf.std.first_item_maybe`, `wf.std.truthy`, `wf.std.first_item`, `wf.std.first_item_maybe`,
`wf.std.first_item_or_none`, `wf.std.last_item`, `wf.std.last_item_or_none`, `wf.std.first_item_or_none`, `wf.std.last_item`, `wf.std.last_item_or_none`,
`wf.std.length`, `wf.std.is_empty`. `wf.std.length`, `wf.std.is_empty`.
- `reducers`: `wf.std.replace`, `wf.std.append`, `wf.std.merge_object`.
- `prompts`: workflow authoring guide, error-handling guide, mapping guide. - `prompts`: workflow authoring guide, error-handling guide, mapping guide.
- `resources`: reference docs for stdlib node behavior. - `resources`: reference docs for stdlib node behavior.
@@ -215,6 +217,7 @@ Current code has several useful pieces but the boundaries are blurred.
| Current location | Current role | Target source | | Current location | Current role | Target source |
| --- | --- | --- | | --- | --- | --- |
| `wf_authoring.ops` | reusable workflow node specs | `wf.std.node_specs` | | `wf_authoring.ops` | reusable workflow node specs | `wf.std.node_specs` |
| `wf_core` built-in reducers | reusable workflow state reducers | `wf.std.reducers` |
| `wf_mcp.broker.service.builtins` | local workflow specs | `wf.std`, `wf.mcp` | | `wf_mcp.broker.service.builtins` | local workflow specs | `wf.std`, `wf.mcp` |
| `wf_mcp.broker.tools` | compatibility wrapper over shared service-admin registration | `wf.admin.tools` | | `wf_mcp.broker.tools` | compatibility wrapper over shared service-admin registration | `wf.admin.tools` |
| `wf_mcp.admin_surface.tools` | shared service-backed admin tool registration | `wf.admin.tools` | | `wf_mcp.admin_surface.tools` | shared service-backed admin tool registration | `wf.admin.tools` |
+4 -2
View File
@@ -21,7 +21,8 @@ A source is a named owner of capabilities.
Examples: Examples:
- `everything.default`: an upstream MCP connection source - `everything.default`: an upstream MCP connection source
- `wf.std`: the local workflow standard library - `wf.std`: the local workflow standard library, including reusable nodes and
reducers
- `wf.mcp`: local workflow helpers for interacting with MCP backends - `wf.mcp`: local workflow helpers for interacting with MCP backends
- `wf.admin`: privileged control-plane capabilities - `wf.admin`: privileged control-plane capabilities
@@ -237,6 +238,7 @@ Sources own capability kinds:
```text ```text
tools tools
node_specs node_specs
reducers
prompts prompts
resources resources
``` ```
@@ -262,7 +264,7 @@ Today:
- `CapabilitySource` already owns buckets for tools, node specs, prompts, and - `CapabilitySource` already owns buckets for tools, node specs, prompts, and
resources resources
- connection sources represent upstream MCP snapshots - connection sources represent upstream MCP snapshots
- `wf.std` owns local reusable workflow node specs - `wf.std` owns local reusable workflow node specs and reducers
- `wf.mcp` owns workflow-facing MCP runtime helpers - `wf.mcp` owns workflow-facing MCP runtime helpers
- discovered upstream tools can already become workflow node specs - discovered upstream tools can already become workflow node specs
- saved artifacts can be tagged with `kind="workflow"` or `kind="wrapper"` - saved artifacts can be tagged with `kind="workflow"` or `kind="wrapper"`
+2
View File
@@ -7,6 +7,7 @@ from .models import (
NodeDef, NodeDef,
NodeResult, NodeResult,
NodeUse, NodeUse,
ReducerSpec,
SchemaRef, SchemaRef,
StateField, StateField,
StateSchema, StateSchema,
@@ -51,6 +52,7 @@ __all__ = [
"NodeDef", "NodeDef",
"NodeResult", "NodeResult",
"NodeUse", "NodeUse",
"ReducerSpec",
"SchemaRef", "SchemaRef",
"StateField", "StateField",
"StateSchema", "StateSchema",
+2
View File
@@ -9,6 +9,7 @@ from wf_core.models.conditions import (
VariadicCondition, VariadicCondition,
) )
from wf_core.models.results import NodeResult from wf_core.models.results import NodeResult
from wf_core.models.reducers import ReducerSpec
from wf_core.models.schemas import NodeDef, SchemaRef, StateField, StateSchema from wf_core.models.schemas import NodeDef, SchemaRef, StateField, StateSchema
from wf_core.models.steps import ( from wf_core.models.steps import (
ConditionNode, ConditionNode,
@@ -32,6 +33,7 @@ __all__ = [
"LiteralOperand", "LiteralOperand",
"NodeDef", "NodeDef",
"NodeResult", "NodeResult",
"ReducerSpec",
"NodeUse", "NodeUse",
"NotCondition", "NotCondition",
"Operand", "Operand",
+10
View File
@@ -0,0 +1,10 @@
from __future__ import annotations
from pydantic import BaseModel
class ReducerSpec(BaseModel):
"""Inspectable metadata for one named pure state reducer."""
name: str
description: str | None = None
+24 -1
View File
@@ -4,6 +4,7 @@ from typing import Any, Protocol
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from wf_core import ReducerSpec
from wf_authoring import ( from wf_authoring import (
NodeReturn, NodeReturn,
NodeSpec, NodeSpec,
@@ -109,6 +110,25 @@ def builtin_specs() -> dict[str, NodeSpec[Any, Any]]:
return {spec.name: spec for spec in qualified_specs} return {spec.name: spec for spec in qualified_specs}
def builtin_reducers() -> dict[str, ReducerSpec]:
"""Return built-in reducers owned by the workflow standard library."""
specs = (
ReducerSpec(
name="wf.std.replace",
description="Replace the current state value with the incoming value.",
),
ReducerSpec(
name="wf.std.append",
description="Append one value or many values into a list-valued state path.",
),
ReducerSpec(
name="wf.std.merge_object",
description="Shallow-merge object values at one exact state path.",
),
)
return {spec.name: spec for spec in specs}
def mcp_specs(service: ToolCaller) -> dict[str, NodeSpec[Any, Any]]: def mcp_specs(service: ToolCaller) -> dict[str, NodeSpec[Any, Any]]:
"""Return service-bound MCP utility specs available to raw plans.""" """Return service-bound MCP utility specs available to raw plans."""
@@ -138,7 +158,10 @@ def builtin_sources(service: ToolCaller) -> dict[str, CapabilitySource]:
BUILTIN_CONNECTION_ID: CapabilitySource( BUILTIN_CONNECTION_ID: CapabilitySource(
id=BUILTIN_CONNECTION_ID, id=BUILTIN_CONNECTION_ID,
kind="system", kind="system",
capabilities=CapabilityBuckets(node_specs=builtin_specs()), capabilities=CapabilityBuckets(
node_specs=builtin_specs(),
reducers=builtin_reducers(),
),
visibility=SourceVisibility( visibility=SourceVisibility(
planner=True, planner=True,
mcp_client=True, mcp_client=True,
@@ -4,6 +4,7 @@ from dataclasses import dataclass, field
from typing import Any, Literal from typing import Any, Literal
from wf_authoring import NodeSpec from wf_authoring import NodeSpec
from wf_core import ReducerSpec
SourceKind = Literal["system", "connection"] SourceKind = Literal["system", "connection"]
@@ -27,6 +28,7 @@ class SourcePermissions:
class CapabilityBuckets: class CapabilityBuckets:
tools: dict[str, Any] = field(default_factory=dict) tools: dict[str, Any] = field(default_factory=dict)
node_specs: dict[str, NodeSpec[Any, Any]] = field(default_factory=dict) node_specs: dict[str, NodeSpec[Any, Any]] = field(default_factory=dict)
reducers: dict[str, ReducerSpec] = field(default_factory=dict)
prompts: dict[str, Any] = field(default_factory=dict) prompts: dict[str, Any] = field(default_factory=dict)
resources: dict[str, Any] = field(default_factory=dict) resources: dict[str, Any] = field(default_factory=dict)
@@ -60,6 +62,7 @@ class CapabilitySource:
"description": self.description, "description": self.description,
"tool_count": len(self.capabilities.tools), "tool_count": len(self.capabilities.tools),
"node_spec_count": len(self.capabilities.node_specs), "node_spec_count": len(self.capabilities.node_specs),
"reducer_count": len(self.capabilities.reducers),
"prompt_count": len(self.capabilities.prompts), "prompt_count": len(self.capabilities.prompts),
"resource_count": len(self.capabilities.resources), "resource_count": len(self.capabilities.resources),
} }
@@ -71,6 +74,7 @@ class CapabilitySource:
"capabilities": { "capabilities": {
"tools": sorted(self.capabilities.tools), "tools": sorted(self.capabilities.tools),
"node_specs": sorted(self.capabilities.node_specs), "node_specs": sorted(self.capabilities.node_specs),
"reducers": sorted(self.capabilities.reducers),
"prompts": sorted(self.capabilities.prompts), "prompts": sorted(self.capabilities.prompts),
"resources": sorted(self.capabilities.resources), "resources": sorted(self.capabilities.resources),
}, },
+17
View File
@@ -111,7 +111,13 @@ def test_service_lists_all_capability_sources_with_owned_capability_names() -> N
std_source = sources_by_id["wf.std"] std_source = sources_by_id["wf.std"]
assert "wf.std.runtime_error" in std_source["capabilities"]["node_specs"] assert "wf.std.runtime_error" in std_source["capabilities"]["node_specs"]
assert std_source["capabilities"]["reducers"] == [
"wf.std.append",
"wf.std.merge_object",
"wf.std.replace",
]
assert std_source["capabilities"]["tools"] == [] assert std_source["capabilities"]["tools"] == []
assert std_source["reducer_count"] == 3
mcp_source = sources_by_id["wf.mcp"] mcp_source = sources_by_id["wf.mcp"]
assert mcp_source["capabilities"]["node_specs"] == ["wf.mcp.call_tool"] assert mcp_source["capabilities"]["node_specs"] == ["wf.mcp.call_tool"]
@@ -146,6 +152,17 @@ def test_wf_std_source_contains_authoring_ops() -> None:
assert set(specs) == expected assert set(specs) == expected
def test_wf_std_source_contains_builtin_reducers() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "stdlib_reducer_store"))
reducers = service.capability_sources["wf.std"].capabilities.reducers
assert set(reducers) == {
"wf.std.replace",
"wf.std.append",
"wf.std.merge_object",
}
def test_service_sources_have_visibility_and_capability_buckets() -> None: def test_service_sources_have_visibility_and_capability_buckets() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "source_shape_store")) service = WfMcpService(store=FileStore(local_temp_root() / "source_shape_store"))