Files
lda-wf/docs/historical/superpowers/plans/2026-06-03-local-static-workflow-server.md
T

34 KiB

Local Static Workflow Server 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: Add the first long-lived server composition slice: construct and use WorkflowApi from required stores and local/static workflow sources without constructing WfMcpService.

Architecture: This slice introduces wf_server as a process-composition package, not a transport package. It proves the server can run persisted deployments through WorkflowApi with file stores, wf.std capabilities, and static Python NodeSpecs. HTTP/JSON-RPC/WebSocket/MCP transports are later adapters over this server boundary.

Tech Stack: Python 3.14, pytest, Pydantic v2, wf_api, wf_artifacts, wf_core, wf_authoring, wf_platform.


File Map

  • Create src/wf_api/local_sources.py
    • Own protocol-neutral local workflow sources: wf.std, wf.recipes, spec qualification, and qualified-spec lookup.
  • Modify src/wf_api/__init__.py
    • Export local source helpers needed by server composition.
  • Modify src/wf_mcp/broker/service/builtins.py
    • Convert to compatibility re-export shim from wf_api.local_sources.
  • Create src/wf_server/__init__.py
    • Public exports for first-slice server composition.
  • Create src/wf_server/context.py
    • WorkflowServer
    • WorkflowServerConfig
    • StaticWorkflowSpecProvider
    • LocalWorkflowRuntimeRunner
    • InMemoryWorkflowEventRecorder
    • build_local_static_workflow_server
  • Create tests/wf_api/test_local_sources.py
    • Verify moved wf.std helpers and MCP compatibility shim identity.
  • Create tests/wf_server/test_local_static_server.py
    • Verify server composition, no WfMcpService import, run/inspect/trace over WorkflowApi.
  • Modify docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md
    • Mark first-slice implementation status after code lands.
  • Modify docs/current_roadmap.md
    • Add completion note for local/static server composition.

Out of scope:

  • No HTTP routes.
  • No JSON-RPC/WebSocket transport.
  • No live upstream MCP sources.
  • No OpenAPI dynamic source provider.
  • No auth/tenancy.
  • No transactional store.
  • No CLI remote targeting.

Task 1: Move Local Workflow Sources Into wf_api

Files:

  • Create: src/wf_api/local_sources.py

  • Modify: src/wf_api/__init__.py

  • Modify: src/wf_mcp/broker/service/builtins.py

  • Create: tests/wf_api/test_local_sources.py

  • Step 1: Write failing tests for canonical local source helpers

Create tests/wf_api/test_local_sources.py:

from __future__ import annotations

from wf_api.local_sources import (
    BUILTIN_SOURCE_ID,
    RECIPE_SOURCE_ID,
    builtin_sources,
    get_qualified_spec,
    qualify_spec,
)
from wf_authoring import constant


def test_builtin_sources_expose_workflow_stdlib() -> None:
    sources = builtin_sources()

    assert BUILTIN_SOURCE_ID == "wf.std"
    assert RECIPE_SOURCE_ID == "wf.recipes"
    assert "wf.std" in sources
    assert "wf.std.constant" in sources["wf.std"].capabilities.node_specs
    assert "wf.std.replace" in sources["wf.std"].capabilities.reducers


def test_get_qualified_spec_resolves_planner_visible_spec() -> None:
    sources = builtin_sources()

    spec = get_qualified_spec(sources, "wf.std.constant")

    assert spec.name == "wf.std.constant"
    assert spec.outcomes == ("ok",)


def test_qualify_spec_scopes_authoring_node_name() -> None:
    qualified = qualify_spec("custom.local", constant)

    assert qualified.name == "custom.local.constant"
    assert qualified.input_model is constant.input_model
    assert qualified.output_model is constant.output_model


def test_mcp_builtin_module_reexports_canonical_helpers() -> None:
    from wf_mcp.broker.service import builtins as mcp_builtins

    assert mcp_builtins.BUILTIN_CONNECTION_ID == BUILTIN_SOURCE_ID
    assert mcp_builtins.BUILTIN_SOURCE_ID == BUILTIN_SOURCE_ID
    assert mcp_builtins.builtin_sources is builtin_sources
  • Step 2: Run tests and verify they fail

