@three-ws/agent-memory

@three-ws/agent-memory

Persistent, embeddings-backed memory for agents. Store facts and entities, recall them semantically, and surface a working context — over the three.ws Memory engine.

npm install @three-ws/agent-memory
View on GitHubStar the repo

Persistent, embeddings-backed memory for agents — store facts and entities, recall them semantically, in one import.

npm downloads license node

Install · Quick start · API · How it works · three.ws


@three-ws/agent-memory is the official client for the three.ws Memory engine — the durable, embeddings-backed memory that gives an agent continuity across sessions. Your agent remember()s a fact, then recall()s it later by meaning, not keyword: a real semantic search over stored vectors. It wraps the live /api/agent-memory and /api/memory/* endpoints — a tiered store (working / recall / archival), free-first embeddings, and an auto-mined entity graph of the mints, tickers, wallets, people, and strategies your agent has seen. It pairs with the talking-agent runtime: the same memories that feed the Brain Memory node power your agent's replies.

#Why

Every "agent memory" you reach for is either an in-process array that dies with the process, or a raw vector DB that leaves embedding, tiering, entity extraction, ownership, and the cross-model "different vectors, different space" trap to you. Memory is the whole pipeline, done once:

This is the SDK twin of the three.ws Memory Studio — the same engine, exposed as plain functions instead of a UI.

#Install

npm install @three-ws/agent-memory

Zero runtime dependencies. Works in Node 18+ and the browser (uses fetch). Writes require a signed-in three.ws account or a bearer token (see Auth); reads are owner-only.

#Quick start

Bind a client to one agent, then remember and recall:

import { AgentMemory } from '@three-ws/agent-memory';

const memory = new AgentMemory({ agentId: AGENT_ID, token: process.env.THREE_WS_TOKEN });

await memory.remember('User prefers Solana over EVM chains', { tags: ['preference'] });

const hits = await memory.recall('which blockchain does the user like?');
console.log(hits[0].content); // → "User prefers Solana over EVM chains"
console.log(hits[0].score);   // → 0.83  (real cosine; null on a lexical fallback)

Pin something to the always-in-context working core, then read what the agent carries into every reply:

await memory.remember('Never risk more than 2% per trade', {
  tags: ['strategy'],
  pinned: true, // → working tier, always in context
});

const ctx = await memory.context();
console.log(ctx.tokens, '/', ctx.budget, 'tokens in core'); // → 41 / 2000

Walk the entity graph the store mined for you:

const { nodes, edges } = await memory.graph();
// nodes: [{ kind: 'strategy', label: 'Never risk more than 2%…', mentions: 1 }, …]

#API

#new AgentMemory(options)

Option Type Default Notes
agentId string — Required. The agent these memories belong to.
token string — Bearer token for writes (or use cookie session in-browser).
baseUrl string https://three.ws Override for self-host / preview.
fetch typeof fetch global fetch Inject a custom fetch (e.g. an x402 payer).

#remember(content, options?) → Promise<Memory>

Store (or upsert) a memory. Wraps POST /api/agent-memory { agentId, entry }.

Option Type Default Notes
type 'user' | 'feedback' | 'project' | 'reference' 'project' Memory category. Invalid values fall back to project.
tags string[] [] Topic tags — also mined into entity/topic nodes.
context object {} Structured fields ({ mint, symbol, wallet }) extracted as entities.
salience number 0.5 Importance, 0..1. Drives ranking and tier.
pinned boolean false Pin to the working core (forces tier: 'working').
tier 'working' | 'recall' | 'archival' auto Defaults: pinned → working, low-salience reference → archival, else recall.
id string — Pass to upsert an existing memory idempotently.
expiresAt number | string — Auto-expiry timestamp.

Returns the stored Memory (see shape). Content is capped at 10,000 chars. Changing an existing memory's content resets its vector and mined entities, so it re-indexes on the next read.

#recall(query, options?) → Promise<Memory[]>

Semantic search over the agent's memories. Wraps POST /api/memory/search.

Option Type Default Notes
topK number 8 Max results, clamped 1..50.
minScore number 0.25 Min cosine similarity, 0..1.
tiers string[] all Restrict to e.g. ['working', 'recall'].
type string all Restrict to one memory type.

Each result carries a score (cosine, 4-dp) and match of 'semantic' or 'lexical'. Results are semantic-first, then back-filled with salience-ranked substring hits so a query is never empty just because a provider is down. Recalling a memory bumps its accessCount and salience slightly — the recency/reinforcement loop the tiers rely on.

#entities(options?) → Promise<Entity[]>

The knowledge-graph nodes the agent's memories mention. Convenience over graph() — returns nodes ranked by mention count.

#graph() → Promise<{ nodes, edges, stats }>

The full temporal knowledge graph. Wraps GET /api/memory/graph. nodes are entities (kind ∈ mint | ticker | wallet | person | strategy | topic); edges are co-occurrence links weighted by how often two entities share a memory. Lazily mines any not-yet-processed memories on read.

#memoriesFor(entityId, options?) → Promise<Memory[]>

Memories that mention a given entity node. Wraps GET /api/memory/graph?entityId=. Powers node drilldown ("what does my agent remember about this mint?").

#context() → Promise<Context>

The "in-context now" working set: pinned + working-tier memories with a live token budget. Wraps GET /api/memory/context.

type Context = {
  entries: Memory[];
  tokens: number;       // estimated tokens in the working core
  budget: number;       // 2000 — the working-core token budget
  overBudget: boolean;  // tokens > budget
  counts: { total, working, recall, archival, embedded };
};

#list(options?) → Promise<Memory[]>

Every memory for the agent (newest, salience-first). Wraps GET /api/agent-memory?agentId=. Options: type, since (ms), limit (≤ 500, default 200).

#Curation

Owner-only edits over POST /api/memory/curate:

Method Op Effect
pin(id) / unpin(id) pin / unpin Move in/out of the working core.
retier(id, tier) tier Set working | recall | archival.
setSalience(id, n) salience Set importance, 0..1.
edit(id, { content?, tags? }) edit Re-embeds + re-mines on content change.
merge([target, ...dupes]) merge Fold duplicates into one; re-indexes.
forget(id) forget Delete a memory.

#Memory shape

type Memory = {
  id: string;
  type: 'user' | 'feedback' | 'project' | 'reference';
  content: string;
  tags: string[];
  context: object;
  salience: number;            // 0..1
  tier: 'working' | 'recall' | 'archival';
  pinned: boolean;
  embedder: string | null;     // e.g. "nvidia/nv-embedqa-e5-v5@1024"
  hasEmbedding: boolean;
  accessCount: number;
  tokens: number;              // ~chars/4
  createdAt: number; updatedAt: number;
  lastAccessedAt: number | null; expiresAt: number | null;
  // recall() only:
  score?: number | null;       // cosine, or null for a lexical hit
  match?: 'semantic' | 'lexical';
};

#How it works

Three layers turn a string into durable, recall-able memory — a write path, a read-triggered indexing pass, and a query path:

 remember(text) ──▶ POST /api/agent-memory ──▶ agent_memories (Postgres)
                                                     │
        read of recall()/graph() triggers lazy indexing (best-effort, bounded)
                                                     ▼
        ┌──────────────── embed ────────────────┐   ┌──────── mine ────────┐
        │ NIM nv-embedqa-e5-v5 @1024 (free)      │   │ deterministic entity │
        │  → OpenAI text-embedding-3-small @256  │   │ extraction (no LLM)  │
        │ vector tagged with its embedder        │   │ → entity nodes+links │
        └────────────────────┬───────────────────┘   └──────────┬──────────┘
                             ▼                                   ▼
 recall(query) ──▶ cosine within the SAME vector space ──▶ ranked Memory[]
                   (lexical + salience fallback)        graph(): nodes + edges

#Auth & errors

Reads are owner-only: anonymous or non-owner requests get an empty result (an empty array / zeroed context), never a leak and never a 401 that spams a public embed console. Writes require auth and surface as a thrown MemoryError with a code:

code HTTP Meaning Recovery
unauthorized 401 Write with no session / token. Pass a token, or sign in.
forbidden 403 Agent isn't yours. Use an agentId you own.
not_found 404 Agent or memory doesn't exist. Check the id.
id_conflict 409 Upsert id already used by another agent. Omit id to mint a new one.
validation_error 400 Missing agentId / content / bad field. Fix the input.

Browser writes also need a CSRF token (handled automatically by the three.ws session cookie flow); server-to-server calls authenticate with a bearer token.

#Examples

Agent loop — remember every turn, recall before replying:

const memory = new AgentMemory({ agentId, token });

async function onUserMessage(text) {
  const recalled = await memory.recall(text, { topK: 5 });
  const reply = await llm(text, { memories: recalled.map((m) => m.content) });
  await memory.remember(`User said: ${text}\nAgent: ${reply}`, { type: 'user' });
  return reply;
}

Record a trade with structured context — it extracts the entities for you:

await memory.remember('Sniped a fresh launch, took profit at 3x', {
  tags: ['trade', 'lesson'],
  salience: 0.8,
  context: { mint: 'THREEsynthetic1111111111111111111111111111', symbol: '$THREE' },
});

const { nodes } = await memory.graph(); // mint + ticker nodes now linked

Curate the working core so it stays under budget:

const ctx = await memory.context();
if (ctx.overBudget) {
  // demote the least-salient working memory to recall
  const weakest = ctx.entries.sort((a, b) => a.salience - b.salience)[0];
  await memory.retier(weakest.id, 'recall');
}

Built by three.ws · The only coin is $THREE