Skip to content

Add LangGraph durable-execution agent lifecycle events - #536

Draft
meshailabs wants to merge 8 commits into
open-telemetry:mainfrom
meshailabs:feat/langgraph-lifecycle-events
Draft

Add LangGraph durable-execution agent lifecycle events#536
meshailabs wants to merge 8 commits into
open-telemetry:mainfrom
meshailabs:feat/langgraph-lifecycle-events

Conversation

@meshailabs

Copy link
Copy Markdown

Description

Emit gen_ai.agent.paused, gen_ai.agent.checkpointed, and gen_ai.agent.resumed from the LangChain instrumentation for LangGraph durable runs. These follow candidate OTel GenAI conventions (open-telemetry/semantic-conventions-genai#445), reset to draft pending real instrumentation evidence.

Hook LangGraph 1.2 on_interrupt/on_resume on the existing handler. LangGraph already dispatches them; today that path logs an AttributeError on every interrupt and resume. Report checkpoints by wrapping put/aput on the compiled graph's saver.

Known gaps: no gen_ai.agent.execution.id (LangGraph has no id that spans suspend/resume); pause.reason is not derivable from interrupt(value); resumed_from.type is always checkpoint; subgraph runs are chains, so child-namespace checkpoints land on the parent workflow span. emit_event in util-genai mirrors #507 and should drop on rebase if that lands first.

Type of change

  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)

How has this been tested?

tox latest and oldest matrices for the langchain instrumentation, the util-genai suite, typecheck, and pre-commit. Runnable example with a captured run under examples/langgraph-lifecycle/.

Checklist

  • Followed the style guidelines of this project
  • Changelog updated if the change requires an entry
  • Unit tests added
  • Documentation updated

Prototype producer for the candidate conventions in
open-telemetry/semantic-conventions-genai#445, captured from unmodified
LangGraph applications at two real interception points.

LangGraph 1.2 dispatches on_interrupt and on_resume to the handlers in a
run's callback manager, and the instrumentation's handler is already in
that list, so gen_ai.agent.paused and gen_ai.agent.resumed come straight
from GraphInterruptEvent and GraphResumeEvent with the interrupt ids and
checkpoint ids LangGraph produced. Adding these two methods also removes
the AttributeError LangGraph currently logs on every interrupt.

The checkpointer passed to StateGraph.compile is wrapped so every
persisted checkpoint emits gen_ai.agent.checkpointed, correlated to the
live run through the LangGraph thread id. BaseCheckpointSaver.put is
abstract and every saver overrides it, so the instance is patched rather
than the base class.

gen_ai.agent.execution.id and gen_ai.agent.pause.reason are omitted:
LangGraph reports nothing with those semantics. See
docs/design-notes/langgraph-lifecycle-events.md and the captured
telemetry sample beside it.

Claude-Session: https://claude.ai/code/session_01FAnWkWBL3mJ81AMv7KA6br
Review follow-up on the lifecycle events.

Correlation now tracks a stack of live runs per LangGraph thread id, so a
nested run never displaces the run containing it and the innermost live
run owns the checkpoint. Two unrelated live runs on one thread id are
ambiguous, so the event is dropped with a debug log rather than
attributed to a guess. One lock now guards both the thread stacks and the
invocation states, making a checkpoint lookup atomic against a concurrent
chain end. Thread bindings prune dead runs and are bounded, so runs that
never report an end cannot accumulate.

Checkpointer patching checks that a saver is hashable and weak
referenceable before touching it, so a saver that cannot be tracked is
never left partly patched. The instance attributes replaced by the patch
are saved and restored on uninstrument instead of deleted. Checkpoint
de-duplication now remembers the last id per namespace rather than
relying on context propagation, which covers savers whose aput delegates
to put on a worker thread. The saver is discovered from the compiled
graph rather than from the compile arguments.

GenAIInvocation.emit_event is byte-identical to the hunk in PR open-telemetry#507; the
design note and changelog now say so and that it is dropped when rebasing
onto that PR.

Claude-Session: https://claude.ai/code/session_01FAnWkWBL3mJ81AMv7KA6br
…identity

Second review follow-up.

Correlation now carries the LangGraph checkpoint namespace end to end. Each
graph run binds the namespace its own checkpoints are written under, which
is "" for a top level graph and the run's langgraph_checkpoint_ns for a
nested graph. A write resolves to the run bound with that exact namespace,
or to the nearest enclosing run when none is bound, and stays ambiguous
only when two equally specific live runs are unrelated. LangGraph
classifies a subgraph's own graph run as a plain chain rather than a
workflow, so today a child namespace resolves to the parent workflow; the
resolution is namespace aware regardless.

Checkpoint de-duplication no longer remembers the last id per namespace,
which could drop an interleaved delegated write or a legitimate re-persist
of the same id. The identity of the Checkpoint object is held while a write
is in flight instead: a nested call receiving the same object is the
delegation and stays silent, and the outermost call reports. A saver that
copies the checkpoint before delegating is reported twice, documented as a
known limit.

Adds tests for namespace resolution, nested resume provenance, interleaved
and concurrent delegated writes, a separate write reusing a checkpoint id,
and thread unbinding on chain end and chain error. The emit_event hunk from
PR open-telemetry#507 is saved at docs/design-notes/pr507-emit_event.patch so its identity
with the local change is checkable on disk.

Claude-Session: https://claude.ai/code/session_01FAnWkWBL3mJ81AMv7KA6br
Third review follow-up.

The interleaving test only synchronized thread entry, so the ordering it
claimed to cover was never forced. A sequenced saver now blocks on
per-call events so the test drives the exact sequence: outer A enters,
the delegated A enters and returns, an independent outer B enters, outer A
returns, outer B returns. It asserts two writes and exactly two events,
one per write, proving the delegation is silent and neither independent
write is dropped as a false duplicate.

Verified in langgraph 1.2.9 that two independent writes can never share a
checkpoint object: the loop hands the saver copy_checkpoint(self.checkpoint),
a freshly constructed mapping, once per superstep. The design note records
that assumption as a stated limit, along with the over-reporting fallback
for a saver that copies before delegating, and its stale description of
the previous id based de-duplication is rewritten.

The PR open-telemetry#507 reference is now stored as extracted source text rather than a
raw unified diff, so it carries no trailing whitespace, and a test checks
the local emit_event against it.

Claude-Session: https://claude.ai/code/session_01FAnWkWBL3mJ81AMv7KA6br
The design note, its generating script, and the captured telemetry lived
under a new top-level docs/design-notes directory. They now live where the
package's other runnable material does.

The script becomes examples/langgraph-lifecycle/main.py alongside a
requirements.txt and a README.rst, matching the shape of the sibling
examples. The design note is now the README's design note section in
reStructuredText, unchanged in substance, and the captured telemetry is
sample-output.json referenced from it. The PR open-telemetry#507 reference source moves
to util/opentelemetry-util-genai/tests/fixtures, next to the test that
checks it, which now resolves the path from the test file rather than the
repository root. The top-level docs/design-notes directory is gone and
nothing else under docs/ changed.

Claude-Session: https://claude.ai/code/session_01FAnWkWBL3mJ81AMv7KA6br
@meshailabs

Copy link
Copy Markdown
Author

Tracking in #537

@opentelemetry-pr-dashboard

Copy link
Copy Markdown

Pull request dashboard status

Waiting on the author · refreshed 2026-09-02 14:52 UTC

Move out of draft to request review.

Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

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.

1 participant