Run:

uv run pytest tests/wf_api/test_local_sources.py -q

Expected:

FAIL with ModuleNotFoundError: No module named 'wf_api.local_sources'
  • Step 3: Implement wf_api.local_sources

Create src/wf_api/local_sources.py:

from __future__ import annotations

from collections.abc import Mapping
from typing import TYPE_CHECKING, Any

from wf_authoring import NodeSpec, coalesce, concat, constant, default_if_none
from wf_authoring import extract_field, filter_items, filter_items_present, first_item
from wf_authoring import first_item_maybe, first_item_or_none, is_empty, last_item
from wf_authoring import last_item_or_none, length, node, pick_key, pick_path
from wf_authoring import project_fields, rename_fields, runtime_error, truthy
from wf_authoring import extract_text_content
from wf_core.runtime.ops.merges import DEFAULT_REDUCER_DEFINITIONS
from wf_platform import (
    CapabilityBuckets,
    CapabilitySource,
    SourcePermissions,
    SourceVisibility,
)

if TYPE_CHECKING:
    from wf_core import ReducerSpec

BUILTIN_SOURCE_ID = "wf.std"
"""Internal source id for workflow standard-library node specs."""

BUILTIN_CONNECTION_ID = BUILTIN_SOURCE_ID
"""Compatibility alias for older MCP broker code."""

MCP_SOURCE_ID = "wf.mcp"
"""Reserved source id for future workflow-safe MCP utility node specs."""

RECIPE_SOURCE_ID = "wf.recipes"
"""Internal source id for first-party composed workflow recipes."""

AUTHORING_STD_SPECS: tuple[NodeSpec[Any, Any], ...] = (
    coalesce,
    default_if_none,
    constant,
    pick_key,
    pick_path,
    project_fields,
    rename_fields,
    truthy,
    runtime_error,
    first_item,
    first_item_or_none,
    first_item_maybe,
    last_item,
    last_item_or_none,
    length,
    is_empty,
    filter_items,
    filter_items_present,
    extract_field,
    concat,
)
"""Existing authoring ops exposed through the workflow stdlib."""

RECIPE_SPECS: tuple[NodeSpec[Any, Any], ...] = (extract_text_content,)
"""Composed first-party recipes exposed as workflow-facing capabilities."""


def qualify_node_name(source_id: str, local_name: str) -> str:
    """Return one source-qualified node name without assuming MCP connections."""
    if not source_id:
        raise ValueError("source_id must not be empty")
    if not local_name:
        raise ValueError("local node name must not be empty")
    return f"{source_id}.{local_name}"


def qualify_spec(source_id: str, spec: NodeSpec[Any, Any]) -> NodeSpec[Any, Any]:
    """Return a copy of a spec with its node name scoped to a source."""
    return NodeSpec(
        name=qualify_node_name(source_id, spec.name),
        input_model=spec.input_model,
        output_model=spec.output_model,
        outcomes=spec.outcomes,
        fn=spec.fn,
        description=spec.description,
        is_async=spec.is_async,
        accepts_context=spec.accepts_context,
        input_schema_contract=spec.input_schema_contract,
        output_schema_contract=spec.output_schema_contract,
    )


def get_qualified_spec(
    sources: Mapping[str, CapabilitySource],
    qualified_name: str,
) -> NodeSpec[Any, Any]:
    """Resolve a namespaced node spec from enabled planner-visible sources."""
    for source in sources.values():
        if not source.enabled or not source.visibility.planner:
            continue
        spec = source.capabilities.node_specs.get(qualified_name)
        if spec is not None:
            return spec
    raise KeyError(f"unknown qualified node {qualified_name!r}")


def _qualified_specs(
    source_id: str,
    specs: tuple[NodeSpec[Any, Any], ...],
) -> dict[str, NodeSpec[Any, Any]]:
    """Return specs with authoring names rewritten under one source id."""
    local_specs = [
        node(spec, name=spec.name.removeprefix("authoring.")) for spec in specs
    ]
    qualified_specs = [qualify_spec(source_id, spec) for spec in local_specs]
    return {spec.name: spec for spec in qualified_specs}


