From 594e64b29db62685e4538ee231e069ce0500c797 Mon Sep 17 00:00:00 2001 From: ved015 <122012786+ved015@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:01:54 +0530 Subject: [PATCH 1/2] fix(tools): align v4 search and safe deletion --- packages/tools/README.md | 9 +++-- packages/tools/src/ai-sdk.ts | 14 +++---- packages/tools/src/openai/tools.ts | 11 +++--- packages/tools/src/tools-shared.ts | 60 +++++++++++++++++++++--------- 4 files changed, 57 insertions(+), 37 deletions(-) diff --git a/packages/tools/README.md b/packages/tools/README.md index 69856f69..be27c777 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -22,7 +22,7 @@ The package provides three submodule imports: ```typescript import { supermemoryTools, searchMemoriesTool, addMemoryTool } from "@supermemory/tools/ai-sdk" import { createOpenAI } from "@ai-sdk/openai" -import { generateText } from "ai" +import { generateText, stepCountIs } from "ai" const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY!, @@ -43,6 +43,7 @@ const result = await generateText({ }, ], tools, + stopWhen: stepCountIs(5), }) // Or create individual tools @@ -606,7 +607,7 @@ interface SupermemoryToolsConfig { ``` - **baseUrl**: Custom base URL for the supermemory API -- **containerTags**: Array of custom container tags (mutually exclusive with projectId) +- **containerTags**: Non-empty array of custom container tags (mutually exclusive with `projectId`). `searchMemories`, `getProfile`, and `memoryForget` use the first tag because v4 memory APIs are single-space. Add operations attach every configured tag, while `documentList` and `documentDelete` use the configured tags as their supported union scope. `documentDelete` still refuses a document with any tag outside that scope or a nonterminal processing status. - **projectId**: Project ID which gets converted to container tag format (mutually exclusive with containerTags) - **strict**: Enable strict schema mode for OpenAI strict validation. When `true`, all schema properties are required (satisfies OpenAI strict mode). When `false` (default), optional fields remain optional for maximum compatibility with all models. @@ -670,11 +671,11 @@ interface WithSupermemoryOptions { ## Available Tools ### Search Memories -Searches through stored memories based on a query string. +Runs v4 hybrid search in the primary (first) configured container tag. Results can contain learned memories (`memory`) and source chunks (`chunk`). Only IDs on results containing `memory` can be passed to `memoryForget`; chunk-result IDs cannot. **Parameters:** - `informationToGet` (string): Terms to search for -- `includeFullDocs` (boolean, optional): Whether to include full document content (default: true) +- `includeFullDocs` (boolean, optional): Deprecated compatibility input; ignored by v4 hybrid search - `limit` (number, optional): Maximum number of results (default: 10) ### Add Memory diff --git a/packages/tools/src/ai-sdk.ts b/packages/tools/src/ai-sdk.ts index 1b710808..bf0cb5e4 100644 --- a/packages/tools/src/ai-sdk.ts +++ b/packages/tools/src/ai-sdk.ts @@ -51,18 +51,14 @@ export const searchMemoriesTool = ( .default(DEFAULT_VALUES.limit) .describe(PARAMETER_DESCRIPTIONS.limit), }), - execute: async ({ - informationToGet, - includeFullDocs = DEFAULT_VALUES.includeFullDocs, - limit = DEFAULT_VALUES.limit, - }) => { + execute: async ({ informationToGet, limit = DEFAULT_VALUES.limit }) => { try { - const response = await client.search.documents({ + const response = await client.search({ q: informationToGet, - containerTags, + containerTag: containerTags[0], limit, - chunkThreshold: DEFAULT_VALUES.chunkThreshold, - includeFullDocs, + threshold: DEFAULT_VALUES.searchThreshold, + searchMode: "hybrid", }) return { diff --git a/packages/tools/src/openai/tools.ts b/packages/tools/src/openai/tools.ts index 22257727..ad428076 100644 --- a/packages/tools/src/openai/tools.ts +++ b/packages/tools/src/openai/tools.ts @@ -15,7 +15,7 @@ import type { SupermemoryToolsConfig } from "../types" */ export interface MemorySearchResult { success: boolean - results?: Awaited>["results"] + results?: Awaited>["results"] count?: number error?: string } @@ -245,7 +245,6 @@ export function createSearchMemoriesFunction( return async function searchMemories({ informationToGet, - includeFullDocs = DEFAULT_VALUES.includeFullDocs, limit = DEFAULT_VALUES.limit, }: { informationToGet: string @@ -253,12 +252,12 @@ export function createSearchMemoriesFunction( limit?: number }): Promise { try { - const response = await client.search.documents({ + const response = await client.search({ q: informationToGet, - containerTags, + containerTag: containerTags[0], limit, - chunkThreshold: DEFAULT_VALUES.chunkThreshold, - includeFullDocs, + threshold: DEFAULT_VALUES.searchThreshold, + searchMode: "hybrid", }) return { diff --git a/packages/tools/src/tools-shared.ts b/packages/tools/src/tools-shared.ts index c0a3b540..fa25bcae 100644 --- a/packages/tools/src/tools-shared.ts +++ b/packages/tools/src/tools-shared.ts @@ -8,19 +8,19 @@ import type { MemoryMode } from "./shared/types" // Tool descriptions export const TOOL_DESCRIPTIONS = { searchMemories: - "Search stored source documents for relevant facts, preferences, history, and other context. Use when explicitly asked to search or recall, or when past context could materially improve the response; do not invoke reflexively on every turn. Results contain document IDs and matching text chunks, not profile-memory IDs for memoryForget.", + "Search the primary configured container tag for relevant facts, preferences, history, and source context. Use when explicitly asked to search or recall, or when past context could materially improve the response; do not invoke reflexively on every turn. Hybrid results mix learned memories (memory field) and source chunks (chunk field). Only an ID on a result containing a memory field is a profile-memory ID that can be passed to memoryForget; chunk-result IDs cannot be forgotten.", addMemory: "Add (remember) memories/details/information about the user or other facts or entities. Run when explicitly asked or when the user mentions any information generalizable beyond the context of the current conversation.", getProfile: - "Get user profile containing static memories (permanent facts) and dynamic memories (recent context). Profile entries are text without IDs. Provide a query to include searchResults, whose memory entries may include IDs usable with memoryForget.", + "Get the user profile for the primary configured container tag, unless containerTag explicitly overrides it. The profile contains static memories (permanent facts) and dynamic memories (recent context). Profile entries are text without IDs. Provide a query to include searchResults, whose memory entries may include IDs usable with memoryForget.", documentList: "List stored source documents (conversations, URLs, files, pasted text) with pagination. Configured container tags are treated as the default union; an optional containerTag replaces that union with one tag for this operation. Returns document metadata and IDs for documentDelete, not raw document content or memory IDs for memoryForget.", documentDelete: - "Permanently delete a stored source document. Memories extracted from that source are soft-forgotten so they no longer appear in profile or search; they are not hard-deleted. Use a document ID or customId when removing an entire conversation, file, URL, or other source. The effective scope is the configured container-tag union, or the explicit one-tag override; if documentList used an override, pass the same value here. To forget one learned fact, use memoryForget instead.", + "Permanently delete a stored source document. Memories extracted from that source are soft-forgotten so they no longer appear in profile or search; they are not hard-deleted. Use a document ID or customId when removing an entire conversation, file, URL, or other source. The effective scope is the configured container-tag union, or the explicit one-tag override; if documentList used an override, pass the same value here. For safety, deletion is refused while the document is processing or nonterminal, or when its authoritative tag set is empty, unavailable, or contains any tag outside the effective scope. To forget one learned fact, use memoryForget instead.", documentAdd: "Store a source document for asynchronous processing and automatic memory extraction. Use when the user gives you raw content to ingest — a pasted text blob, conversation transcript, chat history, notes, URL, article link, or other substantial text — rather than a single atomic fact (use addMemory for one short generalizable sentence). The document is queued immediately; Supermemory post-processes it in the background (chunking, embedding, indexing) and extracts profile memories automatically — you do not need to call addMemory for facts buried inside the document. Good for saving full conversations, long-form notes, knowledge-base articles, meeting transcripts, or any large body of text the user wants remembered beyond this chat turn. Processing may take a moment; extracted memories appear in profile/search after indexing completes.", memoryForget: - "Soft-forget a single extracted profile memory (a learned fact) so it no longer appears in profile or search. Does NOT delete source documents. Provide memoryId from query-backed getProfile searchResults, or memoryContent for an exact text match; document and chunk IDs from searchMemories are not valid. Use when the user retracts or corrects a specific fact. To remove an entire source, use documentDelete instead.", + "Soft-forget a single extracted profile memory (a learned fact) in the primary configured container tag, unless containerTag explicitly overrides it, so the fact no longer appears in profile or search. Does NOT delete source documents. Provide memoryId from query-backed getProfile searchResults or from a searchMemories result containing a memory field, or provide memoryContent for an exact text match. Chunk-result IDs from searchMemories are not valid. Use when the user retracts or corrects a specific fact. To remove an entire source, use documentDelete instead.", } as const // Parameter descriptions @@ -28,23 +28,23 @@ export const PARAMETER_DESCRIPTIONS = { informationToGet: "What to look up in stored context — keywords from the user's message, topic, entity names, or question phrasing.", includeFullDocs: - "Whether to include the full document content in the response. Defaults to true for better AI context.", + "Deprecated compatibility input. It is ignored because v4 hybrid search returns learned memories and matching chunks, not full source documents.", limit: "Maximum number of results to return", memory: "The text content of the memory to add. This should be a single sentence or a short paragraph.", containerTag: "Tag to filter/scope the operation (e.g., user ID, project ID)", documentContainerTag: - "Optional one-tag scope override. When deleting a document returned by documentList with a containerTag override, pass the same value here. In strict mode, pass null to use the configured union.", + "Optional one-tag scope override. When deleting a document returned by documentList with a containerTag override, pass the same value here. In strict mode, pass null to use the configured union. Deletion is refused if the document has any tag outside the resulting effective scope.", query: "Optional search query to include relevant search results", page: "Page number to fetch, 1-based (default: 1)", documentId: - "Document ID from documentList, or the document customId. Permanently deletes the source document and soft-forgets its extracted memories. If documentList used a containerTag override, pass it again. Not a profile-memory ID.", + "Document ID from documentList, or the document customId. Permanently deletes the source document and soft-forgets its extracted memories only after processing reaches a terminal done or failed state. If documentList used a containerTag override, pass it again. Deletion is refused if the document has any tag outside the effective scope. Not a profile-memory ID.", content: "Document body to store — plain text, a conversation transcript, a long pasted blob, or a URL to a webpage/PDF/image/video. Content is queued and memories are extracted automatically after background processing; do not split into addMemory calls.", title: "Optional title for the document", description: "Optional description for the document", memoryId: - "Profile-memory ID from query-backed getProfile searchResults. Soft-forgets one learned fact; document and chunk IDs from searchMemories are not valid.", + "Profile-memory ID from query-backed getProfile searchResults or a searchMemories result containing a memory field. Soft-forgets one learned fact; chunk-result and document IDs are not valid.", memoryContent: "Exact text of the profile memory to forget (alternative to memoryId). Must match precisely; if unsure, query getProfile and use a search-result memory ID.", reason: @@ -55,7 +55,7 @@ export const PARAMETER_DESCRIPTIONS = { export const DEFAULT_VALUES = { includeFullDocs: true, limit: 10, - chunkThreshold: 0.6, + searchThreshold: 0.6, } as const // Container tag constants @@ -128,10 +128,8 @@ export async function deleteDocumentByIdentifier( containerTags: readonly [string, ...string[]], ): Promise { const directMatch = await getDocumentIfFound(client, documentIdentifier) - if ( - directMatch?.id === documentIdentifier && - hasContainerTagOverlap(directMatch.containerTags, containerTags) - ) { + if (directMatch?.id === documentIdentifier) { + assertDocumentCanBeDeleted(directMatch, containerTags) await deleteDocumentById(client, directMatch.id) return } @@ -170,9 +168,7 @@ export async function deleteDocumentByIdentifier( hasUnverifiedCandidate = true continue } - if (!hasContainerTagOverlap(document.containerTags, containerTags)) { - continue - } + assertDocumentCanBeDeleted(document, containerTags) if (document.id === documentIdentifier) { exactIdMatch = document.id break @@ -230,11 +226,39 @@ function isNotFoundError(error: unknown): boolean { ) } -function hasContainerTagOverlap( +const TERMINAL_DOCUMENT_STATUSES = new Set(["done", "failed"]) + +function assertDocumentCanBeDeleted( + document: Awaited>, + expectedContainerTags: readonly string[], +): void { + if ( + !hasCompleteContainerTagScope(document.containerTags, expectedContainerTags) + ) { + throw new Error( + `Document ${document.id} could not be verified safely: its complete non-empty container-tag set must be contained in the configured scope.`, + ) + } + + // The current SDK always supplies status. Keeping undefined permissive lets + // older SDKs and lightweight client doubles continue to work. + const status = (document as { status?: string }).status + if (status !== undefined && !TERMINAL_DOCUMENT_STATUSES.has(status)) { + throw new Error( + `Document ${document.id} cannot be deleted while it is processing or otherwise nonterminal (status: ${status}).`, + ) + } +} + +function hasCompleteContainerTagScope( actual: string[] | undefined, expected: readonly string[], ): boolean { - return actual?.some((tag) => expected.includes(tag)) ?? false + return ( + actual !== undefined && + actual.length > 0 && + actual.every((tag) => tag.trim() !== "" && expected.includes(tag)) + ) } /** From 5f9a417ec533c907bd605929b927be3bb54c1d24 Mon Sep 17 00:00:00 2001 From: ved015 <122012786+ved015@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:02:21 +0530 Subject: [PATCH 2/2] docs(ai-sdk): clarify multi-step memory tools --- packages/ai-sdk/README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/ai-sdk/README.md b/packages/ai-sdk/README.md index fbaf0d25..207945ee 100644 --- a/packages/ai-sdk/README.md +++ b/packages/ai-sdk/README.md @@ -110,7 +110,7 @@ Supermemory tools allow AI agents to search, add, inspect, and manage scoped Sup ```typescript import { supermemoryTools } from '@supermemory/ai-sdk' -import { generateText } from 'ai' +import { generateText, stepCountIs } from 'ai' import { openai } from '@ai-sdk/openai' const result = await generateText({ @@ -123,7 +123,8 @@ const result = await generateText({ // Use either projectId OR containerTags, not both. containerTags: ['user-123'] }) - } + }, + stopWhen: stepCountIs(5) }) ``` @@ -133,7 +134,7 @@ const result = await generateText({ ```typescript import { supermemoryTools } from '@supermemory/ai-sdk' -import { generateText } from 'ai' +import { generateText, stepCountIs } from 'ai' import { openai } from '@ai-sdk/openai' const supermemoryApiKey = process.env.SUPERMEMORY_API_KEY! @@ -156,6 +157,7 @@ async function chatWithTools(userMessage: string) { containerTags: ['my-user-id'] }) }, + stopWhen: stepCountIs(5) }) return result.text @@ -181,7 +183,7 @@ interface SupermemoryToolsConfig { } ``` -`projectId` and `containerTags` are mutually exclusive and empty values are rejected. If neither is provided, v2 uses the explicit scope `sm_project_default`. With multiple `containerTags`, operations that support a union use all configured tags; single-profile operations default to the first tag. +`projectId` and `containerTags` are mutually exclusive and empty values are rejected. If neither is provided, v2 uses the explicit scope `sm_project_default`. With multiple `containerTags`, add operations attach every configured tag and document list/delete use their union. V4 search, profile, and forget operations use the first configured tag because those APIs are single-space. In strict mode, fields covered by a strict schema are required or defaulted. For example, `documentDelete.containerTag` must be a string or `null`; pass `null` to use the configured scope. @@ -206,7 +208,7 @@ const tools = supermemoryTools('your-api-key', { | Aggregate key | Individual creator | Purpose | | --- | --- | --- | -| `searchMemories` | `searchMemoriesTool` | Search stored source documents | +| `searchMemories` | `searchMemoriesTool` | Search learned memories and source chunks in the primary configured tag | | `addMemory` | `addMemoryTool` | Add a short, atomic memory | | `getProfile` | `getProfileTool` | Read static/dynamic profile text and optional query results | | `documentList` | `documentListTool` | List paginated source-document metadata | @@ -216,11 +218,15 @@ const tools = supermemoryTools('your-api-key', { There is no `fetchMemory` or `fetchMemoryTool`. Use `getProfile` for profile memories, `searchMemories` for relevant source content, and `documentList` for source-document IDs and metadata. +`memoryForget` accepts a memory ID from query-backed `getProfile` search results or from a `searchMemories` result containing a `memory` field; chunk and document IDs are not valid. For safety, `documentDelete` refuses documents that are still processing, lack a verifiable non-empty tag set, or contain any tag outside the effective scope. + ### Using Individual Tools For more flexibility, you can import and use individual tools: ```typescript +import { openai } from '@ai-sdk/openai' +import { generateText, stepCountIs } from 'ai' import { searchMemoriesTool, addMemoryTool, @@ -241,7 +247,8 @@ const result = await generateText({ messages: [...], tools: { searchMemories: searchTool - } + }, + stopWhen: stepCountIs(5) }) ```