Get started

MemoSprout captures corrections to AI outputs and delivers them to future interactions. Fix a mistake once, and it stops coming back.

1. Install

npm install memosprout

2. Connect an LLM — optional

An LLM lets MemoSprout detect corrections inside ordinary chat messages and extract the fields for you. Skip this and everything still works — you just add corrections yourself with ms.correct() (step 4) or the CLI.

For any endpoint, pick the wire format it speaks — openai-compatible or anthropic-compatible — and supply a base URL, an API key, and a model id (all three required):

import { MemoSprout } from "memosprout";

const ms = new MemoSprout("./corrections", {
  llm: {
    provider: "openai-compatible",   // or "anthropic-compatible"
    baseUrl: "https://api.openai.com/v1",
    apiKey: process.env.LLM_API_KEY,
    model: "gpt-4o-mini",
  },
});

For these eleven named providers you can pass the name instead — baseUrl and a default model are filled in for you, e.g. llm: { provider: "openai", apiKey: "..." }:

ProviderSuggested modelNote
openaigpt-4o-miniBest price/performance
anthropicclaude-haiku-4-5-20251001Cheapest Claude
deepseekdeepseek-chatExtremely cheap

Custom or self-hosted endpoints: use provider openai-compatible or anthropic-compatible with an explicit baseUrl + model. Unsupported provider names throw a clear error listing valid options. Whichever provider you pick, responses and errors are normalized — see docs/PROVIDERS.md for per-provider setup, keys, and caveats.

3. Add to your app

Three calls wrap the AI call you already have: one to learn from the message, one to enrich the prompt, one to check the answer before it goes out.

// ms is the instance from step 2 (or `new MemoSprout("./corrections")`)
async function handleChat(userMessage: string, previousAIAnswer: string) {
  // MemoSprout auto-detects corrections and feedback.
  // "No, it's 15 days, not 12" → correction saved automatically.
  // "My refund seems too low"  → feedback signal for your team.
  // "Thank you"               → ignored.
  const result = await ms.processMessage(userMessage, previousAIAnswer);

  // Get relevant corrections for the AI's context
  const { context } = await ms.context(userMessage);

  // Call your AI with context injected into the system prompt
  const answer = await callYourAI(userMessage, context);

  // Check the answer before sending
  const check = await ms.check(answer);
  if (!check.ok) {
    // A correction is one fact. An answer may carry several, so regenerate
    // the whole answer with every returned correction and preserve the
    // parts that were already right.
    const verified = check.corrections.map((item) => `- ${item.correct}`).join("\n");
    const revised = await callYourAI(
      userMessage,
      `${context}

Revise the entire draft below. Keep every unrelated fact, replace only
the stale claims, and return the complete answer.

Draft:
${answer}

Verified facts:
${verified}`,
    );

    // The repair is not trusted until it passes the same gate.
    if (!(await ms.check(revised)).ok) throw new Error("Unsafe answer blocked");
    return revised;
  }
  return answer;
}

That's the whole loop: corrections are captured, gated, and retrieved for later turns that match them.

check() catches literal, reworded, and reordered wrong answers with no LLM involved. Enable semanticCheck: true to also catch paraphrases and translations, at the cost of one LLM call per check:

const ms = new MemoSprout("./corrections", {
  llm: {
    provider: "openai-compatible",
    baseUrl: "https://api.openai.com/v1",
    apiKey: process.env.LLM_API_KEY,
    model: "gpt-4o-mini",
  },
  semanticCheck: true,  // catches "twelve days of yearly vacation"
});

The mirror image on the read side is semanticRetrieval: true. Retrieval is lexical by default, so a correction filed under "uniform allowance" is not found by a question about "workwear". With it on, a query that lexical cannot answer — or answers only weakly — falls back to embedding similarity. On a 24-correction corpus, overall accuracy goes from 33% to 93%, and wrong corrections served drop from 4 to 1:

const ms = new MemoSprout("./corrections", {
  llm: { provider: "openai", apiKey: process.env.OPENAI_API_KEY },
  semanticRetrieval: true,
});

await ms.context("How much can I claim for workwear?"); // -> match

Lexical runs first, and a confident hit is kept as-is, so queries that already worked cost nothing. Correction vectors are embedded once and cached on disk. With OpenAI text-embedding-3-small at $0.02 per 1M tokens, a million lexical misses costs roughly $0.60.

4. Add corrections manually