def builtin_specs() -> dict[str, NodeSpec[Any, Any]]:
    """Return primitive built-in NodeSpecs available to raw workflow plans."""
    return _qualified_specs(BUILTIN_SOURCE_ID, AUTHORING_STD_SPECS)


def recipe_specs() -> dict[str, NodeSpec[Any, Any]]:
    """Return composed first-party recipe specs."""
    return _qualified_specs(RECIPE_SOURCE_ID, RECIPE_SPECS)


def builtin_reducers() -> dict[str, ReducerSpec]:
    """Return built-in reducers owned by the workflow standard library."""
    return {
        definition.spec.name: definition.spec
        for definition in DEFAULT_REDUCER_DEFINITIONS.values()
    }


def builtin_reducer_definitions():
    """Return executable built-in reducers for trusted runtime dependency wiring."""
    return dict(DEFAULT_REDUCER_DEFINITIONS)


def builtin_sources() -> dict[str, CapabilitySource]:
    """Return all local workflow-facing capability sources."""
    return {
        BUILTIN_SOURCE_ID: CapabilitySource(
            id=BUILTIN_SOURCE_ID,
            kind="system",
            capabilities=CapabilityBuckets(
                node_specs=builtin_specs(),
                reducers=builtin_reducers(),
                reducer_definitions=builtin_reducer_definitions(),
            ),
            visibility=SourceVisibility(
                planner=True,
                mcp_client=True,
                admin_dashboard=True,
            ),
            permissions=SourcePermissions(safe_for_workflow=True),
            description="Workflow standard-library nodes.",
        ),
        RECIPE_SOURCE_ID: CapabilitySource(
            id=RECIPE_SOURCE_ID,
            kind="system",
            capabilities=CapabilityBuckets(node_specs=recipe_specs()),
            visibility=SourceVisibility(
                planner=True,
                mcp_client=True,
                admin_dashboard=True,
            ),
            permissions=SourcePermissions(safe_for_workflow=True),
            description="First-party workflow recipes composed from standard nodes.",
        ),
    }


__all__ = [
    "AUTHORING_STD_SPECS",
    "BUILTIN_CONNECTION_ID",
    "BUILTIN_SOURCE_ID",
    "MCP_SOURCE_ID",
    "RECIPE_SOURCE_ID",
    "RECIPE_SPECS",
    "builtin_reducer_definitions",
    "builtin_reducers",
    "builtin_sources",
    "builtin_specs",
    "get_qualified_spec",
    "qualify_node_name",
    "qualify_spec",
    "recipe_specs",
]
  • Step 4: Export local source helpers from wf_api

Modify src/wf_api/__init__.py:

from .local_sources import builtin_sources, get_qualified_spec, qualify_spec

Add to __all__:

"builtin_sources",
"get_qualified_spec",
"qualify_spec",
  • Step 5: Convert MCP builtins to a shim

Replace src/wf_mcp/broker/service/builtins.py with:

"""Compatibility exports for workflow local sources.

Canonical local workflow source helpers live in `wf_api.local_sources` so
non-MCP process hosts can construct `wf.std` without importing broker internals.
"""

from __future__ import annotations

from wf_api.local_sources import (
    AUTHORING_STD_SPECS,
    BUILTIN_CONNECTION_ID,
    BUILTIN_SOURCE_ID,
    MCP_SOURCE_ID,
    RECIPE_SOURCE_ID,
    RECIPE_SPECS,
    builtin_reducer_definitions,
    builtin_reducers,
    builtin_sources,
    builtin_specs,
    get_qualified_spec,
    qualify_node_name,
    qualify_spec,
    recipe_specs,
)

__all__ = [
    "AUTHORING_STD_SPECS",
    "BUILTIN_CONNECTION_ID",
    "BUILTIN_SOURCE_ID",
    "MCP_SOURCE_ID",
    "RECIPE_SOURCE_ID",
    "RECIPE_SPECS",
    "builtin_reducer_definitions",
    "builtin_reducers",
    "builtin_sources",
    "builtin_specs",
    "get_qualified_spec",
    "qualify_node_name",
    "qualify_spec",
    "recipe_specs",
]
  • Step 6: Run local source tests

