Files
lda-wf/docs/historical/superpowers/plans/2026-06-02-wf-api-remove-double-delegation.md
T

23 KiB

wf_api Remove Double Delegation 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: Collapse workflow calls from WorkflowApi -> WfMcpWorkflowApiBackend -> WorkflowSurfaceHandlers -> domain services to WorkflowApi -> domain services.

Architecture: WorkflowApi becomes the protocol-neutral application facade that composes WorkflowCapabilityApi, WorkflowDraftApi, WorkflowArtifactApi, WorkflowDeploymentApi, and WorkflowRunApi from a WorkflowOperationContext. MCP and CLI construct WorkflowApi(context_from_service(service)) directly. WorkflowSurfaceHandlers remains only as a temporary compatibility shim for legacy imports/tests, and WfMcpWorkflowApiBackend / WorkflowApiBackend are removed.

Tech Stack: Python 3.14, wf_api, wf_mcp, dataclasses, pytest, ruff, basedpyright.


Current Chain

wf_mcp tools / wf_cli
  -> WorkflowApi
  -> WfMcpWorkflowApiBackend
  -> WorkflowSurfaceHandlers
  -> WorkflowCapabilityApi / WorkflowDraftApi / WorkflowArtifactApi / WorkflowDeploymentApi / WorkflowRunApi

This creates two mechanical delegation layers. Adding one workflow operation currently requires touching at least WorkflowApi, WorkflowApiBackend, WfMcpWorkflowApiBackend, and usually WorkflowSurfaceHandlers.

Target Chain

wf_mcp tools / wf_cli
  -> WorkflowApi
  -> WorkflowCapabilityApi / WorkflowDraftApi / WorkflowArtifactApi / WorkflowDeploymentApi / WorkflowRunApi

Legacy imports of WorkflowSurfaceHandlers may still work, but only as a thin wrapper around WorkflowApi.

Files

  • Modify: src/wf_api/models.py
  • Modify: src/wf_api/__init__.py
  • Modify: src/wf_api/service.py
  • Delete: src/wf_api/backend.py
  • Delete: src/wf_mcp/broker/service/workflow_api_backend.py
  • Modify: src/wf_cli/context.py
  • Modify: src/wf_cli/commands/runs.py
  • Modify: src/wf_mcp/workflow_surface/tools.py
  • Modify: src/wf_mcp/workflow_surface/handlers.py
  • Modify: tests/wf_cli/test_context.py
  • Modify: tests/wf_api/test_cli_context_uses_api.py
  • Add: tests/wf_api/test_direct_service.py
  • Add: tests/wf_api/test_no_double_delegation.py
  • Modify docs: docs/current_roadmap.md, docs/wf_mcp_architecture.md, docs/superpowers/plans/2026-06-01-wf-api-extraction-roadmap.md

Task 1: Move TraceRange Out of backend.py

Files:

  • Modify: src/wf_api/models.py

  • Modify: src/wf_api/__init__.py

  • Modify: src/wf_cli/commands/runs.py

  • Modify: src/wf_mcp/workflow_surface/tools.py

  • Step 1: Write import regression test

Add to tests/wf_api/test_raw_workflow_plan_extraction.py:

def test_trace_range_exports_from_wf_api_models() -> None:
    from wf_api import TraceRange
    from wf_api.models import TraceRange as CanonicalTraceRange

    assert TraceRange is CanonicalTraceRange
    assert TraceRange(start=1, limit=2).start == 1
    assert TraceRange(start=1, limit=2).limit == 2
  • Step 2: Run failing test

Run:

uv run pytest tests\wf_api\test_raw_workflow_plan_extraction.py::test_trace_range_exports_from_wf_api_models -q

Expected: fail because wf_api.models.TraceRange does not exist yet.

  • Step 3: Add TraceRange to wf_api.models

In src/wf_api/models.py, add imports:

from dataclasses import dataclass

Then add before RawWorkflowPlan:

@dataclass(frozen=True, slots=True)
class TraceRange:
    """Caller-bounded debug trace slice for durable deployment runs."""

    start: int = 0
    limit: int = 25
  • Step 4: Update wf_api.__init__ export

