Skip to content

Recall & Retrieval

Reading memory back is three altitudes — and every result is traceable to where it came from. This guide is the usage side; for the engine internals of the cascade see Pipelines: Recall.

Three altitudes

You want… Use Returns
Relevant context for a prompt agent.recall(q) ContextBundle (ranked items)
A finished answer agent.answer(q) Answer (generated text + grounding)
Exact nodes you specify agent.query(cypher) Vec<Record> (raw rows)
let bundle = agent.recall("what's the deadline?").await?;        // ranked context
let answer = agent.answer("when is the deadline?").await?;       // needs .llm(...)
let rows   = agent.query("MATCH (m:Message) RETURN m.content").await?;  // read-only

recall/answer/query each have an _in variant taking a Scope to confine the read to a session, participant, or time window.

What a recalled item carries

Each RecallItem answers two orthogonal questions: what it is (kind) and where it came from (sources).

for item in &bundle.items {
    println!("[{:?}{}] {}",
        item.kind,
        if item.kind.is_derived() { " derived" } else { "" },
        item.content);
    for src in &item.sources {
        match src {
            RecallSource::Message { message_id, .. }            => println!("   ← {message_id}"),
            RecallSource::Attachment { artifact_id, message_id, .. } => println!("   ← {artifact_id} (in {message_id})"),
            RecallSource::Document { artifact_id, .. }          => println!("   ← {artifact_id}"),
        }
    }
}
  • kindChunk / Observation / Fact / Procedure / Topic / Episode / Message. kind.is_derived() distinguishes synthesized memory (Fact/Procedure/Topic/Observation) from raw input (Chunk/Message) — "the system concluded X" vs "alice literally said X."
  • sources — the lineage. A chunk has one source; a derived Fact lists the messages/attachments its supporting observations came from. item.primary_source() returns the first.

Provenance is captured, not inferred

Sources are the graph's own edges (HAS_CHUNK, ATTACHED_TO, OBSERVED_IN, SUPPORTED_BY) projected onto the result — so a chunk from a shared document reports the Attachment it came from, and the message that shared it.

Cited answers

agent.answer(...) returns an Answer that carries the grounding context, so answers are citable without a second call — attribution is pure data:

let ans = agent.answer("when is the deadline?").await?;
println!("{}", ans.text);
for src in ans.citations() {                 // deduped grounding sources
    // "per spec.pdf, shared in m-att" / "alice said in m-12"
}
println!("(model {:?}, {} out-tok)", ans.model, ans.output_tokens.unwrap_or(0));

citations() reports the grounding context — what the model was shown — not model-attributed citations. ans.context is the full ranked bundle (items, coverage, total_tokens).

Dereferencing a source — agent.data()

sources hand back ids; agent.data() turns them into content. Recall is the index lookup; data is the document fetch — keep them separate so you can cite over many sources but fetch only the few you open.

// the turn a source points at
let msg = agent.data().message("m-12").await?;          // Option<MessageView>
// the attachment/document: metadata + reassembled text
let art = agent.data().artifact("spec.pdf").await?;     // Option<ArtifactView>
// the original bytes (e.g. to re-open the PDF)
let bytes = agent.data().artifact_bytes("spec.pdf").await?;  // Option<Vec<u8>>

MessageView gives { sender_id, content, timestamp, session_id, addressed_to, attachments }; ArtifactView gives { kind, mime, path, text, attached_to_message }. Unknown ids return Ok(None).

The id is the one observe/ingest handed you:

let result = session.observe(Turn::new("alice", "see attached").attach(IngestSource::path("spec.pdf"))).await?;
if let IngestOutcome::Artifact(a) = &result.attachments[0] {
    let view = agent.data().artifact(&a.artifact_id).await?;   // round-trips
}

Scoping

Confine any read to a session, participant, or time window with Scope + the _in variants:

use chrono::{Duration, Utc};

let scope = Scope::default()
    .sessions(["chat-42"])
    .participants(["alice"])
    .since(Utc::now() - Duration::days(7));

let bundle = agent.recall_in("deadline", scope.clone()).await?;
let rows   = agent.query_in("MATCH (n:Message) WHERE id(n) IN $allow RETURN n", &scope).await?;

recall_in/answer_in apply the scope (and viewer visibility) for you; query_in binds the in-scope node-ids as the $allow list for your Cypher to filter on. Build the instance with .scope_to_agent() to default every read to the agent's own visibility.

See also

Pipelines: Recall

The 3-phase cascade, coverage gates, and tier weights behind recall.

API Reference

Full signatures for recall/answer/query, Answer, Data, and the recall types.