symfonic.core.nodes.precondition_gate¶
precondition_gate ¶
PreconditionGateNode -- routing-time enforcement of authored-tier procedural preconditions.
v7.7.4 (Jarvio PRE-FLIGHT enforcement ask, authored tier only).
Inserted into the LangGraph topology as the third sibling of
elicitation and interrupt in the react fan-out when
FrameworkConfig.procedural_enforce_preconditions=True.
Architectural rationale (from Jarvio's v7.7.2 push-back eval):
- The elicitation node at
presets.py:106-110IS the routing layer -- we don't need new-graph territory.precondition_gateis a parallel sibling that fires when a different state predicate hits. - The synthetic
ToolMessage(content=..., tool_call_id=...)pattern is already production-ready (seeask_userresume atengine.py:3173-3369). The gate reuses it. - Authored skills already store structured
metadata['precondition']viaseed_authored_skill(... precondition=...)(v7.5.0 / v7.6.2). Zero schema surgery for the authored tier. LLM-extractor structured extraction lands in v7.8 with the corpus.
Behavioural contract:
- When the LLM emits an
AIMessagewhosetool_callsreference a tool name matching ANY procedural skill's "action tool" (metadata['action_tool']if set, elsemetadata['steps'][0]), the gate reads that skill'smetadata['precondition']. - The precondition is treated as enforceable ONLY when it is a bare
identifier string (e.g.
"get_integration_status") or a list of bare identifiers. Free-text rules (e.g."before delivery, call get_integration_status") fall through with NO enforcement -- the bare-identifier regex partitions enforced-vs-advisory adopters cleanly without forcing schema migration. - For each bare identifier in the precondition, the gate scans the
conversation message history for a prior
ToolMessagewithname(ortool_call_id-resolved tool name) matching the identifier. - Any precondition tool NOT yet called this conversation produces a
synthetic
ToolMessage(content="Missing precondition: ...", tool_call_id=<the action's tool_call_id>, name=<action>, status= "error")injected into state. React sees the error and the model can recover by calling the precondition tool first.
create_precondition_gate_node ¶
create_precondition_gate_node(
get_active_skills: ActiveSkillsGetter,
) -> Callable[[dict[str, Any]], Awaitable[dict[str, Any]]]
Build a LangGraph node that enforces structured preconditions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
get_active_skills
|
ActiveSkillsGetter
|
async callable that takes the current state
and returns the list of procedural skill entries the gate
should match against. Typical implementation queries
|
required |
Returns:
| Type | Description |
|---|---|
Callable[[dict[str, Any]], Awaitable[dict[str, Any]]]
|
The node function suitable for ``workflow.add_node( |
Callable[[dict[str, Any]], Awaitable[dict[str, Any]]]
|
"precondition_gate", node)``. The node reads the latest |
Callable[[dict[str, Any]], Awaitable[dict[str, Any]]]
|
|
Callable[[dict[str, Any]], Awaitable[dict[str, Any]]]
|
returns a state update injecting synthetic |
Callable[[dict[str, Any]], Awaitable[dict[str, Any]]]
|
errors when enforcement fires. Returns an empty dict when |
Callable[[dict[str, Any]], Awaitable[dict[str, Any]]]
|
every tool call passes (so LangGraph treats the node as a |
Callable[[dict[str, Any]], Awaitable[dict[str, Any]]]
|
no-op). |
Source code in src/symfonic/core/nodes/precondition_gate.py
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 | |
filter_skills_by_state_predicates ¶
Drop skills whose state predicates do not all match state.
Shared by both v7.18.0 evaluation sites (L1 PRE-FLIGHT synthesiser
and reactive precondition_gate). Skills with no state
predicates pass through untouched -- the v7.7.4 behaviour for
tool-only preconditions is preserved byte-for-byte.
Conjunctive semantics: a skill must satisfy EVERY state predicate on its list to survive. Mirrors the existing tool-precondition semantic where every bare tool name must have been called.
Emits one DEBUG log line per dropped skill so adopters can debug "why isn't my Slack-only skill firing" without diffing the dispatched prompt.
Source code in src/symfonic/core/nodes/precondition_gate.py
precondition_condition ¶
Cheap state-shape inspector used by the conditional edge router.
Returns True when the latest AIMessage has tool_calls AND
state carries the _procedural_enforce_preconditions boolean
flag set by the engine (which is itself set from
FrameworkConfig.procedural_enforce_preconditions). The
final enforcement decision happens inside the gate node when it
actually queries the active skills -- this condition is a coarse
pre-check.
The condition deliberately does NOT query the procedural layer itself (that would force an async call from a sync conditional-edge callback). When the precondition_gate node runs and finds no enforcement is needed, it returns an empty state update and the next conditional edge routes onward.