Traces
Every agent run leaves a record you can replay later to see exactly what it did.
Every agent run leaves a trace: a lasting record of what the agent actually did, one you can replay step by step, no matter where the run started (the CLI, the HTTP API, a WebSocket, a Telegram or WhatsApp message, or a scheduled job). A trace answers “why did the agent do that?” long after the run is over.
What a trace holds
- The prompt that triggered the run, and how it ended (
ok, or an error with its reason). - How long it took, and the model token usage.
- The ordered stream of steps: each tool call with its arguments, each tool result, any permission denials, and every model failover.
- The final reply.
Nested sub-agent runs (an agent calling another through send_to_agent) fold
into the same trace, so one record shows the whole tree of work.
In the dashboard
Open Traces in the sidebar. The list shows the most recent runs for the current workspace scope with their outcome, duration, and the tools each one used. Click Replay on any run to walk it step by step: the prompt at the top, then a timeline of every tool call, result, failover, token count, and the final answer.
From the CLI
pepe traces # recent runs across all projects
pepe traces --project acme # only one project's runs
pepe traces --limit 10 # cap the list
pepe traces 1720000000123456 # replay one run by id, step by step
Where traces live
Traces are stored in the same small built-in SQLite file as commitments and watches,
grouped by project (the default project uses default). Each project keeps only a
limited number of traces: as new ones arrive, the oldest are deleted, so the file
never grows without limit. Very long tool arguments and results are shortened before
being stored.
Sending traces to an observability tool
Sending to Langfuse needs nothing beyond the credentials most
installs already have set for it (LANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY):
every finished run is sent as an OTLP trace the moment those are present,
off otherwise, and a delivery failure never touches the run it’s describing.
For any other OTLP-speaking backend, set OTEL_EXPORTER_OTLP_ENDPOINT
instead and it takes over completely:
export OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector.example.com
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <base64 of user:pass>"
OTEL_EXPORTER_OTLP_HEADERS is a comma-separated key=value list, sent as
literal request headers. Both generic OpenTelemetry attributes (gen_ai.*)
and Langfuse’s own (langfuse.*) are set on every span, so a Langfuse
endpoint renders fully and any other OTLP backend still gets a complete
trace either way. Two more standard OTEL variables, if you need them:
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT points the traces signal somewhere other
than <endpoint>/v1/traces, and OTEL_SERVICE_NAME renames the exported
service (default pepe). Full walkthrough: Langfuse.
Beyond the run’s prompt/reply and per-tool-call input/output, each exported
trace also carries: the channel it came from (Telegram, the API, …) as
trace metadata; the session key as session.id; user.id, set to the
actual sender’s display name whenever the channel can supply one (Telegram,
including a private chat, not just its group tag; WhatsApp, from the
contact’s profile; Google Chat; Microsoft Teams; Discord), falling back to
the session key on a surface with no such name to give, so a run in a
shared session (a Telegram or webhook group) is attributed to whoever
actually sent it rather than one shared id for the whole conversation; the
running Pepe version (langfuse.release); a level
(DEFAULT/WARNING/ERROR) derived from how the run actually finished;
and, on each model-call span, the cost of that call in your configured
currency, computed the same way the usage ledger computes it, and left off
entirely rather than sent as a misleading zero when the model has no known
price. A waterfall view’s timing (a tool call, a model generation) reflects
when each step actually happened, not an estimate.