Files
lda-wf/docs/historical/superpowers/plans/2026-06-01-wf-api-slice-4a-operation-context.md
T

536 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# wf_api Slice 4A: Operation Context Scaffolding 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 a protocol-neutral workflow operation context seam so future domain services can be extracted from `WorkflowSurfaceHandlers` without depending on `WfMcpService`.
**Architecture:** This is scaffolding only. `wf_api.operation_context` defines small protocols and a `WorkflowOperationContext` dataclass for stores, capability sources, event recording, workflow runtime operations, and optional live source checking. `wf_mcp.broker.service.workflow_operation_context` adapts the current `WfMcpService` into that context. No workflow handler methods move in this slice and no public payloads change.
**Tech Stack:** Python 3.14+, `typing.Protocol`, dataclasses, existing `wf_artifacts`, `wf_platform`, `wf_authoring`, `wf_core`, pytest, ruff, basedpyright.
---
## Scope
### In Scope
- Create `src/wf_api/operation_context.py`.
- Define focused protocols instead of a new god object.
- Create `src/wf_mcp/broker/service/workflow_operation_context.py` to adapt `WfMcpService`.
- Add tests proving:
- `wf_api.operation_context` imports no `wf_mcp`.
- a `WfMcpService` can be adapted into `WorkflowOperationContext`.
- context stores/sources point to the existing service objects.
- event recording and runtime methods delegate to the service.
- Add docstrings explaining this is scaffolding for later domain splits.
### Out Of Scope
- Do not move methods out of `WorkflowSurfaceHandlers`.
- Do not change `WorkflowApiBackend`.
- Do not change MCP tool request/response models.
- Do not change public payloads.
- Do not rename `WorkflowSurfaceHandlers`.
- Do not add FastAPI/HTTP.
- Do not hide or remove compatibility shims.
---
## Design
### New `wf_api.operation_context`
The context is not a service locator. It is the smallest explicit set of
capabilities that extracted domain services will need.
Protocol groups:
- `WorkflowEventRecorder`: record one lifecycle event object.
- `WorkflowSpecProvider`: look up a qualified node spec and expose capability sources.
- `WorkflowArtifactCataloger`: produce saved artifact catalog entries.
- `WorkflowRuntimeRunner`: run and resume compiled workflow plans.
- `WorkflowLiveSourceChecker`: optional live-source validation hook.
Dataclass:
```python
@dataclass(frozen=True, slots=True)
class WorkflowOperationContext:
artifact_store: WorkflowArtifactStore | None
draft_workspace_store: DraftWorkspaceStore | None
run_store: RunStore | None
capability_sources: Mapping[str, CapabilitySource]
events: WorkflowEventRecorder
specs: WorkflowSpecProvider
artifacts: WorkflowArtifactCataloger
runtime: WorkflowRuntimeRunner
live_sources: WorkflowLiveSourceChecker | None = None
```
This will look somewhat broad, but each field is a small protocol or simple
store reference. If implementation pressure makes this mirror all of
`WfMcpService`, stop and split protocols further.
### MCP Adapter
`wf_mcp.broker.service.workflow_operation_context.context_from_service(service)`
builds the context from the current `WfMcpService`.
This adapter may import `wf_mcp`; `wf_api` must not.
---
## Task 1: Add `wf_api.operation_context`
**Files:**
- Create: `src/wf_api/operation_context.py`
- [ ] **Step 1: Create `src/wf_api/operation_context.py`**
Write this file:
```python
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any, Protocol
from wf_artifacts import (
DraftWorkspaceStore,
RunStore,
WorkflowArtifact,
WorkflowArtifactCatalogEntry,
WorkflowArtifactStore,
WorkflowDeployment,
)
from wf_authoring import AsyncRegistryHandler
from wf_core import AsyncNodeHandler, RunState, Workflow
from wf_core.runtime.ops.merges import ReducerDefinition
from wf_platform import CapabilitySource
from .models import RawWorkflowPlan
from .saved_subgraphs import SavedSubgraphTree
class WorkflowEventRecorder(Protocol):
"""Records workflow lifecycle events without exposing MCP event types."""
def record_event(self, event: object) -> None:
"""Record one event object supplied by an adapter-owned event factory."""
class WorkflowSpecProvider(Protocol):
"""Provides planner-visible capability sources and qualified node specs."""
@property
def capability_sources(self) -> Mapping[str, CapabilitySource]:
"""Planner-visible capability sources keyed by source id."""
def get_qualified_spec(self, qualified_name: str) -> object:
"""Return the node spec for one fully qualified capability name."""
class WorkflowArtifactCataloger(Protocol):
"""Formats saved workflow artifacts for list/detail surfaces."""
def workflow_artifact_catalog_entry(
self, artifact: WorkflowArtifact
) -> WorkflowArtifactCatalogEntry:
"""Return the catalog entry representation for one saved artifact."""
class WorkflowRuntimeRunner(Protocol):
"""Runs and resumes workflow plans using an adapter-owned runtime backend."""
async def run_workflow_from_plan(
self,
plan: RawWorkflowPlan,
*,
workflow_input: dict[str, Any],
node_name_bindings: dict[str, str] | None = None,
registry: dict[str, AsyncRegistryHandler] | None = None,
reducers: dict[str, ReducerDefinition] | None = None,
prepared_subgraphs: dict[str, object] | None = None,
) -> RunState:
"""Execute one raw workflow plan and return its run state."""
async def resume_workflow_from_plan(
self,
plan: RawWorkflowPlan,
*,
run: RunState,
resume_payload: dict[str, Any] | None,
resume_outcome: str,
node_name_bindings: dict[str, str] | None = None,
registry: dict[str, AsyncRegistryHandler] | None = None,
reducers: dict[str, ReducerDefinition] | None = None,
prepared_subgraphs: dict[str, object] | None = None,
) -> RunState:
"""Resume one interrupted raw workflow plan and return its run state."""
class WorkflowLiveSourceChecker(Protocol):
"""Optional hook for validating live external source availability."""
async def available_sources(self) -> list[object]:
"""Return source availability records understood by the caller."""
@dataclass(frozen=True, slots=True)
class WorkflowOperationContext:
"""Protocol-neutral dependencies needed by workflow API operations.
This is scaffolding for splitting the large MCP-backed handler into domain
services. Keep this shape explicit; do not add arbitrary access to the whole
MCP service.
"""
artifact_store: WorkflowArtifactStore | None
draft_workspace_store: DraftWorkspaceStore | None
run_store: RunStore | None
capability_sources: Mapping[str, CapabilitySource]
events: WorkflowEventRecorder
specs: WorkflowSpecProvider
artifacts: WorkflowArtifactCataloger
runtime: WorkflowRuntimeRunner
live_sources: WorkflowLiveSourceChecker | None = None
__all__ = [
"WorkflowArtifactCataloger",
"WorkflowEventRecorder",
"WorkflowLiveSourceChecker",
"WorkflowOperationContext",
"WorkflowRuntimeRunner",
"WorkflowSpecProvider",
]
```
- [ ] **Step 2: Run import smoke check**
```powershell
uv run python -c "from wf_api.operation_context import WorkflowOperationContext; print(WorkflowOperationContext.__name__)"
```
Expected:
```text
WorkflowOperationContext
```
---
## Task 2: Re-export Operation Context Types
**Files:**
- Modify: `src/wf_api/__init__.py`
- [ ] **Step 1: Add imports**
Add:
```python
from .operation_context import (
WorkflowArtifactCataloger,
WorkflowEventRecorder,
WorkflowLiveSourceChecker,
WorkflowOperationContext,
WorkflowRuntimeRunner,
WorkflowSpecProvider,
)
```
- [ ] **Step 2: Add names to `__all__`**
Add:
```python
"WorkflowArtifactCataloger",
"WorkflowEventRecorder",
"WorkflowLiveSourceChecker",
"WorkflowOperationContext",
"WorkflowRuntimeRunner",
"WorkflowSpecProvider",
```
- [ ] **Step 3: Run top-level import smoke check**
```powershell
uv run python -c "from wf_api import WorkflowOperationContext, WorkflowRuntimeRunner; print(WorkflowOperationContext.__name__, WorkflowRuntimeRunner.__name__)"
```
Expected:
```text
WorkflowOperationContext WorkflowRuntimeRunner
```
---
## Task 3: Add MCP Adapter For Operation Context
**Files:**
- Create: `src/wf_mcp/broker/service/workflow_operation_context.py`
- [ ] **Step 1: Create adapter file**
Write this file:
```python
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from wf_api.operation_context import (
WorkflowArtifactCataloger,
WorkflowEventRecorder,
WorkflowLiveSourceChecker,
WorkflowOperationContext,
WorkflowRuntimeRunner,
WorkflowSpecProvider,
)
from .core import WfMcpService
@dataclass(frozen=True, slots=True)
class WfMcpWorkflowEventRecorder(WorkflowEventRecorder):
"""Adapter-owned event recorder backed by WfMcpService."""
service: WfMcpService
def record_event(self, event: object) -> None:
self.service._record_event(event) # noqa: SLF001
@dataclass(frozen=True, slots=True)
class WfMcpWorkflowSpecProvider(WorkflowSpecProvider):
"""Adapter-owned spec provider backed by WfMcpService."""
service: WfMcpService
@property
def capability_sources(self):
return self.service.capability_sources
def get_qualified_spec(self, qualified_name: str) -> object:
return self.service._get_qualified_spec(qualified_name) # noqa: SLF001
@dataclass(frozen=True, slots=True)
class WfMcpWorkflowArtifactCataloger(WorkflowArtifactCataloger):
"""Adapter-owned artifact catalog formatter backed by WfMcpService."""
service: WfMcpService
def workflow_artifact_catalog_entry(self, artifact):
return self.service.workflow_artifact_catalog_entry(artifact)
@dataclass(frozen=True, slots=True)
class WfMcpWorkflowRuntimeRunner(WorkflowRuntimeRunner):
"""Adapter-owned runtime runner backed by WfMcpService."""
service: WfMcpService
async def run_workflow_from_plan(self, plan, **kwargs):
return await self.service.run_workflow_from_plan(plan, **kwargs)
async def resume_workflow_from_plan(self, plan, **kwargs):
return await self.service.resume_workflow_from_plan(plan, **kwargs)
@dataclass(frozen=True, slots=True)
class WfMcpWorkflowLiveSourceChecker(WorkflowLiveSourceChecker):
"""Placeholder live source checker; real live checks remain in handlers today."""
service: WfMcpService
async def available_sources(self) -> list[object]:
# Existing live source availability logic still lives near handlers.
# Slice 4A only creates the seam; it does not move live-check behavior.
return []
def context_from_service(service: WfMcpService) -> WorkflowOperationContext:
"""Adapt the current MCP service stack into a protocol-neutral context."""
specs = WfMcpWorkflowSpecProvider(service)
return WorkflowOperationContext(
artifact_store=service.artifact_store,
draft_workspace_store=service.draft_workspace_store,
run_store=service.run_store,
capability_sources=specs.capability_sources,
events=WfMcpWorkflowEventRecorder(service),
specs=specs,
artifacts=WfMcpWorkflowArtifactCataloger(service),
runtime=WfMcpWorkflowRuntimeRunner(service),
live_sources=WfMcpWorkflowLiveSourceChecker(service),
)
__all__ = [
"WfMcpWorkflowArtifactCataloger",
"WfMcpWorkflowEventRecorder",
"WfMcpWorkflowLiveSourceChecker",
"WfMcpWorkflowRuntimeRunner",
"WfMcpWorkflowSpecProvider",
"context_from_service",
]
```
- [ ] **Step 2: Run adapter import smoke check**
```powershell
uv run python -c "from wf_mcp.broker.service.workflow_operation_context import context_from_service; print(context_from_service.__name__)"
```
Expected:
```text
context_from_service
```
---
## Task 4: Add Focused Tests
**Files:**
- Create: `tests/wf_api/test_operation_context.py`
- [ ] **Step 1: Write tests**
Write this file:
```python
from __future__ import annotations
import ast
import json
from pathlib import Path
from wf_api.operation_context import WorkflowOperationContext
from wf_cli.context import load_cli_context
from wf_mcp.broker.service.workflow_operation_context import context_from_service
def test_wf_api_operation_context_imports_no_wf_mcp() -> None:
path = Path(__file__).resolve().parents[2] / "src" / "wf_api" / "operation_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.startswith("wf_mcp"):
violations.append(f"{node.lineno}: from {node.module} import ...")
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name.startswith("wf_mcp"):
violations.append(f"{node.lineno}: import {alias.name}")
assert violations == []
def test_context_from_service_exposes_existing_store_objects(tmp_path: Path) -> None:
config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text(
json.dumps(
{
"store_root": ".wf_mcp_store",
"connections": [
{
"id": "demo.personal",
"server": "demo",
"account": "personal",
}
],
}
),
encoding="utf-8",
)
cli_context = load_cli_context(config_path)
operation_context = context_from_service(cli_context.service)
assert isinstance(operation_context, WorkflowOperationContext)
assert operation_context.artifact_store is cli_context.service.artifact_store
assert operation_context.draft_workspace_store is cli_context.service.draft_workspace_store
assert operation_context.run_store is cli_context.service.run_store
assert operation_context.capability_sources is cli_context.service.capability_sources
def test_context_from_service_delegates_specs_and_events(tmp_path: Path) -> None:
config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text(
json.dumps({"store_root": ".wf_mcp_store", "connections": []}),
encoding="utf-8",
)
cli_context = load_cli_context(config_path)
operation_context = context_from_service(cli_context.service)
event = object()
operation_context.events.record_event(event)
assert cli_context.service.events[-1] is event
```
- [ ] **Step 2: Run focused tests**
```powershell
uv run pytest tests/wf_api/test_operation_context.py tests/wf_api/test_import_direction.py -q
```
Expected: all pass.
---
## Task 5: Verification
- [ ] **Step 1: Run focused tests**
```powershell
uv run pytest tests/wf_api/test_operation_context.py tests/wf_api/test_import_direction.py tests/wf_cli/test_context.py -q
```
Expected: all pass.
- [ ] **Step 2: Run ruff on touched files**
```powershell
uv run ruff check src/wf_api/operation_context.py src/wf_api/__init__.py src/wf_mcp/broker/service/workflow_operation_context.py tests/wf_api/test_operation_context.py
```
Expected: all checks pass.
- [ ] **Step 3: Run basedpyright on touched files**
```powershell
uv run basedpyright --level error src/wf_api/operation_context.py src/wf_api/__init__.py src/wf_mcp/broker/service/workflow_operation_context.py tests/wf_api/test_operation_context.py
```
Expected: `0 errors`.
- [ ] **Step 4: Optional full suite**
```powershell
uv run pytest -q
```
Expected: full suite passes with the projects existing skipped/xfailed counts.
---
## Self-Review Checklist
- `wf_api.operation_context` imports no `wf_mcp`.
- The context is not a broad wrapper around all of `WfMcpService`.
- No `WorkflowSurfaceHandlers` method body moved.
- No public payload changed.
- MCP-owned adapter code is the only new code that imports `WfMcpService`.
- Live source behavior remains unchanged; the live-source protocol is scaffolding only.