ADR-0039: The Langfuse adapter¶
Status: Accepted; its score adapters superseded by ADR-0049 Date: 2026-09-28 Deciders: Alex Nodeland
Context¶
ADR-0027 makes Langfuse the primary observability backend, through a [langfuse] extra with a span filter, a context helper, a feedback mirror and score configs. ADR-0038 puts the mirror and score configs behind two ports and gives the Runner a TurnContext port. RFC-0002 names what each piece does but leaves some details open:
- which scopes the span filter keeps
- how the context helper reaches a turn
- which tags it sets
- how
configure_telemetry(langfuse=True)wires the client - what happens when a score config's name is one Langfuse refuses
Decision¶
artifactr.langfuseis an adapter (ADR-0034):LangfuseScoresimplementsScoreSinkwithcreate_score,LangfuseScoreConfigsimplementsScoreConfigStorewith the score-config API, andlangfuse_turnimplements theRunner'sTurnContext.- The span filter keeps Langfuse's default (LLM spans, Langfuse's own) and these instrumentation scopes, with their sub-scopes:
artifactrpydantic-graph- the MCP SDK's
mcp-python-sdk, without which an external agent's commits have no root - FastAPI and ASGI
- SQLAlchemy, asyncpg and httpx
-
The turn's attributes are propagated in process, never as baggage, with
propagate_attributes:- the thread as the session, and the person as the user
- the trace name
turn - tags
tenant:…,workspace:…andkind:…for each kind of artifact the thread follows - metadata for the tenant, workspace, thread, run and trigger
Values are made ASCII and cut to 200 characters. Langfuse's baggage option would put them on outgoing HTTP requests.
configure_telemetry(langfuse=True)puts Langfuse on the same tracer provider, as a second span processor with the filter, so the Collector and Langfuse see the same spans, and shuts the client down with the rest.langfuse_client(...)does the same for applications that configure the SDK themselves.- Score config names Langfuse refuses raise. Langfuse accepts 35 characters from a small alphabet; a longer
{type}.{field}raises with the fix (a shortername=), because renaming it silently would break the link between scores and their config.
Options considered¶
| Option | Traces whole in Langfuse | Values leave the process |
|---|---|---|
| Filter plus propagated attributes on the shared provider (chosen) | Yes | No |
| Langfuse's default filter | No: only model and tool spans | No |
propagate_attributes(as_baggage=True) |
Yes | Yes, as HTTP headers on every outgoing request |
| A separate tracer provider for Langfuse | Yes | No, but the Collector and Langfuse see different spans |
Consequences¶
- Easier: a turn in Langfuse shows its commits, queries and HTTP calls, filed under its session, user and tenant.
- Harder: Langfuse's filter scopes are a list to keep current as instrumentations are added.
Action items¶
-
artifactr.langfuse, with contract tests against the port fakes.