Context integrity for LLM chat sessions. Deterministic fact capture, confabulation and sycophancy guards, namesake sealing, tool-syntax scrubbing — and the 13-turn gauntlet that scores them. Zero dependencies, bring your own model.
npm install keepline
import { createTrio, createMemoryStore } from "keepline";
import { anthropicAdapter } from "keepline/src/providers/index.js";
const trio = createTrio({
callModel: anthropicAdapter({ apiKey: process.env.ANTHROPIC_API_KEY }),
store: createMemoryStore(),
sessionId: "demo"
});
const { text } = await trio.turn("our low-stock Slack bot is named Gnocco");
// ... twenty messy turns later ...
await trio.turn("remind me — what did we call the bot?"); // → Gnocco. Not an invention.Or use the pieces à la carte — every module is pure and standalone:
extractFacts / mergeFacts / renderLedgerBlock (capture), runGuards (integrity chain),
sealWebResults (namesake seal), scrubToolSyntax (leak scrub).
I run a small AI product on a flash-tier model. I built a 13-turn benchmark — 12 facts planted across a messy strategy session, with a tangent, two contradiction attacks, a sycophancy pull, and precision traps — and ran my stack against a frontier model.
Nine runs in one day. The arc: 94 → 91 → 98 → 92 → 75 → 95 → 89.5 → 94 → 98, against a frontier anchor of 99. Every dip was a mechanism with a name, and every fix in this library exists because a specific run failed a specific way:
| The failure | The run | The fix in this repo |
|---|---|---|
| Asked for a bot name it never filed, the model invented one ("Tavolo-Alert") | 94 | recallFabrication + record capture: abstain honestly, or better — recall from the ledger |
| A web search on my fictional "Osteria del Sole" found the real 500-year-old Bologna wine bar, imported "serves no food", and flipped a strategic verdict to walk-away | 91, 89.5 | sealWebResults — session entities outrank the web, including on the turn that introduces them |
| A mid-session re-entry flipped recall to record-first and three stacked "answer from the record" instructions fenced the model off from its own memory — honest abstentions everywhere, score cratered to 75 | 75 | the one-voice rule baked into every rendered block: "prefer", never "only" |
| The abstain instruction said "that's not in the record" while the code was holding the record — so the rewrite denied a fact that was on file | 92 | corrections take facts as a parameter: show the record, don't assert about it (recallDenial catches the "we never named it" mask) |
Raw draw_diagram(...) calls and JSON payload blobs printed into user-facing replies on no-tools retry paths |
92, 89.5 | scrubToolSyntax — call + blob excision, code-fence-aware |
| A "revised" number kept its stale twin: with burn updated to €25k, the record still instructed "use €18k VERBATIM" | (caught by tests) | temporal supersede in mergeFacts — singular roles revise, plural roles coexist |
The through-line: every failure was a good instinct in the wrong place — fluency, grounding, record-discipline, all virtues until the session's world is self-contained. And every fix is structure, not prompt text: control flow keyed on retrieval hits, bounded retries, deterministic fallbacks. An invented fact cannot ship even when the rewrite model fails twice.
src/capture/ extractFacts · mergeFacts (temporal supersede) · renderLedgerBlock · buildRecallNote
src/guard/ detectors (fabrication, denial, bait-adoption, stance-flip) · corrections · runGuards chain
src/seal/ findCollisions · sealWebResults (the namesake guard)
src/scrub/ scrubToolSyntax (calls + JSON blobs, fence-aware)
src/agents/ createTrio — reference three-role / shared-record architecture
src/store/ the two-function store contract + in-memory reference
eval/ the gauntlet (13 turns, 12 needles) + scoring rubric + runner
The trio is a reference architecture, not a product harness. Proposer / verifier / builder over one shared record, verifier silent by default, guards on every reply. The role prompts are deliberately generic five-liners — real deployments earn their personas through evals. The mechanism is the gift; the tuning is your work.
node eval/run-gauntlet.js # built-in trio, any API key
or feed any turn(userMsg) → { text } function to runGauntlet() — your app, your agent
framework, a raw model. Score the transcript with eval/rubric.md. If you publish numbers,
publish the caveats too: N, judge model, run conditions.
- Fix behaviour with structure, not more prompt text.
- Capture facts to a record — then abstain, don't guess.
- One voice decides where to look — "prefer", never "only".
- Show the record; don't assert about it.
- Re-test every change on a clean run, meter on.
Regex-based extraction, English-first, precision over recall by design (a noisy ledger is worse than a thin one — the conversation window remains the primary memory). This is a small sharp kit, not a framework. Maintenance is best-effort: issues with failing-test reproductions get attention first; feature requests may sit.
Extracted from the context engine behind Doric — an environment where an AI team builds software with you and keeps the line: every decision filed, every fact recorded, nothing lost between sessions. The benchmark story behind this library, run by run: doric.build/blog/keepline.
The bundled gauntlet is ONE scenario; the full multi-scenario suite (with blind-authored scenarios and the tuning disclosure) is plotline.
Sibling packages: shipline (deploy only the Firebase functions your change affects) · wireline (find code that is built but never wired).
MIT © Gabriel Kerner