Skip to content

telemetry: per-call and per-run correlation IDs via donkey.run() #195

Closed

Description

@amirkhan-ak-sf

Summary

Piece 6 of 6, first half. Every request gets a per-call ID and, optionally, a
per-run ID the developer sets once.

async with donkey.run(id=ticket.id, enduser_id=user.id):
    await triage_agent.run(ticket)

Both are injected as headers, attached to every exception, every span, and
logged — so client-side and gateway-side records join.

Why it matters — Scenario C

Compliance asks "prove the HR bot's answer to user X on date Y went through
content-safety." The correlation ID on the Slack log line joins to the gateway
record. That is what an EU AI Act Article 12 log request looks like in practice.

Acceptance criteria

  • DonkeyError.correlation_id always equals the header that was sent.
  • The per-run ID is attached to every span emitted inside the block.
  • Nested donkey.run() blocks behave predictably and are documented.
  • Works without OTel installed — correlation is not gated on telemetry extras.

Propagation through graph nodes

A per-run ID the developer has to thread into every node is a per-run ID that
gets dropped. Twelve LangGraph nodes call the model across one ticket, and the
audit story only works if all twelve carry the same ID.

  • A run ID set at donkey.run() reaches every LangGraph node via
    contextvar, without the developer threading it through state.
  • Concurrent runs in the same process do not leak IDs into each other.
  • Propagation survives asyncio task boundaries created by the framework.
  • The conformance suite (1.5) includes "propagates correlation ID".

Cross-surface impact

  • core/telemetry + core/errors + area:adapters: the change.
  • Known limitation to record: frameworks where LiteLLM owns the transport
    cannot propagate a per-run ID; assert the exemption rather than skipping.
  • Verification: the header name the gateway reads is unverified — use the
    Unverified(...) placeholder with a config override.

Out of scope

Cost-attribution tags.