Change:

from .backend import TraceRange, WorkflowApiBackend

to:

from .models import RawWorkflowPlan, TraceRange

Remove "WorkflowApiBackend" from __all__. Keep "TraceRange".

If RawWorkflowPlan was not exported before, include it only if already expected by tests; do not add new public API unless the existing file already imports it elsewhere.

  • Step 5: Update imports that referenced wf_api.backend.TraceRange

In src/wf_cli/commands/runs.py, replace:

from wf_api.backend import TraceRange

with:

from wf_api import TraceRange

In src/wf_mcp/workflow_surface/tools.py, remove:

from wf_api.backend import TraceRange as ApiTraceRange

Also remove _to_api_trace_range(). Later tasks pass MCP TraceRange directly because WorkflowRunApi validates trace ranges structurally through TraceRangeLike.

  • Step 6: Verify Task 1

Run:

uv run pytest tests\wf_api\test_raw_workflow_plan_extraction.py::test_trace_range_exports_from_wf_api_models tests\wf_cli\test_run_deploy.py tests\wf_mcp\server\test_tools.py -q
uv run ruff check src\wf_api\models.py src\wf_api\__init__.py src\wf_cli\commands\runs.py src\wf_mcp\workflow_surface\tools.py tests\wf_api\test_raw_workflow_plan_extraction.py
uv run ruff format --check src\wf_api\models.py src\wf_api\__init__.py src\wf_cli\commands\runs.py src\wf_mcp\workflow_surface\tools.py tests\wf_api\test_raw_workflow_plan_extraction.py

Expected: tests pass, lint pass, format pass.


Task 2: Make WorkflowApi Compose Domain Services Directly

Files:

  • Modify: src/wf_api/service.py

  • Add: tests/wf_api/test_direct_service.py

  • Step 1: Write direct-composition tests

Create tests/wf_api/test_direct_service.py:

from __future__ import annotations

import asyncio

from wf_artifacts import FileWorkflowArtifactStore
from wf_api import WorkflowApi
from wf_api.artifacts import WorkflowArtifactApi
from wf_api.capabilities import WorkflowCapabilityApi
from wf_api.deployments import WorkflowDeploymentApi
from wf_api.drafts import WorkflowDraftApi
from wf_api.runs import WorkflowRunApi
from wf_mcp.broker import WfMcpService
from wf_mcp.broker.service.workflow_operation_context import context_from_service
from wf_mcp.models import ConnectionConfig
from wf_mcp.storage import FileStore

from tests.wf_mcp.test_support import echo_tool, local_temp_root


def _api() -> WorkflowApi:
    root = local_temp_root() / "wf_api_direct_composition"
    service = WfMcpService(
        store=FileStore(root / "mcp"),
        artifact_store=FileWorkflowArtifactStore(root),
    )
    service.register_connection(
        ConnectionConfig(id="demo.personal", server="demo", account="personal")
    )
    service.register_specs("demo.personal", echo_tool)
    return WorkflowApi(context_from_service(service))


def test_workflow_api_composes_domain_services() -> None:
    api = _api()

    assert isinstance(api.capabilities, WorkflowCapabilityApi)
    assert isinstance(api.drafts, WorkflowDraftApi)
    assert isinstance(api.artifacts, WorkflowArtifactApi)
    assert isinstance(api.deployments, WorkflowDeploymentApi)
    assert isinstance(api.runs, WorkflowRunApi)
    assert not hasattr(api, "backend")


def test_workflow_api_direct_capability_call() -> None:
    api = _api()

    result = asyncio.run(
        api.call_capability(
            qualified_name="demo.personal.echo_tool",
            payload={"text": "hello"},
        )
    )

    assert result["kind"] == "node_spec"
    assert result["outcome"] == "ok"
    assert result["output"] == {"echoed": "hello"}
  • Step 2: Run failing tests

Run:

uv run pytest tests\wf_api\test_direct_service.py -q

Expected: fail because WorkflowApi still expects a WorkflowApiBackend.

  • Step 3: Rewrite WorkflowApi.__init__

