Files
lda-wf/docs/historical/superpowers/plans/2026-06-01-wf-api-slice-1-dependency-direction-invalid.md
T

1501 lines
45 KiB
Markdown

# [INVALID] wf_api Slice 1: Dependency Direction — Implementation Plan
> **INVALID — replaced by `2026-06-01-wf-api-slice-1-dependency-direction.md`.**
>
> **Why this plan is wrong:** It moves 13 helper modules (`constants`, `refs`, `next_actions`, `wrapper_hints`, `saved_subgraphs`, `run_lifecycle`, `runtime_dependencies`, `events`, `listing`, `models`) into `wf_api` before the extraction seam is proven. That is a large, risky blast radius for a "dependency direction only" slice. It also defines `WorkflowApiBackend` as a low-level store/execution port rather than a high-level operation protocol, which means the facade does real work instead of delegating. The replacement plan keeps the existing `WorkflowSurfaceHandlers` implementation in `wf_mcp`, wraps it behind a high-level `WorkflowApiBackend` protocol, and introduces `wf_api` as a thin delegating facade — zero helper migration, zero response shape risk.
> **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:** Make both CLI and MCP call a protocol-neutral `WorkflowApi` while `wf_api` imports zero `wf_mcp` modules.
**Architecture:** `WorkflowApi` (renamed `WorkflowSurfaceHandlers`) takes a `WorkflowApiBackend` protocol instead of `WfMcpService`. `WfMcpWorkflowApiBackend` adapts `WfMcpService` into that protocol. Helper modules that `WorkflowApi` needs move from `wf_mcp.workflow_surface` to `wf_api` because they are protocol-neutral and their current location would create `wf_api -> wf_mcp` imports.
**Tech Stack:** Python 3.12+, pydantic, anyio, existing `wf_artifacts`/`wf_platform`/`wf_authoring`/`wf_core` packages.
---
## Import Direction Rules
After this plan is complete, the following rules hold:
```text
wf_api -> wf_artifacts, wf_platform, wf_authoring, wf_core (OK)
wf_api -> wf_mcp (FORBIDDEN)
wf_mcp -> wf_api (OK — adapter direction)
wf_cli -> wf_api (OK)
wf_cli -> wf_mcp (OK — config/service construction only)
```
## File Structure
### New files to create
| File | Responsibility |
|------|---------------|
| `src/wf_api/__init__.py` | Package root; re-exports `WorkflowApi`, `WorkflowApiBackend` |
| `src/wf_api/backend.py` | `WorkflowApiBackend` protocol |
| `src/wf_api/service.py` | `WorkflowApi` class (body from `WorkflowSurfaceHandlers`) |
| `src/wf_api/events.py` | `McpEvent` dataclass + `make_event` factory (moved from `wf_mcp.events.models`) |
| `src/wf_api/listing.py` | `matches_query`, `paged_list_payload` (moved from `wf_mcp.shared.listing`) |
| `src/wf_api/models.py` | `RawWorkflowPlan`, `TraceRange` (moved from `wf_mcp.models` + `wf_mcp.workflow_surface.models`) |
| `src/wf_api/constants.py` | Draft helper constants (moved from `wf_mcp.workflow_surface.constants`) |
| `src/wf_api/refs.py` | `parse_workflow_surface_capability_id` (moved from `wf_mcp.workflow_surface.refs`) |
| `src/wf_api/next_actions.py` | `NextActions`, `NextActionTool` (moved from `wf_mcp.workflow_surface.next_actions`) |
| `src/wf_api/wrapper_hints.py` | `wrapper_hints_for_capability` etc. (moved from `wf_mcp.workflow_surface.wrapper_hints`) |
| `src/wf_api/runtime_dependencies.py` | `resolve_runtime_dependencies` (moved from `wf_mcp.workflow_surface.runtime_dependencies`) |
| `src/wf_api/saved_subgraphs.py` | `SavedSubgraphTree` etc. (moved from `wf_mcp.workflow_surface.saved_subgraphs`) |
| `src/wf_api/run_lifecycle.py` | `persist_stopped_run`, `create_pinned_environment` etc. (moved from `wf_mcp.workflow_surface.run_lifecycle`) |
| `src/wf_mcp/broker/service/workflow_api_backend.py` | `WfMcpWorkflowApiBackend` adapter |
### Files to modify
| File | Change |
|------|--------|
| `src/wf_mcp/workflow_surface/handlers.py` | Replace with thin shim: re-export from `wf_api.service` |
| `src/wf_mcp/workflow_surface/constants.py` | Replace with thin shim: re-export from `wf_api.constants` |
| `src/wf_mcp/workflow_surface/refs.py` | Replace with thin shim: re-export from `wf_api.refs` |
| `src/wf_mcp/workflow_surface/next_actions.py` | Replace with thin shim: re-export from `wf_api.next_actions` |
| `src/wf_mcp/workflow_surface/wrapper_hints.py` | Replace with thin shim: re-export from `wf_api.wrapper_hints` |
| `src/wf_mcp/workflow_surface/runtime_dependencies.py` | Replace with thin shim: re-export from `wf_api.runtime_dependencies` |
| `src/wf_mcp/workflow_surface/saved_subgraphs.py` | Replace with thin shim: re-export from `wf_api.saved_subgraphs` |
| `src/wf_mcp/workflow_surface/run_lifecycle.py` | Replace with thin shim: re-export from `wf_api.run_lifecycle` |
| `src/wf_mcp/workflow_surface/models.py` | Update `NextActions` import to `wf_api.next_actions` |
| `src/wf_mcp/workflow_surface/tools.py` | Import `TraceRange` from `wf_api.models`, `WorkflowApi` from `wf_api.service` |
| `src/wf_mcp/workflow_surface/__init__.py` | Keep re-exports (shims make them work) |
| `src/wf_mcp/events/models.py` | Replace with thin shim: re-export from `wf_api.events` |
| `src/wf_mcp/shared/listing.py` | Replace with thin shim: re-export from `wf_api.listing` |
| `src/wf_mcp/shared/__init__.py` | Update `matches_query`/`paged_list_payload` import path |
| `src/wf_cli/context.py` | Import `WorkflowApi` from `wf_api.service` |
| `src/wf_cli/commands/runs.py` | Import `TraceRange` from `wf_api.models` |
| `pyproject.toml` | Add `wf_api` to packages list |
### Files NOT to modify
- `tests/` — shim re-exports preserve all existing import paths; tests pass unchanged
- `examples/` — same; `from wf_mcp.workflow_surface import WorkflowSurfaceHandlers` still works via shim
- `src/wf_mcp/broker/service/core.py``WfMcpService` is unchanged
- `src/wf_mcp/workflow_surface/tools.py` internals — only import paths change; tool registration logic unchanged
---
## Risk: Where the Roadmap Might Be Wrong
1. **The roadmap says "keep helper modules in place."** That is not possible for Slice 1. `wf_api.service` imports `constants`, `refs`, `next_actions`, `wrapper_hints`, `saved_subgraphs`, `run_lifecycle`, `runtime_dependencies`, `matches_query`, `paged_list_payload`, `make_event`, and `RawWorkflowPlan` — all from `wf_mcp.*` today. To establish the `wf_api -/-> wf_mcp` rule, these must move. The extraction map confirms this. The roadmap's "not allowed: do not move every helper module yet" is aspirational for later slices but wrong for this one.
2. **The roadmap puts `WfMcpWorkflowApiBackend` in `src/wf_mcp/broker/service/workflow_api_backend.py`.** This is fine but the adapter needs access to `_live_source_diagnostics` (MCP connections/adapters/auth). The plan keeps that function inside the adapter rather than importing it from the old `handlers.py` shim, because the shim re-exports from `wf_api.service` which no longer contains MCP-specific code.
3. **`McpEvent` naming.** The roadmap defers renaming to Slice 2. This plan keeps the name `McpEvent` but moves it to `wf_api.events`. The rename to `DomainEvent` is a Slice 2 concern.
4. **`wf_mcp.workflow_surface.models` stays in `wf_mcp`.** It contains MCP tool request/response pydantic models (507 lines) that `tools.py` needs. Only the `NextActions` import path changes. `TraceRange` is re-exported via the `__init__.py` shim.
---
## Task 1: Create `wf_api` package root
**Files:**
- Create: `src/wf_api/__init__.py`
- [ ] **Step 1: Create the package directory**
```bash
mkdir -p src/wf_api
```
- [ ] **Step 2: Write `__init__.py`**
```python
from __future__ import annotations
from .backend import WorkflowApiBackend
from .service import WorkflowApi
__all__ = ["WorkflowApi", "WorkflowApiBackend"]
```
- [ ] **Step 3: Verify import**
```bash
uv run python -c "from wf_api import WorkflowApi, WorkflowApiBackend; print('OK')"
```
Expected: `OK` (will fail until later tasks populate the modules — that's fine; create the file now and revisit).
- [ ] **Step 4: Commit**
```bash
git add src/wf_api/__init__.py
git commit -m "feat(wf_api): create package root with re-exports"
```
---
## Task 2: Move protocol-neutral helper modules to `wf_api`
These modules have zero `wf_mcp` imports. Moving them is a prerequisite for `wf_api.service` to not import `wf_mcp`.
### Task 2a: Move `constants`
**Files:**
- Create: `src/wf_api/constants.py`
- Modify: `src/wf_mcp/workflow_surface/constants.py` → shim
- [ ] **Step 1: Create `src/wf_api/constants.py`**
```python
"""Shared workflow-surface literals used by generated draft helpers."""
DEFAULT_CALL_STEP_ID = "call"
DEFAULT_ERROR_STEP_ID = "tool_error"
DEFAULT_OK_OUTCOME = "ok"
DEFAULT_ERROR_OUTCOME = "error"
RUNTIME_ERROR_CAPABILITY = "wf.std.runtime_error"
__all__ = [
"DEFAULT_CALL_STEP_ID",
"DEFAULT_ERROR_OUTCOME",
"DEFAULT_ERROR_STEP_ID",
"DEFAULT_OK_OUTCOME",
"RUNTIME_ERROR_CAPABILITY",
]
```
- [ ] **Step 2: Replace `src/wf_mcp/workflow_surface/constants.py` with shim**
```python
"""Backward-compatibility shim — canonical location is wf_api.constants."""
from wf_api.constants import (
DEFAULT_CALL_STEP_ID,
DEFAULT_ERROR_OUTCOME,
DEFAULT_ERROR_STEP_ID,
DEFAULT_OK_OUTCOME,
RUNTIME_ERROR_CAPABILITY,
)
__all__ = [
"DEFAULT_CALL_STEP_ID",
"DEFAULT_ERROR_OUTCOME",
"DEFAULT_ERROR_STEP_ID",
"DEFAULT_OK_OUTCOME",
"RUNTIME_ERROR_CAPABILITY",
]
```
- [ ] **Step 3: Verify existing tests still pass**
```bash
uv run pytest tests/wf_mcp/workflow_surface/test_drafts.py -q
```
Expected: all pass (imports route through shim).
- [ ] **Step 4: Commit**
```bash
git add src/wf_api/constants.py src/wf_mcp/workflow_surface/constants.py
git commit -m "refactor: move constants to wf_api, leave shim in wf_mcp"
```
### Task 2b: Move `wrapper_hints`
**Files:**
- Create: `src/wf_api/wrapper_hints.py`
- Modify: `src/wf_mcp/workflow_surface/wrapper_hints.py` → shim
- [ ] **Step 1: Copy `src/wf_mcp/workflow_surface/wrapper_hints.py` to `src/wf_api/wrapper_hints.py`**
Use `read` to get the full file content, then `write` it to the new location. Verify the file has no `wf_mcp` imports (it should only import from `pydantic`, `enum`, `typing`).
- [ ] **Step 2: Replace old file with shim**
```python
"""Backward-compatibility shim — canonical location is wf_api.wrapper_hints."""
from wf_api.wrapper_hints import * # noqa: F401,F403
```
- [ ] **Step 3: Verify**
```bash
uv run pytest tests/wf_mcp/test_workflow_wrapper_hints.py -q
```
Expected: all pass.
- [ ] **Step 4: Commit**
```bash
git add src/wf_api/wrapper_hints.py src/wf_mcp/workflow_surface/wrapper_hints.py
git commit -m "refactor: move wrapper_hints to wf_api, leave shim"
```
### Task 2c: Move `refs`
**Files:**
- Create: `src/wf_api/refs.py`
- Modify: `src/wf_mcp/workflow_surface/refs.py` → shim
- [ ] **Step 1: Copy `src/wf_mcp/workflow_surface/refs.py` to `src/wf_api/refs.py`**
Verify: imports only from `wf_artifacts` and `wf_platform`.
- [ ] **Step 2: Replace old file with shim**
```python
"""Backward-compatibility shim — canonical location is wf_api.refs."""
from wf_api.refs import * # noqa: F401,F403
```
- [ ] **Step 3: Verify**
```bash
uv run pytest tests/wf_mcp/test_workflow_surface_refs.py -q
```
Expected: all pass.
- [ ] **Step 4: Commit**
```bash
git add src/wf_api/refs.py src/wf_mcp/workflow_surface/refs.py
git commit -m "refactor: move refs to wf_api, leave shim"
```
### Task 2d: Move `next_actions`
**Files:**
- Create: `src/wf_api/next_actions.py`
- Modify: `src/wf_mcp/workflow_surface/next_actions.py` → shim
- [ ] **Step 1: Copy `src/wf_mcp/workflow_surface/next_actions.py` to `src/wf_api/next_actions.py`**
Update the internal import: change `from .wrapper_hints import WrapperAuthoringHints` to `from wf_api.wrapper_hints import WrapperAuthoringHints`.
- [ ] **Step 2: Replace old file with shim**
```python
"""Backward-compatibility shim — canonical location is wf_api.next_actions."""
from wf_api.next_actions import * # noqa: F401,F403
```
- [ ] **Step 3: Verify**
```bash
uv run pytest tests/wf_mcp/workflow_surface/test_next_actions.py -q
```
Expected: all pass.
- [ ] **Step 4: Commit**
```bash
git add src/wf_api/next_actions.py src/wf_mcp/workflow_surface/next_actions.py
git commit -m "refactor: move next_actions to wf_api, leave shim"
```
### Task 2e: Move `runtime_dependencies`
**Files:**
- Create: `src/wf_api/runtime_dependencies.py`
- Modify: `src/wf_mcp/workflow_surface/runtime_dependencies.py` → shim
- [ ] **Step 1: Copy `src/wf_mcp/workflow_surface/runtime_dependencies.py` to `src/wf_api/runtime_dependencies.py`**
Verify: imports only from `wf_artifacts`, `wf_authoring`, `wf_core`, `wf_platform`. No changes needed.
- [ ] **Step 2: Replace old file with shim**
```python
"""Backward-compatibility shim — canonical location is wf_api.runtime_dependencies."""
from wf_api.runtime_dependencies import * # noqa: F401,F403
```
- [ ] **Step 3: Commit**
```bash
git add src/wf_api/runtime_dependencies.py src/wf_mcp/workflow_surface/runtime_dependencies.py
git commit -m "refactor: move runtime_dependencies to wf_api, leave shim"
```
### Task 2f: Move `saved_subgraphs`
**Files:**
- Create: `src/wf_api/saved_subgraphs.py`
- Modify: `src/wf_mcp/workflow_surface/saved_subgraphs.py` → shim
- [ ] **Step 1: Copy `src/wf_mcp/workflow_surface/saved_subgraphs.py` to `src/wf_api/saved_subgraphs.py`**
Update internal imports:
- `from ..models import RawWorkflowPlan``from wf_api.models import RawWorkflowPlan`
- `from .runtime_dependencies import resolve_runtime_dependencies``from wf_api.runtime_dependencies import resolve_runtime_dependencies`
- [ ] **Step 2: Replace old file with shim**
```python
"""Backward-compatibility shim — canonical location is wf_api.saved_subgraphs."""
from wf_api.saved_subgraphs import * # noqa: F401,F403
```
- [ ] **Step 3: Verify**
```bash
uv run pytest tests/wf_mcp/test_saved_subgraphs.py -q
```
Expected: all pass.
- [ ] **Step 4: Commit**
```bash
git add src/wf_api/saved_subgraphs.py src/wf_mcp/workflow_surface/saved_subgraphs.py
git commit -m "refactor: move saved_subgraphs to wf_api, leave shim"
```
### Task 2g: Move `run_lifecycle`
**Files:**
- Create: `src/wf_api/run_lifecycle.py`
- Modify: `src/wf_mcp/workflow_surface/run_lifecycle.py` → shim
- [ ] **Step 1: Copy `src/wf_mcp/workflow_surface/run_lifecycle.py` to `src/wf_api/run_lifecycle.py`**
Update internal import:
- `from .saved_subgraphs import SavedSubgraphTree``from wf_api.saved_subgraphs import SavedSubgraphTree`
- [ ] **Step 2: Replace old file with shim**
```python
"""Backward-compatibility shim — canonical location is wf_api.run_lifecycle."""
from wf_api.run_lifecycle import * # noqa: F401,F403
```
- [ ] **Step 3: Commit**
```bash
git add src/wf_api/run_lifecycle.py src/wf_mcp/workflow_surface/run_lifecycle.py
git commit -m "refactor: move run_lifecycle to wf_api, leave shim"
```
---
## Task 3: Move `events` and `listing` primitives to `wf_api`
### Task 3a: Move `McpEvent` + `make_event` to `wf_api.events`
**Files:**
- Create: `src/wf_api/events.py`
- Modify: `src/wf_mcp/events/models.py` → shim
- [ ] **Step 1: Create `src/wf_api/events.py`**
Copy content from `src/wf_mcp/events/models.py`:
```python
from __future__ import annotations
import time
from dataclasses import dataclass, field
from typing import Any
@dataclass(slots=True)
class McpEvent:
"""Broker-local event record before protocol-specific projection."""
kind: str
timestamp_epoch_ms: int
connection_id: str | None = None
capability_id: str | None = None
workflow_name: str | None = None
payload: dict[str, Any] = field(default_factory=dict)
def make_event(
kind: str,
*,
connection_id: str | None = None,
capability_id: str | None = None,
workflow_name: str | None = None,
payload: dict[str, Any] | None = None,
) -> McpEvent:
"""Create a timestamped event with optional routing metadata."""
return McpEvent(
kind=kind,
timestamp_epoch_ms=int(time.time() * 1000),
connection_id=connection_id,
capability_id=capability_id,
workflow_name=workflow_name,
payload=payload or {},
)
```
- [ ] **Step 2: Replace `src/wf_mcp/events/models.py` with shim**
```python
"""Backward-compatibility shim — canonical location is wf_api.events."""
from wf_api.events import McpEvent, make_event
__all__ = ["McpEvent", "make_event"]
```
- [ ] **Step 3: Verify**
```bash
uv run pytest tests/wf_mcp/test_events.py -q
```
Expected: all pass. `wf_mcp.events.bus` imports `McpEvent` from `.models` which re-exports from `wf_api.events`.
- [ ] **Step 4: Commit**
```bash
git add src/wf_api/events.py src/wf_mcp/events/models.py
git commit -m "refactor: move McpEvent/make_event to wf_api.events, leave shim"
```
### Task 3b: Move `matches_query` + `paged_list_payload` to `wf_api.listing`
**Files:**
- Create: `src/wf_api/listing.py`
- Modify: `src/wf_mcp/shared/listing.py` → shim
- [ ] **Step 1: Create `src/wf_api/listing.py`**
Copy content from `src/wf_mcp/shared/listing.py`:
```python
from __future__ import annotations
from collections.abc import Sequence
from typing import Any, TypeVar
from wf_platform import page_items
T = TypeVar("T")
def matches_query(*values: object, query: str | None) -> bool:
"""Return whether a compact discovery row matches a human search query."""
if query is None:
return True
needle = query.strip().casefold()
if not needle:
return True
return any(needle in str(value).casefold() for value in values if value is not None)
def paged_list_payload(
key: str,
items: Sequence[T],
*,
cursor: str | None,
limit: int,
) -> dict[str, Any]:
"""Build the common workflow-surface list response shape."""
page = page_items(items, cursor=cursor, limit=limit)
return {
key: list(page.items),
"next_cursor": page.next_cursor,
"total": page.total,
}
```
- [ ] **Step 2: Replace `src/wf_mcp/shared/listing.py` with shim**
```python
"""Backward-compatibility shim — canonical location is wf_api.listing."""
from wf_api.listing import matches_query, paged_list_payload
__all__ = ["matches_query", "paged_list_payload"]
```
- [ ] **Step 3: Update `src/wf_mcp/shared/__init__.py`**
Change the import source for `matches_query` and `paged_list_payload` from `.listing` to keep working (the shim handles it, but update for clarity):
```python
from .errors import error_payload, root_exception
from .listing import matches_query, paged_list_payload # routes through shim to wf_api
from .names import (
ADMIN_NAMESPACE,
LdaNamespace,
ProxyNamespace,
ProxyToolName,
connection_id_to_resource_path,
is_admin_tool_name,
namespaced_tool_name,
parse_namespaced_tool_name,
)
from .pagination import clamp_limit, make_cursor, paginate_items, parse_cursor
__all__ = [
"ADMIN_NAMESPACE",
"LdaNamespace",
"ProxyNamespace",
"ProxyToolName",
"connection_id_to_resource_path",
"clamp_limit",
"error_payload",
"is_admin_tool_name",
"make_cursor",
"matches_query",
"namespaced_tool_name",
"paged_list_payload",
"paginate_items",
"parse_cursor",
"parse_namespaced_tool_name",
"root_exception",
]
```
No change needed — `from .listing import ...` still works because the shim re-exports.
- [ ] **Step 4: Commit**
```bash
git add src/wf_api/listing.py src/wf_mcp/shared/listing.py
git commit -m "refactor: move listing helpers to wf_api.listing, leave shim"
```
---
## Task 4: Move `RawWorkflowPlan` and `TraceRange` to `wf_api.models`
**Files:**
- Create: `src/wf_api/models.py`
- Modify: `src/wf_mcp/models.py` → add re-export shim for `RawWorkflowPlan`
- Modify: `src/wf_mcp/workflow_surface/models.py` → update `NextActions` import
- [ ] **Step 1: Create `src/wf_api/models.py`**
```python
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from pydantic import BaseModel, Field
from wf_core import Edge
from wf_core.models.steps import InputBinding, Step
class RawWorkflowPlan(BaseModel):
"""Raw authoring plan using the same graph step and edge models as core."""
name: str
input_schema: dict[str, Any]
state_schema: dict[str, Any]
output_schema: dict[str, Any]
outcomes: list[str] = Field(
default_factory=lambda: ["ok"],
description=(
"Declared public workflow outcomes. Legacy plans without this field "
"default to ok."
),
)
output: list[InputBinding] = Field(
default_factory=list,
description=(
"Optional root workflow output bindings. Sources read graph paths "
"such as state.result and targets write the public output payload."
),
)
start: str
nodes: list[Step]
edges: list[Edge]
@dataclass(frozen=True, slots=True)
class TraceRange:
"""Caller-bounded debug trace slice for durable deployment runs."""
start: int = 0
limit: int = 20
```
Note: `RawWorkflowPlan` content comes from `src/wf_mcp/models.py:45-68`. `TraceRange` content comes from `src/wf_mcp/workflow_surface/models.py` — grep for `class TraceRange` to get the exact definition.
- [ ] **Step 2: Add re-export shim at bottom of `src/wf_mcp/models.py`**
Append to the end of `src/wf_mcp/models.py`:
```python
# Backward-compatibility re-export — canonical location is wf_api.models
from wf_api.models import RawWorkflowPlan as RawWorkflowPlan
```
This shadows the local `RawWorkflowPlan` class. Keep the local class definition for now so other `wf_mcp` code that imports `RawWorkflowPlan` from `wf_mcp.models` continues to work. The re-export ensures `from wf_mcp.models import RawWorkflowPlan` resolves to the `wf_api` version.
**Alternative (cleaner):** Remove the local `RawWorkflowPlan` class from `wf_mcp/models.py` entirely and keep only the re-export. This is preferred if no other `wf_mcp` code defines behavior on the local class.
- [ ] **Step 3: Update `src/wf_mcp/workflow_surface/models.py` imports**
Change line 7 from:
```python
from .next_actions import NextActionPatchExample, NextActions
```
to:
```python
from wf_api.next_actions import NextActionPatchExample, NextActions
```
Also add a re-export for `TraceRange` at the bottom of the file (for `wf_mcp.workflow_surface.__init__.py`):
```python
# Backward-compatibility re-export — canonical location is wf_api.models
from wf_api.models import TraceRange as TraceRange
```
- [ ] **Step 4: Verify**
```bash
uv run pytest tests/wf_mcp/workflow_surface/test_drafts.py tests/wf_mcp/workflow_surface/test_runs.py -q
```
Expected: all pass.
- [ ] **Step 5: Commit**
```bash
git add src/wf_api/models.py src/wf_mcp/models.py src/wf_mcp/workflow_surface/models.py
git commit -m "refactor: move RawWorkflowPlan and TraceRange to wf_api.models"
```
---
## Task 5: Create `WorkflowApiBackend` protocol
**Files:**
- Create: `src/wf_api/backend.py`
- [ ] **Step 1: Write `src/wf_api/backend.py`**
```python
from __future__ import annotations
from collections.abc import Sequence
from typing import Any, Protocol, runtime_checkable
from wf_artifacts import (
DraftWorkspaceStore,
RunStore,
WorkflowArtifact,
WorkflowArtifactCatalogEntry,
WorkflowArtifactStore,
WorkflowDeployment,
)
from wf_authoring import NodeSpec
from wf_platform import CapabilitySource
from .events import McpEvent
@runtime_checkable
class WorkflowApiBackend(Protocol):
"""Protocol that WorkflowApi requires from its host service.
Implementations adapt a concrete service (e.g. WfMcpService) into this
interface so that wf_api never imports wf_mcp.
"""
@property
def artifact_store(self) -> WorkflowArtifactStore | None: ...
@property
def draft_workspace_store(self) -> DraftWorkspaceStore | None: ...
@property
def run_store(self) -> RunStore | None: ...
@property
def capability_sources(self) -> dict[str, CapabilitySource]: ...
def get_qualified_spec(self, qualified_name: str) -> NodeSpec[Any, Any]:
"""Return the executable NodeSpec for a fully-qualified capability name."""
...
def record_event(self, event: McpEvent) -> None:
"""Publish one domain event."""
...
async def run_workflow_from_plan(
self,
plan: Any,
workflow_input: dict[str, Any],
*,
deployment: WorkflowDeployment | None = None,
artifact: WorkflowArtifact | None = None,
saved_subgraph_tree: Any | None = None,
) -> Any:
"""Execute a compiled workflow plan and return the run state."""
...
async def resume_workflow_from_plan(
self,
plan: Any,
run: Any,
*,
resume_payload: dict[str, Any],
resume_outcome: str = "submitted",
deployment: WorkflowDeployment | None = None,
artifact: WorkflowArtifact | None = None,
saved_subgraph_tree: Any | None = None,
) -> Any:
"""Resume a stopped workflow run."""
...
def workflow_artifact_catalog_entry(
self,
artifact: WorkflowArtifact,
) -> WorkflowArtifactCatalogEntry:
"""Project a saved artifact as a planner catalog entry."""
...
async def check_source_liveness(
self,
*,
deployment: WorkflowDeployment,
artifacts: Sequence[WorkflowArtifact],
) -> list[Any]:
"""Run opt-in live connectivity checks for bound upstream sources.
Default implementation returns empty list. MCP adapter overrides
with actual connection/adapter/auth probe.
"""
...
```
- [ ] **Step 2: Verify import**
```bash
uv run python -c "from wf_api.backend import WorkflowApiBackend; print('OK')"
```
Expected: `OK`.
- [ ] **Step 3: Commit**
```bash
git add src/wf_api/backend.py
git commit -m "feat(wf_api): add WorkflowApiBackend protocol"
```
---
## Task 6: Create `WorkflowApi` service
**Files:**
- Create: `src/wf_api/service.py`
- [ ] **Step 1: Write `src/wf_api/service.py`**
This is the largest step. The body is `WorkflowSurfaceHandlers` from `src/wf_mcp/workflow_surface/handlers.py` with these changes:
1. **Class name:** `WorkflowSurfaceHandlers``WorkflowApi`
2. **Constructor:** `self.service: WfMcpService``self.backend: WorkflowApiBackend`
3. **All `self.service.X`**`self.backend.X` (property access)
4. **All `self.service._get_qualified_spec(...)`**`self.backend.get_qualified_spec(...)`
5. **All `self.service._record_event(...)`**`self.backend.record_event(...)`
6. **All `self.service.run_workflow_from_plan(...)`**`self.backend.run_workflow_from_plan(...)`
7. **All `self.service.resume_workflow_from_plan(...)`**`self.backend.resume_workflow_from_plan(...)`
8. **All `self.service.workflow_artifact_catalog_entry(...)`**`self.backend.workflow_artifact_catalog_entry(...)`
9. **`_live_source_diagnostics(self.service, ...)`** → `self.backend.check_source_liveness(deployment=..., artifacts=...)`
10. **`_observed_node_specs(self.service)`** → `_observed_node_specs(self.backend)` (update function signature)
11. **`_available_sources(self.service)`** → `_available_sources(self.backend)` (update function signature)
12. **`_required_capabilities_for_plan(..., service=self.service)`** → `_required_capabilities_for_plan(..., backend=self.backend)`
**Import changes** (top of file):
- Remove: `from ..broker.service.adapters import require_adapter`
- Remove: `from ..events import make_event`
- Remove: `from ..models import RawWorkflowPlan`
- Remove: `from ..shared import matches_query, paged_list_payload`
- Remove: `if TYPE_CHECKING: from ..broker.service import WfMcpService`
- Remove: `import anyio`, `import httpx`, `from mcp.client.streamable_http import StreamableHTTPError`, `from mcp.shared.exceptions import McpError`
- Remove: `LIVE_SOURCE_CHECK_TIMEOUT_SECONDS`, `_LIVE_SOURCE_CHECK_FAILURES`
- Add: `from .backend import WorkflowApiBackend`
- Add: `from .events import make_event`
- Add: `from .listing import matches_query, paged_list_payload`
- Add: `from .models import RawWorkflowPlan`
- Update all `.constants`, `.models`, `.refs`, `.next_actions`, `.saved_subgraphs`, `.run_lifecycle`, `.wrapper_hints` to `wf_api.*`
**Module-level helpers to update:**
- `_available_sources(service: WfMcpService)``_available_sources(backend: WorkflowApiBackend)` — change `service.capability_sources` to `backend.capability_sources`
- `_observed_node_specs(service: WfMcpService)``_observed_node_specs(backend: WorkflowApiBackend)` — change `service.capability_sources` to `backend.capability_sources`
- `_required_capabilities_for_plan(..., service: WfMcpService)``_required_capabilities_for_plan(..., backend: WorkflowApiBackend)` — change `service` references to `backend`
- **Remove** `_live_source_diagnostics` function entirely (it moves into the adapter)
- **Remove** `_required_live_sources` function (it moves into the adapter)
- **Remove** `LIVE_SOURCE_CHECK_TIMEOUT_SECONDS` and `_LIVE_SOURCE_CHECK_FAILURES` constants
**`validate_deployment` method change:**
```python
async def validate_deployment(
self,
*,
deployment_id: str,
live_check: bool = False,
) -> dict[str, Any]:
deployment, artifact, diagnostics, tree = self._deployment_validation(
deployment_id
)
if live_check:
diagnostics.extend(
await self.backend.check_source_liveness(
deployment=deployment,
artifacts=[artifact, *tree.artifacts_by_ref.values()],
)
)
# ... rest unchanged
```
- [ ] **Step 2: Verify import**
```bash
uv run python -c "from wf_api.service import WorkflowApi; print('OK')"
```
Expected: `OK`.
- [ ] **Step 3: Commit**
```bash
git add src/wf_api/service.py
git commit -m "feat(wf_api): add WorkflowApi service"
```
---
## Task 7: Replace `handlers.py` with shim
**Files:**
- Modify: `src/wf_mcp/workflow_surface/handlers.py`
- [ ] **Step 1: Replace entire file with shim**
```python
"""Backward-compatibility shim — canonical location is wf_api.service."""
from wf_api.service import WorkflowApi as WorkflowSurfaceHandlers
__all__ = ["WorkflowSurfaceHandlers"]
```
- [ ] **Step 2: Verify all workflow surface tests pass**
```bash
uv run pytest tests/wf_mcp/workflow_surface/ -q
```
Expected: all pass. Tests import `WorkflowSurfaceHandlers` from `wf_mcp.workflow_surface` which re-exports from `wf_api.service`.
- [ ] **Step 3: Commit**
```bash
git add src/wf_mcp/workflow_surface/handlers.py
git commit -m "refactor: replace handlers.py with shim to wf_api.service"
```
---
## Task 8: Create `WfMcpWorkflowApiBackend` adapter
**Files:**
- Create: `src/wf_mcp/broker/service/workflow_api_backend.py`
- [ ] **Step 1: Write `src/wf_mcp/broker/service/workflow_api_backend.py`**
```python
from __future__ import annotations
import asyncio
from collections.abc import Sequence
from typing import Any
import anyio
import httpx
from mcp.client.streamable_http import StreamableHTTPError
from mcp.shared.exceptions import McpError
from wf_api.backend import WorkflowApiBackend
from wf_api.events import McpEvent
from wf_artifacts import (
DependencyDiagnostic,
DiagnosticSeverity,
DraftWorkspaceStore,
RunStore,
WorkflowArtifact,
WorkflowArtifactCatalogEntry,
WorkflowArtifactStore,
WorkflowDeployment,
)
from wf_authoring import NodeSpec
from wf_platform import CapabilitySource
from .adapters import require_adapter
from .core import WfMcpService
LIVE_SOURCE_CHECK_TIMEOUT_SECONDS = 8.0
_LIVE_SOURCE_CHECK_FAILURES = (
KeyError,
TimeoutError,
OSError,
anyio.ClosedResourceError,
anyio.EndOfStream,
anyio.BrokenResourceError,
httpx.HTTPError,
McpError,
StreamableHTTPError,
)
class WfMcpWorkflowApiBackend:
"""Adapt WfMcpService into the WorkflowApiBackend protocol."""
def __init__(self, service: WfMcpService) -> None:
self._service = service
@property
def artifact_store(self) -> WorkflowArtifactStore | None:
return self._service.artifact_store
@property
def draft_workspace_store(self) -> DraftWorkspaceStore | None:
return self._service.draft_workspace_store
@property
def run_store(self) -> RunStore | None:
return self._service.run_store
@property
def capability_sources(self) -> dict[str, CapabilitySource]:
return self._service.capability_sources
def get_qualified_spec(self, qualified_name: str) -> NodeSpec[Any, Any]:
return self._service._get_qualified_spec(qualified_name)
def record_event(self, event: McpEvent) -> None:
self._service._record_event(event)
async def run_workflow_from_plan(
self,
plan: Any,
workflow_input: dict[str, Any],
*,
deployment: WorkflowDeployment | None = None,
artifact: WorkflowArtifact | None = None,
saved_subgraph_tree: Any | None = None,
) -> Any:
return await self._service.run_workflow_from_plan(
plan,
workflow_input,
deployment=deployment,
artifact=artifact,
saved_subgraph_tree=saved_subgraph_tree,
)
async def resume_workflow_from_plan(
self,
plan: Any,
run: Any,
*,
resume_payload: dict[str, Any],
resume_outcome: str = "submitted",
deployment: WorkflowDeployment | None = None,
artifact: WorkflowArtifact | None = None,
saved_subgraph_tree: Any | None = None,
) -> Any:
return await self._service.resume_workflow_from_plan(
plan,
run,
resume_payload=resume_payload,
resume_outcome=resume_outcome,
deployment=deployment,
artifact=artifact,
saved_subgraph_tree=saved_subgraph_tree,
)
def workflow_artifact_catalog_entry(
self,
artifact: WorkflowArtifact,
) -> WorkflowArtifactCatalogEntry:
return self._service.workflow_artifact_catalog_entry(artifact)
async def check_source_liveness(
self,
*,
deployment: WorkflowDeployment,
artifacts: Sequence[WorkflowArtifact],
) -> list[DependencyDiagnostic]:
return await _live_source_diagnostics(
self._service,
deployment=deployment,
artifacts=artifacts,
)
async def _live_source_diagnostics(
service: WfMcpService,
*,
deployment: WorkflowDeployment,
artifacts: Sequence[WorkflowArtifact],
) -> list[DependencyDiagnostic]:
"""MCP-specific live connectivity probe for bound upstream sources."""
diagnostics: list[DependencyDiagnostic] = []
bindings = deployment.binding_map()
required: dict[str, str] = {}
for artifact in artifacts:
for logical_ref, capability in artifact.required_capability_map().items():
source_id = bindings.get(capability.logical_source)
if source_id is not None:
required.setdefault(source_id, logical_ref)
for source_id, logical_ref in required.items():
source = service.capability_sources.get(source_id)
if (
source is None
or not source.enabled
or not source.permissions.calls_upstream
):
continue
try:
connection = service.connections.get(source_id)
adapter = require_adapter(connection, service.adapters)
auth = service.load_auth(source_id)
await asyncio.wait_for(
adapter.list_tools(connection, auth),
timeout=LIVE_SOURCE_CHECK_TIMEOUT_SECONDS,
)
except _LIVE_SOURCE_CHECK_FAILURES as exc:
diagnostics.append(
DependencyDiagnostic(
severity=DiagnosticSeverity.ERROR,
code="source_unreachable",
logical_ref=logical_ref,
bound_source=source_id,
message=(
f"Live check for upstream source {source_id!r} failed: "
f"{type(exc).__name__}: {exc}"
),
repair_hint=(
"Start or reconnect the source, fix its transport/auth "
"configuration, or bind this deployment to another source."
),
)
)
return diagnostics
```
- [ ] **Step 2: Verify import**
```bash
uv run python -c "from wf_mcp.broker.service.workflow_api_backend import WfMcpWorkflowApiBackend; print('OK')"
```
Expected: `OK`.
- [ ] **Step 3: Commit**
```bash
git add src/wf_mcp/broker/service/workflow_api_backend.py
git commit -m "feat(wf_mcp): add WfMcpWorkflowApiBackend adapter"
```
---
## Task 9: Update MCP tool registration to use `WorkflowApi`
**Files:**
- Modify: `src/wf_mcp/workflow_surface/tools.py`
- [ ] **Step 1: Update imports in `src/wf_mcp/workflow_surface/tools.py`**
Change:
```python
from wf_mcp.broker.service import WfMcpService
from .handlers import WorkflowSurfaceHandlers
from .models import (
...,
TraceRange,
...
)
```
To:
```python
from wf_api.service import WorkflowApi
from wf_api.models import TraceRange
from wf_mcp.broker.service import WfMcpService
from wf_mcp.broker.service.workflow_api_backend import WfMcpWorkflowApiBackend
from .models import (
...,
# Remove TraceRange from this import
...
)
```
- [ ] **Step 2: Update `register_workflow_tools` function body**
Change:
```python
def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None:
"""Register stable workflow tools on the public MCP server surface."""
handlers = WorkflowSurfaceHandlers(service)
```
To:
```python
def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None:
"""Register stable workflow tools on the public MCP server surface."""
handlers = WorkflowApi(WfMcpWorkflowApiBackend(service))
```
All references to `handlers.X(...)` remain unchanged — `WorkflowApi` has the same methods.
- [ ] **Step 3: Verify all workflow surface tests pass**
```bash
uv run pytest tests/wf_mcp/workflow_surface/ -q
```
Expected: all pass.
- [ ] **Step 4: Commit**
```bash
git add src/wf_mcp/workflow_surface/tools.py
git commit -m "refactor: use WorkflowApi in MCP tool registration"
```
---
## Task 10: Update `wf_cli.context` to use `WorkflowApi`
**Files:**
- Modify: `src/wf_cli/context.py`
- [ ] **Step 1: Update imports**
Change:
```python
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
```
To:
```python
from wf_api import WorkflowApi
from wf_mcp.broker.service.workflow_api_backend import WfMcpWorkflowApiBackend
```
- [ ] **Step 2: Update `CliContext` dataclass**
Change:
```python
@dataclass(frozen=True)
class CliContext:
config_path: Path
service: WfMcpService
handlers: WorkflowSurfaceHandlers
```
To:
```python
@dataclass(frozen=True)
class CliContext:
config_path: Path
service: WfMcpService
handlers: WorkflowApi
```
- [ ] **Step 3: Update `load_cli_context` function**
Change:
```python
def load_cli_context(config_path: str | Path) -> CliContext:
resolved_config_path = Path(config_path)
config = load_broker_config(resolved_config_path)
service = build_service_from_config(config)
return CliContext(
config_path=resolved_config_path,
service=service,
handlers=WorkflowSurfaceHandlers(service),
)
```
To:
```python
def load_cli_context(config_path: str | Path) -> CliContext:
resolved_config_path = Path(config_path)
config = load_broker_config(resolved_config_path)
service = build_service_from_config(config)
return CliContext(
config_path=resolved_config_path,
service=service,
handlers=WorkflowApi(WfMcpWorkflowApiBackend(service)),
)
```
- [ ] **Step 4: Update `src/wf_cli/commands/runs.py`**
Change:
```python
from wf_mcp.workflow_surface import TraceRange
```
To:
```python
from wf_api.models import TraceRange
```
- [ ] **Step 5: Verify CLI tests pass**
```bash
uv run pytest tests/wf_cli/ -q
```
Expected: all pass.
- [ ] **Step 6: Commit**
```bash
git add src/wf_cli/context.py src/wf_cli/commands/runs.py
git commit -m "refactor: use WorkflowApi in CLI context"
```
---
## Task 11: Add `wf_api` to `pyproject.toml` packages
**Files:**
- Modify: `pyproject.toml`
- [ ] **Step 1: Find the packages list in `pyproject.toml`**
Grep for `packages` or `find` in the `[tool.setuptools]` or `[tool.hatch]` section.
- [ ] **Step 2: Add `src/wf_api` to the packages list**
If using `find`:
```toml
[tool.setuptools.packages.find]
where = ["src"]
```
This should auto-discover `wf_api`. If packages are listed explicitly, add `"wf_api"`.
- [ ] **Step 3: Verify package is importable**
```bash
uv run python -c "from wf_api import WorkflowApi, WorkflowApiBackend; print('OK')"
```
Expected: `OK`.
- [ ] **Step 4: Commit**
```bash
git add pyproject.toml
git commit -m "build: add wf_api to package discovery"
```
---
## Task 12: Full test suite verification
- [ ] **Step 1: Run all workflow surface tests**
```bash
uv run pytest tests/wf_mcp/workflow_surface/ -q
```
Expected: all pass.
- [ ] **Step 2: Run all CLI tests**
```bash
uv run pytest tests/wf_cli/ -q
```
Expected: all pass.
- [ ] **Step 3: Run all MCP tests**
```bash
uv run pytest tests/wf_mcp/ -q
```
Expected: all pass.
- [ ] **Step 4: Run full test suite**
```bash
uv run pytest -q
```
Expected: all pass.
- [ ] **Step 5: Verify import direction rule**
```bash
uv run python -c "
import ast, sys
violations = []
for mod in ['wf_api.service', 'wf_api.backend', 'wf_api.events', 'wf_api.listing',
'wf_api.models', 'wf_api.constants', 'wf_api.refs', 'wf_api.next_actions',
'wf_api.wrapper_hints', 'wf_api.runtime_dependencies', 'wf_api.saved_subgraphs',
'wf_api.run_lifecycle']:
try:
file_path = mod.replace('.', '/') + '.py'
with open(file_path) as f:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module and node.module.startswith('wf_mcp'):
violations.append(f'{mod}: from {node.module} import ...')
except FileNotFoundError:
pass
if violations:
print('VIOLATIONS:')
for v in violations:
print(f' {v}')
sys.exit(1)
else:
print('OK: wf_api has no wf_mcp imports')
"
```
Expected: `OK: wf_api has no wf_mcp imports`.
- [ ] **Step 6: Run lint/typecheck**
```bash
uv run ruff check src/wf_api/ src/wf_mcp/workflow_surface/ src/wf_cli/context.py src/wf_mcp/broker/service/workflow_api_backend.py
uv run basedpyright --level error
```
Expected: no new errors.
---
## Task 13: Final commit — all changes
- [ ] **Step 1: Verify clean working tree**
```bash
git status
```
Expected: no uncommitted changes (all tasks committed individually).
- [ ] **Step 2: Verify commit log**
```bash
git log --oneline -15
```
Expected: all task commits visible.
---
## Rollback / Compatibility Notes
1. **Shim-based rollback:** Every moved module has a shim in its original location. To rollback, replace each shim with the original file content and delete the `wf_api/` package.
2. **`CliContext.handlers` attribute name preserved:** All CLI command code uses `context.handlers.X()`. The attribute name `handlers` is unchanged; only the type changes from `WorkflowSurfaceHandlers` to `WorkflowApi`.
3. **`wf_mcp.workflow_surface.__init__.py` re-exports preserved:** `from wf_mcp.workflow_surface import WorkflowSurfaceHandlers` still works via the `handlers.py` shim. Same for `TraceRange`.
4. **Test monkeypatch paths preserved:** Tests that `patch("wf_cli.commands.runs.load_cli_context", ...)` still work because `load_cli_context` is still the function being patched. The patched function constructs `WorkflowApi` instead of `WorkflowSurfaceHandlers`, but the mock replaces the whole function.
5. **`WfMcpService` unchanged:** The `WfMcpService` class itself is not modified. The adapter wraps it without changing its behavior.
6. **`live_check` behavior preserved:** `validate_deployment(live_check=True)` calls `self.backend.check_source_liveness(...)`. The MCP adapter implements this with the same `_live_source_diagnostics` logic (connections, adapters, auth probe). Non-MCP backends can return an empty list or implement their own liveness checks.
7. **Event naming unchanged:** `McpEvent` keeps its name. Renaming to `DomainEvent` is a Slice 2 concern.
---
## Verification Commands Summary
```bash
# Per-task verification
uv run pytest tests/wf_mcp/workflow_surface/ -q
uv run pytest tests/wf_cli/ -q
uv run pytest tests/wf_mcp/ -q
# Full suite
uv run pytest -q
# Import direction check
uv run python -c "
import ast, sys
violations = []
for mod in ['wf_api.service', 'wf_api.backend', 'wf_api.events', 'wf_api.listing',
'wf_api.models', 'wf_api.constants', 'wf_api.refs', 'wf_api.next_actions',
'wf_api.wrapper_hints', 'wf_api.runtime_dependencies', 'wf_api.saved_subgraphs',
'wf_api.run_lifecycle']:
try:
file_path = mod.replace('.', '/') + '.py'
with open(file_path) as f:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module and node.module.startswith('wf_mcp'):
violations.append(f'{mod}: from {node.module} import ...')
except FileNotFoundError:
pass
if violations:
print('VIOLATIONS:'); [print(f' {v}') for v in violations]; sys.exit(1)
else:
print('OK: wf_api has no wf_mcp imports')
"
# Lint
uv run ruff check src/wf_api/
uv run basedpyright --level error
```