reducerspec
This commit is contained in:
@@ -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
|
||||||
@@ -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` |
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user