TL;DR

Agent observability has three separate jobs:

  1. Trace records the bounded events needed to reconstruct a run.
  2. Eval measures whether the result met its contract.
  3. 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:

json
{
  "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=true field 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.

python
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.

typescript
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:

python
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.

Primary Sources