Files
lda-wf/docs/historical/superpowers/plans/2026-06-02-wf-api-slice-4c-artifacts-deployments.md
T

683 lines
19 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 4C: Artifacts And Deployments 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:** Move saved artifact and deployment operations out of `WorkflowSurfaceHandlers` into protocol-neutral `wf_api` domain services.
**Architecture:** Add `WorkflowArtifactApi` and `WorkflowDeploymentApi` that depend on `WorkflowOperationContext`, not `WfMcpService`. Keep `WorkflowSurfaceHandlers` public method signatures unchanged and delegate artifact/deployment methods to the new services. Extend operation-context protocols for event emission and live source checks so `wf_api` does not import MCP event factories, adapters, auth, or connections.
**Tech Stack:** Python 3.14+, `wf_api.operation_context`, `wf_api.drafts`, `wf_artifacts`, `wf_platform`, `wf_core`, pytest, ruff, basedpyright.
---
## Scope
### Move In This Slice
Move these methods from `WorkflowSurfaceHandlers`:
```text
list_artifacts
save_artifact
create_artifact_from_plan
create_artifact_from_draft
create_artifact_from_workspace
create_wrapper_from_workspace
inspect_artifact
list_deployments
inspect_deployment
save_deployment
delete_deployment
validate_deployment
```
Move or duplicate only the helpers required by those methods:
```text
_available_sources
_suggested_self_bindings
_observed_node_specs
_capability_name
_artifact_capability_id
_deployment_summary
```
### Do Not Move In This Slice
Do not move:
```text
list_capabilities
inspect_capability
call_capability
_wrapper_artifact_for_capability_name
_wrapper_capability_summaries
_wrapper_capability_detail
_call_wrapper_artifact
run_deployment
resume_run
inspect_run
read_run_trace
_raw_plan_from_artifact
_run_payload
_interrupt_payload
```
Reasons:
- Capability methods still combine live source specs, saved wrappers, and direct test calls. Move them in Slice 4E.
- Run methods depend on durable run checkpoints, runtime preparation, trace slicing, and saved subgraph execution. Move them in Slice 4D.
- `_raw_plan_from_artifact` is still needed by wrapper capability calls and run methods. Leave it in `handlers.py` until those domains move or extract it separately.
### Invariants
- No public payload changes.
- No MCP tool schema changes.
- `WorkflowSurfaceHandlers` still exposes the same methods.
- `wf_api` imports no `wf_mcp`.
- Event construction stays adapter-owned.
- Live upstream checks stay adapter-owned.
- Temporary private helper duplication is allowed when capability/run methods still need a helper in `handlers.py`.
---
## Task 1: Extend Operation Context For Events And Live Checks
**Files:**
- Modify: `src/wf_api/operation_context.py`
- Create: `src/wf_mcp/broker/service/workflow_live_checks.py`
- Modify: `src/wf_mcp/broker/service/workflow_operation_context.py`
- Modify: `src/wf_mcp/workflow_surface/handlers.py`
- Test: `tests/wf_api/test_operation_context.py`
- [ ] **Step 1: Add protocol methods**
In `src/wf_api/operation_context.py`, update `WorkflowEventRecorder`:
```python
class WorkflowEventRecorder(Protocol):
"""Records workflow lifecycle events without exposing MCP event types."""
def record_event(self, event: object) -> None:
"""Record one adapter-native event object."""
...
def record_workflow_event(
self,
event_type: str,
*,
capability_id: str,
payload: dict[str, Any],
) -> None:
"""Record one workflow lifecycle event by protocol-neutral fields."""
...
```
Update `WorkflowLiveSourceChecker`:
```python
class WorkflowLiveSourceChecker(Protocol):
"""Optional hook for validating live external source availability."""
async def deployment_diagnostics(
self,
*,
deployment: WorkflowDeployment,
artifacts: Sequence[WorkflowArtifact],
) -> list[DependencyDiagnostic]:
"""Return opt-in live-source diagnostics for a deployment tree."""
...
```
Add imports:
```python
from collections.abc import Mapping, Sequence
from wf_artifacts import DependencyDiagnostic, WorkflowDeployment
```
Remove or keep `available_sources()` only if still used by tests. New moved code should use `deployment_diagnostics(...)`.
- [ ] **Step 2: Move MCP live-check helper out of handlers**
Create `src/wf_mcp/broker/service/workflow_live_checks.py` and move these
handler-level live-check pieces into it:
```text
LIVE_SOURCE_CHECK_TIMEOUT_SECONDS
_LIVE_SOURCE_CHECK_FAILURES
live_source_diagnostics
_required_live_sources
```
Rename `_live_source_diagnostics(...)` to public module-private-adapter helper
`live_source_diagnostics(...)`.
The new module should own the MCP-only imports:
```python
import asyncio
import anyio
import httpx
from mcp.client.streamable_http import StreamableHTTPError
from mcp.shared.exceptions import McpError
from wf_artifacts import DependencyDiagnostic, DiagnosticSeverity, WorkflowArtifact, WorkflowDeployment
from wf_mcp.broker.service.adapters import require_adapter
from wf_mcp.broker.service.core import WfMcpService
```
Keep the existing docstring explaining that live checks perform opt-in upstream
I/O. This split is required to avoid a circular import:
```text
handlers.py -> workflow_operation_context.py -> handlers.py
```
After the move, update `handlers.py` to import `live_source_diagnostics` from
the new module for as long as `validate_deployment` still lives in handlers.
When Task 4 delegates `validate_deployment`, remove that handler import if it
is unused.
- [ ] **Step 3: Implement MCP adapter methods**
In `src/wf_mcp/broker/service/workflow_operation_context.py`, import:
```python
from collections.abc import Sequence
from wf_artifacts import DependencyDiagnostic, WorkflowArtifact, WorkflowDeployment
from wf_mcp.events import make_event
from wf_mcp.broker.service.workflow_live_checks import live_source_diagnostics
```
Then update event recorder:
```python
def record_workflow_event(
self,
event_type: str,
*,
capability_id: str,
payload: dict[str, Any],
) -> None:
self.service._record_event( # noqa: SLF001
make_event(event_type, capability_id=capability_id, payload=payload)
)
```
Update live source checker:
```python
async def deployment_diagnostics(
self,
*,
deployment: WorkflowDeployment,
artifacts: Sequence[WorkflowArtifact],
) -> list[DependencyDiagnostic]:
return await live_source_diagnostics(
self.service,
deployment=deployment,
artifacts=artifacts,
)
```
Do not import handler modules from `workflow_operation_context.py`.
- [ ] **Step 4: Update operation context tests**
In `tests/wf_api/test_operation_context.py`, add a test that calls:
```python
operation_context.events.record_workflow_event(
"workflow_artifact_saved",
capability_id="workflow.demo.v1",
payload={"artifact_id": "demo", "version": 1},
)
```
Then assert the service recorded an event with stable fields individually. Do not assert full dict equality.
- [ ] **Step 5: Run focused tests**
```powershell
uv run pytest tests/wf_api/test_operation_context.py -q
```
Expected: pass.
---
## Task 2: Create `wf_api.artifacts`
**Files:**
- Create: `src/wf_api/artifacts.py`
- Modify: `src/wf_api/__init__.py`
- Test: `tests/wf_api/test_artifact_api.py`
- [ ] **Step 1: Create service skeleton**
Create `src/wf_api/artifacts.py`:
```python
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from wf_artifacts import (
ArtifactKind,
RequiredCapability,
WorkflowArtifact,
create_workflow_artifact_from_plan as build_workflow_artifact_from_plan,
)
from wf_platform import CapabilityRef, NodeSpecInventory
from .drafts import WorkflowDraftApi
from .models import RawWorkflowPlan
from .operation_context import WorkflowOperationContext
class WorkflowArtifactApi:
"""Saved workflow artifact operations.
Event construction is intentionally delegated through
WorkflowOperationContext so this module stays protocol-neutral.
"""
def __init__(self, context: WorkflowOperationContext) -> None:
self.context = context
self.drafts = WorkflowDraftApi(context)
def _artifact_store(self):
if self.context.artifact_store is None:
raise KeyError("workflow artifact store is not configured")
return self.context.artifact_store
```
- [ ] **Step 2: Move artifact methods**
Move these bodies from `WorkflowSurfaceHandlers`, replacing `self.service` access:
```text
list_artifacts
save_artifact
create_artifact_from_plan
create_artifact_from_draft
inspect_artifact
```
Required replacements:
```python
self.service.artifact_store -> self._artifact_store()
self.service.workflow_artifact_catalog_entry(artifact) -> self.context.artifacts.workflow_artifact_catalog_entry(artifact)
self.service._record_event(make_event(...)) -> self.context.events.record_workflow_event(...)
_observed_node_specs(self.service) -> _observed_node_specs(self.context)
```
Keep return payloads byte-for-byte equivalent except for dictionary ordering.
- [ ] **Step 3: Move workspace artifact methods**
Move:
```text
create_artifact_from_workspace
create_wrapper_from_workspace
```
Use `self.context.draft_workspace_store` through `self.drafts` or a local store helper. Preserve current behavior:
- validate workspace draft first
- return `saved: False` with diagnostics when invalid
- call `create_artifact_from_draft(...)` when valid
- wrapper path passes `kind="wrapper"`
- [ ] **Step 4: Add helpers**
Add private helpers to `wf_api.artifacts`:
```text
_required_capability_payloads
_suggested_self_bindings
_observed_node_specs
_plan_nodes
_artifact_capability_id
```
Duplicate `_required_capability_payloads`, `_observed_node_specs`, and `_plan_nodes` from `wf_api.drafts` for now instead of importing private draft helpers. We can consolidate after Slice 4C if duplication becomes annoying.
Do not remove `_artifact_capability_id` from `handlers.py`; wrapper capability methods still need it until Slice 4E.
- [ ] **Step 5: Export artifact service**
In `src/wf_api/__init__.py`:
```python
from .artifacts import WorkflowArtifactApi
```
Add `"WorkflowArtifactApi"` to `__all__`.
- [ ] **Step 6: Add focused tests**
Create `tests/wf_api/test_artifact_api.py` with tests that instantiate `WorkflowArtifactApi(context_from_service(service))`:
- `save_artifact` stores a `WorkflowArtifact` and returns `saved: True`.
- `create_artifact_from_plan` saves an artifact and includes observed node specs.
- `create_artifact_from_workspace` returns `saved: False` when workspace validation fails.
- `create_wrapper_from_workspace` saves `kind == "wrapper"`.
- Handler delegation for `inspect_artifact` returns the same stable fields as direct `WorkflowArtifactApi.inspect_artifact`.
Use field-by-field assertions unless asserting a known closed model shape.
- [ ] **Step 7: Run artifact tests**
```powershell
uv run pytest tests/wf_api/test_artifact_api.py tests/wf_api/test_drafts_service.py -q
```
Expected: pass.
---
## Task 3: Create `wf_api.deployments`
**Files:**
- Create: `src/wf_api/deployments.py`
- Modify: `src/wf_api/__init__.py`
- Test: `tests/wf_api/test_deployment_api.py`
- [ ] **Step 1: Create service skeleton**
Create `src/wf_api/deployments.py`:
```python
from __future__ import annotations
from typing import Any
from wf_artifacts import (
AvailableCapability,
AvailableSource,
DependencyDiagnostic,
WorkflowArtifact,
WorkflowDeployment,
hash_json_schema,
validate_deployment_dependencies,
)
from wf_platform import CapabilitySource
from .next_actions import NextActions
from .operation_context import WorkflowOperationContext
from .saved_subgraphs import resolve_saved_subgraph_tree, validate_saved_subgraph_tree
class WorkflowDeploymentApi:
"""Saved deployment operations and dependency validation."""
def __init__(self, context: WorkflowOperationContext) -> None:
self.context = context
def _artifact_store(self):
if self.context.artifact_store is None:
raise KeyError("workflow artifact store is not configured")
return self.context.artifact_store
```
- [ ] **Step 2: Move deployment methods**
Move these bodies from `WorkflowSurfaceHandlers`:
```text
list_deployments
inspect_deployment
save_deployment
delete_deployment
validate_deployment
```
Required replacements:
```python
self.service.artifact_store -> self._artifact_store()
self.service._record_event(make_event(...)) -> self.context.events.record_workflow_event(...)
_available_sources(self.service) -> _available_sources(self.context.capability_sources)
```
For `validate_deployment(live_check=True)`, use:
```python
if live_check and self.context.live_sources is not None:
diagnostics.extend(
await self.context.live_sources.deployment_diagnostics(
deployment=deployment,
artifacts=[artifact, *tree.artifacts_by_ref.values()],
)
)
```
If `live_check=True` and `live_sources is None`, preserve static validation only. Do not invent a new warning payload in this slice.
- [ ] **Step 3: Move deployment validation helper**
Move `_deployment_validation` logic into `WorkflowDeploymentApi` as a private method:
```python
def _deployment_validation(
self,
deployment_id: str,
) -> tuple[WorkflowDeployment, WorkflowArtifact, list[DependencyDiagnostic], SavedSubgraphTree]:
...
```
Use `self._artifact_store()` and `_available_sources(self.context.capability_sources)`.
- [ ] **Step 4: Add helper functions**
Add private helpers:
```text
_available_sources
_capability_name
_deployment_summary
```
Adapt `_available_sources` to accept `Mapping[str, CapabilitySource]` instead of `WfMcpService`.
Do not remove `_available_sources` or `_capability_name` from `handlers.py` if run/capability methods still use them.
- [ ] **Step 5: Export deployment service**
In `src/wf_api/__init__.py`:
```python
from .deployments import WorkflowDeploymentApi
```
Add `"WorkflowDeploymentApi"` to `__all__`.
- [ ] **Step 6: Add focused tests**
Create `tests/wf_api/test_deployment_api.py` with tests that instantiate `WorkflowDeploymentApi(context_from_service(service))`:
- `save_deployment` stores and returns stable deployment fields.
- `list_deployments` returns compact summaries.
- `delete_deployment` removes one deployment.
- `validate_deployment(live_check=False)` returns `runnable` for a valid binding.
- `validate_deployment(live_check=True)` calls the operation-context live checker. A simple fake context may be easier than MCP service setup for this test.
- Handler delegation for `validate_deployment` returns the same stable status/diagnostic fields as direct `WorkflowDeploymentApi.validate_deployment`.
- [ ] **Step 7: Run deployment tests**
```powershell
uv run pytest tests/wf_api/test_deployment_api.py tests/wf_mcp/workflow_surface/test_deployments.py -q
```
Expected: pass.
---
## Task 4: Wire `WorkflowSurfaceHandlers`
**Files:**
- Modify: `src/wf_mcp/workflow_surface/handlers.py`
- [ ] **Step 1: Add imports**
```python
from wf_api.artifacts import WorkflowArtifactApi
from wf_api.deployments import WorkflowDeploymentApi
```
- [ ] **Step 2: Instantiate services**
In `WorkflowSurfaceHandlers.__init__`, avoid building multiple independent contexts:
```python
context = context_from_service(service)
self._drafts = WorkflowDraftApi(context)
self._artifacts = WorkflowArtifactApi(context)
self._deployments = WorkflowDeploymentApi(context)
```
- [ ] **Step 3: Replace moved artifact methods with delegates**
Replace bodies for:
```text
list_artifacts
save_artifact
create_artifact_from_plan
create_artifact_from_draft
create_artifact_from_workspace
create_wrapper_from_workspace
inspect_artifact
```
Example:
```python
async def inspect_artifact(self, *, artifact_id: str, version: int) -> dict[str, Any]:
return await self._artifacts.inspect_artifact(
artifact_id=artifact_id,
version=version,
)
```
- [ ] **Step 4: Replace moved deployment methods with delegates**
Replace bodies for:
```text
list_deployments
inspect_deployment
save_deployment
delete_deployment
validate_deployment
```
Example:
```python
async def validate_deployment(
self,
*,
deployment_id: str,
live_check: bool = False,
) -> dict[str, Any]:
return await self._deployments.validate_deployment(
deployment_id=deployment_id,
live_check=live_check,
)
```
- [ ] **Step 5: Remove only unused imports/helpers**
After delegation, run:
```powershell
rg -n "_available_sources|_suggested_self_bindings|_observed_node_specs|_deployment_summary|_artifact_capability_id|_capability_name" src/wf_mcp/workflow_surface/handlers.py
```
Remove a helper from `handlers.py` only if it has no remaining caller there.
Expected likely result:
- `_suggested_self_bindings`, `_observed_node_specs`, `_deployment_summary` can probably be removed.
- `_artifact_capability_id`, `_capability_name`, `_available_sources` may still be needed by capability/run methods. Keep them if referenced.
---
## Task 5: Verification
- [ ] **Step 1: Run focused wf_api tests**
```powershell
uv run pytest tests/wf_api/test_artifact_api.py tests/wf_api/test_deployment_api.py tests/wf_api/test_drafts_service.py -q
```
Expected: pass.
- [ ] **Step 2: Run workflow surface tests**
```powershell
uv run pytest tests/wf_mcp/workflow_surface -q
```
Expected: pass.
- [ ] **Step 3: Run import-direction test**
```powershell
uv run pytest tests/wf_api/test_import_direction.py -q
```
Expected: pass; `wf_api` has no `wf_mcp` imports.
- [ ] **Step 4: Run ruff on touched files**
```powershell
uv run ruff check src/wf_api/artifacts.py src/wf_api/deployments.py src/wf_api/operation_context.py src/wf_api/__init__.py src/wf_mcp/broker/service/workflow_operation_context.py src/wf_mcp/workflow_surface/handlers.py tests/wf_api/test_artifact_api.py tests/wf_api/test_deployment_api.py
```
Expected: all checks pass.
- [ ] **Step 5: Run basedpyright on touched files**
```powershell
uv run basedpyright --level error src/wf_api/artifacts.py src/wf_api/deployments.py src/wf_api/operation_context.py src/wf_mcp/broker/service/workflow_operation_context.py src/wf_mcp/workflow_surface/handlers.py tests/wf_api/test_artifact_api.py tests/wf_api/test_deployment_api.py
```
Expected: `0 errors`.
- [ ] **Step 6: Optional full suite**
```powershell
uv run pytest -q
```
Expected: full suite passes with the projects existing skipped/xfailed counts.
---
## Self-Review Checklist
- `wf_api.artifacts` imports no `wf_mcp`.
- `wf_api.deployments` imports no `wf_mcp`.
- Event construction remains in `wf_mcp.broker.service.workflow_operation_context`.
- Live upstream adapter/auth probing remains in `wf_mcp`.
- `WorkflowSurfaceHandlers` public artifact/deployment method signatures are unchanged.
- `create_draft_workspace_from_capability` still lives in `WorkflowSurfaceHandlers`.
- Capability methods still live in `WorkflowSurfaceHandlers`.
- Run methods still live in `WorkflowSurfaceHandlers`.
- No public payload shape changed.
- No MCP schema changed.
- Temporary helper duplication is documented and deliberate.