feat(tools): mass forget + bucket tools, and refresh the supermemory skill

@supermemory/tools had no tool for POST /v4/memories/forget-matching or
bucketed profile reads, so agents could forget one memory at a time and
read the whole profile or nothing. Adds both, mirrored across the AI SDK
and OpenAI surfaces, over fetch — the generated SDK has no
forget-matching method and profile()'s params can't express
include/buckets. memoryForgetMatching defaults dryRun to true even
though the API defaults to false: a model shouldn't be able to
bulk-delete in one turn.

The Claude skill had drifted further. profile() was called with
instead of , the profile response shape was invented, search
responses were treated as arrays, Python used dict access on Pydantic
models, and filters used a form the API rejects. The reference
documented 3 endpoints and never mentioned forgetting at all. Rewrote it
against the route schemas in mono, with a table marking which endpoints
have SDK methods and which need HTTP.
This commit is contained in:
Soham Daga 2026-08-10 18:27:49 +05:30
parent 59b148e5b2
commit dfddc2ad06
14 changed files with 1637 additions and 782 deletions

View file

@ -669,7 +669,9 @@ interface WithSupermemoryOptions {
## Available Tools
### Search Memories
`supermemoryTools()` returns all of the following. Each is also exported individually — `searchMemoriesTool`, `memoryForgetMatchingTool`, and so on for the AI SDK, and `createSearchMemoriesFunction` / `createMemoryForgetMatchingTool` and friends for OpenAI function calling.
### Search Memories (`searchMemories`)
Searches through stored memories based on a query string.
**Parameters:**
@ -677,12 +679,51 @@ Searches through stored memories based on a query string.
- `includeFullDocs` (boolean, optional): Whether to include full document content (default: true)
- `limit` (number, optional): Maximum number of results (default: 10)
### Add Memory
### Add Memory (`addMemory`)
Adds a new memory to the system.
**Parameters:**
- `memory` (string): The content to remember
### Get Profile (`getProfile`)
Returns the user's static (long-term) and dynamic (recent) memories, optionally with search results.
**Parameters:**
- `containerTag` (string, optional): Defaults to the configured container tag
- `query` (string, optional): Also run a search and return the results
### Get Profile Buckets (`getProfileBuckets`)
Returns profile memories grouped into topical buckets (`preferences`, `goals`, `work`, …) instead of the whole profile — useful for keeping prompts small when only one slice matters.
**Parameters:**
- `containerTag` (string, optional): Defaults to the configured container tag
- `buckets` (string[], optional): Bucket keys to return. Omit for every bucket configured for the user
### Document List / Add / Delete (`documentList`, `documentAdd`, `documentDelete`)
Browse, ingest, and delete source documents. `documentDelete` removes a document and its extracted memories.
### Forget Memory (`memoryForget`)
Soft-deletes one memory by ID or exact content match.
**Parameters:**
- `containerTag` (string, optional): Defaults to the configured container tag
- `memoryId` (string): The memory to forget — or…
- `memoryContent` (string): …its exact content
- `reason` (string, optional): Recorded on the forgotten memory
### Forget Matching (`memoryForgetMatching`)
Forgets a whole topic in one call. Describe the target and matching memories are soft-deleted; pass explicit IDs to skip the search.
**Parameters:**
- `containerTag` (string, optional): Defaults to the configured container tag
- `query` (string): What to forget, e.g. `"everything about Project Titan"` — or…
- `memoryIds` (string[]): …the exact memories to forget
- `dryRun` (boolean, optional): **Defaults to `true`** — the tool previews rather than deletes
- `maxForget` (number, optional): Cap on how many memories one call may forget (default: 100)
- `reason` (string, optional): Recorded on each forgotten memory
The dry-run default is deliberate: a model gets a preview, not a deletion. The intended loop is preview → show the user the returned `memories` → call again with those `memoryIds` and `dryRun: false`. Applying with a `query` re-runs the semantic match, so it can select a different set than the preview showed.
## Claude Memory Tool

View file

@ -7,7 +7,11 @@ import {
TOOL_DESCRIPTIONS,
getContainerTags,
} from "./tools-shared"
import { forgetMemoryRequest } from "./shared/forget-memory"
import {
forgetMatchingRequest,
forgetMemoryRequest,
} from "./shared/forget-memory"
import { profileBucketsRequest } from "./shared/profile-buckets"
import type { SupermemoryToolsConfig } from "./types"
// Export individual tool creators
@ -354,6 +358,130 @@ export const memoryForgetTool = (
})
}
export const memoryForgetMatchingTool = (
apiKey: string,
config?: SupermemoryToolsConfig,
) => {
const containerTags = getContainerTags(config)
return tool({
description: TOOL_DESCRIPTIONS.memoryForgetMatching,
inputSchema: z.object({
containerTag: z
.string()
.optional()
.describe(PARAMETER_DESCRIPTIONS.containerTag),
query: z.string().optional().describe(PARAMETER_DESCRIPTIONS.forgetQuery),
memoryIds: z
.array(z.string())
.optional()
.describe(PARAMETER_DESCRIPTIONS.forgetMemoryIds),
dryRun: z
.boolean()
.optional()
.default(DEFAULT_VALUES.forgetDryRun)
.describe(PARAMETER_DESCRIPTIONS.forgetDryRun),
maxForget: z.coerce
.number()
.optional()
.default(DEFAULT_VALUES.forgetMaxForget)
.describe(PARAMETER_DESCRIPTIONS.forgetMaxForget),
reason: z.string().optional().describe(PARAMETER_DESCRIPTIONS.reason),
}),
execute: async ({
containerTag,
query,
memoryIds,
dryRun = DEFAULT_VALUES.forgetDryRun,
maxForget = DEFAULT_VALUES.forgetMaxForget,
reason,
}) => {
try {
if (!query && !memoryIds?.length) {
return {
success: false,
error: "Either query or memoryIds must be provided",
}
}
const tag = containerTag || containerTags[0]
const result = await forgetMatchingRequest(
apiKey,
{
containerTag: tag as string,
dryRun,
maxForget,
...(query && { query }),
...(memoryIds?.length && { ids: memoryIds }),
...(reason && { reason }),
},
config?.baseUrl,
)
return {
success: true,
dryRun: result.dryRun,
count: result.count,
summary: result.summary,
memories: result.candidates ?? result.forgotten ?? [],
forgetBatchId: result.forgetBatchId,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
}
},
})
}
export const getProfileBucketsTool = (
apiKey: string,
config?: SupermemoryToolsConfig,
) => {
const containerTags = getContainerTags(config)
return tool({
description: TOOL_DESCRIPTIONS.getProfileBuckets,
inputSchema: z.object({
containerTag: z
.string()
.optional()
.describe(PARAMETER_DESCRIPTIONS.containerTag),
buckets: z
.array(z.string())
.optional()
.describe(PARAMETER_DESCRIPTIONS.bucketKeys),
}),
execute: async ({ containerTag, buckets }) => {
try {
const tag = containerTag || containerTags[0]
const result = await profileBucketsRequest(
apiKey,
{
containerTag: tag as string,
...(buckets?.length && { buckets }),
},
config?.baseUrl,
)
return {
success: true,
buckets: result.buckets,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
}
},
})
}
/**
* Create Supermemory tools for AI SDK
*/
@ -365,10 +493,12 @@ export function supermemoryTools(
searchMemories: searchMemoriesTool(apiKey, config),
addMemory: addMemoryTool(apiKey, config),
getProfile: getProfileTool(apiKey, config),
getProfileBuckets: getProfileBucketsTool(apiKey, config),
documentList: documentListTool(apiKey, config),
documentDelete: documentDeleteTool(apiKey, config),
documentAdd: documentAddTool(apiKey, config),
memoryForget: memoryForgetTool(apiKey, config),
memoryForgetMatching: memoryForgetMatchingTool(apiKey, config),
}
}

View file

@ -6,7 +6,12 @@ import {
TOOL_DESCRIPTIONS,
getContainerTags,
} from "../tools-shared"
import { forgetMemoryRequest } from "../shared/forget-memory"
import {
type ForgetMatchingMemory,
forgetMatchingRequest,
forgetMemoryRequest,
} from "../shared/forget-memory"
import { profileBucketsRequest } from "../shared/profile-buckets"
import type { SupermemoryToolsConfig } from "../types"
/**
@ -62,6 +67,22 @@ export interface MemoryForgetResult {
error?: string
}
export interface MemoryForgetMatchingResult {
success: boolean
dryRun?: boolean
count?: number
summary?: string
memories?: ForgetMatchingMemory[]
forgetBatchId?: string | null
error?: string
}
export interface ProfileBucketsResult {
success: boolean
buckets?: Record<string, string[]>
error?: string
}
/**
* Function schemas for OpenAI function calling
*/
@ -213,6 +234,64 @@ export const memoryToolSchemas = {
required: [],
},
} satisfies OpenAI.FunctionDefinition,
memoryForgetMatching: {
name: "memoryForgetMatching",
description: TOOL_DESCRIPTIONS.memoryForgetMatching,
parameters: {
type: "object",
properties: {
containerTag: {
type: "string",
description: PARAMETER_DESCRIPTIONS.containerTag,
},
query: {
type: "string",
description: PARAMETER_DESCRIPTIONS.forgetQuery,
},
memoryIds: {
type: "array",
items: { type: "string" },
description: PARAMETER_DESCRIPTIONS.forgetMemoryIds,
},
dryRun: {
type: "boolean",
description: PARAMETER_DESCRIPTIONS.forgetDryRun,
default: DEFAULT_VALUES.forgetDryRun,
},
maxForget: {
type: "number",
description: PARAMETER_DESCRIPTIONS.forgetMaxForget,
default: DEFAULT_VALUES.forgetMaxForget,
},
reason: {
type: "string",
description: PARAMETER_DESCRIPTIONS.reason,
},
},
required: [],
},
} satisfies OpenAI.FunctionDefinition,
getProfileBuckets: {
name: "getProfileBuckets",
description: TOOL_DESCRIPTIONS.getProfileBuckets,
parameters: {
type: "object",
properties: {
containerTag: {
type: "string",
description: PARAMETER_DESCRIPTIONS.containerTag,
},
buckets: {
type: "array",
items: { type: "string" },
description: PARAMETER_DESCRIPTIONS.bucketKeys,
},
},
required: [],
},
} satisfies OpenAI.FunctionDefinition,
} as const
/**
@ -511,6 +590,111 @@ export function createMemoryForgetFunction(
}
}
/**
* Mass forget function
*/
export function createMemoryForgetMatchingFunction(
apiKey: string,
config?: SupermemoryToolsConfig,
) {
const containerTags = getContainerTags(config)
return async function memoryForgetMatching({
containerTag,
query,
memoryIds,
dryRun = DEFAULT_VALUES.forgetDryRun,
maxForget = DEFAULT_VALUES.forgetMaxForget,
reason,
}: {
containerTag?: string
query?: string
memoryIds?: string[]
dryRun?: boolean
maxForget?: number
reason?: string
}): Promise<MemoryForgetMatchingResult> {
try {
if (!query && !memoryIds?.length) {
return {
success: false,
error: "Either query or memoryIds must be provided",
}
}
const tag = containerTag || containerTags[0]
const result = await forgetMatchingRequest(
apiKey,
{
containerTag: tag as string,
dryRun,
maxForget,
...(query && { query }),
...(memoryIds?.length && { ids: memoryIds }),
...(reason && { reason }),
},
config?.baseUrl,
)
return {
success: true,
dryRun: result.dryRun,
count: result.count,
summary: result.summary,
memories: result.candidates ?? result.forgotten ?? [],
forgetBatchId: result.forgetBatchId,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
}
}
}
/**
* Get bucketed profile function
*/
export function createGetProfileBucketsFunction(
apiKey: string,
config?: SupermemoryToolsConfig,
) {
const containerTags = getContainerTags(config)
return async function getProfileBuckets({
containerTag,
buckets,
}: {
containerTag?: string
buckets?: string[]
}): Promise<ProfileBucketsResult> {
try {
const tag = containerTag || containerTags[0]
const result = await profileBucketsRequest(
apiKey,
{
containerTag: tag as string,
...(buckets?.length && { buckets }),
},
config?.baseUrl,
)
return {
success: true,
buckets: result.buckets,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
}
}
}
/**
* Create all memory tools functions
*/
@ -525,15 +709,22 @@ export function supermemoryTools(
const documentDelete = createDocumentDeleteFunction(apiKey, config)
const documentAdd = createDocumentAddFunction(apiKey, config)
const memoryForget = createMemoryForgetFunction(apiKey, config)
const memoryForgetMatching = createMemoryForgetMatchingFunction(
apiKey,
config,
)
const getProfileBuckets = createGetProfileBucketsFunction(apiKey, config)
return {
searchMemories,
addMemory,
getProfile,
getProfileBuckets,
documentList,
documentDelete,
documentAdd,
memoryForget,
memoryForgetMatching,
}
}
@ -549,6 +740,8 @@ export function getToolDefinitions(): OpenAI.Chat.Completions.ChatCompletionTool
{ type: "function", function: memoryToolSchemas.documentDelete },
{ type: "function", function: memoryToolSchemas.documentAdd },
{ type: "function", function: memoryToolSchemas.memoryForget },
{ type: "function", function: memoryToolSchemas.memoryForgetMatching },
{ type: "function", function: memoryToolSchemas.getProfileBuckets },
]
}
@ -582,6 +775,10 @@ export function createToolCallExecutor(
return JSON.stringify(await tools.documentAdd(args))
case "memoryForget":
return JSON.stringify(await tools.memoryForget(args))
case "memoryForgetMatching":
return JSON.stringify(await tools.memoryForgetMatching(args))
case "getProfileBuckets":
return JSON.stringify(await tools.getProfileBuckets(args))
default:
return JSON.stringify({
success: false,
@ -725,3 +922,36 @@ export function createMemoryForgetTool(
execute: memoryForget,
}
}
export function createMemoryForgetMatchingTool(
apiKey: string,
config?: SupermemoryToolsConfig,
) {
const memoryForgetMatching = createMemoryForgetMatchingFunction(
apiKey,
config,
)
return {
definition: {
type: "function" as const,
function: memoryToolSchemas.memoryForgetMatching,
},
execute: memoryForgetMatching,
}
}
export function createGetProfileBucketsTool(
apiKey: string,
config?: SupermemoryToolsConfig,
) {
const getProfileBuckets = createGetProfileBucketsFunction(apiKey, config)
return {
definition: {
type: "function" as const,
function: memoryToolSchemas.getProfileBuckets,
},
execute: getProfileBuckets,
}
}

View file

@ -36,3 +36,68 @@ export async function forgetMemoryRequest(
)
}
}
export interface ForgetMatchingParams {
containerTag: string
/** Topic or instruction to forget. Provide either this or `ids`. */
query?: string
/** Exact memory ids to forget, skipping the semantic search. Max 500. */
ids?: string[]
/** Preview without mutating. */
dryRun?: boolean
/** Similarity floor for candidates, 0-1. Lower casts a wider net. */
threshold?: number
/** Safety cap on how many memories a query-mode call may forget, 1-500. */
maxForget?: number
reason?: string
}
export interface ForgetMatchingMemory {
id: string
memory: string
score: number
}
export interface ForgetMatchingResponse {
dryRun: boolean
count: number
/** Tagged on every memory forgotten in this call. Null on a dry run. */
forgetBatchId: string | null
summary: string
/** Present on a dry run: what would be forgotten. */
candidates?: ForgetMatchingMemory[]
/** Present on apply: what was forgotten. */
forgotten?: ForgetMatchingMemory[]
}
/**
* Mass-forgets memories via `POST /v4/memories/forget-matching`.
*
* With `query`, the service semantically searches the container and an LLM
* picks the memories genuinely about the target; with `ids`, it forgets exactly
* those. Applying a `query` re-runs the match, so to delete precisely what a
* dry run showed, pass that preview's ids back as `ids`.
*/
export async function forgetMatchingRequest(
apiKey: string,
params: ForgetMatchingParams,
baseUrl: string = DEFAULT_BASE_URL,
): Promise<ForgetMatchingResponse> {
const response = await fetch(`${baseUrl}/v4/memories/forget-matching`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(params),
})
if (!response.ok) {
const errorText = await response.text().catch(() => "Unknown error")
throw new Error(
`Supermemory forget matching failed: ${response.status} ${response.statusText}. ${errorText}`,
)
}
return await response.json()
}