Run:

uv run pytest tests/wf_api/test_local_sources.py tests/wf_mcp/service/test_catalog.py -q

Expected:

all selected tests pass
  • Step 7: Commit Task 1
git add src/wf_api/local_sources.py src/wf_api/__init__.py src/wf_mcp/broker/service/builtins.py tests/wf_api/test_local_sources.py
git commit -m "refactor: move workflow local sources to wf_api"

Task 2: Add wf_server Composition Package

Files:

  • Create: src/wf_server/__init__.py

  • Create: src/wf_server/context.py

  • Create: tests/wf_server/test_local_static_server.py

  • Step 1: Write failing server composition tests

Create tests/wf_server/test_local_static_server.py:

from __future__ import annotations

import ast
import asyncio
from pathlib import Path

from wf_api.models import RawWorkflowPlan
from wf_core import END
from wf_server import build_local_static_workflow_server


def _constant_plan() -> RawWorkflowPlan:
    return RawWorkflowPlan.model_validate(
        {
            "name": "server_constant",
            "input_schema": {"type": "object", "properties": {}},
            "state_schema": {
                "type": "object",
                "properties": {
                    "result": {"type": "string", "reducer": "wf.std.replace"}
                },
            },
            "output_schema": {
                "type": "object",
                "properties": {"result": {"type": "string"}},
                "required": ["result"],
            },
            "outcomes": ["ok"],
            "start": "constant",
            "nodes": [
                {
                    "id": "constant",
                    "type": "node",
                    "node": "wf.std.constant",
                    "input": [
                        {
                            "value": "hello from server",
                            "target": {"root": "local", "parts": ["value"]},
                        }
                    ],
                    "output": [
                        {
                            "source": {"root": "local", "parts": ["value"]},
                            "target": {"root": "state", "parts": ["result"]},
                        }
                    ],
                }
            ],
            "edges": [{"from": "constant", "outcome": "ok", "to": END}],
            "output": [
                {
                    "path": {"root": "state", "parts": ["result"]},
                    "target": {"root": "local", "parts": ["result"]},
                }
            ],
        }
    )


def test_wf_server_context_imports_no_wfmcp_service() -> None:
    path = Path("src/wf_server/context.py")
    tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))

    violations: list[str] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.ImportFrom) and node.module is not None:
            if node.module == "wf_mcp.broker" or node.module.endswith(".core"):
                violations.append(f"{node.lineno}: from {node.module} import ...")
        elif isinstance(node, ast.Import):
            for alias in node.names:
                if alias.name in {"wf_mcp.broker", "wf_mcp.broker.service.core"}:
                    violations.append(f"{node.lineno}: import {alias.name}")

    assert violations == []


def test_local_static_server_runs_deployment_and_persists_run(tmp_path) -> None:
    server = build_local_static_workflow_server(tmp_path / "store")
    api = server.api
    plan = _constant_plan()

    artifact_result = asyncio.run(
        api.create_artifact_from_plan(
            artifact_id="server_constant",
            version=1,
            title="Server Constant",
            plan=plan,
            outcomes=["ok"],
            source_bindings={"wf.std": "wf.std"},
        )
    )
    deployment_result = asyncio.run(
        api.save_deployment(
            {
                "id": "server_constant.default",
                "artifact_id": "server_constant",
                "artifact_version": 1,
                "bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
            }
        )
    )
    run_result = asyncio.run(
        api.run_deployment(
            deployment_id="server_constant.default",
            workflow_input={},
        )
    )

    assert artifact_result["artifact_id"] == "server_constant"
    assert deployment_result["deployment_id"] == "server_constant.default"
    assert run_result["status"] == "completed"
    assert run_result["output"]["result"] == "hello from server"
    assert isinstance(run_result["run_id"], str)
    assert server.stores.run_store.get_run(run_result["run_id"]).id == run_result["run_id"]


