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(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),
)
|