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:
|
||||
tools
|
||||
node_specs
|
||||
reducers
|
||||
prompts
|
||||
resources
|
||||
```
|
||||
@@ -60,6 +61,7 @@ Expected capabilities:
|
||||
`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.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.
|
||||
- `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 |
|
||||
| --- | --- | --- |
|
||||
| `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.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` |
|
||||
|
||||
@@ -21,7 +21,8 @@ A source is a named owner of capabilities.
|
||||
Examples:
|
||||
|
||||
- `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.admin`: privileged control-plane capabilities
|
||||
|
||||
@@ -237,6 +238,7 @@ Sources own capability kinds:
|
||||
```text
|
||||
tools
|
||||
node_specs
|
||||
reducers
|
||||
prompts
|
||||
resources
|
||||
```
|
||||
@@ -262,7 +264,7 @@ Today:
|
||||
- `CapabilitySource` already owns buckets for tools, node specs, prompts, and
|
||||
resources
|
||||
- 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
|
||||
- discovered upstream tools can already become workflow node specs
|
||||
- saved artifacts can be tagged with `kind="workflow"` or `kind="wrapper"`
|
||||
|
||||
@@ -7,6 +7,7 @@ from .models import (
|
||||
NodeDef,
|
||||
NodeResult,
|
||||
NodeUse,
|
||||
ReducerSpec,
|
||||
SchemaRef,
|
||||
StateField,
|
||||
StateSchema,
|
||||
@@ -51,6 +52,7 @@ __all__ = [
|
||||
"NodeDef",
|
||||
"NodeResult",
|
||||
"NodeUse",
|
||||
"ReducerSpec",
|
||||
"SchemaRef",
|
||||
"StateField",
|
||||
"StateSchema",
|
||||
|
||||
@@ -9,6 +9,7 @@ from wf_core.models.conditions import (
|
||||
VariadicCondition,
|
||||
)
|
||||
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.steps import (
|
||||
ConditionNode,
|
||||
@@ -32,6 +33,7 @@ __all__ = [
|
||||
"LiteralOperand",
|
||||
"NodeDef",
|
||||
"NodeResult",
|
||||
"ReducerSpec",
|
||||
"NodeUse",
|
||||
"NotCondition",
|
||||
"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 wf_core import ReducerSpec
|
||||
from wf_authoring import (
|
||||
NodeReturn,
|
||||
NodeSpec,
|
||||
@@ -109,6 +110,25 @@ def builtin_specs() -> dict[str, NodeSpec[Any, Any]]:
|
||||
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]]:
|
||||
"""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(
|
||||
id=BUILTIN_CONNECTION_ID,
|
||||
kind="system",
|
||||
capabilities=CapabilityBuckets(node_specs=builtin_specs()),
|
||||
capabilities=CapabilityBuckets(
|
||||
node_specs=builtin_specs(),
|
||||
reducers=builtin_reducers(),
|
||||
),
|
||||
visibility=SourceVisibility(
|
||||
planner=True,
|
||||
mcp_client=True,
|
||||
|
||||
@@ -4,6 +4,7 @@ from dataclasses import dataclass, field
|
||||
from typing import Any, Literal
|
||||
|
||||
from wf_authoring import NodeSpec
|
||||
from wf_core import ReducerSpec
|
||||
|
||||
SourceKind = Literal["system", "connection"]
|
||||
|
||||
@@ -27,6 +28,7 @@ class SourcePermissions:
|
||||
class CapabilityBuckets:
|
||||
tools: dict[str, 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)
|
||||
resources: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@@ -60,6 +62,7 @@ class CapabilitySource:
|
||||
"description": self.description,
|
||||
"tool_count": len(self.capabilities.tools),
|
||||
"node_spec_count": len(self.capabilities.node_specs),
|
||||
"reducer_count": len(self.capabilities.reducers),
|
||||
"prompt_count": len(self.capabilities.prompts),
|
||||
"resource_count": len(self.capabilities.resources),
|
||||
}
|
||||
@@ -71,6 +74,7 @@ class CapabilitySource:
|
||||
"capabilities": {
|
||||
"tools": sorted(self.capabilities.tools),
|
||||
"node_specs": sorted(self.capabilities.node_specs),
|
||||
"reducers": sorted(self.capabilities.reducers),
|
||||
"prompts": sorted(self.capabilities.prompts),
|
||||
"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"]
|
||||
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["reducer_count"] == 3
|
||||
|
||||
mcp_source = sources_by_id["wf.mcp"]
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
service = WfMcpService(store=FileStore(local_temp_root() / "source_shape_store"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user