Thread Routing
injectThreadRouting() binds an app-owned active-thread signal to the Angular Router: it restores the thread id from the URL on load, stamps signal changes back into the URL, validates stale links, and treats a bare URL as "no thread" (the welcome state). The URL is the sole source of truth โ nothing is written to localStorage, so links remain shareable without any extra plumbing.
Call injectThreadRouting() from an injection context (a component constructor or field initializer), and declare the active-thread signal at module scope so your provideAgent({ threadId }) provider can reference it.
#What it does
| Behavior | Detail |
|---|---|
| Restore on load | Reads the thread id from the URL on startup and seeds the signal |
| Signal โ URL | When the signal changes (e.g. after the agent allocates a new thread), navigates to the matching URL |
| URL โ signal | On every NavigationEnd keeps the signal in sync with the current URL |
| Validate stale links | If validate returns false, redirects to the bare path (replaceUrl: true) |
| Bare URL = welcome | When the URL holds no thread segment, the signal is null and the chat shows its welcome state |
Because the URL carries the id, browser reload, forward/back, and shared links all land in the correct conversation โ as long as the backend has a durable store (see When to use it below).
#Canonical usage
The examples/chat app is the reference implementation. The pattern has three moving pieces:
1. A module-scope signal
The signal lives at module scope (outside any class) so provideAgent() โ which runs at provider registration time, before component construction โ can reference it directly. A component-class field would not exist yet when the provider config is evaluated.
2. provideAgent() wires the signal
threadId: ACTIVE_THREADโ the adapter watches this signal; changing it switches threads.onThreadIdโ called when the adapter auto-creates a new thread. Setting the signal here lets thesignal โ URLeffect ininjectThreadRoutingpick up the new id and stamp it into the URL automatically.
3. injectThreadRouting() in the shell component
Call injectThreadRouting() inside the constructor (or any injection context such as a factory function). It must run in an injection context because it calls inject(Router) internally.
The validate callback receives a candidate id whenever one appears in the URL. If the thread no longer exists on the server (deleted, expired, or from a different environment) the helper redirects to the bare path with replaceUrl: true, which replaces the stale URL in history rather than pushing a new entry.
#Custom URL shapes
The defaults (/ for welcome, /<threadId> for a thread) work for simple apps. For mode-prefixed or nested paths, supply toCommands and threadIdFromUrl:
threadIdFromUrl extracts the id from any URL the router lands on. toCommands builds the router-navigate command array for a given id (or null for the bare path). Both receive the current URL string and thread id respectively; neither needs to touch the router directly.
#Full API reference
#When to use it
URL-as-source-of-truth thread routing is the right choice when two conditions are both true:
-
The user sees and can share the URL. A standalone app at its own domain or route is the typical case. If the app runs inside an iframe (e.g. an embedded docs widget) and the URL is never surfaced to the user, URL persistence adds friction without benefit โ keep the active thread in an in-memory signal instead.
-
The backend can actually restore the conversation from the id. LangGraph with a Postgres or Redis checkpointer satisfies this. An in-process
MemorySaverdoes not โ a restored id points at ephemeral state that disappears on server restart, so the UI would show an empty welcome surface even though the URL looks correct.
The underlying principle: persistence UI should match what the backend can actually restore. When the backend is stateless, don't surface stateful affordances like bookmarkable thread URLs.
Use injectThreadRouting() when:
- Building a standalone chat app at a user-visible URL
- The backend uses a durable checkpointer (Postgres, Redis, Neon, etc.)
- You want reload-survival, shareable links, and validated stale-link redirects for free
Keep threads in memory / ephemeral when:
- The app is embedded and the URL is not user-visible
- The server is stateless or uses an in-memory
MemorySaver - The chat is a short-lived session and persistence would be surprising to users
The AG-UI protocol is event-stream-only โ it does not define a server-side thread-lookup endpoint, so validate is not available. When using @threadplane/ag-ui, pass no validate callback and manage thread switching at the host-service level before the agent boots. See AG-UI architecture for details.