the WorkflowApiBackend collapses
This commit is contained in:
@@ -40,9 +40,9 @@ implementation state.
|
|||||||
6. **Workflow API seam**
|
6. **Workflow API seam**
|
||||||
- `wf_api.WorkflowApi` is now the process-local application-facing workflow
|
- `wf_api.WorkflowApi` is now the process-local application-facing workflow
|
||||||
API used by both CLI commands and MCP workflow tools.
|
API used by both CLI commands and MCP workflow tools.
|
||||||
- `wf_api` imports no `wf_mcp` modules. The current adapter,
|
- `wf_api` imports no `wf_mcp` modules. `WorkflowApi` composes domain
|
||||||
`WfMcpWorkflowApiBackend`, still wraps the existing MCP service stack while
|
services directly from `WorkflowOperationContext`; MCP owns only context
|
||||||
later slices extract protocol-neutral logic behind that seam.
|
construction and tool schemas.
|
||||||
- Protocol-neutral helpers now live in `wf_api`: refs/constants, wrapper
|
- Protocol-neutral helpers now live in `wf_api`: refs/constants, wrapper
|
||||||
hints, next actions, raw workflow plans, runtime dependencies, saved
|
hints, next actions, raw workflow plans, runtime dependencies, saved
|
||||||
subgraph preparation, and durable run lifecycle helpers. Old
|
subgraph preparation, and durable run lifecycle helpers. Old
|
||||||
@@ -115,9 +115,9 @@ implementation state.
|
|||||||
Protocol-neutral operation context and domain services now exist behind
|
Protocol-neutral operation context and domain services now exist behind
|
||||||
`wf_api`; MCP tool schemas and tool registration stay in `wf_mcp`.
|
`wf_api`; MCP tool schemas and tool registration stay in `wf_mcp`.
|
||||||
- Workflow store ownership is explicit: entrypoints construct/inject `WorkflowStores`; `WfMcpService` no longer guesses stores from the MCP store root.
|
- Workflow store ownership is explicit: entrypoints construct/inject `WorkflowStores`; `WfMcpService` no longer guesses stores from the MCP store root.
|
||||||
- The next useful slice is removing the remaining double-delegation path so
|
- Double-delegation has been removed: CLI and MCP workflow tools construct
|
||||||
`WorkflowApi` composes domain services directly instead of routing through
|
`WorkflowApi(context_from_service(service))` directly. `WorkflowSurfaceHandlers`
|
||||||
`WfMcpWorkflowApiBackend` and `WorkflowSurfaceHandlers`.
|
remains only as a temporary compatibility shim for older imports.
|
||||||
|
|
||||||
Frame stress points remaining for native subgraphs and future fork/gather:
|
Frame stress points remaining for native subgraphs and future fork/gather:
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
|
|
||||||
**Goal:** Extract a protocol-neutral workflow application API from `wf_mcp` while preserving current process-local behavior and avoiding a large semantic rewrite.
|
**Goal:** Extract a protocol-neutral workflow application API from `wf_mcp` while preserving current process-local behavior and avoiding a large semantic rewrite.
|
||||||
|
|
||||||
|
> 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.
|
||||||
|
|
||||||
**Architecture:** `wf_api` becomes the long-lived in-process application service layer. `wf_mcp`, `wf_cli`, and future HTTP/UI adapters call `wf_api`; `wf_api` must not import `wf_mcp`.
|
**Architecture:** `wf_api` becomes the long-lived in-process application service layer. `wf_mcp`, `wf_cli`, and future HTTP/UI adapters call `wf_api`; `wf_api` must not import `wf_mcp`.
|
||||||
|
|
||||||
**Current State:** Slice 1 introduced `wf_api.WorkflowApi`,
|
**Current State:** Slice 1 introduced `wf_api.WorkflowApi`,
|
||||||
|
|||||||
@@ -0,0 +1,754 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
```text
|
||||||
|
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
|
||||||
|
|
||||||
|
```text
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dataclasses import dataclass
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add before `RawWorkflowPlan`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from .backend import TraceRange, WorkflowApiBackend
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_api.backend import TraceRange
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_api import TraceRange
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/wf_mcp/workflow_surface/tools.py`, remove:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from .backend import TraceRange, WorkflowApiBackend
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_mcp.broker.service.workflow_api_backend import WfMcpWorkflowApiBackend
|
||||||
|
```
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_mcp.broker.service.workflow_operation_context import context_from_service
|
||||||
|
```
|
||||||
|
|
||||||
|
Change `load_cli_context()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
assert context.handlers.backend._handlers.service is context.service # type: ignore[attr-defined]
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_mcp.broker.service.workflow_api_backend import WfMcpWorkflowApiBackend
|
||||||
|
```
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_mcp.broker.service.workflow_operation_context import context_from_service
|
||||||
|
```
|
||||||
|
|
||||||
|
Change:
|
||||||
|
|
||||||
|
```python
|
||||||
|
handlers = WorkflowApi(WfMcpWorkflowApiBackend(service))
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```text
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- 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:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf_api.WorkflowApi ───> WorkflowApiBackend
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace that diagram/text with:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf_mcp.workflow_surface.tools
|
||||||
|
-> wf_api.WorkflowApi
|
||||||
|
-> wf_api domain services
|
||||||
|
-> WorkflowOperationContext
|
||||||
|
-> WfMcpService adapters/stores/runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
`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:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
> 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: full suite passes with known skip/xfail counts.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run lint and format checks**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
+13
-20
@@ -41,19 +41,24 @@ relevant concern package directly.
|
|||||||
Workflow lifecycle operations now have a protocol-neutral front door:
|
Workflow lifecycle operations now have a protocol-neutral front door:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
wf_cli ─┐
|
wf_mcp.workflow_surface.tools
|
||||||
├──> wf_api.WorkflowApi ───> WorkflowApiBackend
|
-> wf_api.WorkflowApi
|
||||||
wf_mcp ─┘
|
-> wf_api domain services
|
||||||
|
-> WorkflowOperationContext
|
||||||
|
-> WfMcpService adapters/stores/runtime
|
||||||
```
|
```
|
||||||
|
|
||||||
The current backend is `wf_mcp.broker.service.WfMcpWorkflowApiBackend`, which
|
`WorkflowSurfaceHandlers` is a compatibility shim only. New entrypoints should
|
||||||
wraps the existing `wf_mcp.workflow_surface.WorkflowSurfaceHandlers`. That
|
construct `WorkflowApi(context_from_service(service))` directly.
|
||||||
handler class still contains most workflow-surface logic and still depends on
|
|
||||||
`WfMcpService`; it is kept for compatibility and incremental extraction.
|
The old backend-adapter/protocol layer has been removed. `WorkflowApi` composes
|
||||||
|
domain services (`WorkflowCapabilityApi`, `WorkflowDraftApi`,
|
||||||
|
`WorkflowArtifactApi`, `WorkflowDeploymentApi`, `WorkflowRunApi`) from a
|
||||||
|
`WorkflowOperationContext`.
|
||||||
|
|
||||||
New code should treat `wf_api.WorkflowApi` as the application-facing API. Do not
|
New code should treat `wf_api.WorkflowApi` as the application-facing API. Do not
|
||||||
add new callers that import `WorkflowSurfaceHandlers` directly unless they are
|
add new callers that import `WorkflowSurfaceHandlers` directly unless they are
|
||||||
inside the MCP backend adapter or compatibility tests.
|
compatibility tests.
|
||||||
|
|
||||||
This is a dependency-direction cleanup, not a full domain split. Most API
|
This is a dependency-direction cleanup, not a full domain split. Most API
|
||||||
methods still mirror the old workflow-surface payloads and return
|
methods still mirror the old workflow-surface payloads and return
|
||||||
@@ -66,18 +71,6 @@ old `wf_mcp.workflow_surface.*` helper modules are compatibility shims. MCP
|
|||||||
tool schemas and registration still live in `wf_mcp.workflow_surface.models` and
|
tool schemas and registration still live in `wf_mcp.workflow_surface.models` and
|
||||||
`wf_mcp.workflow_surface.tools`.
|
`wf_mcp.workflow_surface.tools`.
|
||||||
|
|
||||||
The remaining extraction work is the large operation implementation:
|
|
||||||
`WorkflowSurfaceHandlers` still owns most capability, draft, artifact,
|
|
||||||
deployment, and run methods. Split that implementation by domain only after the
|
|
||||||
current helper shims are stable.
|
|
||||||
|
|
||||||
Do not move those method bodies directly into `wf_api` while they still depend
|
|
||||||
on the whole `WfMcpService`. The next extraction step should define a
|
|
||||||
protocol-neutral operation context/protocol seam for stores, capability sources,
|
|
||||||
event recording, and live source calls. MCP-owned code can adapt `WfMcpService`
|
|
||||||
into that seam; domain services can then depend on the seam rather than on MCP
|
|
||||||
service internals.
|
|
||||||
|
|
||||||
## Broker Catalogs
|
## Broker Catalogs
|
||||||
|
|
||||||
The broker keeps two related catalog views:
|
The broker keeps two related catalog views:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from .listing import matches_query, paged_list_payload
|
from .listing import matches_query, paged_list_payload
|
||||||
from .artifacts import WorkflowArtifactApi
|
from .artifacts import WorkflowArtifactApi
|
||||||
from .backend import TraceRange, WorkflowApiBackend
|
from .models import RawWorkflowPlan, TraceRange
|
||||||
from .capabilities import WorkflowCapabilityApi
|
from .capabilities import WorkflowCapabilityApi
|
||||||
from .constants import (
|
from .constants import (
|
||||||
DEFAULT_CALL_STEP_ID,
|
DEFAULT_CALL_STEP_ID,
|
||||||
@@ -56,10 +56,10 @@ __all__ = [
|
|||||||
"OutcomeCandidate",
|
"OutcomeCandidate",
|
||||||
"OutcomeCandidateKind",
|
"OutcomeCandidateKind",
|
||||||
"RUNTIME_ERROR_CAPABILITY",
|
"RUNTIME_ERROR_CAPABILITY",
|
||||||
|
"RawWorkflowPlan",
|
||||||
"RuntimeDependencies",
|
"RuntimeDependencies",
|
||||||
"TraceRange",
|
"TraceRange",
|
||||||
"WorkflowApi",
|
"WorkflowApi",
|
||||||
"WorkflowApiBackend",
|
|
||||||
"WorkflowArtifactApi",
|
"WorkflowArtifactApi",
|
||||||
"WorkflowCapabilityApi",
|
"WorkflowCapabilityApi",
|
||||||
"WorkflowArtifactCataloger",
|
"WorkflowArtifactCataloger",
|
||||||
|
|||||||
@@ -1,321 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Sequence
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Protocol, runtime_checkable
|
|
||||||
|
|
||||||
from wf_artifacts import ArtifactKind
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class TraceRange:
|
|
||||||
"""Caller-bounded debug trace slice for durable deployment runs."""
|
|
||||||
|
|
||||||
start: int = 0
|
|
||||||
limit: int = 25
|
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
|
||||||
class WorkflowApiBackend(Protocol):
|
|
||||||
"""High-level workflow operation protocol.
|
|
||||||
|
|
||||||
Implementations wrap a concrete service (e.g. WorkflowSurfaceHandlers
|
|
||||||
backed by WfMcpService) so that wf_api never imports wf_mcp.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# -- capabilities --
|
|
||||||
|
|
||||||
async def list_capabilities(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
query: str | None = None,
|
|
||||||
source_id: str | None = None,
|
|
||||||
cursor: str | None = None,
|
|
||||||
limit: int = 50,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def inspect_capability(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
qualified_name: str,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def call_capability(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
qualified_name: str,
|
|
||||||
payload: dict[str, Any],
|
|
||||||
deployment_id: str | None = None,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
# -- artifacts --
|
|
||||||
|
|
||||||
async def list_artifacts(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
query: str | None = None,
|
|
||||||
kind: ArtifactKind | None = None,
|
|
||||||
cursor: str | None = None,
|
|
||||||
limit: int = 50,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def inspect_artifact(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def save_artifact(
|
|
||||||
self,
|
|
||||||
artifact: dict[str, Any],
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def create_artifact_from_plan(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
title: str,
|
|
||||||
plan: dict[str, Any],
|
|
||||||
outcomes: Sequence[str],
|
|
||||||
kind: ArtifactKind = "workflow",
|
|
||||||
description: str | None = None,
|
|
||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
||||||
source_bindings: dict[str, str] | None = None,
|
|
||||||
created_from_catalog_version: str | None = None,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def create_artifact_from_draft(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
title: str,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
outcomes: Sequence[str],
|
|
||||||
kind: ArtifactKind = "workflow",
|
|
||||||
description: str | None = None,
|
|
||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
||||||
source_bindings: dict[str, str] | None = None,
|
|
||||||
created_from_catalog_version: str | None = None,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def create_artifact_from_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
title: str,
|
|
||||||
outcomes: Sequence[str],
|
|
||||||
kind: ArtifactKind = "workflow",
|
|
||||||
description: str | None = None,
|
|
||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
||||||
source_bindings: dict[str, str] | None = None,
|
|
||||||
created_from_catalog_version: str | None = None,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def create_wrapper_from_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
title: str,
|
|
||||||
outcomes: Sequence[str],
|
|
||||||
description: str | None = None,
|
|
||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
||||||
source_bindings: dict[str, str] | None = None,
|
|
||||||
created_from_catalog_version: str | None = None,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
# -- drafts --
|
|
||||||
|
|
||||||
async def validate_draft(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def compile_draft(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def patch_draft(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
patch: list[dict[str, Any]],
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
# -- draft workspaces --
|
|
||||||
|
|
||||||
async def list_draft_workspaces(self) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def create_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
title: str | None = None,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def get_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
include_draft: bool = False,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def delete_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def validate_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def patch_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
patch: list[dict[str, Any]],
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def set_draft_name(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
name: str,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def set_draft_route(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
outcome: str,
|
|
||||||
target: str,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def set_step_input_map(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
input_map: dict[str, str],
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def set_step_output_map(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
output_map: dict[str, str],
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def create_minimal_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
name: str,
|
|
||||||
capability_name: str,
|
|
||||||
input_schema: dict[str, Any],
|
|
||||||
state_schema: dict[str, Any],
|
|
||||||
output_schema: dict[str, Any],
|
|
||||||
input: Sequence[Any] | None = None,
|
|
||||||
output: Sequence[Any] | None = None,
|
|
||||||
input_map: dict[str, str] | None = None,
|
|
||||||
output_map: dict[str, str] | None = None,
|
|
||||||
error_message_source: Any | None = None,
|
|
||||||
title: str | None = None,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def create_draft_workspace_from_capability(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
capability_name: str,
|
|
||||||
name: str | None = None,
|
|
||||||
title: str | None = None,
|
|
||||||
input_schema: dict[str, Any] | None = None,
|
|
||||||
state_schema: dict[str, Any] | None = None,
|
|
||||||
output_schema: dict[str, Any] | None = None,
|
|
||||||
input: Sequence[Any] | None = None,
|
|
||||||
output: Sequence[Any] | None = None,
|
|
||||||
input_map: dict[str, str] | None = None,
|
|
||||||
output_map: dict[str, str] | None = None,
|
|
||||||
error_message_source: Any | None = None,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
# -- deployments --
|
|
||||||
|
|
||||||
async def list_deployments(self) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def inspect_deployment(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
deployment_id: str,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def save_deployment(
|
|
||||||
self,
|
|
||||||
deployment: dict[str, Any],
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def delete_deployment(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
deployment_id: str,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def validate_deployment(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
deployment_id: str,
|
|
||||||
live_check: bool = False,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
# -- runs --
|
|
||||||
|
|
||||||
async def run_deployment(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
deployment_id: str,
|
|
||||||
workflow_input: dict[str, Any],
|
|
||||||
trace_range: TraceRange | None = None,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def resume_run(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
run_id: str,
|
|
||||||
resume_payload: dict[str, Any],
|
|
||||||
resume_outcome: str = "submitted",
|
|
||||||
trace_range: TraceRange | None = None,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def inspect_run(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
run_id: str,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def read_run_trace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
run_id: str,
|
|
||||||
trace_range: TraceRange,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -7,6 +8,14 @@ from wf_core import Edge
|
|||||||
from wf_core.models.steps import InputBinding, Step
|
from wf_core.models.steps import InputBinding, Step
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TraceRange:
|
||||||
|
"""Caller-bounded debug trace slice for durable deployment runs."""
|
||||||
|
|
||||||
|
start: int = 0
|
||||||
|
limit: int = 25
|
||||||
|
|
||||||
|
|
||||||
class RawWorkflowPlan(BaseModel):
|
class RawWorkflowPlan(BaseModel):
|
||||||
"""Raw authoring plan using the same graph step and edge models as core."""
|
"""Raw authoring plan using the same graph step and edge models as core."""
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -30,8 +30,10 @@ from .operation_context import WorkflowOperationContext
|
|||||||
class TraceRangeLike(Protocol):
|
class TraceRangeLike(Protocol):
|
||||||
"""Small structural trace range accepted from MCP, CLI, or HTTP adapters."""
|
"""Small structural trace range accepted from MCP, CLI, or HTTP adapters."""
|
||||||
|
|
||||||
start: int
|
@property
|
||||||
limit: int
|
def start(self) -> int: ...
|
||||||
|
@property
|
||||||
|
def limit(self) -> int: ...
|
||||||
|
|
||||||
|
|
||||||
class WorkflowRunApi:
|
class WorkflowRunApi:
|
||||||
|
|||||||
+55
-45
@@ -5,20 +5,30 @@ from typing import Any
|
|||||||
|
|
||||||
from wf_artifacts import ArtifactKind
|
from wf_artifacts import ArtifactKind
|
||||||
|
|
||||||
from .backend import TraceRange, WorkflowApiBackend
|
from .artifacts import WorkflowArtifactApi
|
||||||
|
from .capabilities import WorkflowCapabilityApi
|
||||||
|
from .deployments import WorkflowDeploymentApi
|
||||||
|
from .drafts import WorkflowDraftApi
|
||||||
|
from .models import RawWorkflowPlan
|
||||||
|
from .operation_context import WorkflowOperationContext
|
||||||
|
from .runs import TraceRangeLike, WorkflowRunApi
|
||||||
|
|
||||||
|
|
||||||
class WorkflowApi:
|
class WorkflowApi:
|
||||||
"""Protocol-neutral workflow application facade.
|
"""Protocol-neutral workflow application facade.
|
||||||
|
|
||||||
Delegates every operation to a WorkflowApiBackend implementation.
|
This facade owns the stable application entry point. It composes the
|
||||||
This class owns no business logic; it exists so that callers
|
domain APIs from a WorkflowOperationContext so MCP, CLI, and future HTTP
|
||||||
(wf_cli, wf_mcp tools, future HTTP adapters) share one entry point
|
callers share one operation surface without importing wf_mcp.
|
||||||
that does not import wf_mcp.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, backend: WorkflowApiBackend) -> None:
|
def __init__(self, context: WorkflowOperationContext) -> None:
|
||||||
self.backend: WorkflowApiBackend = backend
|
self.context = context
|
||||||
|
self.capabilities = WorkflowCapabilityApi(context)
|
||||||
|
self.drafts = WorkflowDraftApi(context)
|
||||||
|
self.artifacts = WorkflowArtifactApi(context)
|
||||||
|
self.deployments = WorkflowDeploymentApi(context)
|
||||||
|
self.runs = WorkflowRunApi(context)
|
||||||
|
|
||||||
# -- capabilities --
|
# -- capabilities --
|
||||||
|
|
||||||
@@ -30,7 +40,7 @@ class WorkflowApi:
|
|||||||
cursor: str | None = None,
|
cursor: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.list_capabilities(
|
return await self.capabilities.list_capabilities(
|
||||||
query=query,
|
query=query,
|
||||||
source_id=source_id,
|
source_id=source_id,
|
||||||
cursor=cursor,
|
cursor=cursor,
|
||||||
@@ -42,7 +52,7 @@ class WorkflowApi:
|
|||||||
*,
|
*,
|
||||||
qualified_name: str,
|
qualified_name: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.inspect_capability(qualified_name=qualified_name)
|
return await self.capabilities.inspect_capability(qualified_name=qualified_name)
|
||||||
|
|
||||||
async def call_capability(
|
async def call_capability(
|
||||||
self,
|
self,
|
||||||
@@ -51,7 +61,7 @@ class WorkflowApi:
|
|||||||
payload: dict[str, Any],
|
payload: dict[str, Any],
|
||||||
deployment_id: str | None = None,
|
deployment_id: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.call_capability(
|
return await self.capabilities.call_capability(
|
||||||
qualified_name=qualified_name,
|
qualified_name=qualified_name,
|
||||||
payload=payload,
|
payload=payload,
|
||||||
deployment_id=deployment_id,
|
deployment_id=deployment_id,
|
||||||
@@ -67,7 +77,7 @@ class WorkflowApi:
|
|||||||
cursor: str | None = None,
|
cursor: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.list_artifacts(
|
return await self.artifacts.list_artifacts(
|
||||||
query=query,
|
query=query,
|
||||||
kind=kind,
|
kind=kind,
|
||||||
cursor=cursor,
|
cursor=cursor,
|
||||||
@@ -80,7 +90,7 @@ class WorkflowApi:
|
|||||||
artifact_id: str,
|
artifact_id: str,
|
||||||
version: int,
|
version: int,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.inspect_artifact(
|
return await self.artifacts.inspect_artifact(
|
||||||
artifact_id=artifact_id,
|
artifact_id=artifact_id,
|
||||||
version=version,
|
version=version,
|
||||||
)
|
)
|
||||||
@@ -89,7 +99,7 @@ class WorkflowApi:
|
|||||||
self,
|
self,
|
||||||
artifact: dict[str, Any],
|
artifact: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.save_artifact(artifact)
|
return await self.artifacts.save_artifact(artifact)
|
||||||
|
|
||||||
async def create_artifact_from_plan(
|
async def create_artifact_from_plan(
|
||||||
self,
|
self,
|
||||||
@@ -97,7 +107,7 @@ class WorkflowApi:
|
|||||||
artifact_id: str,
|
artifact_id: str,
|
||||||
version: int,
|
version: int,
|
||||||
title: str,
|
title: str,
|
||||||
plan: dict[str, Any],
|
plan: RawWorkflowPlan | dict[str, Any],
|
||||||
outcomes: Sequence[str],
|
outcomes: Sequence[str],
|
||||||
kind: ArtifactKind = "workflow",
|
kind: ArtifactKind = "workflow",
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
@@ -105,7 +115,7 @@ class WorkflowApi:
|
|||||||
source_bindings: dict[str, str] | None = None,
|
source_bindings: dict[str, str] | None = None,
|
||||||
created_from_catalog_version: str | None = None,
|
created_from_catalog_version: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.create_artifact_from_plan(
|
return await self.artifacts.create_artifact_from_plan(
|
||||||
artifact_id=artifact_id,
|
artifact_id=artifact_id,
|
||||||
version=version,
|
version=version,
|
||||||
title=title,
|
title=title,
|
||||||
@@ -132,7 +142,7 @@ class WorkflowApi:
|
|||||||
source_bindings: dict[str, str] | None = None,
|
source_bindings: dict[str, str] | None = None,
|
||||||
created_from_catalog_version: str | None = None,
|
created_from_catalog_version: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.create_artifact_from_draft(
|
return await self.artifacts.create_artifact_from_draft(
|
||||||
artifact_id=artifact_id,
|
artifact_id=artifact_id,
|
||||||
version=version,
|
version=version,
|
||||||
title=title,
|
title=title,
|
||||||
@@ -159,7 +169,7 @@ class WorkflowApi:
|
|||||||
source_bindings: dict[str, str] | None = None,
|
source_bindings: dict[str, str] | None = None,
|
||||||
created_from_catalog_version: str | None = None,
|
created_from_catalog_version: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.create_artifact_from_workspace(
|
return await self.artifacts.create_artifact_from_workspace(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
artifact_id=artifact_id,
|
artifact_id=artifact_id,
|
||||||
version=version,
|
version=version,
|
||||||
@@ -185,7 +195,7 @@ class WorkflowApi:
|
|||||||
source_bindings: dict[str, str] | None = None,
|
source_bindings: dict[str, str] | None = None,
|
||||||
created_from_catalog_version: str | None = None,
|
created_from_catalog_version: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.create_wrapper_from_workspace(
|
return await self.artifacts.create_wrapper_from_workspace(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
artifact_id=artifact_id,
|
artifact_id=artifact_id,
|
||||||
version=version,
|
version=version,
|
||||||
@@ -204,14 +214,14 @@ class WorkflowApi:
|
|||||||
*,
|
*,
|
||||||
draft: dict[str, Any],
|
draft: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.validate_draft(draft=draft)
|
return await self.drafts.validate_draft(draft=draft)
|
||||||
|
|
||||||
async def compile_draft(
|
async def compile_draft(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
draft: dict[str, Any],
|
draft: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.compile_draft(draft=draft)
|
return await self.drafts.compile_draft(draft=draft)
|
||||||
|
|
||||||
async def patch_draft(
|
async def patch_draft(
|
||||||
self,
|
self,
|
||||||
@@ -219,12 +229,12 @@ class WorkflowApi:
|
|||||||
draft: dict[str, Any],
|
draft: dict[str, Any],
|
||||||
patch: list[dict[str, Any]],
|
patch: list[dict[str, Any]],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.patch_draft(draft=draft, patch=patch)
|
return await self.drafts.patch_draft(draft=draft, patch=patch)
|
||||||
|
|
||||||
# -- draft workspaces --
|
# -- draft workspaces --
|
||||||
|
|
||||||
async def list_draft_workspaces(self) -> dict[str, Any]:
|
async def list_draft_workspaces(self) -> dict[str, Any]:
|
||||||
return await self.backend.list_draft_workspaces()
|
return await self.drafts.list_draft_workspaces()
|
||||||
|
|
||||||
async def create_draft_workspace(
|
async def create_draft_workspace(
|
||||||
self,
|
self,
|
||||||
@@ -233,7 +243,7 @@ class WorkflowApi:
|
|||||||
draft: dict[str, Any],
|
draft: dict[str, Any],
|
||||||
title: str | None = None,
|
title: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.create_draft_workspace(
|
return await self.drafts.create_draft_workspace(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
draft=draft,
|
draft=draft,
|
||||||
title=title,
|
title=title,
|
||||||
@@ -245,7 +255,7 @@ class WorkflowApi:
|
|||||||
workspace_id: str,
|
workspace_id: str,
|
||||||
include_draft: bool = False,
|
include_draft: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.get_draft_workspace(
|
return await self.drafts.get_draft_workspace(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
include_draft=include_draft,
|
include_draft=include_draft,
|
||||||
)
|
)
|
||||||
@@ -255,14 +265,14 @@ class WorkflowApi:
|
|||||||
*,
|
*,
|
||||||
workspace_id: str,
|
workspace_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.delete_draft_workspace(workspace_id=workspace_id)
|
return await self.drafts.delete_draft_workspace(workspace_id=workspace_id)
|
||||||
|
|
||||||
async def validate_draft_workspace(
|
async def validate_draft_workspace(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
workspace_id: str,
|
workspace_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.validate_draft_workspace(workspace_id=workspace_id)
|
return await self.drafts.validate_draft_workspace(workspace_id=workspace_id)
|
||||||
|
|
||||||
async def patch_draft_workspace(
|
async def patch_draft_workspace(
|
||||||
self,
|
self,
|
||||||
@@ -271,7 +281,7 @@ class WorkflowApi:
|
|||||||
revision: int,
|
revision: int,
|
||||||
patch: list[dict[str, Any]],
|
patch: list[dict[str, Any]],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.patch_draft_workspace(
|
return await self.drafts.patch_draft_workspace(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
revision=revision,
|
revision=revision,
|
||||||
patch=patch,
|
patch=patch,
|
||||||
@@ -284,7 +294,7 @@ class WorkflowApi:
|
|||||||
revision: int,
|
revision: int,
|
||||||
name: str,
|
name: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.set_draft_name(
|
return await self.drafts.set_draft_name(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
revision=revision,
|
revision=revision,
|
||||||
name=name,
|
name=name,
|
||||||
@@ -299,7 +309,7 @@ class WorkflowApi:
|
|||||||
outcome: str,
|
outcome: str,
|
||||||
target: str,
|
target: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.set_draft_route(
|
return await self.drafts.set_draft_route(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
revision=revision,
|
revision=revision,
|
||||||
step_id=step_id,
|
step_id=step_id,
|
||||||
@@ -315,7 +325,7 @@ class WorkflowApi:
|
|||||||
step_id: str,
|
step_id: str,
|
||||||
input_map: dict[str, str],
|
input_map: dict[str, str],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.set_step_input_map(
|
return await self.drafts.set_step_input_map(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
revision=revision,
|
revision=revision,
|
||||||
step_id=step_id,
|
step_id=step_id,
|
||||||
@@ -330,7 +340,7 @@ class WorkflowApi:
|
|||||||
step_id: str,
|
step_id: str,
|
||||||
output_map: dict[str, str],
|
output_map: dict[str, str],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.set_step_output_map(
|
return await self.drafts.set_step_output_map(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
revision=revision,
|
revision=revision,
|
||||||
step_id=step_id,
|
step_id=step_id,
|
||||||
@@ -353,7 +363,7 @@ class WorkflowApi:
|
|||||||
error_message_source: Any | None = None,
|
error_message_source: Any | None = None,
|
||||||
title: str | None = None,
|
title: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.create_minimal_draft_workspace(
|
return await self.drafts.create_minimal_draft_workspace(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
name=name,
|
name=name,
|
||||||
capability_name=capability_name,
|
capability_name=capability_name,
|
||||||
@@ -384,7 +394,7 @@ class WorkflowApi:
|
|||||||
output_map: dict[str, str] | None = None,
|
output_map: dict[str, str] | None = None,
|
||||||
error_message_source: Any | None = None,
|
error_message_source: Any | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.create_draft_workspace_from_capability(
|
return await self.capabilities.create_draft_workspace_from_capability(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
capability_name=capability_name,
|
capability_name=capability_name,
|
||||||
name=name,
|
name=name,
|
||||||
@@ -402,27 +412,27 @@ class WorkflowApi:
|
|||||||
# -- deployments --
|
# -- deployments --
|
||||||
|
|
||||||
async def list_deployments(self) -> dict[str, Any]:
|
async def list_deployments(self) -> dict[str, Any]:
|
||||||
return await self.backend.list_deployments()
|
return await self.deployments.list_deployments()
|
||||||
|
|
||||||
async def inspect_deployment(
|
async def inspect_deployment(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
deployment_id: str,
|
deployment_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.inspect_deployment(deployment_id=deployment_id)
|
return await self.deployments.inspect_deployment(deployment_id=deployment_id)
|
||||||
|
|
||||||
async def save_deployment(
|
async def save_deployment(
|
||||||
self,
|
self,
|
||||||
deployment: dict[str, Any],
|
deployment: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.save_deployment(deployment)
|
return await self.deployments.save_deployment(deployment)
|
||||||
|
|
||||||
async def delete_deployment(
|
async def delete_deployment(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
deployment_id: str,
|
deployment_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.delete_deployment(deployment_id=deployment_id)
|
return await self.deployments.delete_deployment(deployment_id=deployment_id)
|
||||||
|
|
||||||
async def validate_deployment(
|
async def validate_deployment(
|
||||||
self,
|
self,
|
||||||
@@ -430,7 +440,7 @@ class WorkflowApi:
|
|||||||
deployment_id: str,
|
deployment_id: str,
|
||||||
live_check: bool = False,
|
live_check: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.validate_deployment(
|
return await self.deployments.validate_deployment(
|
||||||
deployment_id=deployment_id,
|
deployment_id=deployment_id,
|
||||||
live_check=live_check,
|
live_check=live_check,
|
||||||
)
|
)
|
||||||
@@ -442,9 +452,9 @@ class WorkflowApi:
|
|||||||
*,
|
*,
|
||||||
deployment_id: str,
|
deployment_id: str,
|
||||||
workflow_input: dict[str, Any],
|
workflow_input: dict[str, Any],
|
||||||
trace_range: TraceRange | None = None,
|
trace_range: TraceRangeLike | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.run_deployment(
|
return await self.runs.run_deployment(
|
||||||
deployment_id=deployment_id,
|
deployment_id=deployment_id,
|
||||||
workflow_input=workflow_input,
|
workflow_input=workflow_input,
|
||||||
trace_range=trace_range,
|
trace_range=trace_range,
|
||||||
@@ -456,9 +466,9 @@ class WorkflowApi:
|
|||||||
run_id: str,
|
run_id: str,
|
||||||
resume_payload: dict[str, Any],
|
resume_payload: dict[str, Any],
|
||||||
resume_outcome: str = "submitted",
|
resume_outcome: str = "submitted",
|
||||||
trace_range: TraceRange | None = None,
|
trace_range: TraceRangeLike | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.resume_run(
|
return await self.runs.resume_run(
|
||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
resume_payload=resume_payload,
|
resume_payload=resume_payload,
|
||||||
resume_outcome=resume_outcome,
|
resume_outcome=resume_outcome,
|
||||||
@@ -470,15 +480,15 @@ class WorkflowApi:
|
|||||||
*,
|
*,
|
||||||
run_id: str,
|
run_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.inspect_run(run_id=run_id)
|
return await self.runs.inspect_run(run_id=run_id)
|
||||||
|
|
||||||
async def read_run_trace(
|
async def read_run_trace(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
run_id: str,
|
run_id: str,
|
||||||
trace_range: TraceRange,
|
trace_range: TraceRangeLike,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.backend.read_run_trace(
|
return await self.runs.read_run_trace(
|
||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
trace_range=trace_range,
|
trace_range=trace_range,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import typer
|
|||||||
|
|
||||||
from wf_cli.context import config_path_from_context, load_cli_context
|
from wf_cli.context import config_path_from_context, load_cli_context
|
||||||
from wf_cli.io import CliInputError, emit_json, parse_json_input
|
from wf_cli.io import CliInputError, emit_json, parse_json_input
|
||||||
from wf_api.backend import TraceRange
|
from wf_api import TraceRange
|
||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(
|
||||||
name="run",
|
name="run",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import typer
|
|||||||
from wf_api import WorkflowApi
|
from wf_api import WorkflowApi
|
||||||
from wf_mcp.broker import build_service_from_config, load_broker_config
|
from wf_mcp.broker import build_service_from_config, load_broker_config
|
||||||
from wf_mcp.broker.service import WfMcpService
|
from wf_mcp.broker.service import WfMcpService
|
||||||
from wf_mcp.broker.service.workflow_api_backend import WfMcpWorkflowApiBackend
|
from wf_mcp.broker.service.workflow_operation_context import context_from_service
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -41,5 +41,5 @@ def load_cli_context(config_path: str | Path) -> CliContext:
|
|||||||
return CliContext(
|
return CliContext(
|
||||||
config_path=resolved_config_path,
|
config_path=resolved_config_path,
|
||||||
service=service,
|
service=service,
|
||||||
handlers=WorkflowApi(WfMcpWorkflowApiBackend(service)),
|
handlers=WorkflowApi(context_from_service(service)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,489 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Sequence
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from wf_artifacts import ArtifactKind
|
|
||||||
from wf_api.backend import TraceRange as ApiTraceRange
|
|
||||||
|
|
||||||
from ...workflow_surface.handlers import WorkflowSurfaceHandlers
|
|
||||||
from ...workflow_surface.models import TraceRange as HandlerTraceRange
|
|
||||||
from .core import WfMcpService
|
|
||||||
|
|
||||||
|
|
||||||
def _to_handler_trace_range(tr: ApiTraceRange) -> HandlerTraceRange:
|
|
||||||
return HandlerTraceRange(start=tr.start, limit=tr.limit)
|
|
||||||
|
|
||||||
|
|
||||||
class WfMcpWorkflowApiBackend:
|
|
||||||
"""Adapt existing WorkflowSurfaceHandlers into WorkflowApiBackend."""
|
|
||||||
|
|
||||||
def __init__(self, service: WfMcpService) -> None:
|
|
||||||
self._handlers = WorkflowSurfaceHandlers(service)
|
|
||||||
|
|
||||||
# -- capabilities --
|
|
||||||
|
|
||||||
async def list_capabilities(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
query: str | None = None,
|
|
||||||
source_id: str | None = None,
|
|
||||||
cursor: str | None = None,
|
|
||||||
limit: int = 50,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.list_capabilities(
|
|
||||||
query=query,
|
|
||||||
source_id=source_id,
|
|
||||||
cursor=cursor,
|
|
||||||
limit=limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def inspect_capability(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
qualified_name: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.inspect_capability(qualified_name=qualified_name)
|
|
||||||
|
|
||||||
async def call_capability(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
qualified_name: str,
|
|
||||||
payload: dict[str, Any],
|
|
||||||
deployment_id: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.call_capability(
|
|
||||||
qualified_name=qualified_name,
|
|
||||||
payload=payload,
|
|
||||||
deployment_id=deployment_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# -- artifacts --
|
|
||||||
|
|
||||||
async def list_artifacts(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
query: str | None = None,
|
|
||||||
kind: ArtifactKind | None = None,
|
|
||||||
cursor: str | None = None,
|
|
||||||
limit: int = 50,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.list_artifacts(
|
|
||||||
query=query,
|
|
||||||
kind=kind,
|
|
||||||
cursor=cursor,
|
|
||||||
limit=limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def inspect_artifact(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.inspect_artifact(
|
|
||||||
artifact_id=artifact_id,
|
|
||||||
version=version,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def save_artifact(
|
|
||||||
self,
|
|
||||||
artifact: dict[str, Any],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.save_artifact(artifact)
|
|
||||||
|
|
||||||
async def create_artifact_from_plan(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
title: str,
|
|
||||||
plan: dict[str, Any],
|
|
||||||
outcomes: Sequence[str],
|
|
||||||
kind: ArtifactKind = "workflow",
|
|
||||||
description: str | None = None,
|
|
||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
||||||
source_bindings: dict[str, str] | None = None,
|
|
||||||
created_from_catalog_version: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.create_artifact_from_plan(
|
|
||||||
artifact_id=artifact_id,
|
|
||||||
version=version,
|
|
||||||
title=title,
|
|
||||||
plan=plan,
|
|
||||||
outcomes=outcomes,
|
|
||||||
kind=kind,
|
|
||||||
description=description,
|
|
||||||
required_capabilities=required_capabilities,
|
|
||||||
source_bindings=source_bindings,
|
|
||||||
created_from_catalog_version=created_from_catalog_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_artifact_from_draft(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
title: str,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
outcomes: Sequence[str],
|
|
||||||
kind: ArtifactKind = "workflow",
|
|
||||||
description: str | None = None,
|
|
||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
||||||
source_bindings: dict[str, str] | None = None,
|
|
||||||
created_from_catalog_version: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.create_artifact_from_draft(
|
|
||||||
artifact_id=artifact_id,
|
|
||||||
version=version,
|
|
||||||
title=title,
|
|
||||||
draft=draft,
|
|
||||||
outcomes=outcomes,
|
|
||||||
kind=kind,
|
|
||||||
description=description,
|
|
||||||
required_capabilities=required_capabilities,
|
|
||||||
source_bindings=source_bindings,
|
|
||||||
created_from_catalog_version=created_from_catalog_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_artifact_from_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
title: str,
|
|
||||||
outcomes: Sequence[str],
|
|
||||||
kind: ArtifactKind = "workflow",
|
|
||||||
description: str | None = None,
|
|
||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
||||||
source_bindings: dict[str, str] | None = None,
|
|
||||||
created_from_catalog_version: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.create_artifact_from_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
artifact_id=artifact_id,
|
|
||||||
version=version,
|
|
||||||
title=title,
|
|
||||||
outcomes=outcomes,
|
|
||||||
kind=kind,
|
|
||||||
description=description,
|
|
||||||
required_capabilities=required_capabilities,
|
|
||||||
source_bindings=source_bindings,
|
|
||||||
created_from_catalog_version=created_from_catalog_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_wrapper_from_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
title: str,
|
|
||||||
outcomes: Sequence[str],
|
|
||||||
description: str | None = None,
|
|
||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
||||||
source_bindings: dict[str, str] | None = None,
|
|
||||||
created_from_catalog_version: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.create_wrapper_from_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
artifact_id=artifact_id,
|
|
||||||
version=version,
|
|
||||||
title=title,
|
|
||||||
outcomes=outcomes,
|
|
||||||
description=description,
|
|
||||||
required_capabilities=required_capabilities,
|
|
||||||
source_bindings=source_bindings,
|
|
||||||
created_from_catalog_version=created_from_catalog_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
# -- drafts --
|
|
||||||
|
|
||||||
async def validate_draft(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.validate_draft(draft=draft)
|
|
||||||
|
|
||||||
async def compile_draft(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.compile_draft(draft=draft)
|
|
||||||
|
|
||||||
async def patch_draft(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
patch: list[dict[str, Any]],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.patch_draft(draft=draft, patch=patch)
|
|
||||||
|
|
||||||
# -- draft workspaces --
|
|
||||||
|
|
||||||
async def list_draft_workspaces(self) -> dict[str, Any]:
|
|
||||||
return await self._handlers.list_draft_workspaces()
|
|
||||||
|
|
||||||
async def create_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
title: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.create_draft_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
draft=draft,
|
|
||||||
title=title,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
include_draft: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.get_draft_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
include_draft=include_draft,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def delete_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.delete_draft_workspace(workspace_id=workspace_id)
|
|
||||||
|
|
||||||
async def validate_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.validate_draft_workspace(workspace_id=workspace_id)
|
|
||||||
|
|
||||||
async def patch_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
patch: list[dict[str, Any]],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.patch_draft_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
patch=patch,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_draft_name(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
name: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.set_draft_name(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
name=name,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_draft_route(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
outcome: str,
|
|
||||||
target: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.set_draft_route(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
step_id=step_id,
|
|
||||||
outcome=outcome,
|
|
||||||
target=target,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_step_input_map(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
input_map: dict[str, str],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.set_step_input_map(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
step_id=step_id,
|
|
||||||
input_map=input_map,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_step_output_map(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
output_map: dict[str, str],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.set_step_output_map(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
step_id=step_id,
|
|
||||||
output_map=output_map,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_minimal_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
name: str,
|
|
||||||
capability_name: str,
|
|
||||||
input_schema: dict[str, Any],
|
|
||||||
state_schema: dict[str, Any],
|
|
||||||
output_schema: dict[str, Any],
|
|
||||||
input: Sequence[Any] | None = None,
|
|
||||||
output: Sequence[Any] | None = None,
|
|
||||||
input_map: dict[str, str] | None = None,
|
|
||||||
output_map: dict[str, str] | None = None,
|
|
||||||
error_message_source: Any | None = None,
|
|
||||||
title: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.create_minimal_draft_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
name=name,
|
|
||||||
capability_name=capability_name,
|
|
||||||
input_schema=input_schema,
|
|
||||||
state_schema=state_schema,
|
|
||||||
output_schema=output_schema,
|
|
||||||
input=input,
|
|
||||||
output=output,
|
|
||||||
input_map=input_map,
|
|
||||||
output_map=output_map,
|
|
||||||
error_message_source=error_message_source,
|
|
||||||
title=title,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_draft_workspace_from_capability(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
capability_name: str,
|
|
||||||
name: str | None = None,
|
|
||||||
title: str | None = None,
|
|
||||||
input_schema: dict[str, Any] | None = None,
|
|
||||||
state_schema: dict[str, Any] | None = None,
|
|
||||||
output_schema: dict[str, Any] | None = None,
|
|
||||||
input: Sequence[Any] | None = None,
|
|
||||||
output: Sequence[Any] | None = None,
|
|
||||||
input_map: dict[str, str] | None = None,
|
|
||||||
output_map: dict[str, str] | None = None,
|
|
||||||
error_message_source: Any | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.create_draft_workspace_from_capability(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
capability_name=capability_name,
|
|
||||||
name=name,
|
|
||||||
title=title,
|
|
||||||
input_schema=input_schema,
|
|
||||||
state_schema=state_schema,
|
|
||||||
output_schema=output_schema,
|
|
||||||
input=input,
|
|
||||||
output=output,
|
|
||||||
input_map=input_map,
|
|
||||||
output_map=output_map,
|
|
||||||
error_message_source=error_message_source,
|
|
||||||
)
|
|
||||||
|
|
||||||
# -- deployments --
|
|
||||||
|
|
||||||
async def list_deployments(self) -> dict[str, Any]:
|
|
||||||
return await self._handlers.list_deployments()
|
|
||||||
|
|
||||||
async def inspect_deployment(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
deployment_id: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.inspect_deployment(deployment_id=deployment_id)
|
|
||||||
|
|
||||||
async def save_deployment(
|
|
||||||
self,
|
|
||||||
deployment: dict[str, Any],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.save_deployment(deployment)
|
|
||||||
|
|
||||||
async def delete_deployment(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
deployment_id: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.delete_deployment(deployment_id=deployment_id)
|
|
||||||
|
|
||||||
async def validate_deployment(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
deployment_id: str,
|
|
||||||
live_check: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.validate_deployment(
|
|
||||||
deployment_id=deployment_id,
|
|
||||||
live_check=live_check,
|
|
||||||
)
|
|
||||||
|
|
||||||
# -- runs --
|
|
||||||
|
|
||||||
async def run_deployment(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
deployment_id: str,
|
|
||||||
workflow_input: dict[str, Any],
|
|
||||||
trace_range: ApiTraceRange | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.run_deployment(
|
|
||||||
deployment_id=deployment_id,
|
|
||||||
workflow_input=workflow_input,
|
|
||||||
trace_range=_to_handler_trace_range(trace_range)
|
|
||||||
if trace_range is not None
|
|
||||||
else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def resume_run(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
run_id: str,
|
|
||||||
resume_payload: dict[str, Any],
|
|
||||||
resume_outcome: str = "submitted",
|
|
||||||
trace_range: ApiTraceRange | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.resume_run(
|
|
||||||
run_id=run_id,
|
|
||||||
resume_payload=resume_payload,
|
|
||||||
resume_outcome=resume_outcome,
|
|
||||||
trace_range=_to_handler_trace_range(trace_range)
|
|
||||||
if trace_range is not None
|
|
||||||
else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def inspect_run(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
run_id: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.inspect_run(run_id=run_id)
|
|
||||||
|
|
||||||
async def read_run_trace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
run_id: str,
|
|
||||||
trace_range: ApiTraceRange,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._handlers.read_run_trace(
|
|
||||||
run_id=run_id,
|
|
||||||
trace_range=_to_handler_trace_range(trace_range),
|
|
||||||
)
|
|
||||||
@@ -1,497 +1,26 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from typing import TYPE_CHECKING
|
||||||
from typing import TYPE_CHECKING, Any
|
|
||||||
|
|
||||||
from wf_artifacts import (
|
from wf_api import WorkflowApi
|
||||||
ArtifactKind,
|
|
||||||
)
|
|
||||||
from wf_core.models.steps import (
|
|
||||||
InputBinding,
|
|
||||||
OutputBinding,
|
|
||||||
)
|
|
||||||
from wf_core.paths import GraphSourcePath
|
|
||||||
|
|
||||||
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.listing import paged_list_payload
|
|
||||||
from wf_api.models import RawWorkflowPlan
|
|
||||||
from wf_api.runs import WorkflowRunApi
|
|
||||||
|
|
||||||
from ..broker.service.workflow_operation_context import context_from_service
|
from ..broker.service.workflow_operation_context import context_from_service
|
||||||
from .models import TraceRange
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..broker.service import WfMcpService
|
from ..broker.service import WfMcpService
|
||||||
|
|
||||||
|
|
||||||
class WorkflowSurfaceHandlers:
|
class WorkflowSurfaceHandlers(WorkflowApi):
|
||||||
"""Reusable implementation behind MCP workflow artifact tools."""
|
"""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:
|
def __init__(self, service: WfMcpService) -> None:
|
||||||
self.service = service
|
self.service = service
|
||||||
context = context_from_service(service)
|
super().__init__(context_from_service(service))
|
||||||
self._capabilities = WorkflowCapabilityApi(context)
|
|
||||||
self._drafts = WorkflowDraftApi(context)
|
|
||||||
self._artifacts = WorkflowArtifactApi(context)
|
|
||||||
self._deployments = WorkflowDeploymentApi(context)
|
|
||||||
self._runs = WorkflowRunApi(context)
|
|
||||||
|
|
||||||
async def list_artifacts(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
query: str | None = None,
|
|
||||||
kind: ArtifactKind | None = None,
|
|
||||||
cursor: str | None = None,
|
|
||||||
limit: int = 50,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Return compact paged saved artifact summaries.
|
|
||||||
|
|
||||||
Saved artifacts can contain full raw workflow plans, so list results
|
__all__ = ["WorkflowSurfaceHandlers"]
|
||||||
deliberately stay summary-only. Use inspect/run tools for detail.
|
|
||||||
"""
|
|
||||||
if self.service.artifact_store is None:
|
|
||||||
return paged_list_payload("nodes", [], cursor=cursor, limit=limit)
|
|
||||||
return await self._artifacts.list_artifacts(
|
|
||||||
query=query,
|
|
||||||
kind=kind,
|
|
||||||
cursor=cursor,
|
|
||||||
limit=limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def list_capabilities(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
query: str | None = None,
|
|
||||||
source_id: str | None = None,
|
|
||||||
cursor: str | None = None,
|
|
||||||
limit: int = 50,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Return compact paged planner-visible workflow capability summaries."""
|
|
||||||
return await self._capabilities.list_capabilities(
|
|
||||||
query=query,
|
|
||||||
source_id=source_id,
|
|
||||||
cursor=cursor,
|
|
||||||
limit=limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def inspect_capability(self, *, qualified_name: str) -> dict[str, Any]:
|
|
||||||
"""Return one planner-visible workflow capability contract."""
|
|
||||||
return await self._capabilities.inspect_capability(
|
|
||||||
qualified_name=qualified_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def call_capability(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
qualified_name: str,
|
|
||||||
payload: dict[str, Any],
|
|
||||||
deployment_id: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Execute one planner-visible workflow capability for authoring tests."""
|
|
||||||
return await self._capabilities.call_capability(
|
|
||||||
qualified_name=qualified_name,
|
|
||||||
payload=payload,
|
|
||||||
deployment_id=deployment_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def save_artifact(self, artifact: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
if self.service.artifact_store is None:
|
|
||||||
raise KeyError("workflow artifact store is not configured")
|
|
||||||
return await self._artifacts.save_artifact(artifact)
|
|
||||||
|
|
||||||
async def create_artifact_from_plan(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
title: str,
|
|
||||||
plan: RawWorkflowPlan | dict[str, Any],
|
|
||||||
outcomes: Sequence[str],
|
|
||||||
kind: ArtifactKind = "workflow",
|
|
||||||
description: str | None = None,
|
|
||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
||||||
source_bindings: dict[str, str] | None = None,
|
|
||||||
created_from_catalog_version: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
if self.service.artifact_store is None:
|
|
||||||
raise KeyError("workflow artifact store is not configured")
|
|
||||||
return await self._artifacts.create_artifact_from_plan(
|
|
||||||
artifact_id=artifact_id,
|
|
||||||
version=version,
|
|
||||||
title=title,
|
|
||||||
plan=plan,
|
|
||||||
outcomes=outcomes,
|
|
||||||
kind=kind,
|
|
||||||
description=description,
|
|
||||||
required_capabilities=required_capabilities,
|
|
||||||
source_bindings=source_bindings,
|
|
||||||
created_from_catalog_version=created_from_catalog_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def validate_draft(self, *, draft: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
return await self._drafts.validate_draft(draft=draft)
|
|
||||||
|
|
||||||
async def compile_draft(self, *, draft: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
return await self._drafts.compile_draft(draft=draft)
|
|
||||||
|
|
||||||
async def create_artifact_from_draft(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
title: str,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
outcomes: Sequence[str],
|
|
||||||
kind: ArtifactKind = "workflow",
|
|
||||||
description: str | None = None,
|
|
||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
||||||
source_bindings: dict[str, str] | None = None,
|
|
||||||
created_from_catalog_version: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
if self.service.artifact_store is None:
|
|
||||||
raise KeyError("workflow artifact store is not configured")
|
|
||||||
return await self._artifacts.create_artifact_from_draft(
|
|
||||||
artifact_id=artifact_id,
|
|
||||||
version=version,
|
|
||||||
title=title,
|
|
||||||
draft=draft,
|
|
||||||
outcomes=outcomes,
|
|
||||||
kind=kind,
|
|
||||||
description=description,
|
|
||||||
required_capabilities=required_capabilities,
|
|
||||||
source_bindings=source_bindings,
|
|
||||||
created_from_catalog_version=created_from_catalog_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def patch_draft(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
patch: list[dict[str, Any]],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._drafts.patch_draft(draft=draft, patch=patch)
|
|
||||||
|
|
||||||
async def list_draft_workspaces(self) -> dict[str, Any]:
|
|
||||||
"""Return compact summaries for stored draft workspaces."""
|
|
||||||
return await self._drafts.list_draft_workspaces()
|
|
||||||
|
|
||||||
async def create_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
draft: dict[str, Any],
|
|
||||||
title: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._drafts.create_draft_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
draft=draft,
|
|
||||||
title=title,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
include_draft: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._drafts.get_draft_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
include_draft=include_draft,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def delete_draft_workspace(self, *, workspace_id: str) -> dict[str, Any]:
|
|
||||||
return await self._drafts.delete_draft_workspace(workspace_id=workspace_id)
|
|
||||||
|
|
||||||
async def validate_draft_workspace(self, *, workspace_id: str) -> dict[str, Any]:
|
|
||||||
"""Refresh stored validation status without changing draft revision."""
|
|
||||||
return await self._drafts.validate_draft_workspace(workspace_id=workspace_id)
|
|
||||||
|
|
||||||
async def patch_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
patch: list[dict[str, Any]],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._drafts.patch_draft_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
patch=patch,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_draft_name(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
name: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._drafts.set_draft_name(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
name=name,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_draft_route(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
outcome: str,
|
|
||||||
target: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._drafts.set_draft_route(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
step_id=step_id,
|
|
||||||
outcome=outcome,
|
|
||||||
target=target,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_step_input_map(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
input_map: dict[str, str],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._drafts.set_step_input_map(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
step_id=step_id,
|
|
||||||
input_map=input_map,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_step_output_map(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
output_map: dict[str, str],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._drafts.set_step_output_map(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
step_id=step_id,
|
|
||||||
output_map=output_map,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_minimal_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
name: str,
|
|
||||||
capability_name: str,
|
|
||||||
input_schema: dict[str, Any],
|
|
||||||
state_schema: dict[str, Any],
|
|
||||||
output_schema: dict[str, Any],
|
|
||||||
input: Sequence[InputBinding] | None = None,
|
|
||||||
output: Sequence[OutputBinding] | None = None,
|
|
||||||
input_map: dict[str, str] | None = None,
|
|
||||||
output_map: dict[str, str] | None = None,
|
|
||||||
error_message_source: str | GraphSourcePath | None = None,
|
|
||||||
title: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Bootstrap the smallest patchable draft around one workflow capability."""
|
|
||||||
return await self._drafts.create_minimal_draft_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
name=name,
|
|
||||||
capability_name=capability_name,
|
|
||||||
input_schema=input_schema,
|
|
||||||
state_schema=state_schema,
|
|
||||||
output_schema=output_schema,
|
|
||||||
input=input,
|
|
||||||
output=output,
|
|
||||||
input_map=input_map,
|
|
||||||
output_map=output_map,
|
|
||||||
error_message_source=error_message_source,
|
|
||||||
title=title,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_draft_workspace_from_capability(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
capability_name: str,
|
|
||||||
name: str | None = None,
|
|
||||||
title: str | None = None,
|
|
||||||
input_schema: dict[str, Any] | None = None,
|
|
||||||
state_schema: dict[str, Any] | None = None,
|
|
||||||
output_schema: dict[str, Any] | None = None,
|
|
||||||
input: Sequence[InputBinding] | None = None,
|
|
||||||
output: Sequence[OutputBinding] | None = None,
|
|
||||||
input_map: dict[str, str] | None = None,
|
|
||||||
output_map: dict[str, str] | None = None,
|
|
||||||
error_message_source: str | GraphSourcePath | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Create a patchable draft workspace from inspect_capability hints."""
|
|
||||||
return await self._capabilities.create_draft_workspace_from_capability(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
capability_name=capability_name,
|
|
||||||
name=name,
|
|
||||||
title=title,
|
|
||||||
input_schema=input_schema,
|
|
||||||
state_schema=state_schema,
|
|
||||||
output_schema=output_schema,
|
|
||||||
input=input,
|
|
||||||
output=output,
|
|
||||||
input_map=input_map,
|
|
||||||
output_map=output_map,
|
|
||||||
error_message_source=error_message_source,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_artifact_from_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
title: str,
|
|
||||||
outcomes: Sequence[str],
|
|
||||||
kind: ArtifactKind = "workflow",
|
|
||||||
description: str | None = None,
|
|
||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
||||||
source_bindings: dict[str, str] | None = None,
|
|
||||||
created_from_catalog_version: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
if self.service.artifact_store is None:
|
|
||||||
raise KeyError("workflow artifact store is not configured")
|
|
||||||
return await self._artifacts.create_artifact_from_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
artifact_id=artifact_id,
|
|
||||||
version=version,
|
|
||||||
title=title,
|
|
||||||
outcomes=outcomes,
|
|
||||||
kind=kind,
|
|
||||||
description=description,
|
|
||||||
required_capabilities=required_capabilities,
|
|
||||||
source_bindings=source_bindings,
|
|
||||||
created_from_catalog_version=created_from_catalog_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_wrapper_from_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
title: str,
|
|
||||||
outcomes: Sequence[str],
|
|
||||||
description: str | None = None,
|
|
||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
||||||
source_bindings: dict[str, str] | None = None,
|
|
||||||
created_from_catalog_version: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Save the current draft workspace as a callable wrapper artifact."""
|
|
||||||
if self.service.artifact_store is None:
|
|
||||||
raise KeyError("workflow artifact store is not configured")
|
|
||||||
return await self._artifacts.create_wrapper_from_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
artifact_id=artifact_id,
|
|
||||||
version=version,
|
|
||||||
title=title,
|
|
||||||
outcomes=outcomes,
|
|
||||||
description=description,
|
|
||||||
required_capabilities=required_capabilities,
|
|
||||||
source_bindings=source_bindings,
|
|
||||||
created_from_catalog_version=created_from_catalog_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def inspect_artifact(
|
|
||||||
self, *, artifact_id: str, version: int
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
if self.service.artifact_store is None:
|
|
||||||
raise KeyError("workflow artifact store is not configured")
|
|
||||||
return await self._artifacts.inspect_artifact(
|
|
||||||
artifact_id=artifact_id,
|
|
||||||
version=version,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def list_deployments(self) -> dict[str, Any]:
|
|
||||||
if self.service.artifact_store is None:
|
|
||||||
return {"deployments": []}
|
|
||||||
return await self._deployments.list_deployments()
|
|
||||||
|
|
||||||
async def inspect_deployment(self, *, deployment_id: str) -> dict[str, Any]:
|
|
||||||
if self.service.artifact_store is None:
|
|
||||||
raise KeyError("workflow artifact store is not configured")
|
|
||||||
return await self._deployments.inspect_deployment(
|
|
||||||
deployment_id=deployment_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def save_deployment(self, deployment: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
if self.service.artifact_store is None:
|
|
||||||
raise KeyError("workflow artifact store is not configured")
|
|
||||||
return await self._deployments.save_deployment(deployment)
|
|
||||||
|
|
||||||
async def delete_deployment(self, *, deployment_id: str) -> dict[str, Any]:
|
|
||||||
"""Delete one mutable deployment environment binding."""
|
|
||||||
if self.service.artifact_store is None:
|
|
||||||
raise KeyError("workflow artifact store is not configured")
|
|
||||||
return await self._deployments.delete_deployment(
|
|
||||||
deployment_id=deployment_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def validate_deployment(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
deployment_id: str,
|
|
||||||
live_check: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
if self.service.artifact_store is None:
|
|
||||||
raise KeyError("workflow artifact store is not configured")
|
|
||||||
return await self._deployments.validate_deployment(
|
|
||||||
deployment_id=deployment_id,
|
|
||||||
live_check=live_check,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def run_deployment(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
deployment_id: str,
|
|
||||||
workflow_input: dict[str, Any],
|
|
||||||
trace_range: TraceRange | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._runs.run_deployment(
|
|
||||||
deployment_id=deployment_id,
|
|
||||||
workflow_input=workflow_input,
|
|
||||||
trace_range=trace_range,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def resume_run(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
run_id: str,
|
|
||||||
resume_payload: dict[str, Any],
|
|
||||||
resume_outcome: str = "submitted",
|
|
||||||
trace_range: TraceRange | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Resume one durable interrupted deployment run."""
|
|
||||||
return await self._runs.resume_run(
|
|
||||||
run_id=run_id,
|
|
||||||
resume_payload=resume_payload,
|
|
||||||
resume_outcome=resume_outcome,
|
|
||||||
trace_range=trace_range,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def inspect_run(self, *, run_id: str) -> dict[str, Any]:
|
|
||||||
"""Return one durable stopped-run summary without debug trace entries."""
|
|
||||||
return await self._runs.inspect_run(run_id=run_id)
|
|
||||||
|
|
||||||
async def read_run_trace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
run_id: str,
|
|
||||||
trace_range: TraceRange,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Return only a caller-bounded debug trace slice from a stopped run."""
|
|
||||||
return await self._runs.read_run_trace(
|
|
||||||
run_id=run_id,
|
|
||||||
trace_range=trace_range,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ from pydantic import Field
|
|||||||
from wf_artifacts import ArtifactKind
|
from wf_artifacts import ArtifactKind
|
||||||
from wf_artifacts.models import RequiredCapability
|
from wf_artifacts.models import RequiredCapability
|
||||||
from wf_api import WorkflowApi
|
from wf_api import WorkflowApi
|
||||||
from wf_api.backend import TraceRange as ApiTraceRange
|
|
||||||
from wf_mcp.broker.service import WfMcpService
|
from wf_mcp.broker.service import WfMcpService
|
||||||
from wf_mcp.broker.service.workflow_api_backend import WfMcpWorkflowApiBackend
|
from wf_mcp.broker.service.workflow_operation_context import context_from_service
|
||||||
from .models import (
|
from .models import (
|
||||||
CallCapabilityResult,
|
CallCapabilityResult,
|
||||||
CreateArtifactFromWorkspaceRequest,
|
CreateArtifactFromWorkspaceRequest,
|
||||||
@@ -37,10 +36,7 @@ from .models import (
|
|||||||
|
|
||||||
def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None:
|
def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None:
|
||||||
"""Register stable workflow tools on the public MCP server surface."""
|
"""Register stable workflow tools on the public MCP server surface."""
|
||||||
handlers = WorkflowApi(WfMcpWorkflowApiBackend(service))
|
handlers = WorkflowApi(context_from_service(service))
|
||||||
|
|
||||||
def _to_api_trace_range(tr: TraceRange) -> ApiTraceRange:
|
|
||||||
return ApiTraceRange(start=tr.start, limit=tr.limit)
|
|
||||||
|
|
||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.list_artifacts",
|
name="wf.workflow.list_artifacts",
|
||||||
@@ -669,9 +665,7 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
await handlers.run_deployment(
|
await handlers.run_deployment(
|
||||||
deployment_id=deployment_id,
|
deployment_id=deployment_id,
|
||||||
workflow_input=workflow_input,
|
workflow_input=workflow_input,
|
||||||
trace_range=_to_api_trace_range(trace_range)
|
trace_range=trace_range,
|
||||||
if trace_range is not None
|
|
||||||
else None,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -703,9 +697,7 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
resume_payload=resume_payload,
|
resume_payload=resume_payload,
|
||||||
resume_outcome=resume_outcome,
|
resume_outcome=resume_outcome,
|
||||||
trace_range=_to_api_trace_range(trace_range)
|
trace_range=trace_range,
|
||||||
if trace_range is not None
|
|
||||||
else None,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -742,6 +734,6 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
return RunDeploymentResult.model_validate(
|
return RunDeploymentResult.model_validate(
|
||||||
await handlers.read_run_trace(
|
await handlers.read_run_trace(
|
||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
trace_range=_to_api_trace_range(trace_range),
|
trace_range=trace_range,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ def test_load_cli_context_returns_workflow_api(tmp_path: Path) -> None:
|
|||||||
context = load_cli_context(config_path)
|
context = load_cli_context(config_path)
|
||||||
|
|
||||||
assert isinstance(context.handlers, WorkflowApi)
|
assert isinstance(context.handlers, WorkflowApi)
|
||||||
assert hasattr(context.handlers, "backend")
|
assert hasattr(context.handlers, "context")
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
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"}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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",
|
||||||
|
)
|
||||||
@@ -18,3 +18,12 @@ def test_canonical_and_compat_are_identical() -> None:
|
|||||||
from wf_mcp.models import RawWorkflowPlan as Compat
|
from wf_mcp.models import RawWorkflowPlan as Compat
|
||||||
|
|
||||||
assert Canonical is Compat
|
assert Canonical is Compat
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -30,4 +30,9 @@ def test_load_cli_context_builds_service_and_handlers(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
assert context.config_path == config_path
|
assert context.config_path == config_path
|
||||||
assert context.service.connections.list_all()[0].id == "demo.personal"
|
assert context.service.connections.list_all()[0].id == "demo.personal"
|
||||||
assert context.handlers.backend._handlers.service is context.service # type: ignore[attr-defined]
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user