In src/wf_api/service.py, replace:

from .backend import TraceRange, WorkflowApiBackend

with:

from .artifacts import WorkflowArtifactApi
from .capabilities import WorkflowCapabilityApi
from .deployments import WorkflowDeploymentApi
from .drafts import WorkflowDraftApi
from .models import TraceRange
from .operation_context import WorkflowOperationContext
from .runs import TraceRangeLike, WorkflowRunApi

Replace the class docstring and constructor:

class WorkflowApi:
    """Protocol-neutral workflow application facade.

    This facade owns the stable application entry point. It composes the
    domain APIs from a WorkflowOperationContext so MCP, CLI, and future HTTP
    callers share one operation surface without importing wf_mcp.
    """

    def __init__(self, context: WorkflowOperationContext) -> None:
        self.context = context
        self.capabilities = WorkflowCapabilityApi(context)
        self.drafts = WorkflowDraftApi(context)
        self.artifacts = WorkflowArtifactApi(context)
        self.deployments = WorkflowDeploymentApi(context)
        self.runs = WorkflowRunApi(context)
  • Step 4: Replace backend delegations with domain service delegations

In src/wf_api/service.py, replace these groups:

Capabilities:

self.backend.list_capabilities(...) -> self.capabilities.list_capabilities(...)
self.backend.inspect_capability(...) -> self.capabilities.inspect_capability(...)
self.backend.call_capability(...) -> self.capabilities.call_capability(...)
self.backend.create_draft_workspace_from_capability(...) -> self.capabilities.create_draft_workspace_from_capability(...)

Artifacts:

self.backend.list_artifacts(...) -> self.artifacts.list_artifacts(...)
self.backend.inspect_artifact(...) -> self.artifacts.inspect_artifact(...)
self.backend.save_artifact(...) -> self.artifacts.save_artifact(...)
self.backend.create_artifact_from_plan(...) -> self.artifacts.create_artifact_from_plan(...)
self.backend.create_artifact_from_draft(...) -> self.artifacts.create_artifact_from_draft(...)
self.backend.create_artifact_from_workspace(...) -> self.artifacts.create_artifact_from_workspace(...)
self.backend.create_wrapper_from_workspace(...) -> self.artifacts.create_wrapper_from_workspace(...)

Drafts:

self.backend.validate_draft(...) -> self.drafts.validate_draft(...)
self.backend.compile_draft(...) -> self.drafts.compile_draft(...)
self.backend.patch_draft(...) -> self.drafts.patch_draft(...)
self.backend.list_draft_workspaces() -> self.drafts.list_draft_workspaces()
self.backend.create_draft_workspace(...) -> self.drafts.create_draft_workspace(...)
self.backend.get_draft_workspace(...) -> self.drafts.get_draft_workspace(...)
self.backend.delete_draft_workspace(...) -> self.drafts.delete_draft_workspace(...)
self.backend.validate_draft_workspace(...) -> self.drafts.validate_draft_workspace(...)
self.backend.patch_draft_workspace(...) -> self.drafts.patch_draft_workspace(...)
self.backend.set_draft_name(...) -> self.drafts.set_draft_name(...)
self.backend.set_draft_route(...) -> self.drafts.set_draft_route(...)
self.backend.set_step_input_map(...) -> self.drafts.set_step_input_map(...)
self.backend.set_step_output_map(...) -> self.drafts.set_step_output_map(...)
self.backend.create_minimal_draft_workspace(...) -> self.drafts.create_minimal_draft_workspace(...)

Deployments:

self.backend.list_deployments() -> self.deployments.list_deployments()
self.backend.inspect_deployment(...) -> self.deployments.inspect_deployment(...)
self.backend.save_deployment(...) -> self.deployments.save_deployment(...)
self.backend.delete_deployment(...) -> self.deployments.delete_deployment(...)
self.backend.validate_deployment(...) -> self.deployments.validate_deployment(...)

Runs:

self.backend.run_deployment(...) -> self.runs.run_deployment(...)
self.backend.resume_run(...) -> self.runs.resume_run(...)
self.backend.inspect_run(...) -> self.runs.inspect_run(...)
self.backend.read_run_trace(...) -> self.runs.read_run_trace(...)

For run methods, change type hints from TraceRange | None to TraceRangeLike | None and TraceRange to TraceRangeLike so MCP Pydantic TraceRange and CLI dataclass TraceRange both remain accepted structurally.

  • Step 5: Verify Task 2

Run:

uv run pytest tests\wf_api\test_direct_service.py tests\wf_api\test_capability_api.py tests\wf_api\test_drafts_service.py tests\wf_api\test_artifact_api.py tests\wf_api\test_deployment_api.py tests\wf_api\test_run_api.py -q
uv run ruff check src\wf_api\service.py tests\wf_api\test_direct_service.py
uv run ruff format --check src\wf_api\service.py tests\wf_api\test_direct_service.py

Expected: direct service tests and domain API tests pass.


Task 3: Update CLI and MCP Tool Construction

Files:

  • Modify: src/wf_cli/context.py

  • Modify: tests/wf_cli/test_context.py

  • Modify: src/wf_mcp/workflow_surface/tools.py

  • Add: tests/wf_api/test_no_double_delegation.py

  • Step 1: Write no-backend-chain test

Create tests/wf_api/test_no_double_delegation.py:

from __future__ import annotations

import ast
from pathlib import Path


def _imports_module(path: Path, module_name: str) -> bool:
    tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    for node in ast.walk(tree):
        if isinstance(node, ast.ImportFrom) and node.module == module_name:
            return True
        if isinstance(node, ast.Import):
            if any(alias.name == module_name for alias in node.names):
                return True
    return False


def test_cli_and_mcp_tools_do_not_import_backend_adapter() -> None:
    root = Path(__file__).resolve().parents[2]

    assert not _imports_module(
        root / "src" / "wf_cli" / "context.py",
        "wf_mcp.broker.service.workflow_api_backend",
    )
    assert not _imports_module(
        root / "src" / "wf_mcp" / "workflow_surface" / "tools.py",
        "wf_mcp.broker.service.workflow_api_backend",
    )
  • Step 2: Run failing no-backend-chain test

Run:

uv run pytest tests\wf_api\test_no_double_delegation.py -q

Expected: fail because CLI context and MCP tools still import WfMcpWorkflowApiBackend.

  • Step 3: Update CLI context

In src/wf_cli/context.py, remove:

from wf_mcp.broker.service.workflow_api_backend import WfMcpWorkflowApiBackend

Add:

from wf_mcp.broker.service.workflow_operation_context import context_from_service

Change load_cli_context():

handlers=WorkflowApi(context_from_service(service)),
  • Step 4: Update CLI context test

In tests/wf_cli/test_context.py, replace the private backend-chain assertion:

assert context.handlers.backend._handlers.service is context.service  # type: ignore[attr-defined]

with:

assert context.handlers.context.artifact_store is context.service.artifact_store
assert context.handlers.context.draft_workspace_store is context.service.draft_workspace_store
assert context.handlers.context.run_store is context.service.run_store

This tests the public context seam instead of the deleted backend chain.

  • Step 5: Update MCP workflow tools

In src/wf_mcp/workflow_surface/tools.py, remove:

from wf_mcp.broker.service.workflow_api_backend import WfMcpWorkflowApiBackend

Add:

from wf_mcp.broker.service.workflow_operation_context import context_from_service

Change:

handlers = WorkflowApi(WfMcpWorkflowApiBackend(service))

to:

handlers = WorkflowApi(context_from_service(service))

For run tools, pass request.trace_range or trace_range directly to handlers.*. Remove conversions through ApiTraceRange.

  • Step 6: Verify Task 3

Run:

uv run pytest tests\wf_api\test_no_double_delegation.py tests\wf_cli\test_context.py tests\wf_cli tests\wf_mcp\server\test_tools.py tests\wf_mcp\workflow_surface -q
uv run ruff check src\wf_cli\context.py src\wf_mcp\workflow_surface\tools.py tests\wf_cli\test_context.py tests\wf_api\test_no_double_delegation.py
uv run ruff format --check src\wf_cli\context.py src\wf_mcp\workflow_surface\tools.py tests\wf_cli\test_context.py tests\wf_api\test_no_double_delegation.py