View file

@ -0,0 +1,52 @@
const DEFAULT_BASE_URL = "https://api.supermemory.ai"
export interface ProfileBucketsParams {
containerTag: string
/** Bucket keys to return. Omit for every bucket configured for the tag. */
buckets?: string[]
}
export interface ProfileBucketsResponse {
/** Memory lists keyed by bucket key. */
buckets: Record<string, string[]>
}
/**
* Reads bucket-organized profile memories via `POST /v4/profile` with
* `include: ["buckets"]`, which skips the static and dynamic sections entirely.
*
* The supermemory SDK's `profile()` params don't cover `include`/`buckets`, so
* the endpoint is called directly the same pattern the middleware already
* uses for `/v4/profile`.
*/
export async function profileBucketsRequest(
apiKey: string,
params: ProfileBucketsParams,
baseUrl: string = DEFAULT_BASE_URL,
): Promise<ProfileBucketsResponse> {
const response = await fetch(`${baseUrl}/v4/profile`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
containerTag: params.containerTag,
include: ["buckets"],
...(params.buckets?.length ? { buckets: params.buckets } : {}),
}),
})
if (!response.ok) {
const errorText = await response.text().catch(() => "Unknown error")
throw new Error(
`Supermemory profile buckets failed: ${response.status} ${response.statusText}. ${errorText}`,
)
}
const body = (await response.json()) as {
profile?: { buckets?: Record<string, string[]> }
}
return { buckets: body.profile?.buckets ?? {} }
}

