symfonic.core.observability¶
observability ¶
symfonic.core.observability โ Instrumentation hooks.
NODE_LABELS
module-attribute
¶
NODE_LABELS: dict[str, str] = {NodeName.REACT.value: 'React Loop', NodeName.METACOGNITION_CRITIC.value: 'Metacognition Critic', NodeName.METACOGNITION_SKILL_GATE.value: 'Skill-Gate Critic', NodeName.CONTEXT_WINDOW_SUMMARISER.value: 'Context Window Summariser', NodeName.INSIGHT_EXTRACTOR.value: 'Insight Extractor', NodeName.ENTITY_EXTRACTOR_LLM.value: 'Entity Extractor (LLM)', NodeName.CONSOLIDATION_EXTRACTOR.value: 'Consolidation Extractor', NodeName.PROCEDURAL_ROUTER.value: 'Procedural Router', NodeName.PROCEDURAL_EXTRACTOR_LLM.value: 'Procedural Extractor'}
UI-friendly labels for the canonical node_name set.
Adopters wiring an admin dashboard can surface these directly instead of synthesising labels at the UI layer::
from symfonic.core.observability import human_label
row_label = human_label(event.node_name) # round-trips unknown names
NODE_NAMES
module-attribute
¶
Set of canonical node_name strings emitted by the framework.
Adopters can pre-populate dashboards, validate pricing-table coverage, or allocate per-node budget ceilings using this set::
from symfonic.core.observability import NODE_NAMES
for node_name in NODE_NAMES:
budget_ceilings[node_name] = float(env.get(f"BUDGET_{node_name.upper()}", 0.50))
AuditLogEntry ¶
Bases: BaseModel
Immutable audit event emitted for every destructive operation.
Fields intentionally kept small so both in-memory and Postgres
implementations can store them without a schema migration dance.
metadata is the escape hatch for per-action context (counts,
previous value, confirmation token, ...).
action
class-attribute
instance-attribute
¶
Free-form action code. Convention: snake_case verb +
optional resource suffix, e.g. delete_memory, bulk_delete,
export_data, erase_all.
resource_type
class-attribute
instance-attribute
¶
E.g. memory_node, edge, procedure, tenant.
to_dict ¶
Return a JSON-safe dict representation.
Source code in src/symfonic/core/observability/audit.py
AuditLogger ¶
Bases: Protocol
Destination for audit events.
Implementations MAY persist synchronously or asynchronously. The contract is "record this entry, or raise" โ retry / batching is an implementation detail. Callers in the router layer shield failures (fire-and-forget) so a sick audit store never 500s a user request.
BudgetCheckResult
dataclass
¶
BudgetCheckResult(allowed: bool, reason: str | None = None, daily_used_usd: float = 0.0, daily_limit_usd: float | None = None, monthly_used_usd: float = 0.0, monthly_limit_usd: float | None = None)
Result of a pre-call budget check.
BudgetStore ¶
Bases: Protocol
Optional persistence layer for daily aggregates.
Implementations should be idempotent on conflict โ the tracker calls
upsert_daily after every LLM call, so the underlying storage must
handle UPSERT semantics (ON CONFLICT ... DO UPDATE).
The optional read_today / read_month methods hydrate the
tracker's in-memory buckets on demand so that multiple app replicas
converge on the same view of a tenant's cumulative usage. A store
that only implements upsert_daily still works (writes only) โ
multi-replica correctness requires all three.
read_month
async
¶
Return the aggregated month-to-date usage for tenant_id.
year_month is the YYYY-MM key produced by
:func:month_key. Optional โ see :meth:read_today.
Source code in src/symfonic/core/observability/budget_models.py
read_today
async
¶
Return today's usage row for tenant_id (None if absent).
Optional โ implementations without this method get write-through semantics only (single-process correctness).
Source code in src/symfonic/core/observability/budget_models.py
BudgetUsage
dataclass
¶
BudgetUsage(tenant_id: str, window: str, window_key: str, input_tokens: int = 0, output_tokens: int = 0, cached_tokens: int = 0, cache_write_tokens: int = 0, cost_usd: float = 0.0, request_count: int = 0, updated_at: datetime = (lambda: datetime.now(UTC))())
Cumulative usage for a single (tenant, window) pair.
ConversationMetricsCollector ¶
Bases: MetricsQuiescenceMixin, MetricsAttemptMixin, MetricsExecutionMixin, MetricsQueryMixin
Aggregates token usage and cost per conversation (session_id).
Satisfies the CallbackHandler protocol via structural typing.
Shared hosts partition records by tenant while preserving public session IDs.
Source code in src/symfonic/core/observability/metrics.py
on_agent_end
async
¶
Increment turn count and purge run-scoped bookkeeping.
on_llm_end
async
¶
Accumulate token/cost metrics into the matching conversation record.
Also fans the same usage out to the process-wide
:class:TokenBudgetTracker (if one has been registered via
set_budget_tracker) so per-tenant ceilings can fire on the
NEXT call. Failures in the tracker must never break the LLM flow.
Source code in src/symfonic/core/observability/metrics.py
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | |
on_token_composition
async
¶
Cache the composition breakdown keyed by run_id.
set_conversation ¶
Associate a run_id with a conversation (session) id.
Call this before the agent run so metrics land under the right conversation bucket (e.g. use the session_id as conversation_id).
Source code in src/symfonic/core/observability/metrics.py
set_lineage ¶
Associate one execution with its delegation root and parent.
Source code in src/symfonic/core/observability/metrics.py
set_tenant ¶
Associate a run_id with a tenant_id.
Call this before the agent run so on_llm_end can fan usage
into the registered TokenBudgetTracker. Without this the
tracker never fires because the ConversationRecord's tenant_id
stays empty.
Source code in src/symfonic/core/observability/metrics.py
InMemoryAuditLogger ¶
Default in-process logger โ keeps entries in an in-memory list.
Primarily for unit tests and local development. Production
deployments should register a durable implementation (e.g. the
scaffolded Postgres logger). The list is bounded by
max_entries to prevent unbounded growth when no one is draining
it.
Source code in src/symfonic/core/observability/audit.py
drain
async
¶
Return all buffered entries, clearing the buffer.
Tests use this to assert what was emitted during a request.
Source code in src/symfonic/core/observability/audit.py
entries ¶
Return a non-draining snapshot (sync, for inspection).
Not guaranteed to be consistent under concurrent record()
calls โ prefer drain() in assertions.
Source code in src/symfonic/core/observability/audit.py
record
async
¶
Append entry to the in-memory buffer (drops oldest when full).
Source code in src/symfonic/core/observability/audit.py
LLMLifecycleHook ¶
Bases: Protocol
Async instrumentation for LLM call start/end lifecycle.
NoOpObservabilityHook ¶
Zero-overhead default hook. All methods are no-ops.
Always registered in BaseAgentDeps so nodes can safely call deps.require(ObservabilityHook) without risk of MissingCapabilityError.
LSP: signatures match ObservabilityHook protocol exactly.
NodeLifecycleHook ¶
Bases: Protocol
Async instrumentation for graph-node start/end/error lifecycle.
NodeName ¶
Bases: StrEnum
Canonical LLM emission-site discriminators (v7.15.0).
ObservabilityHook ¶
Bases: NodeLifecycleHook, LLMLifecycleHook, ToolLifecycleHook, Protocol
Composed protocol -- union of all lifecycle hooks.
Backward-compatible: any class implementing all 7 methods satisfies this protocol. New consumers should prefer the granular protocols when they only need a subset of methods.
TokenBudgetTracker ¶
TokenBudgetTracker(*, default_daily_budget_usd: float | None = None, default_monthly_budget_usd: float | None = None, store: BudgetStore | None = None)
Tracks cumulative token usage + cost per tenant.
Two in-memory buckets per tenant (day, month) roll over
lazily on date/month boundaries. Limits configured globally via
default_*_budget_usd or per-tenant via meth:
set_tenant_budget;
None means unlimited. Admin callers bypass the breaker.
Source code in src/symfonic/core/observability/budget.py
check_budget
async
¶
Return whether tenant_id may incur another LLM call.
When a BudgetStore with read_today / read_month is
registered, the in-memory buckets are first hydrated from the
store so every replica shares one view of the running total.
Store errors are logged at WARNING and fall through to in-memory
state โ the ledger is never allowed to break auth. Admin
callers bypass the breaker.
Source code in src/symfonic/core/observability/budget.py
clear_tenant_budget ¶
Remove per-tenant overrides so the defaults apply again.
get_tenant_limits ¶
Return (daily_limit, monthly_limit) applied to tenant_id.
Source code in src/symfonic/core/observability/budget.py
record
async
¶
Record token usage for a single LLM call; returns its cost_usd.
Source code in src/symfonic/core/observability/budget.py
reset
async
¶
Reset in-memory day + month counters for a tenant.
When a persistent store is wired, the shared Postgres row is NOT cleared โ use the admin billing endpoint to zero the ledger.
Source code in src/symfonic/core/observability/budget.py
set_tenant_budget ¶
set_tenant_budget(tenant_id: str, *, daily_usd: float | None = None, monthly_usd: float | None = None) -> None
Override budget ceilings for a tenant; None = unlimited.
Source code in src/symfonic/core/observability/budget.py
ToolLifecycleHook ¶
Bases: Protocol
Async instrumentation for tool invocation start/end lifecycle.
emit_audit_event ¶
Schedule an audit write without blocking the caller.
Never raises โ failures are logged. If no logger is registered the call is a complete no-op (zero allocations beyond the entry itself).
The FastAPI routers use this directly in handlers so a slow / sick audit backend can never 500 a user request. The task is scheduled on the currently-running loop; when called outside an async context (defensive guard) we log a warning and drop the entry โ no audit logger is expected to live that far up the stack.
Source code in src/symfonic/core/observability/audit.py
exchanges_for_rows ¶
Align (prompt, response) pairs onto one run's snapshot rows.
rows must be the timestamp-ordered snapshots of a single run
group (as build_conversation_detail groups them). Position is
the join, input_tokens the verification: a mismatch attaches
nothing rather than a neighboring call's text (fail-closed on ring
skew, e.g. the flag toggled mid-process). All-None when the
flag is off or nothing was captured.
Source code in src/symfonic/core/observability/debug_exchanges.py
get_audit_logger ¶
get_budget_tracker ¶
human_label ¶
Return the UI-friendly label for node_name.
Unknown names (adopter-extended sites) round-trip unchanged so dashboards do not lose attribution -- this is the explicit non-validation contract documented at module level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_name
|
str
|
The discriminator string from |
required |
Returns:
| Type | Description |
|---|---|
str
|
The matching label from :data: |
str
|
|
Source code in src/symfonic/core/observability/node_names.py
observed_node ¶
observed_node(node_name: str, *, swallow_errors: bool = False, always_log: bool = False) -> Callable[..., Callable[..., Any]]
Decorator factory for observable graph nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_name
|
str
|
Identifier recorded in node_execution_log and passed to ObservabilityHook callbacks. |
required |
swallow_errors
|
bool
|
When True, catch non-programming exceptions
(not MissingCapabilityError, ConfigurationError) and return
|
False
|
always_log
|
bool
|
When True, append to node_execution_log even when
the wrapped function returns |
False
|
Usage::
@observed_node("react")
async def react(state: dict[str, Any]) -> dict[str, Any]:
... # pure business logic
Source code in src/symfonic/core/observability/decorators.py
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
set_audit_logger ¶
Register (or clear) the process-wide audit logger.
Call once at app startup. Passing None disables audit emission
โ useful for tests that registered a logger and want to tear down.
Source code in src/symfonic/core/observability/audit.py
set_budget_tracker ¶
Register (or clear) the process-wide budget tracker.
Consulted by check_budget_before_llm and SymfonicAgent.run();
when unset both skip the check.