Skip to content

symfonic.platform.telemetry

telemetry

The seam that costs a kernel turn.

ConversationMetricsCollector.on_llm_end does two jobs: it records the call for the usage dashboard, and it fans the same usage into the process-wide TokenBudgetTracker so a per-tenant ceiling can fire on the next call. Nothing on the kernel path called it, so a generated app installed a budget tracker, mounted the 429 dependency, and never incremented a single row. A tenant over their real spend was not refused.

The gap was never the collector -- platform.observability already builds one and the host already holds it. What was missing was a place for a turn to hand it a call.

Why post-model. The kernel dispatches it once per model round, so a turn that calls three tools reports three calls rather than one. pre-model would be too early to know the usage, and finalize runs once and would collapse a multi-round turn into a single billing event.

What the stage never does is fail a turn. Telemetry that can break a run is worse than telemetry that is missing: the collector's own fan-out already swallows tracker failures for that reason, and this stage takes the same posture one layer out.

TelemetryCapability

TelemetryCapability(collector: Any, tenant: str = '', event_sinks: tuple[Any, ...] = ())

Reports each model round to the collector the host built.

Source code in src/symfonic/platform/telemetry.py
def __init__(
    self, collector: Any, tenant: str = "", event_sinks: tuple[Any, ...] = ()
) -> None:
    self._collector = collector
    self._tenant = tenant
    self._event_sinks = event_sinks
    self._sink = TelemetryEventSink(collector, event_sinks, tenant)

event_sink property

event_sink: Any

The collector's opt-in kernel event observer.

tenant property

tenant: str

Who these calls are charged to, or "" for nobody.

contribute

contribute(request: Any) -> Any

Declare the post-model stage and the handler that reports the call.

request is read for its grants and found to need none. Counting a call is not an effect on the turn: nothing downstream reads what this stage writes, and a grant would be authority never exercised.

Source code in src/symfonic/platform/telemetry.py
def contribute(self, request: Any) -> Any:
    """Declare the post-model stage and the handler that reports the call.

    ``request`` is read for its grants and found to need none. Counting a
    call is not an effect on the turn: nothing downstream reads what this
    stage writes, and a grant would be authority never exercised.
    """
    from symfonic.kernel.contracts.contributions import CapabilityContribution

    return CapabilityContribution(
        capability=TELEMETRY_CAPABILITY,
        stages=(
            StageDescriptor(
                stage_id=TELEMETRY_STAGE,
                phase=Phase.POST_MODEL,
                capability=TELEMETRY_CAPABILITY,
                priority=_PRIORITY,
            ),
        ),
        handlers={TELEMETRY_STAGE: self._handle},
    )

attribution_is_bound

attribution_is_bound(capability: TelemetryCapability) -> bool

Whether these calls can be charged to a tenant at all.

False means the agent was composed without a scope, so every call is recorded against the run and no per-tenant ceiling can ever fire. That is a legitimate choice for a single-tenant deployment and a silent billing hole for any other -- the same reason budget_is_shared exists next door.

Source code in src/symfonic/platform/telemetry.py
def attribution_is_bound(capability: TelemetryCapability) -> bool:
    """Whether these calls can be charged to a tenant at all.

    ``False`` means the agent was composed without a scope, so every call is
    recorded against the run and no per-tenant ceiling can ever fire. That is a
    legitimate choice for a single-tenant deployment and a silent billing hole
    for any other -- the same reason ``budget_is_shared`` exists next door.
    """
    return bool(capability.tenant)

telemetry

telemetry(services: Any, scope: Any = None) -> TelemetryCapability

Bind the host's collector to the turn, so a call is counted and charged.

Agent(provider, capabilities=[telemetry(resources.services, scope)])

Parameters:

Name Type Description Default
services Any

the observability bundle from platform.observability -- anything carrying a collector.

required
scope Any

the scope this agent was composed for. Without it the calls are recorded and charged to nobody; see :func:attribution_is_bound.

None
Source code in src/symfonic/platform/telemetry.py
def telemetry(services: Any, scope: Any = None) -> TelemetryCapability:
    """Bind the host's collector to the turn, so a call is counted and charged.

        Agent(provider, capabilities=[telemetry(resources.services, scope)])

    Args:
        services: the observability bundle from ``platform.observability`` --
            anything carrying a ``collector``.
        scope: the scope this agent was composed for. Without it the calls are
            recorded and charged to nobody; see :func:`attribution_is_bound`.
    """
    collector = getattr(services, "collector", services)
    try:
        event_sinks = tuple(getattr(services, "event_sinks", ()))
    except Exception:  # noqa: BLE001 - broken telemetry still cannot fail a turn
        event_sinks = ()
    return TelemetryCapability(
        collector,
        _tenant_of(scope) if scope is not None else "",
        event_sinks,
    )