fix(langgraph): lifecycle token, error kind, root registry, required interrupt, awaitable mock transport - #1054
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
blove
force-pushed
the
blove/langgraph-lifecycle-followups
branch
from
September 8, 2026 01:09
0b4282e to
a35c3de
Compare
Contributor
blove
enabled auto-merge (squash)
September 8, 2026 02:12
Contributor
blove
force-pushed
the
blove/langgraph-lifecycle-followups
branch
from
September 8, 2026 02:24
787c58a to
aef7e88
Compare
Contributor
…ss injects `AGENT_LIFECYCLE` was exported but never provided, so `inject(AGENT_LIFECYCLE)` threw NG0201 unless the app wired the token itself. Both forms of `provideAgent()` now provide it, resolving to the same object as `injectAgent().lifecycle`. The ref form also warns in development mode when several `provideAgent(ref, …)` calls share an injector level and the ambiguous ref-less token is resolved: the message names every competing ref and the one that won. Injecting by ref stays silent, and production builds never log. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the class name `streamErrorAt().classification` stored `error.name`. The bridge normalizes every failure through `toAgentError()` first, so the field was the literal 'AgentError' on every real stream error — useless as a discriminator. The field is renamed to `kind` and now carries the `AgentErrorKind` (`connection` | `auth` | `server` | `interrupted` | `aborted`), the same value `agent.error()?.kind` carries. A failure that slipped past normalization still falls back to a constructor name, so the type is `AgentErrorKind | string`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tion is not construction-ordered The agent injected the registry optionally at construction, so a registry provided below the agent's injector — or provided after the agent was built — collected nothing. The registry is now `providedIn: 'root'`: every agent registers into the same instance regardless of which injector built it, and an agent created in a route or component injector is visible from the root. Registration is also scoped to the agent's lifetime — an agent unregisters when its injector is destroyed — so `lifecycles()` no longer accumulates dead agents. `provideCockpitTelemetry()` stops re-providing the class, which would have shadowed the instance agents register into. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`interrupt` is optional on the runtime-neutral `Agent` contract because a runtime without human-in-the-loop support omits it, which meant `injectAgent().interrupt()` did not compile under `strictNullChecks` even though the LangGraph adapter always provides it. `LangGraphAgent` now narrows it to a required `Signal<AgentInterrupt | undefined>`; the chat contract is untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…le, add flush() `stream()` is an async generator, so `emit()` only woke the suspended loop and nothing had reached the signals when it returned. Every spec paid for that with a hand-rolled `await new Promise(r => setTimeout(r, 0))`. `emit()`, `emitError()` and `close()` now return a promise that settles once the generator has drained everything queued at the time of the call (or the run has ended), plus one macrotask so signal writes have landed. `flush()` waits the same way without emitting. An emit after the run finished resolves instead of hanging. The langgraph specs that hand-rolled the macrotask flush after an emit are converted to `await transport.emit(...)`; removing the await makes them fail, so the await is load-bearing. Throttle waits (16 ms and up) are left alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ind, required interrupt, and awaitable transport - lifecycle: `kind` replaces `classification` and holds the `AgentErrorKind`; the registry is root-provided and unregisters on destroy; `AGENT_LIFECYCLE` comes from `provideAgent()` and follows the last-ref-wins rule. - provide-agent: documents the `AGENT_LIFECYCLE` token and the dev-mode warning on an ambiguous ref-less inject. - testing and mock-stream-transport: every emit is awaited rather than chased with a macrotask flush; `flush()` is documented; `chat.interrupt()` drops the `?.` now that `LangGraphAgent` requires it. Both pages' spec fences were executed verbatim against the adapter and pass; the transport page's first fence was additionally missing the optimistic user message in its assertion. - agent-contract and introduction: the narrowed `interrupt`, and the corrected lifecycle/registry facts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
LangGraphAgent.interrupt is now a required member, so `agent.interrupt &&` is always true and the packaged-consumer build rejects it with TS2774. The call itself still returns AgentInterrupt | undefined, so the remaining `agent.interrupt()` test is the real condition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove
force-pushed
the
blove/langgraph-lifecycle-followups
branch
from
September 8, 2026 02:45
aef7e88 to
249bf5e
Compare
Contributor
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.
Closes the LangGraph half of the adapter follow-ups: items 4a–4e and item 5.
What changed
4a —
AGENT_LIFECYCLEis exported but never provided. Both forms ofprovideAgent()now provide it, soinject(AGENT_LIFECYCLE)returns the same object asinjectAgent().lifecycleinstead of throwing NG0201. With several refs at one injector level it follows the last ref, the same ruleAGENTfollows; that is documented onprovideAgent()and on the lifecycle guide.4b —
streamErrorAt().classificationwas always'AgentError'. The bridge normalizes every failure throughtoAgentError()before it reaches the lifecycle, so storinge.nameproduced a constant. The field is renamed tokind(no compat shim) and carries theAgentErrorKind—connection|auth|server|interrupted|aborted— falling back to the constructor name for anything that slipped past normalization.4c — registry registration was construction-ordered.
AgentLifecycleRegistryis nowprovidedIn: 'root', so an agent built in a route or component injector registers into the instance the root sees, and a consumer no longer has to provide the registry above everyprovideAgent(). Agents also unregister on destroy.provideCockpitTelemetry()stops re-providing the class, which would have shadowed the root instance.4d —
agent.interrupt()did not compile understrictNullChecks.LangGraphAgentdeclaresinterruptas a requiredSignal<AgentInterrupt | undefined>; the neutral chat contract keeps it optional. A type-level assertion ininject-agent.type-spec.tspins it.4e —
MockAgentTransport.emit()could not be awaited.emit(),emitError()andclose()return a promise that settles once the generator has drained the batch (or the run ended), plus one macrotask so signal writes have landed;flush()waits the same way without emitting. An emit after the run finished resolves rather than hanging. The langgraph specs that hand-rolledawait new Promise(r => setTimeout(r, 0))after an emit are converted; throttle waits (16 ms and up) are untouched.5 — dev-mode warning for several refs at one injector. Resolving the ambiguous ref-less token in dev mode logs a
console.warnnaming every competing ref and the one that won. Injecting by ref is unambiguous and stays silent; production never logs.Docs
guides/lifecycle.mdx,api/provide-agent.mdx,guides/testing.mdx,api/mock-stream-transport.mdx,concepts/agent-contract.mdx,getting-started/introduction.mdx, plus a regeneratedlanggraph/api/api-docs.json.Both pages carrying spec fences were re-executed verbatim through a temporary
__docs_check__.spec.ts(deleted before commit): 8 fences from the testing guide and 2 from the transport page, all green. Reverting theawaiton the converted emits makes two of them fail, so the awaits are load-bearing rather than decorative. The transport page's first fence was additionally wrong before this PR — it assertedmessages()held only the assistant reply, omitting the optimistic user message — and is corrected.Verification
npx nx run-many -t lint,test,build --projects=langgraph,cockpit-telemetry,chat,ag-ui— green (0 lint errors; warnings pre-existing).libs/langgraphvitest: 22 files, 444 tests passing, plus the new specs below.npx vitest run --root apps/website— 131 files, 1326 tests passing.GROWTH_FORM_POLICY=growth_v1 npx nx build website— succeeds.npx tsc -p libs/langgraph/tsconfig.type-tests.json— the newinterruptassertions fail before the change and pass after. (Thelanggraph:type-testsNx target itself is red onmainfor unrelated reasons: abaseUrldeprecation error androotDirviolations fromlibs/a2ui.)New specs:
lifecycle-token.spec.ts(3,AGENT_LIFECYCLEwiring for both provider forms and last-ref-wins), 2 inagent.provider.spec.ts(warning fires on the ambiguous path, stays silent for a lone ref), 1 inlifecycle.spec.ts(kindmirrors theAgentErrorkind), 2 inagent-lifecycle-registry.spec.ts(child-injector agent visible from root; unregister on destroy), 6 inmock-stream.transport.spec.ts(awaitable emit/flush/close/error semantics). Each failed before its change.Not done
Example apps still call
agent.interrupt?.(); that remains valid on a required member and the task scoped the simplification to the docs pages.🤖 Generated with Claude Code