Opt-in OTel logging for agent responses

Open 💬 4 comments Opened May 11, 2026 by chdeskur

What variant of Codex are you using?

CLI, App

What feature would you like to see?

Codex OpenTelemetry exports should support an explicit opt-in option to log the agent/assistant response content for a turn.

This feature is needed for monitoring and evaluating agent behavior, especially regarding adherence to SKILL.md directives.

Suggested behavior

Add a config option such as:

[otel]
log_agent_responses = true

or an environment/config equivalent.

When enabled, Codex should emit a log event for the final assistant message, for example:

  • event.name = "codex.agent_response"
  • conversation.id
  • turn.id if available
  • response_length
  • response = "<final assistant message>"

A documented truncation limit would be acceptable.

Additional information

This feature is available in Claude Code's OTel implementation

View original on GitHub ↗

4 Comments

michelsciortino · 3 months ago

Any update on this?

subomi · 2 months ago

This would be very useful.

aleksandr-vin · 2 months ago

I would add opt-in full telemetry spans support, to point it for ex. to local/personal mlflow genai instance, check https://mlflow.org/docs/latest/genai/eval-monitor/

Claude Code has this in beta: https://code.claude.com/docs/en/monitoring-usage#traces-beta

bilby91 · 10 days ago

We hit this from the observability side and have measurements plus a working implementation, so sharing the analysis here per docs/contributing.md rather than opening a PR.

Why we need it

We host Codex as one of the agents inside a workspace product and render each agent's trajectory in our own UI — tool calls, results, messages — from telemetry the agent reports to an in-pod OTLP receiver. Every row we need is available except the agent's message, which means a Codex trajectory renders as tool steps with nothing between them: the turn's actual answer never arrives.

What we measured (codex-cli 0.146.0)

Captured OTLP/HTTP logs to a local sink from both entry points — codex exec and the interactive TUI (originator=codex-tui) — with log_user_prompt = true:

  • Across several hundred log records, the assistant's message text appears in no attribute. We ran a marker-word test (asking for a replybeginning with a distinctive word) and grepped every exported attribute value: the marker appears only in codex.user_prompt.prompt and in codex.tool_result.arguments, never as agent output.
  • codex.sse_event fires once per stream event (221 of 224 records in a two-tool turn are event.kind = message carrying only duration_ms); the three response.completed records carry token counts. No content on any of them.
  • Both entry points emit an identical event set, so this is not an exec-only gap.
  • The same trajectory is fully present in the rollout JSONL (event_msg/agent_message), but reading that requires filesystem access to $CODEX_HOME and its schema stability is an open question (#20952). Telemetry is the supported push interface, which is why we would rather consume it there.

Two adjacent observations, in case they are useful:

  • Our configured [otel] trace_exporter produced no exports at all in this environment — tried protocol = "json" and "binary", and an endpoint both with and without the /v1/traces path — while log records from the same process arrived normally. #12913's table lists traces as working for both entry points, so we may be misconfiguring something; happy to open a separate issue with a repro if that is not a known gap.
  • There is a shape asymmetry worth noting alongside #17909: tool payloads (arguments, output) are logged in full by default, while the agent's own message cannot be logged at all. An opt-in flag for the message would make the two consistent.

Proposed shape

Mirrors the existing log_user_prompt plumbing, with no new concepts:

[otel]
log_agent_message = true   # default false; text is "[REDACTED]" otherwise
  • OtelConfigToml.log_agent_message: Option<bool> → OtelConfig.log_agent_message: bool (resolved in core/src/config/otel.rs).
  • A log_agent_messages flag on SessionTelemetryMetadata, set through a with_log_agent_messages(...) builder rather than a new SessionTelemetry::new parameter — new has 20 call sites (2 production, 17 tests, 1 in the README), and a builder leaves all of them untouched.
  • A codex.agent_message event modelled directly on user_prompt: message_length always, the text gated by the flag. The trace event carries the length only, never the text, matching how user_prompt splits log vs trace.
  • One call site: Session::emit_turn_item_completed in core/src/session/mod.rs, matching on TurnItem::AgentMessage. That is the single funnel every completed turn item passes through — plan mode reaches it via emit_agent_message_in_plan_mode — and it sits next to the existingrecord_turn_ttfm_metric(turn_context, &item) call, so no new plumbing is needed to reach turn_context.session_telemetry.

One trap worth knowing before anyone implements this

Naming the payload attribute message does not work, and it fails silently. tracing reserves that field name, so the appender routes the value to the LogRecord body instead of an attribute. Attribute-only consumers — including our receiver, and, we suspect, most collector pipelines people point at Codex — see the event arrive with no text on it. Straight out of the test that caught it:

RECORD BODY: Some(String(Owned("Kaleidoscope: notes.txt has three lines.")))
ATTRIBUTE KEYS: [app.version, auth_mode, conversation.id, event.name,
                 event.timestamp, message_length, model, originator, slug,
                 terminal.type, user.account_id, user.email]

Naming the attribute text fixes it, and matches the rollout's own vocabulary (agent_reasoning.text).

Status

We have this implemented against rust-v0.146.0 — 6 files, +53/−1, plus two tests added to otel/tests/suite/otel_export_routing_policy.rs using the existing InMemoryLogExporter/InMemorySpanExporter harness: one asserting the log record carries the text and message_length when opted in, one asserting [REDACTED] by default, both asserting the payload is an attribute rather than the record body. cargo check -p codex-config -p codex-core is clean and the full codex-otel suite passes (37 + 13 tests).

Not opening a PR since contributions are invitation-only. If the team would like this upstream, we are glad to submit it against main with just write-config-schema regenerated, just fmt / just fix clean, and an integration test under core/suite using test_codex to pin that the event fires exactly once per assistant message. Equally happy to leave the design and the trap documented here if you would rather implement it yourselves.