stateful to not deadhang, graph context -> output, more explicit state errors, other misc changes idfk any of those good thing theyre fixed tho
120 lines
4.3 KiB
Markdown
120 lines
4.3 KiB
Markdown
# Schema Validation Boundary
|
|
|
|
`wf_core` uses `SchemaRef` to carry JSON-schema-like shapes on workflow input,
|
|
node input, node output, and workflow output. Runtime payload validation is
|
|
delegated to the existing `jsonschema` library.
|
|
|
|
## Current Behavior
|
|
|
|
`wf_core.runtime.ops.schemas.validate_payload_against_schema` currently:
|
|
|
|
- converts `SchemaRef` into a JSON Schema dictionary
|
|
- asks `jsonschema` to validate the schema itself
|
|
- asks `jsonschema` to validate the payload
|
|
- wraps validation failures in `WorkflowExecutionError`
|
|
|
|
This means normal JSON Schema checks such as object shape, required fields,
|
|
property types, nested required fields, arrays, and item types are enforced by
|
|
the library.
|
|
|
|
It still does not solve:
|
|
|
|
- semantic compatibility between Pydantic-generated schemas and every possible
|
|
external JSON Schema dialect
|
|
- typed Python object creation from arbitrary JSON Schema
|
|
- workflow state merge behavior beyond declared exact-path metadata
|
|
- deep node-local map-path validation beyond statically knowable schema roots
|
|
- better domain-specific error payloads beyond `WorkflowExecutionError`
|
|
|
|
This means schema fields are contracts for authoring, planning, documentation,
|
|
and mapping validation. Runtime state merge behavior is separate metadata on
|
|
declared exact state paths; undeclared paths still use `replace`.
|
|
|
|
Final workflow output has two projection modes:
|
|
|
|
- If `Workflow.output` contains bindings, those bindings build the public output
|
|
payload from graph paths such as `state.result.message`.
|
|
- If `Workflow.output` is empty, legacy projection copies same-named top-level
|
|
state keys listed in `workflow.output_schema.properties`.
|
|
|
|
Prefer explicit `Workflow.output` bindings for new workflows. The same-named
|
|
state projection exists for compatibility with older plans.
|
|
|
|
The projected payload is then validated against `workflow.output_schema`.
|
|
|
|
## Why This Matters
|
|
|
|
Node and workflow boundaries can now reject wrong primitive/container types when
|
|
the schema declares them. This matters before workflows are generated by an LLM
|
|
or backed by arbitrary MCP tools.
|
|
|
|
## Authoring Footguns
|
|
|
|
### Prefer explicit entry arrays at LLM-facing boundaries
|
|
|
|
`dict[str, SomeModel]` is often pleasant in Python but weak as a human- or
|
|
LLM-facing schema. It commonly appears as a generic object with arbitrary keys,
|
|
and many clients communicate or render it much less clearly than an explicit
|
|
list shape.
|
|
|
|
Prefer:
|
|
|
|
```python
|
|
class Entry(BaseModel):
|
|
key: str
|
|
value: SomeModel
|
|
|
|
|
|
entries: list[Entry]
|
|
```
|
|
|
|
over:
|
|
|
|
```python
|
|
entries: dict[str, SomeModel]
|
|
```
|
|
|
|
when the schema is meant for MCP tools, LLM planning, or durable workflow
|
|
authoring contracts. Internal Python state can still use dictionaries when that
|
|
is the right runtime shape.
|
|
|
|
### Do not assume all JSON Schema consumers handle references equally
|
|
|
|
Pydantic-generated schemas may use `$defs` and local `$ref` references for
|
|
nested models. The runtime delegates validation to `jsonschema`, which supports
|
|
that structure, but display layers and downstream consumers may vary in how well
|
|
they present or reason about referenced shapes.
|
|
|
|
`wf_authoring` resolves the local Pydantic `$ref -> $defs` pattern only for its
|
|
own state-field projection. That helper is not a general-purpose JSON Schema
|
|
flattener, and arbitrary external schemas should not be assumed to share the
|
|
same shape.
|
|
|
|
## Intended Seam
|
|
|
|
The schema adapter lives behind:
|
|
|
|
```text
|
|
wf_core.runtime.ops.schemas.validate_payload_against_schema
|
|
```
|
|
|
|
Callers should not choose or invoke the validation backend directly. The runtime
|
|
keeps one small validation interface and hides the validation backend.
|
|
|
|
## Future Requirements
|
|
|
|
- Validate workflow input, node input, node output, and final workflow output
|
|
with the same semantics.
|
|
- Return errors that name the failing boundary and path.
|
|
- Keep schema validation separate from graph structure validation.
|
|
- Keep `wf_authoring` free to generate schemas from Pydantic models without
|
|
making the core runtime depend on authoring internals.
|
|
- Add targeted tests as externally sourced schemas grow more complex.
|
|
|
|
## Non-Goals For Now
|
|
|
|
- Do not add ad hoc type checks throughout runtime state operations.
|
|
- Do not let each node wrapper invent separate validation behavior.
|
|
- Do not conflate graph validation with payload validation.
|
|
- Do not hand-write a general JSON Schema implementation.
|