398 lines
12 KiB
Markdown
398 lines
12 KiB
Markdown
# Saved Subgraph Platform Resolution 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:** Execute non-interrupting saved child workflow artifacts natively from a parent deployment while resolving all descendant dependencies through the parent deployment binding environment.
|
|
|
|
**Architecture:** `wf_core` remains unaware of artifact storage: it only looks up caller-prepared `PreparedSubgraph` dependencies by structural workflow-ref display key. A new focused workflow-surface resolver loads exact child artifact versions, traverses descendants with cycle detection, validates descendant capabilities and existing interrupt limitations, and prepares child workflows for `WfMcpService` execution. Future child deployment overrides remain outside this slice and must be keyed by subgraph use site, not artifact identity.
|
|
|
|
**Tech Stack:** Python 3.14, Pydantic v2, `wf_core` native subgraphs, `wf_artifacts` stores/deployment diagnostics, `wf_mcp` workflow surface, pytest, ruff, basedpyright.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
- Modify `src/wf_core/runtime/subgraphs.py`: accept a caller-prepared structural saved `WorkflowRef`; do not load artifacts.
|
|
- Create `src/wf_mcp/workflow_surface/saved_subgraphs.py`: own saved-child traversal, diagnostic production, and preparation of executable child dependencies.
|
|
- Modify `src/wf_mcp/workflow_surface/handlers.py`: include descendant dependency/interrupt diagnostics in deployment validation.
|
|
- Modify `src/wf_mcp/broker/service/core.py`: supply prepared saved children to `execute_workflow_async`.
|
|
- Modify `tests/core/test_subgraph_step.py`: cover core execution of an already-prepared saved ref.
|
|
- Create `tests/wf_mcp/test_saved_subgraphs.py`: cover deployment-bound saved child execution and unrunnable descendant cases.
|
|
- Modify `docs/current_roadmap.md` and `docs/workflow_artifacts.md`: record the new runnable saved-child path and remaining persisted-resume limitation.
|
|
|
|
### Task 1: Core Accepts Prepared Saved References
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_core/runtime/subgraphs.py`
|
|
- Test: `tests/core/test_subgraph_step.py`
|
|
|
|
- [ ] **Step 1: Write the failing core test**
|
|
|
|
Add a test that constructs a parent `SubgraphNode` with:
|
|
|
|
```python
|
|
workflow=WorkflowRef(artifact_id="child", version=1)
|
|
```
|
|
|
|
and supplies:
|
|
|
|
```python
|
|
subgraphs={
|
|
"workflow.child.v1": PreparedSubgraph(
|
|
workflow=child,
|
|
registry={"echo": echo_handler},
|
|
reducers={},
|
|
)
|
|
}
|
|
```
|
|
|
|
Assert the parent run completes and maps the child output into parent state.
|
|
|
|
- [ ] **Step 2: Run the core test to verify it fails**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest -q tests/core/test_subgraph_step.py
|
|
```
|
|
|
|
Expected: FAIL because `resolve_prepared_subgraph()` currently rejects a saved structural ref before checking supplied prepared dependencies.
|
|
|
|
- [ ] **Step 3: Make prepared dependency lookup structural**
|
|
|
|
Update `resolve_prepared_subgraph()`:
|
|
|
|
```python
|
|
def resolve_prepared_subgraph(
|
|
ref: WorkflowRef,
|
|
subgraphs: Mapping[str, PreparedSubgraph[HandlerT]] | None,
|
|
) -> PreparedSubgraph[HandlerT]:
|
|
"""Resolve a caller-prepared child; artifact loading is not a core concern."""
|
|
key = ref.name if ref.name is not None else ref.display
|
|
prepared = None if subgraphs is None else subgraphs.get(key)
|
|
if prepared is None:
|
|
raise WorkflowExecutionError(
|
|
f"no prepared child workflow registered for {ref.display!r}"
|
|
)
|
|
return prepared
|
|
```
|
|
|
|
- [ ] **Step 4: Run the core test to verify it passes**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest -q tests/core/test_subgraph_step.py
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
### Task 2: Traverse and Prepare Saved Child Artifacts
|
|
|
|
**Files:**
|
|
|
|
- Create: `src/wf_mcp/workflow_surface/saved_subgraphs.py`
|
|
- Test: `tests/wf_mcp/test_saved_subgraphs.py`
|
|
|
|
- [ ] **Step 1: Write failing traversal tests**
|
|
|
|
Add focused tests for a helper that receives a root artifact plan containing a
|
|
structural `SubgraphNode` ref and a `FileWorkflowArtifactStore`:
|
|
|
|
```python
|
|
resolution = resolve_saved_subgraph_tree(
|
|
root_artifact=parent,
|
|
artifact_store=artifact_store,
|
|
)
|
|
assert resolution.artifacts_by_ref["workflow.child.v1"].id == "child"
|
|
assert resolution.diagnostics == []
|
|
```
|
|
|
|
Add tests asserting:
|
|
|
|
```python
|
|
assert resolution.diagnostics[0].code == "workflow_dependency_missing"
|
|
assert resolution.diagnostics[0].code == "workflow_dependency_cycle"
|
|
```
|
|
|
|
for a missing child and a parent/child cycle respectively.
|
|
|
|
- [ ] **Step 2: Run tests to verify they fail**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest -q tests/wf_mcp/test_saved_subgraphs.py
|
|
```
|
|
|
|
Expected: FAIL because `saved_subgraphs.py` and its resolver do not exist.
|
|
|
|
- [ ] **Step 3: Implement saved-child tree discovery**
|
|
|
|
Create a typed resolution object:
|
|
|
|
```python
|
|
@dataclass(frozen=True, slots=True)
|
|
class SavedSubgraphTree:
|
|
"""Saved descendant artifacts keyed by structural workflow-ref display."""
|
|
|
|
artifacts_by_ref: dict[str, WorkflowArtifact]
|
|
diagnostics: list[DependencyDiagnostic]
|
|
```
|
|
|
|
Implement:
|
|
|
|
```python
|
|
def resolve_saved_subgraph_tree(
|
|
*,
|
|
root_artifact: WorkflowArtifact,
|
|
artifact_store: WorkflowArtifactStore,
|
|
) -> SavedSubgraphTree:
|
|
"""Load exact saved descendants and report missing refs or cycles."""
|
|
```
|
|
|
|
Parse each artifact plan as `RawWorkflowPlan`, visit only `SubgraphNode`
|
|
instances whose `workflow.artifact_id` and `.version` are present, load the
|
|
exact artifact, and recurse. Keep an active artifact stack of
|
|
`(artifact_id, version)` values so only recursion cycles fail; repeated reuse
|
|
of the same child in separate branches is allowed.
|
|
|
|
Construct direct diagnostics without inventing a fake capability:
|
|
|
|
```python
|
|
DependencyDiagnostic(
|
|
severity=DiagnosticSeverity.ERROR,
|
|
code="workflow_dependency_missing",
|
|
logical_ref=ref.display,
|
|
message=f"Saved child workflow {ref.display!r} is unavailable.",
|
|
repair_hint="Save the referenced artifact version or update the parent graph.",
|
|
)
|
|
```
|
|
|
|
Use analogous text for `workflow_dependency_cycle`.
|
|
|
|
- [ ] **Step 4: Run traversal tests to verify they pass**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest -q tests/wf_mcp/test_saved_subgraphs.py
|
|
```
|
|
|
|
Expected: traversal tests PASS.
|
|
|
|
### Task 3: Validate Descendants Under One Deployment Environment
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_mcp/workflow_surface/saved_subgraphs.py`
|
|
- Modify: `src/wf_mcp/workflow_surface/handlers.py`
|
|
- Test: `tests/wf_mcp/test_saved_subgraphs.py`
|
|
|
|
- [ ] **Step 1: Write failing public-validation tests**
|
|
|
|
Add tests that save a parent artifact referencing a child artifact whose plan
|
|
uses logical node `demo.echo_tool`. Save one parent deployment:
|
|
|
|
```python
|
|
WorkflowDeployment(
|
|
id="parent.personal",
|
|
artifact_id="parent",
|
|
artifact_version=1,
|
|
bindings={"demo": "demo.personal"},
|
|
)
|
|
```
|
|
|
|
Assert:
|
|
|
|
```python
|
|
result = asyncio.run(handlers.validate_deployment(deployment_id="parent.personal"))
|
|
assert result["status"] == "runnable"
|
|
```
|
|
|
|
Add descendant failure assertions:
|
|
|
|
```python
|
|
assert result["diagnostics"][0]["code"] == "binding_missing"
|
|
assert result["diagnostics"][0]["logical_ref"] == "demo.echo_tool"
|
|
```
|
|
|
|
and for an interrupting saved child:
|
|
|
|
```python
|
|
assert result["status"] == "unrunnable"
|
|
assert result["diagnostics"][0]["code"] == "unsupported_interrupt"
|
|
```
|
|
|
|
The interrupt diagnostic must be reported before execution because
|
|
`run_deployment` is still one-shot.
|
|
|
|
- [ ] **Step 2: Run validation tests to verify they fail**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest -q tests/wf_mcp/test_saved_subgraphs.py
|
|
```
|
|
|
|
Expected: FAIL because `_deployment_validation()` validates only the root artifact.
|
|
|
|
- [ ] **Step 3: Add descendant validation composition**
|
|
|
|
Add a helper in `saved_subgraphs.py`:
|
|
|
|
```python
|
|
def validate_saved_subgraph_tree(
|
|
*,
|
|
tree: SavedSubgraphTree,
|
|
deployment: WorkflowDeployment,
|
|
sources: list[AvailableSource],
|
|
unsupported_interrupt: Callable[[WorkflowArtifact], DependencyDiagnostic | None],
|
|
) -> list[DependencyDiagnostic]:
|
|
"""Validate descendants in the root deployment environment."""
|
|
```
|
|
|
|
It should begin with tree discovery diagnostics, then for each loaded child
|
|
call `validate_deployment_dependencies(...)`, and finally append the existing
|
|
unsupported-interrupt diagnostic for that child when present.
|
|
|
|
Update `WorkflowSurfaceHandlers._deployment_validation()` to discover the
|
|
saved tree and extend the root diagnostic list with descendant diagnostics.
|
|
Preserve the root artifact interrupt check in `run_deployment()`; it remains
|
|
the existing surface behavior.
|
|
|
|
- [ ] **Step 4: Run validation tests to verify they pass**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest -q tests/wf_mcp/test_saved_subgraphs.py
|
|
```
|
|
|
|
Expected: descendant validation tests PASS.
|
|
|
|
### Task 4: Execute Prepared Saved Children
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_mcp/workflow_surface/saved_subgraphs.py`
|
|
- Modify: `src/wf_mcp/broker/service/core.py`
|
|
- Test: `tests/wf_mcp/test_saved_subgraphs.py`
|
|
|
|
- [ ] **Step 1: Write failing end-to-end execution tests**
|
|
|
|
Use the parent deployment and child artifact from Task 3. Assert:
|
|
|
|
```python
|
|
payload = asyncio.run(
|
|
handlers.run_deployment(
|
|
deployment_id="parent.personal",
|
|
workflow_input={"text": "hello"},
|
|
)
|
|
)
|
|
assert payload["status"] == "completed"
|
|
assert payload["output"]["echoed"] == "hello"
|
|
```
|
|
|
|
Add a nested parent -> middle -> child test where only the parent deployment
|
|
contains `{"demo": "demo.personal"}`, and assert the grandchild node executes
|
|
through the inherited binding.
|
|
|
|
- [ ] **Step 2: Run execution tests to verify they fail**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest -q tests/wf_mcp/test_saved_subgraphs.py
|
|
```
|
|
|
|
Expected: FAIL because the service does not provide prepared saved children to core.
|
|
|
|
- [ ] **Step 3: Prepare executable children and supply them to core**
|
|
|
|
Add:
|
|
|
|
```python
|
|
def prepare_saved_subgraphs(
|
|
*,
|
|
tree: SavedSubgraphTree,
|
|
deployment: WorkflowDeployment | None,
|
|
sources: dict[str, CapabilitySource],
|
|
compile_plan: Callable[[RawWorkflowPlan, dict[str, str] | None], Workflow],
|
|
) -> dict[str, PreparedSubgraph[AsyncRegistryHandler]]:
|
|
"""Compile loaded descendants with the parent deployment bindings."""
|
|
```
|
|
|
|
For each child artifact, parse its plan, resolve its node/reducer runtime
|
|
dependencies with `resolve_runtime_dependencies(...)`, compile it, and return
|
|
the dependency under its structural ref display key:
|
|
|
|
```python
|
|
prepared[workflow_ref_display] = PreparedSubgraph(
|
|
workflow=compile_plan(plan, dependencies.node_name_bindings),
|
|
registry=dependencies.node_registry,
|
|
reducers=dependencies.reducers,
|
|
)
|
|
```
|
|
|
|
Update `WfMcpService.run_workflow_from_plan()` to resolve the saved tree for
|
|
`runtime_artifact` when an artifact store exists, prepare loaded children, and
|
|
pass:
|
|
|
|
```python
|
|
subgraphs=prepared_subgraphs
|
|
```
|
|
|
|
to `execute_workflow_async(...)`.
|
|
|
|
- [ ] **Step 4: Run saved-child tests to verify they pass**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest -q tests/wf_mcp/test_saved_subgraphs.py
|
|
```
|
|
|
|
Expected: all saved-subgraph tests PASS.
|
|
|
|
### Task 5: Documentation and Full Verification
|
|
|
|
**Files:**
|
|
|
|
- Modify: `docs/current_roadmap.md`
|
|
- Modify: `docs/workflow_artifacts.md`
|
|
|
|
- [ ] **Step 1: Document current support**
|
|
|
|
Record:
|
|
|
|
- Non-interrupting saved child artifacts now run natively through deployments.
|
|
- Descendant logical dependencies inherit the root deployment binding environment.
|
|
- Missing/cyclic/interrupting saved descendants are reported as unrunnable.
|
|
- Explicit per-child deployment overrides and persisted saved-interrupt resume remain future work.
|
|
|
|
- [ ] **Step 2: Run focused and full verification**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest -q tests/core/test_subgraph_step.py tests/wf_mcp/test_saved_subgraphs.py
|
|
uv run pytest -q
|
|
uvx ruff check src/wf_core src/wf_mcp tests/core/test_subgraph_step.py tests/wf_mcp/test_saved_subgraphs.py
|
|
uvx ruff format --check src/wf_core/runtime/subgraphs.py src/wf_mcp/workflow_surface/saved_subgraphs.py src/wf_mcp/workflow_surface/handlers.py src/wf_mcp/broker/service/core.py tests/core/test_subgraph_step.py tests/wf_mcp/test_saved_subgraphs.py
|
|
uv run basedpyright --level error src/wf_core src/wf_mcp tests/core/test_subgraph_step.py tests/wf_mcp/test_saved_subgraphs.py
|
|
```
|
|
|
|
Expected: all commands pass, with the repository's intentionally skipped live
|
|
integration test remaining skipped unless its environment is provided.
|
|
|
|
## Self-Review
|
|
|
|
- Spec coverage: the plan covers exact artifact loading, inherited bindings,
|
|
cycle/missing diagnostics, preserved interrupt rejection, and native execution.
|
|
- Boundary check: artifact traversal and dependency preparation stay in
|
|
`wf_mcp`; `wf_core` accepts only caller-prepared structural refs.
|
|
- Future compatibility: no child deployment field is added; per-use-site
|
|
override remains an additive future platform feature.
|