Skip to content

fix(langgraph): lifecycle token, error kind, root registry, required interrupt, awaitable mock transport - #1054

Merged
blove merged 7 commits into
mainfrom
blove/langgraph-lifecycle-followups
Sep 8, 2026
Merged

fix(langgraph): lifecycle token, error kind, root registry, required interrupt, awaitable mock transport#1054
blove merged 7 commits into
mainfrom
blove/langgraph-lifecycle-followups

Conversation

@blove

@blove blove commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Closes the LangGraph half of the adapter follow-ups: items 4a–4e and item 5.

What changed

4a — AGENT_LIFECYCLE is exported but never provided. Both forms of provideAgent() now provide it, so inject(AGENT_LIFECYCLE) returns the same object as injectAgent().lifecycle instead of throwing NG0201. With several refs at one injector level it follows the last ref, the same rule AGENT follows; that is documented on provideAgent() and on the lifecycle guide.

4b — streamErrorAt().classification was always 'AgentError'. The bridge normalizes every failure through toAgentError() before it reaches the lifecycle, so storing e.name produced a constant. The field is renamed to kind (no compat shim) and carries the AgentErrorKindconnection | auth | server | interrupted | aborted — falling back to the constructor name for anything that slipped past normalization.

4c — registry registration was construction-ordered. AgentLifecycleRegistry is now providedIn: '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 every provideAgent(). Agents also unregister on destroy. provideCockpitTelemetry() stops re-providing the class, which would have shadowed the root instance.

4d — agent.interrupt() did not compile under strictNullChecks. LangGraphAgent declares interrupt as a required Signal<AgentInterrupt | undefined>; the neutral chat contract keeps it optional. A type-level assertion in inject-agent.type-spec.ts pins it.

4e — MockAgentTransport.emit() could not be awaited. emit(), emitError() and close() 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-rolled await 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.warn naming 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 regenerated langgraph/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 the await on 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 asserted messages() 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/langgraph vitest: 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 new interrupt assertions fail before the change and pass after. (The langgraph:type-tests Nx target itself is red on main for unrelated reasons: a baseUrl deprecation error and rootDir violations from libs/a2ui.)

New specs: lifecycle-token.spec.ts (3, AGENT_LIFECYCLE wiring for both provider forms and last-ref-wins), 2 in agent.provider.spec.ts (warning fires on the ambiguous path, stays silent for a lone ref), 1 in lifecycle.spec.ts (kind mirrors the AgentError kind), 2 in agent-lifecycle-registry.spec.ts (child-injector agent visible from root; unregister on destroy), 6 in mock-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

@vercel

vercel Bot commented Sep 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
threadplane Ready Ready Preview Sep 8, 2026 2:50am UTC

Request Review

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@blove
blove force-pushed the blove/langgraph-lifecycle-followups branch from 0b4282e to a35c3de Compare September 8, 2026 01:09
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove enabled auto-merge (squash) September 8, 2026 02:12
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

blove and others added 7 commits September 7, 2026 19:45
…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
blove force-pushed the blove/langgraph-lifecycle-followups branch from aef7e88 to 249bf5e Compare September 8, 2026 02:45
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove merged commit 62b5aa6 into main Sep 8, 2026
76 checks passed
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