Skip to content
Night Owls.dev
Jump to a page

@nightowlsdev/graph

Adapter/Storage

A bi-temporal, provenance-anchored knowledge graph plane, push any source in and get resolved entities and relations that remember when a fact was true, when you believed it, and who told you.

What it does

The knowledge-graph plane, complementary to `@nightowlsdev/knowledge` (flat pgvector RAG, "find text like this") and thread memory: it answers "what do we know about X, who told us, when did it stop being true, and what else connects to it". `graphMigration({ dimensions })` plus the additive `graph_0002`–`graph_0005` follow-ups (`GRAPH_MIGRATIONS` is the ordered set a host ejects) create the Postgres schema, the vector dimension MUST match your embedder. `createGraph({ pool, embedder, model, ontology })` assembles the plane from seams that each ship a working default: an ontology (`defineOntology` / `defaultOntology`) classifies entity and edge types; an extractor turns a record into entities and relations (`llmExtractor` for prose, `structuredExtractor` for provider payloads that already carry the values at zero model calls, `ruleExtractor` for patterns, `composeExtractors` to stack them); `resolveEntity` matches each candidate against what is already known; and `planEdgeWrite` writes it bi-temporally, tracking when a fact was true and when it was believed, invalidating a contradicted edge instead of overwriting it. Provenance is never severed, every edge cites the episode that produced it, so `episodesFor` walks a fact back to its source text. Ingestion is two-phase: `ingest`/`sync` persists episodes cheaply, `enrich`/`drain` does the model work later (an atomic claim column lets concurrent drains take disjoint batches). Ingesters live on the `@nightowlsdev/graph/sources` subpath so a host pays only for what it wires, `textSource`, `threadSource`, `postgresTableSource`, and `connectorSource`, which page-loops any connector-plane proxy behind a hard page cap so a broken paginator cannot spin forever. Reads go through `GraphQuery` (`search`, `neighborhood`, `node`, `timeline`, `path`, `findNodes`, `episodesFor`); `graphTools(graph.query)` hands those to an agent as tools whose output is fenced untrusted, and `createGraphHandlers()` mounts the same surface as framework-agnostic Request/Response handlers. `merge` and `split` correct resolution mistakes as audited operations (merge tombstones rather than deletes, so remembered ids keep resolving), and the compaction layer summarizes communities into compiled notes and lints the graph. ⚠ NOT for metrics: `corroborate` never updates an edge's properties, so an edge carrying a NUMBER silently keeps the first value it ever saw while its corroboration count, and so its confidence, rises with every repetition, the reading gets more trusted as it gets more stale, nothing errors, and a chart built on it is wrong in the direction that looks healthy. This plane is for relationships that hold across time; a time series of values belongs in `@nightowlsdev/metrics`. Prototype status; engine-wall clean, `pg` + `zod` only, with `@nightowlsdev/core` as a peer.

Install

pnpm add @nightowlsdev/graph

Key exports

  • createGraph
  • graphMigration / GRAPH_MIGRATIONS
  • defineOntology / defaultOntology
  • llmExtractor / structuredExtractor / ruleExtractor
  • pgGraphStore
  • createQuery (GraphQuery: search / neighborhood / node / timeline / path / episodesFor)
  • graphTools / rememberTool
  • createGraphHandlers
  • textSource / threadSource / postgresTableSource / connectorSource (@nightowlsdev/graph/sources)

Usage

graph.ts
import { createGraph, GRAPH_MIGRATIONS, graphTools, defaultOntology } from "@nightowlsdev/graph";
import { textSource, connectorSource } from "@nightowlsdev/graph/sources";
import { defineAgent } from "@nightowlsdev/core";

// 1. Eject the schema (dimensions MUST match your embedder). GRAPH_MIGRATIONS is the ordered
//    graph_0001..0005 set — install all of it, never just the first migration.
export const MIGRATIONS = [/* …engine migrations… */ ...GRAPH_MIGRATIONS({ dimensions: 1536 })];

// 2. Build the plane. Every seam has a working default; override what you need.
const graph = createGraph({ pool, embedder, model, ontology: defaultOntology() });

// 3. Register a source, then pull it (or push a record straight in with graph.ingest).
const docs = await graph.registerSource({ orgId, kind: "text", externalId: "handbook", label: "Handbook" });
await graph.sync({ orgId, source: docs, adapter: textSource(items) });

// 4. Ask it things — search / neighborhood / timeline / path, provenance attached.
const facts = await graph.search(orgId, { query: "who owns billing" });

// 5. Or grant the read tools to an agent (output fenced untrusted).
const analyst = defineAgent({ slug: "analyst", skills: graphTools(graph.query), /* … */ });