def test_local_static_server_inspects_and_reads_bounded_trace(tmp_path) -> None:
    server = build_local_static_workflow_server(tmp_path / "store")
    api = server.api
    plan = _constant_plan()
    asyncio.run(
        api.create_artifact_from_plan(
            artifact_id="server_trace",
            version=1,
            title="Server Trace",
            plan=plan.model_copy(update={"name": "server_trace"}),
            outcomes=["ok"],
            source_bindings={"wf.std": "wf.std"},
        )
    )
    asyncio.run(
        api.save_deployment(
            {
                "id": "server_trace.default",
                "artifact_id": "server_trace",
                "artifact_version": 1,
                "bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
            }
        )
    )
    run_result = asyncio.run(
        api.run_deployment(deployment_id="server_trace.default", workflow_input={})
    )

    summary = asyncio.run(api.inspect_run(run_id=run_result["run_id"]))
    trace = asyncio.run(
        api.read_run_trace(
            run_id=run_result["run_id"],
            trace_range=server.trace_range(start=0, limit=1),
        )
    )

    assert "trace" not in summary
    assert summary["trace_count"] >= 1
    assert trace["trace_start"] == 0
    assert trace["trace_limit"] == 1
    assert len(trace["trace"]) == 1
  • Step 2: Run tests and verify they fail

Run:

uv run pytest tests/wf_server/test_local_static_server.py -q

Expected:

FAIL with ModuleNotFoundError: No module named 'wf_server'
  • Step 3: Implement wf_server.context

Create src/wf_server/context.py:

from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from wf_api import WorkflowApi, durable_workflow_api
from wf_api.local_sources import builtin_sources, get_qualified_spec
from wf_api.models import RawWorkflowPlan, TraceRange
from wf_api.operation_context import (
    WorkflowEventRecorder,
    WorkflowOperationContext,
    WorkflowRuntimeRunner,
    WorkflowSpecProvider,
)
from wf_api.runtime_dependencies import resolve_runtime_dependencies
from wf_api.saved_subgraphs import (
    SavedSubgraphTree,
    prepare_saved_subgraphs,
    resolve_saved_subgraph_tree,
)
from wf_api.stores import WorkflowStores, file_workflow_stores
from wf_artifacts import WorkflowArtifact, WorkflowDeployment
from wf_authoring import NodeSpec
from wf_core import (
    NodeUse,
    RunState,
    Workflow,
    execute_workflow_result_async,
    resume_workflow_result_async,
)
from wf_platform import CapabilitySource


@dataclass(frozen=True, slots=True)
class WorkflowServerConfig:
    """Configuration for the first local/static workflow server slice."""

    store_root: Path


@dataclass(slots=True)
class InMemoryWorkflowEventRecorder(WorkflowEventRecorder):
    """Small process-local event sink for server composition tests."""

    events: list[dict[str, Any]] = field(default_factory=list)

    def record_event(self, event: object) -> None:
        self.events.append({"kind": "adapter_event", "event": event})

    def record_workflow_event(
        self,
        event_type: str,
        *,
        capability_id: str,
        payload: dict[str, Any],
    ) -> None:
        self.events.append(
            {
                "kind": event_type,
                "capability_id": capability_id,
                "payload": payload,
            }
        )


@dataclass(frozen=True, slots=True)
class StaticWorkflowSpecProvider(WorkflowSpecProvider):
    """Source provider for local/static server capabilities."""

    sources: Mapping[str, CapabilitySource]

    @property
    def capability_sources(self) -> Mapping[str, CapabilitySource]:
        return self.sources

    def get_qualified_spec(self, qualified_name: str) -> NodeSpec[Any, Any]:
        return get_qualified_spec(self.sources, qualified_name)


