Skip to content

LAM-2168: record OpenAI Agents span errors as exception events - #328

Open
laminar-coding-agent[bot] wants to merge 1 commit into
mainfrom
fix/lam-2168-agents-error-status
Open

LAM-2168: record OpenAI Agents span errors as exception events#328
laminar-coding-agent[bot] wants to merge 1 commit into
mainfrom
fix/lam-2168-agents-error-status

Conversation

@laminar-coding-agent

@laminar-coding-agent laminar-coding-agent Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Problem

Errors on OpenAI Agents SDK spans never surfaced in Laminar. A tool call that raised was recorded as status=success.

apply_span_error reported failure with set_status(Status(StatusCode.ERROR, ...)) — but nothing downstream reads that:

  • app-server/src/traces/spans.rs::from_otel_span never reads otel_span.status; the OTLP status field is discarded at ingestion.
  • app-server/src/traces/utils.rs::prepare_span_for_recording sets status = "error" only when the span carries an event named exception.

So set_status(ERROR) is silently a no-op end-to-end. Verified with a minimal two-way probe against staging, no Agents SDK involved:

SDK call Laminar spans.status
span.set_status(Status(StatusCode.ERROR, "…")) success
span.record_exception(RuntimeError("…")) error

Found while instrumenting the Toolathlon benchmark harness (LAM-2168). The Agents SDK populated the error correctly — {'message': 'Error running tool (non-fatal)', 'data': {'tool_name': 'lookup_paper', 'error': '…'}} — and Laminar still showed the tool span green.

Fix

apply_span_error now emits an exception event alongside the status. The trace view's error card (frontend/components/traces/error-card.tsx) reads exception.type as the headline and exception.message as the body, so both are set:

  • exception.type <- the SpanError label ("Error running tool (non-fatal)", "Max turns exceeded")
  • exception.message <- data as JSON, falling back to the label when there is no data

Second bug fixed in passing: SpanError is a TypedDict, so it is a plain dict at runtime and getattr(error, "message", None) always returned None — the status description fell back to str(error), i.e. the raw dict repr. Both the dict and object shapes are now handled.

Scope

This is a general app-server contract, not an Agents-specific quirk — any instrumentor or user calling set_status(ERROR) gets no error status. Fixing it properly means honoring otel_span.status.code in from_otel_span; that is a separate app-server PR. This one unblocks the Agents path without waiting on it.

Tests

New tests/test_instrumentations/test_openai_agents/test_span_error.py — 3 cases (dict-shaped SpanError, object-shaped, no error), asserting against real exported spans via the span_exporter fixture.

$ uv run pytest tests/test_instrumentations/test_openai_agents/ -q
10 passed, 5 warnings in 4.33s

🤖 Generated with Claude Code


Note

Low Risk
Localized change to OpenAI Agents error reporting plus docs and unit tests; no auth, data migration, or broad API surface impact.

Overview
OpenAI Agents SDK failures (e.g. non-fatal tool errors) were exported with Laminar success status because ingestion ignores OTel set_status(ERROR) and only marks spans error when an exception event is present.

apply_span_error now reads SpanError as either a runtime dict (TypedDict) or an object, sets OTel error status as before, and adds an exception event with exception.type (short label) and exception.message (JSON data or the label) so the trace error card and app-server agree.

CLAUDE.md documents this Laminar-wide contract for the OpenAI Agents instrumentor. New tests in test_span_error.py assert exported spans for dict/object errors and no event when there is no error.

Reviewed by Cursor Bugbot for commit a7face9. Bugbot is set up for automated code reviews on this repo. Configure here.

Agent SDK span errors were set via `set_status(StatusCode.ERROR)` only, which
Laminar never renders: app-server's `from_otel_span` does not read
`otel_span.status`, and `prepare_span_for_recording` derives `status = "error"
solely from the presence of an event named `exception`. A tool call that
raised was therefore recorded as `status=success`.

`apply_span_error` now also emits an `exception` event with the
`exception.type` / `exception.message` attributes the trace view's error card
renders.

Also fixes the field extraction: `SpanError` is a TypedDict, so
`getattr(error, "message")` always returned None and the message fell back to
`str(error)` (the whole dict repr). Both the dict and object shapes are now
handled, with `data` carrying the specifics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant