{
  "markdown": "# TGMS — Agent-Native Bi-Temporal Graph Management System\n\n[![CI](https://github.com/zxf-work/tgms/actions/workflows/ci.yml/badge.svg)](../../actions)\n[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)\n[![Coverage: temporal/ 97%](https://img.shields.io/badge/coverage_(temporal)-97%25-brightgreen.svg)](#correctness)\n\n**A temporal graph database whose query surface is built for LLM agents —\nand whose answers can be audited claim by claim.**\n\n**Project page & blog:** https://zxf-work.github.io/tgms/ · **Paper:** [paper/main.pdf](paper/main.pdf)\n\nLLM agents are unreliable at exactly the things temporal graph analytics\nrequires: arithmetic, identifiers, and asserting only what the evidence\nshows. TGMS's answer is architectural — give the model **no opportunity**\nto do any of them:\n\n- a **bi-temporal property graph** (valid time × transaction time) that\n  distinguishes *evolution* (\"the edge ended\") from *correction* (\"we were\n  wrong\"), so agents can answer *\"what did we believe on March 1?\"* — a\n  question latest-state snapshots and the RAG configurations we evaluated\n  cannot express. Bi-temporality itself is inherited, not invented here —\n  it has a four-decade literature, a place in SQL:2011, and production\n  databases built around it. We measure against the clearest of those,\n  [XTDB](https://xtdb.com): fed the same operation stream, the two systems\n  agree on believed state at 400 of 400 probe points, with TGMS 3.9–4.7×\n  faster at correction-heavy ingest on 23–27× less disk\n  ([the head-to-head](https://zxf-work.github.io/tgms/blog/the-competitor-agrees.html));\n- and because a belief can be corrected *after* you've already acted on an\n  answer, TGMS now tells you when that happened: **`tgms trace check`**\n  reads a saved answer's dependency scope against the event log — no\n  recompute, no store lock required — and returns `FRESH` /\n  `POSSIBLY_STALE` / `UNDECIDABLE`, sound in the direction that matters (it\n  never calls a stale answer fresh). Measured across two injection\n  campaigns, 6,978 trials: **0 false-fresh** verdicts of 898 changed\n  answers, where the obvious cheap check — \"did the correction touch a row\n  in the stored result?\" — is wrong on **47.4%** of the same trials;\n- and a saved result you want to *keep*, not just check, can now maintain\n  itself: **`tgms artifact register/check/refresh`** turns it into a named,\n  generation-numbered artifact — refresh recomputes only what you ask, the\n  old generation stays byte-identical on disk, and a refresh propagates one\n  hop to whatever else was built on top of it, even when that dependent's\n  own scope was never touched. Measured across the M5 maintenance campaign:\n  **0 false-fresh** in **37,371** trials, **0 false-safe** over 5,867\n  propagation decisions (**99.0%** resolved without recomputing anything),\n  and a **600/600** pinned-answer exemption;\n- **15 verified temporal operators** (reachability over time-respecting\n  paths, δ-motifs, snapshot diffs, burst detection, interval joins, grouped\n  aggregation over edge events, and the belief log itself) — typed,\n  deterministic, bounded,\n  cost-guarded, exposed as tools (MCP or in-process); identifiers must come\n  from a resolver, arithmetic from a `compute` operator;\n- a **Planner–Executor–Verifier** loop: the LLM only plans and reports;\n  plans are statically validated (including a grounding rule that makes\n  fabricated identifiers impossible and output-field contracts that reject\n  invented result paths), executed deterministically with content-addressed\n  traces, and every claim in the written answer is **machine-checked\n  against the trace that produced it** — including truncation taint, so\n  \"correct arithmetic over incomplete evidence\" is caught too;\n- a **purpose-built native storage engine** (Rust, PyO3): bi-temporal\n  columnar segments, a temporal-CSR traversal index, group commit, and a\n  single-writer / many-reader concurrency mode — 24.6 bytes per edge\n  version, versus 78.4 on ClickHouse and 549.7 on PostgreSQL for the same\n  1M-event log.\n\n## Quickstart\n\n```bash\npip install tgms\ntgms demo\n```\n\nNo GPU, no API key, no dataset download. `tgms demo` builds a small store of\nits own in a temp directory and runs the arc every TGMS answer follows: what\nthe graph currently believes, what it believed *before* a correction landed,\nand the trace that backs both claims up. Clean environment to first temporal\nresult: under 5 minutes.\n\nOnce you want your own graph data, the native test suite, the MCP server, or\nan agent wired to a real LLM, see [Full setup](#full-setup-from-source)\nbelow — this quickstart is deliberately the smallest possible first step,\nnot a tour of the operator surface.\n\nNext steps, in the order most people need them:\n[bring your own temporal graph data](docs/tutorials/bring-your-own-data.md) ·\n[give TGMS to an agent over MCP](docs/tutorials/agent-setup.md) ·\n[audit an answer](docs/tutorials/audit-an-answer.md) ·\n[maintain derived results](docs/tutorials/maintain-derived-results.md) ·\n[what you can rely on across versions](docs/STABILITY.md) ·\n[what's coming](docs/PUBLIC_ROADMAP.md)\n\n## Does it work?\n\nThree different questions, three different answers. All three are reported\nbecause the third is the least flattering.\n\n**1. Does the agent layer beat the alternatives?** Dev-split campaign\n(CollegeMsg, open-source models served locally on one 24 GB GPU. \"Answer\naccuracy\" is normalized typed-answer accuracy — counts and values scored\nstrictly, interval answers credited at IoU ≥ 0.5. Full receipts ship with\nthe paper and the eval records in `benchmarks/results-v1/`):\n\n| pooled answer accuracy, Qwen2.5-14B | TGMS | vector-RAG | static-graph RAG | text-to-Cypher |\n|---|---:|---:|---:|---:|\n| all task families | **0.41** | 0.09 | 0.05 | 0.18 |\n| correction probes (\"as of tt…\") | **0.67** | 0.00 | 0.00 | 0.00 |\n\n- vs static-graph RAG: **+36 points**, paired-bootstrap 95% CI [0.18, 0.59]\n- verifier fault injection: **500/500 injected false claims caught, 0 false\n  positives**; on the frozen campaign, **0 of 199 emitted answers** contained\n  an unsupported claim with gating (21 of 220 without it) — coverage is\n  199/282, so some of that is bought by declining to answer\n- accuracy tracks planner capability where baselines stay flat: **13.8% /\n  34.0% / 62.8%** at Qwen2.5 7B / 14B / 32B fp16, correction probes\n  saturating at 100% at 32B\n\n**2. Is the engine competitive?** Six systems answer one 13-query registry\n— TGMS native, TGMS-on-DuckDB, PostgreSQL, ClickHouse, Neo4j, Memgraph —\nwith **every cell hash-verified before it was timed**:\n\n| query shape | TGMS native | best other |\n|---|---:|---:|\n| temporal reachability, 200k | **14.7 ms** | 3.9–7.3 s (Memgraph, Neo4j) |\n| closed-triangle δ-motif, 200k | **28.7 ms** | 2.1–5.5 s (Memgraph, Neo4j) |\n| grouped aggregation, 200k | **14.5 ms** | 32.6 ms (ClickHouse) |\n| entity history by identity, 200k | **0.1 ms** | 0.3 ms (PostgreSQL) |\n| whole-window bucketed count, 10M | 84.7 ms | **37.9 ms** (ClickHouse) |\n\nThe last row is the one we cannot close: ClickHouse keeps a factor of 2.2\non whole-window aggregation at both 1M and 10M, and it is a constant of the\nshape rather than something that grows with scale. Three rounds of\nprofiling took that gap from 12× to 2.2× and each round found our own\nimplementation rather than the workload. Single latency cells reproduce to\nabout ±20% between days, which is stated everywhere they are quoted.\n\nAt 10M events the full query suite runs inside **1.76 GB** of peak RSS,\n16 concurrent readers get **10.2×** the throughput of one, and a live\nwriter costs those readers **0–3%** of per-query latency.\n\n**3. Can it answer the questions people actually ask?** This is the honest\none, and it now has a sequel. 110 questions were written by people who saw\na plain-language description of two public datasets and **never saw the\noperator list**. Of those, 94 were expressible under the fixed 15-operator\ncatalog — 10 were expressible when the study was pre-registered. Of LDBC\nSNB's 41 read templates, **3** executed — the **operator-execution axis**\n(does a plan compile, load, admit and run at all), a lower bar than the\nstricter ECQR result-contract axis, which stood at 7 of 41 — and that\nnumber had not moved in eight sessions, because 35 of the 38 misses needed\nlabelled multi-way pattern matching: a deliberately deferred design\ndecision, not a missing operator.\n\nThat deferred decision shipped. **TGIR**, a 12-primitive compositional\ntemporal-graph IR, now runs the entire 15-operator catalog as byte-identical\nleaves and additionally *compiles* some question shapes — including\nlabelled multi-way pattern matching — into chains of those primitives. Both\naxes were forecast before TGIR was built, frozen before the first row was\nmeasured, and moved to *exactly* the predicted level: LDBC\noperator-execution coverage **3 → 24 of 41**, independent-question coverage\n**94 → 102 of 110**, delivered/predicted **29/29** on the full 52-row\nforecast (28/28 on the 51 scoreable rows — one row was excluded by name in\nthe freeze because its canonical corpus carries no corrections to find), 0\nover-deliveries, 0 misses.\n\nThe store is still good and the surface is still narrower than \"24 of 41\"\nsounds. **There is no LDBC SNB dataset in this repository.** The 21 LDBC\nrows execute against a hand-built fixture carrying LDBC's labels,\nrelationship types and multi-hop topology at a size a reviewer can read on\none screen (`scripts/build_ldbc_fixture.py`); it establishes that a plan\ncompiles, loads, admits and executes, and **nothing about scale**. The\nindependent-question axis is the one measured on real data (bitcoin-otc,\nCollegeMsg), and it's where the admission/cost-guard claim is meaningful.\nBoth instruments live in the repo (`scripts/independent_questions.py`,\n`scripts/ldbc_fit.py`), they re-run in seconds, and each capability shipped\nhas been scored against a forecast made *before* it was built —\ndelivered/predicted has now run 14/30, 4/7, 10/13, 14/16, 15/15, 4/8, 5/5,\n4/5, and **29/29**. 5/5 and 4/5 were the first forecasts made per question\nrather than in aggregate, and TGIR's 29/29 is the first forecast — made\nper row, before any row was measured — with zero misses across the whole\nuniverse.\n\n## What the operators can express\n\nFifteen operators — fourteen of them unchanged since D-044, because most of\nthe growth since v0.4.0 happened *inside* them, driven question by question\nby the study above. The newest growth happened *underneath* them instead:\n**TGIR**, a 12-primitive compositional temporal-graph IR, now runs the\nentire operator set as byte-identical leaves — same semantics, digest-\nreceipted, switchable off with `TGIR_PLAN_PATH=off` — and additionally\ncompiles some question shapes, chiefly labelled multi-way pattern matching,\ninto chains of those primitives. TGIR plans are not yet a user-facing query\nlanguage: there is no public syntax for writing one directly, and the\nfifteen operators below are still the whole interface an agent (or you)\ncalls. What changed is what happens underneath a call, and it is measured\nin the byte-reproducible record at\n[`benchmarks/tgir-v1/measured.yaml`](benchmarks/tgir-v1/measured.yaml).\n\nThe pre-existing fifteen:\n\n| capability | where it lives | what it answers |\n|---|---|---|\n| grouped aggregation | `aggregate_events` | counts and distinct counts by time bucket, rel_type, endpoint or endpoint label |\n| arithmetic | `compute` | mean/median over rows; ratio/diff/percent over two scalars — never in the LLM |\n| typed properties | `aggregate_events` | predicates and min/max/mean over an edge property, where a value participates only if its JSON type fits |\n| set operations | `compute`, `aggregate_events` | intersect/difference/union over uid lists, a cohort pre-filter, undirected and reciprocal pair modes |\n| row arithmetic and joins | `compute` | `derive` adds one computed column; `join` aligns two grouped results on a key unique on both sides |\n| ordered sequences | `aggregate_events` | longest gap between consecutive events, busiest sliding window of a given span, longest run with no gap over a threshold |\n| calendar units | `aggregate_events` | grouping by hour of day, day of week or month of year, at a fixed offset from UTC that is an argument rather than a default |\n| the belief log | `version_history` | which beliefs were revised and when — the only operator that reads the correction record rather than a state derived from it |\n\nEvery one of these is verified against the same brute-force oracle as the\noperators themselves, and every one is measured in the session that shipped\nit. What is *not* there is written down too, question by question, in the\nre-audit tables of `scripts/independent_questions.py` and\n`scripts/ldbc_fit.py` — both of which print the current blocked-capability\nboard on `report`.\n\n## Full setup (from source)\n\nEverything below builds TGMS from a checkout instead of the PyPI wheel:\nreal dataset loaders, the native-engine test suite, the MCP server, and an\nagent loop wired to an actual LLM.\n\n```bash\n# macOS note: if this repo sits in an iCloud-synced folder, keep the venv\n# outside it (iCloud sets the hidden flag on .pth files and Python 3.12+\n# silently skips them):  export UV_PROJECT_ENVIRONMENT=$HOME/.venvs/tgms\nuv sync --extra agent\nmake test                     # 271 tests: property, oracle, metamorphic, e2e\n```\n\n```bash\n# build a real store + task suite (downloads CollegeMsg from SNAP)\nmake data-collegemsg suite-collegemsg\n```\n\n```bash\n# call one verified operator — no LLM needed\nuv run tgms call temporal_reachability \\\n  '{\"src\": \"n9\", \"window\": {\"t_a\": 1082040961000000, \"t_b\": 1088000000000000}}' \\\n  --store stores/collegemsg\n```\n\n```bash\n# verifier acceptance experiment (deterministic, no LLM)\nuv run tgms eval c2 --store stores/collegemsg \\\n  --suite stores/suite-collegemsg/suite.json --mutants 500\n```\n\nWith any OpenAI-compatible LLM endpoint (e.g. `vllm serve Qwen/Qwen2.5-7B-Instruct`):\n\n```bash\nuv run tgms ask \"How many nodes can n9 reach between ... and ...?\" \\\n  --store stores/collegemsg --model openai/Qwen/Qwen2.5-7B-Instruct \\\n  --api-base http://localhost:8000/v1 --html trace.html   # auditable trace page\n```\n\n```bash\nbash scripts/run_webapp.sh    # interactive guided demo at localhost:8080\n```\n\n## Interfaces\n\n| Surface | Entry point | What it's for |\n|---|---|---|\n| Python library | `tgms.open(...)`, `Agent(store, model=…).ask(…)` | research code, notebooks |\n| MCP server | `tgms serve --store PATH` | hand the verified toolbox to any MCP-capable agent |\n| CLI | `tgms demo/ingest/synth/tasks/call/ask/bench/memory/eval/trace` | reproducibility |\n| Trace viewer | `tgms ask … --html trace.html` | *ask → answer → audit the evidence* (static, self-contained HTML) |\n| Freshness check | `tgms trace check record.json --store PATH` | *is this answer still fresh?* — `FRESH`/`POSSIBLY_STALE`/`UNDECIDABLE` against the event log, no recompute |\n| Demo GUI | `tgms webapp …` / `scripts/run_webapp.sh` | guided tour: operators → agent → tamper demo → time travel → freshness check |\n\n## Correctness\n\nEvery operator is verified against an independent brute-force oracle (500\nrandomized cases per operator; 97% line coverage in `tgms/temporal/`\nacross both backends), plus\nmetamorphic properties — diff composition and **bi-temporal immutability**:\nany result pinned to a past belief state is byte-identical before and after\nlater corrections. The same suite runs unmodified against **both backends**,\nwhich is the whole acceptance argument for the native engine: it has to\nsatisfy the same human-owned ground truth that DuckDB does.\n\n```bash\nTGMS_TEST_BACKEND=native make test    # same tests, native engine\n```\n\nThe write path is property-tested over random assert/retract/correct\ninterleavings, and the append-only event log replays into either backend\nwith identical store digests. Process rules are enforced in CI and are not\nadvisory: tests and the oracle may never share a commit with the\nimplementation they judge, and every number quoted on the project site is\nresolved from `docs/site_facts.json` at build time, so a stale figure fails\nthe build rather than the review. See [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## Layout\n\n```\ntgms/core       clock, bi-temporal data model, error taxonomy\ntgms/storage    StorageAdapter ABC, native + DuckDB backends, event log, TCSR index\ntgms/temporal   operator algebra O1–O15 + brute-force oracle\ntgms/tools      tool schemas, MCP server / ToolRouter, trace viewer, demo GUI\ntgms/agent      plan IR, planner, executor, verifier, reporter, memory\ntgms/data       dataset loaders (SHA-256 pinned) + synthetic generator\ntgms/eval       task suites, baselines, matrix harness, metrics, fault injection\ncrates/         the native engine: bi-temporal segments, TCSR, motif kernel\n```\n\nDatasets are never bundled: loaders download from source (SNAP) and pin\nSHA-256 manifests. See [docs/eval/](docs/eval/)\nfor design, positioning, measurements, and roadmap.\n\n## License\n\nApache-2.0 — see [LICENSE](LICENSE). Cite via [CITATION.cff](CITATION.cff).\n\n---\n\n<sub>mcp-name: io.github.zxf-work/tgms</sub>\n",
  "bytes": 17181,
  "sha": "92b255a192af69610d803d0a2f8eaa7aa9c849c41fcb229b01d80fb11ca92ccf",
  "repo_slug": "zxf-work/tgms",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_zxf_work_tgms_186a08ac/readme"
}