Inferred Edges
AI-discovered relationships between nodes the authored graph never wired up directly — server compute, browser fetch, dashed-edge rendering, and per-tick position updates in @inferagraph/core 0.15.0.
Overview
The explicit graph schema captures relationships your authors defined. Inferred edges surface the latent ones — connections the schema didn't make explicit but that a combination of graph topology and LLM-assisted similarity can recover. The library renders them as dashed edges so users can tell authoritative connections from AI-discovered ones at a glance.
Pipeline summary: GraphIndexer.computeInferredEdges() runs offline, scores candidate pairs against embedding cosine, asks the provider to describe the surviving pairs, and writes results to a configured InferredEdgeStore. At chat / browse time a route handler serves those edges to the browser, which feeds them to SceneController and renders them as dashed lines whose endpoints track moving nodes on every simulation tick.
- Visually distinct — dashed style separates inferred edges from authored ones; toggling showInferredEdges is a host-level concern.
- Persisted — the store is the source of truth. Cosmos and in-memory impls ship today; custom backends implement a 5-method interface.
- Position-tracked — InferredEdgeMesh.updatePositions(positions) rewrites the BufferAttribute in place, no re-allocation, on every tick of the force simulation.
- Compute-flexible — cron / startup / lazy-on-request — pick the trigger that matches your data velocity.
Architecture
End-to-end data flow, server through renderer:
┌──────────────────────┐
│ DataAdapter │ authoritative graph load (any backend)
└─────────┬────────────┘
│ GraphStore (in-memory, copy-on-write)
▼
┌──────────────────────┐
│ AIEngine │ computeInferredEdges()
│ + GraphIndexer │ ├─ embed nodes (cosine top-K per node)
│ + provider.complete()│ └─ describe each surviving pair
└─────────┬────────────┘
│ write
▼
┌──────────────────────┐
│ InferredEdgeStore │ CosmosInferredEdgeStore | InMemory… | custom
└─────────┬────────────┘
│ read
▼
┌──────────────────────┐
│ Route handler │ createInferredEdgeRouteHandler(engine, opts)
│ /api/inferred-edges │ lazyCompute: true → fall back to engine on cache miss
└─────────┬────────────┘
│ HTTP GET
▼
┌──────────────────────┐
│ RemoteInferredEdge… │ browser-side InferredEdgeStore impl
│ Store │ in-memory cache, dedup in flight
└─────────┬────────────┘
│ <InferaGraph> auto-fetch effect (on showInferredEdges = true)
▼
┌──────────────────────┐
│ SceneController │ setInferredEdges(edges) ← 0.15.0 signature
│ │ applyPositions(positions) ← every tick
└─────────┬────────────┘
▼
┌──────────────────────┐
│ InferredEdgeMesh │ updatePositions(positions) — BufferAttribute mutate
└──────────────────────┘ in place, no re-allocation
Server-side setup
createInferredEdgeRouteHandler(engine, opts) returns a handler the host wires into Next.js, Express, or any Node HTTP framework. The handler reads from the engine's configured InferredEdgeStore; when lazyCompute: true, the first request without cached data triggers engine.computeInferredEdges() and persists the result before responding.
// app/api/inferred-edges/route.ts — Next.js App Router.
// engine is the shared AIEngine your app already constructs for chat / RAG.
import { createInferredEdgeRouteHandler } from '@inferagraph/core';
import { engine } from '@/lib/inferagraph';
// lazyCompute: true — first request without cached edges triggers the LLM-driven
// inference path through engine.computeInferredEdges(). Subsequent requests serve
// from the configured InferredEdgeStore. Good safety net while a nightly compute job
// catches up.
export const GET = createInferredEdgeRouteHandler(engine, {
lazyCompute: true,
});
// lazyCompute: false (default) — strict mode. The route returns whatever the store has;
// uncached pairs come back empty. Use when a cron / startup job pre-computes everything.Hosts arrange the compute
lazyCompute: true is a safety net, not a strategy. The library doesn't schedule background work. Production hosts usually combine a nightly cron job (engine.computeInferredEdges()) with lazyCompute: true so the first request on a fresh node doesn't return empty. See Compute triggering below.
Browser-side wiring
RemoteInferredEdgeStore is a browser-side implementation of the InferredEdgeStore interface that fetches from a URL with in-memory caching and in-flight dedup. Pass it to <InferaGraph> and toggle showInferredEdges — the library handles fetching, rendering, and per-tick updates.
import { InferaGraph, RemoteInferredEdgeStore } from '@inferagraph/core/react';
import { useState, useMemo } from 'react';
function Graph() {
const [showInferred, setShowInferred] = useState(false);
const [loading, setLoading] = useState(false);
// One instance per app — caches fetched edges in memory.
const inferredEdgeStore = useMemo(
() => new RemoteInferredEdgeStore('/api/inferred-edges'),
[]
);
return (
<>
<label>
<input
type="checkbox"
checked={showInferred}
onChange={(e) => setShowInferred(e.target.checked)}
/>
Show inferred edges {loading && '(loading…)'}
</label>
<InferaGraph
data={data}
inferredEdgeStore={inferredEdgeStore}
showInferredEdges={showInferred}
onInferredEdgesLoadingChange={setLoading}
/>
</>
);
}The store implements the same five-method interface as CosmosInferredEdgeStore and InMemoryInferredEdgeStore: get, getAllForNode, getAll, set, clear. Drop in a custom impl when you need a different transport (GraphQL, WebSocket, edge cache).
Per-tick position updates
The force simulation moves every node on every frame. Authored edges are GPU-buffered with indices into the node-position attribute, so they follow node movement implicitly. Inferred edges use a separate InferredEdgeMesh — distinct material, dashed stroke, separate BufferAttribute — and that buffer has to be refreshed on every tick to keep endpoints visually aligned with their moving source / target nodes.
InferredEdgeMesh.updatePositions(positions) rewrites the BufferAttribute in place — no re-allocation, no flicker, no GC pressure. The library wires this up automatically: every time SceneController.applyPositions(positions) fires (once per tick), the inferred-edge mesh receives a matching call.
Hosts don't write any of this. The wiring lives entirely inside SceneController.
Loading UX
The first fetch of inferred edges can be slow — especially if lazyCompute: true kicks off an LLM round on the server. Subscribe to the onInferredEdgesLoadingChange callback on <InferaGraph> to drive a spinner, a busy-state badge, or whatever surfaces the wait in your UI.
// Host wires the library's loading callback to a piece of React state.
const [loading, setLoading] = useState(false);
<InferaGraph
inferredEdgeStore={store}
showInferredEdges={showInferred}
onInferredEdgesLoadingChange={setLoading}
/>
{loading && <Spinner label="Loading inferred edges…" />}The callback fires twice per fetch — true when the request starts, false when it resolves (or rejects). The host owns the visual treatment; the library never renders chrome.
Persistence options
Three impls of InferredEdgeStore ship today; a fourth path is custom. Same interface, different durability and scale.
| Implementation | Package | Use when |
|---|---|---|
| InMemoryInferredEdgeStore | @inferagraph/core | Dev, tests, ephemeral single-process. Default — nothing persists across restarts. |
| CosmosInferredEdgeStore | @inferagraph/cosmosdb | Production. Partition key /sourceId; reads scoped to one source are point-reads. |
| RemoteInferredEdgeStore | @inferagraph/core | Browser only. Fetches from a host-controlled URL (typically the route handler above). |
| Custom | your code | Implement the five-method interface. Useful for SQL-backed stores, GraphQL transports, or edge caches. |
Per-package wiring details (vector index policy, container provisioning, RU costs) live in Datasources → CosmosDB.
Breaking change in 0.15.0
SceneController.setInferredEdges no longer accepts a positions argument. Position updates now flow through the existing SceneController.applyPositions(positions) per-tick hook, which fans out to the inferred-edge mesh internally.
// 0.14.x and earlier — positions had to be threaded through manually.
sceneController.setInferredEdges(edges, positions); // REMOVED in 0.15.0
// 0.15.0+ — positions flow through SceneController.applyPositions on every
// tick automatically; setInferredEdges takes only the edges.
sceneController.setInferredEdges(edges);Only consumers calling SceneController directly are affected. Hosts using <InferaGraph> or useInferaGraph() never touched this surface and don't need to change anything.
Compute triggering
The library doesn't schedule computeInferredEdges(). Hosts choose a trigger that matches data velocity.
Cron / nightly job
Recommended for stable graphs. A scheduled task calls engine.computeInferredEdges() and writes to the store. Daily / weekly is fine when authored content changes slowly. Pair with lazyCompute: true as a safety net for nodes added between runs.
On-startup script
Run once at deploy / boot. Suitable for static corpora where data changes only with code deploys. Heavy graphs need to gate this behind a guard so cold starts don't run a 30-minute inference loop on every container.
Lazy via route handler
createInferredEdgeRouteHandler(engine, { lazyCompute: true }) — the first request on a cache miss kicks off computeInferredEdges(). Slow first-request UX; great safety net. Combine with a spinner via onInferredEdgesLoadingChange.
Manual / CLI / admin endpoint
Drop a thin admin route or CLI command that calls engine.recomputeInferredEdgesFor(nodeId) after a content edit. Targeted re-runs cost a fraction of a full pass and keep authored changes visible immediately.
All four options are compatible. A nightly job + lazy fallback + manual trigger covers most production setups.