BuilderForce Agent Memory
Part of Builderforce open source: the MIT projects Builderforce.ai is built on.
A framework for giving any AI agent persistent, browser-trainable SSM memory. It is provider-neutral — swappable LLM bridges (Anthropic / OpenAI / Fetch), an inference router, online distillation, and a persistent memory store — so any agent or app can depend on it, not just BuilderForce.
BuilderForce.ai is the flagship deployment, not the boundary: it trains a custom SSM in the browser, exports a model, and pushes it to an agent runtime where it runs as Evermind — the model itself, not just memory bolted onto someone else's LLM. Evermind is the whole brain: its own shared-expert generator (the cortex that does reasoning and language), a write-through knowledge memory (the hippocampus), and a trainable affective layer (the limbic system). A request can be served by Evermind directly — evermind/<ref> traffic is generated on-device, not forwarded to Claude/GPT — and external frontier models stay an optional routing choice rather than a hard dependency. The same framework is reusable by other agents off the shelf.
This monorepo consolidates two packages that previously lived in separate repos (mambacode.js and ssmjs) whose names described the technique rather than the role. They are renamed and unified here so that "what they do" is legible: they are Agent Memory.
Packages
The two-package split is deliberate: the engine is zero-dep and WebGPU-pure and can be consumed standalone; the runtime pulls in LLM-vendor bridges. Flattening them would force engine-only consumers to drag in vendor code and vice versa. They release in lockstep from one pipeline, which kills the publish-drift bug class that the separate-repo setup suffered (bumping one version without publishing + regenerating the consumer lockfile).
Cutting token cost
Agent Memory ships three layers that reduce LLM spend, in increasing power. All are portable — the same code runs in the browser (WebGPU SSM) and in Node (the agent's @webgpu/node SSM) because the embedder and storage are injected, never hard-wired.
The semantic cache is the real lever. It is read-through with two tiers, mirroring an L1-Map / L2-KV cache:
- L1 — an in-process vector list, scanned locally with on-device SSM embeddings (free, offline-capable).
- L2 — an optional shared backend (
FetchSemanticCacheBackend→ the BuilderForce.ai gateway), so a paraphrase answered by the web app is reusable by an agent and vice-versa.
import { SemanticCache, FetchSemanticCacheBackend, AnthropicBridge } from '@seanhogg/builderforce-memory';
const cache = new SemanticCache({
embed: (t) => runtime.embed(t), // on-device SSM (free)
l2: new FetchSemanticCacheBackend({ baseUrl, apiKey }), // shared via the gateway
threshold: 0.92,
});
const { response, cached, tier } = await cache.getOrGenerate(
prompt,
() => bridge.generate(prompt), // only runs on a miss
);
Memory-backed fact injection is semantic too: SSMAgent defaults to factSelection: 'semantic', injecting only the top-maxFacts embedding-relevant facts each turn (paraphrase-robust, smaller prompts) instead of an exact key-substring match.
Write-Through Cognition (Evermind)
Caching keeps answers fresh; cognition keeps knowledge fresh. A frozen model — or an append-only memory — drifts: stale and current facts pile up under different keys until something reconciles them by hand. EvermindCognition closes that gap. It is the model-knowledge analogue of a write-through cache with a conflict resolver, so a belief is replaced on write, never appended into a reconciliation backlog. This is what lets Evermind's knowledge stay current without a manual reconcile step.
Every candidate fact flows through one pipeline:
Canonicalize (stable subject key) → recall incumbent → evaluate evidence → reconcile (augment | confirm | supersede | reject) → write-through
- Stable subject key — facts about the same subject collide and replace, instead of accumulating under per-run ids. This is the anti-drift fix.
- Evidence-gated — a conflicting fact only supersedes the incumbent when ground-truth evidence favours it. The
EvidenceGathereris injected, so the evidence source is surface-specific (IDE file tools, a DB probe, an HTTP check);workspacePresenceGathererships for the common "is it still on disk?" case. - Write-through recall —
recall()is served from a version-token cache that invalidates the instant knowledge changes — the same invalidate-on-write rule as the semantic cache, applied to beliefs.
import { EvermindCognition, workspacePresenceGatherer, MemoryStore } from '@seanhogg/builderforce-memory';
const cog = new EvermindCognition({ store: new MemoryStore() });
// Seed a (soon-to-be) stale belief.
await cog.commit({ subjectKey: 'pkg:ssm-stack', content: 'SSM stack = MambaKit + SSMjs' });
// Re-observe: evidence from the real workspace decides the conflict.
const r = await cog.commit(
{ subjectKey: 'pkg:ssm-stack', content: 'SSM stack = builderforce-memory monorepo' },
workspacePresenceGatherer({
list: () => listWorkspace(), // e.g. the IDE `list_files` control tool
mustExist: ['builderforce-memory/'],
mustBeAbsent: ['MambaKit/', 'SSMjs/'],
}),
);
// r.verdict === 'supersede' → exactly one belief held, the stale one retired (replace, not append)
EvermindCognition is store- and surface-agnostic — the CognitionFactStore interface is satisfied structurally by MemoryStore (no adapter) — so the same loop runs in the IDE, on-prem, cloud, and the browser.
Enterprise architecture
Caching cuts the bill and cognition keeps knowledge current, but neither answers the questions an enterprise buyer actually asks before signing: can it read our data, will it leak across tenants, what did that answer cost, and how do we know it got better? Those four questions are what this layer exists to answer. Each is a port with adapters behind it, so a customer's existing stack is an adapter choice rather than a rewrite.
Ingestion — structured and unstructured through one path
A support-ticket export (rows, typed columns) and a policy document (prose) reach the index through the same pipeline; the difference is which parser ran, not which system was built. Structure that survives parsing becomes filterable metadata, which is what makes "summarise open P1 tickets about billing" answerable — status and priority are database predicates rather than something the embedding has to imply.
import { IngestionPipeline, InMemoryIngestManifest } from '@seanhogg/builderforce-memory/ingest';
import { MemoryVectorStore } from '@seanhogg/builderforce-memory/vectorstore';
const pipeline = new IngestionPipeline({
store : new MemoryVectorStore(),
embed : (texts) => embedder.embedBatch(texts),
manifest: new InMemoryIngestManifest(), // enables `diff` sync
tracer,
});
await pipeline.ingest([
{ id: 'policy.md', tenantId: 'acme', title: 'Retention Policy',
content: { kind: 'text', text: markdown, mediaType: 'text/markdown' } },
{ id: 'tickets', tenantId: 'acme', acl: ['support'],
content: { kind: 'rows', rows: ticketRows, rowIdField: 'id' } },
]);
What makes it a pipeline rather than a loader script is everything that happens on the second run: chunk ids are deterministic (a re-run overwrites instead of duplicating), content hashes mean only changed chunks are re-embedded, vanished chunks are deleted, one malformed document fails alone and is reported by id, and forget(sourceId) erases a source completely. Builtin parsers cover markdown (keeping the full heading breadcrumb), HTML, JSON, CSV/TSV and typed rows; a new format is a ParserRegistry entry, not a branch.
Vector store — a port, not a vendor
Enterprises rarely get to choose the vector database; it is already pgvector in their Cloud SQL, Vertex AI Vector Search because they are a GCP shop, or Qdrant because platform standardised on it. So the store is a port and every adapter is infrastructure:
MemoryVectorStore— in-process, HNSW-backed, with BM25 keyword search. The default, the test double, and the honest answer for edge deployments.RestVectorStore— one adapter over a dialect registry.evermind(the contract this package defines),qdrant,pineconeandvertex-aiship;registerDialect()adds your own.
Filters are data, not predicates, because a (record) => boolean cannot be pushed down to a remote database — it forces fetch-everything-then-filter, which is both slow and a leak. When a dialect cannot express a clause, translateFilter returns it as a residual that the store applies locally after over-fetching. A clause is never silently dropped, which on an ACL clause would be a cross-tenant leak.
Tenancy is compiled, not remembered. An AccessScope becomes a filter that is AND-ed into every read, and it fails closed: ingestion always writes a non-empty acl (defaulting to ['*']), so a record written without one matches nothing rather than everything.
const hits = await store.query({
vector, topK: 5,
scope: { tenantId: 'acme', principals: ['sec-team'], maxSensitivity: 3 },
});
Retrieval — hybrid, scoped, cited
EnterpriseRetriever runs dense and lexical arms concurrently, fuses them with RRF, reranks with MMR for diversity, and optionally hands the result to a reranker. Dense retrieval alone misses exact tokens — error codes, SKUs, clause numbers; lexical alone misses paraphrase.
It is also honest about degradation: against a store with no lexical index it reports mode: 'dense-only' rather than quietly returning worse answers. And with no passages in scope it refuses, because a confident answer with no evidence is the exact failure that kills pilots.
const { answer, result } = await retriever.answer(question, bridge, {
scope: { tenantId: 'acme', principals: ['support'] },
});
// answer cites passages as [1], [2] …; result.passages carries title + uri for each
Orchestration — multi-agent as a stateful graph
AgentGraph executes in supersteps: a frontier of nodes runs concurrently, their partial updates merge through declared channel reducers, and the next frontier comes from the edges. The three hard parts of multi-agent work fall out of that model — concurrent fan-out is just a bigger frontier, a merge conflict has a declared answer instead of a race, and the state between supersteps fully describes the run, so a checkpoint written there resumes it exactly.
That last property is why crash recovery, human-in-the-loop approval and time-travel debugging are one mechanism here rather than three features:
const graph = new AgentGraph({ interruptBefore: ['publish'], checkpointer, tracer });
const paused = await graph.invoke(input, { threadId }); // reason: 'interrupted'
await graph.resume(threadId, { draft: humanEditedDraft }); // edits merge through the same reducers
Three patterns ship on top, and they compose — a supervisor's worker can be a ReAct agent:
createReactAgent— interleaved reasoning and tool use. The iteration cap is enforced in the routing rather than asked for in the prompt, and a malformed action or a failing tool comes back as an observation the model can recover from, never an exception that ends the run.createReflectionAgent— generate → critique → revise, bounded. Reflection pays only when the critic may be harsh and the loop may stop; both limits are code, not prompt.createSupervisor— hierarchical delegation. Workers return a result, not their transcript, which keeps the supervisor's context from growing into the sum of everything its workers read.
Telemetry — LLM-native metrics on one span stream
Metrics are a projection of the trace stream rather than a second instrumentation path that can drift out of agreement with it. Attach MetricsRegistry as a Tracer exporter and tokens/sec, cost-per-request, TTFT and cache-hit rate fall out of traffic that is already traced.
const registry = new MetricsRegistry();
const tracer = new Tracer({ exporter: registry });
const bridge = new InstrumentedBridge(new AnthropicBridge({ apiKey }), { tracer });
registry.llm().tokensPerSecond; // throughput the user feels
registry.llm().avgCostPerRequestUsd; // the unit that ties spend to a decision
registry.llm().localCacheHitRate; // the lever that moves cost
registry.llm().unpricedCalls; // honesty: never billed at a silent zero
Every bridge reports what its last call actually consumed, so cost is measured, not estimated — and Anthropic's three-way input split (fresh / cache read / cache write) is priced at three different rates, because folding them together overstates spend by ~10× on exactly the cache-heavy traffic this package is tuned to produce. Spans export to any OTLP/HTTP collector (Cloud Trace, Datadog, Honeycomb, Tempo) using GenAI semantic conventions, over fetch rather than the OpenTelemetry SDK, so the package still runs in a browser and in a Worker.
Evaluation — the launch gate
Projects die either because nobody can say whether the system is good enough, or because it regressed silently after launch. Both are the same missing artefact: a dataset, comparable scores, and a threshold a build can fail on.
const report = await new EvalHarness({
graders: [noError, retrievalRecall, citationValidity, substringChecks],
gate : { minPassRate: 0.9, minGraderScore: { 'retrieval-recall': 0.85 }, maxMeanCostUsd: 0.01 },
tracer,
}).run(dataset, target);
console.log(formatReport(report));
if (!report.gate.passed) process.exit(1);
Two graders matter most and generic LLM evals omit both. retrievalRecall asks whether the right source came back at all — a generation score cannot distinguish "reasoned badly" from "was handed nothing", and those have opposite fixes. citationValidity catches an answer citing [7] when six passages were supplied; that scores fine on similarity and fails an audit. Cost and latency are graded alongside quality, because a quality-only harness quietly ships an unaffordable system. A gate with no thresholds reports itself as vacuous rather than green.
Google Cloud
VertexAIBridge and VertexAIEmbedder serve GCP deployments, and the vertex-ai dialect maps this filter algebra onto Vertex restricts / numericRestricts. Auth is injected, never owned — supply getAccessToken and the same code runs under Application Default Credentials locally, Workload Identity on GKE, and an impersonated token in CI, without this package taking on a credential lifecycle or a Node-only vendor SDK.
Vertex AI Vector Search stores vectors and restricts but not text, so its dialect declares storesText: false and the store hydrates chunk text through a textResolver (Firestore, BigQuery or GCS beside the index). Pretending otherwise would return matches with empty text and an answer with no evidence.
Validation status. The
qdrant,pineconeandvertex-aidialects and the Vertex bridge are covered by unit tests that assert the emitted request bodies and parse recorded response shapes. They have not been exercised against live services from this repository, which has no credentials for them — see the Gap Register.
Discovery
docs/enterprise-discovery.md is the technical discovery guide: the questions to ask a customer, which answer maps to which port, and the decisions that must be made before a line of integration code is written.
Technical report & peer review
The architecture is written up as a formal, peer-review-grade technical report, with the SSM cortex, Write-Through Cognition, and the limbic layer specified mathematically (including a monoid-scan proof and the single-incumbent invariant). It lives in publication/evermind/:
evermind-architecture.pdf(source:.tex, readable.html) — the manuscript. It deliberately scopes the "beats a frozen frontier model" advantages as falsifiable hypotheses with a measurement protocol, not as benchmark results. The language-model benchmarking harness those metrics need (held-out perplexity, bits-per-token, accuracy, throughput, model A/B) now ships in the engine (packages/memory-engine/src/bench/) and on-device in the Studio, so the protocol is an instrument away from numbers, not a skeleton.PEER-REVIEW.md— an adversarial scientific referee report on both the paper and this implementation, grounded infile:lineevidence. It is candid about where the shipped code is first-generation: notably that recall is anO(N)scan with no ANN index, that the "stable subject key" above is caller-supplied and not yet canonicalized (so the single-incumbent guarantee currently rests on an unenforced precondition), and that the MCP HTTP surface lacks tenant isolation. These findings are tracked asEVM-1…EVM-8in the Builderforce.ai roadmap's Consolidated Gap Register. Those Major items have since been resolved in the v2026.6.34 hardening (HNSW ANN index, a subject-key canonicalizer insidecommit(), MCP tenant isolation, fenced recall, a forgetting guard); see the resolution addendum at the top of that review.SUBMISSION.md— venue guide and the TechRxiv/arXiv submission kit.
Read the review before depending on any single guarantee in the report at production scale.
Develop
pnpm install # links workspace packages; memory-engine auto-builds via prepare
pnpm build # builds memory-engine, then memory (order matters — runtime needs core's .d.ts)
pnpm test # full suite across engine (jest) + runtime (jest, incl. cognition) + mcp (node:test)
pnpm lint
@seanhogg/builderforce-memory consumes @seanhogg/builderforce-memory-engine via the pnpm workspace:^ protocol; on publish, pnpm rewrites it to a real ^<version> range.
Release
All three packages (memory-engine, memory, memory-mcp) version in lockstep (YYYY.M.D[-beta.N]).
- Bump
versionin everypackages/*/package.json(and root) to the same value, plus the@seanhogg/builderforce-memoryrange inmemory-mcp'speerDependencies. pnpm release:check(build + test).- Commit, then
git tag vYYYY.M.D. - Push the tag —
.github/workflows/release.ymlrunspnpm -r publish, which publishesmemory-engine,memory, andmemory-mcpto npm with provenance (dependency order is resolved automatically;workspace:specifiers are rewritten to real ranges at pack time).
Migration / redirect plan for the old packages
The old packages (@seanhogg/mambacode.js, @seanhogg/ssmjs) are superseded. To migrate consumers cleanly:
- Publish the new scope from this repo (
@seanhogg/builderforce-memory-engine,@seanhogg/builderforce-memory). - Deprecate the old names pointing at the new scope:
npm deprecate "@seanhogg/mambacode.js" "Moved to @seanhogg/builderforce-memory-engine" npm deprecate "@seanhogg/ssmjs" "Moved to @seanhogg/builderforce-memory" - Update consumers' imports:
@seanhogg/mambacode.js→@seanhogg/builderforce-memory-engine@seanhogg/ssmjs→@seanhogg/builderforce-memoryThe primary in-repo consumer to update is the BuilderForce.ai agent-runtime (which loads the SSM as Evermind — generation plus write-through memory).
- The old
SeanHogg/mambacodejsandSeanHogg/SSMjsrepos can be archived once consumers are cut over. Full git history from both was preserved into this repo via subtree merge — no history was lost.
Consolidated Gap Register
Roadmap items identified during the consolidation but intentionally not closed in this pass:
- [DONE 2026-06-18] Published under
@seanhogg/builderforce-memory*; old names deprecated; mistakes cleaned up. Final published names:@seanhogg/builderforce-memory-engine/@seanhogg/builderforce-memory/@seanhogg/builderforce-memory-mcpat2026.6.19(the@builderforceorg doesn't exist on npm, so the scope is@seanhoggbut thebuilderforce-name prefix is kept). Earlier wrong-name publishes are gone:@builderforce/*never landed (404), and the mistaken@seanhogg/memory{,-engine,-mcp}@2026.6.18were unpublished. Superseded legacy packages deprecated on npm via thedeprecate-old-packages.ymlworkflow:@seanhogg/ssmjs→@seanhogg/builderforce-memory,@seanhogg/mambakit→@seanhogg/builderforce-memory(@seanhogg/mambacode.jswas never on npm — engine was git-only). - [DONE 2026-06-18] BuilderForce.ai consumers repointed to the published packages. frontend now depends on
@seanhogg/builderforce-memory-engine(themambacode.jsgit dep +global.d.tsshim removed;tscclean, tests green); agent-runtime's SSM service imports@seanhogg/builderforce-memory. Open: agent-runtime can't add theoptionalDependenciesentry until2026.6.19is >48h old (itspnpm.minimumReleaseAge: 2880guard); add it after 2026-06-20, plus wire the frontend browserSemanticCache(needs the runtime dep + WebGPU bundle verify). - [DONE 2026-06-18] Multi-host installer — memory now extends to any MCP-capable agent, not just Claude. New
builderforce-memory-installbin +src/install/module (host registry + shared launch spec) register the stdio server into Claude Code, Claude Desktop, Cursor, Windsurf, VS Code, Cline, Gemini CLI, and Codex CLI, idempotently with.bakbackups, all pointing at one shared~/.builderforce-memory/memory.jsonstore. Auto-detects installed hosts (--host=auto), or--host=all|<ids>. 7 unit tests over an in-memory fs (tests/install.test.mjs).buildServerSpec/installMemoryServerexported for reuse. - builderforce.ai's own agents — memory wiring status. (1) On-prem agent hosts run on a machine with an MCP host → covered by the multi-host installer above. (2) [DONE 2026-06-18] Cloud Node/container agents now load memory in-process via — (+) added to agent-runtime ; the 48h cooldown was bypassed for first-party packages only via (third-party guard intact). Verified: recall/remember loads in the agent-runtime env; boots , the orchestrator injects into task prompts, and remembers/learns per run. (3) — instead of the MCP-into-custom-loop path, the capability in was completed with / tool definitions, backed on-prem by the SSM service; on-prem + cloud-container (Node) agents now actively recall/remember mid-run. The server-side semantic cache was also wired into the cortex call. (4) — backed by Postgres (migration 0200, tenant-scoped key→fact, lexical ILIKE recall, read-through cached) wired into the api's + in ; graceful-degrades pre-migration, activates on deploy. (5) — fronts the cloud-inference path with a read-through cache backed by an on-device WebGPU SSM embedder (→), dynamic-imported and gracefully without WebGPU/assets. tsc + 4 vitest cases green; browser smoke + L2 endpoint wiring are the residuals. Every builderforce memory surface is now wired (on-prem, cloud container, cloud Worker/DO, web).