No LLM needed here. Use this for a review queue, an admin panel, or seeding known fixes — role decides whether it goes live immediately (agent, admin, system) or waits for approval (customer):

await ms.correct({
  wrong: "Refund takes 3 business days",
  correct: "Refund takes 5 business days since March 2026",
  keywords: ["refund", "processing"],
  source: "Refund Policy v4.1",
  role: "agent",
});

5. Review the queue from the terminal

Corrections from customers, and those an LLM extracted from a conversation, are stored as suggested and are not served until a human approves them. The CLI is how you work that queue without writing code:

npx memosprout report                    # how big is the queue, and how old?
npx memosprout list --status suggested   # see what is waiting
npx memosprout approve corr_a1b2c3d4     # clear one

report leads with the queue whenever anything is waiting, and stays quiet when it is empty:

2 correction(s) waiting for approval
  oldest: 34 day(s) ago
  corr_e11982a8776fe177
  approve with: memosprout approve <id>

approve is the human sign-off path. activate is a different thing — the last step of the oracle path, which only accepts an already-validated correction. Nothing notifies you that the queue is filling up, so if you accept corrections from untrusted sources, check it on a schedule.

6. Call it from Python, PHP, Go — optional

Run the built-in REST API server and call it over HTTP — the full feature set, not a subset:

MEMOSPROUT_API_KEY=your-secret-key \
MEMOSPROUT_LLM_PROVIDER=openai-compatible \
MEMOSPROUT_LLM_BASE_URL=https://api.openai.com/v1 \
MEMOSPROUT_LLM_API_KEY=your-llm-key \
MEMOSPROUT_LLM_MODEL=gpt-4o-mini \
pnpm api        # http://127.0.0.1:3456
import requests, os

BASE = "http://127.0.0.1:3456"
HEAD = {"Authorization": f"Bearer {os.environ['MEMOSPROUT_API_KEY']}"}

requests.post(f"{BASE}/correct", headers=HEAD, json={
    "wrong": "Refund takes 3 business days",
    "correct": "Refund takes 5 business days",
})

ctx = requests.post(f"{BASE}/context", headers=HEAD,
                    json={"query": "how long is a refund?"}).json()

The server binds to 127.0.0.1 and refuses to bind anywhere else without an API key. All endpoints except /healthrequire the key, requests are rate limited (120/min per key by default), and bodies are capped at 1 MB.

What happens behind the scenes

  • Corrections from agents/admins go live automatically. From customers, they wait for approval.
  • Feedback (complaints without a clear answer) is stored as a signal for your team, never as a correction.
  • Stale corrections are detected automatically (source document changed, conflicting correction, or expired).
  • Storage is plain Markdown files in the directory you chose. No database, no cloud. Git-versionable. Writes are atomic and serialized, so concurrent requests never corrupt a file.
  • Answer checking catches literal, reworded, and reordered wrong answers without an LLM — and paraphrases and translations when semanticCheck is on. If the LLM fails, it falls back to lexical matching and logs a warning rather than blocking your answers.
  • Retrievalis lexical by default — keywords, entities, and the correction's own words, with inflection and reordering. Turn on semanticRetrieval to add an embedding fallback for paraphrased questions.

FAQ

What does MemoSprout store?

Corrections themselves are general knowledge (e.g. "refund takes 5 days"), not personal data, and no chat transcripts are kept. One thing to know: when context() serves a correction, the query text that triggered it is recorded in outcomes.json so you can see which corrections actually get used. If your queries can contain personal data, treat that file as you would any log — it stays on your server either way.

Can customers poison the knowledge base?

Several guardrails make it hard. Corrections submitted with role: "customer" are always saved as suggested and need approval. LLM-extracted corrections also wait for approval by default, because model confidence is not source validation, and prompts treat user text as data rather than instructions. Only an explicit approvalRequired: false enables confidence-based auto-activation; reserve that for a trusted input channel.

Does it require an LLM?

No — the default is no LLM. Capturing, storing, matching, and blocking all work without one. An LLM adds four conveniences: detecting corrections inside ordinary messages (processMessage()), semantic answer checking (semanticCheck), generating synonym triggers so a paraphrased question still finds its correction (generateAliases), and embedding-based retrieval for paraphrased questions (semanticRetrieval). The last one needs an embedding model rather than a chat model, configured separately — point it at a local Ollama instance and that path stays free and offline too.

A source document gets updated — what happens to a correction based on the old version?

