Skip to content

Commit bc9271e

Browse files
Document 0064: concepts, changelog, example
Document the Langfuse trace.userId / trace.sessionId population in the observability concepts page, add the 0.15.0 changelog entry (folding 0064 into the cycle's spec-pin bullet), and demonstrate the userId promotion in the langfuse-observability example (a per-operator userId that surfaces in the captured trace). sessionId is left out of the example since it stays dormant until the sessions capability lands.
1 parent 48db64c commit bc9271e

3 files changed

Lines changed: 29 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The
1010

1111
- **Detached-trace invocation span** (proposal 0061, observability §4.4, spec v0.61.0). The OTel observer now synthesizes an `openarmature.invocation` span at the root of each detached trace (a detached subgraph and each detached fan-out instance), carrying the parent's shared `invocation_id` (detached mode is observer-side trace rendering, not a new run) and the detached unit's own `entry_node`; the detached subgraph / instance span nests under it. A raising detached subgraph surfaces ERROR plus the error category and an OTel exception event on both the parent dispatch span and the detached invocation span. This is observer-side only, with no graph-engine change; the Langfuse observer is unchanged (its Trace entity already plays the invocation-level-container role). Conformance fixtures 008 (rewritten) and 058 (newly wired) run in `test_observability`.
1212
- **Per-attempt LLM spans under call-level retry** (proposal 0050, observability §5.5 / llm-provider §7.1). Completes proposal 0050, which shipped `partial` in v0.14.0 (failure-isolation middleware and the `complete(retry=...)` loop landed then; the per-attempt span surface was deferred). Under call-level retry the OTel observer now emits one `openarmature.llm.complete` span per attempt, each carrying `openarmature.llm.attempt_index` (0-based, 0..N-1, and 0 for a no-retry call). An intermediate failed attempt's span carries ERROR status plus its error category and the request-side attributes; the final attempt's span carries the terminal outcome and, on success, the full response surface. A python-internal `LlmRetryAttemptEvent`, dispatched once per attempt, is the sole source of the OTel span; the terminal `LlmCompletionEvent` / `LlmFailedEvent` stay one per call (payload, latency, Langfuse Generation) and no longer drive the OTel span. Langfuse renders one terminal Generation per call, with the per-attempt detail on the OTel span surface only (a spec-side §8 clarification to pin this is tracked, non-blocking). `conformance.toml` flips proposal 0050 to `implemented`; the call-level fixtures 056-058 are driven through the provider plus OTel observer and the single-attempt observability fixture 057 is wired.
13+
- **Langfuse `trace.userId` / `trace.sessionId` population** (proposal 0064, observability §8.4.1, spec v0.62.0). The Langfuse observer now promotes a recognized `userId` key in the caller-supplied invocation metadata to Langfuse's first-class `trace.userId` field (the Users dashboard), additively: the key also remains at `trace.metadata.userId`. Promotion is automatic and unconditional; an absent key leaves `trace.userId` unset. The `LangfuseClient.trace()` surface (the Protocol, the in-memory client, and the SDK adapter) gains `session_id` / `user_id`. `trace.sessionId` is sourced from `openarmature.session_id`, which the sessions capability (proposal 0020) establishes; that capability is not yet implemented in python, so the `sessionId` plumbing is in place but dormant (no source) and unset in the interim. `conformance.toml` records proposal 0064 `partial` on that basis: fixture 084 cases 2/3/4 (not session-bound, `userId` present additively, `userId` absent) run, and the session-bound cases 1/5 defer until 0020. Langfuse-only: the OTel side already carries `openarmature.session_id` and `openarmature.user.*` as span attributes, and OTel has no trace-level session/user field.
1314

1415
### Changed
1516

16-
- **Pinned spec advances v0.60.0 → v0.61.0** (proposal 0061, the detached-trace invocation span above). A single step this cycle; `conformance.toml` records proposal 0061 as `implemented`. Proposal 0050 needed no pin bump of its own (it was already within the pin from its v0.42.0 acceptance); its v0.14.0 `partial` entry flips to `implemented` with the per-attempt span surface above.
17+
- **Pinned spec advances v0.60.0 → v0.62.0** across the v0.15.0 cycle: v0.61.0 (proposal 0061, the detached-trace invocation span above) and v0.62.0 (proposal 0064, the Langfuse session/user population above). `conformance.toml` records 0061 `implemented` and 0064 `partial` (its `sessionId` half is dormant pending the sessions capability). Proposal 0050 needed no pin bump of its own (it was already within the pin from its v0.42.0 acceptance); its v0.14.0 `partial` entry flips to `implemented` with the per-attempt span surface above.
1718

1819
## [0.14.0] — 2026-06-17
1920

docs/concepts/observability.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,18 @@ for a runnable demo.
10481048
- **Trace name.** Defaults to the entry-node name (spec §8.6
10491049
fallback). Caller-supplied invocation labels land in PR 4
10501050
(proposal 0034).
1051+
- **Session / user grouping (`trace.sessionId` / `trace.userId`).**
1052+
The observer populates the two cross-trace grouping fields behind
1053+
Langfuse's Sessions and Users dashboards (spec §8.4.1, proposal
1054+
0064). `trace.userId` is promoted from a recognized `userId` key in
1055+
the caller-supplied invocation metadata, automatically and
1056+
additively (the key also stays at `trace.metadata.userId`); an
1057+
absent key leaves it unset. `trace.sessionId` is sourced from
1058+
`openarmature.session_id` (the sessions capability), which is not
1059+
yet implemented, so it is unset for now. There is no OTel
1060+
equivalent (an OTel trace has no trace-level session / user field);
1061+
the same identity already rides as `openarmature.session_id` and the
1062+
`openarmature.user.*` family on the OTel span side.
10511063
- **Per-observation metadata.** Each Span / Generation carries
10521064
`namespace`, `step`, `attempt_index`, optional `fan_out_index` /
10531065
`branch_name`, and the `correlation_id` cross-cutting join key

examples/langfuse-observability/main.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
observation picks that up and links back to the entity, which is how
1717
production Langfuse dashboards thread "this generation came from prompt
1818
v7 of `mission-briefing`" without you having to wire anything up
19-
manually.
19+
manually. It also tags each trace with a ``userId`` (operator identity)
20+
via invocation metadata; the observer promotes that to Langfuse's
21+
first-class user dimension, so the Users dashboard groups and filters
22+
the assistant's traffic by operator.
2023
2124
The example uses the bundled ``InMemoryLangfuseClient`` recorder so the
2225
demo runs without a Langfuse account; at the end we print the captured
@@ -193,6 +196,8 @@ def _format_trace(trace: LangfuseTrace) -> str:
193196
lines: list[str] = []
194197
lines.append(f"Trace id={trace.id}")
195198
lines.append(f" name={trace.name!r}")
199+
if trace.user_id is not None:
200+
lines.append(f" userId={trace.user_id!r} (promoted to the Langfuse Users dimension)")
196201
lines.append(f" metadata={_format_metadata(trace.metadata)}")
197202
for obs in trace.children_of(None):
198203
_format_observation(lines, trace, obs, indent=" ")
@@ -274,7 +279,15 @@ async def main() -> None:
274279
graph.attach_observer(observer)
275280

276281
try:
277-
final = await graph.invoke(BriefingState(question=question))
282+
# metadata={"userId": ...} tags the trace with an operator
283+
# identity. The Langfuse observer promotes a recognized ``userId``
284+
# key to the first-class trace.userId field so the Users dashboard
285+
# can group and filter traces by operator (additive: it also stays
286+
# in trace.metadata.userId).
287+
final = await graph.invoke(
288+
BriefingState(question=question),
289+
metadata={"userId": "flight-controller-gene"},
290+
)
278291
finally:
279292
# Required for short-lived processes: invoke() returns when the
280293
# graph reaches END regardless of whether the observer queue

0 commit comments

Comments
 (0)