wf-core reorg 2 split the Models
This commit is contained in:
@@ -11,7 +11,8 @@ and user-facing control belong in `wf_mcp`.
|
|||||||
|
|
||||||
| Package / module | Responsibility |
|
| Package / module | Responsibility |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `wf_core.model` | Pydantic workflow schema: node definitions, node uses, control-flow nodes, edges, schemas, and node results. |
|
| `wf_core.models` | Pydantic workflow schema package: schemas, condition expressions, executable steps, workflow graph, and node results. |
|
||||||
|
| `wf_core.model` | Compatibility facade for older imports of core model objects. |
|
||||||
| `wf_core.run_state` | Serializable execution state: run status, frames, trace entries, interrupt requests, and runtime context. |
|
| `wf_core.run_state` | Serializable execution state: run status, frames, trace entries, interrupt requests, and runtime context. |
|
||||||
| `wf_core.runtime` | Public execution interface: execute, resume, and step in sync or async mode. |
|
| `wf_core.runtime` | Public execution interface: execute, resume, and step in sync or async mode. |
|
||||||
| `wf_core.runtime.ops` | Executor-only operations used behind `wf_core.runtime`: node execution, state writes, frame movement, foreach, interrupts, indexes, and schema checks. |
|
| `wf_core.runtime.ops` | Executor-only operations used behind `wf_core.runtime`: node execution, state writes, frame movement, foreach, interrupts, indexes, and schema checks. |
|
||||||
@@ -20,9 +21,9 @@ and user-facing control belong in `wf_mcp`.
|
|||||||
| `wf_core.paths` | Graph path parsing, reading, existence checks, and nested state writes. |
|
| `wf_core.paths` | Graph path parsing, reading, existence checks, and nested state writes. |
|
||||||
| `wf_core.tokens` | Importable graph boundary tokens: `START` and `END`. |
|
| `wf_core.tokens` | Importable graph boundary tokens: `START` and `END`. |
|
||||||
|
|
||||||
Root modules such as `wf_core.node_exec`, `wf_core.state_ops`, and
|
Root modules such as `wf_core.model`, `wf_core.node_exec`, `wf_core.state_ops`,
|
||||||
`wf_core.validate` are compatibility shims. New internal imports should prefer
|
and `wf_core.validate` are compatibility shims. New internal imports should
|
||||||
the concern package directly.
|
prefer the concern package directly.
|
||||||
|
|
||||||
## Runtime Flow
|
## Runtime Flow
|
||||||
|
|
||||||
@@ -54,7 +55,8 @@ raising at the first failure.
|
|||||||
## Dependency Rules
|
## Dependency Rules
|
||||||
|
|
||||||
- `wf_core` must not import `wf_authoring` or `wf_mcp`.
|
- `wf_core` must not import `wf_authoring` or `wf_mcp`.
|
||||||
- `wf_core.model` and `wf_core.run_state` should stay mostly data-only.
|
- `wf_core.models` and `wf_core.run_state` should stay mostly data-only.
|
||||||
|
- `wf_core.model` should stay a thin import facade.
|
||||||
- `wf_core.runtime` may import `runtime.ops`, but callers should not need to.
|
- `wf_core.runtime` may import `runtime.ops`, but callers should not need to.
|
||||||
- `wf_core.runtime.ops` may use model, run state, paths, conditions, and errors.
|
- `wf_core.runtime.ops` may use model, run state, paths, conditions, and errors.
|
||||||
- `wf_core.validation` may inspect model and path rules, but should not execute
|
- `wf_core.validation` may inspect model and path rules, but should not execute
|
||||||
@@ -63,9 +65,6 @@ raising at the first failure.
|
|||||||
|
|
||||||
## What This Cleanup Does Not Solve Yet
|
## What This Cleanup Does Not Solve Yet
|
||||||
|
|
||||||
- `wf_core.model` is still a single dense schema file. It is coherent today, but
|
|
||||||
if control-flow schemas grow, split it into `model/schemas.py`,
|
|
||||||
`model/steps.py`, and `model/workflow.py`.
|
|
||||||
- `demo_workflow.py` is still large because it is a fixture/demo, not core
|
- `demo_workflow.py` is still large because it is a fixture/demo, not core
|
||||||
runtime. If it becomes a permanent example suite, move it out of `wf_core`.
|
runtime. If it becomes a permanent example suite, move it out of `wf_core`.
|
||||||
- Foreach is still serial-only. Parallel foreach needs an explicit scheduling
|
- Foreach is still serial-only. Parallel foreach needs an explicit scheduling
|
||||||
@@ -76,4 +75,3 @@ raising at the first failure.
|
|||||||
- Runtime errors are still ordinary exceptions plus failed run status. A richer
|
- Runtime errors are still ordinary exceptions plus failed run status. A richer
|
||||||
error payload can be added later, but should be designed as part of trace/run
|
error payload can be added later, but should be designed as part of trace/run
|
||||||
state rather than scattered exceptions.
|
state rather than scattered exceptions.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
"""Compatibility shim for runtime flow operations."""
|
"""Compatibility shim for runtime flow operations."""
|
||||||
|
|
||||||
from wf_core.runtime.ops.flow import * # noqa: F403
|
from wf_core.runtime.ops.flow import * # noqa: F403
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
"""Compatibility shim for foreach runtime operations."""
|
"""Compatibility shim for foreach runtime operations."""
|
||||||
|
|
||||||
from wf_core.runtime.ops.foreach import * # noqa: F403
|
from wf_core.runtime.ops.foreach import * # noqa: F403
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
"""Compatibility shim for frame runtime operations."""
|
"""Compatibility shim for frame runtime operations."""
|
||||||
|
|
||||||
from wf_core.runtime.ops.frames import * # noqa: F403
|
from wf_core.runtime.ops.frames import * # noqa: F403
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
"""Compatibility shim for interrupt runtime operations."""
|
"""Compatibility shim for interrupt runtime operations."""
|
||||||
|
|
||||||
from wf_core.runtime.ops.interrupts import * # noqa: F403
|
from wf_core.runtime.ops.interrupts import * # noqa: F403
|
||||||
|
|
||||||
|
|||||||
+46
-153
@@ -1,161 +1,54 @@
|
|||||||
from __future__ import annotations
|
"""Compatibility facade for core workflow model objects."""
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Annotated, Any, Literal, cast
|
from wf_core.models import (
|
||||||
|
BinaryCondition,
|
||||||
|
Condition,
|
||||||
|
ConditionNode,
|
||||||
|
Edge,
|
||||||
|
ExistsCondition,
|
||||||
|
ForeachNode,
|
||||||
|
InterruptNode,
|
||||||
|
JoinNode,
|
||||||
|
LiteralOperand,
|
||||||
|
NodeDef,
|
||||||
|
NodeResult,
|
||||||
|
NodeUse,
|
||||||
|
NotCondition,
|
||||||
|
Operand,
|
||||||
|
PathOperand,
|
||||||
|
SchemaRef,
|
||||||
|
StateField,
|
||||||
|
StateSchema,
|
||||||
|
Step,
|
||||||
|
VariadicCondition,
|
||||||
|
Workflow,
|
||||||
|
)
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
__all__ = [
|
||||||
|
"BinaryCondition",
|
||||||
if TYPE_CHECKING:
|
"Condition",
|
||||||
from wf_core.validation.issues import ValidationReport
|
"ConditionNode",
|
||||||
|
"Edge",
|
||||||
|
"ExistsCondition",
|
||||||
class SchemaRef(BaseModel):
|
"ForeachNode",
|
||||||
model_config = ConfigDict(extra="allow")
|
"InterruptNode",
|
||||||
|
"JoinNode",
|
||||||
title: str | None = None
|
"LiteralOperand",
|
||||||
type: str | None = None
|
"NodeDef",
|
||||||
properties: dict[str, Any] = Field(default_factory=dict)
|
"NodeResult",
|
||||||
required: list[str] = Field(default_factory=list)
|
"NodeUse",
|
||||||
|
"NotCondition",
|
||||||
|
"Operand",
|
||||||
class StateField(BaseModel):
|
"PathOperand",
|
||||||
type: str
|
"SchemaRef",
|
||||||
merge_strategy: Literal["replace", "append", "merge_object"] = "replace"
|
"StateField",
|
||||||
trace: bool = True
|
"StateSchema",
|
||||||
default: Any = None
|
"Step",
|
||||||
|
"VariadicCondition",
|
||||||
|
"Workflow",
|
||||||
class StateSchema(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="allow")
|
|
||||||
|
|
||||||
fields: dict[str, StateField] = Field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
class NodeDef(BaseModel):
|
|
||||||
name: str
|
|
||||||
input_schema: SchemaRef
|
|
||||||
output_schema: SchemaRef
|
|
||||||
outcomes: list[str] = Field(min_length=1)
|
|
||||||
retry: int | None = Field(default=None, ge=0)
|
|
||||||
timeout_seconds: int | None = Field(default=None, gt=0)
|
|
||||||
|
|
||||||
|
|
||||||
class NodeUse(BaseModel):
|
|
||||||
id: str
|
|
||||||
type: Literal["node"]
|
|
||||||
node: str
|
|
||||||
desc: str | None = None
|
|
||||||
in_map: dict[str, str] = Field(default_factory=dict)
|
|
||||||
out_map: dict[str, str] = Field(default_factory=dict)
|
|
||||||
retry: int | None = Field(default=None, ge=0)
|
|
||||||
timeout_seconds: int | None = Field(default=None, gt=0)
|
|
||||||
|
|
||||||
|
|
||||||
class PathOperand(BaseModel):
|
|
||||||
path: str
|
|
||||||
|
|
||||||
|
|
||||||
class LiteralOperand(BaseModel):
|
|
||||||
value: Any
|
|
||||||
|
|
||||||
|
|
||||||
Operand = Annotated[PathOperand | LiteralOperand, Field(discriminator=None)]
|
|
||||||
|
|
||||||
|
|
||||||
class ExistsCondition(BaseModel):
|
|
||||||
op: Literal["exists"]
|
|
||||||
path: str
|
|
||||||
|
|
||||||
|
|
||||||
class NotCondition(BaseModel):
|
|
||||||
op: Literal["not"]
|
|
||||||
arg: "Condition"
|
|
||||||
|
|
||||||
|
|
||||||
class VariadicCondition(BaseModel):
|
|
||||||
op: Literal["and", "or"]
|
|
||||||
args: list["Condition"] = Field(min_length=1)
|
|
||||||
|
|
||||||
|
|
||||||
class BinaryCondition(BaseModel):
|
|
||||||
op: Literal["eq", "ne", "gt", "lt"]
|
|
||||||
left: PathOperand | LiteralOperand
|
|
||||||
right: PathOperand | LiteralOperand
|
|
||||||
|
|
||||||
|
|
||||||
Condition = Annotated[
|
|
||||||
ExistsCondition | NotCondition | VariadicCondition | BinaryCondition,
|
|
||||||
Field(discriminator="op"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ConditionNode(BaseModel):
|
|
||||||
id: str
|
|
||||||
type: Literal["condition"]
|
|
||||||
check: Condition
|
|
||||||
|
|
||||||
|
|
||||||
class ForeachNode(BaseModel):
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
|
||||||
|
|
||||||
id: str
|
|
||||||
type: Literal["foreach"]
|
|
||||||
over: str
|
|
||||||
as_: str = Field(alias="as")
|
|
||||||
mode: Literal["serial", "parallel"] = "serial"
|
|
||||||
on_item_error: Literal["fail", "collect", "skip"] = "fail"
|
|
||||||
|
|
||||||
|
|
||||||
class JoinNode(BaseModel):
|
|
||||||
id: str
|
|
||||||
type: Literal["join"]
|
|
||||||
|
|
||||||
|
|
||||||
class InterruptNode(BaseModel):
|
|
||||||
id: str
|
|
||||||
type: Literal["interrupt"]
|
|
||||||
kind: str
|
|
||||||
request_map: dict[str, str] = Field(default_factory=dict)
|
|
||||||
out_map: dict[str, str] = Field(default_factory=dict)
|
|
||||||
outcomes: list[str] = Field(default_factory=lambda: ["submitted"])
|
|
||||||
|
|
||||||
|
|
||||||
Step = Annotated[
|
|
||||||
NodeUse | ConditionNode | ForeachNode | JoinNode | InterruptNode,
|
|
||||||
Field(discriminator="type"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Edge(BaseModel):
|
|
||||||
from_: str = Field(alias="from")
|
|
||||||
outcome: str
|
|
||||||
to: str
|
|
||||||
|
|
||||||
|
|
||||||
class Workflow(BaseModel):
|
|
||||||
name: str
|
|
||||||
input_schema: SchemaRef
|
|
||||||
state_schema: StateSchema
|
|
||||||
output_schema: SchemaRef
|
|
||||||
node_defs: list[NodeDef] = Field(default_factory=list)
|
|
||||||
start: str
|
|
||||||
nodes: list[Step]
|
|
||||||
edges: list[Edge]
|
|
||||||
|
|
||||||
def validate_structure(self) -> "ValidationReport":
|
|
||||||
from importlib import import_module
|
|
||||||
|
|
||||||
validation = import_module("wf_core.validation.core")
|
|
||||||
return cast("ValidationReport", validation.validate_workflow(self))
|
|
||||||
|
|
||||||
|
|
||||||
class NodeResult(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="allow")
|
|
||||||
|
|
||||||
outcome: str
|
|
||||||
output: dict[str, Any] = Field(default_factory=dict)
|
|
||||||
meta: dict[str, Any] = Field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from wf_core.models.conditions import (
|
||||||
|
BinaryCondition,
|
||||||
|
Condition,
|
||||||
|
ExistsCondition,
|
||||||
|
LiteralOperand,
|
||||||
|
NotCondition,
|
||||||
|
Operand,
|
||||||
|
PathOperand,
|
||||||
|
VariadicCondition,
|
||||||
|
)
|
||||||
|
from wf_core.models.results import NodeResult
|
||||||
|
from wf_core.models.schemas import NodeDef, SchemaRef, StateField, StateSchema
|
||||||
|
from wf_core.models.steps import (
|
||||||
|
ConditionNode,
|
||||||
|
ForeachNode,
|
||||||
|
InterruptNode,
|
||||||
|
JoinNode,
|
||||||
|
NodeUse,
|
||||||
|
Step,
|
||||||
|
)
|
||||||
|
from wf_core.models.workflow import Edge, Workflow
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"BinaryCondition",
|
||||||
|
"Condition",
|
||||||
|
"ConditionNode",
|
||||||
|
"Edge",
|
||||||
|
"ExistsCondition",
|
||||||
|
"ForeachNode",
|
||||||
|
"InterruptNode",
|
||||||
|
"JoinNode",
|
||||||
|
"LiteralOperand",
|
||||||
|
"NodeDef",
|
||||||
|
"NodeResult",
|
||||||
|
"NodeUse",
|
||||||
|
"NotCondition",
|
||||||
|
"Operand",
|
||||||
|
"PathOperand",
|
||||||
|
"SchemaRef",
|
||||||
|
"StateField",
|
||||||
|
"StateSchema",
|
||||||
|
"Step",
|
||||||
|
"VariadicCondition",
|
||||||
|
"Workflow",
|
||||||
|
]
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Annotated, Any, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class PathOperand(BaseModel):
|
||||||
|
"""Condition operand resolved from a workflow graph path."""
|
||||||
|
|
||||||
|
path: str
|
||||||
|
|
||||||
|
|
||||||
|
class LiteralOperand(BaseModel):
|
||||||
|
"""Condition operand that carries a literal value."""
|
||||||
|
|
||||||
|
value: Any
|
||||||
|
|
||||||
|
|
||||||
|
Operand = Annotated[PathOperand | LiteralOperand, Field(discriminator=None)]
|
||||||
|
"""Condition operand accepted by binary condition expressions."""
|
||||||
|
|
||||||
|
|
||||||
|
class ExistsCondition(BaseModel):
|
||||||
|
"""Condition that is true when a graph path resolves to a present value."""
|
||||||
|
|
||||||
|
op: Literal["exists"]
|
||||||
|
path: str
|
||||||
|
|
||||||
|
|
||||||
|
class NotCondition(BaseModel):
|
||||||
|
"""Condition that negates another condition expression."""
|
||||||
|
|
||||||
|
op: Literal["not"]
|
||||||
|
arg: "Condition"
|
||||||
|
|
||||||
|
|
||||||
|
class VariadicCondition(BaseModel):
|
||||||
|
"""Condition that combines one or more child conditions."""
|
||||||
|
|
||||||
|
op: Literal["and", "or"]
|
||||||
|
args: list["Condition"] = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class BinaryCondition(BaseModel):
|
||||||
|
"""Condition that compares two operands."""
|
||||||
|
|
||||||
|
op: Literal["eq", "ne", "gt", "lt"]
|
||||||
|
left: PathOperand | LiteralOperand
|
||||||
|
right: PathOperand | LiteralOperand
|
||||||
|
|
||||||
|
|
||||||
|
Condition = Annotated[
|
||||||
|
ExistsCondition | NotCondition | VariadicCondition | BinaryCondition,
|
||||||
|
Field(discriminator="op"),
|
||||||
|
]
|
||||||
|
"""Discriminated union of all supported condition expressions."""
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class NodeResult(BaseModel):
|
||||||
|
"""Normalized result returned by a node handler after execution."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
outcome: str
|
||||||
|
output: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
meta: dict[str, Any] = Field(default_factory=dict)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaRef(BaseModel):
|
||||||
|
"""JSON-schema-like shape used at workflow boundaries."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
title: str | None = None
|
||||||
|
type: str | None = None
|
||||||
|
properties: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
required: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class StateField(BaseModel):
|
||||||
|
"""Declared root state field plus its runtime merge behavior."""
|
||||||
|
|
||||||
|
type: str
|
||||||
|
merge_strategy: Literal["replace", "append", "merge_object"] = "replace"
|
||||||
|
trace: bool = True
|
||||||
|
default: Any = None
|
||||||
|
|
||||||
|
|
||||||
|
class StateSchema(BaseModel):
|
||||||
|
"""Workflow state schema keyed by declared root field name."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
fields: dict[str, StateField] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeDef(BaseModel):
|
||||||
|
"""Reusable node contract referenced by one or more node uses."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
input_schema: SchemaRef
|
||||||
|
output_schema: SchemaRef
|
||||||
|
outcomes: list[str] = Field(min_length=1)
|
||||||
|
retry: int | None = Field(default=None, ge=0)
|
||||||
|
timeout_seconds: int | None = Field(default=None, gt=0)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
from wf_core.models.conditions import Condition
|
||||||
|
|
||||||
|
|
||||||
|
class NodeUse(BaseModel):
|
||||||
|
"""Concrete use of a reusable node definition inside a workflow graph."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
type: Literal["node"]
|
||||||
|
node: str
|
||||||
|
desc: str | None = None
|
||||||
|
in_map: dict[str, str] = Field(default_factory=dict)
|
||||||
|
out_map: dict[str, str] = Field(default_factory=dict)
|
||||||
|
retry: int | None = Field(default=None, ge=0)
|
||||||
|
timeout_seconds: int | None = Field(default=None, gt=0)
|
||||||
|
|
||||||
|
|
||||||
|
class ConditionNode(BaseModel):
|
||||||
|
"""Control-flow step that routes through `true` or `false` outcomes."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
type: Literal["condition"]
|
||||||
|
check: Condition
|
||||||
|
|
||||||
|
|
||||||
|
class ForeachNode(BaseModel):
|
||||||
|
"""Control-flow step that iterates over an input or state list."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
id: str
|
||||||
|
type: Literal["foreach"]
|
||||||
|
over: str
|
||||||
|
as_: str = Field(alias="as")
|
||||||
|
mode: Literal["serial", "parallel"] = "serial"
|
||||||
|
on_item_error: Literal["fail", "collect", "skip"] = "fail"
|
||||||
|
|
||||||
|
|
||||||
|
class JoinNode(BaseModel):
|
||||||
|
"""Control-flow step that marks a branch or frame as joined."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
type: Literal["join"]
|
||||||
|
|
||||||
|
|
||||||
|
class InterruptNode(BaseModel):
|
||||||
|
"""Control-flow step that pauses a run and waits for resume input."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
type: Literal["interrupt"]
|
||||||
|
kind: str
|
||||||
|
request_map: dict[str, str] = Field(default_factory=dict)
|
||||||
|
out_map: dict[str, str] = Field(default_factory=dict)
|
||||||
|
outcomes: list[str] = Field(default_factory=lambda: ["submitted"])
|
||||||
|
|
||||||
|
|
||||||
|
Step = Annotated[
|
||||||
|
NodeUse | ConditionNode | ForeachNode | JoinNode | InterruptNode,
|
||||||
|
Field(discriminator="type"),
|
||||||
|
]
|
||||||
|
"""Discriminated union of all executable workflow graph steps."""
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from wf_core.models.schemas import NodeDef, SchemaRef, StateSchema
|
||||||
|
from wf_core.models.steps import Step
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from wf_core.validation.issues import ValidationReport
|
||||||
|
|
||||||
|
|
||||||
|
class Edge(BaseModel):
|
||||||
|
"""Outcome-specific transition from one workflow step to another."""
|
||||||
|
|
||||||
|
from_: str = Field(alias="from")
|
||||||
|
outcome: str
|
||||||
|
to: str
|
||||||
|
|
||||||
|
|
||||||
|
class Workflow(BaseModel):
|
||||||
|
"""Serializable workflow graph consumed by the core runtime."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
input_schema: SchemaRef
|
||||||
|
state_schema: StateSchema
|
||||||
|
output_schema: SchemaRef
|
||||||
|
node_defs: list[NodeDef] = Field(default_factory=list)
|
||||||
|
start: str
|
||||||
|
nodes: list[Step]
|
||||||
|
edges: list[Edge]
|
||||||
|
|
||||||
|
def validate_structure(self) -> "ValidationReport":
|
||||||
|
"""Return all structural validation issues for this workflow."""
|
||||||
|
validation = import_module("wf_core.validation.core")
|
||||||
|
return cast("ValidationReport", validation.validate_workflow(self))
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
"""Compatibility shim for node execution operations."""
|
"""Compatibility shim for node execution operations."""
|
||||||
|
|
||||||
from wf_core.runtime.ops.nodes import * # noqa: F403
|
from wf_core.runtime.ops.nodes import * # noqa: F403
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
"""Compatibility shim for run-state construction."""
|
"""Compatibility shim for run-state construction."""
|
||||||
|
|
||||||
from wf_core.runtime.ops.runs import * # noqa: F403
|
from wf_core.runtime.ops.runs import * # noqa: F403
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,3 @@ Root modules such as `wf_core.node_exec` remain as compatibility shims. New
|
|||||||
runtime internals should import from this package so the execution seam stays
|
runtime internals should import from this package so the execution seam stays
|
||||||
easy to navigate.
|
easy to navigate.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
"""Compatibility shim for runtime schema validation helpers."""
|
"""Compatibility shim for runtime schema validation helpers."""
|
||||||
|
|
||||||
from wf_core.runtime.ops.schemas import * # noqa: F403
|
from wf_core.runtime.ops.schemas import * # noqa: F403
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
"""Compatibility shim for state mutation operations."""
|
"""Compatibility shim for state mutation operations."""
|
||||||
|
|
||||||
from wf_core.runtime.ops.state import * # noqa: F403
|
from wf_core.runtime.ops.state import * # noqa: F403
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
"""Compatibility shim for non-node step handlers."""
|
"""Compatibility shim for non-node step handlers."""
|
||||||
|
|
||||||
from wf_core.runtime.ops.handlers import * # noqa: F403
|
from wf_core.runtime.ops.handlers import * # noqa: F403
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,3 @@ __all__ = [
|
|||||||
"ValidationReport",
|
"ValidationReport",
|
||||||
"validate_workflow",
|
"validate_workflow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -166,4 +166,3 @@ def _validate_reachable_outcomes(
|
|||||||
f"nodes[{node_id}]",
|
f"nodes[{node_id}]",
|
||||||
f"reachable node is missing edges for outcomes {sorted(missing)!r}",
|
f"reachable node is missing edges for outcomes {sorted(missing)!r}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -50,4 +50,3 @@ class ValidationReport:
|
|||||||
f"- [{issue.code}] {issue.path}: {issue.message}" for issue in self.errors
|
f"- [{issue.code}] {issue.path}: {issue.message}" for issue in self.errors
|
||||||
)
|
)
|
||||||
raise ValueError(f"Workflow validation failed:\n{rendered}")
|
raise ValueError(f"Workflow validation failed:\n{rendered}")
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ def declared_outcomes_for_step(step: Step, node_defs: dict[str, NodeDef]) -> set
|
|||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
|
||||||
def reachable_node_ids(start: str, edges: list[Edge], nodes_by_id: dict[str, Step]) -> set[str]:
|
def reachable_node_ids(
|
||||||
|
start: str, edges: list[Edge], nodes_by_id: dict[str, Step]
|
||||||
|
) -> set[str]:
|
||||||
if start not in nodes_by_id:
|
if start not in nodes_by_id:
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
@@ -38,4 +40,3 @@ def reachable_node_ids(start: str, edges: list[Edge], nodes_by_id: dict[str, Ste
|
|||||||
seen.add(node_id)
|
seen.add(node_id)
|
||||||
stack.extend(adjacency.get(node_id, []))
|
stack.extend(adjacency.get(node_id, []))
|
||||||
return seen
|
return seen
|
||||||
|
|
||||||
|
|||||||
@@ -220,4 +220,3 @@ def validate_operand(
|
|||||||
path,
|
path,
|
||||||
f"invalid operand path {operand.path!r}",
|
f"invalid operand path {operand.path!r}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user