pi-langfuse 1.5.15 → 1.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/langfuse.ts +32 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-langfuse",
3
- "version": "1.5.15",
3
+ "version": "1.5.16",
4
4
  "description": "Langfuse extension for Pi coding agent",
5
5
  "repository": {
6
6
  "type": "git",
package/src/langfuse.ts CHANGED
@@ -458,6 +458,32 @@ function observationType(asType?: string): FallbackObservationType {
458
458
  return asType === "generation" ? "GENERATION" : "SPAN";
459
459
  }
460
460
 
461
+ /**
462
+ * Langfuse stamps propagated attributes onto a span in
463
+ * `LangfuseSpanProcessor.onStart(span, parentContext)`, reading them from the
464
+ * OTel context that was active when the span was created. `propagateAttributes`
465
+ * only seeds that context for the duration of its callback, so a child created
466
+ * later — from a separate event handler, outside the callback — is written with
467
+ * an empty `session.id`.
468
+ *
469
+ * That is invisible in the legacy data model, where the session lives on the
470
+ * trace, but Langfuse v4 stores `session_id` per event row and aggregates a
471
+ * session with `WHERE session_id != ''`. Unstamped children drop out of
472
+ * `sumMap(cost_details)` and `sumMap(usage_details)`, which is why such
473
+ * sessions report their trace count correctly but no cost and no usage at all.
474
+ *
475
+ * Re-entering the propagated context for every child keeps the whole tree in
476
+ * the session. The id is read back off the parent span rather than from
477
+ * `state.currentSessionId`, so a child created outside an active session scope
478
+ * still inherits whatever its parent was actually stamped with.
479
+ */
480
+ const OTEL_SESSION_ID_ATTRIBUTE = "session.id";
481
+
482
+ function readSessionId(observation: any): string | undefined {
483
+ const value = observation?.otelSpan?.attributes?.[OTEL_SESSION_ID_ATTRIBUTE];
484
+ return typeof value === "string" && value ? value : undefined;
485
+ }
486
+
461
487
  function wrapObservation(
462
488
  observation: any,
463
489
  store: RestFallbackStore,
@@ -517,7 +543,12 @@ function wrapObservation(
517
543
  return observation.end();
518
544
  },
519
545
  startObservation(childName: string, childBody?: Record<string, unknown>, options?: { asType?: string }) {
520
- const child = observation.startObservation(childName, childBody, options);
546
+ const inheritedSessionId = readSessionId(observation) ?? state.currentSessionId ?? undefined;
547
+ const propagate = runtime?.propagateAttributes;
548
+ const createChild = () => observation.startObservation(childName, childBody, options);
549
+ const child = inheritedSessionId && propagate
550
+ ? propagate({ sessionId: inheritedSessionId.slice(0, 200) }, createChild)
551
+ : createChild();
521
552
  return wrapObservation(child, store, childName, childBody, options?.asType, id);
522
553
  },
523
554
  setTraceIO(traceBody?: { input?: unknown; output?: unknown }) {