@dataclass(slots=True)
class LocalWorkflowRuntimeRunner(WorkflowRuntimeRunner):
    """Run workflow plans against local/static source catalogs."""

    specs: StaticWorkflowSpecProvider
    artifact_store: Any

    def compile_plan(
        self,
        plan: RawWorkflowPlan,
        node_name_bindings: dict[str, str] | None = None,
    ) -> Workflow:
        node_defs: dict[str, Any] = {}
        bindings = node_name_bindings or {}
        for step in plan.nodes:
            if not isinstance(step, NodeUse):
                continue
            qualified_name = bindings.get(step.node, step.node)
            spec = self.specs.get_qualified_spec(qualified_name)
            node_defs[qualified_name] = spec.to_node_def()

        nodes = []
        for node in plan.nodes:
            node_payload = node.model_dump(by_alias=True)
            if isinstance(node, NodeUse):
                node_payload["node"] = bindings.get(node.node, node.node)
            nodes.append(node_payload)

        return Workflow.model_validate(
            {
                "name": plan.name,
                "input_schema": plan.input_schema,
                "state_schema": plan.state_schema,
                "output_schema": plan.output_schema,
                "output": [
                    binding.model_dump(mode="json") for binding in plan.output
                ],
                "outcomes": plan.outcomes,
                "start": plan.start,
                "node_defs": [node.model_dump() for node in node_defs.values()],
                "nodes": nodes,
                "edges": [edge.model_dump(by_alias=True) for edge in plan.edges],
            }
        )

    def prepare_workflow_runtime(
        self,
        plan: RawWorkflowPlan,
        *,
        deployment: WorkflowDeployment | None,
        artifact: WorkflowArtifact | None,
        saved_subgraph_tree: SavedSubgraphTree | None = None,
    ) -> tuple[Workflow, dict[str, Any], dict[str, Any], dict[str, Any]]:
        plan_node_names = [
            node.node for node in plan.nodes if isinstance(node, NodeUse)
        ]
        runtime_artifact = artifact or WorkflowArtifact(
            id=plan.name,
            version=1,
            title=plan.name,
            input_schema=plan.input_schema,
            output_schema=plan.output_schema,
            outcomes=("completed",),
            plan=plan.model_dump(mode="json", by_alias=True),
        )
        dependencies = resolve_runtime_dependencies(
            artifact=runtime_artifact,
            deployment=deployment,
            sources=self.specs.capability_sources,
            plan_node_names=plan_node_names,
        )
        prepared_subgraphs = {}
        if saved_subgraph_tree is not None:
            prepared_subgraphs = prepare_saved_subgraphs(
                tree=saved_subgraph_tree,
                deployment=deployment,
                sources=self.specs.capability_sources,
                compile_plan=self.compile_plan,
            )
        elif artifact is not None and self.artifact_store is not None:
            tree = resolve_saved_subgraph_tree(
                root_artifact=artifact,
                artifact_store=self.artifact_store,
            )
            prepared_subgraphs = prepare_saved_subgraphs(
                tree=tree,
                deployment=deployment,
                sources=self.specs.capability_sources,
                compile_plan=self.compile_plan,
            )
        workflow = self.compile_plan(plan, dependencies.node_name_bindings)
        return (
            workflow,
            dependencies.node_registry,
            dependencies.reducers,
            prepared_subgraphs,
        )

    async def run_workflow_from_plan(
        self,
        plan: RawWorkflowPlan,
        workflow_input: dict[str, Any],
        deployment: WorkflowDeployment | None = None,
        artifact: WorkflowArtifact | None = None,
        saved_subgraph_tree: SavedSubgraphTree | None = None,
    ) -> RunState:
        workflow, registry, reducers, prepared_subgraphs = (
            self.prepare_workflow_runtime(
                plan,
                deployment=deployment,
                artifact=artifact,
                saved_subgraph_tree=saved_subgraph_tree,
            )
        )
        return await execute_workflow_result_async(
            workflow,
            workflow_input,
            registry,
            reducers=reducers,
            subgraphs=prepared_subgraphs,
        )

    async def resume_workflow_from_plan(
        self,
        plan: RawWorkflowPlan,
        run: RunState,
        *,
        resume_payload: dict[str, Any],
        resume_outcome: str,
        deployment: WorkflowDeployment | None = None,
        artifact: WorkflowArtifact | None = None,
        saved_subgraph_tree: SavedSubgraphTree | None = None,
    ) -> RunState:
        workflow, registry, reducers, prepared_subgraphs = (
            self.prepare_workflow_runtime(
                plan,
                deployment=deployment,
                artifact=artifact,
                saved_subgraph_tree=saved_subgraph_tree,
            )
        )
        return await resume_workflow_result_async(
            workflow,
            run,
            registry,
            resume_payload=resume_payload,
            resume_outcome=resume_outcome,
            reducers=reducers,
            subgraphs=prepared_subgraphs,
        )


