Skip to content

symfonic.services.observability.values

values

Observation values — what an observer is told, in one vocabulary.

Every type here is a frozen, stdlib-only dataclass. That is the same discipline :mod:symfonic.kernel.contracts.values applies to the loop, and for the same reason: an observer written against these types never learns a provider SDK, a span API, or a metrics schema, so a new exporter is a new adapter rather than a new emission site inside the engine.

Payload fields (query, text, arguments) are None when the capture policy denied them. None here means withheld, never absent — the structural siblings (length, argument_names) stay populated so a denied run is still diagnosable.

CostObservation dataclass

CostObservation(scope: RunScope, model: str, usage: UsageDelta, cost_usd: float, pricing_unknown: bool, breakdown: Mapping[str, int] = (lambda: MappingProxyType({}))(), text: str | None = None, duration_ms: float = 0.0)

What the run cost, and whether the registry could actually say.

pricing_unknown is the discriminator that keeps "a $0 run" apart from "a run whose model the pricing registry did not recognise" — two states that look identical in a dashboard and mean opposite things on a bill.

EventsDropped dataclass

EventsDropped(scope: RunScope, index: int, dropped_kind: str, dropped_count: int, first_dropped_index: int | None, last_dropped_index: int | None)

A shed notice: what the adapter could not deliver, and over what range.

RunFailed dataclass

RunFailed(scope: RunScope, index: int, error_type: str, error_message: str)

The run emitted its error terminal.

The kernel formats the error as "TypeName: message". Splitting it here once means every downstream schema — span status, NodeErrorEvent, a log line — gets the same two fields instead of re-parsing the string.

RunFinished dataclass

RunFinished(scope: RunScope, index: int, reason: str, text: str | None, usage: UsageDelta, duration_ms: float, stop_reason: str | None)

The run reached exactly one terminal event (EVT-1).

RunScope dataclass

RunScope(run_id: str, tenant_id: str | None, session_id: str | None, entry_point: str, model: str, provider_family: str = '', prompt: str = '', root_run_id: str = '', parent_run_id: str | None = None)

The identity every observation is attributed to.

Built once per invocation by whoever composes the sink, because the kernel event stream carries a run_id and nothing else: tenant, session, model and entry point are plan facts, and re-deriving them per event is how two observers end up disagreeing about which tenant paid for a run.

RunStarted dataclass

RunStarted(scope: RunScope, query: str | None)

The run has produced its first event.

TextEmitted dataclass

TextEmitted(scope: RunScope, index: int, kind: str, text: str | None, length: int)

One text-bearing event from the stream, in stream order.

ToolCompleted dataclass

ToolCompleted(scope: RunScope, index: int, outcome: Any = None)

A tool returned. The outcome is opaque, exactly as it is to the kernel.

ToolInvoked dataclass

ToolInvoked(scope: RunScope, index: int, call_id: str, name: str, arguments: Mapping[str, Any] | None, argument_names: tuple[str, ...] = ())

The loop dispatched an allowlisted tool call.