symfonic.core.nodes.precondition_gate¶
precondition_gate ¶
PreconditionGateNode -- routing-time enforcement of authored-tier procedural preconditions.
v7.7.4 (adopter 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 the adopter'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
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 | |
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.