@langchain/angular v1This guide walks application authors through the jump from the pre-v1
injectStream API in @langchain/angular v0 to the injectStream
injector built for new event-based streaming, which ships with
@langchain/angular v1.
Short version: the injectStream import name does not change, but the
return shape, option bag, and protocol semantics do. Most chat apps
migrate in well under an hour by following the checklists below. Apps
that lean heavily on history / branch / fetchStateHistory or on a
custom UseStreamTransport have more work to do and are covered in
dedicated sections.
submit() signature changestools + onTool)UseStreamTransport → AgentServerAdapterprovideStream / zero-argument injectStream()StreamService redesignUseStream<T> → StreamApi<T> & friendsThe legacy injectStream was built against the legacy streaming protocol and
accreted a large surface of opt-in callbacks (onUpdateEvent,
onCustomEvent, onMetadataEvent, onLangChainEvent, onDebugEvent,
onCheckpointEvent, onTaskEvent, onToolEvent, onStop, …) plus
derived state (history, branch, experimental_branchTree,
getMessagesMetadata, joinStream) that had to be recomputed on every
change-detection cycle.
The v1 package injector uses new event-based streaming. In practice that means:
DestroyRef. No more
fan-out cost for views that aren't on screen.values / messages / toolCalls /
interrupts are always available at the root with zero wire cost
beyond the protocol stream itself.isLoading behaves consistently across route
navigations.assistantId and agentServerAdapter is a compile-time error.typeof agent flows through to
values, toolCalls[].args, and subagent-state maps without any
<MyState, MyBag> boilerplate.The net effect is a smaller, faster, more predictable API that still covers every scenario the legacy injector supported.
For the typical app this is the whole migration. Deeper changes are flagged in the later sections.
@langchain/angular to ^1.0.0 and
@langchain/langgraph-sdk to the matching new event-based streaming
runtime.import { injectStream } from "@langchain/angular"
now resolves to the injector built for new event-based streaming.
useStreamExperimental is not
exported from this package.onError, onFinish, onUpdateEvent, onCustomEvent,
onMetadataEvent, onLangChainEvent, onDebugEvent,
onCheckpointEvent, onTaskEvent, onToolEvent, onStop,
fetchStateHistory, reconnectOnMount, throttle, thread,
filterSubagentMessages, subagentToolNames.transport: new FetchStreamTransport(...) with
transport: new HttpAgentServerAdapter(...) — the legacy
FetchStreamTransport class is no longer exported (see §9).branch, setBranch, history, experimental_branchTree,
getMessagesMetadata, toolProgress, joinStream,
switchThread, queue, activeSubagents, getSubagent,
getSubagentsByType, getSubagentsByMessage.getMessagesMetadata(msg)?.firstSeenState?.parent_checkpoint
with injectMessageMetadata(stream, msg.id)?.parentCheckpointId (see
§6).stream.queue with injectSubmissionQueue(stream) (see
§6).stream.switchThread(id) with passing a
threadId signal and letting the injector reload on change (see §4).injectMessages(stream, subagent) etc.) rather than
reading subagent.messages / subagent.toolCalls off the
discovery snapshot (see §7).StreamApi<T> in
Angular app code. Keep UseStreamReturn<T> for helpers that are
intentionally shared with React-style packages (see §13).submit(..., { onDisconnect, streamResumable }) with
stream.stop() (cancel, default) or stream.disconnect()
(join/rejoin) on the stream handle (see §5.3).tsc. The option bag and return type are now
discriminated and strongly typed; most remaining issues will
surface as type errors that map to one of the sections below.These keep working without changes:
| Option | Notes |
|---|---|
assistantId |
Required for the LGP path; optional (defaults to "_") for custom adapters. |
client |
LGP branch only. |
apiUrl, apiKey, callerOptions, defaultHeaders |
LGP branch only. Passed to the auto-constructed Client. |
threadId, onThreadId |
Unchanged. Pass null to detach; passing a new string reloads the thread and resubscribes. |
initialValues |
Unchanged. |
messagesKey |
Unchanged — defaults to "messages". |
onCreated |
Fires with { runId }. Read the current thread from stream.threadId when needed. |
tools, onTool |
Unchanged semantics; see §8. |
| Option | Notes |
|---|---|
transport |
Two meanings: "sse" / "websocket" selects the built-in wire transport (LGP branch, default "sse"); an AgentServerAdapter instance flips the injector into the custom-adapter branch. |
fetch |
LGP branch only. Forwarded to the built-in SSE transport. |
webSocketFactory |
LGP branch only. Forwarded to the built-in WebSocket transport. |
onCompleted |
Convenience callback for run-execution side effects. Fires with { runId?, reason } when active streaming ends; runId may be absent for re-attached in-flight runs. |
| Legacy option | v1 replacement |
|---|---|
onError (injector-level) |
Read stream.error() directly, or pass a per-submit onError via submit(input, { onError }) — the v1 per-submit callback is fire-and-forget and scoped to the one submission it was passed to. |
onFinish |
Use onCompleted for imperative side effects, or derive render state from isLoading / injectValues(stream). |
onUpdateEvent, onCustomEvent, onMetadataEvent, onLangChainEvent, onDebugEvent, onCheckpointEvent, onTaskEvent, onToolEvent |
Drop. New event-based streaming delivers these as structured store updates; read them via selector injectors (injectChannel, injectExtension) when you need raw events, or injectChannelEffect(stream, channels, { onEvent }) for per-event side effects like analytics. |
onStop |
Drop. Use stream.stop() to cancel the active run (default) or stream.disconnect() to leave the agent running server-side. See §5.3. |
fetchStateHistory |
Drop. Fork/edit flows use injectMessageMetadata + submit({}, { forkFrom }) instead (§5). |
reconnectOnMount |
Drop. Re-attach is automatic: recreating the injector with the same threadId attaches to the in-flight run. |
throttle |
Drop. The injector batches state updates natively; call sites that need render throttling can memoize at the selector site. |
thread |
Drop. External thread managers should drive the injector by controlling threadId and initialValues. |
filterSubagentMessages |
Drop. Subagent messages are already absent from stream.messages(); they live on per-subagent selector injectors (§7). |
subagentToolNames |
Drop. Subagent classification is driven by new event-based streaming lifecycle events, not by a client-side tool-name list. |
Any previously silent callback-based side effects should migrate into effects that watch the relevant projection:
// Before
injectStream({ onFinish: (state) => analytics.track("turn_finished", state) });
// After
const stream = injectStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
});
effect(() => {
if (!stream.isLoading()) {
analytics.track("turn_finished", stream.values());
}
});
| Field | Notes |
|---|---|
values |
Now typed as the resolved StateType, non-nullable at the root (falls back to initialValues ?? {}). |
messages |
BaseMessage[] class instances from langchain. |
toolCalls |
AssembledToolCall[] — renamed shape, see §4.3. |
interrupts, interrupt |
Unchanged. interrupt is the most recent root interrupt. |
isLoading |
True while a run is in flight or initial hydration hasn't completed. |
error |
Unchanged. |
threadId |
Unchanged. |
client |
LGP Client when the built-in transport is in use. |
assistantId |
Resolved value including the "_" fallback used by custom adapters. |
submit, stop, respond, disconnect |
submit argument types are wider (§5). stop(options?) cancels server-side by default; disconnect() is join/rejoin client-only (§5.3). |
| Field | What changed |
|---|---|
subagents |
Now a ReadonlyMap<string, SubagentDiscoverySnapshot>. The snapshot only carries id / name / namespace / status — no messages / toolCalls / values. Read those via selector injectors (§7). |
isThreadLoading |
Still exposed but now reflects the initial thread-load lifecycle rather than fetchStateHistory. |
| Legacy field | v1 replacement |
|---|---|
branch, setBranch, experimental_branchTree |
Branching is expressed as fork-from-checkpoint: call injectMessageMetadata(stream, msg.id) to read the message's parent checkpoint and submit(input, { forkFrom }) to fork. |
history, fetchStateHistory |
Dropped from the injector. Fetch history explicitly with client.threads.getHistory(threadId) if you need it; most apps do not. |
getMessagesMetadata(msg, i) |
injectMessageMetadata(stream, msg.id) returns { parentCheckpointId } (see §6). |
toolProgress |
Dropped. Tool progress is now observable via injectToolCalls(stream) — each AssembledToolCall carries its own status. |
joinStream(runId, ...) |
Dropped. Recreating the injector with the right threadId rejoins automatically. |
switchThread(newThreadId) |
Drive threadId as a Signal<string \| null>. The injector reloads when the signal changes. |
queue |
injectSubmissionQueue(stream) companion injector (see §6). |
activeSubagents, getSubagent, getSubagentsByType, getSubagentsByMessage |
Iterate stream.subagents() (a Map) and filter inline; every discovery snapshot carries name, status, parentId, namespace, and the tool-call id that spawned it. |
| Field | Purpose |
|---|---|
subgraphs |
ReadonlyMap<string, SubgraphDiscoverySnapshot> — subgraphs discovered on the thread (distinct from subagents). |
subgraphsByNode |
ReadonlyMap<string, SubgraphDiscoverySnapshot[]> — same data keyed by graph node. Arrays to preserve parallel fan-out order. |
hydrationPromise |
Resolves once initial thread load finishes. Useful for SSR / await-before-render pipelines. |
// Before
@Component({
/* ... */
})
export class Chat {
readonly stream = injectStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
onError: (err) => console.error(err),
fetchStateHistory: true,
});
}
// After
@Component({
/* ... */
})
export class Chat {
readonly stream = injectStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
});
constructor() {
effect(() => {
const error = this.stream.error();
if (error) console.error(error);
});
}
}
submit() signature changessubmit() now accepts either a wire-format message payload or an
array of BaseMessage class instances:
// All three forms are valid:
await submit({ messages: [{ role: "user", content: "hi" }] });
await submit({ messages: [new HumanMessage("hi")] });
await submit({ messages: new HumanMessage("hi") });
This is driven by the new WidenUpdateMessages<T> helper (exported
publicly for stream-forwarding scenarios; see §13). You do not need to
reach for it unless you're typing an intermediate variable.
Legacy SubmitOptions field |
v1 StreamSubmitOptions equivalent |
|---|---|
config.configurable |
config.configurable (unchanged) |
context |
Drop — fold into config.configurable. |
checkpoint: { checkpoint_id } |
forkFrom: "cp_123" (direct checkpoint id string). The earlier non-functional forkFrom: { checkpointId } object form was removed. |
command: { resume } |
Use stream.respond() instead. |
interruptBefore, interruptAfter |
Drop — not supported with new event-based streaming. |
metadata |
Unchanged. |
multitaskStrategy |
Unchanged. "rollback" (default), "reject", and "enqueue" are honoured client-side today; "interrupt" falls back to "rollback" pending server support (see §13). |
onCompletion |
Use the injector-level onCompleted option for run-completion side effects. |
onDisconnect, feedbackKeys, streamMode, runId, optimisticValues, streamSubgraphs, streamResumable, checkpointDuring |
Drop from submit. Disconnect/cancel policy now lives on stop() / disconnect() instead of per-submit options (§5.3). optimisticValues has no client-side analogue — reconcile via values after the run settles. |
(new submit option) onError |
Per-submit fire-and-forget error callback. There is no injector-level onError option; transport-level stream.error() updates still happen in parallel. |
(new) threadId |
Per-submit thread override — rebinds the controller to the given thread before dispatching, then keeps it bound until the configured threadId changes again. |
// Before
await submit(
{ messages: [new HumanMessage("retry")] },
{
checkpoint: { checkpoint_id: "cp_123" },
multitaskStrategy: "rollback",
optimisticValues: (prev) => ({ ...prev, pending: true }),
},
);
// After
await submit(
{ messages: [new HumanMessage("retry")] },
{ forkFrom: "cp_123", multitaskStrategy: "rollback" },
);
Legacy stop() only aborted the client transport. Per-submit
onDisconnect: "continue" | "cancel" (often paired with
streamResumable: true) decided whether the agent kept running when
that transport dropped.
v1 makes the split explicit on the stream handle:
| Legacy pattern | v1 replacement |
|---|---|
| Stop button in a normal chat (cancel the agent) | await stream.stop() — default { cancel: true } calls client.runs.cancel, then disconnects the client |
| Join/rejoin — leave the agent running | await stream.disconnect() or await stream.stop({ cancel: false }) |
submit(..., { onDisconnect: "cancel" }) |
Drop from submit — call stream.stop() when the user cancels |
submit(..., { onDisconnect: "continue", streamResumable: true }) |
Drop from submit — call stream.disconnect() when navigating away; reattach by remounting with the same threadId |
// Before — disconnect policy lived on submit
await submit(input, { onDisconnect: "continue", streamResumable: true });
// stream.stop() only aborted the client; the server kept running only
// when onDisconnect was "continue"
// After — explicit stop vs disconnect
await submit(input);
await stream.stop(); // chat cancel (server + client)
await stream.disconnect(); // join/rejoin (client only)
runs.cancel is issued only once onCreated has provided a runId.
Stopping before the run is accepted still disconnects the client
immediately.
Legacy injectStream returned everything in one object. v1 keeps the
always-on data on the root return and pushes the rest into companion
selector injectors that ref-count their server subscriptions. Call
them from an Angular injection context; the host DestroyRef handles
cleanup.
All of these are exported from @langchain/angular:
| Hook | Replaces | Notes |
|---|---|---|
injectValues(stream) |
stream.values |
Root form is a free read; scoped form (injectValues(stream, target)) opens a namespaced subscription. Explicit generic: injectValues<State>(stream, sub). |
injectMessages(stream) |
stream.messages |
Same pattern. Scoped view yields subagent / subgraph messages without fan-out. |
injectToolCalls(stream) |
stream.toolCalls |
Typed tool-call union is inferred from typeof agent or an explicit tools array. |
injectMessageMetadata(stream, msgId) |
stream.getMessagesMetadata(msg, i) |
Returns { parentCheckpointId } \| undefined. Drives fork-from-checkpoint. |
injectSubmissionQueue(stream) |
stream.queue |
Returns { entries, size, cancel(id), clear() }. Backed by multitaskStrategy: "enqueue". |
injectExtension(stream, name) |
Per-event callbacks | Read a named protocol extension (custom channel). |
injectChannel(stream, channels) |
Raw event callbacks | Low-level escape hatch for buffered raw events. |
injectChannelEffect(stream, channels, { onEvent }) |
onLangChainEvent, onCustomEvent |
Per-event side-effect callback for analytics or logging. No template update. |
injectAudio, injectImages, injectVideo, injectFiles |
— | New, multimodal streaming. |
injectMediaUrl |
— | Managed object URLs for media handles. |
// Before: everything on the root
const { messages, toolCalls, getMessagesMetadata, queue } = injectStream({
assistantId,
});
// After: always-on stays on the root; rest moves to selectors
const stream = injectStream({ assistantId });
const messages = injectMessages(stream); // or just stream.messages
const toolCalls = injectToolCalls(stream); // or just stream.toolCalls
const metadata = injectMessageMetadata(stream, messages.at(-1)?.id);
const { entries, size, cancel, clear } = injectSubmissionQueue(stream);
branch flow)@Component({
standalone: true,
template: `
<button [disabled]="!metadata()?.parentCheckpointId" (click)="editFromHere()">
Edit from here
</button>
`,
})
export class EditButton {
@Input({ required: true }) message!: BaseMessage;
readonly stream = injectStream();
readonly metadata = injectMessageMetadata(this.stream, () => this.message.id);
editFromHere() {
const forkFrom = this.metadata()?.parentCheckpointId;
if (!forkFrom) return;
void this.stream.submit({ messages: [new HumanMessage("...revised prompt...")] }, { forkFrom });
}
}
queue flow)@Component({
standalone: true,
template: `
<button (click)="queueTurn()">Queue turn</button>
@for (entry of queue.entries(); track entry.id) {
<li>
pending...
<button (click)="queue.cancel(entry.id)">cancel</button>
</li>
}
@if (queue.size() > 0) {
<button (click)="queue.clear()">Clear queue</button>
}
`,
})
export class Composer {
readonly stream = injectStream();
readonly queue = injectSubmissionQueue(this.stream);
queueTurn() {
void this.stream.submit(
{ messages: [new HumanMessage("go")] },
{ multitaskStrategy: "enqueue" },
);
}
}
Subagents and subgraphs are now discovered eagerly but streamed
lazily. The discovery maps (stream.subagents(),
stream.subgraphs(), stream.subgraphsByNode()) are kept in sync with
zero extra wire cost; each snapshot exposes identity fields only:
interface SubagentDiscoverySnapshot {
readonly id: string; // tool-call id that spawned it
readonly name: string; // "researcher", "writer", …
readonly namespace: readonly string[];
readonly parentId: string | null;
readonly depth: number;
readonly status: "pending" | "running" | "complete" | "error";
// No messages / toolCalls / values. Use selector injectors below.
}
Status names above follow the Angular source docs and current protocol examples. Verify them against pinned or forked servers before hard-coding status-specific UI.
Replace every subagent.messages / subagent.toolCalls / subagent.values
read with the matching selector, passing the discovery snapshot:
@Component({
standalone: true,
selector: "app-subagent-card",
template: `
@for (m of messages(); track m.id ?? $index) {
<div>{{ asText(m.content) }}</div>
}
`,
})
export class SubagentCard {
@Input({ required: true }) subagent!: SubagentDiscoverySnapshot;
readonly stream = injectStream();
readonly messages = injectMessages(this.stream, () => this.subagent);
readonly toolCalls = injectToolCalls(this.stream, () => this.subagent);
readonly values = injectValues<ResearcherState>(this.stream, () => this.subagent);
asText(value: unknown) {
return typeof value === "string" ? value : JSON.stringify(value);
}
}
The first time any component mounts injectMessages(stream, subagent) a
messages-channel subscription is opened, scoped to
subagent.namespace. When the last consumer unmounts, the subscription
is released automatically. This is the single biggest wire-cost win of
the new design — views that don't render a subagent's messages never
pay for them.
activeSubagents, getSubagent(id), getSubagentsByType(name), and
getSubagentsByMessage(msg) are gone. Derive the equivalents inline:
const active = [...stream.subagents().values()].filter((s) => s.status === "running");
const researcher = [...stream.subagents().values()].find((s) => s.name === "researcher");
const byType = new Map<string, SubagentDiscoverySnapshot[]>();
for (const s of stream.subagents().values()) {
const bucket = byType.get(s.name) ?? [];
bucket.push(s);
byType.set(s.name, bucket);
}
tools + onTool)The legacy tools / onTool options are preserved one-for-one. The
root injector listens for interrupt payloads that target a registered tool,
invokes the handler, and auto-resumes the run with the handler's return
value — exactly the pre-v1 behaviour. A mount-safe dedupe guard prevents
double invocation when the same interrupt is observed twice.
const stream = injectStream({
assistantId: "deep-agent",
tools: [getCurrentLocation, confirmAction],
onTool: (event) => {
if (event.type === "error") logger.error(event.error);
},
});
No migration is needed if you were already using this API. The helper
exports (flushPendingHeadlessToolInterrupts, findHeadlessTool,
handleHeadlessToolInterrupt, …) are still available from
@langchain/angular for advanced flows that compose their own interrupt
handling.
UseStreamTransport → AgentServerAdapterThe legacy custom-transport surface looked like:
interface UseStreamTransport<S, Bag> {
stream(payload: UseStreamTransportPayload<S, Bag>): Promise<
AsyncGenerator<{ id?: string; event: string; data: unknown }>
>;
}
// The convenience class that implemented it:
class FetchStreamTransport implements UseStreamTransport { ... }
v1 replaces this with AgentServerAdapter, a richer interface that
owns the entire transport — both commands and the event stream —
and matches the new event-based streaming protocol's request shape. There is a convenience
class HttpAgentServerAdapter that covers the common case (SSE + WS
with injectable fetch / webSocketFactory / defaultHeaders).
HttpAgentServerAdapter// Before
import { FetchStreamTransport, injectStream } from "@langchain/angular";
const transport = new FetchStreamTransport({ apiUrl: "/api/chat" });
const stream = injectStream({ transport });
// After
import { HttpAgentServerAdapter, injectStream } from "@langchain/angular";
const transport = new HttpAgentServerAdapter({
apiUrl: "/api/chat",
threadId: "thread-123", // required: the adapter is bound to a thread
defaultHeaders: { Authorization: `Bearer ${token}` },
// Optional: fetch override or webSocketFactory for WebSocket transports
fetch: myAuthedFetch,
});
const stream = injectStream({ transport });
If you hand-rolled a UseStreamTransport, migrate to
AgentServerAdapter:
interface AgentServerAdapter {
readonly threadId: string;
open(): Promise<void>;
send(command: Command): Promise<CommandResponse | ErrorResponse | void>;
events(): AsyncIterable<Message>;
openEventStream?(params: SubscribeParams): EventStreamHandle;
close(): Promise<void>;
// Optional — implement if your server supports them:
getState?(): Promise<{
values: unknown;
checkpoint?: { checkpoint_id?: string } | null;
} | null>;
getHistory?(options?: { limit?: number }): Promise<
Array<{
values: unknown;
checkpoint?: { checkpoint_id?: string } | null;
}>
>;
}
The adapter is used exactly as-is; the LGP Client is not
constructed when a custom adapter is supplied, so bundles that only
use a custom adapter tree-shake the entire sse/websocket transport
stack.
Passing both assistantId + apiUrl and a transport: AgentServerAdapter is now a compile-time error:
injectStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
transport: myAdapter, // ❌ `apiUrl` is `never` on the custom-adapter branch
});
Pick one branch per injectStream instance.
provideStream / zero-argument injectStream()The provider and consumer are unchanged at the call site — the
provider wraps whatever injectStream returns and publishes it through
Angular dependency injection:
@Component({
providers: [
provideStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
}),
],
template: `<app-chat />`,
})
export class ChatContainer {}
@Component({
template: `
@for (msg of stream.messages(); track msg.id ?? $index) {
<div>{{ msg.content }}</div>
}
`,
})
export class Chat {
readonly stream = injectStream();
}
Zero-argument injectStream() throws if no ancestor called
provideStream(...). Pair it with provideStreamDefaults(...) in app
configuration to avoid repeating apiUrl / apiKey in every provider.
Because the underlying value is now a v1 StreamApi, consumers that
previously read stream.queue, stream.branch, or subagent payloads
directly must move to the selector injectors in §6.
StreamService redesignLegacy StreamService wrapped a hidden orchestrator and exposed mixed
reactive and plain fields. v1 redesigns it as a thin class that
composes the framework-agnostic useStream factory:
@Injectable({ providedIn: "root" })
export class ChatService extends StreamService<ChatState> {
constructor() {
super({ assistantId: "agent", apiUrl: "http://localhost:2024" });
}
}
The service forwards every StreamApi getter and method (messages,
values, isLoading, submit, stop, and so on), so components can
inject(ChatService) and keep calling chat.messages() or
chat.submit(...). The raw handle is also available as chat.stream
for code that needs to pass it into selector injectors.
useStream(options, destroyRef?) is also exported for library code that
needs to own the controller outside Angular's injection context. It
returns the same StreamApi shape as injectStream.
@langchain/angular does not ship a separate suspense injector. Use
injectStream directly and gate your templates on
isLoading, isThreadLoading, or hydrationPromise:
import { Component, inject } from "@angular/core";
import { injectStream } from "@langchain/angular";
@Component({
template: `
@if (stream.isThreadLoading()) {
<app-spinner />
} @else {
<app-message-list />
}
`,
})
export class Chat {
readonly stream = injectStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
threadId,
});
}
hydrationPromise settles when the active thread's initial hydrate
completes — useful for deferring child routes until the first snapshot
lands.
The v1 package exports a small set of type helpers you reach for when passing a stream handle between components:
| Helper | Use |
|---|---|
StreamApi<T> |
Preferred Angular stream-handle type. Use for component/service app code. |
UseStreamReturn<T> |
Same resolved handle shape, useful for shared utilities that intentionally match React-style package naming. |
AnyStream |
Type-erased handle for components that only forward the stream to selector injectors. |
InferStateType<T> |
Unwraps a compiled graph / agent brand / agent tool array into its state shape. |
InferToolCalls<T> |
Derives a discriminated union of tool-call shapes from tools array, agent brand, or an explicit shape. |
InferSubagentStates<T> |
{ name: State, … } map derived from a DeepAgent brand. |
WidenUpdateMessages<T> |
Widens messages in a partial state update so both wire-format and BaseMessage instances typecheck in submit(). |
StreamSubmitOptions<State, Configurable> |
Options shape accepted by submit(). |
AgentServerAdapter |
Interface for custom transports (see §9). |
HttpAgentServerAdapter, HttpAgentServerAdapterOptions |
Convenience adapter (see §9). |
UseStreamOptions, AgentServerOptions, CustomAdapterOptions |
Discriminated options union; rarely needed at call sites. |
SelectorTarget, SubagentDiscoverySnapshot, SubgraphDiscoverySnapshot |
For components that render per-subagent/subgraph views. |
AssembledToolCall, ToolCallStatus |
For rendering tool-call UI. |
MessageMetadata, MessageMetadataMap, UseSubmissionQueueReturn, SubmissionQueueEntry, SubmissionQueueSnapshot |
Companion-injector return shapes. |
Breaking change: the legacy type aliases that used to be
re-exported from @langchain/angular are no longer available from
this package. The following names are gone:
UseStream, UseSuspenseStream, UseStreamCustom, UseStreamOptions,
UseStreamCustomOptions, UseStreamTransport, UseStreamThread,
GetToolCallsType, QueueEntry, QueueInterface,
SubagentStream, SubagentStreamInterface,
BaseSubagentState, SubagentStateMap, DefaultSubagentStates,
InferAgentToolCalls, InferDeepAgentSubagents,
InferSubagentByName, InferSubagentNames, InferSubagentState,
SubAgentLike, CompiledSubAgentLike, DeepAgentTypeConfigLike,
AgentTypeConfigLike, IsAgentLike, IsDeepAgentLike,
ExtractAgentConfig, ExtractDeepAgentConfig,
ExtractSubAgentMiddleware, SubagentToolCall, SubagentStatus,
ClassSubagentStreamInterface, FetchStreamTransport.
Migrate each call site to the v1 name:
| Legacy name | v1 replacement |
|---|---|
UseStream<State, Bag> |
StreamApi<State> in Angular app code; UseStreamReturn<State> in shared helpers. |
UseStreamOptions<State, Bag> |
UseStreamOptions<State> (or just let the injector infer). |
UseStreamTransport |
AgentServerAdapter (§9). |
FetchStreamTransport |
HttpAgentServerAdapter (§9). |
GetToolCallsType<State> |
InferToolCalls<typeof agent>. |
UseSuspenseStream<…> |
UseSuspenseStreamReturn<T> (v1 package API shape — see §11). |
QueueEntry, QueueInterface |
SubmissionQueueEntry, UseSubmissionQueueReturn (§6). |
SubagentStream, SubagentStreamInterface |
SubagentDiscoverySnapshot + injectMessages(stream, subagent) (§7). |
Apps that cannot migrate all call sites in one pass can keep using the
legacy types by importing them directly from
@langchain/langgraph-sdk/ui during the transition — but mixing
legacy and v1 types on the same injectStream result will not
typecheck.
MessageMetadata collisionBreaking change: @langchain/angular v1 exports a new
MessageMetadata from @langchain/langgraph-sdk/stream with a
different shape ({ parentCheckpointId }) than the legacy one
({ messageId, firstSeenState, branch, branchOptions }). Legacy call
sites must import the legacy type directly:
// Before
import type { MessageMetadata } from "@langchain/angular";
// After (only if you still need the legacy shape)
import type { MessageMetadata } from "@langchain/langgraph-sdk/ui";
Some v1 type-level features are accepted but not yet executed end- to-end — they are wired through the client and will activate once the matching server-side protocol work ships. This matters if you are on self-hosted LangGraph or a pinned server version.
| Feature | Status today |
|---|---|
submit(input, { forkFrom }) |
Type-accepted; forwarded on run.start. |
multitaskStrategy: "enqueue" |
Fully honoured client-side. The controller records the submission in queueStore, exposes it via injectSubmissionQueue(stream), and drains queued entries sequentially after each run settles. Server-native queueing lands with A0.3. |
multitaskStrategy: "reject" |
Fully honoured client-side — submit() throws when a run is already in flight. |
multitaskStrategy: "rollback" (default) |
Fully honoured client-side — the in-flight run is aborted and the new submission dispatches immediately. |
multitaskStrategy: "interrupt" |
Type-accepted. Falls back to "rollback" behaviour until server-side interrupt semantics land (A0.3). |
injectMessageMetadata().parentCheckpointId |
Populated from the parent_checkpoint field on values events. |
Until those land, the client-side scaffolding is a no-op for the relevant field rather than a crash — code written against the v1 surface will start to function the moment the server catches up.
onLangChainEvent / onDebugEvent / onCustomEvent?Use injectChannel(stream, channels) for a bounded buffer of raw events
scoped to a namespace, or subscribe to a specific extension with
injectExtension(stream, name). For app-wide telemetry that should not
trigger change detection, use injectChannelEffect(stream, channels, { onEvent }). You can also pipe the raw stream through a custom
AgentServerAdapter (§9) and tee events to your analytics sink there.
values events (no messages channel). Will streaming still work?Yes — stream.messages() merges messages-channel deltas and
values.messages snapshots. Backends that only emit values will
render full turns at once instead of token-by-token. This is a backend
concern; the Angular layer faithfully renders whatever the server sends.
@langchain/langgraph-sdk in app code. Do we need to bump it?Yes. @langchain/angular v1 depends on the new event-based streaming runtime in
@langchain/langgraph-sdk. Bumping the SDK is mandatory, but the
public v1 type helpers (InferStateType, AgentServerAdapter, …)
are re-exported from @langchain/angular so app imports rarely need to
reach into the SDK directly.
injectStream call that was deeply generic (injectStream<State, Bag>)?v1 takes three generics: injectStream<T, InterruptType, ConfigurableType>
where T is either a plain state shape or an agent brand
(typeof agent). The legacy Bag options (UpdateType,
CustomEventType, MetaType) are gone — widening is automatic for
messages, and update / custom / meta shapes are no longer tracked by
type. If you passed InterruptType via Bag, lift it to the second
generic slot.
// Before
injectStream<MyState, { InterruptType: MyInterrupt }>({ ... });
// After
injectStream<MyState, MyInterrupt>({ ... });
injectStream / FetchStreamTransport surface go?The pre-v1 injectStream implementation and legacy type re-exports have
been removed from @langchain/angular v1. In particular:
import { FetchStreamTransport } from "@langchain/angular" no longer
resolves — use HttpAgentServerAdapter from the same package (§9).UseStream, UseStreamOptions,
UseStreamTransport, QueueEntry, SubagentStream, …) are no
longer re-exported; use the v1 names from §13.@langchain/angular does not ship useSuspenseStream. Use
injectStream and gate on isThreadLoading / hydrationPromise
(§12).Apps that still need the legacy types mid-migration can import them
directly from @langchain/langgraph-sdk/ui. No pre-v1 runtime code
remains in the @langchain/angular bundle.
multitaskStrategy: "enqueue" work today?Yes, end-to-end on the client. Submissions issued with { multitaskStrategy: "enqueue" } while another run is in flight are
recorded in the controller's queueStore, exposed via
injectSubmissionQueue(stream) (with cancel(id) / clear()
affordances), and drained sequentially once the active run settles.
Switching threads clears the queue.