Streaming
Real streaming lives on the wire client — the *Frames methods drain a verb's response frame-by-frame with cursors, cumulative counts, and isFinal. The HTTP client has none.
- The wire client
BrainClient(streaming is wire-only — see below). - A connection token; see Wire client.
- Familiarity with Recall and Pagination.
A recall call that drains streamed RECALL_RESP frames, reads the running cumulativeCount and estimatedRemaining, and takes the final verdict from the terminal frame — plus the same pattern for graph fetch and memory listing.
Streaming is a wire feature
There is no SSE and no stream: true flag. The HTTP client (BrainHttpClient) returns one buffered JSON body per call. Streaming is a property of the wire protocol: several verbs answer with a stream of frames terminated by an end-of-stream marker, and the wire client exposes both a convenience form (drains and flattens) and a *Frames form (hands you the raw frames).
| Convenience | Streamed frames | What the frames carry |
|---|---|---|
recall(req) | recallFrames(req) | answerKind, memories, isFinal, cumulativeCount, estimatedRemaining |
memoryList(req) | memoryListFrames(req) | page items, cursor, isFinal |
graphFetch(req) | graphFetchFrames(req) | nodes, edges, nextCursor, isFinal |
traverseRelations(req) | traverseRelationsFrames(req) | paths, truncated, totalPaths |
listEntities / listStatements / listRelationsFrom / listRelationsTo / listSchemas | each has a *Frames variant | items + cursor |
The convenience method calls the *Frames method and concatenates — reach for *Frames only when you need the per-frame metadata.
Step-by-step
The last frame carries isFinal and the terminal answerKind; earlier frames carry batches of memories plus the running counts.
import { RecallBuilder } from "@brain-db/sdk";
const frames = await client.recallFrames(
new RecallBuilder("travel preferences").maxResults(50).build(),
);
for (const f of frames) {
console.log(`batch of ${f.memories.length}, seen ${f.cumulativeCount}, ~${f.estimatedRemaining} left`);
}
const verdict = frames.at(-1)?.answerKind ?? "None";from brain_db_sdk import RecallBuilder
frames = client.recall_frames(RecallBuilder("travel preferences").limit(50).build())
for f in frames:
print(f"batch of {len(f.memories)}, seen {f.cumulative_count}, ~{f.estimated_remaining} left")
verdict = frames[-1].answer_kind if frames else "None"use brain_db_sdk::RecallBuilder;
use brain_db_sdk::wire::types::AnswerKindWire;
let frames = client
.recall_frames(&RecallBuilder::new("travel preferences").max_results(50).build())
.await?;
for f in &frames {
println!(
"batch of {}, seen {}, ~{} left",
f.memories.len(),
f.cumulative_count,
f.estimated_remaining
);
}
let verdict = frames.last().map_or(AnswerKindWire::None, |f| f.answer_kind);graphFetch walks the whole typed graph; the frames carry a nextCursor. Nodes and edges may repeat across pages (completeness, not disjointness), so dedupe by id as you go.
const seen = new Set<string>();
const frames = await client.graphFetchFrames({
limit: 200,
cursor: new Uint8Array(),
includeStatements: false,
includeMemories: true,
includeTombstoned: false,
actAs: null,
});
for (const f of frames) {
for (const node of f.nodes) {
if (!seen.has(node.id)) { seen.add(node.id); render(node); }
}
if (f.isFinal) break; // an empty nextCursor also means the export is complete
}seen = set()
frames = client.graph_fetch_frames(
limit=200, cursor=b"", include_statements=False,
include_memories=True, include_tombstoned=False,
)
for f in frames:
for node in f.nodes:
if node.id not in seen:
seen.add(node.id)
render(node)
if f.is_final:
breakuse std::collections::HashSet;
use brain_db_sdk::wire::types::GraphFetchRequest;
let mut seen: HashSet<[u8; 16]> = HashSet::new();
let frames = client
.graph_fetch_frames(&GraphFetchRequest {
limit: 200,
cursor: Vec::new(),
include_statements: false,
include_memories: true,
include_memory_edges: false, // requires include_memories
include_tombstoned: false,
act_as: None,
})
.await?;
for f in &frames {
for node in &f.nodes {
if seen.insert(node.id.clone()) {
render(node); // first time we've seen this id
}
}
if f.is_final {
break; // an empty next_cursor also means the export is complete
}
}For a truly long-lived stream (not a one-shot verb response), use subscribe. It opens a durable change feed you drain and tear down explicitly.
const sub = await client.subscribe({
filter: myFilter,
includeHistory: false,
fromLsn: null,
maxInflight: 64,
});
for await (const event of sub) {
handle(event);
}
await sub.unsubscribe();When to use frames
Use the plain convenience verb (recall, graphFetch, memoryList) for everything that fits in memory — it is simpler and returns a flat result. Reach for the *Frames variant when you want to:
- render partial results before the whole response arrives;
- show progress from
cumulativeCount/estimatedRemaining; - page an unbounded export by following
nextCursoryourself.
Over HTTP there is no streaming. If you need incremental delivery or live change feeds, use the wire client. See Wire client.
Was this page helpful?
Building context for prompts
Brain has no build-context verb — you assemble a prompt block yourself from recall output. This shows the pattern, with token budgeting and provenance.
Idempotency
Two mechanisms keep writes safe to repeat — request_id idempotency (same id, same response) and content dedup (BLAKE3 fingerprint, was_deduplicated).