@dataclass(frozen=True, slots=True)
class WorkflowServer:
    """First-slice long-lived server composition without transport concerns."""

    config: WorkflowServerConfig
    stores: WorkflowStores
    context: WorkflowOperationContext
    api: WorkflowApi
    events: InMemoryWorkflowEventRecorder

    @staticmethod
    def trace_range(*, start: int, limit: int) -> TraceRange:
        return TraceRange(start=start, limit=limit)


def build_local_static_workflow_server(root: str | Path) -> WorkflowServer:
    """Build a durable local/static workflow server composition."""
    config = WorkflowServerConfig(store_root=Path(root))
    stores = file_workflow_stores(config.store_root)
    events = InMemoryWorkflowEventRecorder()
    specs = StaticWorkflowSpecProvider(builtin_sources())
    runtime = LocalWorkflowRuntimeRunner(
        specs=specs,
        artifact_store=stores.artifact_store,
    )
    context = WorkflowOperationContext(
        artifact_store=stores.artifact_store,
        draft_workspace_store=stores.draft_workspace_store,
        run_store=stores.run_store,
        events=events,
        specs=specs,
        runtime=runtime,
        live_sources=None,
    )
    api = durable_workflow_api(context)
    return WorkflowServer(
        config=config,
        stores=stores,
        context=context,
        api=api,
        events=events,
    )
  • Step 4: Implement wf_server.__init__

Create src/wf_server/__init__.py:

from __future__ import annotations

from .context import (
    InMemoryWorkflowEventRecorder,
    LocalWorkflowRuntimeRunner,
    StaticWorkflowSpecProvider,
    WorkflowServer,
    WorkflowServerConfig,
    build_local_static_workflow_server,
)

__all__ = [
    "InMemoryWorkflowEventRecorder",
    "LocalWorkflowRuntimeRunner",
    "StaticWorkflowSpecProvider",
    "WorkflowServer",
    "WorkflowServerConfig",
    "build_local_static_workflow_server",
]
  • Step 5: Run server tests

Run:

uv run pytest tests/wf_server/test_local_static_server.py -q

Expected:

3 passed
  • Step 6: Run import-direction sanity checks

Run:

rg -n "WfMcpService|WorkflowSurfaceHandlers" src/wf_server

Expected:

no matches

Then run:

uv run pytest tests/wf_api/test_import_direction.py -q

Expected:

1 passed
  • Step 7: Commit Task 2
git add src/wf_server tests/wf_server
git commit -m "feat: add local static workflow server"

Task 3: Verify Existing MCP Path Still Works

Files:

  • Verify compatibility only; no intended source changes unless tests fail.

  • Step 1: Run MCP service/catalog tests

Run:

uv run pytest tests/wf_mcp/service/test_catalog.py tests/wf_mcp/service/test_workflow_runtime.py -q

Expected:

all selected tests pass
  • Step 2: Run workflow surface run tests

Run:

uv run pytest tests/wf_mcp/workflow_surface/test_runs.py tests/wf_mcp/test_saved_subgraphs.py -q

Expected:

all selected tests pass
  • Step 3: If imports fail, fix only compatibility shims

If MCP tests fail because symbols moved from wf_mcp.broker.service.builtins, add missing re-export names to the shim. Do not reintroduce duplicate stdlib source implementation under wf_mcp.

  • Step 4: Commit compatibility fix if needed

Only if Step 3 changed files:

git add src/wf_mcp/broker/service/builtins.py
git commit -m "fix: preserve mcp builtin source compatibility"

Task 4: Document First Slice Status

Files:

  • Modify: docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md

  • Modify: docs/current_roadmap.md

  • Step 1: Update long-lived API spec first-slice status

In docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md, under ## First Slice, add this paragraph after the proof bullets:

Implementation status:

- `wf_server.build_local_static_workflow_server()` constructs a durable
  `WorkflowApi` with required file-backed stores, local `wf.std`/`wf.recipes`
  sources, and a local runtime runner.
