Skip to content

Replay answers and history

View Markdown

The quickstart runs its path query at the commit the search read. It passes as_of_commit_seq: found.served_at_seq, so search and path read one version of the data.

This page shows how to store that commit with an answer and replay the query later. It also shows how to find the commit where an answer changed.

The last sections cover pinned reads, valid time and how long history is kept. Commits are explained in core concepts.

  • Each write is a commit with a number, commit_seq. A later write never changes an earlier commit.
  • Each read reports the commit it read. SPARQL text returns snapshot.served_at_seq, and search returns served_at_seq.
  • A query with as_of_commit_seq reads the graph as it was at that commit. It returns the same rows every time while the commit is retained.
  • A pinned read is exact. The platform never answers from a nearby commit instead.

Replay reproduces the evidence behind an answer. The generated text needs more. To reproduce it, also store the prompt, the model version and its settings.

The steps on this page continue the quickstart on the same support graph. Run the quickstart first.

Store these with each answer:

Item Why
The graph name A commit number belongs to one graph.
The question and the search hits Search takes no commit pin, so store its hits.
The query text and its options A replay runs the same query.
The rows You can compare a replay with what the agent saw.
The commit, served_at_seq A replay reads the graph at this commit.

Add this snippet to the end of your quickstart file and run the file once. It writes answer.json on the first run only, so later runs keep the first answer.

import { existsSync, writeFileSync } from "node:fs";
if (!existsSync("answer.json")) {
const answer = {
graph: "support",
question: "users cannot sign in after the upgrade",
hits: found.hits.map((hit) => ({ iri: hit.iri, label: hit.label, score: hit.score })),
query,
rows,
commit: found.served_at_seq,
};
writeFileSync("answer.json", JSON.stringify(answer, null, 2));
}

On a new graph, commit is 1.

Suppose the release notes were wrong. The fix for AUTH-112 shipped in 3.2.1. Two writes correct the graph. The first retracts the old FIXED_IN link. The second adds the new one.

Create a new file, correct.mts or correct.py, and run it once:

import { LbbClient } from "@littlebigbrain/client";
const lbb = new LbbClient({
baseUrl: process.env.LBB_URL!,
apiKey: process.env.LBB_API_KEY!,
graph: "support",
});
const auth = { type: "Bug", key: "AUTH-112", name: "session token expiry" };
const r32 = { type: "Release", key: "3.2", name: "Release 3.2" };
const r321 = { type: "Release", key: "3.2.1", name: "Release 3.2.1" };
await lbb.graph("support").retract(
{ edges: [{ source: auth, relation: "FIXED_IN", target: r32 }] },
{ idempotencyKey: "fix-moved-1" },
);
const corrected = await lbb.graph("support").facts.create(
{ triplets: [{ source: auth, relation: "FIXED_IN", target: r321, evidence: "Release notes 3.2.1" }] },
{ idempotencyKey: "fix-moved-2" },
);
await lbb.waitForPublished(corrected.commit_seq);
console.log("corrected at commit", corrected.commit_seq);

After the quickstart, the SDK examples print corrected at commit 3. The retraction is commit 2. A retraction is a new commit, and commit 1 still holds the old link.

Replay reads answer.json and runs the stored query twice. The first run is pinned to the stored commit. The second reads the latest published commit.

Create replay.mts or replay.py:

import { readFileSync } from "node:fs";
import { LbbClient, parseSparqlResults } from "@littlebigbrain/client";
const lbb = new LbbClient({
baseUrl: process.env.LBB_URL!,
apiKey: process.env.LBB_API_KEY!,
graph: "support",
});
const answer = JSON.parse(readFileSync("answer.json", "utf8"));
const then = await lbb.sparqlText({ query: answer.query, as_of_commit_seq: answer.commit });
const now = await lbb.sparqlText({ query: answer.query });
console.log("commit", then.snapshot?.served_at_seq, parseSparqlResults(then).rows);
console.log("commit", now.snapshot?.served_at_seq, parseSparqlResults(now).rows);

The TypeScript example prints this:

