Replay answers and history
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.
How replay works
Section titled “How replay works”- 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 returnsserved_at_seq. - A query with
as_of_commit_seqreads 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.
1. Store the answer with its commit
Section titled “1. Store the answer with its commit”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));}if not os.path.exists("answer.json"): answer = { "graph": "support", "question": "users cannot sign in after the upgrade", "hits": [{"iri": h["iri"], "label": h["label"], "score": h["score"]} for h in found["hits"]], "query": query, "rows": rows, "commit": found["served_at_seq"], } with open("answer.json", "w") as f: json.dump(answer, f, indent=2)Run this in the directory that holds found.json from the quickstart. It
stores the query as path.rq, then the answer without its rows.
cat > path.rq <<EOFPREFIX dr: <https://littlebigbrain.com/r/>PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>SELECT ?bug ?release WHERE { <$(jq -r '.hits[0].iri' found.json)> dr:reported_as ?b . ?b rdfs:label ?bug ; dr:fixed_in ?r . ?r rdfs:label ?release .}EOF
[ -f answer.json ] || jq --rawfile query path.rq '{ graph: "support", question: "users cannot sign in after the upgrade", hits: [.hits[] | {iri: .iri, label: .label, score: .score}], query: $query, commit: .served_at_seq }' found.json > answer.jsonOn a new graph, commit is 1.
2. Correct the data
Section titled “2. Correct the data”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);import os
from lbb import LbbClient
lbb = LbbClient(os.environ["LBB_URL"], api_key=os.environ["LBB_API_KEY"], graph="support")graph = lbb.graph("support")auth = {"type": "Bug", "key": "AUTH-112", "name": "session token expiry"}r32 = {"type": "Release", "key": "3.2", "name": "Release 3.2"}r321 = {"type": "Release", "key": "3.2.1", "name": "Release 3.2.1"}
graph.retract( {"edges": [{"source": auth, "relation": "FIXED_IN", "target": r32}]}, idempotency_key="fix-moved-1",)corrected = graph.facts.create( {"triplets": [{"source": auth, "relation": "FIXED_IN", "target": r321, "evidence": "Release notes 3.2.1"}]}, idempotency_key="fix-moved-2",)graph.wait_for_published(corrected["commit_seq"])print("corrected at commit", corrected["commit_seq"])AUTH='{"type": "Bug", "key": "AUTH-112", "name": "session token expiry"}'
curl --fail-with-body -sS -X POST "$LBB_URL/v1/graph/retract?graph=support" \ -H "Authorization: Bearer $LBB_API_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: fix-moved-1" \ -d "{\"edges\": [{\"source\": $AUTH, \"relation\": \"FIXED_IN\", \"target\": {\"type\": \"Release\", \"key\": \"3.2\", \"name\": \"Release 3.2\"}}]}"
curl --fail-with-body -sS -X POST "$LBB_URL/v1/graph/commit?graph=support" \ -H "Authorization: Bearer $LBB_API_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: fix-moved-2" \ -d "{\"triplets\": [{\"source\": $AUTH, \"relation\": \"FIXED_IN\", \"target\": {\"type\": \"Release\", \"key\": \"3.2.1\", \"name\": \"Release 3.2.1\"}, \"evidence\": \"Release notes 3.2.1\"}]}"
until curl -sS "$LBB_URL/v1/graph/publication-status?graph=support" \ -H "Authorization: Bearer $LBB_API_KEY" | grep -q '"state": *"current"'; do sleep 1; doneAfter 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.
3. Replay the answer
Section titled “3. Replay the answer”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);import jsonimport os
from lbb import LbbClient, SparqlResults
lbb = LbbClient(os.environ["LBB_URL"], api_key=os.environ["LBB_API_KEY"], graph="support")with open("answer.json") as f: answer = json.load(f)
def run(body): resp = lbb.raw_request("POST", "/v1/query/sparql-text", params={"graph": answer["graph"]}, body=body) return resp.data["snapshot"]["served_at_seq"], SparqlResults.from_envelope(resp.data).rows()
print("commit", *run({"query": answer["query"], "as_of_commit_seq": answer["commit"]}))print("commit", *run({"query": answer["query"]}))for body in "$(jq '{query, as_of_commit_seq: .commit}' answer.json)" "$(jq '{query}' answer.json)"; do curl --fail-with-body -sS -X POST "$LBB_URL/v1/query/sparql-text?graph=support" \ -H "Authorization: Bearer $LBB_API_KEY" -H "Content-Type: application/json" \ -d "$body" \ | jq -c '{commit: .snapshot.served_at_seq, rows: [.results | fromjson | .results.bindings[] | {bug: .bug.value, release: .release.value}]}'doneThe 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");}published = lbb.publication_status()["published_seq"]for seq in range(answer["commit"], published + 1): _, rows = run({"query": answer["query"], "as_of_commit_seq": seq}) print(seq, ", ".join(row["release"] for row in rows) or "no rows")LATEST=$(curl -sS "$LBB_URL/v1/graph/publication-status?graph=support" \ -H "Authorization: Bearer $LBB_API_KEY" | jq '.published_seq')
for seq in $(seq "$(jq '.commit' answer.json)" "$LATEST"); do printf '%s ' "$seq" curl --fail-with-body -sS -X POST "$LBB_URL/v1/query/sparql-text?graph=support" \ -H "Authorization: Bearer $LBB_API_KEY" -H "Content-Type: application/json" \ -d "$(jq --argjson seq "$seq" '{query, as_of_commit_seq: $seq}' answer.json)" \ | jq -r '[.results | fromjson | .results.bindings[].release.value] | join(", ")'doneEach example prints one line per commit:
1 Release 3.22 no rows3 Release 3.2.1Commit 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.
Where a commit pin works
Section titled “Where a commit pin works”| 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. |
Record the validation state
Section titled “Record the validation state”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:
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.
How long history is kept
Section titled “How long history is kept”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.
Valid time
Section titled “Valid time”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.
Related
Section titled “Related”- Quickstart: the search and path query this page replays.
- Load RDF data: pins work the same on RDF data.
- Query and validate: SPARQL, SHACL shapes and the conformance report.
- Reasoning & inference: combine
entailmentwith a commit pin. - HTTP API: every route and parameter.