Skip to content

symfonic.capabilities.prompting.guardrails

guardrails

A domain's guardrails, public, and delivered to both places they belong.

CorePreference is what a scaffold uses to say "confirm before changing stock", and it is imported from symfonic.agent.domain -- a path registered as open gap PUB-4, which means the generated project depends on a surface the framework has not committed to. This is the committed one.

Two destinations, and today's field reaches one. A guardrail belongs in the prompt, so the model knows the rule exists, and in the governance pipeline, so a turn that crosses it is caught. DomainTemplate.core_preferences feeds only the second, and only when metacognition_enabled is on -- which is off by default. A deployment that wrote three guardrails and never enabled metacognition had three sentences nothing read.

A rule the model is never told is not a constraint on its behaviour. It is a criterion someone else will judge that behaviour by, which is a different and much later thing.

Why this lives in prompting rather than in governance. It was written there first and the governance package refused it: that package imports nothing outside itself, on the rule that "a capability that reaches into the legacy agent is not extracted". A module producing prompt sources would have broken containment for every module beside it. So the value lives with the half that needs an import, and :func:sensitive_terms returns plain strings -- governance consumes them without depending on anything here, which is the direction that keeps both packages contained.

Guardrail dataclass

Guardrail(statement: str, sensitive_tags: Sequence[str] = tuple(), priority: int = 0)

One rule a deployment wants honoured, and the tags that watch it.

Frozen, like every other composition input: a plan compiled from mutable rules is a plan whose rules nobody can quote afterwards.

guardrail_sources

guardrail_sources(guardrails: Iterable[Guardrail]) -> tuple[Any, ...]

The prompt contributions guardrails render through.

Ordered by descending priority, because a budget drops from the end and priority already existed on this value while deciding nothing. If two rules cannot both fit, the deployment has said which one matters.

No guardrails contribute no source -- not an empty section. A heading with nothing under it reads as "this deployment has no rules", which is a claim rather than an absence.

Source code in src/symfonic/capabilities/prompting/guardrails.py
def guardrail_sources(guardrails: Iterable[Guardrail]) -> tuple[Any, ...]:
    """The prompt contributions ``guardrails`` render through.

    Ordered by descending priority, because a budget drops from the end and
    priority already existed on this value while deciding nothing. If two rules
    cannot both fit, the deployment has said which one matters.

    No guardrails contribute no source -- not an empty section. A heading with
    nothing under it reads as "this deployment has no rules", which is a claim
    rather than an absence.
    """
    ordered = sorted(guardrails, key=lambda rule: -rule.priority)
    return tuple(
        StaticSource(
            text=rule.statement.strip(),
            untrusted=False,
            offline_safe=True,
            scope_aware=False,
        )
        for rule in ordered
    )

sensitive_terms

sensitive_terms(guardrails: Iterable[Guardrail]) -> tuple[str, ...]

The terms a governance stage should watch, from the rules themselves.

MetacognitionStage takes terms and guardrails carry tags; deriving one from the other is what stops them drifting. Threaded by hand, a tag added to a rule and forgotten in the stage's configuration is a rule that renders and is never enforced.

Sorted and deduplicated, because an unordered set makes a stage's configuration differ between runs for no reason a reader can see.

Source code in src/symfonic/capabilities/prompting/guardrails.py
def sensitive_terms(guardrails: Iterable[Guardrail]) -> tuple[str, ...]:
    """The terms a governance stage should watch, from the rules themselves.

    ``MetacognitionStage`` takes terms and guardrails carry tags; deriving one
    from the other is what stops them drifting. Threaded by hand, a tag added
    to a rule and forgotten in the stage's configuration is a rule that renders
    and is never enforced.

    Sorted and deduplicated, because an unordered set makes a stage's
    configuration differ between runs for no reason a reader can see.
    """
    return tuple(
        sorted({tag.strip() for rule in guardrails for tag in rule.sensitive_tags if tag.strip()})
    )