Skip to content

symfonic.platform.governance_subject

governance_subject

What a governance stage is handed, built from a kernel turn.

Split from :mod:symfonic.platform.governance_rungs, which decides which rung runs which stages. This decides what those stages get to look at, and the two changed for different reasons often enough to be read separately -- and together they crossed the repository's module-size budget, which is usually where that shows up first.

Two vocabularies meet here and neither is wrong: the kernel names a tool request's payload arguments, governance names it args. Handing the kernel's object straight to a stage raised AttributeError on the first scrub, under a fail-closed policy -- the loudest way to find a field rename, and still not the same as translating it.

subject_for

subject_for(gov_phase: str, turn_request: Any, context: Any) -> GovernanceSubject

What this rung hands the pipeline to examine.

Each rung sees what exists by then and nothing it would have to invent: the query is present from the start, tool calls once the model has asked for them, a draft only after the model has answered.

Source code in src/symfonic/platform/governance_subject.py
def subject_for(gov_phase: str, turn_request: Any, context: Any) -> GovernanceSubject:
    """What this rung hands the pipeline to examine.

    Each rung sees what exists by then and nothing it would have to invent: the
    query is present from the start, tool calls once the model has asked for
    them, a draft only after the model has answered.
    """
    query = str(getattr(turn_request, "prompt", "") or "")
    # What the caller declared about this turn, already frozen at the facade
    # boundary. The same object at every rung of one turn, because it is on the
    # request the kernel carries -- so a rule cannot be told one thing before
    # the model answers and another before the tool runs.
    #
    # Never invented. This used to be empty everywhere with a comment saying
    # the request carried nothing shaped like it, which was true and is why the
    # field was added rather than synthesised from the fields that did exist.
    # A scrubber reporting on a bag someone assembled out of prompt and scope
    # is reporting on something it was not written to protect.
    properties = getattr(turn_request, "properties", None) or EMPTY_PROPERTIES
    if gov_phase == "ingress":
        return GovernanceSubject(query=query, properties=properties)

    # Everything after the model call reads the model's own turn: ``text`` is
    # the draft and ``tool_requests`` the calls it asked for. Found by
    # inspection, because reading ``context.draft`` -- the name the phase
    # suggests -- silently produced an empty draft, and a metacognition gate
    # over an empty draft never fires and never says why.
    turn = getattr(context, "turn", None)
    draft = str(getattr(turn, "text", "") or "")
    if gov_phase in ("effect", "post-tool"):
        return GovernanceSubject(
            query=query,
            draft=draft,
            properties=properties,
            tool_calls=_calls_of(context, turn, results=gov_phase == "post-tool"),
        )
    return GovernanceSubject(
        query=query, draft=draft, properties=properties,
        tool_calls=_egress_calls(context, turn),
    )