@contextq/ai-sdk-memory
v0.1.0
Published
Vercel AI SDK memory tools (searchMemories, addMemory) backed by ContextQ -- a tool-provider adapter for the AI SDK memory page
Maintainers
Readme
@contextq/ai-sdk-memory
Vercel AI SDK memory tools -- searchMemories and
addMemory -- backed by ContextQ.
This is a tool provider, the same integration shape the AI SDK's own
memory page documents for
Supermemory (addMemory/searchMemories), not a language-model middleware
or provider wrapper -- there is no documented AI SDK extension point for
"read memory before generation, write after," so this package hands the
model two ordinary tools instead. Background: .contextq/tasks/tasks/T726.md
("Decisions" section) is the design record this package implements.
Install
npm install @contextq/ai-sdk-memory ai @contextq/sdkai and @contextq/sdk are peer dependencies -- bring your own versions
(tested against ai@^7.0.90, the version current when this package was
built; @contextq/sdk@^0.2.0).
Quick start
import { generateText } from 'ai';
import { contextQMemoryTools } from '@contextq/ai-sdk-memory';
const memory = contextQMemoryTools({
apiKey: process.env.CONTEXT_API_KEY!, // sk_live_<40 hex>
subjectId: 'user-42', // the end-user this conversation is with
});
// Turn 1: the model decides to remember something.
await generateText({
model: yourModel,
tools: memory,
prompt: 'Remember that I prefer dark mode, then confirm it.',
});
// Turn 2 (later, same subject): the model decides to recall it.
const { text } = await generateText({
model: yourModel,
tools: memory,
prompt: 'What UI theme do I prefer?',
});
console.log(text);contextQMemoryTools(options) returns a plain ToolSet ({ searchMemories,
addMemory }) -- pass it straight to tools on generateText/streamText,
or spread it into a larger tool set alongside your own tools.
Options
| Option | Default | Notes |
|---|---|---|
| subjectId | -- | Required. ContextQ's end-user scoping id (the same field POST /api/retrieve/POST /api/memory already accept). Fixed at factory time, not a tool input -- the model can never read or write another subject's memories. |
| apiKey | process.env.CONTEXT_API_KEY | ContextQ API key, sk_live_<40 hex>. Required (one of this, client, or the env var). |
| baseUrl | process.env.CONTEXT_API_URL || http://localhost:38200 | ContextQ API base URL. Ignored when client is given. |
| client | -- | A pre-configured @contextq/sdk ContextQ instance. Use this instead of apiKey/baseUrl to control retries, timeouts, extra headers (e.g. a gateway auth header), or a custom fetch. |
| workspace | tenant default (global when none set) | ContextQ workspace slug both tools are scoped to. |
| defaultLimit | 8 | Default top_k for searchMemories when the model omits limit. |
The two tools
searchMemories
Input: { query: string, limit?: number }.
Calls client.retrieve.query() (POST /api/retrieve) and flattens all
three response arrays -- passages (workspace/tenant context chunks),
memories (this subject's own extracted facts), and groupMemories (T414
shared account/group facts the subject belongs to) -- into one list sorted
by score, plus a single citedText block:
{
citedText: "[1] (memory, score 0.812) The user's preferred identifier is ...\n\n[2] ...",
hits: [{ ref: 1, source: 'memory', text: '...', score: 0.812, contextId: 42, title: '...' }],
empty: false,
}addMemory
Input: { content: string } -- a plain-language statement to remember.
Calls client.memory.add([{ role: 'user', content }], { subjectId,
workspace }) (POST /api/memory), ContextQ's real LLM-extraction pipeline
(Mem0-parity): it is not a raw append log -- ContextQ extracts durable
atomic facts from the text and dedupes/updates/archives against what it
already knows. A short, test-artifact-shaped string (e.g. a bare UUID with
no first-person framing) can legitimately extract zero facts; write real
statements ("My preferred X is Y") for this to do anything.
Two possible outputs, depending on the server's sync/async threshold:
{ status: 'written', created: [...], updated: [...], extractedClaims: 1 }
// or, for a long conversation the server queues:
{ status: 'queued', jobId: 99, statusUrl: '/api/memory/jobs/99' }Why subjectId is a factory option, not a tool argument
The AI SDK memory page's Mem0 row configures the equivalent (user_id) the
same way -- once, at setup, not per tool call. Exposing it as a tool
argument instead would let a model (or a prompt-injected instruction) read
or write an arbitrary subject's memories by just naming a different id in
its tool call. searchMemories/addMemory's execute closures capture
subjectId from the factory call and never read it from the model's input,
so the wire request always carries the id the calling application chose.
Development
npm install
npm test # unit tests against @contextq/sdk with a fake `fetch` -- no real ContextQ needed
npm run typecheck # typecheck (src + scripts)
npm run build # emit dist/ (src only, tests excluded)@contextq/sdk is not published to npm yet (see "What 'publishable' means
here" below), so local development resolves it via a file:../../sdk
devDependency instead of the registry. That only works when this
repository's own sdk/ directory sits two levels up, which is always true
inside a clone of this repo but NOT inside a container that mounts only
this package's own directory -- sdk/ must be built (cd ../../sdk && npm
ci && npm run build) and reachable on disk before npm ci/npm install
here can resolve it.
Live e2e
scripts/e2e-live.ts proves the full loop against a real, running local
ContextQ instance -- no fakes, no mocks:
addMemorywrites a real fact through the server's real LLM-extraction pipeline, verified independently viaGET /api/contexts(not just "the tool didn't throw").- A second, separate
searchMemoriescall retrieves that same fact via the server's real embedding-backed retrieval.
Unlike the sibling adapters/anthropic-memory-tool/scripts/e2e-live.ts,
this script never calls a real LLM API itself -- this package is a tool
provider, so calling tool.execute() directly is exactly what the AI
SDK's own tool-calling runtime does once a model decides to invoke a tool.
The ContextQ round trip on the other end (the part this package actually
wraps) is real.
Requires:
- The local ContextQ dev stack already running and reachable (default
http://localhost:38200), with both an LLM and an embedding provider configured -- checkcurl $CONTEXT_API_URL/health/deepshowsllm.statusandembedding.statusboth"ok". Without an LLM configured,addMemorystill returns 200 but extracts zero facts (server-side policy, not an adapter bug) and the script fails loudly with a message pointing at this check rather than a confusing downstream assertion. - Docker access to the
shared-context-postgrescontainer (used to create a throwaway tenant + owner user and mint an API key for them by directINSERT-- see the script's own doc comments; same sanctioned local-dev pattern the sibling adapter's e2e-live.ts uses).
CONTEXT_API_URL=http://localhost:38200 npm run e2e:liveEvery write is confined to a throwaway tenant + workspace created at the
start; cleanup runs in the script's finally block regardless of pass/fail
(the throwaway tenant row itself is left in place only if it already owns a
chained activity_logs row -- the same accepted trade-off
src/test-helpers/tenant-cleanup.ts documents, to avoid forking the global
audit hash chain).
What "publishable" means here
This package is built and tested to be published, but publishing itself
(npm publish) is deliberately out of scope -- see the repo's task tracker
(T679) for the user-owned release flow this hands off to. Note that
@contextq/sdk itself is not published to npm yet either, so a real
npm install of this package by an external user is blocked until that
release lands; the peer dependency range is set for that future state.
