Skip to content

Emit trace.input/output via Langfuse SDK adapter#100

Merged
chris-colinsky merged 2 commits into
mainfrom
feature/0043-sdk-adapter-trace-io
May 31, 2026
Merged

Emit trace.input/output via Langfuse SDK adapter#100
chris-colinsky merged 2 commits into
mainfrom
feature/0043-sdk-adapter-trace-io

Conversation

@chris-colinsky

Copy link
Copy Markdown
Member

Summary

  • Closes the SDK-adapter gap from PR Implement proposal 0043 (trace input/output) #99 (proposal 0043). The Langfuse observer's three-lever decision tree was shipping correctly via the InMemoryLangfuseClient, but the production LangfuseSDKAdapter's update_trace(input=..., output=...) was a no-op. Operators saw blank Input / Output columns in the Langfuse Traces list view despite the observer emitting values.
  • Input path: update_trace(input=...) caches pending_input in _trace_info; the next _start_observation for that trace pops the cache and applies obs.set_trace_io(input=cached) on the just-created observation. Piggybacks on a real span; no extra observation in the common case.
  • Output path: update_trace(output=...) opens a synthetic short-lived openarmature.trace_io observation as the carrier for set_trace_io(output=...). By the time the InvocationCompletedEvent reaches the observer, all real node spans have ended — a synthetic span is the only path with an active OTel span context.
  • Edge case: invocation that fails before any node fires has no real span. The synthetic output observation also applies the cached pending_input, so both fields still land.

Deprecation note

The v4 Langfuse SDK marks set_trace_io deprecated ("removal in a future major version"). Empirical verification against Langfuse Cloud v4.7.1 (2026-05-29) confirms it remains the only path that surfaces trace.input / trace.output on the Traces list view headline columns; propagate_attributes(metadata=...) writes values into the metadata bag but the UI doesn't project them as headline columns from there. Documented in CHANGELOG. A coord thread (discuss-langfuse-trace-io-deprecation) will be filed after merge to flag the deprecation back to spec for the long-term direction.

Test plan

  • uv run pytest tests/ --ignore=tests/integration -q — 993 passed, 203 skipped, 0 failed
  • uv run pyright src/openarmature — 0 errors
  • uv run ruff check src/ tests/ examples/ — clean
  • Live Langfuse Cloud integration tests (tests/integration/test_langfuse_sdk_adapter.py) — both pass against the test account. Two cases: real-obs + synthetic-output path, synthetic-only path. Gated by LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY so CI skips when no creds.
  • Trace dump from the live tests confirms trace.input and trace.output populate as expected

PR #99 (proposal 0043) shipped the Langfuse observer's three-lever
decision tree but left the SDK adapter's `update_trace(input=...,
output=...)` as a no-op — only the InMemoryLangfuseClient applied
the values. Production users of `LangfuseSDKAdapter` saw blank
`Input` / `Output` columns in the Langfuse Traces list view despite
the observer emitting the values.

Wire the adapter to apply both via the v4 SDK's `set_trace_io`:

- `update_trace(input=...)` caches `pending_input` in `_trace_info`.
  The next `_start_observation` for that trace pops the cache and
  calls `obs.set_trace_io(input=cached)` on the just-created
  observation. Piggybacks on a real span; no extra observations
  added in the common case.
- `update_trace(output=...)` opens a synthetic short-lived
  `openarmature.trace_io` observation as the carrier for
  `set_trace_io(output=...)`. By the time the
  `InvocationCompletedEvent` reaches the observer all real node
  spans have ended, so a synthetic span is the only path with an
  active OTel span context.
- Edge case: an invocation that fails before any node fires has no
  real span. The synthetic output observation also applies the
  cached pending_input, so both fields still land.

The Langfuse v4 SDK marks `set_trace_io` deprecated ("removal in a
future major version"). Empirical verification against Langfuse
Cloud v4.7.1 confirms it remains the only path that surfaces
`trace.input` / `trace.output` on the Traces list view headline
columns; `propagate_attributes(metadata=...)` writes the values
into the metadata bag but the UI does not project them as headline
columns from there. Documented in CHANGELOG; will revisit when
Langfuse publishes a v5 migration path.

Adds two integration tests (`tests/integration/`) gated by
`LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY`. Both pass against
Langfuse Cloud end-to-end (real-obs + synthetic-only paths).
Copilot AI review requested due to automatic review settings May 30, 2026 14:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fills the SDK-adapter side of proposal 0043 so LangfuseSDKAdapter.update_trace(input=..., output=...) actually populates the Input/Output headline columns on live Langfuse Traces (previously a no-op while InMemoryLangfuseClient worked correctly). Input is staged on the per-trace cache and applied to the next real observation; output is delivered via a synthetic short-lived openarmature.trace_io span (the only carrier with an active OTel context after node spans have ended). Adds opt-in live-Cloud integration tests and a CHANGELOG note covering Langfuse v4's set_trace_io deprecation.

Changes:

  • Rework update_trace to stage pending_input and emit output via a synthetic span helper _emit_trace_output_synthetic.
  • Apply cached pending_input to the first real observation inside _start_observation via set_trace_io.
  • Add tests/integration/test_langfuse_sdk_adapter.py covering the real-span and synthetic-only paths against Langfuse Cloud.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/openarmature/observability/langfuse/adapter.py Implements the input piggyback + synthetic-output carrier paths and updates comments.
tests/integration/test_langfuse_sdk_adapter.py New live Langfuse Cloud tests for both emission paths.
tests/integration/init.py Adds the integration test package marker.
CHANGELOG.md Documents the SDK-adapter emission behaviour and Langfuse set_trace_io deprecation note.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/integration/test_langfuse_sdk_adapter.py
Comment thread tests/integration/test_langfuse_sdk_adapter.py
PR #100 review caught a gap: the integration tests gated only on
env-var presence are still picked up by `pytest tests/` when a
developer has `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` in
scope locally. The default `pyproject.toml` config excludes
`@pytest.mark.integration` via `addopts = ["-m", "not integration"]`
but not unmarked tests in a separate directory.

Add the marker to both tests so they match the existing precedent
at `tests/unit/test_observability_langfuse_adapter.py:177` and stay
out of the default test run regardless of credential availability.
@chris-colinsky chris-colinsky merged commit b2d22b1 into main May 31, 2026
6 checks passed
@chris-colinsky chris-colinsky deleted the feature/0043-sdk-adapter-trace-io branch May 31, 2026 00:45
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.

2 participants