TL;DR
Agent observability has three separate jobs:
- Trace records the bounded events needed to reconstruct a run.
- Eval measures whether the result met its contract.
- Debug explains which state transition, policy decision, dependency, or budget caused a failure.
The goal is not to capture every token. The goal is to preserve enough evidence to answer an operational question while minimizing sensitive data and keeping quality signals independent from the model's own explanation.
Why Agent Observability Is Different
Traditional APM can often treat a successful HTTP response as a useful technical signal. An agent can return HTTP 200 while selecting the wrong tool, violating a tenant boundary, citing unsupported evidence, or spending an unacceptable amount of money. Conversely, a dependency timeout may be a technical failure even when the agent correctly refuses to act.
| Question | Technical telemetry | Agent-specific evidence |
|---|---|---|
| Did the request finish? | status, duration, exception | final outcome and cancellation reason |
| What did the runtime do? | span tree and dependency calls | state transitions, tool calls, policy decisions |
| Was the answer good? | rarely observable by APM | contract checks, evidence checks, human or model assessment |
| Did the system stay safe? | auth errors and network events | denied actions, cross-tenant attempts, external egress |
| What did it cost? | CPU and memory | input/output tokens, provider price, tool and retry budgets |
Do not infer semantic quality from a status code, a model explanation, or a single scalar score. Store the evidence required for each decision separately.
The Event Contract Comes First
Before choosing LangSmith, Langfuse, Phoenix, a vendor backend, or a custom store, define an event contract owned by the application:
{
"event": "tool_call_completed",
"schema_version": 3,
"run_id": "run_7f...",
"trace_id": "4bf92f...",
"step": 4,
"tenant_hash": "h:...",
"subject_hash": "h:...",
"tool": "invoice.list",
"tool_version": "2026-05-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
}
}
Recommended event types include run_started, model_call_completed, tool_call_requested, policy_decision, tool_call_completed, state_transition, budget_exhausted, run_completed, and run_failed. Each event should have a stable schema version, a run identifier, and a redaction policy.
The contract makes backend migration possible. A trace platform can store transport metadata; the business event remains meaningful if the team later changes SDKs or model providers.
Trace Boundaries and Privacy
OpenTelemetry is a useful vendor-neutral transport for traces, metrics, and logs. It does not decide what an agent should record. Use spans for timing and causal relationships, and events or logs for structured business outcomes.
Avoid these defaults:
- raw prompts or full tool arguments in every span;
- complete model responses, hidden reasoning, or private retrieved documents;
- bearer tokens, API keys, authorization headers, or raw user IDs;
- unbounded tool results as span attributes;
- a single
success=truefield that hides policy denial or semantic failure.
Use hashes or stable pseudonyms only when they serve a documented correlation purpose. Hashing does not make data anonymous if the input space is small or the mapping is reversible. Apply tenant access controls to the telemetry store, and propagate deletion requests to derived exports and evaluation datasets.
from dataclasses import dataclass
from hashlib import sha256
from typing import Any
from opentelemetry import trace
def digest(value: str) -> str:
return "sha256:" + sha256(value.encode("utf-8")).hexdigest()
@dataclass(frozen=True)
class RunContext:
run_id: str
tenant_id: str
subject_id: str
def record_tool_event(
tracer: trace.Tracer,
context: RunContext,
*,
tool_name: str,
tool_version: str,
arguments: dict[str, Any],
policy_decision: str,
state_version: int,
duration_ms: int,
result_class: str,
error_code: str | None,
) -> None:
with tracer.start_as_current_span("ToolCall") as span:
span.set_attribute("agent.run_id", context.run_id)
span.set_attribute("agent.tenant_hash", digest(context.tenant_id))
span.set_attribute("agent.subject_hash", digest(context.subject_id))
span.set_attribute("agent.tool.name", tool_name)
span.set_attribute("agent.tool.version", tool_version)
span.set_attribute("agent.argument_digest", digest(repr(sorted(arguments.items()))))
span.set_attribute("agent.policy.decision", policy_decision)
span.set_attribute("agent.state.version", state_version)
span.set_attribute("agent.duration_ms", duration_ms)
span.set_attribute("agent.result.class", result_class)
if error_code:
span.set_attribute("agent.error.code", error_code)
This fragment records a digest and outcome class, not the argument payload. A separate, access-controlled audit record may retain a narrowly selected field when a legal or operational purpose requires it.
Span Naming and Context Propagation
Use stable, low-cardinality span names such as AgentRun, ModelCall, ToolCall, PolicyCheck, and StateTransition. Do not put user input, resource IDs, or arbitrary tool arguments in span names. Propagate W3C trace context across gateways and trusted tool services, and create a new internal run ID if a request crosses a tenant or security boundary.
Keep the following identifiers distinct:
trace_id: transport-level causal correlation;run_id: one agent execution and its retry/replay identity;request_id: an external request or idempotency boundary;argument_digest: exact input identity without exposing its contents.
When a tool call is denied, emit the policy decision and reason class. Do not use the trace backend as the authorization system.
Quality Evaluation Is a Separate Plane
Evaluation should answer a specific question with a known oracle:
| Evaluation layer | Example oracle | Best use |
|---|---|---|
| Contract | schema, enum, citation, budget, policy | every run or CI |
| Scenario | expected tool, resource, state, and outcome | regression suite |
| Replay | fixed model, prompt, fixtures, and tool doubles | release comparison |
| Abuse | injection, cross-tenant, exfiltration, duplicate side effect | security gate |
| Human review | rubric and adjudication | ambiguous or high-impact cases |
| Model-based judge | calibrated rubric and reference set | scalable semantic signal |
Do not let the evaluated model grade its own hidden reasoning. Judge observable outputs, evidence support, policy decisions, and side effects. A judge's confidence field is not a calibrated probability unless it has been calibrated against labeled examples.
A Safer Judge Contract
Pass only the minimum data needed for the rubric. Treat retrieved text and model output as untrusted input; delimit them and prevent them from changing the evaluator's instructions.
type JudgeInput = {
question: string;
answer: string;
evidence: Array<{ id: string; text: string }>;
};
type JudgeResult = {
supported: "yes" | "no" | "uncertain";
relevant: "yes" | "no" | "uncertain";
missingEvidence: string[];
rationale: string;
};
function validateJudgeResult(value: unknown): JudgeResult {
if (!value || typeof value !== "object") throw new Error("invalid_judge_result");
const result = value as Record<string, unknown>;
const allowed = new Set(["yes", "no", "uncertain"]);
if (!allowed.has(String(result.supported)) ||
!allowed.has(String(result.relevant)) ||
!Array.isArray(result.missingEvidence) ||
!result.missingEvidence.every((item) => typeof item === "string")) {
throw new Error("invalid_judge_result");
}
return {
supported: result.supported as JudgeResult["supported"],
relevant: result.relevant as JudgeResult["relevant"],
missingEvidence: result.missingEvidence as string[],
rationale: typeof result.rationale === "string" ? result.rationale : "",
};
}
Use deterministic checks for permission, tool arguments, structured output, evidence IDs, budgets, and idempotency. Compare model-based scores with a labeled calibration set, monitor inter-rater agreement, and send high-impact disagreements to a human reviewer.
Metrics That Explain Outcomes
Avoid a dashboard made only of average latency and one quality score. Track dimensions that map to an action:
- run completion, cancellation, and budget-exhaustion rates;
- model calls, tool calls, retries, and state transitions per run;
- policy allow/confirm/deny counts, including cross-tenant denials;
- tool argument validation failures and result-size violations;
- evidence coverage and unsupported-claim rate on sampled evaluations;
- p50/p95/p99 latency by model, tool, tenant class, and outcome;
- input/output tokens, estimated provider cost, and cost per successful task;
- queue time, downstream error class, and cancellation propagation.
Define metric dimensions with bounded vocabularies. A label containing a user query or arbitrary resource ID will create cardinality and privacy problems.
Debugging Without Capturing Chain-of-Thought
An agent loop is observable through state and action events:
from collections.abc import Iterable
from dataclasses import dataclass
@dataclass(frozen=True)
class StepEvent:
step: int
tool: str | None
argument_digest: str | None
state_version: int
policy: str
outcome: str
def detect_stall(events: Iterable[StepEvent], window: int = 4) -> bool:
sequence = list(events)
if len(sequence) < window * 2:
return False
recent = sequence[-window:]
previous = sequence[-window * 2:-window]
signature = lambda item: (
item.tool,
item.argument_digest,
item.state_version,
item.policy,
item.outcome,
)
return [signature(item) for item in recent] == [
signature(item) for item in previous
]
Combine repeated signatures with explicit budgets, unchanged state versions, duplicate idempotency keys, and dependency timeouts. A repeated tool name alone is not proof of a loop: pagination and polling can be legitimate. The runtime, not the telemetry backend, must enforce maximum steps and cancel work.
For replay, store redacted event fixtures and deterministic tool doubles. Re-run the same workflow with a pinned model or recorded model response, then compare the event contract and business outcome. Do not call this “time travel” if external side effects are executed again; use dry-run or compensation semantics.
Sampling and Cost Control
Sampling is a policy decision, not a universal percentage:
| Signal | Possible policy |
|---|---|
| high-impact action or explicit incident | retain the required audit fields |
| policy denial, timeout, or budget breach | retain an expanded redacted trace |
| ordinary low-risk run | retain metadata and a bounded sample |
| aggregate health metric | retain counters and histograms without content |
Make sampling deterministic per run_id so child spans are consistent. Tail sampling should be applied after enough outcome information is available, while privacy filtering must happen before export. Estimate cost from event size, volume, backend retention, evaluation calls, and deletion requirements.
Choosing a Stack
Choose components by contracts and data control, not by a universal ranking:
| Need | Candidate capability |
|---|---|
| vendor-neutral transport | OpenTelemetry SDK and Collector |
| trace exploration | an OTel-compatible backend |
| prompt/eval workflow | a platform that supports your data residency and retention needs |
| replay and regression | versioned fixtures, datasets, and deterministic tool doubles |
| dashboards and alerts | metrics backend with bounded labels |
Hosted and self-hosted products change quickly. Verify current SDK support, data residency, access controls, deletion behavior, export formats, and pricing in the provider documentation before selecting one. A platform's LangChain integration is not evidence that it supports your agent runtime or authorization model.
Production Controls
Access and Retention
- Separate operator, developer, evaluator, and tenant access to telemetry.
- Encrypt transport and storage; rotate exporter credentials.
- Keep raw content, event metadata, aggregate metrics, and evaluation artifacts in separate retention classes.
- Propagate subject deletion to trace backends, caches, exports, and evaluation datasets.
- Record the redaction and policy version so an event can be interpreted after a schema change.
Alerts
Alert on deviations from a workload-specific baseline:
- a sudden increase in denied or cross-tenant attempts;
- repeated state-without-progress patterns;
- p99 latency or queue time beyond the service objective;
- cost per successful task outside the approved budget;
- unsupported evidence or contract failures on a calibrated sample;
- exporter failure or telemetry loss.
Do not encode 90%, 80%, 15%, or a fixed “production-ready level” as universal thresholds. The correct threshold depends on impact, traffic, model, and business tolerance.
Release Gates
Require deterministic contract tests, scenario tests, abuse tests, replay comparisons, and a privacy review before enabling a new tool or model. For high-impact actions, require human approval or a separately authorized workflow; a trace or judge score cannot grant permission.
Common Failure Modes
| Failure | Evidence to collect | Corrective action |
|---|---|---|
| repeated loop | state version, action digest, budget events | stop the run and fix transition or budget |
| wrong tool | candidate set, selected tool, schema result | improve boundaries and add scenario cases |
| unsupported answer | evidence IDs and claim checks | require evidence or refuse |
| cross-tenant attempt | principal hash, tenant policy result | deny, alert, and inspect caller path |
| runaway cost | token counters, retries, model route | apply budget and routing policy |
| telemetry leak | redaction audit and access log | revoke access, delete affected exports, patch filter |
Frequently Asked Questions
What should an agent trace contain?
Record bounded identifiers, timing, versions, usage, policy decisions, result classes, and final business outcomes. Redact or omit content according to purpose. A trace is not a hidden-reasoning transcript.
Is OpenTelemetry an agent-specific standard?
No. It is a vendor-neutral observability foundation. The application must define agent events, privacy rules, and business semantics, then map those events to spans, logs, and metrics.
Can an LLM judge be the release gate?
Not by itself. Combine deterministic checks, calibrated model assessment, replay, abuse tests, and human review for ambiguous high-impact cases.
How can I debug an agent loop without storing chain-of-thought?
Use bounded step events, state versions, argument digests, policy outcomes, and budgets. Replay with redacted fixtures and tool doubles.
How should telemetry retention be chosen?
Choose it from purpose, sensitivity, legal obligations, incident needs, and deletion propagation. There is no universal duration or sampling rate.
Conclusion
Good agent observability is a controlled evidence system. OpenTelemetry can carry causal context, but an application-owned event contract defines what matters. Evaluation should measure observable contracts rather than trust model explanations. Debugging should reconstruct state and action transitions without turning private prompts or hidden reasoning into a permanent log. When trace, eval, policy, privacy, and replay are designed together, operators can answer “what happened and what should change?” without creating a second data-governance problem.