For engineers & scientists

Give your agents a record they can't flatter.

One Python package, no dependencies. You decide who reviews, whose runs count as recorded, and what the work costs; HypGraph enforces it, checks every claim as it's written, and derives what's actually been shown.

Install

pip install hypgraph            # engine, CLI, JSON API, Atlas — standard library only
pip install "hypgraph[mcp]"     # + the MCP server (FastMCP)

Python 3.10+. One SQLite file per record (Graph("research.hypgraph")), or Graph() in memory for tests and notebooks.

The model

Typed nodes — question, hypothesis, experiment, run, result, claim, source, review, decision — joined by typed links whose endpoints are enforced (supports, challenges, requires, tests, produced, replicates, based_on …). Every table is append-only; every write is an event in a SHA-256 chain. Nothing about standing is stored. Each read derives:

  • review — checked / unchecked / contested / stale, from reviewers' verdicts on the current version;
  • risk — fallen premises upstream (AT RISK), decisions whose basis moved (REVISIT), every support fallen (UNSUPPORTED);
  • independent lines — supports that share no run, data artifact or source;
  • evidence — the strongest line: agent-reported → instrument-recorded → reproduced → separate implementation → new data → external source.

The full rules are in the record format; hypgraph schema --json prints the vocabulary.

Policy & roles

from hypgraph import Graph, Policy

POLICY = Policy(
    reviewers={"critic", "stats-reviewer"},   # whose verdicts count (an investigation can add its own)
    instrument="runner",                      # the only author whose kind=lab runs are "recorded" evidence
    operator="operator",                      # revises anything, settles disputes
    importer="importer",                      # brings legacy records in, with their original dates
)
g = Graph("research.hypgraph", policy=POLICY)
Authentication is yours. HypGraph takes author= at its word. Wherever an agent reaches the graph, fix its author on your side and refuse POLICY.reserved — that's what the bundled MCP server and HTTP API do.

A pattern that works for a team of agents:

role in your systemin HypGraph
worker agents (researchers, analysts)each its own author name; their claims are checked; their reviews are comments
critic / reviewer agentsreviewers; their verdicts set standing; never on their own nodes
the job runner / sandbox that executes codeinstrument: files runs with hashes, so results count as recorded
you, or an orchestrator acting for youoperator

Connecting agents

MCP

One server process per agent identity — the author is fixed at start:

hypgraph --db research.hypgraph --as critic mcp                                # stdio
hypgraph --db research.hypgraph --as worker-1 mcp --transport http --port 8766 # streamable HTTP

Or build it in-process (custom policy, your own auth): hypgraph.mcp.build(graph, author="worker-1") returns the FastMCP server.

Agent Skill

hypgraph skill --install writes .claude/skills/hypgraph/SKILL.md: the method, the refusals and their fixes, through the CLI.

Your own tools

Any framework that takes Python functions as tools (Claude Agent SDK, LangGraph, OpenAI Agents …) — return the refusal text so the model can act on it:

from hypgraph import GraphError

def add_claim(statement: str, scope: str, result: str, measurements: list[dict], why: str) -> str:
    """Add a claim supported by a result; every number in the statement must be bound in measurements."""
    try:
        n = g.create(author=AGENT_NAME, type="claim", inv=INV,
                     fields={"statement": statement, "scope": scope, "measurements": measurements},
                     links=[{"rel": "supports", "from": result, "why": why}])
        return f"Added {n['code']}: {n['state']['sentence']}"
    except GraphError as e:
        return f"Refused — fix this and retry:\n{e}"

Put g.report(inv)["markdown"] or the MCP overview text in the agent's context at the start of a task, so it reads what's established before re-deriving it.

Recording runs

from hypgraph import file_sha256

data, code = file_sha256("evals/heldout.jsonl"), file_sha256("eval.py")
a_data = g.create(author="runner", type="artifact", inv=INV, fields={**data, "format": "jsonl"})["id"]
a_code = g.create(author="runner", type="artifact", inv=INV, fields={**code, "format": "py"})["id"]
run = g.create(author="runner", type="run", inv=INV, fields={"kind": "lab", "state": "succeeded", "seed": 7},
               links=[{"rel": "uses", "to": a_data, "data": {"role": "data"}},
                      {"rel": "uses", "to": a_code, "data": {"role": "code"}}])["id"]
res = g.create(author="runner", type="result", inv=INV, links=[{"rel": "produced", "from": run}],
               fields={"summary": "Held-out accuracy, fine-tune vs baseline.",
                       "values": {"finetune": {"k": 1624, "n": 2000}, "baseline": {"k": 1568, "n": 2000}}})["id"]

Artifacts are content-addressed (same bytes, one node), so two results computed from the same data merge into one line of evidence automatically. A rerun links replicates with data.differs (new_seed, new_impl, new_data …) and raises the evidence profile.

