Brainby arc-labs/docs
Guide

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.

Prerequisites
  • The wire client BrainClient (streaming is wire-only — see below).
  • A connection token; see Wire client.
  • Familiarity with Recall and Pagination.
What you'll build

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).

ConvenienceStreamed framesWhat 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 / listSchemaseach has a *Frames variantitems + 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:
        break
use 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();

See Typed graph → Subscriptions.

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 nextCursor yourself.

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?

On this page