- This first slice has no transport adapter. Clients still call the in-process
  `WorkflowApi` in tests; HTTP/JSON-RPC/WebSocket/MCP transport adapters are
  later slices.
  • Step 2: Update current roadmap

In docs/current_roadmap.md, under Durable API service shape, add:

- First slice implemented: `wf_server` can construct a local/static durable
  `WorkflowApi` without `WfMcpService`. Transport adapters remain future work.
  • Step 3: Run docs sanity check

Run:

rg -n "build_local_static_workflow_server|wf_server|transport adapters remain" docs

Expected:

matches in long-lived API spec and current roadmap
  • Step 4: Commit Task 4
git add docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md docs/current_roadmap.md
git commit -m "docs: record local static workflow server slice"

Task 5: Final Verification

Files:

  • Verify all touched code/tests/docs.

  • Step 1: Run focused tests

Run:

uv run pytest tests/wf_api/test_local_sources.py tests/wf_server/test_local_static_server.py tests/wf_api/test_import_direction.py -q

Expected:

all selected tests pass
  • Step 2: Run relevant API/MCP suites

Run:

uv run pytest tests/wf_api tests/wf_mcp/service/test_catalog.py tests/wf_mcp/service/test_workflow_runtime.py tests/wf_mcp/workflow_surface/test_runs.py tests/wf_mcp/test_saved_subgraphs.py -q

Expected:

all selected tests pass
  • Step 3: Run ruff

Run:

uv run ruff check src/wf_api src/wf_server src/wf_mcp/broker/service/builtins.py tests/wf_api tests/wf_server
uv run ruff format --check src/wf_api src/wf_server src/wf_mcp/broker/service/builtins.py tests/wf_api tests/wf_server

Expected:

All checks passed
  • Step 4: Run basedpyright

Run:

uv run basedpyright --level error

Expected:

0 errors, 0 warnings, 0 notes

Known caveat: this repo may still exit nonzero with the workspace enumeration warning even when it reports 0 errors.

  • Step 5: Report

Report:

Implemented first local/static workflow server slice:
- moved local workflow source helpers to wf_api
- added wf_server process composition without WfMcpService
- proved run/inspect/trace through WorkflowApi
- preserved MCP builtin compatibility

Verification:
- focused tests: ...
- relevant suites: ...
- ruff: ...
- basedpyright: ...

Later Slice Pointers

These are intentionally not part of this implementation plan:

  1. Transport adapter plan

    • Add JSON-RPC 2.0 over HTTP, likely wf_transport_rpc_http.
    • Prefer fastapi-jsonrpc + uvicorn for server dispatch/docs.
    • Start with health, list/inspect capabilities, run deployment, inspect run, read trace, and resume run.
    • Use stable dotted method names such as workflow.runs.start.
    • Keep method handlers thin over WorkflowApi.
    • Do not dynamically register saved workflows as JSON-RPC methods.
  2. CLI remote target plan

    • Let wf_cli choose local server composition or remote transport client.
    • Keep command names stable.
  3. Source provider plan

    • Add explicit provider interfaces for static Python specs, OpenAPI sources, and upstream MCP sources.
    • Avoid making "source" mean "MCP connection."
  4. Live upstream MCP source plan

    • Add connection lifecycle, auth, catalog refresh, source liveness, and side-effect failure semantics to the long-lived server only after local server + transport are proven.
  5. Transactional store plan

    • Add SQLite/Postgres or similar for multi-process safety and compare-and-swap resume.

Self-Review

Spec coverage:

  • Client/transport/server/runner/source flow is represented by wf_server + later transport slices.
  • First slice is local/static only, as requested.
  • WfMcpService is not used by wf_server.
  • Required stores are enforced through durable_workflow_api.
  • MCP/OpenAPI/live source support is explicitly deferred.

Placeholder scan:

  • No placeholder implementation steps.
  • Every task includes exact file paths and test commands.

Type consistency:

  • Uses current WorkflowApi, WorkflowOperationContext, WorkflowStores, RawWorkflowPlan, and TraceRange names.
  • wf_server is the process-composition package; transports are later siblings.