Skip to content

Validation

nae.validation

Compile-time state-availability validation for AgenticGraph.

The core promise: when a graph is built we statically verify that every state key a node READS is produced upstream (or present in the initial schema), and that every key it WRITES is declared in the schema. The build FAILS on a hard error.

The core is a forward dataflow over a tagged edge list (recorded during the build): may (any-path availability, a monotone-increasing union fixpoint) and must (guaranteed-on-all-paths, a monotone-decreasing fixpoint that UNIONs across AND-parallel joins and INTERSECTs within an exclusive DecisionNode choice-group). Hard errors come from may; must only drives warnings. Loop-back edges are excluded when computing incoming availability, so loop-carried reads (a key produced only by a downstream node that cycles back) are caught as read-before-write on the first iteration rather than silently credited.

This is a may/must dataflow analysis, not a soundness proof. It reasons over DECLARED node reads/writes and the graph's edges — it does NOT read the code inside ConditionNode predicates (arbitrary lambdas), tool bodies, or other custom callables, so a state key touched only inside such a callable is invisible to it unless the node declares it. Within those limits it flags read-before-write and undeclared reads/writes; no false positives have been observed on our example corpus, but "compiles" is not a guarantee of dataflow soundness.

GraphValidationError

Bases: Exception

Raised at build time when validation finds a hard error.

Issue

Issue(level: str, node: str, message: str)

A single validation finding.

ValidationReport

ValidationReport(issues, graph_name: str = 'graph')

Collected issues for one graph; .ok is True when there are no errors.

check_placeholders

check_placeholders(name, node_prompt, scalar_reads: set, placeholders: set)

Cross-check a node's scalar reads against its node_prompt placeholders.

A scalar read exists to be interpolated into the prompt. With no scalar reads the node skips .format_map entirely (brace-heavy prompts pass untouched), so this check is skipped. Otherwise: - Check A (hard error): every {name} placeholder must be a declared scalar read — an undeclared one is a guaranteed runtime KeyError. - Check B (warning): every scalar read should appear as a placeholder — an uninterpolated scalar read is a dead read.

Parameters:

Name Type Description Default
scalar_reads set

the node's reads that are NOT message-history channels.

required
placeholders set

{name} field names parsed from node_prompt.

required

collect_issues

collect_issues(graph_name, schema, reduced, node_io: dict, may, must, concurrency_pairs: set, decision_choices: dict, end_with_children: set, unreachable: set)

Run all checks and return a list of Issues. See module docstring / spec.

Parameters:

Name Type Description Default
node_io dict

{name: (reads, writes)} for every real node.

required
concurrency_pairs set

set of frozenset({x, y}) node-name pairs that run in parallel (AND-parallel branches).

required
decision_choices dict

{decision_name: [(choice_name, has_child_bool)]}.

required
end_with_children set

names of declared end nodes that still have children.

required
unreachable set

names of nodes not reachable from START.

required

compute_availability

compute_availability(initial: set, edges: list, writes: dict, send_workers: dict | None = None, spread_collectors: dict | None = None) -> tuple

Forward dataflow fixpoint over the tagged edge list.

Parameters:

Name Type Description Default
initial set

the INPUT keys — keys guaranteed available at invoke (reduced channels, schema keys no node writes, and user-declared inputs). Seeding with these (not all schema keys) is what makes the may/must analysis catch read-before-written on COMPUTED keys.

required
edges list

list of (src, dst, kind) where kind is "seq" (AND, unconditional) or "cond" (OR, exclusive — DecisionNode choices and the Send fan-out).

required
writes dict

{node_name: set(keys_written)}.

required
send_workers dict | None

{worker_name: forwarded_keys} — a Send worker sees ONLY the forwarded payload, not global state.

None
spread_collectors dict | None

{collector_name: (spread_pred_name, worker_name)} — a collector's input is the state just before the spread plus the worker's writes.

None

Returns:

Type Description
tuple

(may, must) dicts: {node_name: set(available_keys)}.

reduced_keys

reduced_keys(state) -> set

Keys whose annotation is Annotated[..., <reducer>] (i.e. have a reducer).

schema_keys

schema_keys(state) -> set

Every key the state schema declares (including inherited TypedDict keys).