Subagents and subgraphs are discovered eagerly but streamed
lazily. The root injector keeps cheap identity snapshots on
stream.subagents(), stream.subgraphs(), and
stream.subgraphsByNode(). To render a subagent's content, pass its
snapshot into a companion selector injector — the subscription is scoped
to the subagent's namespace and reference-counted.
Learn more: For deep-agent UI patterns, see the Deep Agents frontend documentation.
Runnable examples: The streaming cookbook
streamingpackage hassubagents:*,subagent-status:*, andsubgraphs:*scripts, and thea2uiapp renders a Deep Agent's output live.
Subagent / subgraph identity is always available from the root:
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";
}
Subgraph snapshots carry the same metadata plus the producing node id:
interface SubgraphDiscoverySnapshot {
readonly id: string;
readonly namespace: readonly string[];
readonly nodeId: string;
readonly status: "pending" | "running" | "complete" | "error";
}
The status literals above match the Angular source docs and current streaming protocol examples. If your app targets a pinned or forked server, verify the exact lifecycle values it emits before hard-coding UI branches.
The root stream handle exposes three discovery maps:
stream.subagents() — a ReadonlyMap of
SubagentDiscoverySnapshot keyed
by id.stream.subgraphs() — a ReadonlyMap of
SubgraphDiscoverySnapshot keyed
by id.stream.subgraphsByNode() — the same subgraph snapshots keyed by the
producing graph node.They update live as the server emits discovery events, without opening per-subagent subscriptions.
Pass the snapshot as the target argument to any selector injector.
Messages, tool calls, and values stream only for the subagents that are
actively rendered:
import { Component, Input, computed } from "@angular/core";
import {
injectMessages,
injectStream,
injectToolCalls,
injectValues,
type AnyStream,
type SubagentDiscoverySnapshot,
} from "@langchain/angular";
@Component({
standalone: true,
template: `
@for (subagent of researchers(); track subagent.id) {
<app-subagent-card [stream]="stream" [subagent]="subagent" />
}
`,
})
export class ResearchersComponent {
readonly stream = injectStream();
readonly researchers = computed(() =>
[...this.stream.subagents().values()].filter((subagent) => subagent.name === "researcher"),
);
}
@Component({
standalone: true,
selector: "app-subagent-card",
template: `
<section>
<header>{{ subagent.name }} - {{ subagent.status }}</header>
@for (message of messages(); track message.id ?? $index) {
<app-bubble [msg]="message" />
}
</section>
`,
})
export class SubagentCardComponent {
@Input({ required: true }) stream!: AnyStream;
@Input({ required: true }) subagent!: SubagentDiscoverySnapshot;
readonly messages = injectMessages(this.stream, () => this.subagent);
readonly toolCalls = injectToolCalls(this.stream, () => this.subagent);
readonly values = injectValues<ResearcherState>(this.stream, () => this.subagent);
}
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 — this is the single biggest wire-cost win of the design.
To find subagents by status or name, filter the discovery map inline:
const active = [...stream.subagents().values()].filter((s) => s.status === "running");
const researcher = [...stream.subagents().values()].find((s) => s.name === "researcher");
Subgraph snapshots work the same way. The subgraphsByNode map is handy for laying out nested-graph visualisations:
@Component({
standalone: true,
template: `
@for (entry of stream.subgraphsByNode() | keyvalue; track entry.key) {
<section>
<h3>{{ entry.key }}</h3>
@for (subgraph of entry.value; track subgraph.id) {
<app-subgraph-card [stream]="stream" [subgraph]="subgraph" />
}
</section>
}
`,
})
export class NestedGraphViewComponent {
readonly stream = injectStream();
}
@Component({
standalone: true,
selector: "app-subgraph-card",
template: `<pre>{{ values() | json }}</pre>`,
})
export class SubgraphCardComponent {
@Input({ required: true }) stream!: AnyStream;
@Input({ required: true }) subgraph!: SubgraphDiscoverySnapshot;
readonly values = injectValues(this.stream, () => this.subgraph);
}