Skip to content

About

Keep a local LLM honest: it proposes validated typed deltas instead of mutating state. Anti-hallucination by construction. Zero deps, OpenAI-compatible.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

grounding-engine

Keep a local LLM honest. Instead of letting a model rewrite your application state (and hallucinate it into an illegal shape), the model only proposes typed deltas. A deterministic engine validates every delta against your rules and applies only the legal ones. The model proposes; the engine decides.

Zero dependencies. Works with any OpenAI-compatible endpoint (local or hosted). Built for small, fast, local models, which is exactly where hallucination and malformed output hurt most.

Applied:
  ✓ t2 → done
  ✓ ana → t1
Rejected (engine refused — anti-hallucination):
  ✗ ASSIGN — unknown person zoe          # model invented an id
  ✗ ASSIGN — cannot reassign a done task # broke a domain rule
  ✗ SET_STATUS — invalid status shipped  # value not in the enum
  ✗ NUKE_EVERYTHING — unknown op          # not a real operation

Why

Give an LLM write access to your state and it will, sooner or later, reference an entity that doesn't exist, set a field to a nonsense value, or claim an outcome it shouldn't decide. Telling it "please don't" in the prompt is not a guarantee.

This flips the contract:

  • The engine owns the truth. State lives in your code; the model never mutates it directly.
  • The model is handed only valid identifiers (your context) and may reference nothing else.
  • The model emits typed deltas via a forced tool call: { op, ...args }.
  • Every delta is validated against your validate(state, args) rules before apply. Illegal deltas are rejected with a reason, never silently applied.
  • Numbers/outcomes are computed by your code, not asserted by the model.

The result: the worst a hallucinating model can do is get rejected. Your state can never enter an illegal shape.

Quick start

git clone <this repo> && cd grounding-engine
node examples/taskboard.js          # offline: feeds a mixed (legal + illegal) proposal
node examples/taskboard.js --live   # calls a local OpenAI-compatible model

For --live, point it at your server:

AL_LLM_URL=http://localhost:8088/v1 AL_LLM_MODEL=<your-model-id> node examples/taskboard.js --live

Node 18+ (uses built-in fetch). No npm install needed.

Usage

import { GroundingEngine, createLLM } from "grounding-engine";

const state = { people: { ana: {} }, tasks: { t1: { status: "todo", assignee: null } } };

const operations = {
  ASSIGN: {
    description: "Assign a task to a person",
    params: { task: { type: "string" }, person: { type: "string" } },
    validate: (s, a) =>
      !s.tasks[a.task]   ? { ok: false, reason: "unknown task" }
      : !s.people[a.person] ? { ok: false, reason: "unknown person" }
      : { ok: true },
    apply: (s, a) => { s.tasks[a.task].assignee = a.person; return `${a.person} → ${a.task}`; },
  },
};

// the ONLY facts/ids the model is allowed to reference:
const context = (s) => ({ people: Object.keys(s.people), tasks: Object.keys(s.tasks) });

const engine = new GroundingEngine({ state, operations, context, llm: createLLM(), domain: "task board" });

const { narrative, applied, rejected } = await engine.propose("Put Ana on t1.");

Offline / deterministic

You don't need a model to use the core. Feed deltas straight in (great for tests):

const { applied, rejected } = engine.applyDeltas([
  { op: "ASSIGN", task: "t1", person: "ana" },   // applied
  { op: "ASSIGN", task: "t1", person: "zoe" },   // rejected: unknown person
]);

API

  • new GroundingEngine({ state, operations, context, llm?, domain? })
  • engine.applyDeltas(deltas) → { applied, rejected }: the deterministic validate+apply core.
  • await engine.propose(instruction) → { narrative, applied, rejected }: full LLM loop.
  • engine.tool() / engine.systemPrompt(): the generated tool schema and prompt, if you want to drive the LLM yourself.
  • createLLM({ baseUrl, model, apiKey, noThink }): OpenAI-compatible client. noThink sends reasoning_effort:"none" (skips a model's thinking pass when supported, ignored otherwise).

operations map shape:

OP_NAME: {
  description,                         // shown to the model
  params: { field: { type, enum? } },  // JSON-schema-ish, merged into the tool
  validate(state, args) -> { ok } | { ok: false, reason },
  apply(state, args) -> string|void,   // mutate state; return an optional log line
}

Notes for local models

  • Forced tool call + a union params object keeps output structured even when the model can't do strict per-op schemas.
  • Defensive JSON parsing recovers from truncated/oversized tool arguments (brace-balanced fallback), with one retry and a safe empty fallback, so a bad generation degrades to "nothing applied", never to corruption.

License

MIT © eauchs

About

Keep a local LLM honest: it proposes validated typed deltas instead of mutating state. Anti-hallucination by construction. Zero deps, OpenAI-compatible.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages