Skip to content

symfonic.agent.cutover.run_observability

run_observability

One run's half of TA8.20's observability: the handle the compile seam takes.

Its companion, :mod:symfonic.agent.cutover.observability, owns the agent-lifetime half and explains the split. The short version is that RunScope is the identity every observation is attributed to and three of its five fields (run_id, session_id, tenant_id) are facts about one turn, so a suite built once per agent cannot be a scope built once per agent.

This module holds one class, and it is separate from the suite for the reason the repository asks of every module: the two have different lifetimes. Reading "what did this deployment buy" and "what is this turn attributed to" in one file is what let the first draft of this seam attach a per-run identity to an agent-lifetime object.

RunObservability

RunObservability(suite: ObservabilitySuite, *, run_id: str, session_id: str | None, tenant_id: str | None, entry_point: str, prompt: str = '', request: Any = None)

One run's event sink: built when the plan compiles, released with it.

Instances are callables so they can be handed to AgentPlanFactory.compile(..., sink_factory=...): the factory calls this object once, with the ModelResolution it just computed, and binds the return value as ServiceBindings.event_sink. Taking the resolution as an argument rather than re-deriving the model here is what keeps RunScope naming the model the plan actually runs — a second resolution is a second answer that can disagree with the first.

:attr:sink is None until the plan compiles, and stays None when nothing is watching. :meth:release is safe in either state and safe after a normal terminal, which is why the caller may put it in a bare finally.

Source code in src/symfonic/agent/cutover/run_observability.py
def __init__(
    self,
    suite: ObservabilitySuite,
    *,
    run_id: str,
    session_id: str | None,
    tenant_id: str | None,
    entry_point: str,
    prompt: str = "",
    request: Any = None,
) -> None:
    self._suite = suite
    self._lineage = (
        str(getattr(request, "root_run_id", "") or ""),
        str(getattr(request, "parent_run_id", "") or "") or None,
    )
    self._run_id = run_id
    self._session_id = session_id
    self._tenant_id = tenant_id
    self._entry_point = entry_point
    self._prompt = prompt
    self.sink: Any = None

release async

release() -> None

Release what an abandoned run left held. Idempotent, never raises.

Source code in src/symfonic/agent/cutover/run_observability.py
async def release(self) -> None:
    """Release what an abandoned run left held. Idempotent, never raises."""
    sink = self.sink
    if sink is None:
        return
    aclose = getattr(sink, "aclose", None)
    if aclose is None:
        return
    await aclose()