It stops being served. Give a correction a fingerprint of its source (sourceHash) when you capture it, and tell MemoSprout how to fetch the current fingerprint with setSourceHashProvider. On every context() and check() the hash is recomputed; if the document changed, the correction is quarantined — dropped from retrieval, not deleted — because once its basis shifts it may have become right, wrong, or redundant, and serving it anyway would be worse than serving nothing. A correction also stops on an expiresAt date, when a newer correction supersedes it, or when you remove() it. Without a sourceHash, the source-change check has nothing to compare against and does not run.

I already have agent memory — MEMORY.md, CLAUDE.md, Cursor rules, a system prompt. How does MemoSprout fit with it?

They do different jobs and compose in one prompt. A memory file is static, whole-file, always-on context with no relevance, no gate, and no staleness — every line is loaded every turn and nothing checks whether it is still true. MemoSprout is the opposite on each axis: it holds corrections, retrieves only the ones relevant to the question, gates them before serving, and quarantines them when their source changes. Keep durable, always-true context in the memory file; put facts that get corrected, change over time, or need verification in MemoSprout. Because context() returns a string, you inject it alongside the memory file: [memory, context].filter(Boolean).join("\n\n"). The one thing to avoid is writing the same fact into both — if they ever disagree, the model sees two authoritative answers with no way to choose.

How do I know it's working?

report() shows corrections being served and blocked, and — the honest part — the two ways MemoSprout fails silently, without raising an error.

Questions that found nothing. queriesWithoutMatch with unmatchedQueries: questions that found no correction although the domain had some. A high count usually means your trigger keywords do not match how users phrase things. Add the words from that list, enable generateAliases, or — if those queries are paraphrases rather than missing vocabulary — turn on semanticRetrieval.

Corrections nobody approved. pendingApprovals counts corrections waiting on a human. One that is never approved is never served, so a climbing number means knowledge is being captured and then dropped. oldestPendingApprovalAt is the sharper signal: three corrections filed this morning is a queue, three filed last quarter is an abandoned one. Nothing notifies you — poll it, or run memosprout report.

Is the REST API secured?

Yes. The API server binds to localhost by default and refuses to expose itself without MEMOSPROUT_API_KEY set. Requests authenticate via Authorization: Bearer or x-api-key.

My users ask questions in their own words and nothing is found. What do I do?

That is lexical retrieval's known limit: it matches trigger keywords, entities, and the words of the correction, so it handles inflection and reordering but cannot relate two different words for the same thing. Turn on semanticRetrieval: true. Lexical still runs first — free, instant, and precise on exact terms — and embeddings are consulted when it finds nothing, or finds only a weak match. On a 24-correction corpus with text-embedding-3-small, paraphrase recall went from 8% to 83% and overall accuracy from 33% to 93%.

Precision improves as well — wrong corrections served fall from 4 of 30 to 1 — and that hinges on the word weak. Retrieval keeps a lexical answer only when it is confident: a phrase keyword, or a keyword with corroborating content. A bare single-keyword match is treated as a guess and re-checked against the embeddings, which is what stops "what time does the office open?" from returning a home-office allowance. An earlier version trusted any lexical hit and scored 83% — worse than using embeddings alone. Run pnpm semantic:eval; it prints the queries it got wrong, so you can tune semanticRetrievalThreshold against a corpus resembling yours. If the embedding provider is unreachable, context() logs a warning and falls back to the lexical result.

What does semantic retrieval cost?

Very little, because the expensive half is cached. Each correction is embedded once and stored in embeddings.json, keyed by a hash of its text, so editing a correction re-embeds it and nothing else does. Only the query is embedded per call, and only for queries lexical did not already answer. At OpenAI's $0.02 per 1M tokens for text-embedding-3-small and ~30 tokens a query, one million lexical misses costs about $0.60; indexing 1,000 corrections costs well under a cent. Next to the chat model that consumes the context, it is a rounding error.

Where does data live?

On your server. Markdown files in a directory, no cloud, no telemetry.

Worth being precise here, because it is easy to assume otherwise: storing corrections, retrieving them, and blocking wrong answers involve no LLM and no network calls at all. That is the default — no API key required. An LLM only enters when you switch on one of the optional features (processMessage(), semanticCheck, generateAliases, semanticRetrieval), and then the only outbound calls go to the endpoint you configured.

Of those, semanticRetrieval is the one that uploads correction content — comparing a query to a correction means embedding both. If corrections must not leave your infrastructure, point embedding.baseUrl at a local Ollama instance, or leave the feature off.