feat(langgraph): add before_builtins opt-in for stream transformers#7882
Merged
Nick Hollon (nick-hollon-lc) merged 2 commits intoMay 21, 2026
Merged
Conversation
Adds a `before_builtins: ClassVar[bool] = False` flag on `StreamTransformer`. When `True`, the mux registers the transformer ahead of the rest, preserving relative order within each lane. This lets content-mutating transformers (PII redaction, content filters, etc.) run before built-ins like `MessagesTransformer` that eagerly snapshot text fields into their projections. Behavior is unchanged for existing transformers — the default is `False` and built-ins keep it `False`, so registration order is identical to before for anyone who hasn't opted in. The class docstring documents the foot-gun: pre-lane transformers see `tasks` events before `LifecycleTransformer` / `SubgraphTransformer` consume them, so mutating `event["params"]["namespace"]` or the data dict's `id` / `result` / `error` / `interrupts` fields will desync their bookkeeping. Observe freely; mutate only fields no built-in reads (`delta.text` on `messages` events is the canonical case).
Sydney Runkle (sydney-runkle)
approved these changes
May 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a
before_builtins: ClassVar[bool] = Falseflag onStreamTransformer. WhenTrue, the mux registers the transformer ahead of the rest, preserving relative order within each lane.Motivation
Content-mutating transformers — PII redaction, content filters, profanity scrubbers — need to run before built-in transformers like
MessagesTransformer, which eagerly snapshots text fields (delta.text,delta.reasoning) into its projection. Today there's no way to register a transformer ahead of the built-ins, so any mutation a user transformer makes todelta.textlands too late:MessagesTransformerhas already pushed the original string into theChatModelStreamtext accumulator.Motivating use case: PII redaction middleware in langchain (see langchain-ai/langchain#37591). Stream-level redaction wants to mutate
delta.textbefore any consumer projection captures it. Withoutbefore_builtins, the only workable Python-side approach iswrap_model_call, which is heavier and only catches model output (not tool deltas, custom events, or subgraph outputs).API
The flag is a class attribute so transformers carry the placement decision with them — callers wiring middleware don't need to know whether the transformer needs pre-lane placement; the transformer itself declares it.
Ordering contract
Within each lane, the supplied order is preserved. The full registration order ends up as:
before_builtins=Truefactories, in supplied ordertransformers=instances, partitioned the same wayBuilt-ins keep the default
before_builtins=False, so registration order is identical to before for anyone who hasn't opted in. No backwards-compatibility break.Foot-gun (documented on the class)
Pre-lane transformers see
tasksevents beforeLifecycleTransformerandSubgraphTransformerconsume them. Mutatingevent["params"]["namespace"]or the data dict'sid/result/error/interruptsfields will desync their bookkeeping. Observe freely; mutate only fields no built-in reads.The canonical safe mutation is
delta.text(anddelta.reasoning) onmessagesevents — exactly the case content-filter transformers need.Tests
tests/test_stream_before_builtins.pycovers:MessagesTransformereven when supplied secondLifecycleTransformer's bookkeeping (read-only observation is safe)Falseon the base class and all built-insdelta.textresults in the redacted string landing inMessagesTransformer's text accumulator — the actual proof that pre-lane placement does what it's supposed to doExisting stream-transformer tests (
test_stream_data_transformers,test_stream_messages_transformer,test_stream_lifecycle_transformer,test_stream_subgraph_transformer— 133 tests) all still pass.