Skip to content

feat: attach pi turns to an external trace via LANGFUSE_PI_TRACEPARENT - #46

Merged
hassiebp merged 1 commit into
mainfrom
feat/inbound-traceparent
Sep 25, 2026
Merged

hassiebp merged 1 commit into
mainfrom
feat/inbound-traceparent

Conversation

@milanagm

@milanagm milanagm commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

LFE-16707

An application that already traces its own run can launch pi as one step, but pi opens an unrelated trace per prompt, so the application's work and pi's turns never meet.

pi was the only one of the four coding-agent plugins where a launcher could not pass a W3C traceparent — it accepted the trace-id/span-id pair only, which exists mainly because pi sets it on itself when it spawns a pi subagent.

The change

LANGFUSE_PI_TRACEPARENT, with the same contract as CC_LANGFUSE_TRACEPARENT in the Claude Code plugin:

with langfuse.start_as_current_span(name="Pi run") as run_span:
    traceparent = f"00-{run_span.trace_id}-{run_span.id}-01"
    subprocess.run(
        ["pi", "-p", "Refactor utils.py"],
        env={**os.environ, "LANGFUSE_PI_TRACEPARENT": traceparent},
    )

The traceparent wins over the id pair; a malformed one falls back to the pair rather than disabling nesting, since a typo in a launcher variable must not detach a subagent from its parent turn. All-zero ids are rejected, because that is what INVALID_SPAN_CONTEXT serialises to when a launcher has no active span. The flags field is read and discarded. A bare TRACEPARENT is deliberately not read: pi runs as a child of whatever launched it, and an OpenTelemetry-instrumented parent exports that variable into its subprocesses.

parseTraceparent is not pinned to version 00. The spec's versioning rule says a receiver that meets a higher version should read the first three fields and ignore the rest; only ff is refused, and trailing fields remain an error at version 00. This mirrors OTel's own parseTraceParent and is a deliberate difference from the Claude Code plugin and Codex PR #100, which both hardcode ^00-. Worth pulling those two along.

Verification

Against a real Anthropic provider (claude-haiku-4-5) and a real Langfuse project, not a mock:

  • Ten-arm edge-case matrix, 10/10: standalone, attached, malformed → pair fallback, malformed → own trace, all-zero rejected, version 01 accepted, version ff rejected, trailing fields at 00 rejected, bare TRACEPARENT ignored, subagent.
  • Three turns of one attached session: all three nest under the application span, which is the part a single-prompt run does not cover.
  • Attribute-level check against a local OTLP sink, because the cloud read path only exposes aggregated trace fields and this defect is invisible there: no span from either process carries langfuse.trace.name, session.id, langfuse.trace.tags or user.id in attached mode, while the standalone arm still carries all three. Mutation-checked — without the ownership propagation the probe reports LLM Call trägt session.id, langfuse.trace.tags, user.id auf einem fremden Trace.
  • The application keeps name, session, user and tags, and totalCost still aggregates on its trace: suppressing the labels costs no cost aggregation, because that rolls up through the observation tree.

143/143 tests, tsc --noUnusedLocals --noUnusedParameters clean.

@milanagm
milanagm requested a review from hassiebp September 24, 2026 11:38
@hassiebp
hassiebp merged commit c02b69d into main Sep 25, 2026
11 of 13 checks passed
@hassiebp
hassiebp deleted the feat/inbound-traceparent branch September 25, 2026 08:37
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