Brainby arc-labs/docs

Pagination

Cursor-based enumeration on the wire client — the flatten vs *_frames pattern, and how to page memory_list and graph_fetch.

Pagination lives on BrainClient. BrainHttpClient does not expose memory_list, graph_fetch, or the typed-graph list_* verbs; its recall takes a max_results cap and returns a single RecallResult. See Wire client. All request types live under brain_db_sdk::wire::types.

Two forms per enumeration

Every streamed verb exposes both a flattening method and a *_frames method:

FlattenRaw frames
memory_listmemory_list_frames
graph_fetchgraph_fetch_frames
list_entitieslist_entities_frames
list_statementslist_statements_frames
list_relations_fromlist_relations_from_frames
list_relations_tolist_relations_to_frames
list_schemaslist_schemas_frames
statement_historystatement_history_frames
traverse_relationstraverse_relations_frames

The flatten form is the common case: it drains every streamed frame and concatenates the items into a single Vec for you.

MemoryListRequest has no Default — spell out every field. v1 supports sort: Created (Asc/Desc); other sorts, the text filter, and the occurred axis are rejected with InvalidRequest.

use brain_db_sdk::wire::types::{
    MemoryListRequest, MemoryListSortWire, MemoryListDirWire, MemoryListTimeAxisWire,
};

let memories = client
    .memory_list(&MemoryListRequest {
        sort: MemoryListSortWire::Created,
        dir: MemoryListDirWire::Desc,
        limit: 100,                 // server validates to 1..=100
        cursor: Vec::new(),         // empty = first page
        kinds: Vec::new(),          // empty = all kinds
        include_tombstoned: false,
        time_axis: MemoryListTimeAxisWire::Created,
        from_unix_nanos: 0,         // 0 = no lower bound
        to_unix_nanos: 0,           // 0 = no upper bound
        salience_min: 0.0,
        salience_max: 1.0,
        text_contains: String::new(),
        act_as: None,
    })
    .await?;

Driving the cursor yourself

Use a *_frames method when you want to page incrementally — render a page, then fetch the next. Each frame carries next_cursor, cumulative_count, and is_final; a non-empty next_cursor means more pages remain.

let mut cursor: Vec<u8> = Vec::new(); // empty on the first page
loop {
    let frames = client
        .memory_list_frames(&MemoryListRequest {
            sort: MemoryListSortWire::Created,
            dir: MemoryListDirWire::Desc,
            limit: 50,
            cursor: cursor.clone(),
            kinds: Vec::new(),
            include_tombstoned: false,
            time_axis: MemoryListTimeAxisWire::Created,
            from_unix_nanos: 0,
            to_unix_nanos: 0,
            salience_min: 0.0,
            salience_max: 1.0,
            text_contains: String::new(),
            act_as: None,
        })
        .await?;

    for frame in &frames {
        for item in &frame.items {
            render(item);
        }
    }

    let last = frames.last().expect("at least one frame");
    if last.is_final && last.next_cursor.is_empty() {
        break;
    }
    cursor = last.next_cursor.clone(); // opaque continuation token
}

The cursor is an opaque byte string — pass it back verbatim, don't parse it. An empty cursor requests the first page; a non-empty next_cursor from a frame requests the following page.

graph_fetch

graph_fetch exports the caller's whole typed graph the same way, but pages { nodes, edges }. Its limit validates server-side to 1..=500. Nodes and edges may repeat across pages (completeness, not disjointness) — dedup by id if you need a unique set.

GraphFetchRequest has no Default either. include_memory_edges requires include_memories — setting it alone is rejected, since the edges would have no rendered endpoints.

use std::collections::HashMap;
use brain_db_sdk::wire::types::GraphFetchRequest;

let mut cursor: Vec<u8> = Vec::new();
let mut nodes = HashMap::new();
let mut edges = HashMap::new();
loop {
    let frames = client
        .graph_fetch_frames(&GraphFetchRequest {
            limit: 500,
            cursor: cursor.clone(),
            include_statements: true,
            include_memories: false,
            include_memory_edges: false,
            include_tombstoned: false,
            act_as: None,
        })
        .await?;

    for frame in &frames {
        for n in &frame.nodes {
            nodes.insert(n.id, n.clone());                        // dedup by node id
        }
        for e in &frame.edges {
            edges.insert((e.from_id, e.to_id, e.kind), e.clone()); // dedup by endpoints + kind
        }
    }

    let last = frames.last().expect("at least one frame");
    if last.is_final && last.next_cursor.is_empty() {
        break;
    }
    cursor = last.next_cursor.clone();
}

When the whole result fits in memory, the flattening graph_fetch is simpler — it drains every frame and returns the combined { nodes, edges } for you.

Streamed recall

recall streams too, but its frames carry no cursor — they terminate at EOS. The flattening recall concatenates them into one RecallAnswer; use recall_frames to see each frame's cumulative counts and estimated_remaining (and the per-stage trace when you asked for it). See recall.

Was this page helpful?

On this page