Emit trace.input/output via Langfuse SDK adapter#100
Conversation
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).
There was a problem hiding this comment.
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_traceto stagepending_inputand emitoutputvia a synthetic span helper_emit_trace_output_synthetic. - Apply cached
pending_inputto the first real observation inside_start_observationviaset_trace_io. - Add
tests/integration/test_langfuse_sdk_adapter.pycovering 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.
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.
Summary
InMemoryLangfuseClient, but the productionLangfuseSDKAdapter'supdate_trace(input=..., output=...)was a no-op. Operators saw blankInput/Outputcolumns in the Langfuse Traces list view despite the observer emitting values.update_trace(input=...)cachespending_inputin_trace_info; the next_start_observationfor that trace pops the cache and appliesobs.set_trace_io(input=cached)on the just-created observation. Piggybacks on a real span; no extra observation in the common case.update_trace(output=...)opens a synthetic short-livedopenarmature.trace_ioobservation as the carrier forset_trace_io(output=...). By the time theInvocationCompletedEventreaches the observer, all real node spans have ended — a synthetic span is the only path with an active OTel span context.pending_input, so both fields still land.Deprecation note
The v4 Langfuse SDK marks
set_trace_iodeprecated ("removal in a future major version"). Empirical verification against Langfuse Cloud v4.7.1 (2026-05-29) confirms it remains the only path that surfacestrace.input/trace.outputon 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 faileduv run pyright src/openarmature— 0 errorsuv run ruff check src/ tests/ examples/— cleantests/integration/test_langfuse_sdk_adapter.py) — both pass against the test account. Two cases: real-obs + synthetic-output path, synthetic-only path. Gated byLANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEYso CI skips when no creds.trace.inputandtrace.outputpopulate as expected