Expected: CLI and MCP workflow tool tests pass.


Task 4: Shrink WorkflowSurfaceHandlers to Compatibility Shim

Files:

  • Modify: src/wf_mcp/workflow_surface/handlers.py

  • Modify: tests/wf_api/test_direct_service.py or add a small handler shim test

  • Step 1: Add compatibility shim test

Add to tests/wf_api/test_direct_service.py:

def test_workflow_surface_handlers_is_compatibility_shim() -> None:
    from wf_api import WorkflowApi
    from wf_mcp.workflow_surface import WorkflowSurfaceHandlers

    root = local_temp_root() / "workflow_surface_handler_shim"
    service = WfMcpService(
        store=FileStore(root / "mcp"),
        artifact_store=FileWorkflowArtifactStore(root),
    )

    handlers = WorkflowSurfaceHandlers(service)

    assert isinstance(handlers, WorkflowApi)
    assert handlers.service is service
    assert handlers.context.artifact_store is service.artifact_store
  • Step 2: Run failing compatibility test

Run:

uv run pytest tests\wf_api\test_direct_service.py::test_workflow_surface_handlers_is_compatibility_shim -q

Expected: fail because WorkflowSurfaceHandlers is not a WorkflowApi subclass yet.

  • Step 3: Replace WorkflowSurfaceHandlers implementation

Replace src/wf_mcp/workflow_surface/handlers.py with:

from __future__ import annotations

from wf_api import WorkflowApi

from ..broker.service.workflow_operation_context import context_from_service

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ..broker.service import WfMcpService


class WorkflowSurfaceHandlers(WorkflowApi):
    """Compatibility wrapper for old wf_mcp.workflow_surface imports.

    New code should construct `WorkflowApi(context_from_service(service))`
    directly. This shim keeps tests and legacy broker artifact tools working
    while the MCP surface is migrated.
    """

    def __init__(self, service: WfMcpService) -> None:
        self.service = service
        super().__init__(context_from_service(service))


__all__ = ["WorkflowSurfaceHandlers"]

This file should no longer import domain services directly.

  • Step 4: Verify handler compatibility

Run:

uv run pytest tests\wf_api\test_direct_service.py::test_workflow_surface_handlers_is_compatibility_shim tests\wf_mcp\workflow_surface tests\wf_mcp\test_saved_subgraphs.py tests\wf_mcp\broker -q
uv run ruff check src\wf_mcp\workflow_surface\handlers.py tests\wf_api\test_direct_service.py
uv run ruff format --check src\wf_mcp\workflow_surface\handlers.py tests\wf_api\test_direct_service.py

Expected: old handler tests pass through the shim.


Task 5: Delete Backend Protocol and Adapter

Files:

  • Delete: src/wf_api/backend.py

  • Delete: src/wf_mcp/broker/service/workflow_api_backend.py

  • Modify: src/wf_api/__init__.py

  • Modify docs that describe the old backend chain

  • Step 1: Delete backend files

Delete:

src/wf_api/backend.py
src/wf_mcp/broker/service/workflow_api_backend.py
  • Step 2: Remove public backend export

In src/wf_api/__init__.py, ensure there is no import or __all__ entry for WorkflowApiBackend.

  • Step 3: Search for live backend references

Run:

rg -n "WorkflowApiBackend|WfMcpWorkflowApiBackend|workflow_api_backend|\\.backend" src tests

Expected: no live source/test references.

Historical docs under docs/superpowers/plans/2026-06-01-* may still mention the old slice. Do not rewrite historical plans except the active roadmap files named in Task 6.

  • Step 4: Verify deletion

Run:

uv run pytest tests\wf_api\test_import_direction.py tests\wf_api\test_no_double_delegation.py tests\wf_api\test_cli_context_uses_api.py -q
uv run ruff check src\wf_api src\wf_cli\context.py src\wf_mcp\workflow_surface src\wf_mcp\broker\service tests\wf_api
uv run ruff format --check src\wf_api src\wf_cli\context.py src\wf_mcp\workflow_surface src\wf_mcp\broker\service tests\wf_api

Expected: tests pass, lint pass, format pass.


Task 6: Update Active Docs

Files:

  • Modify: docs/current_roadmap.md

  • Modify: docs/wf_mcp_architecture.md

  • Modify: docs/superpowers/plans/2026-06-01-wf-api-extraction-roadmap.md

  • Step 1: Update docs/current_roadmap.md

Replace the bullet that says the next useful slice is removing double-delegation with:

  - Double-delegation has been removed: CLI and MCP workflow tools construct
    `WorkflowApi(context_from_service(service))` directly. `WorkflowSurfaceHandlers`
    remains only as a temporary compatibility shim for older imports.
  • Step 2: Update docs/wf_mcp_architecture.md

Find the architecture text that contains:

wf_api.WorkflowApi ───> WorkflowApiBackend

Replace that diagram/text with:

wf_mcp.workflow_surface.tools
  -> wf_api.WorkflowApi
  -> wf_api domain services
  -> WorkflowOperationContext
  -> WfMcpService adapters/stores/runtime

Add:

`WorkflowSurfaceHandlers` is a compatibility shim only. New entrypoints should
construct `WorkflowApi(context_from_service(service))` directly.
  • Step 3: Update active extraction roadmap

In docs/superpowers/plans/2026-06-01-wf-api-extraction-roadmap.md, add a current-state note near the top:

> Current update: the original `WorkflowApiBackend` seam was useful for proving
> dependency direction, but has been collapsed. `WorkflowApi` now composes
> domain services directly from `WorkflowOperationContext`; MCP owns only
> context construction and tool schemas.

Do not rewrite the historical task bodies. They describe prior slices.

  • Step 4: Verify docs

Run:

git diff --check -- docs\current_roadmap.md docs\wf_mcp_architecture.md docs\superpowers\plans\2026-06-01-wf-api-extraction-roadmap.md

Expected: no whitespace errors.


Task 7: Final Verification

Files:

  • All touched files.

  • Step 1: Run focused workflow API/MCP/CLI tests

Run:

uv run pytest tests\wf_api tests\wf_cli tests\wf_mcp\workflow_surface tests\wf_mcp\server\test_tools.py tests\wf_mcp\test_saved_subgraphs.py -q

Expected: selected tests pass.

  • Step 2: Run full suite

Run:

uv run pytest -q

Expected: full suite passes with known skip/xfail counts.

  • Step 3: Run lint and format checks

Run:

uv run ruff check src\wf_api src\wf_cli src\wf_mcp tests\wf_api tests\wf_cli tests\wf_mcp
uv run ruff format --check src\wf_api src\wf_cli src\wf_mcp tests\wf_api tests\wf_cli tests\wf_mcp

Expected: all checks pass.

  • Step 4: Run typecheck

Run:

uv run basedpyright --level error

Expected: 0 errors, 0 warnings, 0 notes. If the command exits nonzero only because of the known workspace enumeration warning, report that exactly.

  • Step 5: Final reference check

Run:

rg -n "WorkflowApiBackend|WfMcpWorkflowApiBackend|workflow_api_backend|\\.backend" src tests docs\current_roadmap.md docs\wf_mcp_architecture.md

Expected: no references in live source/tests/current docs.


Self-Review

  • Spec coverage: The plan removes WorkflowApiBackend, deletes WfMcpWorkflowApiBackend, updates CLI/MCP construction, keeps handler compatibility, moves TraceRange, and updates active docs.
  • Placeholder scan: No TODO/TBD placeholders remain.
  • Type consistency: WorkflowApi accepts WorkflowOperationContext; run trace methods accept TraceRangeLike; TraceRange is a convenience DTO exported from wf_api.models.
  • Scope check: This does not remove all WorkflowSurfaceHandlers tests or legacy imports. It reduces handlers to a shim; deleting the shim is a later cleanup once wf_mcp.broker.artifact_tools and legacy tests stop importing it.