move subgraph operations to wf_api
ok these are some composition stuff so tgis is probably the right place to live
This commit is contained in:
@@ -0,0 +1,239 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
|
from wf_artifacts import (
|
||||||
|
AvailableSource,
|
||||||
|
DependencyDiagnostic,
|
||||||
|
DiagnosticSeverity,
|
||||||
|
WorkflowArtifact,
|
||||||
|
WorkflowArtifactStore,
|
||||||
|
WorkflowDeployment,
|
||||||
|
validate_deployment_dependencies,
|
||||||
|
)
|
||||||
|
from wf_core import (
|
||||||
|
AsyncNodeHandler,
|
||||||
|
InterruptNode,
|
||||||
|
NodeUse,
|
||||||
|
PreparedSubgraph,
|
||||||
|
SubgraphNode,
|
||||||
|
Workflow,
|
||||||
|
)
|
||||||
|
from wf_core.models.steps import Step
|
||||||
|
from wf_core.models.workflow_refs import WorkflowRef
|
||||||
|
from wf_platform import CapabilitySource
|
||||||
|
|
||||||
|
from .models import RawWorkflowPlan
|
||||||
|
from .runtime_dependencies import resolve_runtime_dependencies
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"SavedSubgraphTree",
|
||||||
|
"direct_wrapper_interrupt_diagnostic",
|
||||||
|
"prepare_saved_subgraphs",
|
||||||
|
"resolve_saved_subgraph_tree",
|
||||||
|
"saved_subgraph_tree_from_snapshots",
|
||||||
|
"validate_saved_subgraph_tree",
|
||||||
|
]
|
||||||
|
|
||||||
|
_STEPS_ADAPTER = TypeAdapter(list[Step])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SavedSubgraphTree:
|
||||||
|
"""Saved descendant artifacts prepared from one root artifact boundary."""
|
||||||
|
|
||||||
|
artifacts_by_ref: dict[str, WorkflowArtifact]
|
||||||
|
diagnostics: list[DependencyDiagnostic]
|
||||||
|
|
||||||
|
|
||||||
|
def saved_subgraph_tree_from_snapshots(
|
||||||
|
child_artifacts: list[WorkflowArtifact],
|
||||||
|
) -> SavedSubgraphTree:
|
||||||
|
"""Restore the exact saved-child definitions pinned by a durable run."""
|
||||||
|
return SavedSubgraphTree(
|
||||||
|
artifacts_by_ref={
|
||||||
|
f"workflow.{artifact.id}.v{artifact.version}": artifact
|
||||||
|
for artifact in child_artifacts
|
||||||
|
},
|
||||||
|
diagnostics=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_saved_subgraph_tree(
|
||||||
|
*,
|
||||||
|
root_artifact: WorkflowArtifact,
|
||||||
|
artifact_store: WorkflowArtifactStore,
|
||||||
|
) -> SavedSubgraphTree:
|
||||||
|
"""Load exact saved descendants and report missing refs or recursion cycles.
|
||||||
|
|
||||||
|
A saved subgraph ref identifies an immutable artifact version. This loader
|
||||||
|
intentionally does not resolve capabilities or deployment bindings; it
|
||||||
|
identifies the artifact tree that later platform validation/preparation
|
||||||
|
will operate on.
|
||||||
|
"""
|
||||||
|
artifacts_by_ref: dict[str, WorkflowArtifact] = {}
|
||||||
|
diagnostics: list[DependencyDiagnostic] = []
|
||||||
|
_visit_saved_children(
|
||||||
|
artifact=root_artifact,
|
||||||
|
artifact_store=artifact_store,
|
||||||
|
active={(root_artifact.id, root_artifact.version)},
|
||||||
|
artifacts_by_ref=artifacts_by_ref,
|
||||||
|
diagnostics=diagnostics,
|
||||||
|
)
|
||||||
|
return SavedSubgraphTree(
|
||||||
|
artifacts_by_ref=artifacts_by_ref,
|
||||||
|
diagnostics=diagnostics,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_saved_subgraph_tree(
|
||||||
|
*,
|
||||||
|
tree: SavedSubgraphTree,
|
||||||
|
deployment: WorkflowDeployment,
|
||||||
|
sources: list[AvailableSource],
|
||||||
|
) -> list[DependencyDiagnostic]:
|
||||||
|
"""Validate saved descendants under the parent deployment environment."""
|
||||||
|
diagnostics = list(tree.diagnostics)
|
||||||
|
for child in tree.artifacts_by_ref.values():
|
||||||
|
diagnostics.extend(
|
||||||
|
validate_deployment_dependencies(
|
||||||
|
artifact=child,
|
||||||
|
deployment=deployment,
|
||||||
|
sources=sources,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return diagnostics
|
||||||
|
|
||||||
|
|
||||||
|
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[AsyncNodeHandler]]:
|
||||||
|
"""Compile saved descendants using one inherited deployment environment.
|
||||||
|
|
||||||
|
The tree has already fixed exact artifact versions. Binding resolution is
|
||||||
|
deliberately shared with the root deployment; per-use-site deployment
|
||||||
|
overrides are a future platform feature rather than an implicit fallback.
|
||||||
|
"""
|
||||||
|
if tree.diagnostics:
|
||||||
|
messages = "; ".join(diagnostic.message for diagnostic in tree.diagnostics)
|
||||||
|
raise ValueError(f"cannot prepare invalid saved subgraph tree: {messages}")
|
||||||
|
|
||||||
|
prepared: dict[str, PreparedSubgraph[AsyncNodeHandler]] = {}
|
||||||
|
for ref_display, child in tree.artifacts_by_ref.items():
|
||||||
|
plan = RawWorkflowPlan.model_validate(child.plan)
|
||||||
|
dependencies = resolve_runtime_dependencies(
|
||||||
|
artifact=child,
|
||||||
|
deployment=deployment,
|
||||||
|
sources=sources,
|
||||||
|
plan_node_names=[
|
||||||
|
node.node for node in plan.nodes if isinstance(node, NodeUse)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
prepared[ref_display] = PreparedSubgraph(
|
||||||
|
workflow=compile_plan(plan, dependencies.node_name_bindings),
|
||||||
|
registry=dependencies.node_registry,
|
||||||
|
reducers=dependencies.reducers,
|
||||||
|
)
|
||||||
|
return prepared
|
||||||
|
|
||||||
|
|
||||||
|
def direct_wrapper_interrupt_diagnostic(
|
||||||
|
artifact: WorkflowArtifact,
|
||||||
|
) -> DependencyDiagnostic | None:
|
||||||
|
"""Reject direct wrapper calls that cannot return a resumable run handle.
|
||||||
|
|
||||||
|
Deployment execution supports interrupt/resume through a durable `run_id`;
|
||||||
|
`call_capability` remains a single-call authoring probe.
|
||||||
|
"""
|
||||||
|
if not any(isinstance(node, InterruptNode) for node in _artifact_steps(artifact)):
|
||||||
|
return None
|
||||||
|
return DependencyDiagnostic(
|
||||||
|
severity=DiagnosticSeverity.ERROR,
|
||||||
|
code="interrupting_wrapper_call_unsupported",
|
||||||
|
logical_ref=f"workflow.{artifact.id}.v{artifact.version}",
|
||||||
|
message=(
|
||||||
|
"Direct wrapper calls cannot pause for interrupt input; run the "
|
||||||
|
"artifact through a deployment to receive a resumable run_id."
|
||||||
|
),
|
||||||
|
repair_hint="Save a deployment and call wf.workflow.run_deployment instead.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _visit_saved_children(
|
||||||
|
*,
|
||||||
|
artifact: WorkflowArtifact,
|
||||||
|
artifact_store: WorkflowArtifactStore,
|
||||||
|
active: set[tuple[str, int]],
|
||||||
|
artifacts_by_ref: dict[str, WorkflowArtifact],
|
||||||
|
diagnostics: list[DependencyDiagnostic],
|
||||||
|
) -> None:
|
||||||
|
for ref in _saved_child_refs(artifact):
|
||||||
|
identity = _saved_identity(ref)
|
||||||
|
if identity in active:
|
||||||
|
diagnostics.append(_cycle_diagnostic(ref))
|
||||||
|
continue
|
||||||
|
if ref.display in artifacts_by_ref:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
child = artifact_store.get_artifact(*identity)
|
||||||
|
except KeyError:
|
||||||
|
diagnostics.append(_missing_diagnostic(ref))
|
||||||
|
continue
|
||||||
|
artifacts_by_ref[ref.display] = child
|
||||||
|
_visit_saved_children(
|
||||||
|
artifact=child,
|
||||||
|
artifact_store=artifact_store,
|
||||||
|
active=active | {identity},
|
||||||
|
artifacts_by_ref=artifacts_by_ref,
|
||||||
|
diagnostics=diagnostics,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _saved_child_refs(artifact: WorkflowArtifact) -> list[WorkflowRef]:
|
||||||
|
return [
|
||||||
|
node.workflow
|
||||||
|
for node in _artifact_steps(artifact)
|
||||||
|
if isinstance(node, SubgraphNode) and node.workflow.artifact_id is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact_steps(artifact: WorkflowArtifact) -> list[Step]:
|
||||||
|
"""Validate only step payloads needed for dependency discovery."""
|
||||||
|
raw_nodes = artifact.plan.get("nodes", [])
|
||||||
|
return _STEPS_ADAPTER.validate_python(raw_nodes)
|
||||||
|
|
||||||
|
|
||||||
|
def _saved_identity(ref: WorkflowRef) -> tuple[str, int]:
|
||||||
|
"""Return the required saved-ref fields after structural model validation."""
|
||||||
|
if ref.artifact_id is None or ref.version is None:
|
||||||
|
raise ValueError(f"workflow ref {ref.display!r} is not a saved artifact ref")
|
||||||
|
return ref.artifact_id, ref.version
|
||||||
|
|
||||||
|
|
||||||
|
def _missing_diagnostic(ref: WorkflowRef) -> DependencyDiagnostic:
|
||||||
|
return 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."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cycle_diagnostic(ref: WorkflowRef) -> DependencyDiagnostic:
|
||||||
|
return DependencyDiagnostic(
|
||||||
|
severity=DiagnosticSeverity.ERROR,
|
||||||
|
code="workflow_dependency_cycle",
|
||||||
|
logical_ref=ref.display,
|
||||||
|
message=f"Saved child workflow {ref.display!r} creates a dependency cycle.",
|
||||||
|
repair_hint="Remove the recursive saved subgraph reference.",
|
||||||
|
)
|
||||||
@@ -55,7 +55,7 @@ from ...shared.errors import error_payload
|
|||||||
from ...shared.names import RESERVED_CONNECTION_IDS
|
from ...shared.names import RESERVED_CONNECTION_IDS
|
||||||
from ...storage import Store
|
from ...storage import Store
|
||||||
from ...workflow.wrappers import _model_from_schema
|
from ...workflow.wrappers import _model_from_schema
|
||||||
from ...workflow_surface.saved_subgraphs import (
|
from wf_api.saved_subgraphs import (
|
||||||
SavedSubgraphTree,
|
SavedSubgraphTree,
|
||||||
prepare_saved_subgraphs,
|
prepare_saved_subgraphs,
|
||||||
resolve_saved_subgraph_tree,
|
resolve_saved_subgraph_tree,
|
||||||
|
|||||||
@@ -57,18 +57,22 @@ from wf_api.constants import (
|
|||||||
from wf_api.models import RawWorkflowPlan
|
from wf_api.models import RawWorkflowPlan
|
||||||
from wf_api.next_actions import NextActions
|
from wf_api.next_actions import NextActions
|
||||||
from wf_api.refs import parse_workflow_surface_capability_id
|
from wf_api.refs import parse_workflow_surface_capability_id
|
||||||
|
from wf_api.saved_subgraphs import (
|
||||||
from ..broker.service.adapters import require_adapter
|
|
||||||
from ..events import make_event
|
|
||||||
from ..shared import matches_query, paged_list_payload
|
|
||||||
from .models import TraceRange
|
|
||||||
from .saved_subgraphs import (
|
|
||||||
SavedSubgraphTree,
|
SavedSubgraphTree,
|
||||||
direct_wrapper_interrupt_diagnostic,
|
direct_wrapper_interrupt_diagnostic,
|
||||||
resolve_saved_subgraph_tree,
|
resolve_saved_subgraph_tree,
|
||||||
saved_subgraph_tree_from_snapshots,
|
saved_subgraph_tree_from_snapshots,
|
||||||
validate_saved_subgraph_tree,
|
validate_saved_subgraph_tree,
|
||||||
)
|
)
|
||||||
|
from wf_api.wrapper_hints import (
|
||||||
|
workflow_output_schema_for_authoring,
|
||||||
|
wrapper_hints_for_capability,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..broker.service.adapters import require_adapter
|
||||||
|
from ..events import make_event
|
||||||
|
from ..shared import matches_query, paged_list_payload
|
||||||
|
from .models import TraceRange
|
||||||
from .run_lifecycle import (
|
from .run_lifecycle import (
|
||||||
create_pinned_environment,
|
create_pinned_environment,
|
||||||
has_blocking_diagnostics,
|
has_blocking_diagnostics,
|
||||||
@@ -78,10 +82,6 @@ from .run_lifecycle import (
|
|||||||
restore_interrupted_run,
|
restore_interrupted_run,
|
||||||
validate_pinned_resume_environment,
|
validate_pinned_resume_environment,
|
||||||
)
|
)
|
||||||
from wf_api.wrapper_hints import (
|
|
||||||
workflow_output_schema_for_authoring,
|
|
||||||
wrapper_hints_for_capability,
|
|
||||||
)
|
|
||||||
|
|
||||||
LIVE_SOURCE_CHECK_TIMEOUT_SECONDS = 8.0
|
LIVE_SOURCE_CHECK_TIMEOUT_SECONDS = 8.0
|
||||||
_LIVE_SOURCE_CHECK_FAILURES = (
|
_LIVE_SOURCE_CHECK_FAILURES = (
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from wf_core import (
|
|||||||
load_run_state,
|
load_run_state,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .saved_subgraphs import SavedSubgraphTree
|
from wf_api.saved_subgraphs import SavedSubgraphTree
|
||||||
|
|
||||||
|
|
||||||
def create_pinned_environment(
|
def create_pinned_environment(
|
||||||
|
|||||||
@@ -1,230 +1,27 @@
|
|||||||
|
"""Compatibility shim — canonical implementation moved to wf_api.saved_subgraphs.
|
||||||
|
|
||||||
|
This module re-exports every public symbol so that existing
|
||||||
|
``from wf_mcp.workflow_surface.saved_subgraphs import ...`` continues to work
|
||||||
|
without changes. New code should import from ``wf_api.saved_subgraphs``
|
||||||
|
directly.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from wf_api.saved_subgraphs import (
|
||||||
from dataclasses import dataclass
|
SavedSubgraphTree,
|
||||||
|
direct_wrapper_interrupt_diagnostic,
|
||||||
from pydantic import TypeAdapter
|
prepare_saved_subgraphs,
|
||||||
|
resolve_saved_subgraph_tree,
|
||||||
from wf_artifacts import (
|
saved_subgraph_tree_from_snapshots,
|
||||||
AvailableSource,
|
validate_saved_subgraph_tree,
|
||||||
DependencyDiagnostic,
|
|
||||||
DiagnosticSeverity,
|
|
||||||
WorkflowArtifact,
|
|
||||||
WorkflowArtifactStore,
|
|
||||||
WorkflowDeployment,
|
|
||||||
validate_deployment_dependencies,
|
|
||||||
)
|
|
||||||
from wf_core import (
|
|
||||||
AsyncNodeHandler,
|
|
||||||
InterruptNode,
|
|
||||||
NodeUse,
|
|
||||||
PreparedSubgraph,
|
|
||||||
SubgraphNode,
|
|
||||||
Workflow,
|
|
||||||
)
|
|
||||||
from wf_core.models.steps import Step
|
|
||||||
from wf_core.models.workflow_refs import WorkflowRef
|
|
||||||
from wf_platform import CapabilitySource
|
|
||||||
|
|
||||||
from wf_api.runtime_dependencies import resolve_runtime_dependencies
|
|
||||||
from wf_api.models import RawWorkflowPlan
|
|
||||||
|
|
||||||
_STEPS_ADAPTER = TypeAdapter(list[Step])
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class SavedSubgraphTree:
|
|
||||||
"""Saved descendant artifacts prepared from one root artifact boundary."""
|
|
||||||
|
|
||||||
artifacts_by_ref: dict[str, WorkflowArtifact]
|
|
||||||
diagnostics: list[DependencyDiagnostic]
|
|
||||||
|
|
||||||
|
|
||||||
def saved_subgraph_tree_from_snapshots(
|
|
||||||
child_artifacts: list[WorkflowArtifact],
|
|
||||||
) -> SavedSubgraphTree:
|
|
||||||
"""Restore the exact saved-child definitions pinned by a durable run."""
|
|
||||||
return SavedSubgraphTree(
|
|
||||||
artifacts_by_ref={
|
|
||||||
f"workflow.{artifact.id}.v{artifact.version}": artifact
|
|
||||||
for artifact in child_artifacts
|
|
||||||
},
|
|
||||||
diagnostics=[],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
def resolve_saved_subgraph_tree(
|
"SavedSubgraphTree",
|
||||||
*,
|
"direct_wrapper_interrupt_diagnostic",
|
||||||
root_artifact: WorkflowArtifact,
|
"prepare_saved_subgraphs",
|
||||||
artifact_store: WorkflowArtifactStore,
|
"resolve_saved_subgraph_tree",
|
||||||
) -> SavedSubgraphTree:
|
"saved_subgraph_tree_from_snapshots",
|
||||||
"""Load exact saved descendants and report missing refs or recursion cycles.
|
"validate_saved_subgraph_tree",
|
||||||
|
|
||||||
A saved subgraph ref identifies an immutable artifact version. This loader
|
|
||||||
intentionally does not resolve capabilities or deployment bindings; it
|
|
||||||
identifies the artifact tree that later platform validation/preparation
|
|
||||||
will operate on.
|
|
||||||
"""
|
|
||||||
artifacts_by_ref: dict[str, WorkflowArtifact] = {}
|
|
||||||
diagnostics: list[DependencyDiagnostic] = []
|
|
||||||
_visit_saved_children(
|
|
||||||
artifact=root_artifact,
|
|
||||||
artifact_store=artifact_store,
|
|
||||||
active={(root_artifact.id, root_artifact.version)},
|
|
||||||
artifacts_by_ref=artifacts_by_ref,
|
|
||||||
diagnostics=diagnostics,
|
|
||||||
)
|
|
||||||
return SavedSubgraphTree(
|
|
||||||
artifacts_by_ref=artifacts_by_ref,
|
|
||||||
diagnostics=diagnostics,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_saved_subgraph_tree(
|
|
||||||
*,
|
|
||||||
tree: SavedSubgraphTree,
|
|
||||||
deployment: WorkflowDeployment,
|
|
||||||
sources: list[AvailableSource],
|
|
||||||
) -> list[DependencyDiagnostic]:
|
|
||||||
"""Validate saved descendants under the parent deployment environment."""
|
|
||||||
diagnostics = list(tree.diagnostics)
|
|
||||||
for child in tree.artifacts_by_ref.values():
|
|
||||||
diagnostics.extend(
|
|
||||||
validate_deployment_dependencies(
|
|
||||||
artifact=child,
|
|
||||||
deployment=deployment,
|
|
||||||
sources=sources,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return diagnostics
|
|
||||||
|
|
||||||
|
|
||||||
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[AsyncNodeHandler]]:
|
|
||||||
"""Compile saved descendants using one inherited deployment environment.
|
|
||||||
|
|
||||||
The tree has already fixed exact artifact versions. Binding resolution is
|
|
||||||
deliberately shared with the root deployment; per-use-site deployment
|
|
||||||
overrides are a future platform feature rather than an implicit fallback.
|
|
||||||
"""
|
|
||||||
if tree.diagnostics:
|
|
||||||
messages = "; ".join(diagnostic.message for diagnostic in tree.diagnostics)
|
|
||||||
raise ValueError(f"cannot prepare invalid saved subgraph tree: {messages}")
|
|
||||||
|
|
||||||
prepared: dict[str, PreparedSubgraph[AsyncNodeHandler]] = {}
|
|
||||||
for ref_display, child in tree.artifacts_by_ref.items():
|
|
||||||
plan = RawWorkflowPlan.model_validate(child.plan)
|
|
||||||
dependencies = resolve_runtime_dependencies(
|
|
||||||
artifact=child,
|
|
||||||
deployment=deployment,
|
|
||||||
sources=sources,
|
|
||||||
plan_node_names=[
|
|
||||||
node.node for node in plan.nodes if isinstance(node, NodeUse)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
prepared[ref_display] = PreparedSubgraph(
|
|
||||||
workflow=compile_plan(plan, dependencies.node_name_bindings),
|
|
||||||
registry=dependencies.node_registry,
|
|
||||||
reducers=dependencies.reducers,
|
|
||||||
)
|
|
||||||
return prepared
|
|
||||||
|
|
||||||
|
|
||||||
def direct_wrapper_interrupt_diagnostic(
|
|
||||||
artifact: WorkflowArtifact,
|
|
||||||
) -> DependencyDiagnostic | None:
|
|
||||||
"""Reject direct wrapper calls that cannot return a resumable run handle.
|
|
||||||
|
|
||||||
Deployment execution supports interrupt/resume through a durable `run_id`;
|
|
||||||
`call_capability` remains a single-call authoring probe.
|
|
||||||
"""
|
|
||||||
if not any(isinstance(node, InterruptNode) for node in _artifact_steps(artifact)):
|
|
||||||
return None
|
|
||||||
return DependencyDiagnostic(
|
|
||||||
severity=DiagnosticSeverity.ERROR,
|
|
||||||
code="interrupting_wrapper_call_unsupported",
|
|
||||||
logical_ref=f"workflow.{artifact.id}.v{artifact.version}",
|
|
||||||
message=(
|
|
||||||
"Direct wrapper calls cannot pause for interrupt input; run the "
|
|
||||||
"artifact through a deployment to receive a resumable run_id."
|
|
||||||
),
|
|
||||||
repair_hint="Save a deployment and call wf.workflow.run_deployment instead.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _visit_saved_children(
|
|
||||||
*,
|
|
||||||
artifact: WorkflowArtifact,
|
|
||||||
artifact_store: WorkflowArtifactStore,
|
|
||||||
active: set[tuple[str, int]],
|
|
||||||
artifacts_by_ref: dict[str, WorkflowArtifact],
|
|
||||||
diagnostics: list[DependencyDiagnostic],
|
|
||||||
) -> None:
|
|
||||||
for ref in _saved_child_refs(artifact):
|
|
||||||
identity = _saved_identity(ref)
|
|
||||||
if identity in active:
|
|
||||||
diagnostics.append(_cycle_diagnostic(ref))
|
|
||||||
continue
|
|
||||||
if ref.display in artifacts_by_ref:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
child = artifact_store.get_artifact(*identity)
|
|
||||||
except KeyError:
|
|
||||||
diagnostics.append(_missing_diagnostic(ref))
|
|
||||||
continue
|
|
||||||
artifacts_by_ref[ref.display] = child
|
|
||||||
_visit_saved_children(
|
|
||||||
artifact=child,
|
|
||||||
artifact_store=artifact_store,
|
|
||||||
active=active | {identity},
|
|
||||||
artifacts_by_ref=artifacts_by_ref,
|
|
||||||
diagnostics=diagnostics,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _saved_child_refs(artifact: WorkflowArtifact) -> list[WorkflowRef]:
|
|
||||||
return [
|
|
||||||
node.workflow
|
|
||||||
for node in _artifact_steps(artifact)
|
|
||||||
if isinstance(node, SubgraphNode) and node.workflow.artifact_id is not None
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _artifact_steps(artifact: WorkflowArtifact) -> list[Step]:
|
|
||||||
"""Validate only step payloads needed for dependency discovery."""
|
|
||||||
raw_nodes = artifact.plan.get("nodes", [])
|
|
||||||
return _STEPS_ADAPTER.validate_python(raw_nodes)
|
|
||||||
|
|
||||||
|
|
||||||
def _saved_identity(ref: WorkflowRef) -> tuple[str, int]:
|
|
||||||
"""Return the required saved-ref fields after structural model validation."""
|
|
||||||
if ref.artifact_id is None or ref.version is None:
|
|
||||||
raise ValueError(f"workflow ref {ref.display!r} is not a saved artifact ref")
|
|
||||||
return ref.artifact_id, ref.version
|
|
||||||
|
|
||||||
|
|
||||||
def _missing_diagnostic(ref: WorkflowRef) -> DependencyDiagnostic:
|
|
||||||
return 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."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _cycle_diagnostic(ref: WorkflowRef) -> DependencyDiagnostic:
|
|
||||||
return DependencyDiagnostic(
|
|
||||||
severity=DiagnosticSeverity.ERROR,
|
|
||||||
code="workflow_dependency_cycle",
|
|
||||||
logical_ref=ref.display,
|
|
||||||
message=f"Saved child workflow {ref.display!r} creates a dependency cycle.",
|
|
||||||
repair_hint="Remove the recursive saved subgraph reference.",
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import wf_api.saved_subgraphs as canonical
|
||||||
|
import wf_mcp.workflow_surface.saved_subgraphs as shim
|
||||||
|
|
||||||
|
|
||||||
|
def test_canonical_import_exports_expected_symbols() -> None:
|
||||||
|
assert hasattr(canonical, "SavedSubgraphTree")
|
||||||
|
assert hasattr(canonical, "resolve_saved_subgraph_tree")
|
||||||
|
assert hasattr(canonical, "prepare_saved_subgraphs")
|
||||||
|
assert hasattr(canonical, "validate_saved_subgraph_tree")
|
||||||
|
assert hasattr(canonical, "saved_subgraph_tree_from_snapshots")
|
||||||
|
assert hasattr(canonical, "direct_wrapper_interrupt_diagnostic")
|
||||||
|
|
||||||
|
|
||||||
|
def test_shim_import_still_works() -> None:
|
||||||
|
assert hasattr(shim, "SavedSubgraphTree")
|
||||||
|
assert hasattr(shim, "resolve_saved_subgraph_tree")
|
||||||
|
assert hasattr(shim, "prepare_saved_subgraphs")
|
||||||
|
|
||||||
|
|
||||||
|
def test_shim_symbols_are_identical_to_canonical() -> None:
|
||||||
|
assert shim.SavedSubgraphTree is canonical.SavedSubgraphTree
|
||||||
|
assert shim.resolve_saved_subgraph_tree is canonical.resolve_saved_subgraph_tree
|
||||||
|
assert shim.prepare_saved_subgraphs is canonical.prepare_saved_subgraphs
|
||||||
|
assert shim.validate_saved_subgraph_tree is canonical.validate_saved_subgraph_tree
|
||||||
|
assert (
|
||||||
|
shim.saved_subgraph_tree_from_snapshots
|
||||||
|
is canonical.saved_subgraph_tree_from_snapshots
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
shim.direct_wrapper_interrupt_diagnostic
|
||||||
|
is canonical.direct_wrapper_interrupt_diagnostic
|
||||||
|
)
|
||||||
@@ -14,7 +14,7 @@ from wf_mcp.broker import WfMcpService
|
|||||||
from wf_mcp.models import ConnectionConfig
|
from wf_mcp.models import ConnectionConfig
|
||||||
from wf_mcp.storage import FileStore
|
from wf_mcp.storage import FileStore
|
||||||
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
|
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
|
||||||
from wf_mcp.workflow_surface.saved_subgraphs import resolve_saved_subgraph_tree
|
from wf_api.saved_subgraphs import resolve_saved_subgraph_tree
|
||||||
|
|
||||||
from .test_support import echo_tool, input_binding, local_temp_root, output_binding
|
from .test_support import echo_tool, input_binding, local_temp_root, output_binding
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user