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-trackedInferredEdgeMesh.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.