Skip to content

symfonic.agent.preflight_l1

preflight_l1

v7.7.5 PRE-FLIGHT L1 imperative synthesiser.

Jarvio v7.5.1-L2-prose-vs-L1-imperative architectural ask. When FrameworkConfig.procedural_render_preflight_in_l1=True, the engine synthesises an imperative PRE-FLIGHT block from the active procedural skills and injects it into the L1 CACHED prompt prefix alongside adopter inject_contributions(position='cached') blocks.

Closes the v7.5.1 over-promise:

  • CHANGELOG v7.5.1 + Guide 10 framed PRE-FLIGHT: markers as a "hard ordering constraint" -- but the marker landed only in L2 MEMORY_CONTEXT (volatile, NOT cached) where Anthropic models treat the text as descriptive prose rather than imperative.
  • Jarvio's 3-probe wire evidence: rule fully rendered 3x, 0 tool_use blocks across both turns -- the model never attempted the action, so the v7.7.4 precondition_gate had nothing to intercept.
  • The reference pattern that DOES work for Anthropic models is the Jarvio _JARVIO_FAILURE_HANDLING directive at jarvio_domain.py:112-122 (their monorepo): a short, named section header in L1 with capitalised modals.

This module's :func:build_preflight_contribution mirrors that pattern:

  • Header: ## ENFORCED PRE-FLIGHT RULES (named section, matches the L0/L1 directive style adopters' models already follow).
  • Bullets: - YOU MUST call \`{pre_tool}\` BEFORE calling \`{action_tool}\` (skill: {label}). One bullet per (skill, bare-id precondition) pair so multi-precondition list[str] shapes (v7.6.2) render as N bullets.

Only bare-identifier preconditions participate -- free-text rules cannot be safely rewritten into YOU MUST call X without inventing tool names. The bare-identifier discriminator is shared with the v7.7.4 gate (_extract_bare_identifier in symfonic.core.nodes.precondition_gate).

Default off: when the knob is False, the synthesiser short-circuits at the first check and returns None. Zero new code paths fire when opt-out, preserving v7.7.4 byte-for-byte cache parity.

build_preflight_contribution

build_preflight_contribution(
    skills: list[Any], state: Any | None = None
) -> PluginContribution | None

Synthesise the L1 PRE-FLIGHT PluginContribution for this turn.

Returns None when the snapshot has no enforceable bare- identifier preconditions OR when the knob is off (caller responsible for the knob check; this function trusts its input). When non-None, the contribution lands at position="cached" with order=5 (BEFORE adopter plugins' default order=100) so the section sits immediately after the bundled L1 body and adopter blocks render below it.

header="" because the rendered body already carries the ## ENFORCED PRE-FLIGHT RULES header -- the contribution renderer would otherwise auto-prepend ### symfonic.procedural on the first emission per (origin, position).

origin="symfonic.procedural" makes the contribution grep-able in prompt audits and labels it as framework-emitted (vs adopter-plugin-emitted).

v7.18.0 (Option A): state is forwarded to :func:_build_preflight_body for state-predicate filtering. state=None preserves the pre-v7.18 byte-identical contribution shape for tests + direct callers that haven't yet plumbed state through.

Source code in src/symfonic/agent/preflight_l1.py
def build_preflight_contribution(
    skills: list[Any],
    state: Any | None = None,
) -> PluginContribution | None:
    """Synthesise the L1 PRE-FLIGHT ``PluginContribution`` for this turn.

    Returns ``None`` when the snapshot has no enforceable bare-
    identifier preconditions OR when the knob is off (caller responsible
    for the knob check; this function trusts its input).  When non-None,
    the contribution lands at ``position="cached"`` with
    ``order=5`` (BEFORE adopter plugins' default ``order=100``) so the
    section sits immediately after the bundled L1 body and adopter
    blocks render below it.

    ``header=""`` because the rendered body already carries the
    ``## ENFORCED PRE-FLIGHT RULES`` header -- the contribution
    renderer would otherwise auto-prepend ``### symfonic.procedural``
    on the first emission per ``(origin, position)``.

    ``origin="symfonic.procedural"`` makes the contribution
    grep-able in prompt audits and labels it as framework-emitted (vs
    adopter-plugin-emitted).

    v7.18.0 (Option A): ``state`` is forwarded to
    :func:`_build_preflight_body` for state-predicate filtering.
    ``state=None`` preserves the pre-v7.18 byte-identical contribution
    shape for tests + direct callers that haven't yet plumbed state
    through.
    """
    body = _build_preflight_body(skills, state=state)
    if body is None:
        return None
    return PluginContribution(
        content=body,
        position="cached",
        order=_L1_PREFLIGHT_ORDER,
        header="",
        origin=_L1_PREFLIGHT_ORIGIN,
    )