View file

@ -158,6 +158,149 @@ describe("memoryForget", () => {
})
})
describe("memoryForgetMatching", () => {
function stubFetch(
body: unknown = {
dryRun: true,
count: 1,
forgetBatchId: null,
summary: "Selected 1 memory about Project Titan",
candidates: [{ id: "mem_1", memory: "Titan ships in Q3", score: 0.7 }],
},
) {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response(JSON.stringify(body), { status: 200 }))
vi.stubGlobal("fetch", fetchMock)
return fetchMock
}
it("previews by default rather than deleting", async () => {
const fetchMock = stubFetch()
const tool = aiSdk.memoryForgetMatchingTool(API_KEY, {
containerTags: ["user_1"],
})
const result = (await executeTool(tool, {
query: "everything about Project Titan",
})) as { success: boolean; dryRun: boolean; memories: unknown[] }
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toBe("https://api.supermemory.ai/v4/memories/forget-matching")
expect(JSON.parse(init.body as string)).toEqual({
containerTag: "user_1",
dryRun: true,
maxForget: 100,
query: "everything about Project Titan",
})
expect(result.success).toBe(true)
expect(result.dryRun).toBe(true)
expect(result.memories).toHaveLength(1)
})
it("applies an explicit id list when dryRun is turned off", async () => {
const fetchMock = stubFetch({
dryRun: false,
count: 2,
forgetBatchId: "batch_1",
summary: "Forgot 2 memories",
forgotten: [
{ id: "mem_1", memory: "a", score: 1 },
{ id: "mem_2", memory: "b", score: 1 },
],
})
const memoryForgetMatching =
openAi.createMemoryForgetMatchingFunction(API_KEY)
const result = await memoryForgetMatching({
containerTag: "user_3",
memoryIds: ["mem_1", "mem_2"],
dryRun: false,
reason: "project cancelled",
})
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(JSON.parse(init.body as string)).toMatchObject({
containerTag: "user_3",
ids: ["mem_1", "mem_2"],
dryRun: false,
reason: "project cancelled",
})
expect(result.count).toBe(2)
expect(result.forgetBatchId).toBe("batch_1")
})
it("requires a query or ids", async () => {
const fetchMock = stubFetch()
const memoryForgetMatching =
openAi.createMemoryForgetMatchingFunction(API_KEY)
const result = await memoryForgetMatching({ containerTag: "user_1" })
expect(result.success).toBe(false)
expect(fetchMock).not.toHaveBeenCalled()
})
})
describe("getProfileBuckets", () => {
function stubFetch(
body: unknown = {
profile: { buckets: { preferences: ["Likes dark mode"] } },
},
) {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response(JSON.stringify(body), { status: 200 }))
vi.stubGlobal("fetch", fetchMock)
return fetchMock
}
it("requests only the bucket section of the profile", async () => {
const fetchMock = stubFetch()
const tool = aiSdk.getProfileBucketsTool(API_KEY, {
containerTags: ["user_1"],
})
const result = (await executeTool(tool, {
buckets: ["preferences"],
})) as { success: boolean; buckets: Record<string, string[]> }
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toBe("https://api.supermemory.ai/v4/profile")
expect(JSON.parse(init.body as string)).toEqual({
containerTag: "user_1",
include: ["buckets"],
buckets: ["preferences"],
})
expect(result.buckets).toEqual({ preferences: ["Likes dark mode"] })
})
it("omits the bucket filter to return every configured bucket", async () => {
const fetchMock = stubFetch()
const getProfileBuckets = openAi.createGetProfileBucketsFunction(API_KEY, {
containerTags: ["user_2"],
})
await getProfileBuckets({})
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(JSON.parse(init.body as string)).toEqual({
containerTag: "user_2",
include: ["buckets"],
})
})
it("returns an empty bucket map when the profile has none", async () => {
stubFetch({ profile: {} })
const getProfileBuckets = openAi.createGetProfileBucketsFunction(API_KEY)
const result = await getProfileBuckets({ containerTag: "user_1" })
expect(result.success).toBe(true)
expect(result.buckets).toEqual({})
})
})
describe("ClaudeMemoryTool", () => {
const FILE_PATH = "/memories/prefs.txt"
const CUSTOM_ID = "memories_prefs_txt"

View file

@ -20,6 +20,10 @@ export const TOOL_DESCRIPTIONS = {
"Add a new document (URL, text, or content) to memory. The content is queued for processing, and memories will be extracted automatically.",
memoryForget:
"Forget (soft delete) a specific memory by ID or content match. The memory is marked as forgotten but not permanently deleted. Use when user wants to remove specific information from their profile.",
memoryForgetMatching:
"Forget a whole topic at once — describe what to forget ('everything about Project Titan') and matching memories are soft deleted. Runs as a preview by default: review the returned candidates with the user, then call again with their ids and dryRun false to apply. Use when the user wants a subject removed rather than one specific memory.",
getProfileBuckets:
"Get profile memories grouped into topical buckets (e.g. preferences, goals, work) instead of the full profile. Use when only one slice of context is relevant, to keep the prompt small. Omit bucket keys to see every bucket configured for this user.",
} as const
// Parameter descriptions
@ -41,6 +45,16 @@ export const PARAMETER_DESCRIPTIONS = {
memoryContent:
"Exact content match of the memory entry to operate on (alternative to ID)",
reason: "Optional reason for forgetting this memory",
forgetQuery:
"What to forget, as a topic or instruction (e.g. 'everything about Project Titan'). Leave empty when passing explicit memoryIds.",
forgetMemoryIds:
"Exact memory ids to forget, skipping the search. Use the ids from a preview to apply exactly what was reviewed.",
forgetDryRun:
"Preview without deleting anything. Defaults to true — only set false once the user has confirmed the specific memories to forget.",
forgetMaxForget:
"Maximum number of memories this call may forget (1-500). Lower it when the topic is broad.",
bucketKeys:
"Bucket keys to return (e.g. ['preferences', 'goals']). Omit to return every bucket configured for this user.",
} as const
// Default values
@ -48,6 +62,9 @@ export const DEFAULT_VALUES = {
includeFullDocs: true,
limit: 10,
chunkThreshold: 0.6,
/** Mass forget previews by default so a model can't delete in one shot. */
forgetDryRun: true,
forgetMaxForget: 100,
} as const
// Container tag constants

View file

@ -114,18 +114,23 @@ Ready-to-use code snippets for TypeScript and Python showing the basic workflow:
- Store new memories
### 2. Complete SDK Documentation
Full reference for all SDK methods:
- `add()` - Store memories
Full reference for all SDK methods, plus which endpoints have no SDK method yet and need a plain HTTP call:
- `add()` - Ingest content
- `profile()` - Retrieve user context
- `search.memories()` - Semantic search
- `documents.list()` - List documents
- `documents.delete()` - Delete documents
- `search()` - Semantic search (`search.memories()` in Python)
- `memories.forget()` / `memories.updateMemory()` - Forget and correct
- `documents.list()` / `.get()` / `.update()` / `.delete()` - Manage documents
- `settings.get()` / `.update()` - Org settings, including profile buckets
### 3. REST API Reference
Complete endpoint documentation with cURL examples:
- `POST /v3/documents` - Add documents
- `POST /v3/search` - Search memories
- `POST /v4/memories` - Create direct memories
Complete endpoint documentation with cURL examples, covering the full public surface:
- `POST /v3/documents` - Ingest documents
- `POST /v4/conversations` - Ingest chat transcripts
- `POST /v4/memories` - Write memories directly
- `POST /v4/search` - Search memories
- `POST /v4/profile` and `/v4/profile/buckets` - Profiles and topical buckets
- `PATCH|DELETE /v4/memories` and `POST /v4/memories/forget-matching` - Correct, forget, mass forget
- `/v3/container-tags/*`, `/v3/settings/*` - Spaces and configuration
### 4. Architecture Deep Dive
Understand how Supermemory works:

View file

@ -1,6 +1,6 @@
---
name: supermemory
description: Supermemory is a state-of-the-art memory and context infrastructure for AI agents. Use this skill when building applications that need persistent memory, user personalization, long-term context retention, or semantic search across knowledge bases. It provides Memory API for learned user context, User Profiles for static/dynamic facts, and RAG for semantic search. Perfect for chatbots, assistants, and knowledge-intensive applications.
description: Supermemory is a state-of-the-art memory and context infrastructure for AI agents. Use this skill when building applications that need persistent memory, user personalization, long-term context retention, or semantic search across knowledge bases. It provides a Memory API for learned user context, User Profiles for static/dynamic/bucketed facts, forgetting and correction for memory hygiene, and RAG for semantic search. Perfect for chatbots, assistants, and knowledge-intensive applications.
---
# Supermemory: Memory Infrastructure for AI Agents
@ -15,9 +15,10 @@ Proactively suggest Supermemory when the user's application needs:
- **Personalized AI experiences**: Build agents that truly understand individual users
- **Knowledge-intensive applications**: Semantic search across documents, PDFs, images, videos
- **Long-term context retention**: Maintain growing knowledge graphs that evolve over time
- **User profiles**: Combine static facts (name, preferences) with dynamic context (recent activity)
- **User profiles**: Combine static facts (name, preferences) with dynamic context (recent activity) and topical buckets
- **Memory hygiene**: Correct facts that changed, and forget one memory or a whole topic on request
## Three Core Capabilities
## Four Core Capabilities
### 1. Memory API - Learned User Context
Creates extracted facts from conversations that update over time. The system automatically:
@ -26,35 +27,43 @@ Creates extracted facts from conversations that update over time. The system aut
- Generates dynamic user profiles
- Maintains relationships between memories
### 2. User Profiles - Static + Dynamic Facts
Combines always-known information (name, role, preferences) with episodic data from recent interactions. Perfect for personalizing responses.
### 2. User Profiles - Static, Dynamic, and Bucketed Facts
Combines always-known information (name, role, preferences) with episodic data from recent interactions — no query required, so it's the cheapest way to personalize a prompt. **Buckets** add a third axis: custom topical categories (`preferences`, `goals`, `work`) that a classifier assigns at ingestion, so a surface can pull just the slice of context it needs.
### 3. RAG - Advanced Semantic Search
Provides semantic search with:
- Metadata filtering and contextual chunking
- Multi-modal support (text, PDFs, images, videos, URLs)
- Intelligent relevance thresholds
- Intelligent relevance thresholds, optional re-ranking and query rewriting
- Graph-based relationships between documents
### 4. Forgetting and Correction - Memory Hygiene
Memories are versioned and soft-deleted, so the store can be corrected rather than just appended to:
- Update a fact and the new version supersedes the old one, history intact
- Forget a single memory by ID or exact content
- **Mass forget**: give a topic ("forget everything about Project Titan") and an agent selects the matching memories and soft-deletes them — with a dry-run preview and a safety cap
## Quick Integration Examples
### TypeScript (Vercel AI SDK)
### TypeScript
```typescript
import { Supermemory } from 'supermemory';
import Supermemory from 'supermemory';
const client = new Supermemory({
apiKey: process.env.SUPERMEMORY_API_KEY
});
// 1. Retrieve personalized context
const context = await client.profile({
const { profile } = await client.profile({
containerTag: "user_123",
query: "What are my preferences?"
q: "What are my preferences?"
});
// 2. Enrich your prompt with context
const systemMessage = `User Profile: ${context.profile}
Relevant Memories: ${context.memories.join('\n')}`;
const systemMessage = [
`Long-term facts:\n${(profile.static ?? []).map(f => `- ${f}`).join('\n')}`,
`Recent context:\n${(profile.dynamic ?? []).map(f => `- ${f}`).join('\n')}`
].join('\n\n');
// 3. Store new memories after conversation
await client.add({
@ -71,19 +80,36 @@ from supermemory import Supermemory
client = Supermemory(api_key=os.environ["SUPERMEMORY_API_KEY"])
# Retrieve context
context = client.profile(
response = client.profile(
container_tag="user_123",
query="What are my preferences?"
q="What are my preferences?",
)
print(response.profile.static, response.profile.dynamic)
# Add memories
client.add(
content=conversation_text,
container_tag="user_123",
metadata={"type": "conversation"}
metadata={"type": "conversation"},
)
```
### Forgetting
```typescript
// One memory
await client.memories.forget({ containerTag: "user_123", id: "mem_abc123" });
// A whole topic — preview first, then apply exactly what you reviewed
const preview = await fetch("https://api.supermemory.ai/v4/memories/forget-matching", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify({ containerTag: "user_123", query: "Project Titan", dryRun: true })
}).then(r => r.json());
```
Mass forget has no SDK method yet — call it over HTTP. See `references/sdk-guide.md` for the full list of HTTP-only endpoints.
## Key Value Propositions
1. **Zero-boilerplate personalization**: Just a few lines of code to add persistent memory
@ -117,14 +143,18 @@ See `references/quickstart.md` for complete setup instructions.
## Integration Patterns
**For Chatbots**: Use `profile()` before each response to get user context, then `add()` after conversations
**For Chatbots**: Use `profile()` before each response to get user context, then `add()` after conversations — or `POST /v4/conversations` to send the structured transcript and keep roles, tool calls, and images intact
**For Knowledge Bases (RAG)**: Use `add()` for ingestion, then `search.memories({ q, searchMode: "hybrid" })` for retrieval with combined semantic + keyword search
**For Knowledge Bases (RAG)**: Use `add()` for ingestion, then `client.search({ q, searchMode: "hybrid" })` for retrieval with combined memory + chunk results
**For Task Assistants**: Combine user profiles with document search for context-aware task completion
**For Customer Support**: Index documentation and tickets, retrieve relevant knowledge per customer
**For "forget what I told you"**: Expose forgetting as a tool. Single memory → `memories.forget()`; a whole topic → `forget-matching` with `dryRun: true`, show the user what matched, then apply with the returned `ids`
**For agent frameworks**: Use `@supermemory/tools` instead of hand-rolling calls — ready-made tools and memory-injecting middleware for Vercel AI SDK, OpenAI, Mastra, and VoltAgent
## Reference Documentation
- **Quickstart Guide**: `references/quickstart.md` - Complete setup walkthrough
@ -135,11 +165,12 @@ See `references/quickstart.md` for complete setup instructions.
## Best Practices
1. **Container Tags**: Use consistent user/project IDs as containerTags for proper isolation
2. **Metadata**: Add custom metadata for advanced filtering (source, type, timestamp)
3. **Thresholds**: Start with `threshold: 0.3` for balanced precision/recall
4. **Static Memories**: Mark permanent facts as `isStatic: true` for better performance
1. **Container Tags**: Use consistent user/project IDs as containerTags for proper isolation. If one entity ends up split across two tags, merge them rather than re-ingesting
2. **Metadata**: Add custom metadata for advanced filtering (source, type, timestamp). Filter conditions must be wrapped in `AND`/`OR` arrays
3. **Thresholds**: 0.30.5 favours recall, 0.50.7 is balanced, 0.7+ favours precision. Tune against real queries
4. **Static Memories**: Mark genuine identity traits as `isStatic: true` (name, hometown, profession) — not things that should be allowed to age
5. **Batch Operations**: Use bulk endpoints for multiple documents
6. **Destructive Operations**: Always `dryRun` a mass forget first, and apply with the `ids` from the preview so the delete is bound to exactly what was reviewed
## Integration Ecosystem

File diff suppressed because it is too large Load diff

View file

@ -200,6 +200,18 @@ When querying, you can choose:
- Full version history
- Specific version
`PATCH /v4/memories` is what creates a version: it writes a new memory carrying `parentMemoryId` and `rootMemoryId`, and the old version stops being the latest. `POST /v4/memories/list` returns entries with their history, which is what an audit or review UI should read.
### Forgetting
Forgetting is a soft delete — the row survives, marked forgotten, so a correction is auditable and reversible rather than a hole in the graph:
- **Single memory**: `DELETE /v4/memories` by `id` or exact `content`, with an optional `reason` stored as `forgetReason`
- **Scheduled**: set `forgetAfter` (ISO datetime) at write or update time and the memory is auto-forgotten when it expires
- **Mass forget**: `POST /v4/memories/forget-matching` takes a topic or instruction, semantically searches the container, and an LLM decides which candidates are genuinely about the target. Every memory forgotten in one call shares a `forgetBatchId` for traceability
The mass path is bounded by design: `threshold` sets the similarity floor, `maxForget` caps the blast radius, `dryRun` previews without mutating, and the LLM only ever sees opaque handles for memories a search already returned — so it cannot reach outside those results or outside the `containerTag`.
## Retrieval Mechanism
### Semantic Search Process
@ -309,25 +321,36 @@ Recent Activity:
- Discussed performance optimization (last week)
```
**Combined Profile:**
**Buckets** (custom topical categories, assigned by a classifier at ingestion):
```
preferences: Dark mode, TypeScript, Vim keybindings
work: Senior Software Engineer, currently on the auth revamp
goals: Wants to ship SSO this quarter
```
Buckets are a second axis over the same memories: static/dynamic splits by how long-lived a fact is, buckets split by subject. They're defined per-org (and extended per container tag), so a surface can request just the slice it needs.
**Combined Profile** (what `POST /v4/profile` actually returns):
```javascript
{
"profile": "John Doe, Senior Software Engineer who prefers TypeScript and dark mode",
"memories": [
{
"content": "Currently working on React authentication",
"score": 0.95,
"timestamp": "2 hours ago"
},
{
"content": "Completed advanced TypeScript course",
"score": 0.87,
"timestamp": "yesterday"
}
]
"profile": {
"static": ["John Doe, Senior Software Engineer in Seattle"],
"dynamic": ["[Recent] [2026-08-09] Working on React authentication"],
"buckets": { "preferences": ["[Summary] Prefers dark mode and TypeScript"] }
},
"searchResults": { // only when a query `q` was provided
"results": [
{ "id": "mem_abc", "memory": "Currently working on React authentication",
"similarity": 0.95, "updatedAt": "2026-08-10T10:00:00Z", "metadata": null }
],
"total": 1,
"timing": 88
}
}
```
Entries prefixed `[Summary]` are aggregated older context; `[Recent]` entries arrived since the last aggregation. `buckets` appears only when requested.
## Graph Evolution
The knowledge graph continuously evolves:

View file

@ -51,7 +51,7 @@ SUPERMEMORY_API_KEY=your_api_key_here
### TypeScript Example
```typescript
import { Supermemory } from 'supermemory';
import Supermemory from 'supermemory';
const client = new Supermemory({
apiKey: process.env.SUPERMEMORY_API_KEY
@ -73,10 +73,10 @@ async function main() {
// 2. Enrich your LLM prompt
const systemMessage = `
Static Profile:
${response.profile.static.map(f => `- ${f}`).join('\n')}
${(response.profile.static ?? []).map(f => `- ${f}`).join('\n')}
Recent Context:
${response.profile.dynamic.map(f => `- ${f}`).join('\n')}
${(response.profile.dynamic ?? []).map(f => `- ${f}`).join('\n')}
`;
// Send systemMessage to your LLM...
@ -112,14 +112,14 @@ def main():
q="What does the user prefer?"
)
print("Static Profile:", response["profile"]["static"])
print("Dynamic Profile:", response["profile"]["dynamic"])
if "searchResults" in response:
print("Search Results:", response["searchResults"]["results"])
print("Static Profile:", response.profile.static)
print("Dynamic Profile:", response.profile.dynamic)
if response.search_results:
print("Search Results:", response.search_results.results)
# 2. Enrich your LLM prompt
static_facts = "\n".join(f"- {fact}" for fact in response["profile"]["static"])
dynamic_facts = "\n".join(f"- {fact}" for fact in response["profile"]["dynamic"])
static_facts = "\n".join(f"- {fact}" for fact in response.profile.static or [])
dynamic_facts = "\n".join(f"- {fact}" for fact in response.profile.dynamic or [])
system_message = f"""
Static Profile:
@ -163,7 +163,7 @@ async def main():
q="What does the user prefer?"
)
print("User facts:", response["profile"]["static"])
print("User facts:", response.profile.static)
# 2. Store new memories
await client.add(
@ -206,7 +206,7 @@ Control relevance strictness with the `threshold` parameter:
```typescript
const context = await client.profile({
containerTag: "user_123",
query: "user preferences",
q: "user preferences",
threshold: 0.7 // 0-1: higher = stricter matching
});
```
@ -217,10 +217,11 @@ const context = await client.profile({
## Next Steps
- **User Profiles**: Learn about static vs. dynamic facts
- **Search API**: Explore advanced filtering and metadata queries
- **Document Ingestion**: Add PDFs, images, videos, and URLs
- **Integration Guides**: Connect with Vercel AI SDK, LangChain, CrewAI
- **User Profiles**: Learn about static vs. dynamic facts, and topical buckets
- **Search API**: Explore advanced filtering, re-ranking, and query rewriting
- **Document Ingestion**: Add PDFs, images, videos, URLs, and chat transcripts
- **Forgetting**: Correct facts that changed, and forget a memory or a whole topic
- **Integration Guides**: Connect with Vercel AI SDK, LangChain, CrewAI — or use `@supermemory/tools`
## Common Patterns
@ -229,7 +230,7 @@ const context = await client.profile({
// Before generating response
const context = await client.profile({
containerTag: userId,
query: userMessage
q: userMessage
});
// After receiving LLM response
@ -262,13 +263,32 @@ const response = await client.search({
// Get user profile
const profile = await client.profile({
containerTag: userId,
query: "user interests and preferences"
q: "user interests and preferences"
});
// Use profile to personalize recommendations
const recommendations = generateRecommendations(profile);
```
### Correcting and Forgetting
```typescript
// A fact changed → new version supersedes the old one, history preserved
await client.memories.updateMemory({
containerTag: userId,
id: "mem_abc123",
newContent: "Now prefers light mode"
});
// Should never have been stored → forget it
await client.memories.forget({
containerTag: userId,
id: "mem_abc123",
reason: "user asked"
});
```
To forget a whole topic at once, use `POST /v4/memories/forget-matching` with `dryRun: true`, review what matched, then apply with the returned `ids`. It has no SDK method yet — see `sdk-guide.md`.
## Troubleshooting
**API Key Not Working**
@ -284,7 +304,7 @@ const recommendations = generateRecommendations(profile);
**Slow Processing**
- Large PDFs (100 pages) take 1-2 minutes
- Videos take 5-10 minutes
- Check document status with `documents.list()`
- Check document status with `documents.listProcessing()`
## Support

View file

@ -2,9 +2,9 @@
Complete reference for the Supermemory SDK in TypeScript and Python.
## Installation
Method availability below was verified against `supermemory@4.25.4` (npm) and `supermemory==3.56.0` (PyPI). The SDKs are generated from the OpenAPI spec and trail the API slightly — anything marked "no SDK method" is still callable over HTTP, and may have gained a method in a newer release.
Supermemory works with the following SDKs natively:
## Installation
### TypeScript/JavaScript
```bash
@ -34,11 +34,12 @@ Discover all available SDKs, community integrations, and framework-specific guid
### TypeScript
```typescript
import { Supermemory } from 'supermemory';
import Supermemory from 'supermemory'; // default export
// import { Supermemory } from 'supermemory'; // named export also works
const client = new Supermemory({
apiKey: process.env.SUPERMEMORY_API_KEY, // Optional if env var is set
baseURL: 'https://api.supermemory.ai' // Optional, defaults to this
baseURL: 'https://api.supermemory.ai' // Optional, defaults to this
});
```
@ -60,31 +61,55 @@ async_client = AsyncSupermemory(
)
```
## Method map
| Task | TypeScript | Python |
|---|---|---|
| Ingest content | `client.add()` / `client.documents.add()` | `client.add()` / `client.documents.add()` |
| Batch ingest | `client.documents.batchAdd()` | `client.documents.batch_add()` |
| Upload a file | `client.documents.uploadFile()` | `client.documents.upload_file()` |
| Search memories | `client.search()` | `client.search.memories()` |
| Get a profile | `client.profile()` | `client.profile()` |
| Forget a memory | `client.memories.forget()` | `client.memories.forget()` |
| Update a memory | `client.memories.updateMemory()` | `client.memories.update_memory()` |
| List documents | `client.documents.list()` | `client.documents.list()` |
| Get / update / delete a document | `client.documents.get()` / `.update()` / `.delete()` | `client.documents.get()` / `.update()` / `.delete()` |
| Bulk delete documents | `client.documents.deleteBulk()` | `client.documents.delete_bulk()` |
| Ingestion status | `client.documents.listProcessing()` | `client.documents.list_processing()` |
| Org settings | `client.settings.get()` / `.update()` | `client.settings.get()` / `.update()` |
| Connections | `client.connections.*` | `client.connections.*` |
No SDK method yet — [call these over HTTP](#endpoints-without-sdk-methods): direct memory writes, mass forget, memory listing, profile buckets, conversation ingestion, container tags, settings reset, bucket suggestions.
## Core Methods
### `add()` - Store Memories
### `add()` - Store Content
Add content to Supermemory for processing and memory extraction.
Queue content for processing and memory extraction. `client.add()` and `client.documents.add()` are the same endpoint (`POST /v3/documents`).
#### TypeScript
```typescript
await client.add({
content: string | URL, // Required: text, URL, or file path
containerTag?: string, // Optional: isolation identifier
entityContext?: string, // Optional: context for memory extraction
customId?: string, // Optional: your custom identifier
metadata?: Record<string, any> // Optional: custom key-value pairs
content: string, // Required: text, URL, or file reference
containerTag?: string, // Optional: isolation identifier
customId?: string, // Optional: your idempotency key
metadata?: Record<string, string | number | boolean | string[]>,
entityContext?: string, // Optional: context that steers extraction (max 1500 chars)
filepath?: string, // Optional: virtual path
taskType?: "memory" | "superrag" // Optional: defaults to "memory"
});
// Returns: { id: string, status: string }
```
#### Python
```python
client.add(
content=str | url, # Required: text, URL, or file path
container_tag=str, # Optional: isolation identifier
entity_context=str, # Optional: context for memory extraction
custom_id=str, # Optional: your custom identifier
metadata=dict # Optional: custom key-value pairs
content=str, # Required: text, URL, or file reference
container_tag=str, # Optional: isolation identifier
custom_id=str, # Optional: your idempotency key
metadata=dict, # Optional: custom key-value pairs
entity_context=str, # Optional: context that steers extraction
task_type=str, # Optional: "memory" (default) or "superrag"
)
```
@ -112,41 +137,39 @@ await client.add({
});
```
**Add with custom ID:**
**Add with custom ID (idempotent):**
```typescript
await client.add({
content: "Project requirements document...",
containerTag: "project_abc",
customId: "requirements_v1",
customId: "requirements_v1", // re-posting this ID updates the same document
metadata: { version: "1.0", author: "john@example.com" }
});
```
Ingestion is asynchronous. `status` comes back as `"queued"`; poll `client.documents.listProcessing()` or `client.documents.get(id)` to see when memories are available.
### `profile()` - Retrieve User Context
Get personalized context including static profile data and relevant dynamic memories.
Pre-computed facts for a container tag. The cheapest way to personalize a prompt — no query required.
#### TypeScript
```typescript
const response = await client.profile({
containerTag: string, // Required: user/project identifier
q?: string, // Optional: search query to include search results
threshold?: number // Optional: relevance threshold (0-1, default 0.5)
q?: string, // Optional: also run a search, returned as searchResults
threshold?: number, // Optional: similarity floor for searchResults (0-1)
filters?: FilterObject // Optional: metadata filters, see "Metadata Filtering"
});
// Returns:
// {
// profile: {
// static: string[], // Array of static memories (permanent facts)
// dynamic: string[] // Array of dynamic memories (recent context)
// static?: string[], // Long-term facts (name, profession, stable preferences)
// dynamic?: string[] // Recent context, prefixed [Recent] [YYYY-MM-DD]
// },
// searchResults?: { // Only included if q parameter was provided
// results: Array<{ // Search results
// id: string,
// memory?: string,
// similarity: number,
// metadata: object | null
// }>,
// searchResults?: { // Only when q was provided
// results: Array<{ id, memory?, similarity, updatedAt, metadata }>,
// total: number,
// timing: number
// }
@ -157,78 +180,99 @@ const response = await client.profile({
```python
response = client.profile(
container_tag=str, # Required: user/project identifier
q=str, # Optional: search query to include search results
threshold=float # Optional: relevance threshold (0-1, default 0.5)
q=str, # Optional: also run a search
threshold=float, # Optional: similarity floor for search results (0-1)
filters=dict, # Optional: metadata filters
)
# Returns dict:
# {
# "profile": {
# "static": List[str], # Array of static memories (permanent facts)
# "dynamic": List[str] # Array of dynamic memories (recent context)
# },
# "searchResults": { # Only included if q parameter was provided
# "results": List[dict], # Search results
# "total": int,
# "timing": int
# }
# }
# Returns a model, accessed by attribute:
# response.profile.static -> list[str]
# response.profile.dynamic -> list[str]
# response.search_results -> present only when q was provided
```
#### Examples
**Get user profile:**
**Profile with a query:**
```typescript
const response = await client.profile({
containerTag: "user_123",
q: "What are the user's preferences and settings?"
});
console.log(response.profile.static); // ["User John Doe", "Prefers dark mode", ...]
console.log(response.profile.dynamic); // ["Recently mentioned...", "Last conversation..."]
console.log(response.searchResults); // Search results for the query (if provided)
console.log(response.profile.static); // ["John Doe, staff engineer in Seattle", ...]
console.log(response.profile.dynamic); // ["[Recent] [2026-08-09] Switched their editor to Zed"]
console.log(response.searchResults); // Query-relevant memories
```
**Profile without search (just get stored memories):**
**Profile without a query:**
```typescript
const response = await client.profile({
containerTag: "user_456"
// No q parameter = only returns profile.static and profile.dynamic
});
const response = await client.profile({ containerTag: "user_456" });
console.log(response.profile.static); // All static facts
console.log(response.profile.dynamic); // Recent dynamic memories
// response.searchResults will be undefined
console.log(response.profile.static); // All long-term facts
console.log(response.profile.dynamic); // Recent context
// response.searchResults is undefined
```
**Bucketed profile (HTTP — `include`/`buckets` are not in the SDK types yet):**
```typescript
const res = await fetch("https://api.supermemory.ai/v4/profile", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
containerTag: "user_123",
include: ["buckets"], // omits static + dynamic entirely
buckets: ["preferences", "goals"], // omit for all configured buckets
}),
});
const { profile } = await res.json();
console.log(profile.buckets.preferences);
```
Entries prefixed `[Summary]` are aggregated older context; `[Recent]` entries arrived since the last aggregation. Strip the prefixes for raw text, or keep them to signal recency.
### `search()` - Semantic Search
Search across memories using semantic understanding, not just keywords. `client.search.memories()` and `client.search.documents()` still work (deprecated) but `client.search()` is the current, recommended call — Python keeps `client.search.memories()`.
Semantic search over memories, not keyword matching. In TypeScript the client's `search` is callable — `client.search({...})` hits `/v4/search`, the current endpoint. `client.search.memories()` is an alias for it; `client.search.documents()` and `client.search.execute()` are deprecated and hit the legacy `/v3/search`. In Python, `client.search` is not callable — use `client.search.memories()`.
#### TypeScript
```typescript
const response = await client.search({
q: string, // Required: search query
containerTag?: string, // Optional: filter by container tag
limit?: number, // Optional: max results (default 10)
threshold?: number, // Optional: similarity threshold (0-1, default 0.5)
searchMode?: "memories" | "hybrid" | "documents", // Optional: "memories" (default), "hybrid" (memories + document chunks), or "documents" (chunks only)
filters?: FilterObject // Optional: advanced filtering
limit?: number, // Optional: max results
threshold?: number, // Optional: similarity floor (0-1)
searchMode?: "memories" | "hybrid" | "documents", // "memories" (default), "hybrid" (memories + chunks), "documents" (chunks only)
filters?: FilterObject, // Optional: metadata filters
include?: { // Optional: extra payload per result
chunks?: boolean,
documents?: boolean,
summaries?: boolean,
relatedMemories?: boolean,
forgottenMemories?: boolean
},
rerank?: boolean, // Optional: cross-encoder re-ranking (higher precision, slower)
rewriteQuery?: boolean, // Optional: let the service rewrite q before searching
aggregate?: boolean // Optional: collapse near-duplicate memories
});
// Returns:
// {
// results: Array<{
// id: string,
// memory?: string, // Memory content (for memory results)
// chunk?: string, // Chunk content (for chunk results in hybrid mode)
// metadata: object | null,
// memory?: string, // set on memory results
// chunk?: string, // set on chunk results (hybrid / documents mode)
// similarity: number, // the score — not `score`
// updatedAt: string,
// similarity: number,
// version?: number | null
// metadata: object | null,
// version?: number | null,
// chunks?, documents?, context? // only when requested via `include`
// }>,
// total: number,
// timing: number // Search time in milliseconds
// timing: number // milliseconds
// }
```
@ -237,18 +281,15 @@ const response = await client.search({
response = client.search.memories(
q=str, # Required: search query
container_tag=str, # Optional: filter by container tag
threshold=float, # Optional: similarity threshold (0-1, default 0.5)
limit=int, # Optional: max results (default 50)
threshold=float, # Optional: similarity floor (0-1)
limit=int, # Optional: max results
search_mode=str, # Optional: "memories" (default), "hybrid", or "documents"
filters=dict # Optional: advanced filtering
filters=dict, # Optional: metadata filters
rerank=bool, # Optional: cross-encoder re-ranking
rewrite_query=bool, # Optional: server-side query rewriting
)
# Returns dict:
# {
# "results": List[dict], # Array of search results
# "total": int,
# "timing": int # Search time in milliseconds
# }
# response.results, response.total, response.timing
```
#### Examples
@ -261,141 +302,209 @@ const response = await client.search({
limit: 10
});
response.results.forEach(result => {
console.log(`Score: ${result.score}`);
console.log(`Content: ${result.content}`);
});
for (const result of response.results) {
console.log(result.similarity, result.memory ?? result.chunk);
}
```
**Hybrid search for RAG (semantic + keyword):**
**Hybrid search for RAG:**
```typescript
const response = await client.search({
q: "authentication methods",
containerTag: "docs",
searchMode: "hybrid", // Combines semantic and keyword search for better RAG accuracy
searchMode: "hybrid", // memories + document chunks
threshold: 0.3,
limit: 10
});
```
**Search with metadata filters:**
**High-precision search:**
```typescript
const response = await client.search({
q: "authentication methods",
containerTag: "docs",
threshold: 0.3,
filters: {
metadata: {
type: "tutorial",
category: "security"
}
}
q: "what did we decide about rate limiting?",
containerTag: "eng_notes",
rerank: true, // re-rank for precision
rewriteQuery: true // helps with terse or pronoun-heavy queries
});
```
**Search within specific document:**
### `memories.forget()` - Forget a Memory
Soft-deletes one memory. Identify it by `id`, or by exact `content` when you don't have the ID.
```typescript
const response = await client.search({
q: "rate limiting configuration",
containerTag: "specific_project"
const res = await client.memories.forget({
containerTag: "user_123", // Required
id: "mem_abc123", // Either id...
// content: "John prefers dark mode", // ...or exact content
reason: "outdated information" // Optional, recorded as forgetReason
});
// { id: "mem_abc123", forgotten: true }
```
```python
res = client.memories.forget(
container_tag="user_123",
id="mem_abc123",
reason="outdated information",
)
```
### `memories.updateMemory()` - Correct a Memory
Creates a new version that supersedes the old one, preserving history. Prefer this over forget-then-add when a fact merely changed.
```typescript
const res = await client.memories.updateMemory({
containerTag: "user_123", // Required
newContent: "John now prefers light mode", // Required
id: "mem_abc123", // Either id or exact content
metadata: { source: "chat" }, // Optional: inherits previous version if omitted
forgetAfter: "2026-12-01T00:00:00Z", // Optional: ISO expiry, null clears it
forgetReason: "temporary preference" // Optional
});
// { id: "mem_xyz789", memory: "...", version: 2, parentMemoryId: "mem_abc123", rootMemoryId: "mem_abc123", ... }
```
```python
res = client.memories.update_memory(
container_tag="user_123",
new_content="John now prefers light mode",
id="mem_abc123",
)
```
### `documents.list()` - List Documents
Retrieve stored documents with optional filtering and pagination.
#### TypeScript
```typescript
const docs = await client.documents.list({
containerTag?: string, // Optional: filter by container
limit?: number, // Optional: number of results (default 20)
offset?: number, // Optional: pagination offset
status?: string // Optional: filter by processing status
containerTags?: string[], // Note: array, not a single tag
limit?: number | string, // Page size
page?: number | string, // 1-based
sort?: "createdAt" | "updatedAt",
order?: "asc" | "desc",
filters?: FilterObject,
includeContent?: boolean,
filepath?: string
});
// Returns:
// {
// documents: Array<{
// id: string,
// content: string,
// status: string,
// metadata: object,
// createdAt: string
// }>,
// total: number
// memories: Array<{ id, title?, status, metadata, createdAt, updatedAt, ... }>,
// pagination: { currentPage, limit, totalItems, totalPages }
// }
```
#### Python
```python
docs = client.documents.list(
container_tag=str, # Optional: filter by container
limit=int, # Optional: number of results (default 20)
offset=int, # Optional: pagination offset
status=str # Optional: filter by processing status
container_tags=["user_123"],
limit=50,
page=1,
)
# docs.memories, docs.pagination
```
#### Examples
**List all documents for a user:**
```typescript
const docs = await client.documents.list({
containerTag: "user_123",
limit: 50
});
docs.documents.forEach(doc => {
console.log(`${doc.id}: ${doc.status}`);
});
```
The response field is `memories` (documents with their extracted memories), not `documents`, and pagination is page-based — there is no `offset`.
**Paginated listing:**
```typescript
const page1 = await client.documents.list({ limit: 20, offset: 0 });
const page2 = await client.documents.list({ limit: 20, offset: 20 });
const page1 = await client.documents.list({ containerTags: ["user_123"], limit: 20, page: 1 });
const page2 = await client.documents.list({ containerTags: ["user_123"], limit: 20, page: 2 });
```
**Filter by status:**
**Check what is still processing:**
```typescript
const processing = await client.documents.list({
containerTag: "project_abc",
status: "processing"
});
const processing = await client.documents.listProcessing();
```
### `documents.delete()` - Delete Document
### `documents.get()` / `update()` / `delete()`
Remove a document and its associated memories.
IDs are positional arguments, not body fields.
#### TypeScript
```typescript
await client.documents.delete({
docId: string // Required: document ID
});
const doc = await client.documents.get("doc_abc123");
await client.documents.update("doc_abc123", { metadata: { reviewed: true } });
await client.documents.delete("doc_abc123"); // also deletes its memories
await client.documents.deleteBulk({ ids: ["doc_1", "doc_2"] });
```
#### Python
```python
client.documents.delete(
doc_id=str # Required: document ID
)
doc = client.documents.get("doc_abc123")
client.documents.delete("doc_abc123")
```
#### Example
## Endpoints without SDK methods
These are documented, supported endpoints that the generated SDKs don't cover yet. Call them with `fetch` (or `requests`) against the same base URL with the same bearer token. Full request/response shapes are in `api-reference.md`.
| Endpoint | What it does |
|---|---|
| `POST /v4/memories` | Write memories directly, skipping document ingestion and extraction |
| `POST /v4/memories/forget-matching` | Agentic mass forget by query or ID list, with `dryRun` |
| `POST /v4/memories/list` | List memory entries with version history |
| `POST /v4/profile` with `include` / `buckets` | Bucketed profile reads (the SDK's `profile()` params omit these) |
| `POST /v4/profile/buckets` | List effective bucket definitions for a container tag |
| `POST /v4/conversations` | Ingest structured chat transcripts with append detection |
| `/v3/container-tags/*` | List, configure, delete, and merge container tags |
| `POST /v3/settings/suggest-buckets` | AI-suggested bucket definitions |
| `POST /v3/settings/reset` | Reset org settings to defaults |
| `GET /v3/documents/{id}/chunks`, `/file-url` | Raw chunks, signed file URL |
A small helper keeps call sites clean:
```typescript
await client.documents.delete({
docId: "doc_abc123"
const sm = async (path: string, body: unknown) => {
const res = await fetch(`https://api.supermemory.ai${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`supermemory ${path} failed: ${res.status} ${await res.text()}`);
return res.json();
};
// Write memories directly
await sm("/v4/memories", {
containerTag: "user_123",
memories: [
{ content: "John prefers dark mode" },
{ content: "John is from Seattle", isStatic: true },
],
});
// Mass forget — preview, then apply exactly what you reviewed
const preview = await sm("/v4/memories/forget-matching", {
containerTag: "user_123",
query: "forget everything about Project Titan",
dryRun: true,
});
const applied = await sm("/v4/memories/forget-matching", {
containerTag: "user_123",
ids: preview.candidates.map((c: { id: string }) => c.id),
reason: "project cancelled",
});
// Ingest a conversation
await sm("/v4/conversations", {
conversationId: "conv_123",
containerTags: ["user_123"],
messages: [
{ role: "user", content: "I switched my editor to Zed" },
{ role: "assistant", content: "Noted — how are you finding it?" },
],
});
```
Always `dryRun` a `forget-matching` call before applying it, and prefer applying with the `ids` from the preview: applying with a `query` re-runs the semantic match, which can drift if the container changed in between.
## Advanced Features
### Metadata Filtering
Add rich metadata to enable advanced filtering:
Add rich metadata at write time, then filter at read time. Filter conditions must be wrapped in `AND` or `OR` — a bare `{ metadata: {...} }` object is rejected.
```typescript
await client.add({
@ -409,23 +518,22 @@ await client.add({
}
});
// Search with metadata filters
const results = await client.search({
q: "phone reviews",
containerTag: "reviews",
filters: {
metadata: {
rating: { $gte: 4.0 }, // Rating >= 4.0
verified: true,
tags: { $contains: "apple" }
}
AND: [
{ filterType: "numeric", key: "rating", value: "4", numericOperator: ">=" },
{ key: "verified", value: "true" },
{ filterType: "array_contains", key: "tags", value: "apple" }
]
}
});
```
### Entity Context for Better Extraction
Condition types: string equality (default), `string_contains`, `numeric` (with `numericOperator`), and `array_contains`. `AND`/`OR` nest freely, and any condition accepts `negate` and `ignoreCase`. Numeric values are passed as strings.
Provide context to guide memory extraction:
### Entity Context for Better Extraction
```typescript
await client.add({
@ -435,7 +543,7 @@ await client.add({
});
```
The `entityContext` helps Supermemory understand what type of information to extract and prioritize.
`entityContext` (max 1,500 chars) guides what gets extracted and how it's classified. Set it per-call as above, or once per container tag via `PATCH /v3/container-tags/{tag}`. For guidance that should apply org-wide, use `filterPrompt` in `PATCH /v3/settings` instead.
### Container Tag Patterns
@ -468,12 +576,16 @@ await client.add({
});
```
If one entity ends up split across two tags (an anonymous session that later signs in), merge them with `POST /v3/container-tags/merge` rather than re-ingesting.
## Integration with AI Frameworks
For Vercel AI SDK, OpenAI, Mastra, and VoltAgent there is a purpose-built package — `@supermemory/tools` — with ready-made tools and middleware that inject memory automatically. Reach for it before hand-rolling the calls below.
### Vercel AI SDK
```typescript
import { Supermemory } from 'supermemory';
import Supermemory from 'supermemory';
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';
@ -481,19 +593,19 @@ const memory = new Supermemory();
async function chat(userId: string, message: string) {
// 1. Get context
const context = await memory.profile({
containerTag: userId,
q: message
});
const { profile } = await memory.profile({ containerTag: userId, q: message });
// 2. Generate response with context
const { text } = await generateText({
model: openai('gpt-4'),
system: `User Profile: ${context.profile}\n\nRelevant Context:\n${context.memories.map(m => m.content).join('\n')}`,
model: openai('gpt-5'),
system: [
`Long-term facts:\n${(profile.static ?? []).map(f => `- ${f}`).join('\n')}`,
`Recent context:\n${(profile.dynamic ?? []).map(f => `- ${f}`).join('\n')}`
].join('\n\n'),
prompt: message
});
// 3. Store conversation
// 3. Store the exchange
await memory.add({
content: `User: ${message}\nAssistant: ${text}`,
containerTag: userId
@ -506,29 +618,27 @@ async function chat(userId: string, message: string) {
### LangChain
```typescript
import { Supermemory } from 'supermemory';
import Supermemory from 'supermemory';
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
const memory = new Supermemory();
const llm = new ChatOpenAI({ model: 'gpt-4' });
const llm = new ChatOpenAI({ model: 'gpt-5' });
async function chatWithMemory(userId: string, userMessage: string) {
// Retrieve context
const context = await memory.profile({
containerTag: userId,
q: userMessage
});
const { profile } = await memory.profile({ containerTag: userId, q: userMessage });
// Create messages with context
const messages = [
new SystemMessage(`Context: ${JSON.stringify(context)}`),
new SystemMessage(
`What you know about this user:\n${[...(profile.static ?? []), ...(profile.dynamic ?? [])]
.map(f => `- ${f}`)
.join('\n')}`
),
new HumanMessage(userMessage)
];
const response = await llm.invoke(messages);
// Store interaction
await memory.add({
content: `${userMessage}\n${response.content}`,
containerTag: userId
@ -542,25 +652,21 @@ async function chatWithMemory(userId: string, userMessage: string) {
```python
from supermemory import Supermemory
from crewai import Agent, Task, Crew
from crewai import Agent
memory = Supermemory()
def create_memory_enhanced_agent(user_id: str):
# Get user context
context = memory.profile(
container_tag=user_id,
query="user preferences and history"
)
response = memory.profile(container_tag=user_id)
facts = "\n".join(f"- {fact}" for fact in response.profile.static or [])
recent = "\n".join(f"- {fact}" for fact in response.profile.dynamic or [])
agent = Agent(
return Agent(
role="Personal Assistant",
goal="Help the user with personalized assistance",
backstory=f"User Context: {context['profile']}\n\nRecent interactions:\n{context['memories']}",
verbose=True
backstory=f"What you know about this user:\n{facts}\n\nRecent context:\n{recent}",
verbose=True,
)
return agent
```
## Best Practices
@ -602,23 +708,40 @@ await client.add({
```
### 4. Appropriate Thresholds
Start with default (0.5) and adjust based on results:
Start with the default and adjust based on real queries:
- **0.3-0.5**: Broader recall, good for discovery
- **0.5-0.7**: Balanced precision and recall
- **0.7-1.0**: High precision, fewer but more relevant results
### 5. Error Handling
Always handle errors gracefully:
### 5. Correct Memories Instead of Rewriting Them
```typescript
// Fact changed → new version, history preserved
await client.memories.updateMemory({
containerTag: userId,
id: memoryId,
newContent: "Now prefers light mode"
});
// Should never have been stored → forget it
await client.memories.forget({ containerTag: userId, id: memoryId, reason: "user asked" });
```
### 6. Error Handling
The SDKs raise typed errors:
```typescript
import { APIError, RateLimitError, AuthenticationError } from 'supermemory';
try {
await client.add({ content: "...", containerTag: "user_123" });
} catch (error) {
if (error.status === 401) {
if (error instanceof AuthenticationError) {
console.error("Invalid API key");
} else if (error.status === 429) {
} else if (error instanceof RateLimitError) {
console.error("Rate limit exceeded");
} else if (error instanceof APIError) {
console.error(error.status, error.message);
} else {
console.error("Failed to add memory:", error.message);
throw error;
}
}
```
@ -626,27 +749,33 @@ try {
## Naming Conventions
### TypeScript (camelCase)
- `containerTag`
- `containerTag` / `containerTags`
- `entityContext`
- `customId`
- `newContent`
- `searchMode`
- `threshold`
- `docId`
- `q`
### Python (snake_case)
- `container_tag`
- `container_tag` / `container_tags`
- `entity_context`
- `custom_id`
- `new_content`
- `search_mode`
- `threshold`
- `doc_id`
- `q`
Python responses are models, so read them by attribute (`response.profile.static`), not by key.
## Performance Tips
1. **Batch Operations**: Add multiple documents in quick succession if needed
2. **Async/Await**: Always use async operations to avoid blocking
3. **Pagination**: Use `limit` and `offset` for large result sets
4. **Caching**: Cache profile() results for short periods if making multiple calls
5. **Processing Time**: Allow 1-2 minutes for PDFs, 5-10 minutes for videos
1. **Batch Operations**: Use `documents.batchAdd()` rather than a loop of `add()` calls
2. **Async/Await**: Always use async operations to avoid blocking; Python has `AsyncSupermemory`
3. **Pagination**: `documents.list()` and `/v4/memories/list` are page-based (`limit` + `page`)
4. **Caching**: Cache `profile()` results for short periods if making multiple calls per turn
5. **Cost of quality knobs**: `rerank` and `rewriteQuery` improve results but add latency — enable them per-query, not globally
6. **Processing Time**: Allow 1-2 minutes for PDFs, 5-10 minutes for videos
## Support

View file

@ -97,8 +97,8 @@ def chat(user_id: str, message: str) -> str:
)
# 2. Build system prompt
static_facts = "\n".join(f"- {fact}" for fact in response['profile']['static'])
dynamic_facts = "\n".join(f"- {fact}" for fact in response['profile']['dynamic'])
static_facts = "\n".join(f"- {fact}" for fact in response.profile.static or [])
dynamic_facts = "\n".join(f"- {fact}" for fact in response.profile.dynamic or [])
system_prompt = f"""
You are a helpful assistant with perfect memory.
@ -277,21 +277,21 @@ async function indexDocumentation() {
// 2. Search documentation
async function searchDocs(query: string, category?: string) {
const filters = category ? {
metadata: { category }
AND: [{ key: 'category', value: category }]
} : undefined;
const results = await memory.search.memories({
const response = await memory.search.memories({
q: query,
containerTag: 'documentation',
searchMode: 'hybrid', // Use hybrid search for better RAG accuracy
searchMode: 'hybrid', // Memories + document chunks, best for RAG
threshold: 0.3,
limit: 10,
filters
});
return results.map(r => ({
content: r.content,
relevance: r.score,
return response.results.map(r => ({
content: r.memory ?? r.chunk,
relevance: r.similarity,
metadata: r.metadata
}));
}
@ -545,10 +545,10 @@ async function reviewCode(projectId: string, code: string, fileName: string) {
You are a code review assistant familiar with this codebase.
Similar Code Patterns:
${similarCode.map(c => c.content).slice(0, 3).join('\n\n---\n\n')}
${similarCode.results.slice(0, 3).map(c => c.memory ?? c.chunk).join('\n\n---\n\n')}
Past Review Patterns:
${pastReviews.map(p => p.content).slice(0, 3).join('\n\n---\n\n')}
${pastReviews.results.slice(0, 3).map(p => p.memory ?? p.chunk).join('\n\n---\n\n')}
Provide a thoughtful code review, considering existing patterns and past feedback.
`,
@ -769,14 +769,14 @@ async function search(orgId: string, userId: string, query: string, includeShare
? [tags.user, tags.shared] // User + org shared
: [tags.user]; // User only
const results = await memory.search.memories({
const response = await memory.search.memories({
q: query,
containerTag: containerTags[0], // Use first tag
containerTag: containerTags[0], // One tag per search — run one call per tag to span several
threshold: 0.3,
limit: 10
});
return results;
return response.results;
}
// Usage
@ -865,7 +865,7 @@ async function addResearchNote(userId: string, note: string, relatedPapers: stri
}
async function findRelatedResearch(userId: string, topic: string) {
const results = await memory.search.memories({
const response = await memory.search.memories({
q: topic,
containerTag: `${userId}_research`,
threshold: 0.3,
@ -873,8 +873,8 @@ async function findRelatedResearch(userId: string, topic: string) {
});
// Group by type
const papers = results.filter(r => r.metadata?.type === 'paper');
const notes = results.filter(r => r.metadata?.type === 'note');
const papers = response.results.filter(r => r.metadata?.type === 'paper');
const notes = response.results.filter(r => r.metadata?.type === 'note');
return { papers, notes };
}
@ -884,9 +884,9 @@ async function synthesizeInsights(userId: string, research_question: string) {
const context = [
'=== Related Papers ===',
...related.papers.map(p => p.content),
...related.papers.map(p => p.memory ?? p.chunk),
'\n=== Your Notes ===',
...related.notes.map(n => n.content)
...related.notes.map(n => n.memory ?? n.chunk)
].join('\n\n');
const { text } = await generateText({
@ -997,7 +997,10 @@ const results = await memory.search.memories({
q: 'billing issues',
containerTag: 'user_123',
filters: {
metadata: { priority: 'high', type: 'conversation' }
AND: [
{ key: 'priority', value: 'high' },
{ key: 'type', value: 'conversation' }
]
}
});
```