The root injectStream hook exposes always-on projections (values, messages, toolCalls, interrupts, error, isLoading, discovery maps). Anything else — scoped subagent state, message metadata, the submission queue, raw channels, media — is available through the companion selector hooks.
Each selector hook opens a ref-counted subscription when the first component mounts it and releases it when the last consumer unmounts. Root calls (no target) are free — they read the already-mounted root projection directly.
All scoped selectors accept a target argument. Valid targets are:
undefined (or omitted) — the root namespace. Free read.SubagentDiscoverySnapshot — as exposed via stream.subagents.values().SubgraphDiscoverySnapshot — as exposed via stream.subgraphs / stream.subgraphsByNode.{ namespace: string[] } (or a raw string[]) — an explicit namespace, useful for custom routing.Subscriptions open on mount and close when the last consumer for a given (channel, namespace) tuple unmounts. Components that don't render a subagent's content never pay for its wire traffic.
| Hook | Returns | Use for |
|---|---|---|
injectValues(stream, target?) |
StateType (root) / T \| undefined (scoped) |
Arbitrary state / scoped snapshot. |
injectMessages(stream, target?) |
BaseMessage[] |
Message stream, root or scoped. |
injectToolCalls(stream, target?) |
AssembledToolCall[] |
Tool-call stream, with per-call status. |
injectMessageMetadata(stream, msgId) |
{ parentCheckpointId } \| undefined |
Powers fork / edit flows. See Fork & edit. |
injectSubmissionQueue(stream) |
{ entries, size, cancel, clear } |
Reactive client-side submission queue. See Queue. |
injectExtension(stream, name, target?) |
T \| undefined |
Read a named custom:<name> extension. |
injectChannel(stream, channels, target?, options?) |
Event[] |
Low-level raw-events escape hatch. |
injectAudio / injectImages / injectVideo / injectFiles |
AudioMedia[] / ImageMedia[] / VideoMedia[] / FileMedia[] |
Assembled multimodal streams. See Multimodal. |
injectMediaUrl(handle) |
string \| undefined |
Turns a media handle into an <img/audio/video src>. |
injectMediaUrl(handle, options?) / injectMediaUrl(handle, options?) |
Player handles | Opinionated playback helpers on top of the media hooks. |
import {
injectStream,
injectMessages,
injectToolCalls,
injectValues,
type AnyStream,
type SubagentDiscoverySnapshot,
} from "@langchain/angular";
function Chat() {
const stream = injectStream({ assistantId: "agent", apiUrl: "/api" });
// Root projections — identical to `stream.messages` / `stream.values`.
// These calls are free: no new subscription is opened.
const rootMessages = injectMessages(stream);
const rootValues = injectValues(stream);
return (
<>
<ThreadView messages={rootMessages} />
{[...stream.subagents.values()].map((s) => (
<SubagentCard key={s.id} stream={stream} subagent={s} />
))}
</>
);
}
function SubagentCard({
stream,
subagent,
}: {
stream: AnyStream;
subagent: SubagentDiscoverySnapshot;
}) {
// Scoped: opens a namespaced subscription for this subagent only.
const messages = injectMessages(stream, subagent);
const toolCalls = injectToolCalls(stream, subagent);
const values = injectValues<ResearcherState>(stream, subagent);
return (
<section>
<header>
{subagent.name} — {subagent.status}
</header>
{messages.map((m) => (
<Bubble key={m.id} msg={m} />
))}
</section>
);
}
injectMessageMetadataReturns { parentCheckpointId } (and undefined while loading). Use it to drive fork / edit UIs:
import { injectMessageMetadata } from "@langchain/angular";
function EditButton({ stream, message }) {
const metadata = injectMessageMetadata(stream, message.id);
return (
<button
disabled={!metadata?.parentCheckpointId}
onClick={() => {
const forkFrom = metadata?.parentCheckpointId;
if (!forkFrom) return;
void stream.submit({ messages: [new HumanMessage("...revised prompt...")] }, { forkFrom });
}}
>
Edit from here
</button>
);
}
See Fork & edit from a checkpoint for the full flow.
injectChannelEscape hatch to the raw protocol event stream. Subscribe to one or more channels and get the buffered events as an array:
const events = injectChannel(stream, ["values", "updates"]);
Pass target (subagent / subgraph / { namespace }) to scope. Useful for bespoke reducers that can't be expressed through injectValues / injectMessages.
injectExtensionRead a single custom extension (wire-level custom:<name> channel) as a reactive snapshot:
const telemetry = injectExtension<Telemetry>(stream, "telemetry");
Runnable example: The
a2uiapp in the streaming cookbook drives a generative UI withinjectExtension(stream, "a2ui"); thestreamingpackage'scustom-transformer:*scripts show the server side.
injectProjection — building your own selectorinjectProjection is the low-level primitive every built-in selector is composed from. Reach for it only when you need a custom projection that the built-in hooks don't express. It acquires a ref-counted projection from the stream's channel registry and subscribes to its store via useSyncExternalStore:
import { injectProjection } from "@langchain/angular";
import { STREAM_CONTROLLER } from "@langchain/angular"; // low-level access key
import { valuesProjection } from "@langchain/langgraph-sdk/stream";
function useScoredValues<T>(stream: AnyStream, namespace: readonly string[]) {
const registry = stream[STREAM_CONTROLLER].registry;
return injectProjection<T | undefined>(
registry,
() => valuesProjection<T>(namespace, "messages"),
`values|${namespace.join(">")}`,
undefined,
);
}
The first render (before the effect runs) returns initialValue. Subsequent renders read from the acquired store. When the last consumer of a given key unmounts, the underlying server subscription is released automatically. Most apps never need this — prefer injectValues / injectMessages / injectToolCalls / injectChannel.
Subscribe to a scoped values stream — the most recent state
Subscribe to a scoped messages stream.
Subscribe to a scoped tools (tool-call) stream. Same target and
Read metadata recorded for a specific message id — today exposes
Subscribe to a custom:<name> stream extension — the most recent
Angular-side primitive that composes ChannelRegistry.acquire