Activity

  1. added
    enhancementNew feature or request
    size/m1-2 days, multiple files
    area:telemetryOTel GenAI spans, correlation IDs, cost tags (1.6, 1.7)
    six-piece-minimumPart of the minimum set that justifies installing the SDK
    priority/p0Critical: blocks the phase
    on Sep 4, 2026
  2. self-assigned this
    on Sep 10, 2026
  3. tommasobolis commented on Sep 10, 2026

    @tommasobolis
    Collaborator

    Agreed design (brainstormed 2026-09-10)

    Two decisions settled before implementation:

    1. Two ids, two headers (matches the issue title "per-call and per-run").
    2. enduser_id deferred to telemetry: validated cost-attribution tags (team, project, env, enduser.id) #196 — this issue ships fabric.run(id=…) only.
      fabric.run() is designed so telemetry: validated cost-attribution tags (team, project, env, enduser.id) #196 can add enduser_id/team/project/env
      without a breaking change.

    The two ids

    Id Scope Header (name UNVERIFIED — see below) Lands on
    run id set once via fabric.run(id=…), shared by every request in the block; falls back to the auto-bound context id (a "run of one") outside a block X-Correlation-Id FabricError.correlation_id, span fabric.correlation_id, log line — the client↔gateway join key
    call id fresh per logical request, always unique, stable across retries X-Fabric-Request-Id new FabricError.call_id, log line — pinpoints one request within a run; exists even on a transport error with no response

    FabricError.correlation_id stays = the run/correlation header (preserving
    today's semantics + the verified join), so "correlation_id always equals the
    header that was sent"
    holds — that header is always emitted.

    Public API

    fabric.run(id: str | None = None) — a dual sync/async context manager
    (both with and async with), binding the run id to the existing
    telemetry._correlation_id contextvar. fabric.run_context() becomes a thin
    back-compat alias. Nested blocks rebind and restore on exit (contextvar token).

    Verification (§0.3)

    docs/verified-apis.md records x-correlation-id as VERIFIED (LIVE) — a
    response echo
    ; whether the gateway reads an inbound correlation header is
    UNVERIFIED. So both request-header names move into core/_verify.py as
    Unverified(...) placeholders (verified=False), with a genuine config
    override: two new FabricConfig fields (correlation_header,
    call_id_header; env AGENT_FABRIC_CORRELATION_HEADER /
    AGENT_FABRIC_CALL_ID_HEADER). The transport resolves each name once and uses
    it both to inject and to read back in classify().

    Propagation + conformance

    Every LangGraph node calls the model through the one shared FabricAsyncClient
    (http_async_client), so the contextvar-bound run id already reaches every
    node, and contextvars copy across asyncio task boundaries. Fill the
    existing correlation_id_propagated conformance scenario body (one run id
    across N node calls; no cross-run leakage); keep the adk/crewai LiteLLM
    exemption asserted, not skipped.

    Works without OTel

    Correlation is pure contextvar + headers; spans only decorate when OTel is
    installed. Covered by a base-only test.

    File-by-file

    • core/_verify.py — two Unverified header placeholders.
    • core/config.py — correlation_header / call_id_header override fields.
    • core/telemetry.py — call-id helper alongside the run-id contextvar.
    • core/transport.py — inject both headers (call id once, before the retry
      loop); resolve names from config→_verify; read both back.
    • core/errors.py — FabricError.call_id; classify(..., call_id=…).
    • fabric.py — fabric.run(id=…) dual CM; keep run_context alias.
    • tests/conformance/suite.py (+ a LangGraph propagation test) — fill the
      scenario; keep the exemption.
    • Docs: observability.mdx, errors.mdx, a short correlation concept,
      docs/verified-apis.md (correlation request-header row = UNVERIFIED with
      the response-echo note), README ergonomics, CHANGELOG.md.
    • TS parity (Phase 5): the TS port mirrors fabric.run() + the two headers
      — recorded here, no TS code yet.

    Out of scope

    Cost-attribution tags (enduser_id, team/project/env) — #196.

  4. added
    area:refusalsclassify(), the typed error taxonomy, handlers (1.2, 2.1, 2.2)
    area:adaptersFramework adapters and connection_kwargs() (1.8, 2.8)
    on Sep 10, 2026
  5. added a commit that references this issue on Sep 10, 2026
    ca5dfaa
  6. tommasobolis commented on Sep 10, 2026

    @tommasobolis
    Collaborator

    Fixed by #326 (merged as ca5dfaa).

  7. tommasobolis commented on Sep 10, 2026

    @tommasobolis
    Collaborator

    Cross-surface lockstep — TypeScript parity (Phase 5, BG §3.5).

    Recording the obligation for when the TS port ships (no TS code exists yet, so nothing to change now). The TypeScript SDK must reach parity with what #326 landed on the Python side:

    • fabric.run(id=…) as a run-scoping construct (the TS-idiomatic equivalent of the dual sync/async context manager).
    • The two-id model: a shared run id → X-Correlation-Id request header (→ span fabric.correlation_id), and a fresh per-request call id → X-Fabric-Request-Id, stable across a request's retries.
    • FabricError.correlation_id and FabricError.call_id, with classify()/its TS equivalent deriving both client-sent ids from the response's request.
    • The same UNVERIFIED-placeholder discipline for both header names (§0.3), overridable via config/env.
    • Propagation that survives async boundaries without threading through state, and does not leak across concurrent runs.

    Fold this into the Phase-5 TS work item when it's cut.

  8. changed the title [-]telemetry: per-call and per-run correlation IDs via fabric.run()[/-] [+]telemetry: per-call and per-run correlation IDs via donkey.run()[/+] on Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:adaptersFramework adapters and connection_kwargs() (1.8, 2.8)area:refusalsclassify(), the typed error taxonomy, handlers (1.2, 2.1, 2.2)area:telemetryOTel GenAI spans, correlation IDs, cost tags (1.6, 1.7)enhancementNew feature or requestpriority/p0Critical: blocks the phasesix-piece-minimumPart of the minimum set that justifies installing the SDKsize/m1-2 days, multiple files

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions