The Design Goal
Agent observability should answer three different questions:
- What happened? Protocol, state, policy, tool, latency, and error evidence.
- Did it achieve the task? Deterministic checks, scenario evaluation, human review, and calibrated judges.
- What did it cost? Reconciled provider usage, downstream charges, retries, and budget outcomes.
These questions need different data and retention. Putting all raw model input and output into one permanent trace increases privacy and security risk without guaranteeing better debugging.
An Event Contract, Not a Transcript
Define an application-owned event schema. A bounded event can look like:
{
"event": "tool_call_completed",
"schema_version": 3,
"run_id": "run_7f...",
"request_id": "req_21...",
"trace_id": "4bf92f...",
"step": 4,
"tenant_hash": "h:...",
"subject_hash": "h:...",
"tool": "invoice.list",
"tool_version": "2026-06-1",
"argument_digest": "sha256:...",
"policy": "allow",
"state_version": 12,
"duration_ms": 184,
"result_class": "bounded_page",
"error_code": null,
"usage": {
"input_tokens": 812,
"output_tokens": 164,
"estimated_cost_usd": 0.0019,
"price_table_version": "2026-06-01"
}
}
The event is evidence, not a claim that the model’s private reasoning was captured. Keep trace_id, run_id, and request_id distinct: one trace can contain many runs, and a request can be retried.
Recommended Fields
| Field | Why it matters | Default treatment |
|---|---|---|
| run/request/trace ID | correlate bounded events | opaque identifiers |
| tenant/subject | aggregate and investigate access | keyed hash or approved surrogate |
| tool/version | compare behavior across releases | low-cardinality names |
| argument digest | detect duplicate or changed calls | digest, not raw arguments |
| policy decision | explain allow/deny/confirm | enum |
| state version | detect stale updates and loops | integer |
| result class | measure bounded outcome | enum and size bucket |
| duration/error | diagnose operations | numeric and controlled code |
| usage/cost | reconcile budgets | source and price version |
Never put access tokens, authorization headers, raw secrets, hidden reasoning, or unrestricted user content into a default span.
Redaction and Retention
Privacy is part of the observability design:
- classify fields before instrumentation;
- redact at the producer, not only in a downstream dashboard;
- hash identifiers with a tenant-appropriate key and document rotation;
- bound strings, arrays, tool results, and exception messages;
- keep raw fixtures in a separate access-controlled store when a replay is justified;
- define retention by purpose and legal basis;
- propagate deletion to traces, indexes, caches, backups, exports, and evaluation datasets;
- test that denied requests do not leak object existence through telemetry.
Sampling is not a privacy policy. A 1% sample can still contain a sensitive record, while a security event may require complete metadata without raw content.
Trace Semantics
Use spans or events for observable transitions:
run_started
-> model_call_completed
-> tool_call_requested
-> policy_decision
-> tool_call_completed
-> state_updated
-> run_finished
Record the policy decision and outcome separately. A model-proposed Tool call is not an executed Tool call. A successful HTTP response is not proof that the business action was authorized or correct.
Loop detection can use bounded evidence:
from dataclasses import dataclass
from typing import Iterable
@dataclass(frozen=True)
class StepEvent:
step: int
tool: str | None
argument_digest: str | None
state_version: int
policy: str
outcome: str
def repeats(events: Iterable[StepEvent], window: int = 4) -> bool:
items = list(events)
if window <= 0 or len(items) < window * 2:
return False
signature = lambda event: (
event.tool,
event.argument_digest,
event.state_version,
event.policy,
event.outcome,
)
return [signature(e) for e in items[-window * 2:-window]] == [
signature(e) for e in items[-window:]
]
This identifies repeated observable behavior. It does not infer hidden model reasoning or prove that a loop is malicious.
Evaluation Is Measurement
Use several evaluation layers with explicit oracles:
| Layer | Example | Strength | Limitation |
|---|---|---|---|
| contract | schema, event, policy, size, timeout checks | deterministic | narrow scope |
| scenario | expected tool, object, state, and final outcome | task-oriented | requires fixtures |
| replay | fixed redacted cases across versions | regression evidence | may miss new distribution |
| abuse | cross-tenant, injection, duplicate, and egress cases | safety evidence | needs threat modeling |
| human review | expert labels and disagreement analysis | nuanced judgment | costly and variable |
| model judge | calibrated rubric with evidence | scalable triage | biased and non-deterministic |
Do not collapse these into a single “quality score.” A high judge score cannot override a failed authorization invariant.
Calibrating an LLM Judge
If a judge is useful for triage:
- define a rubric and allowed evidence;
- create positive, negative, ambiguous, and adversarial examples;
- compare it with independent human labels;
- measure agreement by task slice, not only a global average;
- test prompt, model, and rubric changes as evaluator regressions;
- keep the judge’s result separate from the authoritative pass/fail oracle.
Do not ask a judge to reconstruct hidden reasoning. Evaluate observable claims, citations, tool outcomes, policy events, and task results.
Cost Accounting
A cost ledger should preserve source and uncertainty:
{
"run_id": "run_7f...",
"currency": "USD",
"input_tokens": 812,
"output_tokens": 164,
"cached_tokens": 0,
"provider_charge": 0.0017,
"tool_charge": 0.0002,
"retry_charge": 0,
"estimated": false,
"price_table_version": "provider-2026-06-01"
}
Reconcile by run, tenant, model, task type, tool, and release. Separate user-visible billing from internal estimates. Account for retries, canceled work, cache hits, streaming, currency conversion, and downstream services.
Cost controls should be budgets and policy:
- per-request and per-run token/step/time limits;
- per-principal and tenant quotas;
- model routing tested against quality and latency;
- cache only when privacy, freshness, and key scope are correct;
- approval for expensive or external side effects;
- alerts based on workload baselines and budget commitments.
There is no universal saving percentage. Measure the workload before and after a change.
Tool and Framework Choice
Langfuse, LangSmith, Phoenix, Helicone, Braintrust, and OpenTelemetry differ in hosting, data model, integrations, retention, evaluation features, and pricing. A useful selection record includes:
| Decision | Evidence to collect |
|---|---|
| data residency | regions, subprocessors, encryption, deletion API |
| instrumentation | SDK versions, OpenTelemetry export, async behavior |
| privacy | producer redaction, field controls, access audit |
| evaluation | datasets, human labels, judge calibration, replay |
| operations | sampling, tail latency, alerts, outage behavior |
| cost | ingestion, retention, query, seat, and egress pricing |
| exit | export format, ownership, and migration effort |
Use the least data and smallest integration that answers the current operational question. Re-evaluate vendors as versions and pricing change.
A Risk-Ordered Rollout
Stage 1: Contract and privacy
Define event schemas, redaction, retention, deletion, access roles, and budget fields before adding dashboards.
Stage 2: Runtime evidence
Instrument initialization, model calls, Tool proposals, policy decisions, execution outcomes, state changes, cancellations, and errors with bounded fields.
Stage 3: Evaluation
Create a small redacted scenario set, deterministic oracles, abuse cases, replay comparisons, and a human review path. Add a judge only after its disagreement is measurable.
Stage 4: Cost and operations
Reconcile provider invoices, set workload-specific budgets, monitor tail latency and downstream saturation, and test exporter failure without blocking the agent unexpectedly.
Stage 5: Release gates
Require contract, scenario, replay, abuse, privacy, and cost evidence for changes to prompts, models, tools, policies, and instrumentation.
What Observability Cannot Prove
- A trace cannot prove that a model’s reasoning was truthful.
- A successful Tool span cannot prove object authorization.
- A judge score cannot prove safety for unseen inputs.
- A token count cannot prove the provider invoice.
- A dashboard cannot replace deletion, access control, or incident response.
- An OpenTelemetry exporter cannot define application semantics for you.
Production Checklist
- [ ] Own and version the event contract.
- [ ] Redact and bound at the producer.
- [ ] Keep hidden reasoning, tokens, secrets, and raw user content out of default spans.
- [ ] Separate trace, run, and request identifiers.
- [ ] Record policy decisions and business outcomes independently.
- [ ] Use contract, scenario, replay, abuse, human, and calibrated judge evaluation.
- [ ] Keep authoritative oracles separate from model scores.
- [ ] Reconcile usage with a versioned price table and uncertainty flag.
- [ ] Propagate deletion through telemetry and evaluation stores.
- [ ] Test exporter outages, retention, access, rollback, and budget enforcement.
Conclusion
Good Agent observability is not a transcript of everything the model saw or “thought.” It is a privacy-aware evidence system: bounded events explain execution, layered evaluation measures outcomes, and a reconciled ledger controls cost. Design those contracts before choosing a dashboard, then keep every claim proportional to the evidence it can actually provide.