commit 1 [ { bug: 'session token expiry', release: 'Release 3.2' } ]
commit 3 [ { bug: 'session token expiry', release: 'Release 3.2.1' } ]

The replay still returns Release 3.2, the answer the agent gave. The latest data says Release 3.2.1. Both results name their commit.

The Python SDK’s sparql() method takes no as_of_commit_seq. Use raw_request for a pinned read, as above.

4. Find the commit where the answer changed

Section titled “4. Find the commit where the answer changed”

Run the stored query at each commit from the stored one to the latest. Add this to the end of replay.mts or replay.py:

const { published_seq } = await lbb.publicationStatus();
for (let seq = answer.commit; seq <= published_seq; seq++) {
const res = await lbb.sparqlText({ query: answer.query, as_of_commit_seq: seq });
const releases = parseSparqlResults(res).rows.map((row) => row.release);
console.log(seq, releases.join(", ") || "no rows");
}

Each example prints one line per commit:

1 Release 3.2
2 no rows
3 Release 3.2.1

Commit 2 removed the old link, and commit 3 added the new one. cURL prints nothing after the 2.

This runs one query per commit. For a long history, compare the stored commit with the latest first. Then halve the range until you find the commit.

Surface How to pin Notes
POST /v1/query/sparql-text as_of_commit_seq in the JSON body TypeScript: sparqlText({ query, as_of_commit_seq }). Python: raw_request.
GET or POST /sparql ?as_of_commit_seq= For standard SPARQL tools.
POST /v1/query/sparql as_of_commit_seq in the JSON body The structured JSON query.
GET /v1/graph/entity ?as_of_commit_seq= One JSON record with its links. TypeScript: entityDetail({ type, key, asOfCommitSeq }).
POST /v1/search No pin Store the hits with the answer.

A pin works the same on a graph loaded from RDF. GET /v1/graph/entity reads JSON records only. On an RDF-native graph, query the subject IRI with SPARQL instead.

A pinned response describes the commit in snapshot:

Field Meaning
as_of_commit_seq The pin you sent.
served_at_seq The commit the read used. It equals the pin.
commit_seq The newest commit of the graph.
stale, stale_reason true and eventual_consistency when the pin is older than the newest commit. This is expected for a replay.

These requests fail:

Request Response
A pin above the newest commit 400 invalid_request, as-of commit 9 is beyond graph head 3
min_indexed_seq above the pin 400 invalid_request
A commit that is not retained 400 historical_generation_unavailable. Retrying does not help.
/sparql?as_of_valid_time= 400 historical_generation_unavailable. See Valid time.

If the graph has SHACL shapes, store the validation state with the answer. GET /v1/ontology/conformance returns the report of the latest validated commit:

Terminal window
curl --fail-with-body -sS "$LBB_URL/v1/ontology/conformance?graph=support" \
-H "Authorization: Bearer $LBB_API_KEY"

Store conforms, result_count, validated_at_seq and ontology_version. Store shapes_version too when the graph has published shapes.

The report covers the commit in validated_at_seq. A request never validates a past commit. With consistency=strong, the route returns a retryable 429 strong_read_pending until the report covers the newest commit.

A pinned read works while the commit is retained. A commit that is no longer retained fails with historical_generation_unavailable. The read never returns a nearby commit instead. Deleting the graph deletes its history.

Keep your own copy of evidence that must outlive the graph. That is the question, the query, the rows and the commit. The answer.json from step 1 is such a copy.

A commit pin answers one question: what did the graph record at commit N? A second question is when a fact was true in the world. That is valid time.

A link can carry valid_time with a start and an end:

{
"source": {"type": "Bug", "key": "AUTH-112", "name": "session token expiry"},
"relation": "FIXED_IN",
"target": {"type": "Release", "key": "3.2", "name": "Release 3.2"},
"valid_time": {"start": "2026-09-02T00:00:00Z"}
}

Hosted graphs store valid_time but do not query by it. SPARQL and the record read take commit pins only.

To ask what was true on a date, store the dates as date_time fields. Then filter on them in SPARQL. Build with JSON records shows a date_time field.