Recipe: eval reports that hold up

  1. Preregister. An experiment with tests links to each hypothesis ("fine-tune is better", "no difference"), each with data.expected, before running.
  2. Record the run as above; put counts in result values as {k, n}, not just percentages.
  3. Claim with measurements. "1,624 of 2,000 against 1,568" binds; "5 points better" with no result field for 5 is refused.
  4. "No difference" needs power. Plant a known effect (inject a 2-point gap into a copy of the predictions), show the comparison detects it at this sample size, and mark that result power: "demonstrated". Without it, file the claim as inconclusive — the record insists.
  5. Review. A critic agent re-runs the scorer on the stored predictions; its pass or fail pins to this version of the claim.
  6. Report. g.report(inv)["markdown"] is the facts table for the top of the write-up; it can't restate a withdrawn claim.

Cost

def spend(inv, tasks, since=None):            # your meter: model calls per task id
    rows = ledger.calls(task_ids=tasks, since=since)
    return {"usd": sum(r.usd for r in rows), "calls": len(rows), "input_tokens": ..., "cached_tokens": ...,
            "output_tokens": ..., "by_task": [{"task_id": t, "usd": ...} for t in tasks]}

g = Graph("research.hypgraph", policy=POLICY, spend=spend)
with g.writing_for(task_id):                    # events carry the task
    g.create(...)
g.cost(inv)        # $ for the investigation, per node, per verdict, and since the record last moved

HTTP & the Atlas

hypgraph serve --db research.hypgraph            # Atlas + JSON API on 127.0.0.1:8765, read-only
hypgraph serve --db research.hypgraph --writable # + POST /api/graph/write

In your own app (FastAPI shown; any framework works):

from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from hypgraph.server import dispatch
from hypgraph.ui import static_dir

app = FastAPI()
app.mount("/atlas", StaticFiles(directory=static_dir(), html=True))

@app.get("/api/graph/{path:path}")
def graph_api(path: str, request: Request):
    status, body = dispatch(g, "GET", "/" + path, dict(request.query_params))
    return body          # (set the status code too)

The Atlas itself is framework-free: HypAtlas.mount(el, {source: HypAtlas.HttpSource('/api/graph')}), or HypAtlas.BundleSource(bundle) with no server at all. Theme it with --hg-* CSS variables.

Bundles in CI

hypgraph export -o record.bundle.json     # every event as the exact string that was hashed
hypgraph verify record.bundle.json        # exit 1 if anything was edited

Publish the bundle's head hash somewhere you don't control (a tag, a paper, a ticket) when a result ships: a later record that passes through that hash can't have rewritten anything before it. Reviewers can check a bundle at /verify without installing anything.

Lint the write-up

Most hype lives in the final report, not the record. hypgraph check reads markdown against the record and exits 1 on errors, so the summary an agent hands over can't contradict what was actually shown:

hypgraph check summary.md              # errors fail
hypgraph check --strict summary.md     # warnings (unsourced numbers …) fail too
errorcatches
REPORT_OVERSTATES_STANDING"established" / "confirmed" about a claim the record has as unchecked, contested or at risk
REPORT_CITES_WITHDRAWNciting a withdrawn claim as if it stood
REPORT_UNKNOWN_CODEa citation to a claim or result that doesn't exist
REPORT_ABSOLUTE_UNBACKED · REPORT_ABSENCE_UNBACKED · REPORT_SIGNIFICANCE_UNBACKEDnever / no effect / significantly with nothing cited behind them

Warnings: numbers no result contains or implies, disputes left out, superseded citations, uncited causal language. Code blocks, inline code, URLs, quoted passages, negations ("cannot be proven") and instructions ("never use the eval set") are not treated as claims — calibrated on two dozen real write-ups from PegasusDMZ against its live record. In Python: g.check_text(text, inv); over HTTP: POST /api/graph/check; for agents: the MCP tool check_report.

The refusal log

Every refused claim is logged — author, statement, codes — and marked fixed when the same author comes back with the honest version. It's the number nobody else can show you: how often your agents tried to overclaim, which rules caught them, and which ones they quietly abandoned.

hypgraph refusals                      # per author and rule, fixed vs open
hypgraph refusals --author analyst     # one agent's refusals in full

Append-only with its own hash chain (g.verify_refusals()), separate from the record's. In Python: g.refusal_report(inv); HTTP GET /api/graph/refusals; MCP refusal_log; and an "Overclaims caught" card in the Atlas's Work view.

Performance

nodesderive all standingread one nodewriteverify chain
1,0000.02 s0.1 ms~6 ms0.02 s
3,3000.07 s0.2 ms~7 ms0.07 s
10,0000.09 s0.3 ms~10 ms0.18 s

Pure Python, measured on a laptop. A write costs what it touches: snapshots share untouched data, and cached standing survives for every node the write can't reach (checked against a full rebuild by a randomized test suite). Most of a write is SQLite committing to disk.

Reference deployment

HypGraph is the research graph of PegasusDMZ — a governed team of a few dozen AI agents doing long investigations behind an egress firewall. There, the graph lives in the proxy; agents reach it only through a broker that fixes their author; the lab's job runner is the instrument; critic agents form a review board; a heartbeat routes unchecked claims to reviewers and fallen premises back to their authors; and spend comes from the proxy's metered ledger. Its Atlas is this package's.