feat(tools)!: @supermemory/tools 3.0 on the v5 API, retire @supermemory/ai-sdk (#1773)

Every integration (AI SDK, OpenAI, Mastra, VoltAgent, Claude memory) now calls v5 through `supermemory@5`. The config takes one `namespace` instead of `containerTags` or `projectId`, and `withSupermemory` takes `namespace` and `id` instead of `containerTag` and `customId`. No aliases. With no config the tools still use `sm_project_default`.

Conversations are stored as one document per conversation keyed by `id` instead of `/v4/conversations`. `memoryForget` drops `reason`; forgetting by text previews with `forgetMatching` and then forgets exact matches by id. Claude memory marks its files with `metadata.source` instead of a second tag. Search keeps the 2.x defaults so results do not shift.

`packages/ai-sdk` is removed: npm already deprecates it in favor of `@supermemory/tools/ai-sdk`, so its CI steps and playground wiring go too. `apps/sdk-playground` moves to the new tools API and v5 routes, including its Python server.

Tested against production: add, search, profile, list and delete. 112 unit tests pass; type errors drop from 148 to 141, none new. The `supermemory` dependency pins the rc until 5.0.0 is published.
This commit is contained in:
MaheshtheDev 2026-10-06 17:03:47 +00:00
parent 10888a94bc
commit 5c24e67d60
81 changed files with 2181 additions and 3417 deletions

View file

@ -41,12 +41,6 @@ jobs:
echo "tools=true" >> "$GITHUB_OUTPUT" echo "tools=true" >> "$GITHUB_OUTPUT"
fi fi
if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- packages/ai-sdk; then
echo "ai_sdk=false" >> "$GITHUB_OUTPUT"
else
echo "ai_sdk=true" >> "$GITHUB_OUTPUT"
fi
if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- apps/sdk-playground; then if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- apps/sdk-playground; then
echo "sdk_playground=false" >> "$GITHUB_OUTPUT" echo "sdk_playground=false" >> "$GITHUB_OUTPUT"
else else
@ -81,21 +75,9 @@ jobs:
run: bun run --cwd packages/tools test:unit run: bun run --cwd packages/tools test:unit
- name: Build Tools package - name: Build Tools package
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true' if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd packages/tools build run: bun run --cwd packages/tools build
- name: Run AI SDK type checking
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd packages/ai-sdk check-types
- name: Run AI SDK unit tests
if: steps.sdk-changes.outputs.ai_sdk == 'true'
run: bun run --cwd packages/ai-sdk test:unit
- name: Build AI SDK package
if: steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd packages/ai-sdk build
- name: Run SDK Playground type checking - name: Run SDK Playground type checking
if: steps.sdk-changes.outputs.sdk_playground == 'true' if: steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd apps/sdk-playground check-types:app run: bun run --cwd apps/sdk-playground check-types:app

View file

@ -6,7 +6,7 @@ Chat with a **real agent** and switch which Supermemory SDK integration powers i
> This is a local, single-user development tool. It makes real API calls, stores > This is a local, single-user development tool. It makes real API calls, stores
> browser-entered keys only in memory unless you opt into tab-scoped > browser-entered keys only in memory unless you opt into tab-scoped
> `sessionStorage`, and exposes tools that can permanently delete documents. Use > `sessionStorage`, and exposes tools that can permanently delete documents. Use
> disposable development credentials and a test container; do not deploy it or > disposable development credentials and a test namespace; do not deploy it or
> point it at production data. > point it at production data.
## Integrations ## Integrations
@ -17,7 +17,6 @@ Chat with a **real agent** and switch which Supermemory SDK integration powers i
| OpenAI + middleware | automatic | same, via OpenAI client wrapper | | OpenAI + middleware | automatic | same, via OpenAI client wrapper |
| AI SDK + tools | explicit | model calls 7 memory tools via `generateText` | | AI SDK + tools | explicit | model calls 7 memory tools via `generateText` |
| OpenAI + tools | explicit | OpenAI function-calling loop | | OpenAI + tools | explicit | OpenAI function-calling loop |
| `@supermemory/ai-sdk` | explicit | re-export of tools/ai-sdk |
| Python OpenAI middleware | automatic | `with_supermemory` | | Python OpenAI middleware | automatic | `with_supermemory` |
| Python OpenAI tools | explicit | `SupermemoryTools` loop | | Python OpenAI tools | explicit | `SupermemoryTools` loop |
| Python supermemory direct | manual | `profile()` + OpenAI + `add()` | | Python supermemory direct | manual | `profile()` + OpenAI + `add()` |
@ -39,9 +38,8 @@ cp apps/sdk-playground/.env.example apps/sdk-playground/.env.local
# OPENAI_API_KEY=... # OPENAI_API_KEY=...
``` ```
The playground scripts build `@supermemory/tools` first and The playground scripts build `@supermemory/tools` before starting, type-checking, or building the
`@supermemory/ai-sdk` second before starting, type-checking, or building the Next.js app. Development mode also watches the workspace package.
Next.js app. Development mode also watches both workspace packages.
## Run ## Run

View file

@ -1,7 +1,7 @@
import type { NextConfig } from "next" import type { NextConfig } from "next"
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
transpilePackages: ["@supermemory/tools", "@supermemory/ai-sdk"], transpilePackages: ["@supermemory/tools"],
} }
export default nextConfig export default nextConfig

View file

@ -5,10 +5,10 @@
"portless": { "name": "sdk.dev.supermemory", "script": "dev:app" }, "portless": { "name": "sdk.dev.supermemory", "script": "dev:app" },
"scripts": { "scripts": {
"dev": "portless", "dev": "portless",
"dev:app": "bun run build:dependencies && concurrently -k -n tools,ai-sdk,next,py -c yellow,magenta,blue,green \"bun run --cwd ../../packages/tools dev\" \"bun run --cwd ../../packages/ai-sdk dev\" \"next dev --hostname 127.0.0.1 --port ${PORT:-3005}\" \"bun run dev:python\"", "dev:app": "bun run build:dependencies && concurrently -k -n tools,next,py -c yellow,blue,green \"bun run --cwd ../../packages/tools dev\" \"next dev --hostname 127.0.0.1 --port ${PORT:-3005}\" \"bun run dev:python\"",
"dev:next": "bun run build:dependencies && next dev --hostname 127.0.0.1 --port ${PORT:-3005}", "dev:next": "bun run build:dependencies && next dev --hostname 127.0.0.1 --port ${PORT:-3005}",
"dev:python": "cd python && uv run server.py", "dev:python": "cd python && uv run server.py",
"build:dependencies": "bun run --cwd ../../packages/tools build && bun run --cwd ../../packages/ai-sdk build", "build:dependencies": "bun run --cwd ../../packages/tools build",
"build:app": "next build", "build:app": "next build",
"build": "bun run build:dependencies && bun run build:app", "build": "bun run build:dependencies && bun run build:app",
"start": "concurrently -k -n next,py -c blue,green \"next start --hostname 127.0.0.1 --port ${PORT:-3005}\" \"bun run dev:python\"", "start": "concurrently -k -n next,py -c blue,green \"next start --hostname 127.0.0.1 --port ${PORT:-3005}\" \"bun run dev:python\"",
@ -18,14 +18,13 @@
}, },
"dependencies": { "dependencies": {
"@ai-sdk/openai": "^2.0.22", "@ai-sdk/openai": "^2.0.22",
"@supermemory/ai-sdk": "workspace:*",
"@supermemory/tools": "workspace:*", "@supermemory/tools": "workspace:*",
"ai": "^5.0.113", "ai": "^5.0.113",
"next": "^16.3.3", "next": "^16.3.3",
"openai": "^4.104.0", "openai": "^4.104.0",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"supermemory": "^4.25.4" "supermemory": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",

View file

@ -7,7 +7,6 @@ dependencies = [
"httpx>=0.28.0", "httpx>=0.28.0",
"uvicorn[standard]>=0.32.0", "uvicorn[standard]>=0.32.0",
"python-dotenv>=1.0.1", "python-dotenv>=1.0.1",
"supermemory>=3.50.0",
"openai>=1.102.0", "openai>=1.102.0",
"supermemory-openai-sdk[async]", "supermemory-openai-sdk[async]",
] ]

View file

@ -8,7 +8,7 @@ import re
import time import time
from pathlib import Path from pathlib import Path
from typing import Annotated, Any, Literal, Optional from typing import Annotated, Any, Literal, Optional
from urllib.parse import urlparse from urllib.parse import quote, urlparse
from dotenv import load_dotenv from dotenv import load_dotenv
from fastapi import FastAPI, Header, Query from fastapi import FastAPI, Header, Query
@ -27,13 +27,16 @@ CHAT_TIMEOUT_SECONDS = 115.0
CONTEXT_DEBUG_TIMEOUT_SECONDS = 10.0 CONTEXT_DEBUG_TIMEOUT_SECONDS = 10.0
DIRECT_SAVE_TIMEOUT_SECONDS = 10.0 DIRECT_SAVE_TIMEOUT_SECONDS = 10.0
MAX_OUTPUT_TOKENS = 2_048 MAX_OUTPUT_TOKENS = 2_048
DOCUMENT_LIST_LIMIT = 25
# Matches the v4 profile search defaults the TS middlewares keep (memories mode, 0.6).
PROFILE_SEARCH_THRESHOLD = 0.6
MAX_MESSAGE_LENGTH = 20_000 MAX_MESSAGE_LENGTH = 20_000
MAX_MESSAGES = 64 MAX_MESSAGES = 64
MAX_TOTAL_MESSAGE_LENGTH = 100_000 MAX_TOTAL_MESSAGE_LENGTH = 100_000
MAX_API_KEY_LENGTH = 1_024 MAX_API_KEY_LENGTH = 1_024
MAX_CONTAINER_TAG_LENGTH = 100 MAX_NAMESPACE_LENGTH = 100
MAX_CONVERSATION_ID_LENGTH = 242 MAX_CONVERSATION_ID_LENGTH = 242
CONTAINER_TAG_PATTERN = r"^[a-zA-Z0-9_:-]+$" NAMESPACE_PATTERN = r"^[a-zA-Z0-9_:-]+$"
TOOLS_SYSTEM_PROMPT = """You are a helpful assistant with Supermemory long-term memory. TOOLS_SYSTEM_PROMPT = """You are a helpful assistant with Supermemory long-term memory.
@ -82,11 +85,11 @@ class ChatRequest(BaseModel):
"py-supermemory-direct", "py-supermemory-direct",
] ]
messages: list[ChatMessage] = Field(min_length=1, max_length=MAX_MESSAGES) messages: list[ChatMessage] = Field(min_length=1, max_length=MAX_MESSAGES)
containerTag: str = Field( namespace: str = Field(
default="sdk-playground", default="sdk-playground",
min_length=1, min_length=1,
max_length=MAX_CONTAINER_TAG_LENGTH, max_length=MAX_NAMESPACE_LENGTH,
pattern=CONTAINER_TAG_PATTERN, pattern=NAMESPACE_PATTERN,
) )
conversationId: str = Field( conversationId: str = Field(
min_length=1, min_length=1,
@ -114,11 +117,11 @@ class ChatRequest(BaseModel):
class ContextRequest(BaseModel): class ContextRequest(BaseModel):
containerTag: str = Field( namespace: str = Field(
default="sdk-playground", default="sdk-playground",
min_length=1, min_length=1,
max_length=MAX_CONTAINER_TAG_LENGTH, max_length=MAX_NAMESPACE_LENGTH,
pattern=CONTAINER_TAG_PATTERN, pattern=NAMESPACE_PATTERN,
) )
query: Optional[str] = Field(default=None, max_length=MAX_MESSAGE_LENGTH) query: Optional[str] = Field(default=None, max_length=MAX_MESSAGE_LENGTH)
apiKeys: Optional[SupermemoryApiKeys] = None apiKeys: Optional[SupermemoryApiKeys] = None
@ -172,7 +175,7 @@ def public_error(error: Exception, *secrets: str) -> str:
async def chat_openai_middleware( async def chat_openai_middleware(
messages: list[ChatMessage], messages: list[ChatMessage],
container_tag: str, namespace: str,
conversation_id: str, conversation_id: str,
memory_mode: str, memory_mode: str,
middleware_config: MiddlewareConfig, middleware_config: MiddlewareConfig,
@ -189,7 +192,7 @@ async def chat_openai_middleware(
max_retries=1, max_retries=1,
), ),
OpenAIMiddlewareOptions( OpenAIMiddlewareOptions(
container_tag=container_tag, container_tag=namespace,
custom_id=conversation_id, custom_id=conversation_id,
mode=memory_mode, mode=memory_mode,
add_memory=middleware_config.addMemory, add_memory=middleware_config.addMemory,
@ -221,7 +224,7 @@ async def chat_openai_middleware(
async def chat_openai_tools( async def chat_openai_tools(
messages: list[ChatMessage], messages: list[ChatMessage],
container_tag: str, namespace: str,
sm_key: str, sm_key: str,
oai_key: str, oai_key: str,
) -> tuple[str, list[dict[str, Any]]]: ) -> tuple[str, list[dict[str, Any]]]:
@ -235,7 +238,7 @@ async def chat_openai_tools(
) )
config: dict[str, Any] = { config: dict[str, Any] = {
"base_url": supermemory_base_url(), "base_url": supermemory_base_url(),
"container_tags": [container_tag], "container_tags": [namespace],
} }
tools = SupermemoryTools(sm_key, config) tools = SupermemoryTools(sm_key, config)
@ -296,24 +299,6 @@ def list_field(value: Any, name: str) -> list[Any]:
return result if isinstance(result, list) else [] return result if isinstance(result, list) else []
def extract_profile_context(profile_response: Any) -> dict[str, list[Any]]:
profile = object_field(profile_response, "profile", {}) or {}
search_results = object_field(profile_response, "search_results", None)
if search_results is None and isinstance(profile_response, dict):
search_results = profile_response.get("searchResults")
if isinstance(search_results, list):
search_list = search_results
else:
search_list = list_field(search_results, "results")
return {
"static": list_field(profile, "static"),
"dynamic": list_field(profile, "dynamic"),
"searchResults": search_list,
}
def display_context_item(item: Any) -> str: def display_context_item(item: Any) -> str:
if hasattr(item, "model_dump"): if hasattr(item, "model_dump"):
return json.dumps(item.model_dump(mode="json"), ensure_ascii=False) return json.dumps(item.model_dump(mode="json"), ensure_ascii=False)
@ -322,7 +307,7 @@ def display_context_item(item: Any) -> str:
return str(item) return str(item)
def direct_conversation_custom_id(conversation_id: str) -> str: def direct_conversation_id(conversation_id: str) -> str:
readable = re.sub(r"[^A-Za-z0-9._-]+", "-", conversation_id).strip("-._") readable = re.sub(r"[^A-Za-z0-9._-]+", "-", conversation_id).strip("-._")
readable = readable[:40] or "session" readable = readable[:40] or "session"
digest = hashlib.sha256(conversation_id.encode("utf-8")).hexdigest()[:12] digest = hashlib.sha256(conversation_id.encode("utf-8")).hexdigest()[:12]
@ -339,45 +324,65 @@ def conversation_transcript(messages: list[ChatMessage], assistant_text: str) ->
return "\n\n".join(transcript) return "\n\n".join(transcript)
def supermemory_http(sm_key: str, timeout: float = HTTP_TIMEOUT_SECONDS):
import httpx
return httpx.AsyncClient(
base_url=supermemory_base_url(),
headers={"Authorization": f"Bearer {sm_key}"},
timeout=timeout,
follow_redirects=False,
)
def namespace_path(namespace: str, suffix: str) -> str:
return f"/ns/{quote(namespace, safe='')}{suffix}"
async def fetch_profile_context( async def fetch_profile_context(
container_tag: str, namespace: str,
sm_key: str, sm_key: str,
query: Optional[str] = None, query: Optional[str] = None,
*,
include: Optional[list[str]] = None,
) -> dict[str, list[Any]]: ) -> dict[str, list[Any]]:
from supermemory import AsyncSupermemory async with supermemory_http(sm_key) as http:
profile_request = http.post(namespace_path(namespace, "/profile"), json={})
search_request = (
http.post(
namespace_path(namespace, "/search"),
json={
"query": query,
"searchMode": "memories",
"threshold": PROFILE_SEARCH_THRESHOLD,
},
)
if query
else None
)
responses = await asyncio.gather(
profile_request, *([search_request] if search_request else [])
)
for response in responses:
response.raise_for_status()
client = AsyncSupermemory( profile = responses[0].json().get("profile") or {}
api_key=sm_key, search = responses[1].json() if len(responses) > 1 else {}
base_url=supermemory_base_url(), return {
timeout=HTTP_TIMEOUT_SECONDS, "static": profile.get("static") or [],
) "dynamic": profile.get("dynamic") or [],
request: dict[str, Any] = {"container_tag": container_tag} "searchResults": search.get("results") or [],
if query: }
request["q"] = query
if include is not None:
request["include"] = include
profile_response = await client.profile(**request)
return extract_profile_context(profile_response)
async def chat_supermemory_direct( async def chat_supermemory_direct(
messages: list[ChatMessage], messages: list[ChatMessage],
container_tag: str, namespace: str,
conversation_id: str, conversation_id: str,
sm_key: str, sm_key: str,
oai_key: str, oai_key: str,
) -> tuple[str, str, dict[str, list[Any]]]: ) -> tuple[str, str, dict[str, list[Any]]]:
"""Manual pattern: profile() for context, then OpenAI, then add() conversation.""" """Manual pattern: profile + search for context, then OpenAI, then add the conversation."""
from openai import AsyncOpenAI from openai import AsyncOpenAI
from supermemory import AsyncSupermemory
sm_client = AsyncSupermemory(
api_key=sm_key,
base_url=supermemory_base_url(),
timeout=HTTP_TIMEOUT_SECONDS,
)
openai_client = AsyncOpenAI( openai_client = AsyncOpenAI(
api_key=oai_key, api_key=oai_key,
timeout=HTTP_TIMEOUT_SECONDS, timeout=HTTP_TIMEOUT_SECONDS,
@ -387,11 +392,7 @@ async def chat_supermemory_direct(
user_messages = [m for m in messages if m.role == "user"] user_messages = [m for m in messages if m.role == "user"]
last_user = user_messages[-1].content if user_messages else "" last_user = user_messages[-1].content if user_messages else ""
profile_response = await sm_client.profile( profile_context = await fetch_profile_context(namespace, sm_key, last_user)
container_tag=container_tag,
**({"q": last_user} if last_user else {}),
)
profile_context = extract_profile_context(profile_response)
context = "\n".join( context = "\n".join(
( (
"Profile static: " "Profile static: "
@ -418,40 +419,39 @@ async def chat_supermemory_direct(
) )
assistant_text = response.choices[0].message.content or "" assistant_text = response.choices[0].message.content or ""
custom_id = direct_conversation_custom_id(conversation_id) document_id = direct_conversation_id(conversation_id)
return assistant_text, custom_id, profile_context return assistant_text, document_id, profile_context
async def save_direct_conversation( async def save_direct_conversation(
messages: list[ChatMessage], messages: list[ChatMessage],
assistant_text: str, assistant_text: str,
container_tag: str, namespace: str,
custom_id: str, document_id: str,
sm_key: str, sm_key: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
from supermemory import AsyncSupermemory
try: try:
client = AsyncSupermemory(
api_key=sm_key,
base_url=supermemory_base_url(),
timeout=DIRECT_SAVE_TIMEOUT_SECONDS,
)
async with asyncio.timeout(DIRECT_SAVE_TIMEOUT_SECONDS): async with asyncio.timeout(DIRECT_SAVE_TIMEOUT_SECONDS):
response = await client.add( async with supermemory_http(sm_key, DIRECT_SAVE_TIMEOUT_SECONDS) as http:
content=conversation_transcript(messages, assistant_text), response = await http.post(
container_tag=container_tag, namespace_path(namespace, "/document"),
custom_id=custom_id, json={
) "content": conversation_transcript(messages, assistant_text),
"id": document_id,
"dreaming": "instant",
},
)
response.raise_for_status()
accepted = response.json()
return { return {
"type": "conversation_save_accepted", "type": "conversation_save_accepted",
"label": "Full conversation accepted for processing", "label": "Full conversation accepted for processing",
"detail": { "detail": {
"nonFatal": True, "nonFatal": True,
"containerTag": container_tag, "namespace": namespace,
"customId": custom_id, "id": document_id,
"documentId": object_field(response, "id"), "documentId": accepted.get("id"),
"status": object_field(response, "status"), "status": accepted.get("status"),
}, },
} }
except Exception as error: except Exception as error:
@ -460,77 +460,74 @@ async def save_direct_conversation(
"label": "Conversation save unavailable", "label": "Conversation save unavailable",
"detail": { "detail": {
"nonFatal": True, "nonFatal": True,
"containerTag": container_tag, "namespace": namespace,
"customId": custom_id, "id": document_id,
"error": public_error(error, sm_key), "error": public_error(error, sm_key),
}, },
} }
async def fetch_container_context( async def fetch_document_memories(http: Any, namespace: str, document_id: str) -> list[Any]:
container_tag: str, try:
response = await http.get(
namespace_path(namespace, f"/document/{quote(document_id, safe='')}"),
params={"include": "memories"},
)
response.raise_for_status()
return response.json().get("memories") or []
except Exception:
return []
async def fetch_namespace_context(
namespace: str,
sm_key: str, sm_key: str,
query: Optional[str] = None, query: Optional[str] = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
if not sm_key: if not sm_key:
raise RuntimeError("Supermemory API key must be supplied") raise RuntimeError("Supermemory API key must be supplied")
profile_context = await fetch_profile_context(container_tag, sm_key, query) profile_context = await fetch_profile_context(namespace, sm_key, query)
base_url = supermemory_base_url()
import httpx async with supermemory_http(sm_key) as http:
async with httpx.AsyncClient(
timeout=HTTP_TIMEOUT_SECONDS,
follow_redirects=False,
) as http:
docs_response = await http.post( docs_response = await http.post(
f"{base_url}/v3/documents/documents", namespace_path(namespace, "/list/documents"),
headers={ params={
"Authorization": f"Bearer {sm_key}", "limit": DOCUMENT_LIST_LIMIT,
"Content-Type": "application/json",
},
json={
"containerTags": [container_tag],
"limit": 25,
"sort": "createdAt", "sort": "createdAt",
"order": "desc", "order": "desc",
}, },
json={},
) )
docs_response.raise_for_status() docs_response.raise_for_status()
docs = docs_response.json() docs = docs_response.json()
raw_documents = docs.get("documents") or []
raw_documents = docs.get("documents", []) if isinstance(docs, dict) else [] # v5 lists omit memories, so each document is fetched with include=memories.
memories = await asyncio.gather(
*(fetch_document_memories(http, namespace, doc["id"]) for doc in raw_documents)
)
documents = [] documents = []
for doc in raw_documents: for doc, memory_entries in zip(raw_documents, memories):
record = doc if isinstance(doc, dict) else getattr(doc, "__dict__", {}) system = doc.get("system") or {}
memory_entries = (
record.get("memoryEntries") or record.get("memory_entries") or []
)
if not memory_entries and isinstance(record.get("memories"), list):
nested = record.get("memories") or []
if nested and isinstance(nested[0], dict) and nested[0].get("memory"):
memory_entries = nested
documents.append( documents.append(
{ {
"id": record.get("id"), "id": doc.get("id"),
"title": record.get("title"), "title": doc.get("title"),
"status": record.get("status"), "status": system.get("status"),
"customId": record.get("customId") or record.get("custom_id"), "createdAt": system.get("createdAt"),
"createdAt": record.get("createdAt") or record.get("created_at"), "updatedAt": system.get("updatedAt"),
"updatedAt": record.get("updatedAt") or record.get("updated_at"), "summary": doc.get("summary"),
"summary": record.get("summary"),
"memoryEntries": memory_entries, "memoryEntries": memory_entries,
} }
) )
return { return {
"containerTag": container_tag, "namespace": namespace,
"query": query, "query": query,
"profile": profile_context, "profile": profile_context,
"documents": documents, "documents": documents,
"pagination": docs.get("pagination") if isinstance(docs, dict) else None, "pagination": docs.get("pagination"),
} }
@ -578,7 +575,7 @@ def reconstruct_python_sdk_memory_block(
def build_middleware_memory_debug( def build_middleware_memory_debug(
container_tag: str, namespace: str,
conversation_id: str, conversation_id: str,
memory_mode: str, memory_mode: str,
last_user_message: str, last_user_message: str,
@ -610,9 +607,11 @@ def build_middleware_memory_debug(
"detail": { "detail": {
"authoritativeMiddlewareCapture": False, "authoritativeMiddlewareCapture": False,
"timing": "after model response", "timing": "after model response",
"endpoint": "POST /v4/profile", "endpoint": (
"containerTag": container_tag, "POST /ns/{namespace}/profile + POST /ns/{namespace}/search"
"customId": conversation_id, ),
"namespace": namespace,
"id": conversation_id,
"memoryMode": memory_mode, "memoryMode": memory_mode,
"query": context.get("query"), "query": context.get("query"),
"staticCount": len(profile.get("static", [])), "staticCount": len(profile.get("static", [])),
@ -640,8 +639,8 @@ def build_middleware_memory_debug(
) )
save_detail = { save_detail = {
"containerTag": container_tag, "namespace": namespace,
"customId": f"conversation:{conversation_id}", "id": f"conversation:{conversation_id}",
"addMemory": middleware_config.addMemory, "addMemory": middleware_config.addMemory,
"verbose": middleware_config.verbose, "verbose": middleware_config.verbose,
} }
@ -665,21 +664,16 @@ def build_middleware_memory_debug(
async def fetch_context_for_debug( async def fetch_context_for_debug(
container_tag: str, namespace: str,
query: Optional[str], query: Optional[str],
sm_key: str, sm_key: str,
) -> tuple[Optional[dict[str, Any]], Optional[str]]: ) -> tuple[Optional[dict[str, Any]], Optional[str]]:
try: try:
async with asyncio.timeout(CONTEXT_DEBUG_TIMEOUT_SECONDS): async with asyncio.timeout(CONTEXT_DEBUG_TIMEOUT_SECONDS):
profile = await fetch_profile_context( profile = await fetch_profile_context(namespace, sm_key, query)
container_tag,
sm_key,
query,
include=["static", "dynamic"],
)
return ( return (
{ {
"containerTag": container_tag, "namespace": namespace,
"query": query, "query": query,
"profile": profile, "profile": profile,
}, },
@ -691,12 +685,12 @@ async def fetch_context_for_debug(
@app.get("/context") @app.get("/context")
async def context_get( async def context_get(
containerTag: Annotated[ namespace: Annotated[
str, str,
Query( Query(
min_length=1, min_length=1,
max_length=MAX_CONTAINER_TAG_LENGTH, max_length=MAX_NAMESPACE_LENGTH,
pattern=CONTAINER_TAG_PATTERN, pattern=NAMESPACE_PATTERN,
), ),
] = "sdk-playground", ] = "sdk-playground",
query: Annotated[Optional[str], Query(max_length=MAX_MESSAGE_LENGTH)] = None, query: Annotated[Optional[str], Query(max_length=MAX_MESSAGE_LENGTH)] = None,
@ -712,7 +706,7 @@ async def context_get(
"X-Supermemory-API-Key header", "X-Supermemory-API-Key header",
) )
async with asyncio.timeout(HTTP_TIMEOUT_SECONDS): async with asyncio.timeout(HTTP_TIMEOUT_SECONDS):
ctx = await fetch_container_context(containerTag, sm_key, query) ctx = await fetch_namespace_context(namespace, sm_key, query)
return {"ok": True, "context": ctx} return {"ok": True, "context": ctx}
except Exception as error: except Exception as error:
return JSONResponse( return JSONResponse(
@ -731,7 +725,7 @@ async def context_post(req: ContextRequest):
try: try:
sm_key = resolve_supermemory_key(req.apiKeys) sm_key = resolve_supermemory_key(req.apiKeys)
async with asyncio.timeout(HTTP_TIMEOUT_SECONDS): async with asyncio.timeout(HTTP_TIMEOUT_SECONDS):
ctx = await fetch_container_context(req.containerTag, sm_key, req.query) ctx = await fetch_namespace_context(req.namespace, sm_key, req.query)
return {"ok": True, "context": ctx} return {"ok": True, "context": ctx}
except Exception as error: except Exception as error:
return JSONResponse( return JSONResponse(
@ -775,7 +769,7 @@ async def chat(req: ChatRequest):
middleware_config = req.middlewareConfig or MiddlewareConfig() middleware_config = req.middlewareConfig or MiddlewareConfig()
text = await chat_openai_middleware( text = await chat_openai_middleware(
req.messages, req.messages,
req.containerTag, req.namespace,
req.conversationId, req.conversationId,
req.memoryMode or "full", req.memoryMode or "full",
middleware_config, middleware_config,
@ -790,12 +784,12 @@ async def chat(req: ChatRequest):
middleware_debug = (middleware_config, last_user, query) middleware_debug = (middleware_config, last_user, query)
elif req.sdkId == "py-openai-tools": elif req.sdkId == "py-openai-tools":
text, tool_trace = await chat_openai_tools( text, tool_trace = await chat_openai_tools(
req.messages, req.containerTag, sm_key, oai_key req.messages, req.namespace, sm_key, oai_key
) )
elif req.sdkId == "py-supermemory-direct": elif req.sdkId == "py-supermemory-direct":
text, custom_id, profile_context = await chat_supermemory_direct( text, document_id, profile_context = await chat_supermemory_direct(
req.messages, req.messages,
req.containerTag, req.namespace,
req.conversationId, req.conversationId,
sm_key, sm_key,
oai_key, oai_key,
@ -804,19 +798,19 @@ async def chat(req: ChatRequest):
(m.content for m in reversed(req.messages) if m.role == "user"), (m.content for m in reversed(req.messages) if m.role == "user"),
"", "",
) )
direct_debug = (custom_id, profile_context, last_user) direct_debug = (document_id, profile_context, last_user)
else: else:
raise RuntimeError(f"Unsupported Python SDK: {req.sdkId}") raise RuntimeError(f"Unsupported Python SDK: {req.sdkId}")
if middleware_debug is not None: if middleware_debug is not None:
middleware_config, last_user, query = middleware_debug middleware_config, last_user, query = middleware_debug
ctx, context_error = await fetch_context_for_debug( ctx, context_error = await fetch_context_for_debug(
req.containerTag, req.namespace,
query, query,
sm_key, sm_key,
) )
memory_debug = build_middleware_memory_debug( memory_debug = build_middleware_memory_debug(
req.containerTag, req.namespace,
req.conversationId, req.conversationId,
req.memoryMode or "full", req.memoryMode or "full",
last_user, last_user,
@ -825,12 +819,12 @@ async def chat(req: ChatRequest):
middleware_config, middleware_config,
) )
elif direct_debug is not None: elif direct_debug is not None:
custom_id, profile_context, last_user = direct_debug document_id, profile_context, last_user = direct_debug
save_debug = await save_direct_conversation( save_debug = await save_direct_conversation(
req.messages, req.messages,
text, text,
req.containerTag, req.namespace,
custom_id, document_id,
sm_key, sm_key,
) )
memory_debug = [ memory_debug = [
@ -838,7 +832,7 @@ async def chat(req: ChatRequest):
"type": "manual_profile", "type": "manual_profile",
"label": "Profile context used for this response", "label": "Profile context used for this response",
"detail": { "detail": {
"containerTag": req.containerTag, "namespace": req.namespace,
"query": last_user, "query": last_user,
"staticCount": len(profile_context["static"]), "staticCount": len(profile_context["static"]),
"dynamicCount": len(profile_context["dynamic"]), "dynamicCount": len(profile_context["dynamic"]),

View file

@ -1043,7 +1043,6 @@ dependencies = [
{ name = "httpx" }, { name = "httpx" },
{ name = "openai" }, { name = "openai" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "supermemory" },
{ name = "supermemory-openai-sdk", extra = ["async"] }, { name = "supermemory-openai-sdk", extra = ["async"] },
{ name = "uvicorn", extra = ["standard"] }, { name = "uvicorn", extra = ["standard"] },
] ]
@ -1054,7 +1053,6 @@ requires-dist = [
{ name = "httpx", specifier = ">=0.28.0" }, { name = "httpx", specifier = ">=0.28.0" },
{ name = "openai", specifier = ">=1.102.0" }, { name = "openai", specifier = ">=1.102.0" },
{ name = "python-dotenv", specifier = ">=1.0.1" }, { name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "supermemory", specifier = ">=3.50.0" },
{ name = "supermemory-openai-sdk", extras = ["async"], editable = "../../../packages/openai-sdk-python" }, { name = "supermemory-openai-sdk", extras = ["async"], editable = "../../../packages/openai-sdk-python" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.32.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.32.0" },
] ]
@ -1100,7 +1098,7 @@ wheels = [
[[package]] [[package]]
name = "supermemory-openai-sdk" name = "supermemory-openai-sdk"
version = "1.0.8" version = "1.0.9"
source = { editable = "../../../packages/openai-sdk-python" } source = { editable = "../../../packages/openai-sdk-python" }
dependencies = [ dependencies = [
{ name = "openai" }, { name = "openai" },
@ -1116,17 +1114,17 @@ async = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "aiohttp", marker = "extra == 'async'", specifier = ">=3.8.0" }, { name = "aiohttp", marker = "extra == 'async'", specifier = ">=3.14.3" },
{ name = "openai", specifier = ">=1.102.0" }, { name = "openai", specifier = ">=1.102.0" },
{ name = "requests", specifier = ">=2.25.0" }, { name = "requests", specifier = ">=2.25.0" },
{ name = "supermemory", specifier = ">=3.50.0" }, { name = "supermemory", specifier = ">=3.50.0,<5" },
{ name = "typing-extensions", specifier = ">=4.0.0" }, { name = "typing-extensions", specifier = ">=4.0.0" },
] ]
provides-extras = ["async"] provides-extras = ["async"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "black", specifier = ">=24.8.0" }, { name = "black", specifier = ">=26.3.1" },
{ name = "flake8", specifier = ">=7.1.2" }, { name = "flake8", specifier = ">=7.1.2" },
{ name = "isort", specifier = ">=5.13.2" }, { name = "isort", specifier = ">=5.13.2" },
{ name = "mypy", specifier = ">=1.14.1" }, { name = "mypy", specifier = ">=1.14.1" },

View file

@ -14,7 +14,7 @@ import {
assertTrustedBrowserRequest, assertTrustedBrowserRequest,
mayUseEnvironmentKeys, mayUseEnvironmentKeys,
parseApiKeys, parseApiKeys,
parseContainerTag, parseNamespace,
parseConversationId, parseConversationId,
parseIdentifier, parseIdentifier,
parseMemoryMode, parseMemoryMode,
@ -63,7 +63,7 @@ export async function POST(req: Request) {
const body = await readJsonObject(req) const body = await readJsonObject(req)
const sdkId = parseIdentifier(body.sdkId, "sdkId") const sdkId = parseIdentifier(body.sdkId, "sdkId")
const messages = parseMessages(body.messages) const messages = parseMessages(body.messages)
const containerTag = parseContainerTag(body.containerTag) const namespace = parseNamespace(body.namespace)
const conversationId = parseConversationId(body.conversationId) const conversationId = parseConversationId(body.conversationId)
const memoryMode = parseMemoryMode(body.memoryMode) const memoryMode = parseMemoryMode(body.memoryMode)
const middlewareConfig = parseMiddlewareConfig(body.middlewareConfig) const middlewareConfig = parseMiddlewareConfig(body.middlewareConfig)
@ -97,7 +97,7 @@ export async function POST(req: Request) {
body: JSON.stringify({ body: JSON.stringify({
sdkId, sdkId,
messages, messages,
containerTag, namespace,
conversationId, conversationId,
memoryMode, memoryMode,
middlewareConfig, middlewareConfig,
@ -129,12 +129,11 @@ export async function POST(req: Request) {
{ {
sdkId, sdkId,
messages, messages,
containerTag, namespace,
conversationId, conversationId,
memoryMode, memoryMode,
middlewareConfig, middlewareConfig,
apiKeys, apiKeys,
containerTags: [containerTag],
}, },
apiKeys, apiKeys,
) )

View file

@ -1,12 +1,12 @@
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
import { resolveSupermemoryApiKey } from "@/lib/api-keys" import { resolveSupermemoryApiKey } from "@/lib/api-keys"
import { fetchContainerContext } from "@/lib/context-api" import { fetchNamespaceContext } from "@/lib/context-api"
import { import {
PlaygroundRequestError, PlaygroundRequestError,
assertTrustedBrowserRequest, assertTrustedBrowserRequest,
mayUseEnvironmentKeys, mayUseEnvironmentKeys,
parseApiKeys, parseApiKeys,
parseContainerTag, parseNamespace,
parseOptionalText, parseOptionalText,
readJsonObject, readJsonObject,
} from "@/lib/request-validation" } from "@/lib/request-validation"
@ -15,7 +15,7 @@ export async function GET(req: Request) {
try { try {
assertTrustedBrowserRequest(req) assertTrustedBrowserRequest(req)
const { searchParams } = new URL(req.url) const { searchParams } = new URL(req.url)
const containerTag = parseContainerTag(searchParams.get("containerTag")) const namespace = parseNamespace(searchParams.get("namespace"))
const query = parseOptionalText(searchParams.get("query"), "query") const query = parseOptionalText(searchParams.get("query"), "query")
const supermemoryApiKey = resolveSupermemoryApiKey(null, { const supermemoryApiKey = resolveSupermemoryApiKey(null, {
allowEnvironment: mayUseEnvironmentKeys(req), allowEnvironment: mayUseEnvironmentKeys(req),
@ -32,8 +32,8 @@ export async function GET(req: Request) {
) )
} }
const context = await fetchContainerContext( const context = await fetchNamespaceContext(
containerTag, namespace,
query, query,
supermemoryApiKey, supermemoryApiKey,
) )
@ -54,7 +54,7 @@ export async function POST(req: Request) {
try { try {
assertTrustedBrowserRequest(req) assertTrustedBrowserRequest(req)
const body = await readJsonObject(req) const body = await readJsonObject(req)
const containerTag = parseContainerTag(body.containerTag) const namespace = parseNamespace(body.namespace)
const query = parseOptionalText(body.query, "query") const query = parseOptionalText(body.query, "query")
const supermemoryApiKey = resolveSupermemoryApiKey( const supermemoryApiKey = resolveSupermemoryApiKey(
parseApiKeys(body.apiKeys), parseApiKeys(body.apiKeys),
@ -71,8 +71,8 @@ export async function POST(req: Request) {
) )
} }
const context = await fetchContainerContext( const context = await fetchNamespaceContext(
containerTag, namespace,
query, query,
supermemoryApiKey, supermemoryApiKey,
) )

View file

@ -52,7 +52,7 @@ export default function AgentPlaygroundPage() {
const keysReady = supermemoryKeyReady && openAiKeyReady const keysReady = supermemoryKeyReady && openAiKeyReady
const [sdkId, setSdkId] = useState("ts-ai-sdk-middleware") const [sdkId, setSdkId] = useState("ts-ai-sdk-middleware")
const [containerTag, setContainerTag] = useState("sdk-playground") const [namespace, setNamespace] = useState("sdk-playground")
const [memoryMode, setMemoryMode] = useState<"profile" | "query" | "full">( const [memoryMode, setMemoryMode] = useState<"profile" | "query" | "full">(
"full", "full",
) )
@ -134,7 +134,7 @@ export default function AgentPlaygroundPage() {
...chatHistory, ...chatHistory,
{ role: "user", content: userMessage.content }, { role: "user", content: userMessage.content },
], ],
containerTag, namespace,
conversationId: activeConversationId, conversationId: activeConversationId,
memoryMode: memoryMode:
selectedSdk.mode === "middleware" ? memoryMode : undefined, selectedSdk.mode === "middleware" ? memoryMode : undefined,
@ -301,16 +301,16 @@ export default function AgentPlaygroundPage() {
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<label className="space-y-1"> <label className="space-y-1">
<span className="text-xs text-zinc-500">Container tag</span> <span className="text-xs text-zinc-500">Namespace</span>
<input <input
value={containerTag} value={namespace}
onChange={(e) => setContainerTag(e.target.value)} onChange={(e) => setNamespace(e.target.value)}
maxLength={100} maxLength={100}
className="rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm w-40" className="rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm w-40"
/> />
</label> </label>
<label className="space-y-1"> <label className="space-y-1">
<span className="text-xs text-zinc-500">customId (session)</span> <span className="text-xs text-zinc-500">id (session)</span>
<input <input
value={conversationId} value={conversationId}
onChange={(e) => setConversationId(e.target.value)} onChange={(e) => setConversationId(e.target.value)}
@ -521,7 +521,7 @@ export default function AgentPlaygroundPage() {
<aside className="hidden w-80 shrink-0 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900/20 p-3 xl:flex xl:w-96 xl:flex-col min-h-0"> <aside className="hidden w-80 shrink-0 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900/20 p-3 xl:flex xl:w-96 xl:flex-col min-h-0">
<ContextPanel <ContextPanel
containerTag={containerTag} namespace={namespace}
lastUserMessage={lastUserMessage} lastUserMessage={lastUserMessage}
refreshKey={contextRefreshKey} refreshKey={contextRefreshKey}
supermemoryApiKey={supermemoryApiKey} supermemoryApiKey={supermemoryApiKey}

View file

@ -1,24 +1,24 @@
"use client" "use client"
import { useCallback, useEffect, useRef, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import type { ContainerContext } from "@/lib/context-api" import type { NamespaceContext } from "@/lib/context-api"
type ContextDocument = ContainerContext["documents"][number] type ContextDocument = NamespaceContext["documents"][number]
export function ContextPanel({ export function ContextPanel({
containerTag, namespace,
lastUserMessage, lastUserMessage,
refreshKey, refreshKey,
supermemoryApiKey, supermemoryApiKey,
supermemoryKeyReady, supermemoryKeyReady,
}: { }: {
containerTag: string namespace: string
lastUserMessage?: string lastUserMessage?: string
refreshKey: number refreshKey: number
supermemoryApiKey: string supermemoryApiKey: string
supermemoryKeyReady: boolean supermemoryKeyReady: boolean
}) { }) {
const [context, setContext] = useState<ContainerContext | null>(null) const [context, setContext] = useState<NamespaceContext | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [useQuery, setUseQuery] = useState(false) const [useQuery, setUseQuery] = useState(false)
@ -31,9 +31,9 @@ export function ContextPanel({
setContext(null) setContext(null)
return return
} }
const normalizedContainerTag = containerTag.trim() const normalizedNamespace = namespace.trim()
if (!normalizedContainerTag) { if (!normalizedNamespace) {
setError("Enter a container tag to load context") setError("Enter a namespace to load context")
setContext(null) setContext(null)
return return
} }
@ -49,7 +49,7 @@ export function ContextPanel({
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
signal: controller.signal, signal: controller.signal,
body: JSON.stringify({ body: JSON.stringify({
containerTag: normalizedContainerTag, namespace: normalizedNamespace,
...(useQuery && lastUserMessage ? { query: lastUserMessage } : {}), ...(useQuery && lastUserMessage ? { query: lastUserMessage } : {}),
...(supermemoryApiKey.trim() ...(supermemoryApiKey.trim()
? { ? {
@ -75,7 +75,7 @@ export function ContextPanel({
} }
} }
}, [ }, [
containerTag, namespace,
lastUserMessage, lastUserMessage,
useQuery, useQuery,
supermemoryApiKey, supermemoryApiKey,
@ -85,7 +85,7 @@ export function ContextPanel({
useEffect(() => { useEffect(() => {
// The counter changes after a successful chat and explicitly refreshes context. // The counter changes after a successful chat and explicitly refreshes context.
void refreshKey void refreshKey
if (!supermemoryKeyReady || !containerTag.trim()) { if (!supermemoryKeyReady || !namespace.trim()) {
activeRequest.current?.abort() activeRequest.current?.abort()
activeRequest.current = null activeRequest.current = null
setLoading(false) setLoading(false)
@ -102,13 +102,13 @@ export function ContextPanel({
window.clearTimeout(timeout) window.clearTimeout(timeout)
activeRequest.current?.abort() activeRequest.current?.abort()
} }
}, [load, refreshKey, supermemoryKeyReady, containerTag]) }, [load, refreshKey, supermemoryKeyReady, namespace])
useEffect(() => { useEffect(() => {
// A different container must not retain the previous document selection. // A different namespace must not retain the previous document selection.
void containerTag void namespace
setSelectedDocKey(null) setSelectedDocKey(null)
}, [containerTag]) }, [namespace])
const selectedDoc = const selectedDoc =
context?.documents.find((doc) => documentKey(doc) === selectedDocKey) ?? context?.documents.find((doc) => documentKey(doc) === selectedDocKey) ??
@ -118,12 +118,12 @@ export function ContextPanel({
<div className="flex min-h-0 flex-col gap-3 text-sm"> <div className="flex min-h-0 flex-col gap-3 text-sm">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<h2 className="text-xs font-medium uppercase tracking-wider text-zinc-500"> <h2 className="text-xs font-medium uppercase tracking-wider text-zinc-500">
Container context Namespace context
</h2> </h2>
<button <button
type="button" type="button"
onClick={() => void load()} onClick={() => void load()}
disabled={!supermemoryKeyReady || !containerTag.trim()} disabled={!supermemoryKeyReady || !namespace.trim()}
className="text-xs text-zinc-400 hover:text-zinc-200 disabled:cursor-not-allowed disabled:text-zinc-700" className="text-xs text-zinc-400 hover:text-zinc-200 disabled:cursor-not-allowed disabled:text-zinc-700"
> >
Refresh Refresh
@ -210,12 +210,9 @@ export function ContextPanel({
<div className="text-zinc-500 truncate"> <div className="text-zinc-500 truncate">
{doc.title ?? "untitled"} {doc.title ?? "untitled"}
</div> </div>
<div className="text-zinc-600"> {doc.status && (
{doc.customId <div className="text-zinc-600">{doc.status}</div>
? `session: ${doc.customId}` )}
: "no customId"}
{doc.status ? ` · ${doc.status}` : ""}
</div>
</button> </button>
</li> </li>
) )
@ -307,7 +304,7 @@ function MemoryEntryCard({ entry }: { entry: unknown }) {
} }
function documentKey(doc: ContextDocument): string { function documentKey(doc: ContextDocument): string {
return doc.id ?? doc.customId ?? doc.title ?? "unknown" return doc.id ?? doc.title ?? "unknown"
} }
function memoryEntryKey(entry: unknown, index: number): string { function memoryEntryKey(entry: unknown, index: number): string {

View file

@ -1,7 +1,6 @@
import { createOpenAI } from "@ai-sdk/openai" import { createOpenAI } from "@ai-sdk/openai"
import { generateText, stepCountIs, type ModelMessage } from "ai" import { generateText, stepCountIs, type ModelMessage } from "ai"
import OpenAI from "openai" import OpenAI from "openai"
import { supermemoryTools as aiSdkPackageTools } from "@supermemory/ai-sdk"
import { withSupermemory as withSupermemoryAiSdk } from "@supermemory/tools/ai-sdk" import { withSupermemory as withSupermemoryAiSdk } from "@supermemory/tools/ai-sdk"
import { import {
createToolCallsExecutor, createToolCallsExecutor,
@ -39,13 +38,11 @@ export interface ChatResult {
export interface ChatRequest { export interface ChatRequest {
sdkId: string sdkId: string
messages: ChatMessage[] messages: ChatMessage[]
containerTag: string namespace: string
conversationId: string conversationId: string
memoryMode?: "profile" | "query" | "full" memoryMode?: "profile" | "query" | "full"
middlewareConfig?: Partial<MiddlewareRuntimeConfig> middlewareConfig?: Partial<MiddlewareRuntimeConfig>
apiKeys?: Partial<PlaygroundApiKeys> apiKeys?: Partial<PlaygroundApiKeys>
containerTags?: string[]
projectId?: string
} }
export class PlaygroundChatTimeoutError extends Error { export class PlaygroundChatTimeoutError extends Error {
@ -118,15 +115,8 @@ function getModelName(): string {
return process.env.MODEL_NAME ?? "gpt-4o-mini" return process.env.MODEL_NAME ?? "gpt-4o-mini"
} }
function getToolsConfig( function getToolsConfig(namespace: string): SupermemoryToolsConfig {
containerTags?: string[], return { baseUrl: process.env.SUPERMEMORY_BASE_URL, namespace }
projectId?: string,
): SupermemoryToolsConfig {
return {
baseUrl: process.env.SUPERMEMORY_BASE_URL,
...(containerTags?.length ? { containerTags } : {}),
...(projectId ? { projectId } : {}),
}
} }
function toModelMessages(messages: ChatMessage[]): ModelMessage[] { function toModelMessages(messages: ChatMessage[]): ModelMessage[] {
@ -168,7 +158,7 @@ function lastUserMessage(messages: ChatMessage[]): string {
async function chatAiSdkMiddleware( async function chatAiSdkMiddleware(
keys: PlaygroundApiKeys, keys: PlaygroundApiKeys,
messages: ChatMessage[], messages: ChatMessage[],
containerTag: string, namespace: string,
conversationId: string, conversationId: string,
memoryMode: "profile" | "query" | "full", memoryMode: "profile" | "query" | "full",
middlewareConfig: MiddlewareRuntimeConfig, middlewareConfig: MiddlewareRuntimeConfig,
@ -176,8 +166,8 @@ async function chatAiSdkMiddleware(
): Promise<ChatResult> { ): Promise<ChatResult> {
const openai = createOpenAI({ apiKey: keys.openaiApiKey }) const openai = createOpenAI({ apiKey: keys.openaiApiKey })
const model = withSupermemoryAiSdk(openai(getModelName()), { const model = withSupermemoryAiSdk(openai(getModelName()), {
containerTag, namespace,
customId: conversationId, id: conversationId,
apiKey: keys.supermemoryApiKey, apiKey: keys.supermemoryApiKey,
mode: memoryMode, mode: memoryMode,
addMemory: middlewareConfig.addMemory, addMemory: middlewareConfig.addMemory,
@ -201,7 +191,7 @@ async function chatAiSdkMiddleware(
async function chatOpenAiMiddleware( async function chatOpenAiMiddleware(
keys: PlaygroundApiKeys, keys: PlaygroundApiKeys,
messages: ChatMessage[], messages: ChatMessage[],
containerTag: string, namespace: string,
conversationId: string, conversationId: string,
memoryMode: "profile" | "query" | "full", memoryMode: "profile" | "query" | "full",
middlewareConfig: MiddlewareRuntimeConfig, middlewareConfig: MiddlewareRuntimeConfig,
@ -213,8 +203,8 @@ async function chatOpenAiMiddleware(
maxRetries: 1, maxRetries: 1,
}) })
const client = withSupermemoryOpenAi(openai, { const client = withSupermemoryOpenAi(openai, {
containerTag, namespace,
customId: conversationId, id: conversationId,
apiKey: keys.supermemoryApiKey, apiKey: keys.supermemoryApiKey,
mode: memoryMode, mode: memoryMode,
addMemory: middlewareConfig.addMemory, addMemory: middlewareConfig.addMemory,
@ -242,15 +232,11 @@ async function chatAiSdkTools(
keys: PlaygroundApiKeys, keys: PlaygroundApiKeys,
toolsFactory: typeof aiSdkTools, toolsFactory: typeof aiSdkTools,
messages: ChatMessage[], messages: ChatMessage[],
containerTags?: string[], namespace: string,
projectId?: string,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<ChatResult> { ): Promise<ChatResult> {
const openai = createOpenAI({ apiKey: keys.openaiApiKey }) const openai = createOpenAI({ apiKey: keys.openaiApiKey })
const tools = toolsFactory( const tools = toolsFactory(keys.supermemoryApiKey, getToolsConfig(namespace))
keys.supermemoryApiKey,
getToolsConfig(containerTags, projectId),
)
const result = await generateText({ const result = await generateText({
model: openai(getModelName()), model: openai(getModelName()),
@ -272,8 +258,7 @@ async function chatAiSdkTools(
async function chatOpenAiTools( async function chatOpenAiTools(
keys: PlaygroundApiKeys, keys: PlaygroundApiKeys,
messages: ChatMessage[], messages: ChatMessage[],
containerTags?: string[], namespace: string,
projectId?: string,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<ChatResult> { ): Promise<ChatResult> {
const openai = new OpenAI({ const openai = new OpenAI({
@ -281,7 +266,7 @@ async function chatOpenAiTools(
timeout: MODEL_REQUEST_TIMEOUT_MS, timeout: MODEL_REQUEST_TIMEOUT_MS,
maxRetries: 1, maxRetries: 1,
}) })
const config = getToolsConfig(containerTags, projectId) const config = getToolsConfig(namespace)
const executeToolCalls = createToolCallsExecutor( const executeToolCalls = createToolCallsExecutor(
keys.supermemoryApiKey, keys.supermemoryApiKey,
config, config,
@ -351,7 +336,6 @@ export async function runTypeScriptChat(
throw new Error(`Invalid TypeScript chat SDK: ${request.sdkId}`) throw new Error(`Invalid TypeScript chat SDK: ${request.sdkId}`)
} }
const containerTags = request.containerTags ?? [request.containerTag]
const memoryMode = request.memoryMode ?? "full" const memoryMode = request.memoryMode ?? "full"
const middlewareConfig = normalizeMiddlewareConfig(request.middlewareConfig) const middlewareConfig = normalizeMiddlewareConfig(request.middlewareConfig)
@ -361,7 +345,7 @@ export async function runTypeScriptChat(
return await chatAiSdkMiddleware( return await chatAiSdkMiddleware(
keys, keys,
request.messages, request.messages,
request.containerTag, request.namespace,
request.conversationId, request.conversationId,
memoryMode, memoryMode,
middlewareConfig, middlewareConfig,
@ -371,7 +355,7 @@ export async function runTypeScriptChat(
return await chatOpenAiMiddleware( return await chatOpenAiMiddleware(
keys, keys,
request.messages, request.messages,
request.containerTag, request.namespace,
request.conversationId, request.conversationId,
memoryMode, memoryMode,
middlewareConfig, middlewareConfig,
@ -382,25 +366,14 @@ export async function runTypeScriptChat(
keys, keys,
aiSdkTools, aiSdkTools,
request.messages, request.messages,
containerTags, request.namespace,
request.projectId,
signal, signal,
) )
case "ts-openai-tools": case "ts-openai-tools":
return await chatOpenAiTools( return await chatOpenAiTools(
keys, keys,
request.messages, request.messages,
containerTags, request.namespace,
request.projectId,
signal,
)
case "ts-ai-sdk-package":
return await chatAiSdkTools(
keys,
aiSdkPackageTools,
request.messages,
containerTags,
request.projectId,
signal, signal,
) )
default: default:
@ -417,7 +390,7 @@ export async function runTypeScriptChat(
const memoryDebug = await buildBestEffortDebug((signal) => const memoryDebug = await buildBestEffortDebug((signal) =>
buildMiddlewareMemoryDebug( buildMiddlewareMemoryDebug(
request.containerTag, request.namespace,
request.conversationId, request.conversationId,
memoryMode, memoryMode,
lastUserMessage(request.messages), lastUserMessage(request.messages),

View file

@ -1,4 +1,5 @@
import Supermemory from "supermemory" import { Supermemory } from "supermemory"
import { supermemoryProfileSearch } from "../../../../packages/tools/src/shared/memory-client"
import { import {
type MiddlewareRuntimeConfig, type MiddlewareRuntimeConfig,
normalizeMiddlewareConfig, normalizeMiddlewareConfig,
@ -28,8 +29,8 @@ export interface MemoryDebugEntry {
preview?: string preview?: string
} }
export interface ContainerContext { export interface NamespaceContext {
containerTag: string namespace: string
query?: string query?: string
profile: { profile: {
static: unknown[] static: unknown[]
@ -40,7 +41,6 @@ export interface ContainerContext {
id?: string id?: string
title?: string title?: string
status?: string status?: string
customId?: string
createdAt?: string createdAt?: string
updatedAt?: string updatedAt?: string
summary?: string summary?: string
@ -49,31 +49,21 @@ export interface ContainerContext {
pagination?: unknown pagination?: unknown
} }
const REQUEST_TIMEOUT_SECONDS = 10
const DOCUMENT_LIST_LIMIT = 25
function getSupermemoryClient(apiKey: string) { function getSupermemoryClient(apiKey: string) {
if (!apiKey) throw new Error("Supermemory API key is required") if (!apiKey) throw new Error("Supermemory API key is required")
return new Supermemory({ return new Supermemory({
apiKey, apiKey,
timeout: 10_000, timeoutInSeconds: REQUEST_TIMEOUT_SECONDS,
maxRetries: 1, maxRetries: 1,
...(process.env.SUPERMEMORY_BASE_URL ...(process.env.SUPERMEMORY_BASE_URL
? { baseURL: process.env.SUPERMEMORY_BASE_URL } ? { baseUrl: process.env.SUPERMEMORY_BASE_URL }
: {}), : {}),
}) })
} }
function normalizeMemoryEntries(record: Record<string, unknown>): unknown[] {
const raw =
record.memoryEntries ??
record.memory_entries ??
(Array.isArray(record.memories) &&
record.memories.length > 0 &&
typeof (record.memories[0] as Record<string, unknown>)?.memory === "string"
? record.memories
: undefined)
return Array.isArray(raw) ? raw : []
}
function memoryText(item: unknown): string { function memoryText(item: unknown): string {
if (typeof item === "string") return item if (typeof item === "string") return item
if (item && typeof item === "object") { if (item && typeof item === "object") {
@ -85,7 +75,7 @@ function memoryText(item: unknown): string {
return JSON.stringify(item) return JSON.stringify(item)
} }
function summarizeProfile(profile: ContainerContext["profile"]) { function summarizeProfile(profile: NamespaceContext["profile"]) {
return { return {
staticCount: profile.static.length, staticCount: profile.static.length,
dynamicCount: profile.dynamic.length, dynamicCount: profile.dynamic.length,
@ -96,16 +86,6 @@ function summarizeProfile(profile: ContainerContext["profile"]) {
} }
} }
function normalizeSearchResults(searchResults: unknown): unknown[] {
if (!searchResults) return []
if (Array.isArray(searchResults)) return searchResults
if (typeof searchResults === "object") {
const record = searchResults as Record<string, unknown>
if (Array.isArray(record.results)) return record.results
}
return []
}
export function resolveProfileQuery( export function resolveProfileQuery(
lastUserMessage: string, lastUserMessage: string,
mode: "profile" | "query" | "full", mode: "profile" | "query" | "full",
@ -114,82 +94,77 @@ export function resolveProfileQuery(
return lastUserMessage || undefined return lastUserMessage || undefined
} }
// Same profile + memories search the middlewares run before the model call.
async function fetchProfileContext( async function fetchProfileContext(
client: ReturnType<typeof getSupermemoryClient>, apiKey: string,
containerTag: string, namespace: string,
query?: string, query?: string,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<ContainerContext["profile"]> { ): Promise<NamespaceContext["profile"]> {
const profileResponse = await client.post<{ const response = await supermemoryProfileSearch(
profile?: { static?: unknown[]; dynamic?: unknown[] } namespace,
searchResults?: unknown query ?? "",
}>("/v4/profile", { process.env.SUPERMEMORY_BASE_URL ?? "",
body: { apiKey,
containerTag, signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_SECONDS * 1_000),
include: ["static", "dynamic"], )
...(query ? { q: query } : {}),
},
...(signal ? { signal } : {}),
})
const profileRaw = profileResponse.profile
return { return {
static: profileRaw?.static ?? [], static: response.profile.static ?? [],
dynamic: profileRaw?.dynamic ?? [], dynamic: response.profile.dynamic ?? [],
searchResults: normalizeSearchResults(profileResponse.searchResults), searchResults: response.searchResults?.results ?? [],
} }
} }
export async function fetchContainerContext( export async function fetchNamespaceContext(
containerTag: string, namespace: string,
query?: string, query?: string,
supermemoryApiKey?: string, supermemoryApiKey?: string,
): Promise<ContainerContext> { ): Promise<NamespaceContext> {
const apiKey = const apiKey =
supermemoryApiKey?.trim() || process.env.SUPERMEMORY_API_KEY?.trim() supermemoryApiKey?.trim() || process.env.SUPERMEMORY_API_KEY?.trim()
if (!apiKey) throw new Error("Supermemory API key is required") if (!apiKey) throw new Error("Supermemory API key is required")
const client = getSupermemoryClient(apiKey) const client = getSupermemoryClient(apiKey)
const profile = await fetchProfileContext(client, containerTag, query) const [profile, listResponse] = await Promise.all([
fetchProfileContext(apiKey, namespace, query),
const docsResponse = await client.post<{ client.list(namespace, "documents", {
documents?: unknown[] limit: DOCUMENT_LIST_LIMIT,
pagination?: unknown
}>("/v3/documents/documents", {
body: {
containerTags: [containerTag],
limit: 25,
sort: "createdAt", sort: "createdAt",
order: "desc", order: "desc",
}, }),
}) ])
const rawDocuments = docsResponse.documents ?? [] // v5 lists omit memories, so each document is fetched with include=memories.
const documents = rawDocuments.map((doc) => { const documents = await Promise.all(
const record = doc as Record<string, unknown> listResponse.documents.map(async (doc) => {
return { const memories = await client.documents
id: record.id as string | undefined, .get(namespace, doc.id, { include: ["memories"] })
title: record.title as string | undefined, .then((full) => full.memories ?? [])
status: record.status as string | undefined, .catch(() => [])
customId: record.customId as string | undefined, return {
createdAt: record.createdAt as string | undefined, id: doc.id,
updatedAt: record.updatedAt as string | undefined, title: doc.title ?? undefined,
summary: record.summary as string | undefined, status: doc.system.status,
memoryEntries: normalizeMemoryEntries(record), createdAt: doc.system.createdAt,
} updatedAt: doc.system.updatedAt,
}) summary: doc.summary ?? undefined,
memoryEntries: memories,
}
}),
)
return { return {
containerTag, namespace,
query, query,
profile, profile,
documents, documents,
pagination: docsResponse.pagination, pagination: listResponse.pagination,
} }
} }
export async function buildMiddlewareMemoryDebug( export async function buildMiddlewareMemoryDebug(
containerTag: string, namespace: string,
conversationId: string, conversationId: string,
memoryMode: MemoryMode, memoryMode: MemoryMode,
lastUserMessage: string, lastUserMessage: string,
@ -209,12 +184,7 @@ export async function buildMiddlewareMemoryDebug(
const apiKey = const apiKey =
supermemoryApiKey?.trim() || process.env.SUPERMEMORY_API_KEY?.trim() supermemoryApiKey?.trim() || process.env.SUPERMEMORY_API_KEY?.trim()
if (!apiKey) throw new Error("Supermemory API key is required") if (!apiKey) throw new Error("Supermemory API key is required")
const profile = await fetchProfileContext( const profile = await fetchProfileContext(apiKey, namespace, query, signal)
getSupermemoryClient(apiKey),
containerTag,
query,
signal,
)
const reconstructed = reconstructSdkMemoryBlock( const reconstructed = reconstructSdkMemoryBlock(
memoryMode, memoryMode,
profile, profile,
@ -230,9 +200,10 @@ export async function buildMiddlewareMemoryDebug(
detail: { detail: {
authoritativeMiddlewareCapture: false, authoritativeMiddlewareCapture: false,
timing: "after model response", timing: "after model response",
endpoint: "POST /v4/profile", endpoint:
containerTag, "POST /ns/{namespace}/profile + POST /ns/{namespace}/search",
customId: conversationId, namespace,
id: conversationId,
memoryMode, memoryMode,
addMemory: config.addMemory, addMemory: config.addMemory,
verbose: config.verbose, verbose: config.verbose,
@ -264,8 +235,8 @@ export async function buildMiddlewareMemoryDebug(
label: "Conversation save requested by middleware", label: "Conversation save requested by middleware",
detail: { detail: {
confirmed: false, confirmed: false,
containerTag, namespace,
customId: conversationId, id: conversationId,
addMemory: config.addMemory, addMemory: config.addMemory,
verbose: config.verbose, verbose: config.verbose,
...(sdk.includeToolCalls !== undefined ...(sdk.includeToolCalls !== undefined

View file

@ -8,9 +8,9 @@ const MAX_MESSAGE_LENGTH = 20_000
const MAX_TOTAL_MESSAGE_LENGTH = 100_000 const MAX_TOTAL_MESSAGE_LENGTH = 100_000
const MAX_IDENTIFIER_LENGTH = 256 const MAX_IDENTIFIER_LENGTH = 256
const MAX_API_KEY_LENGTH = 1_024 const MAX_API_KEY_LENGTH = 1_024
const MAX_CONTAINER_TAG_LENGTH = 100 const MAX_NAMESPACE_LENGTH = 100
const MAX_CONVERSATION_ID_LENGTH = 242 const MAX_CONVERSATION_ID_LENGTH = 242
const CONTAINER_TAG_PATTERN = /^[a-zA-Z0-9_:-]+$/ const NAMESPACE_PATTERN = /^[a-zA-Z0-9_:-]+$/
export class PlaygroundRequestError extends Error { export class PlaygroundRequestError extends Error {
constructor( constructor(
@ -224,24 +224,24 @@ export function parseIdentifier(
return resolved return resolved
} }
export function parseContainerTag( export function parseNamespace(
value: unknown, value: unknown,
fallback = "sdk-playground", fallback = "sdk-playground",
): string { ): string {
const containerTag = parseIdentifier(value, "containerTag", fallback) const namespace = parseIdentifier(value, "namespace", fallback)
if (containerTag.length > MAX_CONTAINER_TAG_LENGTH) { if (namespace.length > MAX_NAMESPACE_LENGTH) {
throw new PlaygroundRequestError( throw new PlaygroundRequestError(
`containerTag must be ${MAX_CONTAINER_TAG_LENGTH} characters or fewer`, `namespace must be ${MAX_NAMESPACE_LENGTH} characters or fewer`,
400, 400,
) )
} }
if (!CONTAINER_TAG_PATTERN.test(containerTag)) { if (!NAMESPACE_PATTERN.test(namespace)) {
throw new PlaygroundRequestError( throw new PlaygroundRequestError(
"containerTag may only contain letters, numbers, hyphens, underscores, and colons", "namespace may only contain letters, numbers, hyphens, underscores, and colons",
400, 400,
) )
} }
return containerTag return namespace
} }
export function parseConversationId(value: unknown): string { export function parseConversationId(value: unknown): string {

View file

@ -58,15 +58,6 @@ export const CHAT_SDK_REGISTRY: ChatSdkDefinition[] = [
description: "OpenAI function calling with the 7 Supermemory tools", description: "OpenAI function calling with the 7 Supermemory tools",
available: true, available: true,
}, },
{
id: "ts-ai-sdk-package",
label: "@supermemory/ai-sdk",
language: "typescript",
mode: "tools",
package: "@supermemory/ai-sdk",
description: "Re-export of tools/ai-sdk — same 7-tool agent",
available: true,
},
{ {
id: "py-openai-middleware", id: "py-openai-middleware",
label: "OpenAI + middleware", label: "OpenAI + middleware",

View file

@ -1,7 +1,7 @@
import { import {
PARAMETER_DESCRIPTIONS, PARAMETER_DESCRIPTIONS,
TOOL_DESCRIPTIONS, TOOL_DESCRIPTIONS,
} from "../../../../packages/tools/src/tools-shared" } from "../../../../packages/tools/src/tools-descriptions"
export interface CatalogParameter { export interface CatalogParameter {
name: string name: string
@ -59,10 +59,6 @@ export const TOOL_CATALOG: CatalogTool[] = [
pythonName: "get_profile", pythonName: "get_profile",
description: TOOL_DESCRIPTIONS.getProfile, description: TOOL_DESCRIPTIONS.getProfile,
parameters: [ parameters: [
{
name: "containerTag",
description: PARAMETER_DESCRIPTIONS.containerTag,
},
{ {
name: "query", name: "query",
pythonName: "query", pythonName: "query",
@ -75,10 +71,6 @@ export const TOOL_CATALOG: CatalogTool[] = [
pythonName: "document_list", pythonName: "document_list",
description: TOOL_DESCRIPTIONS.documentList, description: TOOL_DESCRIPTIONS.documentList,
parameters: [ parameters: [
{
name: "containerTag",
description: PARAMETER_DESCRIPTIONS.containerTag,
},
{ {
name: "limit", name: "limit",
pythonName: "limit", pythonName: "limit",
@ -102,10 +94,6 @@ export const TOOL_CATALOG: CatalogTool[] = [
description: PARAMETER_DESCRIPTIONS.documentId, description: PARAMETER_DESCRIPTIONS.documentId,
required: true, required: true,
}, },
{
name: "containerTag",
description: PARAMETER_DESCRIPTIONS.documentContainerTag,
},
], ],
}, },
{ {
@ -136,10 +124,6 @@ export const TOOL_CATALOG: CatalogTool[] = [
pythonName: "memory_forget", pythonName: "memory_forget",
description: TOOL_DESCRIPTIONS.memoryForget, description: TOOL_DESCRIPTIONS.memoryForget,
parameters: [ parameters: [
{
name: "containerTag",
description: PARAMETER_DESCRIPTIONS.containerTag,
},
{ {
name: "memoryId", name: "memoryId",
pythonName: "memory_id", pythonName: "memory_id",
@ -150,11 +134,6 @@ export const TOOL_CATALOG: CatalogTool[] = [
pythonName: "memory_content", pythonName: "memory_content",
description: PARAMETER_DESCRIPTIONS.memoryContent, description: PARAMETER_DESCRIPTIONS.memoryContent,
}, },
{
name: "reason",
pythonName: "reason",
description: PARAMETER_DESCRIPTIONS.reason,
},
], ],
}, },
] ]

115
bun.lock
View file

@ -126,14 +126,13 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@ai-sdk/openai": "^2.0.22", "@ai-sdk/openai": "^2.0.22",
"@supermemory/ai-sdk": "workspace:*",
"@supermemory/tools": "workspace:*", "@supermemory/tools": "workspace:*",
"ai": "^5.0.113", "ai": "^5.0.113",
"next": "^16.3.3", "next": "^16.3.3",
"openai": "^4.104.0", "openai": "^4.104.0",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"supermemory": "^4.25.4", "supermemory": "^5.0.0",
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
@ -164,24 +163,6 @@
"wrangler": "^4.125.0", "wrangler": "^4.125.0",
}, },
}, },
"packages/ai-sdk": {
"name": "@supermemory/ai-sdk",
"version": "2.0.0",
"dependencies": {
"@ai-sdk/openai": "^2.0.22",
"@ai-sdk/provider": "^2.0.0",
"@supermemory/tools": "^2.2.0",
"ai": "^5.0.113",
"supermemory": "^4.25.4",
},
"devDependencies": {
"@total-typescript/tsconfig": "^1.0.4",
"@types/bun": "^1.2.21",
"tsdown": "^0.14.2",
"typescript": "^5.9.2",
"vitest": "3.2.6",
},
},
"packages/docs-test": { "packages/docs-test": {
"name": "docs-test", "name": "docs-test",
"version": "1.0.0", "version": "1.0.0",
@ -246,14 +227,14 @@
}, },
"packages/tools": { "packages/tools": {
"name": "@supermemory/tools", "name": "@supermemory/tools",
"version": "2.3.0", "version": "3.0.0",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "^2.0.25", "@ai-sdk/anthropic": "^2.0.25",
"@ai-sdk/openai": "^2.0.23", "@ai-sdk/openai": "^2.0.23",
"ai": "^5.0.29", "ai": "^5.0.29",
"lru-cache": "^11.2.6", "lru-cache": "^11.2.6",
"openai": "^4.104.0", "openai": "^4.104.0",
"supermemory": "^4.25.4", "supermemory": "^5.0.0",
"zod": "^4.1.5", "zod": "^4.1.5",
}, },
"devDependencies": { "devDependencies": {
@ -378,7 +359,7 @@
"@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.25", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-erDa0VXNX9MlOepZLeJXpLsZeM5Sc6rBalPnEsXV8WnOBhYkN+lAUpa278KdE5rxaalthQFOG0C6B3I4crFR/Q=="], "@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.25", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-erDa0VXNX9MlOepZLeJXpLsZeM5Sc6rBalPnEsXV8WnOBhYkN+lAUpa278KdE5rxaalthQFOG0C6B3I4crFR/Q=="],
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
@ -1526,8 +1507,6 @@
"@stoplight/yaml-ast-parser": ["@stoplight/yaml-ast-parser@0.0.50", "", {}, "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ=="], "@stoplight/yaml-ast-parser": ["@stoplight/yaml-ast-parser@0.0.50", "", {}, "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ=="],
"@supermemory/ai-sdk": ["@supermemory/ai-sdk@workspace:packages/ai-sdk"],
"@supermemory/memory-graph": ["@supermemory/memory-graph@workspace:packages/memory-graph"], "@supermemory/memory-graph": ["@supermemory/memory-graph@workspace:packages/memory-graph"],
"@supermemory/tools": ["@supermemory/tools@workspace:packages/tools"], "@supermemory/tools": ["@supermemory/tools@workspace:packages/tools"],
@ -2668,7 +2647,7 @@
"js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="], "js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="],
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="],
@ -3604,7 +3583,7 @@
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
@ -3888,28 +3867,26 @@
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.70", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W3WjQlb0Ho+CVAQUvb8Rtk3hGS3Jlgy79ihY2H0yj2k4yU8XuxpQw0Oz+7JQsB47j+jlHhk7nUXtxhAeRg3S3Q=="], "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.70", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W3WjQlb0Ho+CVAQUvb8Rtk3hGS3Jlgy79ihY2H0yj2k4yU8XuxpQw0Oz+7JQsB47j+jlHhk7nUXtxhAeRg3S3Q=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@3.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bRsb2sDN5u+pKO3Kdr0flpxtL+cPwQ2uCo/pVyzIbj2I4AkKAokJHhw5JWLVOeEwdlYzWfmv+hzaiGarzUcTFQ=="], "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@3.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bRsb2sDN5u+pKO3Kdr0flpxtL+cPwQ2uCo/pVyzIbj2I4AkKAokJHhw5JWLVOeEwdlYzWfmv+hzaiGarzUcTFQ=="],
"@ai-sdk/azure/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@ai-sdk/cerebras/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "@ai-sdk/cerebras/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"@ai-sdk/cohere/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
"@ai-sdk/deepinfra/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
"@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
@ -3918,26 +3895,28 @@
"@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RUpgkG5dWsmkYQsluTdutXakFpyQQ1NvELnQ0KD1VWTNLHWD70fO0FOpOs1cQKeTe7PspcJSii9Zekpaepv6qA=="], "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RUpgkG5dWsmkYQsluTdutXakFpyQQ1NvELnQ0KD1VWTNLHWD70fO0FOpOs1cQKeTe7PspcJSii9Zekpaepv6qA=="],
"@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
"@ai-sdk/groq/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "@ai-sdk/groq/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"@ai-sdk/mistral/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
"@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
"@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"@ai-sdk/perplexity/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"@ai-sdk/provider-utils-v5/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
"@ai-sdk/provider-utils-v6/@ai-sdk/provider": ["@ai-sdk/provider@3.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-m9ka3ptkPQbaHHZHqDXDF9C9B5/Mav0KTdky1k2HZ3/nrW2t1AgObxIVPyGDWQNS9FXT/FS6PIoSjpcP/No8rQ=="], "@ai-sdk/provider-utils-v6/@ai-sdk/provider": ["@ai-sdk/provider@3.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-m9ka3ptkPQbaHHZHqDXDF9C9B5/Mav0KTdky1k2HZ3/nrW2t1AgObxIVPyGDWQNS9FXT/FS6PIoSjpcP/No8rQ=="],
"@ai-sdk/react/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], "@ai-sdk/react/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="],
@ -3946,8 +3925,6 @@
"@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], "@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
"@ai-sdk/togetherai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@ai-sdk/ui-utils/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "@ai-sdk/ui-utils/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
@ -3956,14 +3933,10 @@
"@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], "@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
"@ai-sdk/vercel/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-g3wA57IAQFb+3j4YuFndgkUdXyRETZVvbfAWM+UX7bZSxA3xjes0v3XKgIdKdekPtDGsh4ZX2byHD0gJIMPfiA=="], "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-g3wA57IAQFb+3j4YuFndgkUdXyRETZVvbfAWM+UX7bZSxA3xjes0v3XKgIdKdekPtDGsh4ZX2byHD0gJIMPfiA=="],
"@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
"@aihubmix/ai-sdk-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q=="], "@aihubmix/ai-sdk-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q=="],
@ -3972,8 +3945,6 @@
"@aihubmix/ai-sdk-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bRsb2sDN5u+pKO3Kdr0flpxtL+cPwQ2uCo/pVyzIbj2I4AkKAokJHhw5JWLVOeEwdlYzWfmv+hzaiGarzUcTFQ=="], "@aihubmix/ai-sdk-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bRsb2sDN5u+pKO3Kdr0flpxtL+cPwQ2uCo/pVyzIbj2I4AkKAokJHhw5JWLVOeEwdlYzWfmv+hzaiGarzUcTFQ=="],
"@aihubmix/ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@aihubmix/ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@aihubmix/ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@alcalzone/ansi-tokenize/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], "@alcalzone/ansi-tokenize/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
@ -3994,8 +3965,6 @@
"@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], "@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="],
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
@ -4184,8 +4153,6 @@
"@modelcontextprotocol/server/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@modelcontextprotocol/server/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@mymediset/sap-ai-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@mymediset/sap-ai-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@mymediset/sap-ai-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@mymediset/sap-ai-provider/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "@mymediset/sap-ai-provider/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
@ -4318,19 +4285,13 @@
"@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], "@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="],
"@supermemory/ai-sdk/supermemory": ["supermemory@4.25.4", "", { "bin": { "supermemory": "bin/cli" } }, "sha512-97ME3rlmu7OmsXJTb9OgXOD+3VUv4Wej0ZX9xezG+LKkMwrzi4xeeAZaOJFcr0oI/QQjcHG2WOzm+und1e7MFA=="],
"@supermemory/ai-sdk/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@supermemory/memory-graph/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "@supermemory/memory-graph/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@supermemory/tools/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.70", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W3WjQlb0Ho+CVAQUvb8Rtk3hGS3Jlgy79ihY2H0yj2k4yU8XuxpQw0Oz+7JQsB47j+jlHhk7nUXtxhAeRg3S3Q=="], "@supermemory/tools/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.70", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W3WjQlb0Ho+CVAQUvb8Rtk3hGS3Jlgy79ihY2H0yj2k4yU8XuxpQw0Oz+7JQsB47j+jlHhk7nUXtxhAeRg3S3Q=="],
"@supermemory/tools/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@supermemory/tools/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.65.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw=="], "@supermemory/tools/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.65.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw=="],
"@supermemory/tools/supermemory": ["supermemory@4.25.4", "", { "bin": { "supermemory": "bin/cli" } }, "sha512-97ME3rlmu7OmsXJTb9OgXOD+3VUv4Wej0ZX9xezG+LKkMwrzi4xeeAZaOJFcr0oI/QQjcHG2WOzm+und1e7MFA=="], "@supermemory/tools/supermemory": ["supermemory@5.0.0", "", { "bin": { "supermemory": "bin/cli.mjs" } }, "sha512-qnnGcJ1UHFt8Df/j8U4c1SooMV2axkg03jhrTIDOPnjma32aeXxaao+Z5Q/U7EyorNN7Vn82PY3Rd6lGInEgAA=="],
"@supermemory/tools/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "@supermemory/tools/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@ -4390,6 +4351,8 @@
"aggregate-error/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], "aggregate-error/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
"ai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
"ai-gateway-provider/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "ai-gateway-provider/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
@ -4524,8 +4487,6 @@
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"memory-graph-playground/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="], "memory-graph-playground/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
"memory-graph-playground/react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], "memory-graph-playground/react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
@ -4546,6 +4507,8 @@
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"ollama-ai-provider-v2/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
"ollama-ai-provider-v2/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], "ollama-ai-provider-v2/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
"openai/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "openai/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
@ -4604,7 +4567,7 @@
"sdk-playground/react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], "sdk-playground/react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
"sdk-playground/supermemory": ["supermemory@4.25.4", "", { "bin": { "supermemory": "bin/cli" } }, "sha512-97ME3rlmu7OmsXJTb9OgXOD+3VUv4Wej0ZX9xezG+LKkMwrzi4xeeAZaOJFcr0oI/QQjcHG2WOzm+und1e7MFA=="], "sdk-playground/supermemory": ["supermemory@5.0.0", "", { "bin": { "supermemory": "bin/cli.mjs" } }, "sha512-qnnGcJ1UHFt8Df/j8U4c1SooMV2axkg03jhrTIDOPnjma32aeXxaao+Z5Q/U7EyorNN7Vn82PY3Rd6lGInEgAA=="],
"sdk-playground/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "sdk-playground/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@ -4632,6 +4595,8 @@
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"style-to-js/style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], "style-to-js/style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
@ -4666,6 +4631,8 @@
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"tsdown/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"tsx/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "tsx/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], "verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
@ -4674,8 +4641,6 @@
"vite-node/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "vite-node/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
"vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], "which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"winston/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "winston/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
@ -4684,8 +4649,6 @@
"winston-transport/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "winston-transport/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"workers-ai-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@ -4724,12 +4687,8 @@
"@a2a-js/sdk/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], "@a2a-js/sdk/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
"@ai-sdk/react/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/react/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], "@ai-sdk/react/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="],
"@ai-sdk/react/ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@aihubmix/ai-sdk-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], "@aihubmix/ai-sdk-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
"@aihubmix/ai-sdk-provider/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], "@aihubmix/ai-sdk-provider/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
@ -4878,8 +4837,6 @@
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
"@gitlab/gitlab-ai-provider/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@ -5074,34 +5031,18 @@
"@supermemory/tools/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], "@supermemory/tools/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
"@voltagent/core/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@voltagent/core/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], "@voltagent/core/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
"@voltagent/core/@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], "@voltagent/core/@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
"@voltagent/core/@ai-sdk/cerebras/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@voltagent/core/@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@voltagent/core/@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], "@voltagent/core/@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
"@voltagent/core/@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@voltagent/core/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], "@voltagent/core/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
"@voltagent/core/@ai-sdk/groq/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@voltagent/core/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@voltagent/core/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], "@voltagent/core/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
"@voltagent/core/@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@voltagent/core/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], "@voltagent/core/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
"@voltagent/core/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@voltagent/core/@modelcontextprotocol/sdk/jose": ["jose@6.2.0", "", {}, "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ=="], "@voltagent/core/@modelcontextprotocol/sdk/jose": ["jose@6.2.0", "", {}, "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ=="],
"@voltagent/core/@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "@voltagent/core/@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
@ -5194,8 +5135,6 @@
"concurrently/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "concurrently/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"docs-test/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"docs-test/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], "docs-test/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
"docs-test/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@4.0.21", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-UpbC9C1oht8dhfKPbVXSLRZS3DI8uk8n5v2uMjBPNIYGsC2kL045ywq7D8Hh9KgnH9rP5W/GxQdYtWScRouqlA=="], "docs-test/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@4.0.21", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-UpbC9C1oht8dhfKPbVXSLRZS3DI8uk8n5v2uMjBPNIYGsC2kL045ywq7D8Hh9KgnH9rP5W/GxQdYtWScRouqlA=="],
@ -5240,8 +5179,6 @@
"next-mdx-remote-client/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], "next-mdx-remote-client/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"next-mdx-remote-client/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"public-ip/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], "public-ip/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="],

View file

@ -1,5 +0,0 @@
src/
.turbo/
.env
tsdown.config.ts
tsconfig.json

View file

@ -1,318 +0,0 @@
# supermemory AI SDK Utilities
Vercel AI SDK utilities for supermemory
## Installation
```bash
npm install @supermemory/ai-sdk
# or
bun add @supermemory/ai-sdk
# or
pnpm add @supermemory/ai-sdk
# or
yarn add @supermemory/ai-sdk
```
## Features
Choose **one** of the following approaches (they cannot be used together):
- **Infinite Chat Provider**: Connect to various LLM providers with unlimited context support
- **Memory Tools**: Search, add, inspect, and manage Supermemory data using AI agents
## Infinite Chat Provider
The infinite chat provider allows you to connect to various LLM providers with supermemory's context management.
```typescript
import { generateText } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
// Using a custom provider URL
const supermemoryOpenai = createOpenAI({
baseUrl: 'https://api.supermemory.ai/v3/https://api.openai.com/v1',
apiKey: 'your-provider-api-key',
headers: {
'x-supermemory-api-key': 'supermemory-api-key',
'x-sm-conversation-id': 'conversation-id'
}
})
const result = await generateText({
model: supermemoryOpenai('gpt-5'),
messages: [
{ role: 'user', content: 'Hello, how are you?' }
]
})
```
### Complete Infinite Chat Example
```typescript
import { generateText } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
const supermemoryApiKey = process.env.SUPERMEMORY_API_KEY!
const openaiApiKey = process.env.OPENAI_API_KEY!
// Initialize infinite chat provider
const supermemoryOpenai = createOpenAI({
baseUrl: 'https://api.supermemory.ai/v3/https://api.openai.com/v1',
apiKey: 'your-provider-api-key',
headers: {
'x-supermemory-api-key': 'supermemory-api-key',
'x-sm-conversation-id': 'conversation-id'
}
})
async function chat(userMessage: string) {
const result = await generateText({
model: supermemoryOpenai('gpt-5'),
messages: [
{
role: 'system',
content: 'You are a helpful assistant with unlimited context.'
},
{
role: 'user',
content: userMessage
}
]
// No tools - infinite chat handles context automatically
})
return result.text
}
```
### Configuration
```typescript
// Option 1: Use a named provider
interface ConfigWithProviderName {
providerName: 'openai' | 'anthropic' | 'openrouter' | 'deepinfra' | 'groq' | 'google' | 'cloudflare'
providerApiKey: string
headers?: Record<string, string>
}
// Option 2: Use a custom provider URL
interface ConfigWithProviderUrl {
providerUrl: string
providerApiKey: string
headers?: Record<string, string>
}
```
## Memory Tools
Supermemory tools allow AI agents to search, add, inspect, and manage scoped Supermemory data.
```typescript
import { supermemoryTools } from '@supermemory/ai-sdk'
import { generateText, stepCountIs } from 'ai'
import { openai } from '@ai-sdk/openai'
const result = await generateText({
model: openai('gpt-5'),
messages: [
{ role: 'user', content: 'What do you remember about my preferences?' }
],
tools: {
...supermemoryTools('your-supermemory-api-key', {
// Use either projectId OR containerTags, not both.
containerTags: ['user-123']
})
},
stopWhen: stepCountIs(5)
})
```
> **Important:** `supermemoryTools()` includes destructive operations: `documentDelete` permanently deletes a source document, while `memoryForget` soft-forgets an extracted profile memory. Do not expose the complete aggregate to an agent unless it should be allowed to perform those operations.
### Complete Memory Tools Example
```typescript
import { supermemoryTools } from '@supermemory/ai-sdk'
import { generateText, stepCountIs } from 'ai'
import { openai } from '@ai-sdk/openai'
const supermemoryApiKey = process.env.SUPERMEMORY_API_KEY!
async function chatWithTools(userMessage: string) {
const result = await generateText({
model: openai('gpt-5'), // Use standard provider
messages: [
{
role: 'system',
content: 'You are a helpful assistant with access to user memories.'
},
{
role: 'user',
content: userMessage
}
],
tools: {
...supermemoryTools(supermemoryApiKey, {
containerTags: ['my-user-id']
})
},
stopWhen: stepCountIs(5)
})
return result.text
}
```
### Configuration
```typescript
interface SupermemoryToolsConfig {
// Optional API base URL (default: https://api.supermemory.ai)
baseUrl?: string
// One or more non-empty scope tags (cannot be used with projectId)
containerTags?: string[]
// Converted to sm_project_<projectId> (cannot be used with containerTags)
projectId?: string
// Enable the package's stricter provider-compatible input schemas
// (default: false)
strict?: boolean
}
```
`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.
### Migrating from v1
Version 1 returned only `searchMemories` and `addMemory` from `supermemoryTools()`. Version 2 returns all seven tools listed below, including deletion and forgetting, so review any code that spreads the aggregate directly into an agent.
Version 1 also left `containerTags` undefined when no scope was configured. Version 2 sends `['sm_project_default']` instead. Before upgrading, choose an explicit `projectId` or `containerTags`, or migrate data that should live in the new default scope.
### Self-Hosted supermemory
If you're running a self-hosted supermemory instance:
```typescript
const tools = supermemoryTools('your-api-key', {
baseUrl: 'https://your-supermemory-instance.com',
containerTags: ['production', 'user-memories']
})
```
### Available Tools
| Aggregate key | Individual creator | Purpose |
| --- | --- | --- |
| `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 |
| `documentDelete` | `documentDeleteTool` | Permanently delete a source and soft-forget its extracted memories |
| `documentAdd` | `documentAddTool` | Ingest a source document for asynchronous processing |
| `memoryForget` | `memoryForgetTool` | Soft-forget one extracted profile memory |
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,
getProfileTool,
documentListTool,
documentDeleteTool,
documentAddTool,
memoryForgetTool
} from '@supermemory/ai-sdk'
const searchTool = searchMemoriesTool('your-api-key', {
projectId: 'your-project-id'
})
// Use only the search tool
const result = await generateText({
model: openai('gpt-5'),
messages: [...],
tools: {
searchMemories: searchTool
},
stopWhen: stepCountIs(5)
})
```
To expose a non-destructive subset, create the aggregate once and select only the tools the agent needs:
```typescript
const allTools = supermemoryTools('your-api-key', {
containerTags: ['user-123']
})
const safeTools = {
searchMemories: allTools.searchMemories,
addMemory: allTools.addMemory,
getProfile: allTools.getProfile,
documentList: allTools.documentList,
documentAdd: allTools.documentAdd
}
```
### Error Handling
All tool executions return a result object with a `success` field:
```typescript
const result = await tools.searchMemories.execute({
informationToGet: 'user preferences'
})
if (result.success) {
console.log('Found memories:', result.results)
console.log('Total count:', result.count)
} else {
console.error('Error searching memories:', result.error)
}
```
## Development
### Running Tests
```bash
# From the repository root
bun run --cwd packages/ai-sdk test:unit
# Or from packages/ai-sdk
bun run test:unit
```
#### Environment Variables for Tests
Local initialization and unit checks do not require API keys. Network integration checks run only when both of these are set; otherwise they are skipped:
- `SUPERMEMORY_API_KEY`: Supermemory API key
- `OPENAI_API_KEY`: OpenAI API key
**Optional:**
- `SUPERMEMORY_BASE_URL`: Custom Supermemory base URL
- `MODEL_NAME`: OpenAI model used by integration checks (defaults to `gpt-5-nano`)
## License
MIT
## Support
Email our [24/7 Founder/CEO/Support Executive](dhravya@supermemory.com)

View file

@ -1,47 +0,0 @@
{
"name": "@supermemory/ai-sdk",
"type": "module",
"version": "2.0.0",
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch --ignore-watch .turbo",
"check-types": "tsc --noEmit",
"test": "vitest",
"test:unit": "vitest run src/tools.test.ts",
"test:watch": "vitest --watch"
},
"dependencies": {
"@ai-sdk/openai": "^2.0.22",
"@ai-sdk/provider": "^2.0.0",
"@supermemory/tools": "^2.2.0",
"ai": "^5.0.113",
"supermemory": "^4.25.4"
},
"devDependencies": {
"@total-typescript/tsconfig": "^1.0.4",
"@types/bun": "^1.2.21",
"tsdown": "^0.14.2",
"typescript": "^5.9.2",
"vitest": "3.2.6"
},
"files": [
"dist"
],
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
},
"repository": {
"url": "https://github.com/supermemoryai/supermemory",
"directory": "packages/ai-sdk"
},
"keywords": [
"ai",
"sdk",
"supermemory",
"memory"
]
}

View file

@ -1 +0,0 @@
export * from "./tools"

View file

@ -1,169 +0,0 @@
import { createOpenAI } from "@ai-sdk/openai"
import { generateText } from "ai"
import { describe, expect, it } from "vitest"
import { type SupermemoryToolsConfig, supermemoryTools } from "./tools"
import "dotenv/config"
const hasIntegrationKeys = Boolean(
process.env.SUPERMEMORY_API_KEY && process.env.OPENAI_API_KEY,
)
const testApiKey = process.env.SUPERMEMORY_API_KEY ?? "test-api-key"
const testOpenAIKey = process.env.OPENAI_API_KEY ?? "test-openai-key"
const testBaseUrl = process.env.SUPERMEMORY_BASE_URL ?? undefined
const testModelName = process.env.MODEL_NAME || "gpt-5-nano"
describe("supermemoryTools", () => {
describe("client initialization", () => {
it("should create tools with default configuration", () => {
const config: SupermemoryToolsConfig = {}
const tools = supermemoryTools(testApiKey, config)
expect(tools).toBeDefined()
expect(tools.searchMemories).toBeDefined()
expect(tools.addMemory).toBeDefined()
expect(tools.getProfile).toBeDefined()
expect(tools.documentList).toBeDefined()
expect(tools.documentDelete).toBeDefined()
expect(tools.documentAdd).toBeDefined()
expect(tools.memoryForget).toBeDefined()
})
it("should create tools with custom baseUrl", () => {
const config: SupermemoryToolsConfig = {
baseUrl: testBaseUrl,
}
const tools = supermemoryTools(testApiKey, config)
expect(tools).toBeDefined()
expect(tools.searchMemories).toBeDefined()
expect(tools.addMemory).toBeDefined()
})
it("should create tools with projectId configuration", () => {
const config: SupermemoryToolsConfig = {
projectId: "test-project-123",
}
const tools = supermemoryTools(testApiKey, config)
expect(tools).toBeDefined()
expect(tools.searchMemories).toBeDefined()
expect(tools.addMemory).toBeDefined()
})
it("should create tools with custom container tags", () => {
const config: SupermemoryToolsConfig = {
containerTags: ["custom-tag-1", "custom-tag-2"],
}
const tools = supermemoryTools(testApiKey, config)
expect(tools).toBeDefined()
expect(tools.searchMemories).toBeDefined()
expect(tools.addMemory).toBeDefined()
})
})
describe.skipIf(!hasIntegrationKeys)("AI SDK integration", () => {
it("should work with AI SDK generateText", async () => {
const openai = createOpenAI({
apiKey: testOpenAIKey,
})
const result = await generateText({
model: openai(testModelName),
messages: [
{
role: "system",
content:
"You are a helpful assistant with access to user memories. Use the search tool when the user asks about preferences or past information.",
},
{
role: "user",
content: "What do you remember about my preferences?",
},
],
tools: {
...supermemoryTools(testApiKey, {
projectId: "test-ai-integration",
baseUrl: testBaseUrl,
}),
},
})
expect(result).toBeDefined()
expect(result.text).toBeDefined()
expect(typeof result.text).toBe("string")
})
it("should use tools when prompted", async () => {
const openai = createOpenAI({
apiKey: testOpenAIKey,
})
const tools = supermemoryTools(testApiKey, {
projectId: "test-tool-usage",
baseUrl: testBaseUrl,
})
const result = await generateText({
model: openai(testModelName),
messages: [
{
role: "system",
content:
"You are a helpful assistant. When the user asks you to remember something, use the addMemory tool.",
},
{
role: "user",
content: "Please remember that I prefer dark roast coffee",
},
],
tools: {
addMemory: tools.addMemory,
},
})
expect(result).toBeDefined()
expect(result.text).toBeDefined()
expect(result.toolCalls).toBeDefined()
if (result.toolCalls && result.toolCalls.length > 0) {
const addMemoryCall = result.toolCalls.find(
(call) => call.toolName === "addMemory",
)
expect(addMemoryCall).toBeDefined()
}
})
it("should handle multiple tool types", async () => {
const openai = createOpenAI({
apiKey: testOpenAIKey,
})
const result = await generateText({
model: openai(testModelName),
messages: [
{
role: "system",
content:
"You are a helpful assistant with access to user memories. Use search when asked about preferences, and addMemory when told to remember something.",
},
{
role: "user",
content:
"Search for my preferences and then remember that I also like green tea",
},
],
tools: {
...supermemoryTools(testApiKey, {
containerTags: ["test-multi-tools"],
}),
},
})
expect(result).toBeDefined()
expect(result.text).toBeDefined()
expect(typeof result.text).toBe("string")
})
})
})

View file

@ -1,17 +0,0 @@
/**
* Re-export the canonical Supermemory AI SDK tools from @supermemory/tools.
* Prefer @supermemory/tools for middleware, OpenAI, Mastra, and VoltAgent integrations.
*/
export {
supermemoryTools,
searchMemoriesTool,
addMemoryTool,
getProfileTool,
documentListTool,
documentDeleteTool,
documentAddTool,
memoryForgetTool,
getContainerTags,
} from "@supermemory/tools/ai-sdk"
export type { SupermemoryToolsConfig } from "@supermemory/tools"

View file

@ -1,12 +0,0 @@
{
"extends": "@total-typescript/tsconfig/bundler/dom/library-monorepo",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@lib/*": ["../lib/*"]
},
"declaration": true,
"declarationMap": true
}
}

View file

@ -1,16 +0,0 @@
import { defineConfig } from "tsdown"
export default defineConfig({
entry: ["src/index.ts"],
format: "esm",
sourcemap: true,
target: "es2020",
tsconfig: "./tsconfig.json",
clean: true,
hash: false,
minify: true,
dts: {
sourcemap: true,
},
exports: true,
})

View file

@ -1,8 +1,18 @@
# @supermemory/tools # @supermemory/tools
Memory tools for AI SDK, OpenAI, and Mastra with supermemory Memory tools and middleware for Vercel AI SDK, OpenAI, Mastra, VoltAgent and Claude, backed by the [Supermemory](https://supermemory.ai) v5 API.
This package provides supermemory tools for AI SDK, OpenAI, and Mastra through dedicated submodule exports, each with function-based architectures optimized for their respective use cases. Pick the entry point for your stack:
| Import | Use it for |
| --- | --- |
| `@supermemory/tools/ai-sdk` | Vercel AI SDK: 7 memory tools for `generateText`/`streamText`, plus `withSupermemory` model middleware |
| `@supermemory/tools/openai` | OpenAI SDK: function-calling tool definitions and executor, plus `withSupermemory` client middleware |
| `@supermemory/tools/mastra` | Mastra agents: input/output processors or the `withSupermemory` config wrapper |
| `@supermemory/tools/voltagent` | VoltAgent agents: `withSupermemory` config wrapper built on hooks |
| `@supermemory/tools/claude-memory` | Claude's native memory tool (`memory_20250818`), stored as Supermemory documents |
Every entry point reads and writes one **namespace**: the Supermemory v5 name for an isolated memory space, usually one per end user. Middleware also takes an **`id`** that groups a conversation's messages into one document.
## Installation ## Installation
@ -10,12 +20,25 @@ This package provides supermemory tools for AI SDK, OpenAI, and Mastra through d
npm install @supermemory/tools npm install @supermemory/tools
``` ```
Requires Node 18 or later and a `SUPERMEMORY_API_KEY` from [console.supermemory.ai](https://console.supermemory.ai). The `supermemory@5` SDK comes along as a dependency. Against a self-hosted server, pass `baseUrl` and run `supermemory-server` v0.0.9 or later, which is the first version that serves the v5 routes.
## Migrating from 2.x
3.0 moves every call to the Supermemory v5 API (`supermemory@5`). The full guide is at [supermemory.ai/docs/migration/tools-v3-upgrade](https://supermemory.ai/docs/migration/tools-v3-upgrade).
- **One namespace per config.** `containerTags` and `projectId` are gone. Pass `namespace: string`; every tool reads and writes only that namespace. To keep using data from `projectId: "x"`, pass `namespace: "sm_project_x"`. With no config, tools use `sm_project_default` as before.
- **v5 option names everywhere.** `withSupermemory` (AI SDK, OpenAI, Mastra, VoltAgent) takes `namespace` instead of `containerTag` and `id` instead of `customId`.
- **No per-call scope overrides.** `getProfile`, `documentList`, `documentDelete`, and `memoryForget` no longer accept a `containerTag` argument.
- **`memoryForget`** no longer takes `reason`. Forgetting by `memoryContent` previews matching memories and forgets only the ones whose text matches exactly.
- **`getProfile`** returns v5 profile entries (`{ id, memory }`), so profile IDs can be passed to `memoryForget`. With a `query`, `searchResults` is the v5 search `results` array.
- **`documentList`** returns v5 documents; status is under `system.status`.
- **Conversations** saved by the middlewares are stored as one document per conversation (document `id` = the `id` you pass), instead of through `/v4/conversations`.
- **Claude memory tool** drops `memoryContainerTag`. Its files live in the configured namespace and are marked with `metadata.source = "claude-memory"`. Files written by 2.x are not migrated.
- **VoltAgent** search options use v5 shapes: `filters` → typed `filter`, `rerank` is `"none" | "order" | "aggregate"`, `searchMode: "documents"` → `"chunks"`, `include` keys are `documents`, `related`, `forgotten`, and `entityContext` → `supportingContext`.
## Usage ## Usage
The package provides three submodule imports: Each section below covers one entry point: AI SDK, OpenAI, Mastra, VoltAgent, then the Claude memory tool. The `withSupermemory` middleware takes the same options everywhere; see [withSupermemory Middleware Options](#withsupermemory-middleware-options).
- `@supermemory/tools/ai-sdk` - For use with the AI SDK framework (includes `withSupermemory` middleware)
- `@supermemory/tools/openai` - For use with OpenAI SDK (includes `withSupermemory` middleware and function calling tools)
- `@supermemory/tools/mastra` - For use with Mastra AI agents (includes `withSupermemory` wrapper and processors)
### AI SDK Usage ### AI SDK Usage
@ -30,7 +53,7 @@ const openai = createOpenAI({
// Create all tools // Create all tools
const tools = supermemoryTools(process.env.SUPERMEMORY_API_KEY!, { const tools = supermemoryTools(process.env.SUPERMEMORY_API_KEY!, {
containerTags: ["your-user-id"], namespace: "your-user-id",
}) })
// Use with AI SDK // Use with AI SDK
@ -48,17 +71,17 @@ const result = await generateText({
// Or create individual tools // Or create individual tools
const searchTool = searchMemoriesTool(process.env.SUPERMEMORY_API_KEY!, { const searchTool = searchMemoriesTool(process.env.SUPERMEMORY_API_KEY!, {
projectId: "your-project-id", namespace: "your-user-id",
}) })
const addTool = addMemoryTool(process.env.SUPERMEMORY_API_KEY!, { const addTool = addMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
projectId: "your-project-id", namespace: "your-user-id",
}) })
``` ```
#### AI SDK Middleware with Supermemory #### AI SDK Middleware with Supermemory
- `withSupermemory` will take advantage supermemory profile v4 endpoint personalized based on container tag - `withSupermemory` injects the Supermemory v5 profile for the given `namespace`, plus a memories search on the latest user message in `query` and `full` modes
- You can provide the Supermemory API key via the `apiKey` option to `withSupermemory` (recommended for browser usage), or fall back to `SUPERMEMORY_API_KEY` in the environment for server usage. - You can provide the Supermemory API key via the `apiKey` option to `withSupermemory` (recommended for browser usage), or fall back to `SUPERMEMORY_API_KEY` in the environment for server usage.
- **Per-turn caching**: Memory injection is cached for tool-call continuations within the same user turn. The middleware detects when the AI SDK is continuing a multi-step flow (e.g., after a tool call) and reuses the cached memories instead of making redundant API calls. A fresh fetch occurs on each new user message turn. - **Per-turn caching**: Memory injection is cached for tool-call continuations within the same user turn. The middleware detects when the AI SDK is continuing a multi-step flow (e.g., after a tool call) and reuses the cached memories instead of making redundant API calls. A fresh fetch occurs on each new user message turn.
@ -68,8 +91,8 @@ import { withSupermemory } from "@supermemory/tools/ai-sdk"
import { openai } from "@ai-sdk/openai" import { openai } from "@ai-sdk/openai"
const modelWithMemory = withSupermemory(openai("gpt-5"), { const modelWithMemory = withSupermemory(openai("gpt-5"), {
containerTag: "user_id_life", namespace: "user_id_life",
customId: "conversation-456", id: "conversation-456",
}) })
const result = await generateText({ const result = await generateText({
@ -90,8 +113,8 @@ import { withSupermemory } from "@supermemory/tools/ai-sdk"
import { openai } from "@ai-sdk/openai" import { openai } from "@ai-sdk/openai"
const modelWithMemory = withSupermemory(openai("gpt-5"), { const modelWithMemory = withSupermemory(openai("gpt-5"), {
containerTag: "user_id_life", namespace: "user_id_life",
customId: "conversation-456", id: "conversation-456",
verbose: true, verbose: true,
}) })
@ -105,7 +128,7 @@ console.log(result.text)
When verbose mode is enabled, you'll see console output like: When verbose mode is enabled, you'll see console output like:
``` ```
[supermemory] Searching memories for container: user_id_life [supermemory] Searching memories for namespace: user_id_life
[supermemory] User message: where do i live? [supermemory] User message: where do i live?
[supermemory] System prompt exists: false [supermemory] System prompt exists: false
[supermemory] Found 3 memories [supermemory] Found 3 memories
@ -125,14 +148,14 @@ import { openai } from "@ai-sdk/openai"
// Uses profile mode by default - gets all user profile memories // Uses profile mode by default - gets all user profile memories
const modelWithMemory = withSupermemory(openai("gpt-4"), { const modelWithMemory = withSupermemory(openai("gpt-4"), {
containerTag: "user-123", namespace: "user-123",
customId: "conversation-456", id: "conversation-456",
}) })
// Explicitly specify profile mode // Explicitly specify profile mode
const modelWithProfile = withSupermemory(openai("gpt-4"), { const modelWithProfile = withSupermemory(openai("gpt-4"), {
containerTag: "user-123", namespace: "user-123",
customId: "conversation-456", id: "conversation-456",
mode: "profile", mode: "profile",
}) })
@ -149,8 +172,8 @@ import { withSupermemory } from "@supermemory/tools/ai-sdk"
import { openai } from "@ai-sdk/openai" import { openai } from "@ai-sdk/openai"
const modelWithQuery = withSupermemory(openai("gpt-4"), { const modelWithQuery = withSupermemory(openai("gpt-4"), {
containerTag: "user-123", namespace: "user-123",
customId: "conversation-456", id: "conversation-456",
mode: "query", mode: "query",
}) })
@ -167,8 +190,8 @@ import { withSupermemory } from "@supermemory/tools/ai-sdk"
import { openai } from "@ai-sdk/openai" import { openai } from "@ai-sdk/openai"
const modelWithFull = withSupermemory(openai("gpt-4"), { const modelWithFull = withSupermemory(openai("gpt-4"), {
containerTag: "user-123", namespace: "user-123",
customId: "conversation-456", id: "conversation-456",
mode: "full", mode: "full",
}) })
@ -189,8 +212,8 @@ import { withSupermemory } from "@supermemory/tools/ai-sdk"
import { openai } from "@ai-sdk/openai" import { openai } from "@ai-sdk/openai"
const modelWithAutoSave = withSupermemory(openai("gpt-4"), { const modelWithAutoSave = withSupermemory(openai("gpt-4"), {
containerTag: "user-123", namespace: "user-123",
customId: "conversation-456", id: "conversation-456",
addMemory: "always", addMemory: "always",
}) })
@ -204,8 +227,8 @@ const result = await generateText({
**Never Save Memories** - Only retrieves memories without storing new ones: **Never Save Memories** - Only retrieves memories without storing new ones:
```typescript ```typescript
const modelWithNoSave = withSupermemory(openai("gpt-4"), { const modelWithNoSave = withSupermemory(openai("gpt-4"), {
containerTag: "user-123", namespace: "user-123",
customId: "conversation-456", id: "conversation-456",
addMemory: "never", // explicit since default is now "always" addMemory: "never", // explicit since default is now "always"
}) })
``` ```
@ -213,8 +236,8 @@ const modelWithNoSave = withSupermemory(openai("gpt-4"), {
**Combined Options** - Use verbose logging with specific modes and memory storage: **Combined Options** - Use verbose logging with specific modes and memory storage:
```typescript ```typescript
const modelWithOptions = withSupermemory(openai("gpt-4"), { const modelWithOptions = withSupermemory(openai("gpt-4"), {
containerTag: "user-123", namespace: "user-123",
customId: "conversation-456", id: "conversation-456",
mode: "profile", mode: "profile",
addMemory: "always", addMemory: "always",
verbose: true, verbose: true,
@ -242,8 +265,8 @@ ${data.generalSearchMemories}
`.trim() `.trim()
const modelWithCustomPrompt = withSupermemory(openai("gpt-4"), { const modelWithCustomPrompt = withSupermemory(openai("gpt-4"), {
containerTag: "user-123", namespace: "user-123",
customId: "conversation-456", id: "conversation-456",
mode: "full", mode: "full",
promptTemplate: customPrompt, promptTemplate: customPrompt,
}) })
@ -270,8 +293,8 @@ import { withSupermemory } from "@supermemory/tools/openai"
// Create OpenAI client with supermemory middleware // Create OpenAI client with supermemory middleware
const openaiWithSupermemory = withSupermemory(openai, { const openaiWithSupermemory = withSupermemory(openai, {
containerTag: "user-123", // Required: identifies the user/container namespace: "user-123", // Required: identifies the user or project
customId: "conversation-456", // Required: groups messages into the same document id: "conversation-456", // Required: groups messages into the same document
apiKey: process.env.SUPERMEMORY_API_KEY, // Optional env fallback apiKey: process.env.SUPERMEMORY_API_KEY, // Optional env fallback
baseUrl: process.env.SUPERMEMORY_BASE_URL, baseUrl: process.env.SUPERMEMORY_BASE_URL,
mode: "full", mode: "full",
@ -296,8 +319,8 @@ The middleware supports the same configuration options as the AI SDK version:
```typescript ```typescript
const openaiWithSupermemory = withSupermemory(openai, { const openaiWithSupermemory = withSupermemory(openai, {
containerTag: "user-123", // Required: identifies the user/container namespace: "user-123", // Required: identifies the user or project
customId: "conversation-456", // Required: groups messages for contextual memory id: "conversation-456", // Required: groups messages for contextual memory
apiKey: process.env.SUPERMEMORY_API_KEY, // Optional; captured per client apiKey: process.env.SUPERMEMORY_API_KEY, // Optional; captured per client
baseUrl: process.env.SUPERMEMORY_BASE_URL, baseUrl: process.env.SUPERMEMORY_BASE_URL,
mode: "full", // "profile" | "query" | "full" mode: "full", // "profile" | "query" | "full"
@ -322,8 +345,8 @@ export async function POST(req: Request) {
} }
const openaiWithSupermemory = withSupermemory(openai, { const openaiWithSupermemory = withSupermemory(openai, {
containerTag: "user-123", namespace: "user-123",
customId: conversationId, id: conversationId,
apiKey: process.env.SUPERMEMORY_API_KEY, apiKey: process.env.SUPERMEMORY_API_KEY,
baseUrl: process.env.SUPERMEMORY_BASE_URL, baseUrl: process.env.SUPERMEMORY_BASE_URL,
mode: "full", mode: "full",
@ -356,7 +379,7 @@ const toolDefinitions = getToolDefinitions()
// Create tool executor // Create tool executor
const executeToolCall = createToolCallExecutor(process.env.SUPERMEMORY_API_KEY!, { const executeToolCall = createToolCallExecutor(process.env.SUPERMEMORY_API_KEY!, {
projectId: "your-project-id", namespace: "your-user-id",
}) })
// Use with OpenAI Chat Completions // Use with OpenAI Chat Completions
@ -381,7 +404,7 @@ if (completion.choices[0]?.message.tool_calls) {
// Or create individual function-based tools // Or create individual function-based tools
const tools = supermemoryTools(process.env.SUPERMEMORY_API_KEY!, { const tools = supermemoryTools(process.env.SUPERMEMORY_API_KEY!, {
containerTags: ["your-user-id"], namespace: "your-user-id",
}) })
const searchResult = await tools.searchMemories({ const searchResult = await tools.searchMemories({
@ -418,8 +441,8 @@ const agent = new Agent(withSupermemory(
instructions: "You are a helpful assistant.", instructions: "You are a helpful assistant.",
}, },
{ {
containerTag: "user-123", // Required: scopes memories to this user namespace: "user-123", // Required: scopes memories to this user
customId: "conv-456", // Required: groups messages for contextual memory id: "conv-456", // Required: groups messages for contextual memory
mode: "full", mode: "full",
} }
)) ))
@ -438,8 +461,8 @@ import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
import { openai } from "@ai-sdk/openai" import { openai } from "@ai-sdk/openai"
const { input, output } = createSupermemoryProcessors({ const { input, output } = createSupermemoryProcessors({
containerTag: "user-123", namespace: "user-123",
customId: "conv-456", id: "conv-456",
mode: "full", mode: "full",
verbose: true, // Enable logging verbose: true, // Enable logging
}) })
@ -467,11 +490,11 @@ import { openai } from "@ai-sdk/openai"
async function main() { async function main() {
const userId = "user-alex-123" const userId = "user-alex-123"
const customId = `thread-${Date.now()}` const id = `thread-${Date.now()}`
const { input, output } = createSupermemoryProcessors({ const { input, output } = createSupermemoryProcessors({
containerTag: userId, namespace: userId,
customId, id,
mode: "profile", // Fetch user profile memories mode: "profile", // Fetch user profile memories
verbose: true, verbose: true,
}) })
@ -509,22 +532,22 @@ main()
```typescript ```typescript
// Profile mode - good for general personalization // Profile mode - good for general personalization
const { input } = createSupermemoryProcessors({ const { input } = createSupermemoryProcessors({
containerTag: "user-123", namespace: "user-123",
customId: "conv-456", id: "conv-456",
mode: "profile", mode: "profile",
}) })
// Query mode - good for specific lookups // Query mode - good for specific lookups
const { input } = createSupermemoryProcessors({ const { input } = createSupermemoryProcessors({
containerTag: "user-123", namespace: "user-123",
customId: "conv-456", id: "conv-456",
mode: "query", mode: "query",
}) })
// Full mode - comprehensive context // Full mode - comprehensive context
const { input } = createSupermemoryProcessors({ const { input } = createSupermemoryProcessors({
containerTag: "user-123", namespace: "user-123",
customId: "conv-456", id: "conv-456",
mode: "full", mode: "full",
}) })
``` ```
@ -544,8 +567,8 @@ ${data.generalSearchMemories}
`.trim() `.trim()
const { input, output } = createSupermemoryProcessors({ const { input, output } = createSupermemoryProcessors({
containerTag: "user-123", namespace: "user-123",
customId: "conv-456", id: "conv-456",
mode: "full", mode: "full",
promptTemplate: customTemplate, promptTemplate: customTemplate,
}) })
@ -553,7 +576,7 @@ const { input, output } = createSupermemoryProcessors({
#### Using RequestContext for Dynamic Thread IDs #### Using RequestContext for Dynamic Thread IDs
For server setups where one agent instance handles multiple concurrent conversations, use Mastra's `RequestContext` to provide per-request thread IDs. **RequestContext takes precedence** over the construction-time `customId`: For server setups where one agent instance handles multiple concurrent conversations, use Mastra's `RequestContext` to provide per-request thread IDs. **RequestContext takes precedence** over the construction-time `id`:
```typescript ```typescript
import { Agent } from "@mastra/core/agent" import { Agent } from "@mastra/core/agent"
@ -561,8 +584,8 @@ import { RequestContext, MASTRA_THREAD_ID_KEY } from "@mastra/core/request-conte
import { createSupermemoryProcessors } from "@supermemory/tools/mastra" import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
const { input, output } = createSupermemoryProcessors({ const { input, output } = createSupermemoryProcessors({
containerTag: "user-123", namespace: "user-123",
customId: "fallback-conv", // Used only when RequestContext doesn't provide a threadId id: "fallback-conv", // Used only when RequestContext doesn't provide a threadId
mode: "profile", mode: "profile",
}) })
@ -574,7 +597,7 @@ const agent = new Agent({
outputProcessors: [output], outputProcessors: [output],
}) })
// Per-request threadId takes precedence over customId // Per-request threadId takes precedence over id
const ctx = new RequestContext() const ctx = new RequestContext()
ctx.set(MASTRA_THREAD_ID_KEY, "user-456-session-789") ctx.set(MASTRA_THREAD_ID_KEY, "user-456-session-789")
@ -582,14 +605,14 @@ const response = await agent.generate("Hello!", { requestContext: ctx })
// This conversation is stored under "user-456-session-789", not "fallback-conv" // This conversation is stored under "user-456-session-789", not "fallback-conv"
``` ```
> **Server-side usage**: Always use `RequestContext` to pass unique conversation IDs per request. Using a fixed `customId` for all requests will merge conversations from different users. > **Server-side usage**: Always use `RequestContext` to pass unique conversation IDs per request. Using a fixed `id` for all requests will merge conversations from different users.
#### Mastra Configuration Options #### Mastra Configuration Options
```typescript ```typescript
interface SupermemoryMastraOptions { interface SupermemoryMastraOptions {
containerTag: string // Required: User/container tag for scoping memories namespace: string // Required: Namespace (e.g. user ID) for scoping memories
customId: string // Required: Groups messages into a single document for contextual memory id: string // Required: Groups messages into a single document for contextual memory
apiKey?: string // Supermemory API key (or use SUPERMEMORY_API_KEY env var) apiKey?: string // Supermemory API key (or use SUPERMEMORY_API_KEY env var)
baseUrl?: string // Custom API endpoint baseUrl?: string // Custom API endpoint
mode?: "profile" | "query" | "full" // Memory search mode (default: "profile") mode?: "profile" | "query" | "full" // Memory search mode (default: "profile")
@ -599,22 +622,69 @@ interface SupermemoryMastraOptions {
} }
``` ```
### VoltAgent Usage
Add memory to a [VoltAgent](https://voltagent.dev) agent by wrapping its config. `withSupermemory` installs hooks that inject memories before each run and save the conversation after it.
```typescript
import { Agent } from "@voltagent/core"
import { withSupermemory } from "@supermemory/tools/voltagent"
import { openai } from "@ai-sdk/openai"
const agent = new Agent(
withSupermemory({
agentConfig: {
name: "my-agent",
instructions: "You are a helpful assistant.",
model: openai("gpt-4o"),
},
namespace: "user-123", // Required: the namespace to read and write
id: "conversation-456", // Required: groups this conversation into one document
mode: "full",
}),
)
const result = await agent.generateText("What's my favorite programming language?")
```
`@voltagent/core` is a peer dependency; install it yourself.
#### VoltAgent Options
On top of the shared middleware options (`mode`, `addMemory`, `verbose`, `apiKey`, `baseUrl`, `promptTemplate`), VoltAgent exposes the v5 search controls. They apply in `query` and `full` mode and are ignored in `profile` mode:
```typescript
withSupermemory({
agentConfig,
namespace: "user-123",
id: "conversation-456",
mode: "query",
searchMode: "hybrid", // "hybrid" (recommended) | "memories" | "chunks"
limit: 10, // 1-100
threshold: 0.3, // 0-1, higher is stricter
rerank: "order", // "none" (default) | "order" | "aggregate"
rewriteQuery: true, // adds ~400ms
filter: { field: "type", operator: "eq", value: "note" }, // typed v5 filter expression
include: { documents: true, related: false, forgotten: false },
metadata: { app: "support-bot" }, // stamped on saved conversations
supportingContext: "Support chats for Acme customers", // guides memory extraction, max 1500 chars
})
```
## Configuration ## Configuration
Both modules accept the same configuration interface: The tool entry points (`ai-sdk` and `openai`) accept the same configuration:
```typescript ```typescript
interface SupermemoryToolsConfig { interface SupermemoryToolsConfig {
baseUrl?: string baseUrl?: string
containerTags?: string[] namespace?: string
projectId?: string
strict?: boolean strict?: boolean
} }
``` ```
- **baseUrl**: Custom base URL for the supermemory API - **baseUrl**: Custom base URL for the supermemory API
- **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. - **namespace**: The one namespace every tool reads and writes (default: `sm_project_default`). `documentDelete` refuses a document that is still processing.
- **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. - **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.
### OpenAI Strict Mode Compatibility ### OpenAI Strict Mode Compatibility
@ -630,11 +700,11 @@ const openrouter = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY })
const tools = { const tools = {
searchMemories: searchMemoriesTool(apiKey, { searchMemories: searchMemoriesTool(apiKey, {
containerTags: [userId], namespace: userId,
strict: true // ✅ Required for OpenAI strict mode strict: true // ✅ Required for OpenAI strict mode
}), }),
addMemory: addMemoryTool(apiKey, { addMemory: addMemoryTool(apiKey, {
containerTags: [userId], namespace: userId,
strict: true strict: true
}), }),
} }
@ -654,8 +724,8 @@ The `withSupermemory` middleware accepts a configuration object as the second ar
```typescript ```typescript
interface WithSupermemoryOptions { interface WithSupermemoryOptions {
containerTag: string // Required: identifies the user/container namespace: string // Required: identifies the user or project
customId: string // Required: groups messages into the same document id: string // Required: groups messages into the same document
verbose?: boolean verbose?: boolean
mode?: "profile" | "query" | "full" mode?: "profile" | "query" | "full"
addMemory?: "always" | "never" // Default: "always" addMemory?: "always" | "never" // Default: "always"
@ -667,8 +737,8 @@ interface WithSupermemoryOptions {
} }
``` ```
- **containerTag**: Required. The container tag/identifier for memory search (e.g., user ID, project ID) - **namespace**: Required. The namespace to read and write (e.g., user ID, project ID)
- **customId**: Required. Custom ID to group messages into a single document for contextual memory generation - **id**: Required. ID that groups messages into a single document for contextual memory generation
- **verbose**: Enable detailed logging of memory search and injection process (default: false) - **verbose**: Enable detailed logging of memory search and injection process (default: false)
- **mode**: Memory search mode - "profile" (default), "query", or "full" - **mode**: Memory search mode - "profile" (default), "query", or "full"
- **addMemory**: Automatic memory storage mode - "always" (default) or "never" - **addMemory**: Automatic memory storage mode - "always" (default) or "never"
@ -677,19 +747,28 @@ interface WithSupermemoryOptions {
## Available Tools ## Available Tools
### Search Memories ### Search Memories
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. Runs v5 hybrid search in the configured namespace (threshold 0.6). 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:** **Parameters:**
- `informationToGet` (string): Terms to search for - `informationToGet` (string): Terms to search for
- `includeFullDocs` (boolean, optional): Deprecated compatibility input; ignored by v4 hybrid search - `includeFullDocs` (boolean, optional): Deprecated compatibility input; ignored by hybrid search
- `limit` (number, optional): Maximum number of results (default: 10) - `limit` (number, optional): Maximum number of results (default: 10)
### Add Memory ### Add Memory
Adds a new memory to the system. Adds a short fact as a document processed with `dreaming: "instant"`, so the memory is available quickly.
**Parameters:** **Parameters:**
- `memory` (string): The content to remember - `memory` (string): The content to remember
### Get Profile
Returns the v5 profile (`static`, `dynamic`, `buckets`; each entry is `{ id, memory }`). With `query`, also runs a memories search and returns its results as `searchResults`.
### Document List / Add / Delete
`documentList` pages through documents in the namespace. `documentAdd` stores raw content for processing. `documentDelete` takes a Supermemory ID or your own document ID and refuses while the document is still processing.
### Memory Forget
Forgets one memory by `memoryId`, or by `memoryContent`: a dry-run preview finds candidates, and only memories whose text matches exactly are forgotten.
## Claude Memory Tool ## Claude Memory Tool
@ -713,7 +792,7 @@ const anthropic = new Anthropic({
}) })
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, { const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
projectId: 'my-app', namespace: 'my-app',
}) })
async function chatWithMemory(userMessage: string) { async function chatWithMemory(userMessage: string) {
@ -758,12 +837,13 @@ Claude can perform these memory operations automatically:
- **`delete`** - Delete memory files - **`delete`** - Delete memory files
- **`rename`** - Rename or move memory files - **`rename`** - Rename or move memory files
All memory files are stored in supermemory with normalized paths and can be searched and retrieved across conversations. All memory files are stored as documents in the configured namespace, keyed by their normalized path and marked with `metadata.source = "claude-memory"`.
## Environment Variables ## Environment Variables
```env ```env
SUPERMEMORY_API_KEY=your_supermemory_api_key SUPERMEMORY_API_KEY=your_supermemory_api_key # read by every entry point when apiKey is not passed
ANTHROPIC_API_KEY=your_anthropic_api_key # for Claude Memory Tool ANTHROPIC_API_KEY=your_anthropic_api_key # for the Claude memory tool
SUPERMEMORY_BASE_URL=https://your-custom-url # optional
``` ```
A custom API URL (self-hosted or enterprise) is not read from the environment. Pass it as `baseUrl` in the config, for example `baseUrl: process.env.SUPERMEMORY_BASE_URL`.

View file

@ -1,7 +1,7 @@
{ {
"name": "@supermemory/tools", "name": "@supermemory/tools",
"type": "module", "type": "module",
"version": "2.3.0", "version": "3.0.0",
"description": "Memory tools for AI SDK, OpenAI, Voltagent and Mastra with supermemory", "description": "Memory tools for AI SDK, OpenAI, Voltagent and Mastra with supermemory",
"scripts": { "scripts": {
"build": "tsdown", "build": "tsdown",
@ -17,7 +17,7 @@
"ai": "^5.0.29", "ai": "^5.0.29",
"lru-cache": "^11.2.6", "lru-cache": "^11.2.6",
"openai": "^4.104.0", "openai": "^4.104.0",
"supermemory": "^4.25.4", "supermemory": "^5.0.0",
"zod": "^4.1.5" "zod": "^4.1.5"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,35 +1,32 @@
import Supermemory from "supermemory"
import { tool } from "ai" import { tool } from "ai"
import { z } from "zod" import { z } from "zod"
import { import {
CLIENT_OPTIONS,
DEFAULT_VALUES, DEFAULT_VALUES,
PARAMETER_DESCRIPTIONS, PARAMETER_DESCRIPTIONS,
SEARCH_LIMIT_BOUNDS, SEARCH_LIMIT_BOUNDS,
TOOL_DESCRIPTIONS, TOOL_DESCRIPTIONS,
clampSearchLimit, clampSearchLimit,
deleteDocumentByIdentifier, createToolsClient,
getContainerTags, deleteDocument,
forgetMemory,
getNamespace,
getProfileWithSearch,
} from "./tools-shared" } from "./tools-shared"
import { forgetMemoryRequest } from "./shared/forget-memory"
import type { SupermemoryToolsConfig } from "./types" import type { SupermemoryToolsConfig } from "./types"
function createClient(apiKey: string, config?: SupermemoryToolsConfig) { function toolError(error: unknown) {
return new Supermemory({ return {
apiKey, success: false as const,
...CLIENT_OPTIONS, error: error instanceof Error ? error.message : "Unknown error",
...(config?.baseUrl ? { baseURL: config.baseUrl } : {}), }
})
} }
// Export individual tool creators
export const searchMemoriesTool = ( export const searchMemoriesTool = (
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) => { ) => {
const client = createClient(apiKey, config) const client = createToolsClient(apiKey, config)
const namespace = getNamespace(config)
const containerTags = getContainerTags(config)
const strict = config?.strict ?? false const strict = config?.strict ?? false
return tool({ return tool({
@ -67,9 +64,8 @@ export const searchMemoriesTool = (
}), }),
execute: async ({ informationToGet, limit = DEFAULT_VALUES.limit }) => { execute: async ({ informationToGet, limit = DEFAULT_VALUES.limit }) => {
try { try {
const response = await client.search({ const response = await client.search(namespace, {
q: informationToGet, query: informationToGet,
containerTag: containerTags[0],
limit: clampSearchLimit(limit), limit: clampSearchLimit(limit),
threshold: DEFAULT_VALUES.searchThreshold, threshold: DEFAULT_VALUES.searchThreshold,
searchMode: "hybrid", searchMode: "hybrid",
@ -81,10 +77,7 @@ export const searchMemoriesTool = (
count: response.results?.length || 0, count: response.results?.length || 0,
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
}, },
}) })
@ -94,9 +87,8 @@ export const addMemoryTool = (
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) => { ) => {
const client = createClient(apiKey, config) const client = createToolsClient(apiKey, config)
const namespace = getNamespace(config)
const containerTags = getContainerTags(config)
return tool({ return tool({
description: TOOL_DESCRIPTIONS.addMemory, description: TOOL_DESCRIPTIONS.addMemory,
@ -105,12 +97,9 @@ export const addMemoryTool = (
}), }),
execute: async ({ memory }) => { execute: async ({ memory }) => {
try { try {
const metadata: Record<string, string | number | boolean> = {} const response = await client.add(namespace, {
const response = await client.add({
content: memory, content: memory,
containerTags, dreaming: "instant",
...(Object.keys(metadata).length > 0 && { metadata }),
}) })
return { return {
@ -118,10 +107,7 @@ export const addMemoryTool = (
memory: response, memory: response,
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
}, },
}) })
@ -131,38 +117,22 @@ export const getProfileTool = (
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) => { ) => {
const client = createClient(apiKey, config) const client = createToolsClient(apiKey, config)
const namespace = getNamespace(config)
const containerTags = getContainerTags(config)
const strict = config?.strict ?? false
return tool({ return tool({
description: TOOL_DESCRIPTIONS.getProfile, description: TOOL_DESCRIPTIONS.getProfile,
inputSchema: z.object({ inputSchema: z.object({
containerTag: strict
? z.string().describe(PARAMETER_DESCRIPTIONS.containerTag)
: z.string().optional().describe(PARAMETER_DESCRIPTIONS.containerTag),
query: z.string().optional().describe(PARAMETER_DESCRIPTIONS.query), query: z.string().optional().describe(PARAMETER_DESCRIPTIONS.query),
}), }),
execute: async ({ containerTag, query }) => { execute: async ({ query }) => {
try { try {
const tag = containerTag || containerTags[0]
const response = await client.profile({
containerTag: tag,
...(query && { q: query }),
})
return { return {
success: true, success: true,
profile: response.profile, ...(await getProfileWithSearch(client, namespace, query)),
searchResults: response.searchResults,
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
}, },
}) })
@ -172,18 +142,13 @@ export const documentListTool = (
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) => { ) => {
const client = createClient(apiKey, config) const client = createToolsClient(apiKey, config)
const namespace = getNamespace(config)
const containerTags = getContainerTags(config)
const strict = config?.strict ?? false const strict = config?.strict ?? false
return tool({ return tool({
description: TOOL_DESCRIPTIONS.documentList, description: TOOL_DESCRIPTIONS.documentList,
inputSchema: z.object({ inputSchema: z.object({
containerTag: z
.string()
.optional()
.describe(PARAMETER_DESCRIPTIONS.containerTag),
limit: strict limit: strict
? z.coerce ? z.coerce
.number() .number()
@ -196,28 +161,20 @@ export const documentListTool = (
.describe(PARAMETER_DESCRIPTIONS.limit), .describe(PARAMETER_DESCRIPTIONS.limit),
page: z.coerce.number().optional().describe(PARAMETER_DESCRIPTIONS.page), page: z.coerce.number().optional().describe(PARAMETER_DESCRIPTIONS.page),
}), }),
execute: async ({ containerTag, limit, page }) => { execute: async ({ limit, page }) => {
try { try {
const scopeTags: [string, ...string[]] = containerTag const response = await client.list(namespace, "documents", {
? [containerTag]
: containerTags
const response = await client.documents.list({
containerTags: scopeTags,
limit: limit || DEFAULT_VALUES.limit, limit: limit || DEFAULT_VALUES.limit,
...(page !== undefined && { page }), ...(page !== undefined && { page }),
}) })
return { return {
success: true, success: true,
documents: response.memories, documents: response.documents,
pagination: response.pagination, pagination: response.pagination,
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
}, },
}) })
@ -227,40 +184,24 @@ export const documentDeleteTool = (
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) => { ) => {
const client = createClient(apiKey, config) const client = createToolsClient(apiKey, config)
const containerTags = getContainerTags(config) const namespace = getNamespace(config)
const strict = config?.strict ?? false
return tool({ return tool({
description: TOOL_DESCRIPTIONS.documentDelete, description: TOOL_DESCRIPTIONS.documentDelete,
inputSchema: z.object({ inputSchema: z.object({
documentId: z.string().describe(PARAMETER_DESCRIPTIONS.documentId), documentId: z.string().describe(PARAMETER_DESCRIPTIONS.documentId),
containerTag: strict
? z
.string()
.nullable()
.describe(PARAMETER_DESCRIPTIONS.documentContainerTag)
: z
.string()
.optional()
.describe(PARAMETER_DESCRIPTIONS.documentContainerTag),
}), }),
execute: async ({ documentId, containerTag }) => { execute: async ({ documentId }) => {
try { try {
const scopeTags: [string, ...string[]] = containerTag await deleteDocument(client, namespace, documentId)
? [containerTag]
: containerTags
await deleteDocumentByIdentifier(client, documentId, scopeTags)
return { return {
success: true, success: true,
message: `Document ${documentId} deleted successfully`, message: `Document ${documentId} deleted successfully`,
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
}, },
}) })
@ -270,9 +211,8 @@ export const documentAddTool = (
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) => { ) => {
const client = createClient(apiKey, config) const client = createToolsClient(apiKey, config)
const namespace = getNamespace(config)
const containerTags = getContainerTags(config)
return tool({ return tool({
description: TOOL_DESCRIPTIONS.documentAdd, description: TOOL_DESCRIPTIONS.documentAdd,
@ -290,9 +230,8 @@ export const documentAddTool = (
if (title) metadata.title = title if (title) metadata.title = title
if (description) metadata.description = description if (description) metadata.description = description
const response = await client.documents.add({ const response = await client.add(namespace, {
content, content,
containerTags,
...(Object.keys(metadata).length > 0 && { metadata }), ...(Object.keys(metadata).length > 0 && { metadata }),
}) })
@ -301,10 +240,7 @@ export const documentAddTool = (
document: response, document: response,
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
}, },
}) })
@ -314,23 +250,19 @@ export const memoryForgetTool = (
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) => { ) => {
const containerTags = getContainerTags(config) const client = createToolsClient(apiKey, config)
const namespace = getNamespace(config)
return tool({ return tool({
description: TOOL_DESCRIPTIONS.memoryForget, description: TOOL_DESCRIPTIONS.memoryForget,
inputSchema: z.object({ inputSchema: z.object({
containerTag: z
.string()
.optional()
.describe(PARAMETER_DESCRIPTIONS.containerTag),
memoryId: z.string().optional().describe(PARAMETER_DESCRIPTIONS.memoryId), memoryId: z.string().optional().describe(PARAMETER_DESCRIPTIONS.memoryId),
memoryContent: z memoryContent: z
.string() .string()
.optional() .optional()
.describe(PARAMETER_DESCRIPTIONS.memoryContent), .describe(PARAMETER_DESCRIPTIONS.memoryContent),
reason: z.string().optional().describe(PARAMETER_DESCRIPTIONS.reason),
}), }),
execute: async ({ containerTag, memoryId, memoryContent, reason }) => { execute: async ({ memoryId, memoryContent }) => {
try { try {
if (!memoryId && !memoryContent) { if (!memoryId && !memoryContent) {
return { return {
@ -339,36 +271,22 @@ export const memoryForgetTool = (
} }
} }
const tag = containerTag || containerTags[0] await forgetMemory(client, namespace, {
...(memoryId && { id: memoryId }),
await forgetMemoryRequest( ...(memoryContent && { content: memoryContent }),
apiKey, })
{
containerTag: tag as string,
...(memoryId && { id: memoryId }),
...(memoryContent && { content: memoryContent }),
...(reason && { reason }),
},
config?.baseUrl,
)
return { return {
success: true, success: true,
message: "Memory forgotten successfully", message: "Memory forgotten successfully",
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
}, },
}) })
} }
/**
* Create Supermemory tools for AI SDK
*/
export function supermemoryTools( export function supermemoryTools(
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
@ -391,4 +309,4 @@ export {
type PromptTemplate, type PromptTemplate,
type MemoryPromptData, type MemoryPromptData,
} from "./vercel" } from "./vercel"
export { getContainerTags } from "./tools-shared" export { getNamespace } from "./tools-shared"

View file

@ -12,7 +12,7 @@ const anthropic = new Anthropic({
}) })
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, { const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
projectId: "my-app", namespace: "my-app",
}) })
async function chatWithMemory(userMessage: string) { async function chatWithMemory(userMessage: string) {

View file

@ -1,24 +1,29 @@
import { beforeEach, describe, expect, it, vi } from "vitest" import { beforeEach, describe, expect, it, vi } from "vitest"
// Mock the Supermemory SDK so the Claude memory tool's document-backed file // Mock the v5 SDK so document-backed file operations run without network access.
// operations can be exercised deterministically without any network access. const sdk = vi.hoisted(() => {
const documentsListMock = vi.fn() class NotFoundError extends Error {
const documentsGetMock = vi.fn() statusCode = 404
const documentsDeleteBulkMock = vi.fn() }
const addMock = vi.fn() return {
NotFoundError,
list: vi.fn(),
get: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
add: vi.fn(),
}
})
const documentsGetMock = sdk.get
const updateMock = sdk.update
vi.mock("supermemory", () => { vi.mock("supermemory", () => {
return { class Supermemory {
default: class MockSupermemory { add = sdk.add
add = addMock list = sdk.list
memories = { forget: vi.fn() } documents = { get: sdk.get, update: sdk.update, delete: sdk.delete }
documents = {
list: documentsListMock,
get: documentsGetMock,
deleteBulk: documentsDeleteBulkMock,
}
},
} }
return { default: Supermemory, Supermemory, NotFoundError: sdk.NotFoundError }
}) })
import { ClaudeMemoryTool } from "./claude-memory" import { ClaudeMemoryTool } from "./claude-memory"
@ -27,45 +32,34 @@ const FILE_PATH = "/memories/notes.txt"
// 5 distinct lines so an off-by-one at either end is observable. // 5 distinct lines so an off-by-one at either end is observable.
const FILE_CONTENT = "line1\nline2\nline3\nline4\nline5" const FILE_CONTENT = "line1\nline2\nline3\nline4\nline5"
const FILE_DOCUMENT = { const FILE_DOCUMENT = {
id: "document-notes", id: "memories_notes_txt",
customId: "memories_notes_txt",
filePath: FILE_PATH, filePath: FILE_PATH,
content: FILE_CONTENT, content: FILE_CONTENT,
} }
const NEIGHBOUR_DOCUMENT = { const NEIGHBOUR_DOCUMENT = {
id: "document-notes-backup", id: "memories_notes_backup_txt",
customId: "memories_notes_backup_txt",
filePath: "/memories/notes.backup.txt", filePath: "/memories/notes.backup.txt",
content: "backup stuff", content: "backup stuff",
} }
// v5 resolves caller-defined IDs inside the namespace.
function mockDocuments(documents: (typeof FILE_DOCUMENT)[]) { function mockDocuments(documents: (typeof FILE_DOCUMENT)[]) {
documentsListMock.mockResolvedValue({ documentsGetMock.mockImplementation(
memories: documents.map((document) => ({ async (_namespace: string, id: string) => {
id: document.id, const document = documents.find((candidate) => candidate.id === id)
customId: document.customId, if (!document) throw new sdk.NotFoundError(`Document not found: ${id}`)
containerTags: ["claude_memory"], return {
metadata: { id: document.id,
claude_memory_type: "file", metadata: {
file_path: document.filePath, source: "claude-memory",
}, claude_memory_type: "file",
})), file_path: document.filePath,
pagination: { totalPages: 1 }, },
}) content: document.content,
documentsGetMock.mockImplementation(async (id: string) => { system: { status: "done" },
const document = documents.find((candidate) => candidate.id === id) }
if (!document) throw new Error(`Document not found: ${id}`) },
return { )
id: document.id,
customId: document.customId,
containerTags: ["sm_project_default", "claude_memory"],
metadata: {
claude_memory_type: "file",
file_path: document.filePath,
},
content: document.content,
}
})
} }
function mockDocument(content: string) { function mockDocument(content: string) {
@ -76,7 +70,6 @@ describe("ClaudeMemoryTool view_range", () => {
let tool: ClaudeMemoryTool let tool: ClaudeMemoryTool
beforeEach(() => { beforeEach(() => {
documentsListMock.mockReset()
documentsGetMock.mockReset() documentsGetMock.mockReset()
mockDocument(FILE_CONTENT) mockDocument(FILE_CONTENT)
tool = new ClaudeMemoryTool("test-api-key") tool = new ClaudeMemoryTool("test-api-key")
@ -132,9 +125,8 @@ describe("ClaudeMemoryTool exact-file matching", () => {
let tool: ClaudeMemoryTool let tool: ClaudeMemoryTool
beforeEach(() => { beforeEach(() => {
documentsListMock.mockReset()
documentsGetMock.mockReset() documentsGetMock.mockReset()
addMock.mockReset() updateMock.mockReset().mockResolvedValue({ id: "doc", status: "queued" })
tool = new ClaudeMemoryTool("test-api-key") tool = new ClaudeMemoryTool("test-api-key")
}) })
@ -176,7 +168,23 @@ describe("ClaudeMemoryTool exact-file matching", () => {
}) })
expect(result.success).toBe(false) expect(result.success).toBe(false)
expect(addMock).not.toHaveBeenCalled() expect(updateMock).not.toHaveBeenCalled()
})
})
describe("ClaudeMemoryTool path collisions", () => {
it("does not serve a file whose path only normalizes to the same ID", async () => {
documentsGetMock.mockReset()
mockDocuments([{ ...FILE_DOCUMENT, filePath: "/memories/notes_txt" }])
const tool = new ClaudeMemoryTool("test-api-key")
const result = await tool.handleCommand({
command: "view",
path: FILE_PATH,
})
expect(result.success).toBe(false)
expect(result.error).toContain("File not found")
}) })
}) })
@ -184,9 +192,8 @@ describe("ClaudeMemoryTool str_replace replacement literalness", () => {
let tool: ClaudeMemoryTool let tool: ClaudeMemoryTool
beforeEach(() => { beforeEach(() => {
documentsListMock.mockReset()
documentsGetMock.mockReset() documentsGetMock.mockReset()
addMock.mockReset() updateMock.mockReset().mockResolvedValue({ id: "doc", status: "queued" })
mockDocument(FILE_CONTENT) mockDocument(FILE_CONTENT)
tool = new ClaudeMemoryTool("test-api-key") tool = new ClaudeMemoryTool("test-api-key")
}) })
@ -205,8 +212,8 @@ describe("ClaudeMemoryTool str_replace replacement literalness", () => {
}) })
expect(result.success).toBe(true) expect(result.success).toBe(true)
expect(addMock).toHaveBeenCalledTimes(1) expect(updateMock).toHaveBeenCalledTimes(1)
const stored = addMock.mock.calls[0]?.[0]?.content as string const stored = updateMock.mock.calls[0]?.[2]?.content as string
expect(stored).toContain(`price is ${dollarSequence} today`) expect(stored).toContain(`price is ${dollarSequence} today`)
}) })
}) })

View file

@ -1,11 +1,14 @@
import Supermemory from "supermemory" import type { Supermemory } from "supermemory"
import { deleteDocumentById, getContainerTags } from "./tools-shared" import {
createToolsClient,
getNamespace,
isNotFoundError,
} from "./tools-shared"
import type { SupermemoryToolsConfig } from "./types" import type { SupermemoryToolsConfig } from "./types"
// Claude Memory Tool Types export type ClaudeMemoryConfig = SupermemoryToolsConfig
export interface ClaudeMemoryConfig extends SupermemoryToolsConfig {
memoryContainerTag?: string export const CLAUDE_MEMORY_SOURCE = "claude-memory"
}
export interface MemoryCommand { export interface MemoryCommand {
command: "view" | "create" | "str_replace" | "insert" | "delete" | "rename" command: "view" | "create" | "str_replace" | "insert" | "delete" | "rename"
@ -45,40 +48,19 @@ interface ClaudeFileDocument {
metadata: ClaudeFileMetadata metadata: ClaudeFileMetadata
} }
/** /** Maps Claude's memory tool commands to Supermemory documents in one namespace. */
* Claude Memory Tool - Client-side implementation
* Maps Claude's memory tool commands to supermemory document operations
*/
export class ClaudeMemoryTool { export class ClaudeMemoryTool {
private client: Supermemory private client: Supermemory
private containerTags: string[] private namespace: string
private scopeContainerTags: [string, ...string[]]
private memoryContainerPrefix: string
/** /** Converts /memories/file.txt -> memories_file_txt */
* Normalize file path to be used as customId private normalizePathToId(path: string): string {
* Converts /memories/file.txt -> memories_file_txt return path.replace(/^\//, "").replace(/\//g, "_").replace(/\./g, "_")
*/
private normalizePathToCustomId(path: string): string {
return path
.replace(/^\//, "") // Remove leading slash
.replace(/\//g, "_") // Replace / with _
.replace(/\./g, "_") // Replace . with _
} }
constructor(apiKey: string, config?: ClaudeMemoryConfig) { constructor(apiKey: string, config?: ClaudeMemoryConfig) {
this.client = new Supermemory({ this.client = createToolsClient(apiKey, config)
apiKey, this.namespace = getNamespace(config)
...(config?.baseUrl && { baseURL: config.baseUrl }),
})
// Use custom memory container tag or default
this.memoryContainerPrefix = config?.memoryContainerTag || "claude_memory"
// Get base container tags and add memory-specific tag
const baseContainerTags = getContainerTags(config)
this.scopeContainerTags = baseContainerTags
this.containerTags = [...baseContainerTags, this.memoryContainerPrefix]
} }
/** /**
@ -203,93 +185,48 @@ export class ClaudeMemoryTool {
*/ */
private async listDirectory(dirPath: string): Promise<MemoryResponse> { private async listDirectory(dirPath: string): Promise<MemoryResponse> {
try { try {
// Document search returns ranked chunks, not a complete inventory. Walk // Walk every page so files cannot disappear from a listing.
// every page of the document-list endpoint so files cannot disappear const filePaths: string[] = []
// from a directory merely because they did not rank in a search page.
const documents: Supermemory.DocumentListResponse.Memory[] = []
let page = 1 let page = 1
while (true) { while (true) {
const response = await this.client.documents.list({ const response = await this.client.list(this.namespace, "documents", {
containerTags: this.scopeContainerTags, filter: {
filters: { operator: "and",
AND: [ operands: [
{ key: "claude_memory_type", value: "file" }, { field: "source", operator: "eq", value: CLAUDE_MEMORY_SOURCE },
{ { field: "claude_memory_type", operator: "eq", value: "file" },
key: "file_path", { field: "file_path", operator: "contains", value: dirPath },
value: dirPath,
filterType: "string_contains",
},
], ],
}, },
includeContent: false,
limit: 100, limit: 100,
page, page,
}) })
documents.push(...response.memories) for (const document of response.documents) {
const filePath = this.getDocumentFilePath(document)
if (filePath?.startsWith(dirPath)) filePaths.push(filePath)
}
if (page >= response.pagination.totalPages) break if (page >= response.pagination.totalPages) break
page += 1 page += 1
} }
// Filter files that match the directory path and extract relative paths
const files: string[] = [] const files: string[] = []
const dirs = new Set<string>() const dirs = new Set<string>()
const candidates: Array<{
document: Supermemory.DocumentListResponse.Memory
filePath: string
}> = []
for (const document of documents) { for (const filePath of filePaths) {
if (!this.isDocumentInConfiguredScope(document)) continue const relativePath = filePath.substring(dirPath.length)
if (!relativePath) continue
const filePath = this.getDocumentFilePath(document) const slashIndex = relativePath.indexOf("/")
if (!filePath || !filePath.startsWith(dirPath)) { if (slashIndex > 0) {
continue dirs.add(`${relativePath.substring(0, slashIndex)}/`)
} } else {
candidates.push({ document, filePath }) files.push(relativePath)
}
// Full GETs are required to verify hidden project tags. Keep them bounded
// so large directories do not become a long serial chain or a burst of
// unbounded requests.
const verificationBatchSize = 8
for (
let index = 0;
index < candidates.length;
index += verificationBatchSize
) {
const batch = candidates.slice(index, index + verificationBatchSize)
const verified = await Promise.all(
batch.map(async (candidate) =>
(await this.isDirectoryDocumentInExactScope(candidate.document))
? candidate
: undefined,
),
)
for (const candidate of verified) {
if (!candidate) continue
const { filePath } = candidate
// Get relative path from directory
const relativePath = filePath.substring(dirPath.length)
if (!relativePath) continue
// If path contains /, it's in a subdirectory
const slashIndex = relativePath.indexOf("/")
if (slashIndex > 0) {
// It's a subdirectory
dirs.add(`${relativePath.substring(0, slashIndex)}/`)
} else if (relativePath !== "") {
// It's a file in this directory
files.push(relativePath)
}
} }
} }
// Format directory listing
const entries = [...Array.from(dirs).sort(), ...files.sort()] const entries = [...Array.from(dirs).sort(), ...files.sort()]
if (entries.length === 0) { if (entries.length === 0) {
@ -384,19 +321,13 @@ export class ClaudeMemoryTool {
fileText: string, fileText: string,
): Promise<MemoryResponse> { ): Promise<MemoryResponse> {
try { try {
const normalizedId = this.normalizePathToCustomId(filePath) await this.writeFile(this.normalizePathToId(filePath), fileText, {
source: CLAUDE_MEMORY_SOURCE,
const _response = await this.client.add({ claude_memory_type: "file",
content: fileText, file_path: filePath,
customId: normalizedId, line_count: fileText.split("\n").length,
containerTags: this.containerTags, created_by: "claude_memory_tool",
metadata: { last_modified: new Date().toISOString(),
claude_memory_type: "file",
file_path: filePath,
line_count: fileText.split("\n").length,
created_by: "claude_memory_tool",
last_modified: new Date().toISOString(),
},
}) })
return { return {
@ -444,17 +375,10 @@ export class ClaudeMemoryTool {
// patterns like $&, $', and $` and silently corrupt the file. // patterns like $&, $', and $` and silently corrupt the file.
const newContent = originalContent.replace(oldStr, () => newStr) const newContent = originalContent.replace(oldStr, () => newStr)
// Update the document await this.writeFile(readResult.document.documentId, newContent, {
const normalizedId = this.normalizePathToCustomId(filePath) ...readResult.document.metadata,
const _updateResponse = await this.client.add({ line_count: newContent.split("\n").length,
content: newContent, last_modified: new Date().toISOString(),
customId: normalizedId,
containerTags: this.containerTags,
metadata: {
...readResult.document.metadata,
line_count: newContent.split("\n").length,
last_modified: new Date().toISOString(),
},
}) })
return { return {
@ -502,17 +426,10 @@ export class ClaudeMemoryTool {
lines.splice(insertLine - 1, 0, insertText) lines.splice(insertLine - 1, 0, insertText)
const newContent = lines.join("\n") const newContent = lines.join("\n")
// Update the document await this.writeFile(readResult.document.documentId, newContent, {
const normalizedId = this.normalizePathToCustomId(filePath) ...readResult.document.metadata,
await this.client.add({ line_count: newContent.split("\n").length,
content: newContent, last_modified: new Date().toISOString(),
customId: normalizedId,
containerTags: this.containerTags,
metadata: {
...readResult.document.metadata,
line_count: newContent.split("\n").length,
last_modified: new Date().toISOString(),
},
}) })
return { return {
@ -541,7 +458,7 @@ export class ClaudeMemoryTool {
} }
} }
await deleteDocumentById(this.client, readResult.document.documentId) await this.deleteFile(readResult.document.documentId)
return { return {
success: true, success: true,
@ -581,26 +498,17 @@ export class ClaudeMemoryTool {
} }
const originalContent = readResult.document.content const originalContent = readResult.document.content
const newNormalizedId = this.normalizePathToCustomId(newPath) const newNormalizedId = this.normalizePathToId(newPath)
// Create new document with new path await this.writeFile(newNormalizedId, originalContent, {
await this.client.add({ ...readResult.document.metadata,
content: originalContent, file_path: newPath,
customId: newNormalizedId, last_modified: new Date().toISOString(),
containerTags: this.containerTags,
metadata: {
...readResult.document.metadata,
file_path: newPath,
last_modified: new Date().toISOString(),
},
}) })
// Remove the old document so the previous path stops showing up in // Same normalized ID means the write above already replaced the file in place.
// listings and search. Skip when both paths normalize to the same if (readResult.document.documentId !== newNormalizedId) {
// customId — the add above already replaced the content. await this.deleteFile(readResult.document.documentId)
const oldNormalizedId = this.normalizePathToCustomId(oldPath)
if (oldNormalizedId !== newNormalizedId) {
await deleteDocumentById(this.client, readResult.document.documentId)
} }
return { return {
@ -615,129 +523,74 @@ export class ClaudeMemoryTool {
} }
} }
/** // v5 `add` with an existing id appends; `update` replaces, so writes try update first.
* Helper: Get document by file path private async writeFile(
*/ id: string,
content: string,
metadata: ClaudeFileMetadata,
): Promise<void> {
try {
await this.client.documents.update(this.namespace, id, {
content,
metadata,
})
} catch (error) {
if (!isNotFoundError(error)) throw error
await this.client.add(this.namespace, { content, id, metadata })
}
}
private async deleteFile(id: string): Promise<void> {
const response = await this.client.documents.delete(this.namespace, {
ids: [id],
})
if (response.count === 1) return
const detail = response.errors?.find((error) => error.id === id)?.error
throw new Error(
detail
? `Failed to delete document ${id}: ${detail}`
: `Failed to delete document ${id}: expected one deletion, received ${response.count}`,
)
}
private async getFileDocument(filePath: string): Promise<{ private async getFileDocument(filePath: string): Promise<{
success: boolean success: boolean
document?: ClaudeFileDocument document?: ClaudeFileDocument
error?: string error?: string
}> { }> {
try { try {
const normalizedId = this.normalizePathToCustomId(filePath) const id = this.normalizePathToId(filePath)
let page = 1 let document: Awaited<ReturnType<Supermemory["documents"]["get"]>>
const candidates = new Map< try {
string, document = await this.client.documents.get(this.namespace, id)
Supermemory.DocumentListResponse.Memory } catch (error) {
>() if (isNotFoundError(error)) {
return { success: false, error: `File not found: ${filePath}` }
// customId values are only unique within an exact container-tag set in
// Mono. Resolve the matching document inside this tool's configured
// scope before fetching by internal ID; a direct get(customId) can pick
// another project/user's same-named file.
while (true) {
const response = await this.client.documents.list({
containerTags: this.scopeContainerTags,
filters: {
AND: [
{ key: "claude_memory_type", value: "file" },
{ key: "file_path", value: filePath },
],
},
includeContent: false,
limit: 100,
page,
})
for (const document of response.memories) {
if (
document.customId === normalizedId &&
this.getDocumentFilePath(document) === filePath &&
this.isDocumentInConfiguredScope(document)
) {
candidates.set(document.id, document)
}
} }
throw error
if (page >= response.pagination.totalPages) break
page += 1
} }
const exactMatches: Array<{ // Different paths can normalize to the same ID; only the exact file counts.
candidate: Supermemory.DocumentListResponse.Memory if (
document: Supermemory.DocumentGetResponse document.metadata?.source !== CLAUDE_MEMORY_SOURCE ||
}> = [] this.getDocumentFilePath(document) !== filePath
let hasUnverifiedCandidate = false ) {
for (const candidate of candidates.values()) {
let document: Supermemory.DocumentGetResponse
try {
document = await this.client.documents.get(candidate.id)
} catch (error) {
if (error instanceof Supermemory.NotFoundError) continue
throw error
}
if (document.id !== candidate.id) {
hasUnverifiedCandidate = true
continue
}
if (
document.customId !== normalizedId ||
this.getDocumentFilePath(document) !== filePath ||
!this.hasExactContainerTags(document.containerTags)
) {
continue
}
exactMatches.push({ candidate, document })
}
if (exactMatches.length === 0) {
return {
success: false,
error: `File not found: ${filePath}`,
}
}
if (exactMatches.length > 1) {
return {
success: false,
error: `File path is ambiguous in the configured container scope: ${filePath}`,
}
}
if (hasUnverifiedCandidate) {
return {
success: false,
error: `File path could not be resolved unambiguously in the configured container scope: ${filePath}`,
}
}
const match = exactMatches[0]
if (!match) {
return { success: false, error: `File not found: ${filePath}` } return { success: false, error: `File not found: ${filePath}` }
} }
const { candidate, document } = match if (typeof document.content !== "string") {
const content =
typeof document.content === "string"
? document.content
: typeof document.raw === "string"
? document.raw
: undefined
if (content === undefined) {
return { return {
success: false, success: false,
error: `File content unavailable: ${filePath}`, error: `File content unavailable: ${filePath}`,
} }
} }
const metadata =
document.metadata &&
typeof document.metadata === "object" &&
!Array.isArray(document.metadata)
? (document.metadata as ClaudeFileMetadata)
: {}
return { return {
success: true, success: true,
document: { documentId: candidate.id, content, metadata }, document: {
documentId: id,
content: document.content,
metadata: document.metadata as ClaudeFileMetadata,
},
} }
} catch (error) { } catch (error) {
return { return {
@ -761,46 +614,6 @@ export class ClaudeMemoryTool {
: undefined : undefined
} }
private isDocumentInConfiguredScope(
document: Supermemory.DocumentListResponse.Memory,
): boolean {
const documentTags = document.containerTags ?? []
const expectedTags = this.containerTags.filter(
(tag) => !tag.startsWith("sm_project_"),
)
return (
documentTags.length === expectedTags.length &&
documentTags.every((tag, index) => tag === expectedTags[index])
)
}
private async isDirectoryDocumentInExactScope(
document: Supermemory.DocumentListResponse.Memory,
): Promise<boolean> {
try {
// Mono strips internal project tags from every list response, so only a
// full get can prove that no hidden tags change this document's scope.
const fullDocument = await this.client.documents.get(document.id)
return (
fullDocument.id === document.id &&
this.hasExactContainerTags(fullDocument.containerTags)
)
} catch (error) {
if (!(error instanceof Supermemory.NotFoundError)) throw error
// A document can disappear between list and get. Skip stale entries
// instead of failing the entire directory view.
return false
}
}
private hasExactContainerTags(containerTags?: string[]): boolean {
return (
containerTags?.length === this.containerTags.length &&
containerTags.every((tag, index) => tag === this.containerTags[index])
)
}
/** /**
* Validate that path starts with /memories for security * Validate that path starts with /memories for security
*/ */

View file

@ -1,10 +1,5 @@
/** import type { AddResponse } from "supermemory"
* Client for the Supermemory Conversations API import { createSupermemoryClient } from "./shared/context"
*
* This module provides a helper function to ingest conversations using the
* /v4/conversations endpoint, which supports structured messages with smart
* diffing and append detection on the backend.
*/
export interface ConversationMessage { export interface ConversationMessage {
role: "user" | "assistant" | "system" | "tool" role: "user" | "assistant" | "system" | "tool"
@ -36,7 +31,7 @@ const encodeBase64 = (bytes: Uint8Array): string => {
return encoded return encoded
} }
/** Normalize supported SDK image representations for `/v4/conversations`. */ /** Normalize supported SDK image representations to a URL or data URL. */
export const toConversationImageUrl = ( export const toConversationImageUrl = (
value: unknown, value: unknown,
mediaType = "image/jpeg", mediaType = "image/jpeg",
@ -73,75 +68,73 @@ export interface ToolCall {
} }
export interface AddConversationParams { export interface AddConversationParams {
conversationId: string id: string
messages: ConversationMessage[] messages: ConversationMessage[]
containerTags?: string[] namespace: string
metadata?: Record<string, string | number | boolean> metadata?: Record<string, string | number | boolean>
supportingContext?: string
apiKey: string apiKey: string
baseUrl?: string baseUrl?: string
} }
export interface AddConversationResponse { const CONVERSATION_REQUEST_TIMEOUT_SECONDS = 30
id: string
conversationId: string const ROLE_LABELS: Record<ConversationMessage["role"], string> = {
status: string user: "User",
assistant: "Assistant",
system: "System",
tool: "Tool",
} }
const CONVERSATION_REQUEST_TIMEOUT_MS = 30_000 // Data URLs are dropped: inlining base64 would bloat the document text.
const formatContentPart = (part: ContentPart): string => {
if (part.type === "text") return part.text
const url = part.imageUrl.url
return url.startsWith("data:") ? "[image]" : `[image: ${url}]`
}
/** export function formatConversationText(
* Adds a conversation to Supermemory using the /v4/conversations endpoint messages: ConversationMessage[],
* ): string {
* This endpoint supports: return messages
* - Structured messages with roles (user, assistant, system, tool) .map((message) => {
* - Multi-modal content (text, images) const content =
* - Tool calls and responses typeof message.content === "string"
* ? message.content
* @param params - Configuration for adding the conversation : message.content.map(formatContentPart).join(" ")
* @returns Promise resolving to the conversation response const toolCalls = (message.tool_calls ?? [])
* @throws Error if the API request fails .map(
* (call) =>
* @example `[tool call: ${call.function.name}(${call.function.arguments})]`,
* ```typescript )
* const response = await addConversation({ .join(" ")
* conversationId: "conv-123", const label = message.name
* messages: [ ? `${ROLE_LABELS[message.role]} (${message.name})`
* { role: "user", content: "Hello!" }, : ROLE_LABELS[message.role]
* { role: "assistant", content: "Hi there!" } return `${label}: ${[content, toolCalls].filter(Boolean).join(" ")}`
* ], })
* containerTags: ["user-456"], .join("\n\n")
* apiKey: process.env.SUPERMEMORY_API_KEY, }
* })
* ``` // Same id → same document, so one conversation stays one document.
*/
export async function addConversation( export async function addConversation(
params: AddConversationParams, params: AddConversationParams,
): Promise<AddConversationResponse> { ): Promise<AddResponse> {
const baseUrl = params.baseUrl || "https://api.supermemory.ai" const client = createSupermemoryClient({
const url = `${baseUrl}/v4/conversations` apiKey: params.apiKey,
baseUrl: params.baseUrl,
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${params.apiKey}`,
},
body: JSON.stringify({
conversationId: params.conversationId,
messages: params.messages,
containerTags: params.containerTags,
metadata: params.metadata,
}),
redirect: "error",
signal: AbortSignal.timeout(CONVERSATION_REQUEST_TIMEOUT_MS),
}) })
return await client.add(
if (!response.ok) { params.namespace,
const errorText = await response.text().catch(() => "Unknown error") {
throw new Error( content: formatConversationText(params.messages),
`Failed to add conversation: ${response.status} ${response.statusText}. ${errorText}`, id: params.id,
) dreaming: "instant",
} ...(params.metadata && { metadata: params.metadata }),
...(params.supportingContext && {
return await response.json() supportingContext: params.supportingContext,
}),
},
{ timeoutInSeconds: CONVERSATION_REQUEST_TIMEOUT_SECONDS, maxRetries: 0 },
)
} }

View file

@ -8,5 +8,5 @@ export {
TOOL_DESCRIPTIONS, TOOL_DESCRIPTIONS,
PARAMETER_DESCRIPTIONS, PARAMETER_DESCRIPTIONS,
DEFAULT_VALUES, DEFAULT_VALUES,
getContainerTags, getNamespace,
} from "./tools-shared" } from "./tools-shared"

View file

@ -43,8 +43,8 @@ import type {
* Internal context shared between input and output processors. * Internal context shared between input and output processors.
*/ */
interface ProcessorContext { interface ProcessorContext {
containerTag: string namespace: string
customId: string id: string
apiKey: string apiKey: string
baseUrl: string baseUrl: string
mode: MemoryMode mode: MemoryMode
@ -65,8 +65,8 @@ function createProcessorContext(
const logger = createLogger(options.verbose ?? false) const logger = createLogger(options.verbose ?? false)
return { return {
containerTag: options.containerTag, namespace: options.namespace,
customId: options.customId, id: options.id,
apiKey, apiKey,
baseUrl, baseUrl,
mode: options.mode ?? "profile", mode: options.mode ?? "profile",
@ -78,14 +78,14 @@ function createProcessorContext(
} }
/** /**
* Gets the effective customId from RequestContext (if provided) or falls back to context. * Gets the effective id from RequestContext (if provided) or falls back to context.
* Per-request thread ID takes precedence to support dynamic per-conversation IDs in server setups. * Per-request thread ID takes precedence to support dynamic per-conversation IDs in server setups.
*/ */
function getEffectiveCustomId( function getEffectiveId(
ctx: ProcessorContext, ctx: ProcessorContext,
requestContext?: RequestContext, requestContext?: RequestContext,
): string { ): string {
// Per-request thread ID takes precedence over construction-time customId // Per-request thread ID takes precedence over construction-time id
if (requestContext) { if (requestContext) {
const threadId = requestContext.get(MASTRA_THREAD_ID_KEY) as const threadId = requestContext.get(MASTRA_THREAD_ID_KEY) as
| string | string
@ -94,8 +94,8 @@ function getEffectiveCustomId(
return threadId return threadId
} }
} }
// Fall back to construction-time customId // Fall back to construction-time id
return ctx.customId return ctx.id
} }
/** /**
@ -117,8 +117,8 @@ function getEffectiveCustomId(
* model: openai("gpt-4o"), * model: openai("gpt-4o"),
* inputProcessors: [ * inputProcessors: [
* new SupermemoryInputProcessor({ * new SupermemoryInputProcessor({
* containerTag: "user-123", * namespace: "user-123",
* customId: "conv-456", * id: "conv-456",
* mode: "full", * mode: "full",
* verbose: true, * verbose: true,
* }), * }),
@ -158,9 +158,9 @@ export class SupermemoryInputProcessor implements Processor {
return messageList return messageList
} }
const effectiveThreadId = getEffectiveCustomId(this.ctx, requestContext) const effectiveThreadId = getEffectiveId(this.ctx, requestContext)
const turnKey = MemoryCache.makeTurnKey( const turnKey = MemoryCache.makeTurnKey(
this.ctx.containerTag, this.ctx.namespace,
effectiveThreadId, effectiveThreadId,
this.ctx.mode, this.ctx.mode,
queryText || "", queryText || "",
@ -174,13 +174,13 @@ export class SupermemoryInputProcessor implements Processor {
} }
this.ctx.logger.info("Starting memory search", { this.ctx.logger.info("Starting memory search", {
containerTag: this.ctx.containerTag, namespace: this.ctx.namespace,
threadId: effectiveThreadId, threadId: effectiveThreadId,
mode: this.ctx.mode, mode: this.ctx.mode,
}) })
const memories = await buildMemoriesText({ const memories = await buildMemoriesText({
containerTag: this.ctx.containerTag, namespace: this.ctx.namespace,
queryText: queryText || "", queryText: queryText || "",
mode: this.ctx.mode, mode: this.ctx.mode,
baseUrl: this.ctx.baseUrl, baseUrl: this.ctx.baseUrl,
@ -212,7 +212,7 @@ export class SupermemoryInputProcessor implements Processor {
* *
* This processor runs once after generation completes (processOutputResult). * This processor runs once after generation completes (processOutputResult).
* When addMemory is set to "always", it saves the conversation to Supermemory * When addMemory is set to "always", it saves the conversation to Supermemory
* using the /v4/conversations API for thread-based storage. * as one document per conversation.
* *
* @example * @example
* ```typescript * ```typescript
@ -226,8 +226,8 @@ export class SupermemoryInputProcessor implements Processor {
* model: openai("gpt-4o"), * model: openai("gpt-4o"),
* outputProcessors: [ * outputProcessors: [
* new SupermemoryOutputProcessor({ * new SupermemoryOutputProcessor({
* containerTag: "user-123", * namespace: "user-123",
* customId: "conv-456", * id: "conv-456",
* addMemory: "always", * addMemory: "always",
* }), * }),
* ], * ],
@ -253,7 +253,7 @@ export class SupermemoryOutputProcessor implements Processor {
return messages return messages
} }
const effectiveCustomId = getEffectiveCustomId(this.ctx, requestContext) const effectiveId = getEffectiveId(this.ctx, requestContext)
try { try {
const conversationMessages = this.convertToConversationMessages(messages) const conversationMessages = this.convertToConversationMessages(messages)
@ -264,16 +264,16 @@ export class SupermemoryOutputProcessor implements Processor {
} }
const response = await addConversation({ const response = await addConversation({
conversationId: effectiveCustomId, id: effectiveId,
messages: conversationMessages, messages: conversationMessages,
containerTags: [this.ctx.containerTag], namespace: this.ctx.namespace,
apiKey: this.ctx.apiKey, apiKey: this.ctx.apiKey,
baseUrl: this.ctx.baseUrl, baseUrl: this.ctx.baseUrl,
}) })
this.ctx.logger.info("Conversation saved successfully", { this.ctx.logger.info("Conversation saved successfully", {
containerTag: this.ctx.containerTag, namespace: this.ctx.namespace,
customId: effectiveCustomId, id: effectiveId,
messageCount: conversationMessages.length, messageCount: conversationMessages.length,
responseId: response.id, responseId: response.id,
}) })
@ -330,7 +330,7 @@ export class SupermemoryOutputProcessor implements Processor {
/** /**
* Creates a Supermemory input processor for memory injection. * Creates a Supermemory input processor for memory injection.
* *
* @param options - Configuration options including required containerTag and customId * @param options - Configuration options including required namespace and id
* @returns Configured SupermemoryInputProcessor instance * @returns Configured SupermemoryInputProcessor instance
* *
* @example * @example
@ -340,8 +340,8 @@ export class SupermemoryOutputProcessor implements Processor {
* import { openai } from "@ai-sdk/openai" * import { openai } from "@ai-sdk/openai"
* *
* const processor = createSupermemoryProcessor({ * const processor = createSupermemoryProcessor({
* containerTag: "user-123", * namespace: "user-123",
* customId: "conv-456", * id: "conv-456",
* mode: "full", * mode: "full",
* verbose: true, * verbose: true,
* }) * })
@ -363,7 +363,7 @@ export function createSupermemoryProcessor(
/** /**
* Creates a Supermemory output processor for saving conversations. * Creates a Supermemory output processor for saving conversations.
* *
* @param options - Configuration options including required containerTag and customId * @param options - Configuration options including required namespace and id
* @returns Configured SupermemoryOutputProcessor instance * @returns Configured SupermemoryOutputProcessor instance
* *
* @example * @example
@ -373,8 +373,8 @@ export function createSupermemoryProcessor(
* import { openai } from "@ai-sdk/openai" * import { openai } from "@ai-sdk/openai"
* *
* const processor = createSupermemoryOutputProcessor({ * const processor = createSupermemoryOutputProcessor({
* containerTag: "user-123", * namespace: "user-123",
* customId: "conv-456", * id: "conv-456",
* addMemory: "always", * addMemory: "always",
* }) * })
* *
@ -408,8 +408,8 @@ export function createSupermemoryOutputProcessor(
* import { openai } from "@ai-sdk/openai" * import { openai } from "@ai-sdk/openai"
* *
* const { input, output } = createSupermemoryProcessors({ * const { input, output } = createSupermemoryProcessors({
* containerTag: "user-123", * namespace: "user-123",
* customId: "conv-456", * id: "conv-456",
* mode: "full", * mode: "full",
* addMemory: "always", * addMemory: "always",
* }) * })

View file

@ -35,10 +35,10 @@ export type { RequestContext } from "@mastra/core/request-context"
* Configuration options for the Supermemory Mastra processor. * Configuration options for the Supermemory Mastra processor.
*/ */
export interface SupermemoryMastraOptions { export interface SupermemoryMastraOptions {
/** Container tag/user ID for scoping memories. Required. */ /** Namespace (e.g. user ID) for scoping memories. Required. */
containerTag: string namespace: string
/** Custom ID to group messages into a single document for contextual memory generation. Required. */ /** ID that groups messages into a single document for contextual memory generation. Required. */
customId: string id: string
/** Supermemory API key (falls back to SUPERMEMORY_API_KEY env var) */ /** Supermemory API key (falls back to SUPERMEMORY_API_KEY env var) */
apiKey?: string apiKey?: string
/** Custom Supermemory API base URL */ /** Custom Supermemory API base URL */

View file

@ -37,7 +37,7 @@ interface AgentConfig {
* - Output processor: Saves conversations after responses (when addMemory is "always") * - Output processor: Saves conversations after responses (when addMemory is "always")
* *
* @param config - The Mastra agent configuration to enhance * @param config - The Mastra agent configuration to enhance
* @param options - Configuration options including required containerTag and customId * @param options - Configuration options including required namespace and id
* @returns Enhanced agent config with Supermemory processors injected * @returns Enhanced agent config with Supermemory processors injected
* *
* @example * @example
@ -54,8 +54,8 @@ interface AgentConfig {
* instructions: "You are a helpful assistant.", * instructions: "You are a helpful assistant.",
* }, * },
* { * {
* containerTag: "user-123", * namespace: "user-123",
* customId: "conv-456", * id: "conv-456",
* mode: "full", * mode: "full",
* addMemory: "always", * addMemory: "always",
* } * }
@ -74,11 +74,11 @@ export function withSupermemory<T extends AgentConfig>(
if ( if (
typeof options !== "object" || typeof options !== "object" ||
options === null || options === null ||
!options.containerTag || !options.namespace ||
!options.customId !options.id
) { ) {
throw new Error( throw new Error(
"withSupermemory: options must be an object with required containerTag and customId fields. " + "withSupermemory: options must be an object with required namespace and id fields. " +
"The API changed in v2.0.0 — see https://docs.supermemory.ai/integrations/mastra for the new signature.", "The API changed in v2.0.0 — see https://docs.supermemory.ai/integrations/mastra for the new signature.",
) )
} }

View file

@ -16,8 +16,8 @@ import {
* *
* @param openaiClient - The OpenAI client to wrap with SuperMemory middleware * @param openaiClient - The OpenAI client to wrap with SuperMemory middleware
* @param options - Configuration options for the middleware * @param options - Configuration options for the middleware
* @param options.containerTag - Required. The container tag/identifier for memory search (e.g., user ID, project ID) * @param options.namespace - Required. The namespace for memory search (e.g., user ID, project ID)
* @param options.customId - Required. Custom ID to group messages into a single document for contextual memory generation * @param options.id - Required. ID that groups messages into a single document for contextual memory generation
* @param options.verbose - Optional flag to enable detailed logging of memory search and injection process (default: false) * @param options.verbose - Optional flag to enable detailed logging of memory search and injection process (default: false)
* @param options.mode - Optional mode for memory search: "profile" (default), "query", or "full" * @param options.mode - Optional mode for memory search: "profile" (default), "query", or "full"
* @param options.addMemory - Optional mode for memory addition: "always" (default), "never" * @param options.addMemory - Optional mode for memory addition: "always" (default), "never"
@ -35,8 +35,8 @@ import {
* apiKey: process.env.OPENAI_API_KEY, * apiKey: process.env.OPENAI_API_KEY,
* }) * })
* const openaiWithSupermemory = withSupermemory(openai, { * const openaiWithSupermemory = withSupermemory(openai, {
* containerTag: "user-123", * namespace: "user-123",
* customId: "conversation-456", * id: "conversation-456",
* mode: "full", * mode: "full",
* addMemory: "always" * addMemory: "always"
* }) * })
@ -70,26 +70,26 @@ export function withSupermemory(
) )
} }
if (!options.containerTag) { if (!options.namespace) {
throw new Error( throw new Error(
"containerTag is required — provide a non-empty string to identify the user/container", "namespace is required — provide a non-empty string to identify the user or project",
) )
} }
if (!options.customId) { if (!options.id) {
throw new Error( throw new Error(
"customId is required — provide a non-empty string to group messages into a single document", "id is required — provide a non-empty string to group messages into a single document",
) )
} }
const { containerTag } = options const { namespace } = options
const verbose = options.verbose ?? false const verbose = options.verbose ?? false
const mode = options.mode ?? "profile" const mode = options.mode ?? "profile"
const addMemory = options.addMemory ?? "always" const addMemory = options.addMemory ?? "always"
const openaiWithSupermemory = createOpenAIMiddleware( const openaiWithSupermemory = createOpenAIMiddleware(
openaiClient, openaiClient,
containerTag, namespace,
{ {
...options, ...options,
verbose, verbose,

View file

@ -1,6 +1,6 @@
import type OpenAI from "openai" import type OpenAI from "openai"
import { APIPromise } from "openai/core" import { APIPromise } from "openai/core"
import Supermemory from "supermemory" import { Supermemory } from "supermemory"
import { import {
addConversation, addConversation,
type ContentPart as ConversationContentPart, type ContentPart as ConversationContentPart,
@ -10,7 +10,10 @@ import {
import { import {
replaceMemoryContext, replaceMemoryContext,
stripMemoryContext, stripMemoryContext,
supermemoryProfileSearch as sharedProfileSearch,
wrapMemoryContext, wrapMemoryContext,
type ProfileSearchResult,
type ProfileStructure,
} from "../shared" } from "../shared"
import { deduplicateMemoriesForMode } from "../tools-shared" import { deduplicateMemoriesForMode } from "../tools-shared"
import { createLogger, type Logger } from "../vercel/logger" import { createLogger, type Logger } from "../vercel/logger"
@ -41,10 +44,10 @@ const deferAPIPromise = <T>(
} }
export interface OpenAIMiddlewareOptions { export interface OpenAIMiddlewareOptions {
/** Container tag/identifier for memory search (e.g., user ID, project ID). Required. */ /** Namespace for memory search (e.g., user ID, project ID). Required. */
containerTag: string namespace: string
/** Custom ID to group messages into a single document. Required. */ /** ID that groups messages into a single document. Required. */
customId: string id: string
verbose?: boolean verbose?: boolean
mode?: "profile" | "query" | "full" mode?: "profile" | "query" | "full"
addMemory?: "always" | "never" addMemory?: "always" | "never"
@ -53,28 +56,6 @@ export interface OpenAIMiddlewareOptions {
baseUrl?: string baseUrl?: string
} }
interface SupermemoryProfileSearchResult {
id: string
memory?: string
chunk?: string
metadata: Record<string, unknown> | null
updatedAt: string
similarity: number
}
interface SupermemoryProfileSearch {
profile: {
static?: string[]
dynamic?: string[]
buckets?: Record<string, string[]>
}
searchResults?: {
results: SupermemoryProfileSearchResult[]
total: number
timing: number
}
}
const extractTextContent = (content: unknown): string => { const extractTextContent = (content: unknown): string => {
if (typeof content === "string") return content.trim() if (typeof content === "string") return content.trim()
if (!Array.isArray(content)) return "" if (!Array.isArray(content)) return ""
@ -313,7 +294,7 @@ const stripResponsesInputMemoryContexts = <T>(input: T): T => {
} }
const getSearchResultMemories = ( const getSearchResultMemories = (
results: SupermemoryProfileSearchResult[] | undefined, results: ProfileSearchResult[] | undefined,
): string[] => { ): string[] => {
return (results ?? []).flatMap((result) => { return (results ?? []).flatMap((result) => {
for (const value of [result.memory, result.chunk]) { for (const value of [result.memory, result.chunk]) {
@ -417,72 +398,21 @@ const getLastUserMessage = (
return extractTextContent(lastUserMessage?.content) return extractTextContent(lastUserMessage?.content)
} }
/** const supermemoryProfileSearch = (
* Searches for memories using the SuperMemory profile API. namespace: string,
*
* Makes a POST request to the SuperMemory API to retrieve user profile memories
* and search results based on the provided container tag and optional query text.
*
* @param containerTag - The container tag/identifier for memory search (e.g., user ID, project ID)
* @param queryText - Optional query text to search for specific memories. If empty, returns all profile memories
* @param apiKey - The Supermemory API key used to authenticate the request
* @param baseUrl - The Supermemory API base URL
* @returns Promise that resolves to the SuperMemory profile search response
* @throws {Error} When the API request fails or returns an error status
*
* @example
* ```typescript
* // Search with query
* const results = await supermemoryProfileSearch("user-123", "favorite programming language", apiKey, baseUrl)
*
* // Get all profile memories
* const profile = await supermemoryProfileSearch("user-123", "", apiKey, baseUrl)
* ```
*/
const supermemoryProfileSearch = async (
containerTag: string,
queryText: string, queryText: string,
apiKey: string, apiKey: string,
baseUrl: string, baseUrl: string,
): Promise<SupermemoryProfileSearch> => { mode: "profile" | "query" | "full",
const payload = queryText ): Promise<ProfileStructure> =>
? JSON.stringify({ sharedProfileSearch(
q: queryText, namespace,
containerTag: containerTag, queryText,
include: ["static", "dynamic"], baseUrl,
}) apiKey,
: JSON.stringify({ AbortSignal.timeout(PROFILE_REQUEST_TIMEOUT_MS),
containerTag: containerTag, mode !== "query",
include: ["static", "dynamic"], )
})
try {
const response = await fetch(`${baseUrl}/v4/profile`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: payload,
redirect: "error",
signal: AbortSignal.timeout(PROFILE_REQUEST_TIMEOUT_MS),
})
if (!response.ok) {
const errorText = await response.text().catch(() => "Unknown error")
throw new Error(
`Supermemory profile search failed: ${response.status} ${response.statusText}. ${errorText}`,
)
}
return await response.json()
} catch (error) {
if (error instanceof Error) {
throw error
}
throw new Error(`Supermemory API request failed: ${error}`)
}
}
/** /**
* Adds memory-enhanced system prompts to chat completion messages. * Adds memory-enhanced system prompts to chat completion messages.
@ -492,7 +422,7 @@ const supermemoryProfileSearch = async (
* to it. Otherwise, a new system prompt is created with the memories. * to it. Otherwise, a new system prompt is created with the memories.
* *
* @param messages - Array of chat completion message parameters * @param messages - Array of chat completion message parameters
* @param containerTag - The container tag/identifier for memory search * @param namespace - The namespace for memory search
* @param logger - Logger instance for debugging and info output * @param logger - Logger instance for debugging and info output
* @param mode - Memory search mode: "profile" (all memories), "query" (search-based), or "full" (both) * @param mode - Memory search mode: "profile" (all memories), "query" (search-based), or "full" (both)
* @param apiKey - The Supermemory API key used to authenticate the request * @param apiKey - The Supermemory API key used to authenticate the request
@ -518,7 +448,7 @@ const supermemoryProfileSearch = async (
*/ */
const addSystemPrompt = async ( const addSystemPrompt = async (
messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
containerTag: string, namespace: string,
logger: Logger, logger: Logger,
mode: "profile" | "query" | "full", mode: "profile" | "query" | "full",
apiKey: string, apiKey: string,
@ -529,17 +459,18 @@ const addSystemPrompt = async (
const queryText = mode !== "profile" ? getLastUserMessage(messages) : "" const queryText = mode !== "profile" ? getLastUserMessage(messages) : ""
const memoriesResponse = await supermemoryProfileSearch( const memoriesResponse = await supermemoryProfileSearch(
containerTag, namespace,
queryText, queryText,
apiKey, apiKey,
baseUrl, baseUrl,
mode,
) )
const memoryCountStatic = memoriesResponse.profile.static?.length || 0 const memoryCountStatic = memoriesResponse.profile.static?.length || 0
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0 const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
logger.info("Memory search completed for chat API", { logger.info("Memory search completed for chat API", {
containerTag, namespace,
memoryCountStatic, memoryCountStatic,
memoryCountDynamic, memoryCountDynamic,
queryText: queryText:
@ -644,80 +575,46 @@ const getConversationContent = (
.join("\n\n") .join("\n\n")
} }
/** // With a conversation id and messages, saves the whole conversation as one document.
* Adds a new memory to the SuperMemory system.
*
* Saves the provided content as a memory with the specified container tag and
* optional custom ID. Logs success or failure information for debugging.
*
* If customId starts with "conversation:" and messages are provided, uses the
* /v4/conversations endpoint with structured messages instead of the memories endpoint.
*
* @param client - SuperMemory client instance
* @param containerTag - The container tag/identifier for the memory
* @param content - The content to save as a memory (used for fallback)
* @param customId - Optional custom ID for the memory (e.g., conversation:456)
* @param logger - Logger instance for debugging and info output
* @param conversationMessages - Optional normalized messages (for conversation endpoint)
* @param apiKey - API key for direct conversation endpoint calls
* @param baseUrl - Base URL for API calls
* @returns Promise that resolves when memory is saved (or fails silently)
*
* @example
* ```typescript
* await addMemoryTool(
* supermemoryClient,
* "user-123",
* "User: Hello\n\nAssistant: Hi!",
* "conversation:456",
* logger,
* messages, // OpenAI messages array
* apiKey,
* baseUrl
* )
* ```
*/
const addMemoryTool = async ( const addMemoryTool = async (
client: Supermemory, client: Supermemory,
containerTag: string, namespace: string,
content: string, content: string,
customId: string | undefined, id: string | undefined,
logger: Logger, logger: Logger,
conversationMessages?: ConversationMessage[], conversationMessages?: ConversationMessage[],
apiKey?: string, apiKey?: string,
baseUrl?: string, baseUrl?: string,
): Promise<void> => { ): Promise<void> => {
try { try {
if (customId && conversationMessages && apiKey) { if (id && conversationMessages && apiKey) {
const conversationId = customId.replace("conversation:", "") const conversationDocumentId = id.replace("conversation:", "")
const response = await addConversation({ const response = await addConversation({
conversationId, id: conversationDocumentId,
messages: conversationMessages, messages: conversationMessages,
containerTags: [containerTag], namespace,
apiKey, apiKey,
baseUrl, baseUrl,
}) })
logger.info("Conversation saved successfully via /v4/conversations", { logger.info("Conversation saved successfully", {
containerTag, namespace,
customId, id,
messageCount: conversationMessages.length, messageCount: conversationMessages.length,
responseId: response.id, responseId: response.id,
}) })
return return
} }
// Fallback to old behavior for non-conversation memories const response = await client.add(namespace, {
const response = await client.add({
content, content,
containerTags: [containerTag], ...(id && { id }),
customId,
}) })
logger.info("Memory saved successfully", { logger.info("Memory saved successfully", {
containerTag, namespace,
customId, id,
contentLength: content.length, contentLength: content.length,
memoryId: response.id, memoryId: response.id,
}) })
@ -735,9 +632,9 @@ const addMemoryTool = async (
* into OpenAI chat completions and optionally saves new memories. The middleware * into OpenAI chat completions and optionally saves new memories. The middleware
* can wrap existing OpenAI clients or create new ones with SuperMemory capabilities. * can wrap existing OpenAI clients or create new ones with SuperMemory capabilities.
* *
* @param containerTag - The container tag/identifier for memory search (e.g., user ID, project ID) * @param namespace - The namespace for memory search (e.g., user ID, project ID)
* @param options - Optional configuration options for the middleware * @param options - Optional configuration options for the middleware
* @param options.customId - Optional conversation ID to group messages for contextual memory generation * @param options.id - Optional conversation ID to group messages for contextual memory generation
* @param options.verbose - Enable detailed logging of memory operations (default: false) * @param options.verbose - Enable detailed logging of memory operations (default: false)
* @param options.mode - Memory search mode: "profile" (all memories), "query" (search-based), or "full" (both) (default: "profile") * @param options.mode - Memory search mode: "profile" (all memories), "query" (search-based), or "full" (both) (default: "profile")
* @param options.addMemory - Automatic memory storage mode: "always" or "never" (default: "always") * @param options.addMemory - Automatic memory storage mode: "always" or "never" (default: "always")
@ -748,7 +645,7 @@ const addMemoryTool = async (
* @example * @example
* ```typescript * ```typescript
* const openaiWithSupermemory = createOpenAIMiddleware(openai, "user-123", { * const openaiWithSupermemory = createOpenAIMiddleware(openai, "user-123", {
* customId: "conversation-456", * id: "conversation-456",
* mode: "full", * mode: "full",
* addMemory: "always", * addMemory: "always",
* verbose: true * verbose: true
@ -758,7 +655,7 @@ const addMemoryTool = async (
*/ */
export function createOpenAIMiddleware( export function createOpenAIMiddleware(
openaiClient: OpenAI, openaiClient: OpenAI,
containerTag: string, namespace: string,
options?: OpenAIMiddlewareOptions, options?: OpenAIMiddlewareOptions,
) { ) {
const logger = createLogger(options?.verbose ?? false) const logger = createLogger(options?.verbose ?? false)
@ -772,10 +669,10 @@ export function createOpenAIMiddleware(
const baseUrl = normalizeBaseUrl(options?.baseUrl) const baseUrl = normalizeBaseUrl(options?.baseUrl)
const client = new Supermemory({ const client = new Supermemory({
apiKey, apiKey,
...(baseUrl !== "https://api.supermemory.ai" ? { baseURL: baseUrl } : {}), ...(baseUrl !== "https://api.supermemory.ai" ? { baseUrl } : {}),
}) })
const customId = options?.customId const id = options?.id
const mode = options?.mode ?? "profile" const mode = options?.mode ?? "profile"
const addMemory = options?.addMemory ?? "always" const addMemory = options?.addMemory ?? "always"
@ -789,7 +686,7 @@ export function createOpenAIMiddleware(
* and Responses APIs, reducing code duplication. * and Responses APIs, reducing code duplication.
* *
* @param queryText - The text to search for (empty string for profile-only mode) * @param queryText - The text to search for (empty string for profile-only mode)
* @param containerTag - The container tag for memory search * @param namespace - The namespace for memory search
* @param logger - Logger instance * @param logger - Logger instance
* @param mode - Memory search mode * @param mode - Memory search mode
* @param context - API context for logging differentiation * @param context - API context for logging differentiation
@ -797,23 +694,24 @@ export function createOpenAIMiddleware(
*/ */
const searchAndFormatMemories = async ( const searchAndFormatMemories = async (
queryText: string, queryText: string,
containerTag: string, namespace: string,
logger: Logger, logger: Logger,
mode: "profile" | "query" | "full", mode: "profile" | "query" | "full",
context: "chat" | "responses", context: "chat" | "responses",
) => { ) => {
const memoriesResponse = await supermemoryProfileSearch( const memoriesResponse = await supermemoryProfileSearch(
containerTag, namespace,
queryText, queryText,
apiKey, apiKey,
baseUrl, baseUrl,
mode,
) )
const memoryCountStatic = memoriesResponse.profile.static?.length || 0 const memoryCountStatic = memoriesResponse.profile.static?.length || 0
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0 const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
logger.info(`Memory search completed for ${context} API`, { logger.info(`Memory search completed for ${context} API`, {
containerTag, namespace,
memoryCountStatic, memoryCountStatic,
memoryCountDynamic, memoryCountDynamic,
queryText: queryText:
@ -889,17 +787,17 @@ export function createOpenAIMiddleware(
convertResponsesConversationMessages(cleanedInput) convertResponsesConversationMessages(cleanedInput)
const shouldPersist = const shouldPersist =
addMemory === "always" && addMemory === "always" &&
(customId (id
? hasPersistableUserConversationMessage(conversationMessages) ? hasPersistableUserConversationMessage(conversationMessages)
: Boolean(input.trim())) : Boolean(input.trim()))
const memoryCustomId = customId ? `conversation:${customId}` : undefined const documentId = id ? `conversation:${id}` : undefined
const persistResponsesInput = () => const persistResponsesInput = () =>
addMemoryTool( addMemoryTool(
client, client,
containerTag, namespace,
input, input,
memoryCustomId, documentId,
logger, logger,
conversationMessages, conversationMessages,
apiKey, apiKey,
@ -928,8 +826,8 @@ export function createOpenAIMiddleware(
} }
logger.info("Starting memory search for Responses API", { logger.info("Starting memory search for Responses API", {
containerTag, namespace,
customId, id,
mode, mode,
}) })
@ -939,13 +837,7 @@ export function createOpenAIMiddleware(
const queryText = mode !== "profile" ? input : "" const queryText = mode !== "profile" ? input : ""
operations.push( operations.push(
searchAndFormatMemories( searchAndFormatMemories(queryText, namespace, logger, mode, "responses"),
queryText,
containerTag,
logger,
mode,
"responses",
),
) )
let enhancedInstructions: string let enhancedInstructions: string
@ -999,21 +891,19 @@ export function createOpenAIMiddleware(
) )
const shouldPersist = const shouldPersist =
addMemory === "always" && addMemory === "always" &&
(customId (id
? hasPersistableUserConversationMessage(conversationMessages) ? hasPersistableUserConversationMessage(conversationMessages)
: Boolean(userMessage.trim())) : Boolean(userMessage.trim()))
const memoryContent = customId const memoryContent = id ? getConversationContent(messages) : userMessage
? getConversationContent(messages) const documentId = id ? `conversation:${id}` : undefined
: userMessage
const memoryCustomId = customId ? `conversation:${customId}` : undefined
if (mode !== "profile" && !userMessage) { if (mode !== "profile" && !userMessage) {
if (shouldPersist) { if (shouldPersist) {
await addMemoryTool( await addMemoryTool(
client, client,
containerTag, namespace,
memoryContent, memoryContent,
memoryCustomId, documentId,
logger, logger,
conversationMessages, conversationMessages,
apiKey, apiKey,
@ -1034,8 +924,8 @@ export function createOpenAIMiddleware(
} }
logger.info("Starting memory search", { logger.info("Starting memory search", {
containerTag, namespace,
customId, id,
mode, mode,
}) })
@ -1045,9 +935,9 @@ export function createOpenAIMiddleware(
operations.push( operations.push(
addMemoryTool( addMemoryTool(
client, client,
containerTag, namespace,
memoryContent, memoryContent,
memoryCustomId, documentId,
logger, logger,
conversationMessages, conversationMessages,
apiKey, apiKey,
@ -1057,7 +947,7 @@ export function createOpenAIMiddleware(
} }
operations.push( operations.push(
addSystemPrompt(messages, containerTag, logger, mode, apiKey, baseUrl), addSystemPrompt(messages, namespace, logger, mode, apiKey, baseUrl),
) )
let enhancedMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] let enhancedMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[]

View file

@ -1,21 +1,19 @@
import type OpenAI from "openai" import type OpenAI from "openai"
import Supermemory from "supermemory" import type { Supermemory } from "supermemory"
import { import {
CLIENT_OPTIONS,
DEFAULT_VALUES, DEFAULT_VALUES,
PARAMETER_DESCRIPTIONS, PARAMETER_DESCRIPTIONS,
SEARCH_LIMIT_BOUNDS, SEARCH_LIMIT_BOUNDS,
TOOL_DESCRIPTIONS, TOOL_DESCRIPTIONS,
clampSearchLimit, clampSearchLimit,
deleteDocumentByIdentifier, createToolsClient,
getContainerTags, deleteDocument,
forgetMemory,
getNamespace,
getProfileWithSearch,
} from "../tools-shared" } from "../tools-shared"
import { forgetMemoryRequest } from "../shared/forget-memory"
import type { SupermemoryToolsConfig } from "../types" import type { SupermemoryToolsConfig } from "../types"
/**
* Result types for memory operations
*/
export interface MemorySearchResult { export interface MemorySearchResult {
success: boolean success: boolean
results?: Awaited<ReturnType<Supermemory["search"]>>["results"] results?: Awaited<ReturnType<Supermemory["search"]>>["results"]
@ -31,20 +29,15 @@ export interface MemoryAddResult {
export interface ProfileResult { export interface ProfileResult {
success: boolean success: boolean
profile?: { profile?: Awaited<ReturnType<Supermemory["profile"]>>["profile"]
static: string[] searchResults?: Awaited<ReturnType<Supermemory["search"]>>["results"]
dynamic: string[]
}
searchResults?: Awaited<ReturnType<Supermemory["profile"]>>["searchResults"]
error?: string error?: string
} }
export interface DocumentListResult { export interface DocumentListResult {
success: boolean success: boolean
documents?: Awaited<ReturnType<Supermemory["documents"]["list"]>>["memories"] documents?: Awaited<ReturnType<Supermemory["list"]>>["documents"]
pagination?: Awaited< pagination?: Awaited<ReturnType<Supermemory["list"]>>["pagination"]
ReturnType<Supermemory["documents"]["list"]>
>["pagination"]
error?: string error?: string
} }
@ -56,7 +49,7 @@ export interface DocumentDeleteResult {
export interface DocumentAddResult { export interface DocumentAddResult {
success: boolean success: boolean
document?: Awaited<ReturnType<Supermemory["documents"]["add"]>> document?: Awaited<ReturnType<Supermemory["add"]>>
error?: string error?: string
} }
@ -66,9 +59,6 @@ export interface MemoryForgetResult {
error?: string error?: string
} }
/**
* Function schemas for OpenAI function calling
*/
export const memoryToolSchemas = { export const memoryToolSchemas = {
searchMemories: { searchMemories: {
name: "searchMemories", name: "searchMemories",
@ -118,10 +108,6 @@ export const memoryToolSchemas = {
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {
containerTag: {
type: "string",
description: PARAMETER_DESCRIPTIONS.containerTag,
},
query: { query: {
type: "string", type: "string",
description: PARAMETER_DESCRIPTIONS.query, description: PARAMETER_DESCRIPTIONS.query,
@ -137,10 +123,6 @@ export const memoryToolSchemas = {
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {
containerTag: {
type: "string",
description: PARAMETER_DESCRIPTIONS.containerTag,
},
limit: { limit: {
type: "number", type: "number",
description: PARAMETER_DESCRIPTIONS.limit, description: PARAMETER_DESCRIPTIONS.limit,
@ -165,10 +147,6 @@ export const memoryToolSchemas = {
type: "string", type: "string",
description: PARAMETER_DESCRIPTIONS.documentId, description: PARAMETER_DESCRIPTIONS.documentId,
}, },
containerTag: {
type: "string",
description: PARAMETER_DESCRIPTIONS.documentContainerTag,
},
}, },
required: ["documentId"], required: ["documentId"],
}, },
@ -203,10 +181,6 @@ export const memoryToolSchemas = {
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {
containerTag: {
type: "string",
description: PARAMETER_DESCRIPTIONS.containerTag,
},
memoryId: { memoryId: {
type: "string", type: "string",
description: PARAMETER_DESCRIPTIONS.memoryId, description: PARAMETER_DESCRIPTIONS.memoryId,
@ -215,39 +189,31 @@ export const memoryToolSchemas = {
type: "string", type: "string",
description: PARAMETER_DESCRIPTIONS.memoryContent, description: PARAMETER_DESCRIPTIONS.memoryContent,
}, },
reason: {
type: "string",
description: PARAMETER_DESCRIPTIONS.reason,
},
}, },
required: [], required: [],
}, },
} satisfies OpenAI.FunctionDefinition, } satisfies OpenAI.FunctionDefinition,
} as const } as const
/**
* Create a Supermemory client with configuration
*/
function createClient(apiKey: string, config?: SupermemoryToolsConfig) { function createClient(apiKey: string, config?: SupermemoryToolsConfig) {
const client = new Supermemory({ return {
apiKey, client: createToolsClient(apiKey, config),
...CLIENT_OPTIONS, namespace: getNamespace(config),
...(config?.baseUrl && { baseURL: config.baseUrl }), }
}) }
const containerTags = getContainerTags(config) function toolError(error: unknown) {
return {
return { client, containerTags } success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
/**
* Search memories function
*/
export function createSearchMemoriesFunction( export function createSearchMemoriesFunction(
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) { ) {
const { client, containerTags } = createClient(apiKey, config) const { client, namespace } = createClient(apiKey, config)
return async function searchMemories({ return async function searchMemories({
informationToGet, informationToGet,
@ -258,9 +224,8 @@ export function createSearchMemoriesFunction(
limit?: number limit?: number
}): Promise<MemorySearchResult> { }): Promise<MemorySearchResult> {
try { try {
const response = await client.search({ const response = await client.search(namespace, {
q: informationToGet, query: informationToGet,
containerTag: containerTags[0],
limit: clampSearchLimit(limit), limit: clampSearchLimit(limit),
threshold: DEFAULT_VALUES.searchThreshold, threshold: DEFAULT_VALUES.searchThreshold,
searchMode: "hybrid", searchMode: "hybrid",
@ -272,22 +237,16 @@ export function createSearchMemoriesFunction(
count: response.results?.length || 0, count: response.results?.length || 0,
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
} }
} }
/**
* Add memory function
*/
export function createAddMemoryFunction( export function createAddMemoryFunction(
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) { ) {
const { client, containerTags } = createClient(apiKey, config) const { client, namespace } = createClient(apiKey, config)
return async function addMemory({ return async function addMemory({
memory, memory,
@ -295,12 +254,9 @@ export function createAddMemoryFunction(
memory: string memory: string
}): Promise<MemoryAddResult> { }): Promise<MemoryAddResult> {
try { try {
const metadata: Record<string, string | number | boolean> = {} const response = await client.add(namespace, {
const response = await client.add({
content: memory, content: memory,
containerTags, dreaming: "instant",
...(Object.keys(metadata).length > 0 && { metadata }),
}) })
return { return {
@ -308,138 +264,92 @@ export function createAddMemoryFunction(
memory: response, memory: response,
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
} }
} }
/**
* Get profile function
*/
export function createGetProfileFunction( export function createGetProfileFunction(
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) { ) {
const { client, containerTags } = createClient(apiKey, config) const { client, namespace } = createClient(apiKey, config)
return async function getProfile({ return async function getProfile({
containerTag,
query, query,
}: { }: {
containerTag?: string
query?: string query?: string
}): Promise<ProfileResult> { }): Promise<ProfileResult> {
try { try {
const tag = containerTag || containerTags[0]
const response = await client.profile({
containerTag: tag,
...(query && { q: query }),
})
return { return {
success: true, success: true,
profile: response.profile, ...(await getProfileWithSearch(client, namespace, query)),
searchResults: response.searchResults,
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
} }
} }
/**
* List documents function
*/
export function createDocumentListFunction( export function createDocumentListFunction(
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) { ) {
const { client, containerTags } = createClient(apiKey, config) const { client, namespace } = createClient(apiKey, config)
return async function documentList({ return async function documentList({
containerTag,
limit, limit,
page, page,
}: { }: {
containerTag?: string
limit?: number limit?: number
page?: number page?: number
}): Promise<DocumentListResult> { }): Promise<DocumentListResult> {
try { try {
const scopeTags: [string, ...string[]] = containerTag const response = await client.list(namespace, "documents", {
? [containerTag]
: containerTags
const response = await client.documents.list({
containerTags: scopeTags,
limit: limit || DEFAULT_VALUES.limit, limit: limit || DEFAULT_VALUES.limit,
...(page !== undefined && { page }), ...(page !== undefined && { page }),
}) })
return { return {
success: true, success: true,
documents: response.memories, documents: response.documents,
pagination: response.pagination, pagination: response.pagination,
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
} }
} }
/**
* Delete document function
*/
export function createDocumentDeleteFunction( export function createDocumentDeleteFunction(
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) { ) {
const { client, containerTags } = createClient(apiKey, config) const { client, namespace } = createClient(apiKey, config)
return async function documentDelete({ return async function documentDelete({
documentId, documentId,
containerTag,
}: { }: {
documentId: string documentId: string
containerTag?: string
}): Promise<DocumentDeleteResult> { }): Promise<DocumentDeleteResult> {
try { try {
const scopeTags: [string, ...string[]] = containerTag await deleteDocument(client, namespace, documentId)
? [containerTag]
: containerTags
await deleteDocumentByIdentifier(client, documentId, scopeTags)
return { return {
success: true, success: true,
message: `Document ${documentId} deleted successfully`, message: `Document ${documentId} deleted successfully`,
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
} }
} }
/**
* Add document function
*/
export function createDocumentAddFunction( export function createDocumentAddFunction(
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) { ) {
const { client, containerTags } = createClient(apiKey, config) const { client, namespace } = createClient(apiKey, config)
return async function documentAdd({ return async function documentAdd({
content, content,
@ -455,9 +365,8 @@ export function createDocumentAddFunction(
if (title) metadata.title = title if (title) metadata.title = title
if (description) metadata.description = description if (description) metadata.description = description
const response = await client.documents.add({ const response = await client.add(namespace, {
content, content,
containerTags,
...(Object.keys(metadata).length > 0 && { metadata }), ...(Object.keys(metadata).length > 0 && { metadata }),
}) })
@ -466,33 +375,23 @@ export function createDocumentAddFunction(
document: response, document: response,
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
} }
} }
/**
* Forget memory function
*/
export function createMemoryForgetFunction( export function createMemoryForgetFunction(
apiKey: string, apiKey: string,
config?: SupermemoryToolsConfig, config?: SupermemoryToolsConfig,
) { ) {
const containerTags = getContainerTags(config) const { client, namespace } = createClient(apiKey, config)
return async function memoryForget({ return async function memoryForget({
containerTag,
memoryId, memoryId,
memoryContent, memoryContent,
reason,
}: { }: {
containerTag?: string
memoryId?: string memoryId?: string
memoryContent?: string memoryContent?: string
reason?: string
}): Promise<MemoryForgetResult> { }): Promise<MemoryForgetResult> {
try { try {
if (!memoryId && !memoryContent) { if (!memoryId && !memoryContent) {
@ -502,28 +401,17 @@ export function createMemoryForgetFunction(
} }
} }
const tag = containerTag || containerTags[0] await forgetMemory(client, namespace, {
...(memoryId && { id: memoryId }),
await forgetMemoryRequest( ...(memoryContent && { content: memoryContent }),
apiKey, })
{
containerTag: tag as string,
...(memoryId && { id: memoryId }),
...(memoryContent && { content: memoryContent }),
...(reason && { reason }),
},
config?.baseUrl,
)
return { return {
success: true, success: true,
message: "Memory forgotten successfully", message: "Memory forgotten successfully",
} }
} catch (error) { } catch (error) {
return { return toolError(error)
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
} }
} }

View file

@ -12,20 +12,20 @@ export class MemoryCache<T = string> {
* Generates a cache key for the current turn based on context parameters. * Generates a cache key for the current turn based on context parameters.
* Normalizes the message by trimming and collapsing whitespace. * Normalizes the message by trimming and collapsing whitespace.
* *
* @param containerTag - The container tag/user ID * @param namespace - The namespace (e.g. user ID)
* @param threadId - Optional thread/conversation ID * @param threadId - Optional thread/conversation ID
* @param mode - The memory retrieval mode * @param mode - The memory retrieval mode
* @param message - The user message content * @param message - The user message content
* @returns A unique cache key for this turn * @returns A unique cache key for this turn
*/ */
static makeTurnKey( static makeTurnKey(
containerTag: string, namespace: string,
threadId: string | undefined, threadId: string | undefined,
mode: MemoryMode, mode: MemoryMode,
message: string, message: string,
): string { ): string {
const normalizedMessage = message.trim().replace(/\s+/g, " ") const normalizedMessage = message.trim().replace(/\s+/g, " ")
return `${containerTag}:${threadId || ""}:${mode}:${normalizedMessage}` return `${namespace}:${threadId || ""}:${mode}:${normalizedMessage}`
} }
/** /**

View file

@ -1,4 +1,4 @@
import Supermemory from "supermemory" import { Supermemory } from "supermemory"
/** /**
* Normalizes a base URL by removing trailing slashes. * Normalizes a base URL by removing trailing slashes.
@ -36,7 +36,7 @@ export function createSupermemoryClient(
return new Supermemory({ return new Supermemory({
apiKey: options.apiKey, apiKey: options.apiKey,
...(normalizedBaseUrl !== "https://api.supermemory.ai" ...(normalizedBaseUrl !== "https://api.supermemory.ai"
? { baseURL: normalizedBaseUrl } ? { baseUrl: normalizedBaseUrl }
: {}), : {}),
}) })
} }

View file

@ -1,45 +0,0 @@
const DEFAULT_BASE_URL = "https://api.supermemory.ai"
const FETCH_TIMEOUT_MS = 30_000
export interface ForgetMemoryParams {
containerTag: string
id?: string
content?: string
reason?: string
}
export interface ForgetMemoryRequestOptions {
signal?: AbortSignal
}
/**
* Marks a memory as forgotten via `DELETE /v4/memories`.
*
* The supermemory SDK version this package depends on (v3) has no
* `memories.forget` method, so the endpoint is called directly — the same
* pattern the middleware already uses for `/v4/profile` and
* `/v4/conversations`.
*/
export async function forgetMemoryRequest(
apiKey: string,
params: ForgetMemoryParams,
baseUrl: string = DEFAULT_BASE_URL,
options?: ForgetMemoryRequestOptions,
): Promise<void> {
const response = await fetch(`${baseUrl}/v4/memories`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(params),
signal: options?.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS),
})
if (!response.ok) {
const errorText = await response.text().catch(() => "Unknown error")
throw new Error(
`Supermemory forget memory failed: ${response.status} ${response.statusText}. ${errorText}`,
)
}
}

View file

@ -8,6 +8,7 @@ export type {
AddMemoryMode, AddMemoryMode,
Logger, Logger,
ProfileStructure, ProfileStructure,
ProfileEntry,
ProfileMarkdownData, ProfileMarkdownData,
SupermemoryBaseOptions, SupermemoryBaseOptions,
} from "./types" } from "./types"
@ -35,6 +36,7 @@ export {
// Memory client // Memory client
export { export {
PROFILE_SEARCH_THRESHOLD,
supermemoryProfileSearch, supermemoryProfileSearch,
buildMemoriesText, buildMemoriesText,
extractQueryText, extractQueryText,

View file

@ -1,19 +1,22 @@
import { afterEach, describe, expect, it, vi } from "vitest" import { afterEach, describe, expect, it, vi } from "vitest"
import { buildMemoriesText } from "./memory-client" import { buildMemoriesText } from "./memory-client"
import { createLogger } from "./logger" import { createLogger } from "./logger"
import { profileBody, searchBody, v5Router } from "../../test/v5-fetch"
const API_KEY = "sm_test_key" const API_KEY = "sm_test_key"
const BASE_URL = "https://api.supermemory.ai" const BASE_URL = "https://api.supermemory.ai"
const CONTAINER_TAG = "user-123" const NAMESPACE = "user-123"
const logger = createLogger(false) const logger = createLogger(false)
/** Stubs `/v4/profile` so the injected prompt can be asserted without network access. */ // Stubs the v5 profile and search routes so the injected prompt can be asserted offline.
function mockProfileResponse(body: unknown) { function mockV5(profile: string[], search: string[]) {
const fetchMock = vi.fn().mockResolvedValue({ const fetchMock = vi.fn(
ok: true, v5Router({
json: async () => body, profile: () => profileBody(profile),
}) search: () => searchBody(search),
}),
)
vi.stubGlobal("fetch", fetchMock) vi.stubGlobal("fetch", fetchMock)
return fetchMock return fetchMock
} }
@ -27,16 +30,13 @@ describe("buildMemoriesText", () => {
// results against it would drop a fact present in both, leaving the model // results against it would drop a fact present in both, leaving the model
// with nothing. // with nothing.
it("injects a search result that also exists in the profile in query mode", async () => { it("injects a search result that also exists in the profile in query mode", async () => {
mockProfileResponse({ const fetchMock = mockV5(
profile: { ["User is allergic to peanuts"],
static: [{ memory: "User is allergic to peanuts" }], ["User is allergic to peanuts"],
dynamic: [], )
},
searchResults: { results: [{ memory: "User is allergic to peanuts" }] },
})
const memories = await buildMemoriesText({ const memories = await buildMemoriesText({
containerTag: CONTAINER_TAG, namespace: NAMESPACE,
queryText: "what should I avoid eating?", queryText: "what should I avoid eating?",
mode: "query", mode: "query",
baseUrl: BASE_URL, baseUrl: BASE_URL,
@ -45,24 +45,18 @@ describe("buildMemoriesText", () => {
}) })
expect(memories).toContain("User is allergic to peanuts") expect(memories).toContain("User is allergic to peanuts")
// Query mode never shows the profile, so it is not fetched.
expect(fetchMock).toHaveBeenCalledTimes(1)
}) })
it("does not repeat a profile memory in the search results in full mode", async () => { it("does not repeat a profile memory in the search results in full mode", async () => {
mockProfileResponse({ mockV5(
profile: { ["User is allergic to peanuts"],
static: [{ memory: "User is allergic to peanuts" }], ["User is allergic to peanuts", "User prefers async/await"],
dynamic: [], )
},
searchResults: {
results: [
{ memory: "User is allergic to peanuts" },
{ memory: "User prefers async/await" },
],
},
})
const memories = await buildMemoriesText({ const memories = await buildMemoriesText({
containerTag: CONTAINER_TAG, namespace: NAMESPACE,
queryText: "what should I avoid eating?", queryText: "what should I avoid eating?",
mode: "full", mode: "full",
baseUrl: BASE_URL, baseUrl: BASE_URL,

View file

@ -10,63 +10,67 @@ import type {
ProfileStructure, ProfileStructure,
PromptTemplate, PromptTemplate,
} from "./types" } from "./types"
import { SupermemoryError } from "supermemory"
import { createSupermemoryClient } from "./context"
import { import {
convertProfileToMarkdown, convertProfileToMarkdown,
defaultPromptTemplate, defaultPromptTemplate,
} from "./prompt-builder" } from "./prompt-builder"
/** function describeSupermemoryError(error: unknown): string {
* Fetches profile and search results from the Supermemory API. if (error instanceof SupermemoryError && error.statusCode !== undefined) {
* return `${error.statusCode}. ${error.message}`
* @param containerTag - The container tag/user ID for scoping memories }
* @param queryText - Optional query text for semantic search return error instanceof Error ? error.message : String(error)
* @param baseUrl - The API base URL }
* @param apiKey - The API key for authentication
* @param signal - Optional AbortSignal to cancel the request (e.g. retrieval timeout) // Matches the v4 profile search defaults (memories mode, 0.6 threshold).
* @returns The profile structure with static, dynamic, and search results export const PROFILE_SEARCH_THRESHOLD = 0.6
*/
export const supermemoryProfileSearch = async ( export const supermemoryProfileSearch = async (
containerTag: string, namespace: string,
queryText: string, queryText: string,
baseUrl: string, baseUrl: string,
apiKey: string, apiKey: string,
signal?: AbortSignal, signal?: AbortSignal,
includeProfile = true,
): Promise<ProfileStructure> => { ): Promise<ProfileStructure> => {
const payload = queryText const client = createSupermemoryClient({ apiKey, baseUrl })
? JSON.stringify({ // No retries: this runs before the LLM call, and the raw v4 fetch never retried.
q: queryText, const requestOptions = {
containerTag: containerTag, maxRetries: 0,
include: ["static", "dynamic"], ...(signal && { abortSignal: signal }),
}) }
: JSON.stringify({
containerTag: containerTag,
include: ["static", "dynamic"],
})
try { const [profileResponse, searchResponse] = await Promise.all([
const response = await fetch(`${baseUrl}/v4/profile`, { includeProfile
method: "POST", ? client.profile(namespace, undefined, requestOptions)
headers: { : undefined,
"Content-Type": "application/json", queryText
Authorization: `Bearer ${apiKey}`, ? client.search(
namespace,
{
query: queryText,
searchMode: "memories",
threshold: PROFILE_SEARCH_THRESHOLD,
},
requestOptions,
)
: undefined,
]).catch((error: unknown) => {
throw new Error(
`Supermemory profile search failed: ${describeSupermemoryError(error)}`,
)
})
return {
profile: profileResponse?.profile ?? { static: [], dynamic: [] },
...(searchResponse && {
searchResults: {
results: searchResponse.results,
timing: searchResponse.searchTime,
}, },
body: payload, }),
...(signal ? { signal } : {}),
})
if (!response.ok) {
const errorText = await response.text().catch(() => "Unknown error")
throw new Error(
`Supermemory profile search failed: ${response.status} ${response.statusText}. ${errorText}`,
)
}
return await response.json()
} catch (error) {
if (error instanceof Error) {
throw error
}
throw new Error(`Supermemory API request failed: ${error}`)
} }
} }
@ -74,7 +78,7 @@ export const supermemoryProfileSearch = async (
* Options for building memories text. * Options for building memories text.
*/ */
export interface BuildMemoriesTextOptions { export interface BuildMemoriesTextOptions {
containerTag: string namespace: string
queryText: string queryText: string
mode: MemoryMode mode: MemoryMode
baseUrl: string baseUrl: string
@ -95,7 +99,7 @@ export const buildMemoriesText = async (
options: BuildMemoriesTextOptions, options: BuildMemoriesTextOptions,
): Promise<string> => { ): Promise<string> => {
const { const {
containerTag, namespace,
queryText, queryText,
mode, mode,
baseUrl, baseUrl,
@ -106,18 +110,19 @@ export const buildMemoriesText = async (
} = options } = options
const memoriesResponse = await supermemoryProfileSearch( const memoriesResponse = await supermemoryProfileSearch(
containerTag, namespace,
queryText, queryText,
baseUrl, baseUrl,
apiKey, apiKey,
signal, signal,
mode !== "query",
) )
const memoryCountStatic = memoriesResponse.profile.static?.length || 0 const memoryCountStatic = memoriesResponse.profile.static?.length || 0
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0 const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
logger.info("Memory search completed", { logger.info("Memory search completed", {
containerTag, namespace,
memoryCountStatic, memoryCountStatic,
memoryCountDynamic, memoryCountDynamic,
queryText: queryText:

View file

@ -1,3 +1,5 @@
import type { SearchResponseResultsItem } from "supermemory"
/** /**
* Data provided to the prompt template function for customizing memory injection. * Data provided to the prompt template function for customizing memory injection.
*/ */
@ -22,14 +24,13 @@ export interface MemoryPromptData {
searchResults: MemorySearchResult[] searchResults: MemorySearchResult[]
} }
/** A raw query result returned inside `/v4/profile.searchResults.results`. */ /** A raw v5 search result, as returned by `search(namespace)`. */
export interface ProfileSearchResult { export type ProfileSearchResult = SearchResponseResultsItem
/** One profile memory, as returned by `profile(namespace)`. */
export interface ProfileEntry {
id: string id: string
memory?: string memory: string
chunk?: string
metadata: Record<string, unknown> | null
updatedAt: string
similarity: number
} }
/** A visible, deduplicated query result provided to prompt templates. */ /** A visible, deduplicated query result provided to prompt templates. */
@ -81,31 +82,19 @@ export interface Logger {
error: (message: string, data?: unknown) => void error: (message: string, data?: unknown) => void
} }
/** /** v5 profile plus an optional memories search for the current query. */
* Response structure from the Supermemory profile API.
*/
export interface ProfileStructure { export interface ProfileStructure {
profile: { profile: {
/** /** Core, stable facts (name, profession, long-term preferences). */
* Core, stable facts about the user that rarely change. static?: ProfileEntry[]
* Examples: name, profession, long-term preferences, goals. /** Recently learned or frequently updated context. */
*/ dynamic?: ProfileEntry[]
static?: string[]
/**
* Recently learned or frequently updated information about the user.
* Examples: current projects, recent interests, ongoing topics.
*/
dynamic?: string[]
/** Memories grouped by custom profile bucket. */ /** Memories grouped by custom profile bucket. */
buckets?: Record<string, string[]> buckets?: Record<string, ProfileEntry[]>
} }
searchResults?: { searchResults?: {
/**
* Memories retrieved based on semantic similarity to the current query.
* Most relevant to the immediate conversation context.
*/
results: ProfileSearchResult[] results: ProfileSearchResult[]
total: number /** Server-side search duration in milliseconds. */
timing: number timing: number
} }
} }

View file

@ -1,38 +1,55 @@
import { beforeEach, describe, expect, it, vi } from "vitest" import { beforeEach, describe, expect, it, vi } from "vitest"
// Mock the Supermemory SDK (same pattern as claude-memory.test.ts) so tool // Mock the v5 SDK so tool executions are verified without network access.
// executions can be verified deterministically without network access. const sdk = vi.hoisted(() => {
const documentsDeleteBulk = vi.fn() class NotFoundError extends Error {
const documentsGet = vi.fn() statusCode = 404
const documentsList = vi.fn() }
const clientAdd = vi.fn() return {
const clientSearch = vi.fn() NotFoundError,
const clientOptions: unknown[] = [] add: vi.fn(),
search: vi.fn(),
profile: vi.fn(),
list: vi.fn(),
documentsGet: vi.fn(),
documentsUpdate: vi.fn(),
documentsDelete: vi.fn(),
forget: vi.fn(),
forgetMatching: vi.fn(),
clientOptions: [] as unknown[],
}
})
vi.mock("supermemory", () => { vi.mock("supermemory", () => {
class Supermemory {
constructor(options: unknown) {
sdk.clientOptions.push(options)
}
add = sdk.add
search = sdk.search
profile = sdk.profile
list = sdk.list
documents = {
get: sdk.documentsGet,
update: sdk.documentsUpdate,
delete: sdk.documentsDelete,
}
memories = { forget: sdk.forget, forgetMatching: sdk.forgetMatching }
}
return { return {
default: class MockSupermemory { default: Supermemory,
constructor(options: unknown) { Supermemory,
clientOptions.push(options) NotFoundError: sdk.NotFoundError,
} SupermemoryError: Error,
add = clientAdd
search = clientSearch
documents = {
deleteBulk: documentsDeleteBulk,
get: documentsGet,
list: documentsList,
add: vi.fn(),
}
},
} }
}) })
import * as aiSdk from "./ai-sdk" import * as aiSdk from "./ai-sdk"
import { ClaudeMemoryTool } from "./claude-memory" import { ClaudeMemoryTool } from "./claude-memory"
import { forgetMemoryRequest } from "./shared/forget-memory"
import * as openAi from "./openai/tools" import * as openAi from "./openai/tools"
const API_KEY = "sm_test_key" const API_KEY = "sm_test_key"
const DEFAULT_NS = "sm_project_default"
type ToolWithExecute = { execute: (args: Record<string, unknown>) => unknown } type ToolWithExecute = { execute: (args: Record<string, unknown>) => unknown }
@ -41,28 +58,39 @@ function executeTool(tool: unknown, args: Record<string, unknown>) {
} }
beforeEach(() => { beforeEach(() => {
documentsDeleteBulk.mockReset().mockResolvedValue({ sdk.add.mockReset().mockResolvedValue({ id: "doc_new", status: "queued" })
success: true, sdk.search.mockReset().mockResolvedValue({ results: [], searchTime: 1 })
deletedCount: 1, sdk.profile.mockReset().mockResolvedValue({
errors: [], profile: {
static: [{ id: "mem_s", memory: "Likes tea" }],
dynamic: [],
buckets: {},
},
}) })
documentsGet.mockReset().mockResolvedValue({ sdk.list.mockReset().mockResolvedValue({
id: "doc_123", documents: [{ id: "doc_1", title: "Doc one" }],
customId: "doc_123", chunks: [],
containerTags: ["sm_project_default"], memories: [],
})
documentsList.mockReset().mockResolvedValue({
memories: [{ id: "doc_1", title: "Doc one" }],
pagination: { currentPage: 1, totalItems: 1, totalPages: 1 }, pagination: { currentPage: 1, totalItems: 1, totalPages: 1 },
}) })
clientAdd.mockReset().mockResolvedValue({ id: "doc_new" }) sdk.documentsGet.mockReset().mockResolvedValue({
clientSearch.mockReset().mockResolvedValue({ results: [] }) id: "doc_123",
clientOptions.length = 0 metadata: {},
vi.unstubAllGlobals() system: { status: "done" },
})
sdk.documentsUpdate
.mockReset()
.mockResolvedValue({ id: "doc", status: "queued" })
sdk.documentsDelete.mockReset().mockResolvedValue({ count: 1, errors: [] })
sdk.forget
.mockReset()
.mockResolvedValue({ count: 1, matches: [], errors: [] })
sdk.forgetMatching.mockReset()
sdk.clientOptions.length = 0
}) })
describe("searchMemories", () => { describe("searchMemories", () => {
it("ai-sdk variant clamps an oversized limit before calling the SDK", async () => { it("ai-sdk variant clamps an oversized limit and keeps v4 search defaults", async () => {
const tool = aiSdk.searchMemoriesTool(API_KEY) const tool = aiSdk.searchMemoriesTool(API_KEY)
const result = (await executeTool(tool, { const result = (await executeTool(tool, {
informationToGet: "coffee order", informationToGet: "coffee order",
@ -70,9 +98,12 @@ describe("searchMemories", () => {
})) as { success: boolean } })) as { success: boolean }
expect(result.success).toBe(true) expect(result.success).toBe(true)
expect(clientSearch).toHaveBeenCalledWith( expect(sdk.search).toHaveBeenCalledWith(DEFAULT_NS, {
expect.objectContaining({ q: "coffee order", limit: 50 }), query: "coffee order",
) limit: 50,
threshold: 0.6,
searchMode: "hybrid",
})
}) })
it("ai-sdk schema rejects out-of-range limits", () => { it("ai-sdk schema rejects out-of-range limits", () => {
@ -90,43 +121,135 @@ describe("searchMemories", () => {
).toBe(true) ).toBe(true)
}) })
it("openai variant clamps a non-positive limit before calling the SDK", async () => { it("openai variant clamps a non-positive limit and uses the configured namespace", async () => {
const search = openAi.createSearchMemoriesFunction(API_KEY) const search = openAi.createSearchMemoriesFunction(API_KEY, {
namespace: "user_1",
})
await search({ informationToGet: "coffee order", limit: 0 }) await search({ informationToGet: "coffee order", limit: 0 })
expect(clientSearch).toHaveBeenCalledWith( expect(sdk.search).toHaveBeenCalledWith(
"user_1",
expect.objectContaining({ limit: 1 }), expect.objectContaining({ limit: 1 }),
) )
}) })
it("creates SDK clients with a bounded timeout and retry budget", () => { it("creates SDK clients with a bounded timeout and retry budget", () => {
aiSdk.searchMemoriesTool(API_KEY) aiSdk.searchMemoriesTool(API_KEY, { baseUrl: "https://example.test" })
openAi.createSearchMemoriesFunction(API_KEY) openAi.createSearchMemoriesFunction(API_KEY)
expect(clientOptions).toHaveLength(2) expect(sdk.clientOptions).toHaveLength(2)
for (const options of clientOptions) { expect(sdk.clientOptions[0]).toEqual(
expect(options).toEqual( expect.objectContaining({
expect.objectContaining({ timeout: 30_000, maxRetries: 2 }), timeoutInSeconds: 30,
) maxRetries: 2,
} baseUrl: "https://example.test",
}),
)
expect(sdk.clientOptions[1]).toEqual(
expect.objectContaining({ timeoutInSeconds: 30, maxRetries: 2 }),
)
})
})
describe("addMemory and documentAdd", () => {
it("addMemory writes one instant document to the namespace", async () => {
const tool = aiSdk.addMemoryTool(API_KEY, { namespace: "user_1" })
await executeTool(tool, { memory: "Prefers tea" })
expect(sdk.add).toHaveBeenCalledWith("user_1", {
content: "Prefers tea",
dreaming: "instant",
})
})
it("documentAdd forwards title and description as metadata", async () => {
const documentAdd = openAi.createDocumentAddFunction(API_KEY)
await documentAdd({ content: "notes", title: "T", description: "D" })
expect(sdk.add).toHaveBeenCalledWith(DEFAULT_NS, {
content: "notes",
metadata: { title: "T", description: "D" },
})
})
})
describe("getProfile", () => {
it("returns the profile without searching when no query is given", async () => {
const tool = aiSdk.getProfileTool(API_KEY)
const result = (await executeTool(tool, {})) as Record<string, unknown>
expect(result.success).toBe(true)
expect(result.searchResults).toBeUndefined()
expect(sdk.profile).toHaveBeenCalledWith(DEFAULT_NS)
expect(sdk.search).not.toHaveBeenCalled()
})
it("runs a separate memories search for a query", async () => {
const getProfile = openAi.createGetProfileFunction(API_KEY)
const result = await getProfile({ query: "drinks" })
expect(result.success).toBe(true)
expect(result.searchResults).toEqual([])
expect(sdk.search).toHaveBeenCalledWith(DEFAULT_NS, {
query: "drinks",
searchMode: "memories",
threshold: 0.6,
})
}) })
}) })
describe("documentDelete", () => { describe("documentDelete", () => {
it("ai-sdk variant passes the document id string to the SDK", async () => { it("checks the document in the namespace, then deletes it", async () => {
const tool = aiSdk.documentDeleteTool(API_KEY) const tool = aiSdk.documentDeleteTool(API_KEY)
const result = (await executeTool(tool, { documentId: "doc_123" })) as { const result = (await executeTool(tool, { documentId: "doc_123" })) as {
success: boolean success: boolean
} }
expect(result.success).toBe(true) expect(result.success).toBe(true)
expect(documentsGet).toHaveBeenCalledWith("doc_123") expect(sdk.documentsGet).toHaveBeenCalledWith(DEFAULT_NS, "doc_123")
expect(documentsDeleteBulk).toHaveBeenCalledWith({ ids: ["doc_123"] }) expect(sdk.documentsDelete).toHaveBeenCalledWith(DEFAULT_NS, {
ids: ["doc_123"],
})
})
it("refuses to delete a document that is still processing", async () => {
sdk.documentsGet.mockResolvedValue({
id: "doc_123",
metadata: {},
system: { status: "embedding" },
})
const documentDelete = openAi.createDocumentDeleteFunction(API_KEY)
const result = await documentDelete({ documentId: "doc_123" })
expect(result.success).toBe(false)
expect(result.error).toContain("embedding")
expect(sdk.documentsDelete).not.toHaveBeenCalled()
})
it("reports not-found documents", async () => {
sdk.documentsGet.mockRejectedValue(new sdk.NotFoundError("missing"))
const documentDelete = openAi.createDocumentDeleteFunction(API_KEY)
const result = await documentDelete({ documentId: "nope" })
expect(result.success).toBe(false)
expect(result.error).toContain("not found")
})
it("surfaces per-document delete errors", async () => {
sdk.documentsDelete.mockResolvedValue({
count: 0,
errors: [{ id: "doc_123", error: "locked" }],
})
const documentDelete = openAi.createDocumentDeleteFunction(API_KEY)
const result = await documentDelete({ documentId: "doc_123" })
expect(result.success).toBe(false)
expect(result.error).toContain("locked")
}) })
}) })
describe("documentList", () => { describe("documentList", () => {
it("ai-sdk variant returns the SDK's memories array as documents", async () => { it("ai-sdk variant returns the v5 documents array", async () => {
const tool = aiSdk.documentListTool(API_KEY) const tool = aiSdk.documentListTool(API_KEY)
const result = (await executeTool(tool, {})) as { const result = (await executeTool(tool, {})) as {
success: boolean success: boolean
@ -142,136 +265,140 @@ describe("documentList", () => {
const result = await documentList({ limit: 5, page: 3 }) const result = await documentList({ limit: 5, page: 3 })
expect(result.success).toBe(true) expect(result.success).toBe(true)
expect(result.documents).toEqual([{ id: "doc_1", title: "Doc one" }]) expect(sdk.list).toHaveBeenCalledWith(DEFAULT_NS, "documents", {
expect(documentsList).toHaveBeenCalledWith( limit: 5,
expect.objectContaining({ limit: 5, page: 3 }), page: 3,
) })
}) })
}) })
describe("memoryForget", () => { describe("memoryForget", () => {
function stubFetch(response = new Response(null, { status: 200 })) { it("forgets an exact memory ID", async () => {
const fetchMock = vi.fn().mockResolvedValue(response) const memoryForget = openAi.createMemoryForgetFunction(API_KEY, {
vi.stubGlobal("fetch", fetchMock) namespace: "user_1",
return fetchMock
}
it("issues DELETE /v4/memories with the forget payload", async () => {
const fetchMock = stubFetch()
await forgetMemoryRequest(API_KEY, {
containerTag: "user_1",
id: "mem_1",
reason: "outdated",
}) })
const result = await memoryForget({ memoryId: "mem_1" })
expect(fetchMock).toHaveBeenCalledTimes(1) expect(result.success).toBe(true)
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit] expect(sdk.forget).toHaveBeenCalledWith("user_1", { ids: ["mem_1"] })
expect(url).toBe("https://api.supermemory.ai/v4/memories") expect(sdk.forgetMatching).not.toHaveBeenCalled()
expect(init.method).toBe("DELETE")
expect(init.headers).toMatchObject({
Authorization: `Bearer ${API_KEY}`,
})
expect(JSON.parse(init.body as string)).toEqual({
containerTag: "user_1",
id: "mem_1",
reason: "outdated",
})
expect(init.signal).toBeInstanceOf(AbortSignal)
}) })
it("uses a caller-provided signal instead of creating a timeout", async () => { it("forgets by content via a dry-run preview, keeping only exact matches", async () => {
const fetchMock = stubFetch() sdk.forgetMatching.mockResolvedValue({
const controller = new AbortController() count: 2,
matches: [
await forgetMemoryRequest( { id: "mem_exact", memory: "Stale fact" },
API_KEY, { id: "mem_close", memory: "Stale fact about tea" },
{ containerTag: "user_1", id: "mem_1" }, ],
undefined, errors: [],
{ signal: controller.signal },
)
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(init.signal).toBe(controller.signal)
})
it("throws a descriptive error on non-2xx responses", async () => {
stubFetch(new Response("nope", { status: 401, statusText: "Unauthorized" }))
await expect(
forgetMemoryRequest(API_KEY, { containerTag: "user_1", id: "mem_1" }),
).rejects.toThrow(/401/)
})
it("ai-sdk tool forgets by content through the endpoint", async () => {
const fetchMock = stubFetch()
const tool = aiSdk.memoryForgetTool(API_KEY, {
containerTags: ["user_2"],
}) })
const tool = aiSdk.memoryForgetTool(API_KEY, { namespace: "user_2" })
const result = (await executeTool(tool, { const result = (await executeTool(tool, {
memoryContent: "stale fact", memoryContent: "stale fact",
})) as { success: boolean } })) as { success: boolean }
expect(result.success).toBe(true) expect(result.success).toBe(true)
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit] expect(sdk.forgetMatching).toHaveBeenCalledWith("user_2", {
expect(JSON.parse(init.body as string)).toEqual({ query: "stale fact",
containerTag: "user_2", dryRun: true,
content: "stale fact",
}) })
expect(sdk.forget).toHaveBeenCalledWith("user_2", { ids: ["mem_exact"] })
}) })
it("openai tool surfaces endpoint failures as tool errors", async () => { it("does not forget anything when no preview match is exact", async () => {
stubFetch(new Response("boom", { status: 500, statusText: "Server Error" })) sdk.forgetMatching.mockResolvedValue({
count: 1,
matches: [{ id: "mem_close", memory: "Something else" }],
errors: [],
})
const memoryForget = openAi.createMemoryForgetFunction(API_KEY) const memoryForget = openAi.createMemoryForgetFunction(API_KEY)
const result = await memoryForget({ memoryContent: "stale fact" })
expect(result.success).toBe(false)
expect(sdk.forget).not.toHaveBeenCalled()
})
it("surfaces forget errors as tool errors", async () => {
sdk.forget.mockResolvedValue({
count: 0,
matches: [],
errors: [{ id: "mem_9", error: "Memory not found" }],
})
const memoryForget = openAi.createMemoryForgetFunction(API_KEY)
const result = await memoryForget({ memoryId: "mem_9" }) const result = await memoryForget({ memoryId: "mem_9" })
expect(result.success).toBe(false) expect(result.success).toBe(false)
expect(result.error).toMatch(/500/) expect(result.error).toContain("Memory not found")
}) })
it("still requires an id or content", async () => { it("still requires an id or content", async () => {
const fetchMock = stubFetch()
const memoryForget = openAi.createMemoryForgetFunction(API_KEY) const memoryForget = openAi.createMemoryForgetFunction(API_KEY)
const result = await memoryForget({}) const result = await memoryForget({})
expect(result.success).toBe(false) expect(result.success).toBe(false)
expect(fetchMock).not.toHaveBeenCalled() expect(sdk.forget).not.toHaveBeenCalled()
}) })
}) })
describe("ClaudeMemoryTool", () => { describe("ClaudeMemoryTool", () => {
const FILE_PATH = "/memories/prefs.txt" const FILE_PATH = "/memories/prefs.txt"
const CUSTOM_ID = "memories_prefs_txt" const FILE_ID = "memories_prefs_txt"
const DOCUMENT_ID = "doc_file_1"
function mockFileDocument(content: string) { function mockFileDocument(content: string) {
const metadata = { sdk.documentsGet.mockResolvedValue({
claude_memory_type: "file", id: "doc_file_1",
file_path: FILE_PATH,
}
documentsList.mockResolvedValue({
memories: [
{
id: DOCUMENT_ID,
customId: CUSTOM_ID,
containerTags: ["claude_memory"],
metadata,
},
],
pagination: { currentPage: 1, totalItems: 1, totalPages: 1 },
})
documentsGet.mockResolvedValue({
id: DOCUMENT_ID,
customId: CUSTOM_ID,
containerTags: ["sm_project_default", "claude_memory"],
content, content,
metadata, metadata: {
source: "claude-memory",
claude_memory_type: "file",
file_path: FILE_PATH,
},
system: { status: "done" },
}) })
} }
it("create writes a new file tagged with source claude-memory", async () => {
sdk.documentsUpdate.mockRejectedValue(new sdk.NotFoundError("missing"))
const tool = new ClaudeMemoryTool(API_KEY, { namespace: "user_1" })
const result = await tool.handleCommand({
command: "create",
path: FILE_PATH,
file_text: "hello",
})
expect(result.success).toBe(true)
expect(sdk.add).toHaveBeenCalledWith(
"user_1",
expect.objectContaining({
id: FILE_ID,
content: "hello",
metadata: expect.objectContaining({
source: "claude-memory",
file_path: FILE_PATH,
}),
}),
)
})
it("create overwrites an existing file via update, not append", async () => {
const tool = new ClaudeMemoryTool(API_KEY)
await tool.handleCommand({
command: "create",
path: FILE_PATH,
file_text: "replaced",
})
expect(sdk.documentsUpdate).toHaveBeenCalledWith(
DEFAULT_NS,
FILE_ID,
expect.objectContaining({ content: "replaced" }),
)
expect(sdk.add).not.toHaveBeenCalled()
})
it("str_replace accepts an empty new_str to delete text", async () => { it("str_replace accepts an empty new_str to delete text", async () => {
mockFileDocument("keep this\nremove this\n") mockFileDocument("keep this\nremove this\n")
const tool = new ClaudeMemoryTool(API_KEY) const tool = new ClaudeMemoryTool(API_KEY)
@ -284,7 +411,9 @@ describe("ClaudeMemoryTool", () => {
}) })
expect(result.success).toBe(true) expect(result.success).toBe(true)
expect(clientAdd).toHaveBeenCalledWith( expect(sdk.documentsUpdate).toHaveBeenCalledWith(
DEFAULT_NS,
FILE_ID,
expect.objectContaining({ content: "keep this\n" }), expect.objectContaining({ content: "keep this\n" }),
) )
}) })
@ -314,11 +443,31 @@ describe("ClaudeMemoryTool", () => {
}) })
expect(result.success).toBe(true) expect(result.success).toBe(true)
expect(clientAdd).toHaveBeenCalledWith( expect(sdk.documentsUpdate).toHaveBeenCalledWith(
DEFAULT_NS,
FILE_ID,
expect.objectContaining({ content: "line1\n\nline2" }), expect.objectContaining({ content: "line1\n\nline2" }),
) )
}) })
it("ignores documents not written by claude-memory", async () => {
sdk.documentsGet.mockResolvedValue({
id: "doc_other",
content: "secret",
metadata: { file_path: FILE_PATH },
system: { status: "done" },
})
const tool = new ClaudeMemoryTool(API_KEY)
const result = await tool.handleCommand({
command: "view",
path: FILE_PATH,
})
expect(result.success).toBe(false)
expect(result.error).toContain("File not found")
})
it("delete actually deletes the backing document", async () => { it("delete actually deletes the backing document", async () => {
mockFileDocument("contents") mockFileDocument("contents")
const tool = new ClaudeMemoryTool(API_KEY) const tool = new ClaudeMemoryTool(API_KEY)
@ -329,11 +478,14 @@ describe("ClaudeMemoryTool", () => {
}) })
expect(result.success).toBe(true) expect(result.success).toBe(true)
expect(documentsDeleteBulk).toHaveBeenCalledWith({ ids: [DOCUMENT_ID] }) expect(sdk.documentsDelete).toHaveBeenCalledWith(DEFAULT_NS, {
ids: [FILE_ID],
})
}) })
it("rename removes the old document after creating the new one", async () => { it("rename removes the old document after creating the new one", async () => {
mockFileDocument("contents") mockFileDocument("contents")
sdk.documentsUpdate.mockRejectedValue(new sdk.NotFoundError("missing"))
const tool = new ClaudeMemoryTool(API_KEY) const tool = new ClaudeMemoryTool(API_KEY)
const result = await tool.handleCommand({ const result = await tool.handleCommand({
@ -343,9 +495,39 @@ describe("ClaudeMemoryTool", () => {
}) })
expect(result.success).toBe(true) expect(result.success).toBe(true)
expect(clientAdd).toHaveBeenCalledWith( expect(sdk.add).toHaveBeenCalledWith(
expect.objectContaining({ customId: "memories_renamed_txt" }), DEFAULT_NS,
expect.objectContaining({ id: "memories_renamed_txt" }),
)
expect(sdk.documentsDelete).toHaveBeenCalledWith(DEFAULT_NS, {
ids: [FILE_ID],
})
})
it("lists a directory with a typed v5 filter", async () => {
sdk.list.mockResolvedValue({
documents: [
{ id: "a", metadata: { file_path: "/memories/a.txt" } },
{ id: "b", metadata: { file_path: "/memories/sub/b.txt" } },
],
chunks: [],
memories: [],
pagination: { currentPage: 1, totalItems: 2, totalPages: 1 },
})
const tool = new ClaudeMemoryTool(API_KEY)
const result = await tool.handleCommand({
command: "view",
path: "/memories",
})
expect(result.content).toBe("Directory: /memories/\n- sub/\n- a.txt")
expect(sdk.list).toHaveBeenCalledWith(
DEFAULT_NS,
"documents",
expect.objectContaining({
filter: expect.objectContaining({ operator: "and" }),
}),
) )
expect(documentsDeleteBulk).toHaveBeenCalledWith({ ids: [DOCUMENT_ID] })
}) })
}) })

View file

@ -0,0 +1,39 @@
export const TOOL_DESCRIPTIONS = {
searchMemories:
"Search the configured namespace 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 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 the user profile for the configured namespace. The profile contains static memories (permanent facts) and dynamic memories (recent context); each entry has an id usable with memoryForget. Provide a query to also include searchResults.",
documentList:
"List stored source documents (conversations, URLs, files, pasted text) in the configured namespace with pagination. Returns document metadata and IDs for documentDelete, not raw document content or memory IDs for memoryForget.",
documentDelete:
"Permanently delete a stored source document from the configured namespace. 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 caller-defined ID when removing an entire conversation, file, URL, or other source. For safety, deletion is refused while the document is still processing. 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 memory (a learned fact) in the configured namespace so it no longer appears in profile or search. Does NOT delete source documents. Provide memoryId from getProfile 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
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:
"Deprecated compatibility input. It is ignored because hybrid search returns learned memories and matching chunks, not full source documents.",
limit: "Maximum number of results to return",
searchLimit: "Maximum number of results to return (1-50)",
memory:
"The text content of the memory to add. This should be a single sentence or a short paragraph.",
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 caller-defined document ID. Permanently deletes the source document and soft-forgets its extracted memories once processing has finished. Not a 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:
"Memory ID from getProfile 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 memory to forget (alternative to memoryId). Must match precisely; if unsure, call getProfile and use a memory ID.",
} as const

View file

@ -4,7 +4,7 @@ import {
SEARCH_LIMIT_BOUNDS, SEARCH_LIMIT_BOUNDS,
clampSearchLimit, clampSearchLimit,
deduplicateMemoriesForMode, deduplicateMemoriesForMode,
getContainerTags, getNamespace,
} from "./tools-shared" } from "./tools-shared"
describe("clampSearchLimit", () => { describe("clampSearchLimit", () => {
@ -32,29 +32,19 @@ describe("clampSearchLimit", () => {
}) })
}) })
describe("getContainerTags", () => { describe("getNamespace", () => {
it("uses the default project when no config is provided", () => { it("uses the default namespace when no config is provided", () => {
expect(getContainerTags()).toEqual(["sm_project_default"]) expect(getNamespace()).toBe("sm_project_default")
}) })
it("converts projectId into a project container tag", () => { it("uses the configured namespace", () => {
expect(getContainerTags({ projectId: "abc" })).toEqual(["sm_project_abc"]) expect(getNamespace({ namespace: "user_1" })).toBe("user_1")
}) })
it("uses explicit container tags", () => { it("rejects an empty namespace", () => {
expect(getContainerTags({ containerTags: ["tag-a", "tag-b"] })).toEqual([ expect(() => getNamespace({ namespace: " " })).toThrow(
"tag-a", "non-empty namespace",
"tag-b", )
])
})
it("rejects config with both projectId and containerTags", () => {
expect(() =>
getContainerTags({
projectId: "abc",
containerTags: ["tag-a"],
}),
).toThrow("either projectId or containerTags")
}) })
}) })

View file

@ -1,73 +1,18 @@
/** import { NotFoundError, Supermemory } from "supermemory"
* Shared constants and descriptions for Supermemory tools
*/
import type Supermemory from "supermemory"
import type { MemoryMode } from "./shared/types" import type { MemoryMode } from "./shared/types"
import type { SupermemoryToolsConfig } from "./types"
// Tool descriptions export { TOOL_DESCRIPTIONS, PARAMETER_DESCRIPTIONS } from "./tools-descriptions"
export const TOOL_DESCRIPTIONS = {
searchMemories:
"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 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. 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) 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
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:
"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",
searchLimit: "Maximum number of results to return (1-50)",
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. 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 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 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:
"Optional reason recorded when forgetting (e.g. outdated, user correction)",
} as const
// Default values
export const DEFAULT_VALUES = { export const DEFAULT_VALUES = {
includeFullDocs: true, includeFullDocs: true,
limit: 10, limit: 10,
searchThreshold: 0.6, searchThreshold: 0.6,
} as const } as const
// Bounds for the searchMemories `limit` input.
export const SEARCH_LIMIT_BOUNDS = { min: 1, max: 50 } as const export const SEARCH_LIMIT_BOUNDS = { min: 1, max: 50 } as const
/** // The schema constrains well-behaved models; a prompt-injected one can still send anything.
* Clamp a searchMemories `limit` into SEARCH_LIMIT_BOUNDS.
* The schema constrains well-behaved models; a prompt-injected one can still
* send anything, so tool execution clamps as well. Non-numeric input falls
* back to DEFAULT_VALUES.limit.
*/
export function clampSearchLimit(value: unknown): number { export function clampSearchLimit(value: unknown): number {
const parsed = Number(value) const parsed = Number(value)
if (!Number.isFinite(parsed)) return DEFAULT_VALUES.limit if (!Number.isFinite(parsed)) return DEFAULT_VALUES.limit
@ -77,213 +22,149 @@ export function clampSearchLimit(value: unknown): number {
) )
} }
// Supermemory client options shared by the tool surfaces: bound each request // Bound each request so a slow API cannot stall an agent turn indefinitely.
// and limit retries so a slow API cannot stall an agent turn indefinitely.
export const CLIENT_OPTIONS = { export const CLIENT_OPTIONS = {
timeout: 30_000, timeoutInSeconds: 30,
maxRetries: 2, maxRetries: 2,
} as const } as const
// Container tag constants export const DEFAULT_NAMESPACE = "sm_project_default"
export const CONTAINER_TAG_CONSTANTS = {
projectPrefix: "sm_project_",
defaultTags: ["sm_project_default"] as const,
} as const
/** export function getNamespace(config?: { namespace?: string }): string {
* Helper function to generate container tags based on config if (config?.namespace === undefined) return DEFAULT_NAMESPACE
*/ if (config.namespace.trim() === "") {
export function getContainerTags(config?: { throw new Error("Supermemory tools config requires a non-empty namespace.")
projectId?: string
containerTags?: string[]
}): [string, ...string[]] {
if (config?.projectId !== undefined && config.containerTags !== undefined) {
throw new Error(
"Supermemory tools config accepts either projectId or containerTags, not both.",
)
} }
if (config?.projectId !== undefined) { return config.namespace
if (config.projectId.trim() === "") {
throw new Error(
"Supermemory tools config requires a non-empty projectId.",
)
}
return [`${CONTAINER_TAG_CONSTANTS.projectPrefix}${config.projectId}`]
}
if (config?.containerTags !== undefined) {
const [firstTag, ...remainingTags] = config.containerTags
if (
firstTag === undefined ||
config.containerTags.some((tag) => tag.trim() === "")
) {
throw new Error(
"Supermemory tools config requires at least one non-empty containerTag.",
)
}
return [firstTag, ...remainingTags]
}
return [...CONTAINER_TAG_CONSTANTS.defaultTags]
} }
/** Delete exactly one document by its internal ID. */ export function createToolsClient(
export async function deleteDocumentById( apiKey: string,
client: Supermemory, config?: SupermemoryToolsConfig,
documentId: string, ): Supermemory {
): Promise<void> { return new Supermemory({
const response = await client.documents.deleteBulk({ ids: [documentId] }) apiKey,
if (response.success && response.deletedCount === 1) return ...CLIENT_OPTIONS,
...(config?.baseUrl ? { baseUrl: config.baseUrl } : {}),
const detail = response.errors?.find( })
(error) => error.id === documentId,
)?.error
throw new Error(
detail
? `Failed to delete document ${documentId}: ${detail}`
: `Failed to delete document ${documentId}: expected one deletion, received ${response.deletedCount}`,
)
} }
/** export function isNotFoundError(error: unknown): boolean {
* Resolve an internal ID or customId inside the effective container-tag union, if (error instanceof NotFoundError) return true
* then delete the exact internal document ID. Internal IDs take precedence over if (typeof error !== "object" || error === null) return false
* customId matches. const status =
*/ "statusCode" in error
export async function deleteDocumentByIdentifier( ? error.statusCode
client: Supermemory, : "status" in error
documentIdentifier: string, ? error.status
containerTags: readonly [string, ...string[]], : undefined
): Promise<void> { return status === 404
const directMatch = await getDocumentIfFound(client, documentIdentifier)
if (directMatch?.id === documentIdentifier) {
assertDocumentCanBeDeleted(directMatch, containerTags)
await deleteDocumentById(client, directMatch.id)
return
}
const candidateIds = new Set<string>()
let hasInternalIdCandidate = false
let page = 1
while (true) {
const response = await client.documents.list({
containerTags: [...containerTags],
includeContent: false,
limit: 100,
page,
})
for (const document of response.memories) {
if (document.id === documentIdentifier) {
hasInternalIdCandidate = true
}
if (
document.id === documentIdentifier ||
document.customId === documentIdentifier
) {
candidateIds.add(document.id)
}
}
if (page >= response.pagination.totalPages) break
page += 1
}
let exactIdMatch: string | undefined
let hasUnverifiedCandidate = false
const customIdMatches: string[] = []
for (const candidateId of candidateIds) {
const document = await getDocumentIfFound(client, candidateId)
if (document?.id !== candidateId) {
hasUnverifiedCandidate = true
continue
}
assertDocumentCanBeDeleted(document, containerTags)
if (document.id === documentIdentifier) {
exactIdMatch = document.id
break
}
if (document.customId === documentIdentifier) {
customIdMatches.push(document.id)
} else {
hasUnverifiedCandidate = true
}
}
if (exactIdMatch) {
await deleteDocumentById(client, exactIdMatch)
return
}
if (hasInternalIdCandidate) {
throw new Error(
`Document ID ${documentIdentifier} could not be verified safely in the configured container scope.`,
)
}
if (hasUnverifiedCandidate) {
throw new Error(
`Document identifier ${documentIdentifier} could not be resolved unambiguously in the configured container scope.`,
)
}
if (customIdMatches.length === 1) {
await deleteDocumentById(client, customIdMatches[0] as string)
return
}
if (customIdMatches.length > 1) {
throw new Error(
`Document customId ${documentIdentifier} is ambiguous in the configured container scope.`,
)
}
throw new Error(
`Document ${documentIdentifier} was not found in the configured container scope.`,
)
}
async function getDocumentIfFound(client: Supermemory, documentId: string) {
try {
return await client.documents.get(documentId)
} catch (error) {
if (isNotFoundError(error)) return undefined
throw error
}
}
function isNotFoundError(error: unknown): boolean {
return (
typeof error === "object" &&
error !== null &&
"status" in error &&
error.status === 404
)
} }
const TERMINAL_DOCUMENT_STATUSES = new Set(["done", "failed"]) const TERMINAL_DOCUMENT_STATUSES = new Set(["done", "failed"])
function assertDocumentCanBeDeleted( // Accepts a Supermemory ID or caller-defined ID; v5 resolves both inside the namespace.
document: Awaited<ReturnType<Supermemory["documents"]["get"]>>, export async function deleteDocument(
expectedContainerTags: readonly string[], client: Supermemory,
): void { namespace: string,
if ( documentId: string,
!hasCompleteContainerTagScope(document.containerTags, expectedContainerTags) ): Promise<void> {
) { let document: Awaited<ReturnType<Supermemory["documents"]["get"]>>
try {
document = await client.documents.get(namespace, documentId)
} catch (error) {
if (isNotFoundError(error)) {
throw new Error(
`Document ${documentId} was not found in namespace ${namespace}.`,
)
}
throw error
}
const status = document.system?.status
if (status !== undefined && !TERMINAL_DOCUMENT_STATUSES.has(status)) {
throw new Error( 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.`, `Document ${documentId} cannot be deleted while it is processing or otherwise nonterminal (status: ${status}).`,
) )
} }
// The current SDK always supplies status. Keeping undefined permissive lets const response = await client.documents.delete(namespace, {
// older SDKs and lightweight client doubles continue to work. ids: [document.id],
const status = (document as { status?: string }).status })
if (status !== undefined && !TERMINAL_DOCUMENT_STATUSES.has(status)) { if (response.count === 1) return
throw new Error(
`Document ${document.id} cannot be deleted while it is processing or otherwise nonterminal (status: ${status}).`, const detail = response.errors?.find(
) (error) => error.id === document.id,
)?.error
throw new Error(
detail
? `Failed to delete document ${documentId}: ${detail}`
: `Failed to delete document ${documentId}: expected one deletion, received ${response.count}`,
)
}
// v5 profile takes no query; query-ranked results come from a separate memories search.
export async function getProfileWithSearch(
client: Supermemory,
namespace: string,
query?: string,
) {
const [profileResponse, searchResponse] = await Promise.all([
client.profile(namespace),
query
? client.search(namespace, {
query,
searchMode: "memories",
threshold: DEFAULT_VALUES.searchThreshold,
})
: undefined,
])
return {
profile: profileResponse.profile,
...(searchResponse && { searchResults: searchResponse.results }),
} }
} }
function hasCompleteContainerTagScope( export interface ForgetMemoryParams {
actual: string[] | undefined, id?: string
expected: readonly string[], content?: string
): boolean { }
return (
actual !== undefined && // Exact-text forget previews semantic matches, then forgets only the exact-text ones by ID.
actual.length > 0 && export async function forgetMemory(
actual.every((tag) => tag.trim() !== "" && expected.includes(tag)) client: Supermemory,
namespace: string,
params: ForgetMemoryParams,
): Promise<number> {
let ids: string[]
if (params.id) {
ids = [params.id]
} else if (params.content) {
const target = normalizeMemoryFact(params.content)
const preview = await client.memories.forgetMatching(namespace, {
query: params.content,
dryRun: true,
})
ids = preview.matches
.filter((match) => normalizeMemoryFact(match.memory) === target)
.map((match) => match.id)
if (ids.length === 0) {
throw new Error(
`No memory exactly matching "${params.content}" was found in namespace ${namespace}.`,
)
}
} else {
throw new Error("Either memoryId or memoryContent must be provided")
}
const response = await client.memories.forget(namespace, { ids })
if (response.count > 0) return response.count
const detail = response.errors?.[0]?.error
throw new Error(
detail
? `Failed to forget memory: ${detail}`
: "Failed to forget memory: no memories matched",
) )
} }
@ -296,13 +177,7 @@ export interface MemoryItem {
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
} }
/** // v5 profile and search entries are objects; plain strings are still accepted.
* Profile data from `/v4/profile`.
*
* Current profile arrays contain plain strings and search results contain
* MemoryItem objects. Object profile entries and string search entries remain
* accepted for compatibility with older API responses and SDK fixtures.
*/
export interface ProfileWithMemories { export interface ProfileWithMemories {
static?: Array<MemoryItem | string> static?: Array<MemoryItem | string>
dynamic?: Array<MemoryItem | string> dynamic?: Array<MemoryItem | string>

View file

@ -59,25 +59,25 @@ describe("@supermemory/tools", () => {
it("should create individual tools", () => { it("should create individual tools", () => {
const searchTool = aiSdk.searchMemoriesTool(testApiKey, { const searchTool = aiSdk.searchMemoriesTool(testApiKey, {
projectId: "test-project-123", namespace: "test-project-123",
}) })
const addTool = aiSdk.addMemoryTool(testApiKey, { const addTool = aiSdk.addMemoryTool(testApiKey, {
projectId: "test-project-123", namespace: "test-project-123",
}) })
const profileTool = aiSdk.getProfileTool(testApiKey, { const profileTool = aiSdk.getProfileTool(testApiKey, {
projectId: "test-project-123", namespace: "test-project-123",
}) })
const listTool = aiSdk.documentListTool(testApiKey, { const listTool = aiSdk.documentListTool(testApiKey, {
projectId: "test-project-123", namespace: "test-project-123",
}) })
const deleteTool = aiSdk.documentDeleteTool(testApiKey, { const deleteTool = aiSdk.documentDeleteTool(testApiKey, {
projectId: "test-project-123", namespace: "test-project-123",
}) })
const addDocTool = aiSdk.documentAddTool(testApiKey, { const addDocTool = aiSdk.documentAddTool(testApiKey, {
projectId: "test-project-123", namespace: "test-project-123",
}) })
const forgetTool = aiSdk.memoryForgetTool(testApiKey, { const forgetTool = aiSdk.memoryForgetTool(testApiKey, {
projectId: "test-project-123", namespace: "test-project-123",
}) })
expect(searchTool).toBeDefined() expect(searchTool).toBeDefined()
@ -111,7 +111,7 @@ describe("@supermemory/tools", () => {
], ],
tools: { tools: {
...aiSdk.supermemoryTools(testApiKey, { ...aiSdk.supermemoryTools(testApiKey, {
projectId: "test-ai-integration", namespace: "test-ai-integration",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}), }),
}, },
@ -128,7 +128,7 @@ describe("@supermemory/tools", () => {
}) })
const tools = aiSdk.supermemoryTools(testApiKey, { const tools = aiSdk.supermemoryTools(testApiKey, {
projectId: "test-tool-usage", namespace: "test-tool-usage",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}) })
@ -160,7 +160,7 @@ describe("@supermemory/tools", () => {
}) })
const tools = aiSdk.supermemoryTools(testApiKey, { const tools = aiSdk.supermemoryTools(testApiKey, {
projectId: "test-profile-tool", namespace: "test-profile-tool",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}) })
@ -192,7 +192,7 @@ describe("@supermemory/tools", () => {
}) })
const tools = aiSdk.supermemoryTools(testApiKey, { const tools = aiSdk.supermemoryTools(testApiKey, {
projectId: "test-document-tools", namespace: "test-document-tools",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}) })
@ -222,7 +222,7 @@ describe("@supermemory/tools", () => {
describe("new tool operations", () => { describe("new tool operations", () => {
it("should get profile with getProfileTool", async () => { it("should get profile with getProfileTool", async () => {
const profileTool = aiSdk.getProfileTool(testApiKey, { const profileTool = aiSdk.getProfileTool(testApiKey, {
projectId: "test-profile", namespace: "test-profile",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}) })
@ -234,7 +234,7 @@ describe("@supermemory/tools", () => {
it("should list documents with documentListTool", async () => { it("should list documents with documentListTool", async () => {
const listTool = aiSdk.documentListTool(testApiKey, { const listTool = aiSdk.documentListTool(testApiKey, {
projectId: "test-list", namespace: "test-list",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}) })
@ -245,7 +245,7 @@ describe("@supermemory/tools", () => {
it("should create documentDeleteTool", async () => { it("should create documentDeleteTool", async () => {
const deleteTool = aiSdk.documentDeleteTool(testApiKey, { const deleteTool = aiSdk.documentDeleteTool(testApiKey, {
projectId: "test-delete", namespace: "test-delete",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}) })
@ -256,7 +256,7 @@ describe("@supermemory/tools", () => {
it("should create documentAddTool", async () => { it("should create documentAddTool", async () => {
const addDocTool = aiSdk.documentAddTool(testApiKey, { const addDocTool = aiSdk.documentAddTool(testApiKey, {
projectId: "test-add-doc", namespace: "test-add-doc",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}) })
@ -267,7 +267,7 @@ describe("@supermemory/tools", () => {
it("should create memoryForgetTool", async () => { it("should create memoryForgetTool", async () => {
const forgetTool = aiSdk.memoryForgetTool(testApiKey, { const forgetTool = aiSdk.memoryForgetTool(testApiKey, {
projectId: "test-forget", namespace: "test-forget",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}) })
@ -282,7 +282,7 @@ describe("@supermemory/tools", () => {
describe("function-based tools", () => { describe("function-based tools", () => {
it("should create function-based tools", () => { it("should create function-based tools", () => {
const tools = openAi.supermemoryTools(testApiKey, { const tools = openAi.supermemoryTools(testApiKey, {
projectId: "test-openai-functions", namespace: "test-openai-functions",
}) })
expect(tools).toBeDefined() expect(tools).toBeDefined()
@ -292,10 +292,10 @@ describe("@supermemory/tools", () => {
it("should create individual tool functions", () => { it("should create individual tool functions", () => {
const searchFunction = openAi.createSearchMemoriesFunction(testApiKey, { const searchFunction = openAi.createSearchMemoriesFunction(testApiKey, {
projectId: "test-individual", namespace: "test-individual",
}) })
const addFunction = openAi.createAddMemoryFunction(testApiKey, { const addFunction = openAi.createAddMemoryFunction(testApiKey, {
projectId: "test-individual", namespace: "test-individual",
}) })
expect(searchFunction).toBeDefined() expect(searchFunction).toBeDefined()
@ -333,7 +333,7 @@ describe("@supermemory/tools", () => {
describe("tool execution", () => { describe("tool execution", () => {
it("should create tool call executor", () => { it("should create tool call executor", () => {
const executor = openAi.createToolCallExecutor(testApiKey, { const executor = openAi.createToolCallExecutor(testApiKey, {
containerTags: ["test-executor"], namespace: "test-executor",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}) })
@ -343,7 +343,7 @@ describe("@supermemory/tools", () => {
it("should create tool calls executor", () => { it("should create tool calls executor", () => {
const executor = openAi.createToolCallsExecutor(testApiKey, { const executor = openAi.createToolCallsExecutor(testApiKey, {
containerTags: ["test-executors"], namespace: "test-executors",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}) })
@ -355,7 +355,7 @@ describe("@supermemory/tools", () => {
describe("individual tool creators", () => { describe("individual tool creators", () => {
it("should create individual search tool", () => { it("should create individual search tool", () => {
const searchTool = openAi.createSearchMemoriesTool(testApiKey, { const searchTool = openAi.createSearchMemoriesTool(testApiKey, {
projectId: "test-individual", namespace: "test-individual",
}) })
expect(searchTool).toBeDefined() expect(searchTool).toBeDefined()
@ -366,7 +366,7 @@ describe("@supermemory/tools", () => {
it("should create individual add tool", () => { it("should create individual add tool", () => {
const addTool = openAi.createAddMemoryTool(testApiKey, { const addTool = openAi.createAddMemoryTool(testApiKey, {
projectId: "test-individual", namespace: "test-individual",
}) })
expect(addTool).toBeDefined() expect(addTool).toBeDefined()
@ -379,7 +379,7 @@ describe("@supermemory/tools", () => {
describe("memory operations", () => { describe("memory operations", () => {
it("should search memories", async () => { it("should search memories", async () => {
const searchFunction = openAi.createSearchMemoriesFunction(testApiKey, { const searchFunction = openAi.createSearchMemoriesFunction(testApiKey, {
projectId: "test-search", namespace: "test-search",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}) })
@ -403,7 +403,7 @@ describe("@supermemory/tools", () => {
it("should add memory", async () => { it("should add memory", async () => {
const addFunction = openAi.createAddMemoryFunction(testApiKey, { const addFunction = openAi.createAddMemoryFunction(testApiKey, {
containerTags: ["test-add-memory"], namespace: "test-add-memory",
baseUrl: testBaseUrl, baseUrl: testBaseUrl,
}) })

View file

@ -1,24 +1,8 @@
/**
* Supermemory configuration
* Only one of `projectId` or `containerTags` can be provided.
*/
export interface SupermemoryToolsConfig { export interface SupermemoryToolsConfig {
/** /** Custom base URL for the supermemory API */
* Custom base URL for the supermemory API
*/
baseUrl?: string baseUrl?: string
/** /** Namespace every tool reads and writes. Defaults to `sm_project_default`. */
* Array of custom container tags (mutually exclusive with projectId) namespace?: string
*/ /** When true, all schema properties are required (OpenAI strict mode). */
containerTags?: string[]
/**
* Project ID which gets converted to container tag format (mutually exclusive with containerTags)
*/
projectId?: string
/**
* 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.
*/
strict?: boolean strict?: boolean
} }

View file

@ -16,10 +16,10 @@ import { injectMemoriesIntoParams } from "./memory-prompt"
const DEFAULT_MEMORY_RETRIEVAL_TIMEOUT_MS = 5000 const DEFAULT_MEMORY_RETRIEVAL_TIMEOUT_MS = 5000
interface WrapVercelLanguageModelOptions { interface WrapVercelLanguageModelOptions {
/** The container tag/identifier for memory search (e.g., user ID, project ID) */ /** The namespace for memory search (e.g., user ID, project ID) */
containerTag: string namespace: string
/** Custom ID to group messages into a single document. Required. */ /** ID that groups messages into a single document. Required. */
customId: string id: string
/** Enable detailed logging of memory search and injection */ /** Enable detailed logging of memory search and injection */
verbose?: boolean verbose?: boolean
/** /**
@ -73,7 +73,7 @@ interface WrapVercelLanguageModelOptions {
* Wraps a language model with supermemory middleware to automatically inject relevant memories * Wraps a language model with supermemory middleware to automatically inject relevant memories
* into the system prompt based on the user's message content. * into the system prompt based on the user's message content.
* *
* This wrapper searches the supermemory API for relevant memories using the container tag * This wrapper searches the supermemory API for relevant memories in the namespace
* and user message, then either appends memories to an existing system prompt or creates * and user message, then either appends memories to an existing system prompt or creates
* a new system prompt with the memories. Pre-LLM profile retrieval uses a fixed internal * a new system prompt with the memories. Pre-LLM profile retrieval uses a fixed internal
* time budget and cannot be configured via options. * time budget and cannot be configured via options.
@ -83,8 +83,8 @@ interface WrapVercelLanguageModelOptions {
* *
* @param model - The language model to wrap with supermemory capabilities (V2 or V3) * @param model - The language model to wrap with supermemory capabilities (V2 or V3)
* @param options - Configuration options for Supermemory integration * @param options - Configuration options for Supermemory integration
* @param options.containerTag - Required. The container tag/identifier for memory search (e.g., user ID, project ID) * @param options.namespace - Required. The namespace for memory search (e.g., user ID, project ID)
* @param options.customId - Required. Custom ID to group messages into a single document for contextual memory generation * @param options.id - Required. ID that groups messages into a single document for contextual memory generation
* @param options.verbose - Optional flag to enable detailed logging of memory search and injection process (default: false) * @param options.verbose - Optional flag to enable detailed logging of memory search and injection process (default: false)
* @param options.mode - Optional mode for memory search: "profile", "query", or "full" (default: "profile") * @param options.mode - Optional mode for memory search: "profile", "query", or "full" (default: "profile")
* @param options.addMemory - Optional mode for memory search: "always", "never" (default: "always") * @param options.addMemory - Optional mode for memory search: "always", "never" (default: "always")
@ -100,8 +100,8 @@ interface WrapVercelLanguageModelOptions {
* import { openai } from "@ai-sdk/openai" * import { openai } from "@ai-sdk/openai"
* *
* const modelWithMemory = withSupermemory(openai("gpt-4"), { * const modelWithMemory = withSupermemory(openai("gpt-4"), {
* containerTag: "user-123", * namespace: "user-123",
* customId: "conversation-456", * id: "conversation-456",
* mode: "full", * mode: "full",
* addMemory: "always" * addMemory: "always"
* }) * })
@ -127,16 +127,16 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
) )
} }
if (!options.customId) { if (!options.id) {
throw new Error( throw new Error(
"customId is required — provide a non-empty string to group messages into a single document", "id is required — provide a non-empty string to group messages into a single document",
) )
} }
const ctx = createSupermemoryContext({ const ctx = createSupermemoryContext({
containerTag: options.containerTag, namespace: options.namespace,
apiKey: providedApiKey, apiKey: providedApiKey,
customId: options.customId, id: options.id,
verbose: options.verbose ?? false, verbose: options.verbose ?? false,
mode: options.mode ?? "profile", mode: options.mode ?? "profile",
addMemory: options.addMemory ?? "always", addMemory: options.addMemory ?? "always",
@ -192,8 +192,8 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
) )
saveMemoryAfterResponse( saveMemoryAfterResponse(
ctx.client, ctx.client,
ctx.containerTag, ctx.namespace,
ctx.customId, ctx.id,
assistantResponseText, assistantResponseText,
params, params,
ctx.logger, ctx.logger,
@ -266,8 +266,8 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
) { ) {
saveMemoryAfterResponse( saveMemoryAfterResponse(
ctx.client, ctx.client,
ctx.containerTag, ctx.namespace,
ctx.customId, ctx.id,
generatedText, generatedText,
params, params,
ctx.logger, ctx.logger,

View file

@ -108,7 +108,7 @@ export const injectMemoriesIntoParams = (
*/ */
export const addSystemPrompt = async ( export const addSystemPrompt = async (
params: LanguageModelCallOptions, params: LanguageModelCallOptions,
containerTag: string, namespace: string,
logger: Logger, logger: Logger,
mode: "profile" | "query" | "full", mode: "profile" | "query" | "full",
baseUrl: string, baseUrl: string,
@ -120,7 +120,7 @@ export const addSystemPrompt = async (
const queryText = extractQueryText(params, mode) const queryText = extractQueryText(params, mode)
const memories = await buildMemoriesText({ const memories = await buildMemoriesText({
containerTag, namespace,
queryText, queryText,
mode, mode,
baseUrl, baseUrl,

View file

@ -1,4 +1,4 @@
import Supermemory from "supermemory" import { Supermemory } from "supermemory"
import { import {
addConversation, addConversation,
type ContentPart, type ContentPart,
@ -150,8 +150,8 @@ export const convertToConversationMessages = (
export const saveMemoryAfterResponse = async ( export const saveMemoryAfterResponse = async (
_client: Supermemory, _client: Supermemory,
containerTag: string, namespace: string,
customId: string, id: string,
assistantResponseText: string, assistantResponseText: string,
params: LanguageModelCallOptions, params: LanguageModelCallOptions,
logger: Logger, logger: Logger,
@ -167,16 +167,16 @@ export const saveMemoryAfterResponse = async (
) )
const response = await addConversation({ const response = await addConversation({
conversationId: customId, id,
messages: conversationMessages, messages: conversationMessages,
containerTags: [containerTag], namespace,
apiKey, apiKey,
baseUrl, baseUrl,
}) })
logger.info("Conversation saved successfully via /v4/conversations", { logger.info("Conversation saved successfully", {
containerTag, namespace,
customId, id,
messageCount: conversationMessages.length, messageCount: conversationMessages.length,
responseId: response.id, responseId: response.id,
}) })
@ -191,12 +191,12 @@ export const saveMemoryAfterResponse = async (
* Configuration options for the Supermemory middleware. * Configuration options for the Supermemory middleware.
*/ */
interface SupermemoryMiddlewareOptions { interface SupermemoryMiddlewareOptions {
/** Container tag/identifier for memory search (e.g., user ID, project ID) */ /** Namespace for memory search (e.g., user ID, project ID) */
containerTag: string namespace: string
/** Supermemory API key */ /** Supermemory API key */
apiKey: string apiKey: string
/** Custom ID to group messages into a single document. Required. */ /** ID that groups messages into a single document. Required. */
customId: string id: string
/** Enable detailed logging of memory search and injection */ /** Enable detailed logging of memory search and injection */
verbose?: boolean verbose?: boolean
/** /**
@ -222,15 +222,15 @@ interface SupermemoryMiddlewareOptions {
includeToolCalls?: boolean includeToolCalls?: boolean
/** Custom function to format memory data into the system prompt */ /** Custom function to format memory data into the system prompt */
promptTemplate?: PromptTemplate promptTemplate?: PromptTemplate
/** Max wait (ms) for the pre-LLM `/v4/profile` retrieval. Omit for no limit (e.g. tests). `withSupermemory` sets this internally. */ /** Max wait (ms) for the pre-LLM profile retrieval. Omit for no limit (e.g. tests). `withSupermemory` sets this internally. */
memoryRetrievalTimeoutMs?: number memoryRetrievalTimeoutMs?: number
} }
interface SupermemoryMiddlewareContext { interface SupermemoryMiddlewareContext {
client: Supermemory client: Supermemory
logger: Logger logger: Logger
containerTag: string namespace: string
customId: string id: string
mode: MemoryMode mode: MemoryMode
addMemory: "always" | "never" addMemory: "always" | "never"
includeToolCalls: boolean includeToolCalls: boolean
@ -249,9 +249,9 @@ export const createSupermemoryContext = (
options: SupermemoryMiddlewareOptions, options: SupermemoryMiddlewareOptions,
): SupermemoryMiddlewareContext => { ): SupermemoryMiddlewareContext => {
const { const {
containerTag, namespace,
apiKey, apiKey,
customId, id,
verbose = false, verbose = false,
mode = "profile", mode = "profile",
addMemory = "always", addMemory = "always",
@ -267,15 +267,15 @@ export const createSupermemoryContext = (
const client = new Supermemory({ const client = new Supermemory({
apiKey, apiKey,
...(normalizedBaseUrl !== "https://api.supermemory.ai" ...(normalizedBaseUrl !== "https://api.supermemory.ai"
? { baseURL: normalizedBaseUrl } ? { baseUrl: normalizedBaseUrl }
: {}), : {}),
}) })
return { return {
client, client,
logger, logger,
containerTag, namespace,
customId, id,
mode, mode,
addMemory, addMemory,
includeToolCalls, includeToolCalls,
@ -297,12 +297,7 @@ const makeTurnKey = (
ctx: SupermemoryMiddlewareContext, ctx: SupermemoryMiddlewareContext,
userMessage: string, userMessage: string,
): string => { ): string => {
return MemoryCache.makeTurnKey( return MemoryCache.makeTurnKey(ctx.namespace, ctx.id, ctx.mode, userMessage)
ctx.containerTag,
ctx.customId,
ctx.mode,
userMessage,
)
} }
/** /**
@ -341,8 +336,8 @@ export const transformParamsWithMemory = async (
} }
ctx.logger.info("Starting memory search", { ctx.logger.info("Starting memory search", {
containerTag: ctx.containerTag, namespace: ctx.namespace,
customId: ctx.customId, id: ctx.id,
mode: ctx.mode, mode: ctx.mode,
isNewTurn, isNewTurn,
cacheHit: false, cacheHit: false,
@ -362,7 +357,7 @@ export const transformParamsWithMemory = async (
let memories: string let memories: string
try { try {
memories = await buildMemoriesText({ memories = await buildMemoriesText({
containerTag: ctx.containerTag, namespace: ctx.namespace,
queryText, queryText,
mode: ctx.mode, mode: ctx.mode,
baseUrl: ctx.normalizedBaseUrl, baseUrl: ctx.normalizedBaseUrl,

View file

@ -76,7 +76,7 @@ export const getLastUserMessage = (
.join(" ") .join(" ")
} }
/** Whether the prompt contains user content that `/v4/conversations` can store. */ /** Whether the prompt contains user content that a saved conversation can store. */
export const hasPersistableUserContent = ( export const hasPersistableUserContent = (
params: LanguageModelCallOptions, params: LanguageModelCallOptions,
): boolean => { ): boolean => {

View file

@ -50,7 +50,7 @@ const getOutputText = (output: unknown): string => {
* These hooks intercept the agent lifecycle to inject memories * These hooks intercept the agent lifecycle to inject memories
* before LLM calls and save conversations after completion. * before LLM calls and save conversations after completion.
* *
* @param containerTag - The container tag/user ID for scoping memories * @param namespace - The namespace (e.g. user ID) for scoping memories
* @param options - Configuration options for memory behavior * @param options - Configuration options for memory behavior
* @returns VoltAgent hooks object with onPrepareMessages and onEnd * @returns VoltAgent hooks object with onPrepareMessages and onEnd
* *
@ -61,7 +61,7 @@ const getOutputText = (output: unknown): string => {
* const hooks = createSupermemoryHooks("user-123", { * const hooks = createSupermemoryHooks("user-123", {
* mode: "full", * mode: "full",
* addMemory: "always", * addMemory: "always",
* customId: "conv-456", * id: "conv-456",
* }) * })
* *
* const agent = new Agent({ * const agent = new Agent({
@ -73,10 +73,10 @@ const getOutputText = (output: unknown): string => {
* ``` * ```
*/ */
export function createSupermemoryHooks( export function createSupermemoryHooks(
containerTag: string, namespace: string,
options: SupermemoryVoltAgent, options: SupermemoryVoltAgent,
): VoltAgentHooks { ): VoltAgentHooks {
const ctx = createSupermemoryContext(containerTag, options) const ctx = createSupermemoryContext(namespace, options)
return { return {
onPrepareMessages: async (args: HookPrepareMessagesArgs) => { onPrepareMessages: async (args: HookPrepareMessagesArgs) => {

View file

@ -21,9 +21,9 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
agentConfig: T agentConfig: T
/** /**
* Required. The container tag/user ID for scoping memories (e.g., "user-123") * Required. The namespace (e.g. user ID) for scoping memories (e.g., "user-123")
*/ */
containerTag: string namespace: string
} }
/** /**
@ -36,22 +36,22 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
* *
* @param options - Configuration object containing agent config and Supermemory options * @param options - Configuration object containing agent config and Supermemory options
* @param options.agentConfig - The VoltAgent agent configuration to enhance * @param options.agentConfig - The VoltAgent agent configuration to enhance
* @param options.containerTag - Required. The container tag/user ID for scoping memories (e.g., "user-123") * @param options.namespace - Required. The namespace (e.g. user ID) for scoping memories (e.g., "user-123")
* @param options.mode - Memory retrieval mode: "profile" (default), "query", or "full" * @param options.mode - Memory retrieval mode: "profile" (default), "query", or "full"
* @param options.addMemory - Memory persistence: "always" (default for VoltAgent) or "never" * @param options.addMemory - Memory persistence: "always" (default for VoltAgent) or "never"
* @param options.customId - Required. Custom ID to group messages into a single document * @param options.id - Required. ID that groups messages into a single document
* @param options.apiKey - Supermemory API key (falls back to SUPERMEMORY_API_KEY env var) * @param options.apiKey - Supermemory API key (falls back to SUPERMEMORY_API_KEY env var)
* @param options.baseUrl - Custom Supermemory API base URL * @param options.baseUrl - Custom Supermemory API base URL
* @param options.promptTemplate - Custom function to format memory data into prompt * @param options.promptTemplate - Custom function to format memory data into prompt
* @param options.threshold - Search sensitivity: 0 (more results) to 1 (more accurate) * @param options.threshold - Search sensitivity: 0 (more results) to 1 (more accurate)
* @param options.limit - Maximum number of memory results to return (integer from 1 to 100) * @param options.limit - Maximum number of memory results to return (integer from 1 to 100)
* @param options.rerank - If true, rerank results for relevance. Default: false * @param options.rerank - "none" (default), "order", or "aggregate"
* @param options.rewriteQuery - If true, AI-rewrite query for better results (+400ms latency). Default: false * @param options.rewriteQuery - If true, AI-rewrite query for better results (+400ms latency). Default: false
* @param options.filters - Advanced AND/OR filters for search * @param options.filter - Typed metadata filter for search
* @param options.include - Control what additional data to include (chunks, documents, etc.) * @param options.include - Extra context per result: documents, related, forgotten
* @param options.metadata - Optional metadata to attach to saved conversations * @param options.metadata - Optional metadata to attach to saved conversations
* @param options.searchMode - Search mode: "memories" (atomic facts), "documents" (chunks), or "hybrid" (both) * @param options.searchMode - Search mode: "memories" (atomic facts), "chunks", or "hybrid" (both)
* @param options.entityContext - Deprecated and ignored; configure entity context on the container tag instead * @param options.supportingContext - Context that guides memory extraction for saved conversations
* @returns Enhanced agent config with Supermemory hooks injected * @returns Enhanced agent config with Supermemory hooks injected
* *
* @example * @example
@ -67,8 +67,8 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
* instructions: "You are a helpful assistant", * instructions: "You are a helpful assistant",
* model: openai("gpt-4o"), * model: openai("gpt-4o"),
* }, * },
* containerTag: "user-123", * namespace: "user-123",
* customId: "conversation-123" * id: "conversation-123"
* }) * })
* *
* const agent = new Agent(configWithMemory) * const agent = new Agent(configWithMemory)
@ -83,14 +83,14 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
* instructions: "You are a helpful assistant", * instructions: "You are a helpful assistant",
* model: openai("gpt-4o"), * model: openai("gpt-4o"),
* }, * },
* containerTag: "user-123", // Required: user/project ID * namespace: "user-123", // Required: user/project ID
* mode: "full", // "profile" | "query" | "full" * mode: "full", // "profile" | "query" | "full"
* addMemory: "always", // "always" | "never" * addMemory: "always", // "always" | "never"
* customId: "conv-456", // Group messages by conversation * id: "conv-456", // Group messages by conversation
* threshold: 0.7, // 0.0-1.0 (higher = more accurate) * threshold: 0.7, // 0.0-1.0 (higher = more accurate)
* limit: 15, // Max results to return * limit: 15, // Max results to return
* rerank: true, // Rerank for best relevance * rerank: "order", // Rerank for best relevance
* searchMode: "hybrid", // "memories" | "documents" | "hybrid" * searchMode: "hybrid", // "memories" | "chunks" | "hybrid"
* metadata: { // Custom metadata * metadata: { // Custom metadata
* source: "voltagent", * source: "voltagent",
* version: "1.0" * version: "1.0"
@ -114,8 +114,8 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
* instructions: "...", * instructions: "...",
* model: openai("gpt-4o"), * model: openai("gpt-4o"),
* }, * },
* containerTag: "user-123", * namespace: "user-123",
* customId: "conversation-123", * id: "conversation-123",
* mode: "full", * mode: "full",
* promptTemplate: (data) => ` * promptTemplate: (data) => `
* <user_context> * <user_context>
@ -134,13 +134,10 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
export function withSupermemory<T extends VoltAgentConfig>( export function withSupermemory<T extends VoltAgentConfig>(
options: WithSupermemoryOptions<T>, options: WithSupermemoryOptions<T>,
): T & { hooks: NonNullable<VoltAgentConfig["hooks"]> } { ): T & { hooks: NonNullable<VoltAgentConfig["hooks"]> } {
const { agentConfig, containerTag, ...supermemoryOptions } = options const { agentConfig, namespace, ...supermemoryOptions } = options
// Create Supermemory hooks (internally creates its own context, validates API key) // Create Supermemory hooks (internally creates its own context, validates API key)
const supermemoryHooks = createSupermemoryHooks( const supermemoryHooks = createSupermemoryHooks(namespace, supermemoryOptions)
containerTag,
supermemoryOptions,
)
// Merge with existing hooks if present // Merge with existing hooks if present
const mergedHooks = mergeHooks(agentConfig.hooks, supermemoryHooks) const mergedHooks = mergeHooks(agentConfig.hooks, supermemoryHooks)
@ -158,7 +155,7 @@ export type {
VoltAgentConfig, VoltAgentConfig,
VoltAgentMessage, VoltAgentMessage,
VoltAgentHooks, VoltAgentHooks,
SearchFilters, SearchFilter,
IncludeOptions, IncludeOptions,
PromptTemplate, PromptTemplate,
MemoryMode, MemoryMode,

View file

@ -4,7 +4,7 @@
* Provides memory retrieval, injection, and storage functionality. * Provides memory retrieval, injection, and storage functionality.
*/ */
import Supermemory from "supermemory" import { type SearchRequest, Supermemory } from "supermemory"
import { import {
addConversation, addConversation,
type ContentPart as ConversationContentPart, type ContentPart as ConversationContentPart,
@ -21,6 +21,7 @@ import {
extractQueryText, extractQueryText,
replaceMemoryContext, replaceMemoryContext,
stripMemoryContext, stripMemoryContext,
PROFILE_SEARCH_THRESHOLD,
supermemoryProfileSearch, supermemoryProfileSearch,
wrapMemoryContext, wrapMemoryContext,
type Logger, type Logger,
@ -33,7 +34,7 @@ import {
normalizeMemoryFact, normalizeMemoryFact,
} from "../tools-shared" } from "../tools-shared"
import type { import type {
SearchFilters, SearchFilter,
SupermemoryVoltAgent, SupermemoryVoltAgent,
VoltAgentMessage, VoltAgentMessage,
} from "./types" } from "./types"
@ -44,8 +45,8 @@ import type {
export interface SupermemoryMiddlewareContext { export interface SupermemoryMiddlewareContext {
client: Supermemory client: Supermemory
logger: Logger logger: Logger
containerTag: string namespace: string
customId: string id: string
mode: MemoryMode mode: MemoryMode
addMemory: "always" | "never" addMemory: "always" | "never"
normalizedBaseUrl: string normalizedBaseUrl: string
@ -56,29 +57,22 @@ export interface SupermemoryMiddlewareContext {
* user turn (keyed by turnKey) to avoid redundant API calls. * user turn (keyed by turnKey) to avoid redundant API calls.
*/ */
memoryCache: MemoryCache<string> memoryCache: MemoryCache<string>
// New search parameters
threshold?: number threshold?: number
limit?: number limit?: number
rerank?: boolean rerank?: SupermemoryVoltAgent["rerank"]
rewriteQuery?: boolean rewriteQuery?: boolean
filters?: SearchFilters filter?: SearchFilter
include?: { include?: SupermemoryVoltAgent["include"]
chunks?: boolean
documents?: boolean
forgottenMemories?: boolean
relatedMemories?: boolean
summaries?: boolean
}
// Storage parameters
metadata?: Record<string, string | number | boolean> metadata?: Record<string, string | number | boolean>
searchMode?: "memories" | "documents" | "hybrid" supportingContext?: string
searchMode?: SupermemoryVoltAgent["searchMode"]
} }
/** /**
* Creates a Supermemory middleware context. * Creates a Supermemory middleware context.
*/ */
export const createSupermemoryContext = ( export const createSupermemoryContext = (
containerTag: string, namespace: string,
options: SupermemoryVoltAgent, options: SupermemoryVoltAgent,
): SupermemoryMiddlewareContext => { ): SupermemoryMiddlewareContext => {
const apiKey = options.apiKey ?? process.env.SUPERMEMORY_API_KEY const apiKey = options.apiKey ?? process.env.SUPERMEMORY_API_KEY
@ -89,7 +83,7 @@ export const createSupermemoryContext = (
} }
const { const {
customId, id,
mode = "profile", mode = "profile",
addMemory = "always", // VoltAgent default: save conversations by default for chat apps addMemory = "always", // VoltAgent default: save conversations by default for chat apps
baseUrl, baseUrl,
@ -98,17 +92,18 @@ export const createSupermemoryContext = (
limit, limit,
rerank, rerank,
rewriteQuery, rewriteQuery,
filters, filter,
include, include,
metadata, metadata,
supportingContext,
searchMode, searchMode,
verbose = false, verbose = false,
} = options } = options
// Runtime validation: customId is required // Runtime validation: id is required
if (!customId || typeof customId !== "string" || customId.trim() === "") { if (!id || typeof id !== "string" || id.trim() === "") {
throw new Error( throw new Error(
"customId is required and must be a non-empty string — provide it via `options.customId`", "id is required and must be a non-empty string — provide it via `options.id`",
) )
} }
if ( if (
@ -125,25 +120,20 @@ export const createSupermemoryContext = (
} }
const logger = createLogger(verbose) const logger = createLogger(verbose)
if (options.entityContext !== undefined) {
logger.warn(
"entityContext is not supported by /v4/conversations and will be ignored; configure it on the container tag instead.",
)
}
const normalizedBaseUrl = normalizeBaseUrl(baseUrl) const normalizedBaseUrl = normalizeBaseUrl(baseUrl)
const client = new Supermemory({ const client = new Supermemory({
apiKey, apiKey,
...(normalizedBaseUrl !== "https://api.supermemory.ai" ...(normalizedBaseUrl !== "https://api.supermemory.ai"
? { baseURL: normalizedBaseUrl } ? { baseUrl: normalizedBaseUrl }
: {}), : {}),
}) })
return { return {
client, client,
logger, logger,
containerTag, namespace,
customId, id,
mode, mode,
addMemory, addMemory,
normalizedBaseUrl, normalizedBaseUrl,
@ -154,9 +144,10 @@ export const createSupermemoryContext = (
limit, limit,
rerank, rerank,
rewriteQuery, rewriteQuery,
filters, filter,
include, include,
metadata, metadata,
supportingContext,
searchMode, searchMode,
} }
} }
@ -168,12 +159,7 @@ const makeTurnKey = (
ctx: SupermemoryMiddlewareContext, ctx: SupermemoryMiddlewareContext,
userMessage: string, userMessage: string,
): string => { ): string => {
return MemoryCache.makeTurnKey( return MemoryCache.makeTurnKey(ctx.namespace, ctx.id, ctx.mode, userMessage)
ctx.containerTag,
ctx.customId,
ctx.mode,
userMessage,
)
} }
/** /**
@ -259,8 +245,8 @@ export const enhanceMessagesWithMemories = async (
} }
ctx.logger.info("Starting memory search", { ctx.logger.info("Starting memory search", {
containerTag: ctx.containerTag, namespace: ctx.namespace,
customId: ctx.customId, id: ctx.id,
mode: ctx.mode, mode: ctx.mode,
isNewTurn, isNewTurn,
}) })
@ -277,7 +263,7 @@ export const enhanceMessagesWithMemories = async (
ctx.limit !== undefined || ctx.limit !== undefined ||
ctx.rerank !== undefined || ctx.rerank !== undefined ||
ctx.rewriteQuery !== undefined || ctx.rewriteQuery !== undefined ||
ctx.filters !== undefined || ctx.filter !== undefined ||
ctx.include !== undefined || ctx.include !== undefined ||
ctx.searchMode !== undefined ctx.searchMode !== undefined
@ -285,7 +271,7 @@ export const enhanceMessagesWithMemories = async (
// Profile mode only fetches static/dynamic user data, not query-based search // Profile mode only fetches static/dynamic user data, not query-based search
if (useAdvancedSearch && ctx.mode === "profile") { if (useAdvancedSearch && ctx.mode === "profile") {
ctx.logger.warn( ctx.logger.warn(
"Advanced search parameters (threshold, limit, rerank, rewriteQuery, filters, include, searchMode) " + "Advanced search parameters (threshold, limit, rerank, rewriteQuery, filter, include, searchMode) " +
'are ignored when mode is "profile". Use mode "query" or "full" to enable advanced search.', 'are ignored when mode is "profile". Use mode "query" or "full" to enable advanced search.',
) )
} }
@ -294,25 +280,25 @@ export const enhanceMessagesWithMemories = async (
if (useAdvancedSearch && ctx.mode !== "profile") { if (useAdvancedSearch && ctx.mode !== "profile") {
ctx.logger.info("Using advanced search with custom parameters") ctx.logger.info("Using advanced search with custom parameters")
const searchParams: Supermemory.SearchParams = { // v5 defaults are hybrid/0.3; keep the v4 defaults unless the caller overrides them.
q: queryText, const searchParams: SearchRequest = {
containerTag: ctx.containerTag, query: queryText,
searchMode: ctx.searchMode ?? "memories",
threshold: ctx.threshold ?? PROFILE_SEARCH_THRESHOLD,
} }
if (ctx.threshold !== undefined) searchParams.threshold = ctx.threshold
if (ctx.limit !== undefined) searchParams.limit = ctx.limit if (ctx.limit !== undefined) searchParams.limit = ctx.limit
if (ctx.rerank !== undefined) searchParams.rerank = ctx.rerank if (ctx.rerank !== undefined) searchParams.rerank = ctx.rerank
if (ctx.rewriteQuery !== undefined) if (ctx.rewriteQuery !== undefined)
searchParams.rewriteQuery = ctx.rewriteQuery searchParams.rewriteQuery = ctx.rewriteQuery
if (ctx.filters !== undefined) searchParams.filters = ctx.filters if (ctx.filter !== undefined) searchParams.filter = ctx.filter
if (ctx.include !== undefined) searchParams.include = ctx.include if (ctx.include !== undefined) searchParams.include = ctx.include
if (ctx.searchMode !== undefined) searchParams.searchMode = ctx.searchMode
const [response, profileResponse] = await Promise.all([ const [response, profileResponse] = await Promise.all([
ctx.client.search(searchParams), ctx.client.search(ctx.namespace, searchParams),
ctx.mode === "full" ctx.mode === "full"
? supermemoryProfileSearch( ? supermemoryProfileSearch(
ctx.containerTag, ctx.namespace,
"", "",
ctx.normalizedBaseUrl, ctx.normalizedBaseUrl,
ctx.apiKey, ctx.apiKey,
@ -379,7 +365,7 @@ export const enhanceMessagesWithMemories = async (
} }
return await buildMemoriesText({ return await buildMemoriesText({
containerTag: ctx.containerTag, namespace: ctx.namespace,
queryText, queryText,
mode: ctx.mode, mode: ctx.mode,
baseUrl: ctx.normalizedBaseUrl, baseUrl: ctx.normalizedBaseUrl,
@ -622,17 +608,18 @@ export const saveConversation = async (
} }
const response = await addConversation({ const response = await addConversation({
conversationId: ctx.customId, id: ctx.id,
messages: conversationMessages, messages: conversationMessages,
containerTags: [ctx.containerTag], namespace: ctx.namespace,
metadata: ctx.metadata, metadata: ctx.metadata,
supportingContext: ctx.supportingContext,
apiKey: ctx.apiKey, apiKey: ctx.apiKey,
baseUrl: ctx.normalizedBaseUrl, baseUrl: ctx.normalizedBaseUrl,
}) })
ctx.logger.info("Conversation saved successfully via /v4/conversations", { ctx.logger.info("Conversation saved successfully", {
containerTag: ctx.containerTag, namespace: ctx.namespace,
customId: ctx.customId, id: ctx.id,
messageCount: conversationMessages.length, messageCount: conversationMessages.length,
responseId: response.id, responseId: response.id,
metadata: ctx.metadata, metadata: ctx.metadata,

View file

@ -5,7 +5,12 @@
* @supermemory/tools declarations remain usable when the optional peer is absent. * @supermemory/tools declarations remain usable when the optional peer is absent.
*/ */
import type Supermemory from "supermemory" import type {
FilterExpression,
SearchRequestInclude,
SearchRequestRerank,
SearchRequestSearchMode,
} from "supermemory"
import type { SupermemoryBaseOptions } from "../shared" import type { SupermemoryBaseOptions } from "../shared"
/** /**
@ -14,10 +19,10 @@ import type { SupermemoryBaseOptions } from "../shared"
*/ */
export interface SupermemoryVoltAgent extends SupermemoryBaseOptions { export interface SupermemoryVoltAgent extends SupermemoryBaseOptions {
/** /**
* Custom ID to group messages into a single document. * ID that groups messages into a single document.
* Ensures related messages are added to the same document for that conversation. * Ensures related messages are added to the same document for that conversation.
*/ */
customId: string id: string
/** /**
* Threshold / sensitivity for memory selection. 0 is least sensitive (returns * Threshold / sensitivity for memory selection. 0 is least sensitive (returns
@ -38,12 +43,11 @@ export interface SupermemoryVoltAgent extends SupermemoryBaseOptions {
limit?: number limit?: number
/** /**
* If true, rerank the results based on the query. This helps ensure the most * Post-retrieval ranking: "none", "order", or "aggregate". Default: "none"
* relevant results are returned. Default: false
* *
* Note: Only effective when mode is "query" or "full". Ignored in "profile" mode. * Note: Only effective when mode is "query" or "full". Ignored in "profile" mode.
*/ */
rerank?: boolean rerank?: SearchRequestRerank
/** /**
* If true, rewrites the query to make it easier to find memories. This increases * If true, rewrites the query to make it easier to find memories. This increases
@ -54,12 +58,12 @@ export interface SupermemoryVoltAgent extends SupermemoryBaseOptions {
rewriteQuery?: boolean rewriteQuery?: boolean
/** /**
* Advanced filters to apply to the search using AND/OR logic. * Typed metadata filter applied before ranking.
* Example: { OR: [{ key: "type", value: "note" }, { key: "type", value: "conversation" }] } * Example: { operator: "or", operands: [{ field: "type", operator: "eq", value: "note" }, { field: "type", operator: "eq", value: "conversation" }] }
* *
* Note: Only effective when mode is "query" or "full". Ignored in "profile" mode. * Note: Only effective when mode is "query" or "full". Ignored in "profile" mode.
*/ */
filters?: SearchFilters filter?: SearchFilter
/** /**
* Control what additional data to include in search results. * Control what additional data to include in search results.
@ -77,33 +81,19 @@ export interface SupermemoryVoltAgent extends SupermemoryBaseOptions {
/** /**
* Search mode controlling what type of results to search. * Search mode controlling what type of results to search.
* - "memories": Search only memory entries (atomic facts) * - "memories": Search only memory entries (atomic facts)
* - "documents": Search only document chunks * - "chunks": Search only document chunks
* - "hybrid": Search both memories AND document chunks (recommended) * - "hybrid": Search both memories AND document chunks (recommended)
* *
* Note: Only effective when mode is "query" or "full". Ignored in "profile" mode. * Note: Only effective when mode is "query" or "full". Ignored in "profile" mode.
*/ */
searchMode?: "memories" | "documents" | "hybrid" searchMode?: SearchRequestSearchMode
/** /** Context that guides memory extraction for saved conversations. Max 1500 characters. */
* @deprecated The conversations API does not accept per-request entity context. supportingContext?: string
* Configure entity context on the container tag instead.
*/
entityContext?: string
} }
/** Advanced search filters using AND/OR logic. */ /** Typed v5 metadata filter expression. */
export type SearchFilters = NonNullable<Supermemory.SearchParams["filters"]> export type SearchFilter = FilterExpression
/** Options for including additional data in search results. */ /** Options for including additional data in search results. */
export interface IncludeOptions { export type IncludeOptions = SearchRequestInclude
/** Fetch chunks from documents associated with found memories. */
chunks?: boolean
/** Include full document information in results. */
documents?: boolean
/** Include explicitly forgotten or expired memories. */
forgottenMemories?: boolean
/** Include parent/child memories from the memory graph. */
relatedMemories?: boolean
/** Include document summaries in results. */
summaries?: boolean
}

View file

@ -45,7 +45,7 @@ export type HookEndArgs = OnEndHookArgs
export type { export type {
IncludeOptions, IncludeOptions,
SearchFilters, SearchFilter,
SupermemoryVoltAgent, SupermemoryVoltAgent,
} from "./options" } from "./options"

View file

@ -3,8 +3,8 @@ import { withSupermemory } from "../src/ai-sdk"
import { openai } from "@ai-sdk/openai" import { openai } from "@ai-sdk/openai"
const modelWithMemory = withSupermemory(openai("gpt-5"), { const modelWithMemory = withSupermemory(openai("gpt-5"), {
containerTag: "user_id_life", namespace: "user_id_life",
customId: "conversation-123", id: "conversation-123",
verbose: true, verbose: true,
mode: "query", // options are profile, query, full (default is profile) mode: "query", // options are profile, query, full (default is profile)
addMemory: "always", // options are always, never (default is never) addMemory: "always", // options are always, never (default is never)

View file

@ -32,8 +32,7 @@ async function chatWithMemoryTool() {
// Initialize memory tool // Initialize memory tool
const memoryTool = createClaudeMemoryTool(SUPERMEMORY_API_KEY, { const memoryTool = createClaudeMemoryTool(SUPERMEMORY_API_KEY, {
projectId: "anthropic-sdk-demo", namespace: "anthropic-sdk-demo",
memoryContainerTag: "claude_memory_anthropic",
}) })
// Conversation messages // Conversation messages
@ -177,8 +176,7 @@ async function testMemoryOperations() {
} }
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, { const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, {
projectId: "direct-test", namespace: "direct-test",
memoryContainerTag: "claude_memory_direct",
}) })
const testCases = [ const testCases = [

View file

@ -2,8 +2,8 @@ import { gateway, streamText, type ModelMessage } from "ai"
import { withSupermemory } from "@supermemory/tools/ai-sdk" import { withSupermemory } from "@supermemory/tools/ai-sdk"
const model = withSupermemory(gateway("google/gemini-2.5-flash"), { const model = withSupermemory(gateway("google/gemini-2.5-flash"), {
containerTag: "user-1", namespace: "user-1",
customId: "chat-session", id: "chat-session",
apiKey: process.env.SUPERMEMORY_API_KEY ?? "", apiKey: process.env.SUPERMEMORY_API_KEY ?? "",
mode: "full", mode: "full",
addMemory: "always", addMemory: "always",

View file

@ -14,8 +14,8 @@ export async function POST(req: Request) {
}) })
const openaiWithSupermemory = withSupermemory(openai, { const openaiWithSupermemory = withSupermemory(openai, {
containerTag: "user-123", namespace: "user-123",
customId: conversationId, id: conversationId,
mode: "full", mode: "full",
addMemory: "always", addMemory: "always",
verbose: true, verbose: true,

View file

@ -8,8 +8,8 @@ const SUPERMEMORY_USER_ID = "user-1"
const gatewayModel = gateway("google/gemini-2.5-flash") const gatewayModel = gateway("google/gemini-2.5-flash")
const supermemoryOptions = { const supermemoryOptions = {
containerTag: SUPERMEMORY_USER_ID, namespace: SUPERMEMORY_USER_ID,
customId: "stream-session", id: "stream-session",
apiKey: process.env.SUPERMEMORY_API_KEY ?? "", apiKey: process.env.SUPERMEMORY_API_KEY ?? "",
mode: "full" as const, mode: "full" as const,
addMemory: "always" as const, addMemory: "always" as const,

View file

@ -21,8 +21,7 @@ export async function directFetchExample() {
// Initialize the memory tool // Initialize the memory tool
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, { const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
projectId: "claude-memory-demo", namespace: "claude-memory-demo",
memoryContainerTag: "claude_memory_demo",
}) })
// Example memory commands that Claude might send // Example memory commands that Claude might send
@ -106,8 +105,7 @@ export async function anthropicSdkExample() {
// Initialize memory tool // Initialize memory tool
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, { const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
projectId: "claude-chat-session", namespace: "claude-chat-session",
memoryContainerTag: "claude_memory_chat",
}) })
// Simulate Claude's memory tool usage in a conversation // Simulate Claude's memory tool usage in a conversation
@ -197,8 +195,7 @@ const anthropic = new Anthropic({
}); });
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, { const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
projectId: 'my-chat-app', namespace: 'my-chat-app',
memoryContainerTag: 'claude_memory'
}); });
// Memory tool definition for Claude // Memory tool definition for Claude
@ -243,16 +240,15 @@ async function chatWithMemory(userMessage: string) {
export const curlExamples = ` export const curlExamples = `
# Test the memory tool using cURL commands against your supermemory API # Test the memory tool using cURL commands against your supermemory API
# 1. Create a memory file # 1. Create a memory file
curl -X POST "https://api.supermemory.ai/v3/documents" \\ curl -X POST "https://api.supermemory.ai/ns/my-app/document" \\
-H "Authorization: Bearer YOUR_API_KEY" \\ -H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\ -H "Content-Type: application/json" \\
-d '{ -d '{
"content": "# My Notes\\n\\nThis is a test note for Claude memory tool.", "content": "# My Notes\\n\\nThis is a test note for Claude memory tool.",
"customId": "/memories/test-note.md", "id": "memories_test-note_md",
"containerTags": ["claude_memory", "sm_project_test"],
"metadata": { "metadata": {
"source": "claude-memory",
"claude_memory_type": "file", "claude_memory_type": "file",
"file_path": "/memories/test-note.md", "file_path": "/memories/test-note.md",
"line_count": 3, "line_count": 3,
@ -260,35 +256,24 @@ curl -X POST "https://api.supermemory.ai/v3/documents" \\
} }
}' }'
# 2. Search/read the memory file # 2. Read the memory file
curl -X POST "https://api.supermemory.ai/v3/search" \\ curl "https://api.supermemory.ai/ns/my-app/document/memories_test-note_md" \\
-H "Authorization: Bearer YOUR_API_KEY" \\ -H "Authorization: Bearer YOUR_API_KEY"
-H "Content-Type: application/json" \\
-d '{
"q": "/memories/test-note.md",
"containerTags": ["claude_memory", "sm_project_test"],
"limit": 1,
"includeFullDocs": true
}'
# 3. List all memory files (directory listing) # 3. List all memory files (directory listing)
curl -X POST "https://api.supermemory.ai/v3/search" \\ curl -X POST "https://api.supermemory.ai/ns/my-app/list/documents?limit=100" \\
-H "Authorization: Bearer YOUR_API_KEY" \\ -H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\ -H "Content-Type: application/json" \\
-d '{ -d '{"filter": {"field": "source", "operator": "eq", "value": "claude-memory"}}'
"q": "*",
"containerTags": ["claude_memory", "sm_project_test"],
"limit": 100,
"includeFullDocs": false
}'
# 4. Update a memory file (str_replace operation) # 4. Replace a memory file (str_replace operation)
curl -X PATCH "https://api.supermemory.ai/v3/documents/DOCUMENT_ID" \\ curl -X PATCH "https://api.supermemory.ai/ns/my-app/document/memories_test-note_md" \\
-H "Authorization: Bearer YOUR_API_KEY" \\ -H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\ -H "Content-Type: application/json" \\
-d '{ -d '{
"content": "# My Updated Notes\\n\\nThis note has been updated using str_replace.", "content": "# My Updated Notes\\n\\nThis note has been updated using str_replace.",
"metadata": { "metadata": {
"source": "claude-memory",
"claude_memory_type": "file", "claude_memory_type": "file",
"file_path": "/memories/test-note.md", "file_path": "/memories/test-note.md",
"line_count": 3, "line_count": 3,
@ -297,8 +282,10 @@ curl -X PATCH "https://api.supermemory.ai/v3/documents/DOCUMENT_ID" \\
}' }'
# 5. Delete a memory file # 5. Delete a memory file
curl -X DELETE "https://api.supermemory.ai/v3/documents/DOCUMENT_ID" \\ curl -X DELETE "https://api.supermemory.ai/ns/my-app/document" \\
-H "Authorization: Bearer YOUR_API_KEY" -H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{"ids": ["memories_test-note_md"]}'
` `
// ===================================================== // =====================================================

View file

@ -22,8 +22,7 @@ export async function handleClaudeMemoryToolCall(
}, },
supermemoryApiKey: string, supermemoryApiKey: string,
config?: { config?: {
projectId?: string namespace?: string
memoryContainerTag?: string
baseUrl?: string baseUrl?: string
}, },
) { ) {
@ -34,8 +33,7 @@ export async function handleClaudeMemoryToolCall(
// Initialize memory tool // Initialize memory tool
const memoryTool = createClaudeMemoryTool(supermemoryApiKey, { const memoryTool = createClaudeMemoryTool(supermemoryApiKey, {
projectId: config?.projectId || "claude-chat", namespace: config?.namespace || "claude-chat",
memoryContainerTag: config?.memoryContainerTag || "claude_memory",
baseUrl: config?.baseUrl, baseUrl: config?.baseUrl,
}) })
@ -137,8 +135,7 @@ export async function realClaudeMemoryExample() {
console.log(`Path: ${block.input.path}`) console.log(`Path: ${block.input.path}`)
return handleClaudeMemoryToolCall(block, SUPERMEMORY_API_KEY, { return handleClaudeMemoryToolCall(block, SUPERMEMORY_API_KEY, {
projectId: "python-scraper-help", namespace: "python-scraper-help",
memoryContainerTag: "claude_memory_debug",
}) })
}), }),
) )
@ -194,8 +191,7 @@ export async function processClaudeResponse(
claudeResponseData: any, claudeResponseData: any,
supermemoryApiKey: string, supermemoryApiKey: string,
config?: { config?: {
projectId?: string namespace?: string
memoryContainerTag?: string
baseUrl?: string baseUrl?: string
}, },
): Promise<any[]> { ): Promise<any[]> {
@ -266,8 +262,7 @@ app.post('/chat-with-memory', async (req, res) => {
claudeData, claudeData,
process.env.SUPERMEMORY_API_KEY!, process.env.SUPERMEMORY_API_KEY!,
{ {
projectId: conversationId || 'default-chat', namespace: conversationId || 'default-chat',
memoryContainerTag: 'claude_memory_chat'
} }
); );
@ -320,8 +315,7 @@ export async function testWithRealToolCall() {
realToolCall, realToolCall,
process.env.SUPERMEMORY_API_KEY, process.env.SUPERMEMORY_API_KEY,
{ {
projectId: "python-scraper-debug", namespace: "python-scraper-debug",
memoryContainerTag: "claude_memory_test",
}, },
) )

View file

@ -6,8 +6,7 @@ import "dotenv/config"
const TEST_CONFIG = { const TEST_CONFIG = {
apiKey: process.env.SUPERMEMORY_API_KEY || "test-api-key", apiKey: process.env.SUPERMEMORY_API_KEY || "test-api-key",
baseUrl: process.env.SUPERMEMORY_BASE_URL, baseUrl: process.env.SUPERMEMORY_BASE_URL,
projectId: "test-claude-memory", namespace: "test-claude-memory",
memoryContainerTag: "claude_memory_test",
} }
describe("Claude Memory Tool", () => { describe("Claude Memory Tool", () => {
@ -15,8 +14,7 @@ describe("Claude Memory Tool", () => {
beforeEach(() => { beforeEach(() => {
memoryTool = createClaudeMemoryTool(TEST_CONFIG.apiKey, { memoryTool = createClaudeMemoryTool(TEST_CONFIG.apiKey, {
projectId: TEST_CONFIG.projectId, namespace: TEST_CONFIG.namespace,
memoryContainerTag: TEST_CONFIG.memoryContainerTag,
baseUrl: TEST_CONFIG.baseUrl, baseUrl: TEST_CONFIG.baseUrl,
}) })
}) })
@ -346,8 +344,7 @@ async function runManualTests() {
} }
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, { const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, {
projectId: "manual-test-project", namespace: "manual-test-project",
memoryContainerTag: "claude_memory_manual_test",
baseUrl: process.env.SUPERMEMORY_BASE_URL, baseUrl: process.env.SUPERMEMORY_BASE_URL,
}) })

View file

@ -36,8 +36,8 @@ import "dotenv/config"
const INTEGRATION_CONFIG = { const INTEGRATION_CONFIG = {
apiKey: process.env.SUPERMEMORY_API_KEY || "", apiKey: process.env.SUPERMEMORY_API_KEY || "",
baseUrl: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai", baseUrl: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai",
containerTag: "integration-test-mastra", namespace: "integration-test-mastra",
customId: "integration-test-conversation", id: "integration-test-conversation",
} }
const shouldRunIntegration = !!process.env.SUPERMEMORY_API_KEY const shouldRunIntegration = !!process.env.SUPERMEMORY_API_KEY
@ -103,8 +103,8 @@ describe.skipIf(!shouldRunIntegration)(
describe("SupermemoryInputProcessor", () => { describe("SupermemoryInputProcessor", () => {
it("should fetch real memories and inject into messageList", async () => { it("should fetch real memories and inject into messageList", async () => {
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: INTEGRATION_CONFIG.customId, id: INTEGRATION_CONFIG.id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile", mode: "profile",
@ -134,8 +134,8 @@ describe.skipIf(!shouldRunIntegration)(
const fetchSpy = vi.spyOn(globalThis, "fetch") const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: INTEGRATION_CONFIG.customId, id: INTEGRATION_CONFIG.id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "query", mode: "query",
@ -158,8 +158,7 @@ describe.skipIf(!shouldRunIntegration)(
await processor.processInput(args) await processor.processInput(args)
const profileCalls = fetchSpy.mock.calls.filter( const profileCalls = fetchSpy.mock.calls.filter(
(call) => (call) => typeof call[0] === "string" && call[0].includes("/search"),
typeof call[0] === "string" && call[0].includes("/v4/profile"),
) )
expect(profileCalls.length).toBeGreaterThan(0) expect(profileCalls.length).toBeGreaterThan(0)
@ -168,7 +167,7 @@ describe.skipIf(!shouldRunIntegration)(
const body = JSON.parse( const body = JSON.parse(
(profileCall[1] as RequestInit).body as string, (profileCall[1] as RequestInit).body as string,
) )
expect(body.q).toBe("What are my favorite programming languages?") expect(body.query).toBe("What are my favorite programming languages?")
} }
fetchSpy.mockRestore() fetchSpy.mockRestore()
@ -178,8 +177,8 @@ describe.skipIf(!shouldRunIntegration)(
const fetchSpy = vi.spyOn(globalThis, "fetch") const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: INTEGRATION_CONFIG.customId, id: INTEGRATION_CONFIG.id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "full", mode: "full",
@ -197,8 +196,7 @@ describe.skipIf(!shouldRunIntegration)(
await processor.processInput(args) await processor.processInput(args)
const profileCalls = fetchSpy.mock.calls.filter( const profileCalls = fetchSpy.mock.calls.filter(
(call) => (call) => typeof call[0] === "string" && call[0].includes("/search"),
typeof call[0] === "string" && call[0].includes("/v4/profile"),
) )
expect(profileCalls.length).toBeGreaterThan(0) expect(profileCalls.length).toBeGreaterThan(0)
@ -207,7 +205,7 @@ describe.skipIf(!shouldRunIntegration)(
const body = JSON.parse( const body = JSON.parse(
(profileCall[1] as RequestInit).body as string, (profileCall[1] as RequestInit).body as string,
) )
expect(body.q).toBe("Full mode test query") expect(body.query).toBe("Full mode test query")
} }
fetchSpy.mockRestore() fetchSpy.mockRestore()
@ -217,8 +215,8 @@ describe.skipIf(!shouldRunIntegration)(
const fetchSpy = vi.spyOn(globalThis, "fetch") const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: INTEGRATION_CONFIG.customId, id: INTEGRATION_CONFIG.id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile", mode: "profile",
@ -238,8 +236,7 @@ describe.skipIf(!shouldRunIntegration)(
await processor.processInput(args1) await processor.processInput(args1)
const callsAfterFirst = fetchSpy.mock.calls.filter( const callsAfterFirst = fetchSpy.mock.calls.filter(
(call) => (call) => typeof call[0] === "string" && call[0].includes("/profile"),
typeof call[0] === "string" && call[0].includes("/v4/profile"),
).length ).length
const args2: ProcessInputArgs = { const args2: ProcessInputArgs = {
@ -252,8 +249,7 @@ describe.skipIf(!shouldRunIntegration)(
await processor.processInput(args2) await processor.processInput(args2)
const callsAfterSecond = fetchSpy.mock.calls.filter( const callsAfterSecond = fetchSpy.mock.calls.filter(
(call) => (call) => typeof call[0] === "string" && call[0].includes("/profile"),
typeof call[0] === "string" && call[0].includes("/v4/profile"),
).length ).length
expect(callsAfterSecond).toBe(callsAfterFirst) expect(callsAfterSecond).toBe(callsAfterFirst)
@ -268,8 +264,8 @@ describe.skipIf(!shouldRunIntegration)(
}) => `<mastra-memories>${data.userMemories}</mastra-memories>` }) => `<mastra-memories>${data.userMemories}</mastra-memories>`
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: INTEGRATION_CONFIG.customId, id: INTEGRATION_CONFIG.id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile", mode: "profile",
@ -296,11 +292,11 @@ describe.skipIf(!shouldRunIntegration)(
it("should save conversation when addMemory is always", async () => { it("should save conversation when addMemory is always", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch") const fetchSpy = vi.spyOn(globalThis, "fetch")
const customId = `test-mastra-${Date.now()}` const id = `test-mastra-${Date.now()}`
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId, id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
addMemory: "always", addMemory: "always",
@ -320,8 +316,7 @@ describe.skipIf(!shouldRunIntegration)(
const conversationCalls = fetchSpy.mock.calls.filter( const conversationCalls = fetchSpy.mock.calls.filter(
(call) => (call) =>
typeof call[0] === "string" && typeof call[0] === "string" && call[0].includes("/document"),
call[0].includes("/v4/conversations"),
) )
expect(conversationCalls.length).toBeGreaterThan(0) expect(conversationCalls.length).toBeGreaterThan(0)
@ -332,8 +327,8 @@ describe.skipIf(!shouldRunIntegration)(
const fetchSpy = vi.spyOn(globalThis, "fetch") const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-thread", id: "test-thread",
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
addMemory: "never", addMemory: "never",
@ -353,8 +348,7 @@ describe.skipIf(!shouldRunIntegration)(
const conversationCalls = fetchSpy.mock.calls.filter( const conversationCalls = fetchSpy.mock.calls.filter(
(call) => (call) =>
typeof call[0] === "string" && typeof call[0] === "string" && call[0].includes("/document"),
call[0].includes("/v4/conversations"),
) )
expect(conversationCalls.length).toBe(0) expect(conversationCalls.length).toBe(0)
@ -365,8 +359,8 @@ describe.skipIf(!shouldRunIntegration)(
const fetchSpy = vi.spyOn(globalThis, "fetch") const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: INTEGRATION_CONFIG.customId, id: INTEGRATION_CONFIG.id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
addMemory: "always", addMemory: "always",
@ -391,8 +385,7 @@ describe.skipIf(!shouldRunIntegration)(
const conversationCalls = fetchSpy.mock.calls.filter( const conversationCalls = fetchSpy.mock.calls.filter(
(call) => (call) =>
typeof call[0] === "string" && typeof call[0] === "string" && call[0].includes("/document"),
call[0].includes("/v4/conversations"),
) )
expect(conversationCalls.length).toBeGreaterThan(0) expect(conversationCalls.length).toBeGreaterThan(0)
@ -403,8 +396,8 @@ describe.skipIf(!shouldRunIntegration)(
describe("createSupermemoryProcessors", () => { describe("createSupermemoryProcessors", () => {
it("should create working input and output processors", async () => { it("should create working input and output processors", async () => {
const { input, output } = createSupermemoryProcessors({ const { input, output } = createSupermemoryProcessors({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: `processors-test-${Date.now()}`, id: `processors-test-${Date.now()}`,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile", mode: "profile",
@ -446,8 +439,8 @@ describe.skipIf(!shouldRunIntegration)(
} }
const enhanced = withSupermemory(config, { const enhanced = withSupermemory(config, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: `wrapper-test-${Date.now()}`, id: `wrapper-test-${Date.now()}`,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile", mode: "profile",
@ -499,8 +492,8 @@ describe.skipIf(!shouldRunIntegration)(
} }
const enhanced = withSupermemory(config, { const enhanced = withSupermemory(config, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: INTEGRATION_CONFIG.customId, id: INTEGRATION_CONFIG.id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile", mode: "profile",
@ -520,8 +513,8 @@ describe.skipIf(!shouldRunIntegration)(
describe("Options", () => { describe("Options", () => {
it("verbose mode should not break functionality", async () => { it("verbose mode should not break functionality", async () => {
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: INTEGRATION_CONFIG.customId, id: INTEGRATION_CONFIG.id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile", mode: "profile",
@ -546,8 +539,8 @@ describe.skipIf(!shouldRunIntegration)(
const fetchSpy = vi.spyOn(globalThis, "fetch") const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: INTEGRATION_CONFIG.customId, id: INTEGRATION_CONFIG.id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile", mode: "profile",
@ -564,8 +557,7 @@ describe.skipIf(!shouldRunIntegration)(
await processor.processInput(args) await processor.processInput(args)
const profileCalls = fetchSpy.mock.calls.filter( const profileCalls = fetchSpy.mock.calls.filter(
(call) => (call) => typeof call[0] === "string" && call[0].includes("/profile"),
typeof call[0] === "string" && call[0].includes("/v4/profile"),
) )
expect(profileCalls.length).toBeGreaterThan(0) expect(profileCalls.length).toBeGreaterThan(0)
@ -579,8 +571,8 @@ describe.skipIf(!shouldRunIntegration)(
describe("Error handling", () => { describe("Error handling", () => {
it("should handle invalid API key gracefully", async () => { it("should handle invalid API key gracefully", async () => {
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: INTEGRATION_CONFIG.customId, id: INTEGRATION_CONFIG.id,
apiKey: "invalid-api-key-12345", apiKey: "invalid-api-key-12345",
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile", mode: "profile",
@ -602,8 +594,8 @@ describe.skipIf(!shouldRunIntegration)(
it("output processor should handle save errors gracefully", async () => { it("output processor should handle save errors gracefully", async () => {
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "error-test", id: "error-test",
apiKey: "invalid-api-key-12345", apiKey: "invalid-api-key-12345",
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
addMemory: "always", addMemory: "always",

View file

@ -4,6 +4,14 @@
*/ */
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest" import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"
import {
addBody,
errorResponse,
jsonResponse,
profileBody,
requestJson,
requestPath,
} from "../v5-fetch"
import { import {
RequestContext, RequestContext,
MASTRA_THREAD_ID_KEY, MASTRA_THREAD_ID_KEY,
@ -28,8 +36,8 @@ import type {
const TEST_CONFIG = { const TEST_CONFIG = {
apiKey: "test-api-key", apiKey: "test-api-key",
baseUrl: "https://api.supermemory.ai", baseUrl: "https://api.supermemory.ai",
containerTag: "test-mastra-user", namespace: "test-mastra-user",
customId: "test-conversation", id: "test-conversation",
} }
interface MockAgentConfig { interface MockAgentConfig {
@ -86,25 +94,8 @@ const createMockMessageList = (): MessageList & {
} as unknown as MessageList & { calls: { method: string; args: unknown[] }[] } } as unknown as MessageList & { calls: { method: string; args: unknown[] }[] }
} }
const createMockProfileResponse = ( const createMockProfileResponse = profileBody
staticMemories: string[] = [], const createMockConversationResponse = () => addBody("mem-123")
dynamicMemories: string[] = [],
searchResults: string[] = [],
) => ({
profile: {
static: staticMemories.map((memory) => ({ memory })),
dynamic: dynamicMemories.map((memory) => ({ memory })),
},
searchResults: {
results: searchResults.map((memory) => ({ memory })),
},
})
const createMockConversationResponse = () => ({
id: "mem-123",
conversationId: "conv-456",
status: "created",
})
describe("SupermemoryInputProcessor", () => { describe("SupermemoryInputProcessor", () => {
let originalEnv: string | undefined let originalEnv: string | undefined
@ -132,8 +123,8 @@ describe("SupermemoryInputProcessor", () => {
describe("constructor", () => { describe("constructor", () => {
it("should create processor with required options", () => { it("should create processor with required options", () => {
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
}) })
expect(processor.id).toBe("supermemory-input") expect(processor.id).toBe("supermemory-input")
expect(processor.name).toBe("Supermemory Memory Injection") expect(processor.name).toBe("Supermemory Memory Injection")
@ -144,8 +135,8 @@ describe("SupermemoryInputProcessor", () => {
expect(() => { expect(() => {
new SupermemoryInputProcessor({ new SupermemoryInputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
}) })
}).toThrow("SUPERMEMORY_API_KEY is not set") }).toThrow("SUPERMEMORY_API_KEY is not set")
}) })
@ -154,8 +145,8 @@ describe("SupermemoryInputProcessor", () => {
delete process.env.SUPERMEMORY_API_KEY delete process.env.SUPERMEMORY_API_KEY
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
apiKey: "custom-key", apiKey: "custom-key",
}) })
expect(processor.id).toBe("supermemory-input") expect(processor.id).toBe("supermemory-input")
@ -164,20 +155,18 @@ describe("SupermemoryInputProcessor", () => {
describe("processInput", () => { describe("processInput", () => {
it("should inject memories into messageList", async () => { it("should inject memories into messageList", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(
json: () => createMockProfileResponse(
Promise.resolve( ["User likes TypeScript"],
createMockProfileResponse( ["Recent interest in AI"],
["User likes TypeScript"],
["Recent interest in AI"],
),
), ),
}) ),
)
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
mode: "profile", mode: "profile",
}) })
@ -206,15 +195,13 @@ describe("SupermemoryInputProcessor", () => {
}) })
it("should use cached memories on second call with same message", async () => { it("should use cached memories on second call with same message", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockProfileResponse(["Cached memory"])),
json: () => )
Promise.resolve(createMockProfileResponse(["Cached memory"])),
})
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
mode: "profile", mode: "profile",
}) })
@ -248,18 +235,16 @@ describe("SupermemoryInputProcessor", () => {
let callCount = 0 let callCount = 0
fetchMock.mockImplementation(() => { fetchMock.mockImplementation(() => {
callCount++ callCount++
return Promise.resolve({ return Promise.resolve(
ok: true, jsonResponse(
json: () => createMockProfileResponse([`Memory from call ${callCount}`]),
Promise.resolve( ),
createMockProfileResponse([`Memory from call ${callCount}`]), )
),
})
}) })
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
mode: "query", mode: "query",
}) })
@ -289,8 +274,8 @@ describe("SupermemoryInputProcessor", () => {
it("should return messageList in query mode when no user message", async () => { it("should return messageList in query mode when no user message", async () => {
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
mode: "query", mode: "query",
}) })
@ -312,16 +297,11 @@ describe("SupermemoryInputProcessor", () => {
}) })
it("should handle API errors gracefully", async () => { it("should handle API errors gracefully", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () => errorResponse(500))
ok: false,
status: 500,
statusText: "Internal Server Error",
text: () => Promise.resolve("Server error"),
})
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
mode: "profile", mode: "profile",
}) })
@ -342,14 +322,13 @@ describe("SupermemoryInputProcessor", () => {
}) })
it("should use threadId from options", async () => { it("should use threadId from options", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockProfileResponse(["Memory"])),
json: () => Promise.resolve(createMockProfileResponse(["Memory"])), )
})
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "thread-123", id: "thread-123",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
mode: "profile", mode: "profile",
}) })
@ -368,14 +347,13 @@ describe("SupermemoryInputProcessor", () => {
}) })
it("should use threadId from requestContext when not in options", async () => { it("should use threadId from requestContext when not in options", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockProfileResponse(["Memory"])),
json: () => Promise.resolve(createMockProfileResponse(["Memory"])), )
})
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
mode: "profile", mode: "profile",
}) })
@ -398,14 +376,13 @@ describe("SupermemoryInputProcessor", () => {
}) })
it("should handle messages with array content parts", async () => { it("should handle messages with array content parts", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockProfileResponse(["Memory"])),
json: () => Promise.resolve(createMockProfileResponse(["Memory"])), )
})
const processor = new SupermemoryInputProcessor({ const processor = new SupermemoryInputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
mode: "query", mode: "query",
}) })
@ -467,8 +444,8 @@ describe("SupermemoryOutputProcessor", () => {
describe("constructor", () => { describe("constructor", () => {
it("should create processor with required options", () => { it("should create processor with required options", () => {
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
}) })
expect(processor.id).toBe("supermemory-output") expect(processor.id).toBe("supermemory-output")
expect(processor.name).toBe("Supermemory Conversation Save") expect(processor.name).toBe("Supermemory Conversation Save")
@ -477,14 +454,13 @@ describe("SupermemoryOutputProcessor", () => {
describe("processOutputResult", () => { describe("processOutputResult", () => {
it("should save conversation when addMemory is always", async () => { it("should save conversation when addMemory is always", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockConversationResponse()),
json: () => Promise.resolve(createMockConversationResponse()), )
})
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "conv-456", id: "conv-456",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
addMemory: "always", addMemory: "always",
}) })
@ -504,29 +480,23 @@ describe("SupermemoryOutputProcessor", () => {
await processor.processOutputResult(args) await processor.processOutputResult(args)
expect(fetchMock).toHaveBeenCalledTimes(1) expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith( const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect.stringContaining("/v4/conversations"), expect(requestPath(url)).toBe(`/ns/${TEST_CONFIG.namespace}/document`)
expect.objectContaining({ expect(init.method).toBe("POST")
method: "POST", expect(new Headers(init.headers).get("authorization")).toBe(
headers: expect.objectContaining({ `Bearer ${TEST_CONFIG.apiKey}`,
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_CONFIG.apiKey}`,
}),
}),
) )
const callBody = JSON.parse( const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
(fetchMock.mock.calls[0]?.[1] as { body: string }).body, expect(callBody.id).toBe("conv-456")
) expect(String(callBody.content).split("\n\n")).toHaveLength(2)
expect(callBody.conversationId).toBe("conv-456") expect(callBody.dreaming).toBe("instant")
expect(callBody.messages).toHaveLength(2)
expect(callBody.containerTags).toContain(TEST_CONFIG.containerTag)
}) })
it("should not save conversation when addMemory is never", async () => { it("should not save conversation when addMemory is never", async () => {
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "conv-456", id: "conv-456",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
addMemory: "never", addMemory: "never",
}) })
@ -546,15 +516,14 @@ describe("SupermemoryOutputProcessor", () => {
expect(fetchMock).not.toHaveBeenCalled() expect(fetchMock).not.toHaveBeenCalled()
}) })
it("should use customId from options for conversation save", async () => { it("should use id from options for conversation save", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockConversationResponse()),
json: () => Promise.resolve(createMockConversationResponse()), )
})
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "my-custom-id", id: "my-custom-id",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
addMemory: "always", addMemory: "always",
}) })
@ -572,21 +541,18 @@ describe("SupermemoryOutputProcessor", () => {
await processor.processOutputResult(args) await processor.processOutputResult(args)
expect(fetchMock).toHaveBeenCalledTimes(1) expect(fetchMock).toHaveBeenCalledTimes(1)
const callBody = JSON.parse( const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
(fetchMock.mock.calls[0]?.[1] as { body: string }).body, expect(callBody.id).toBe("my-custom-id")
)
expect(callBody.conversationId).toBe("my-custom-id")
}) })
it("should use threadId from requestContext (takes precedence over customId)", async () => { it("should use threadId from requestContext (takes precedence over id)", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockConversationResponse()),
json: () => Promise.resolve(createMockConversationResponse()), )
})
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "fallback-custom-id", id: "fallback-custom-id",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
addMemory: "always", addMemory: "always",
}) })
@ -608,22 +574,19 @@ describe("SupermemoryOutputProcessor", () => {
await processor.processOutputResult(args) await processor.processOutputResult(args)
expect(fetchMock).toHaveBeenCalledTimes(1) expect(fetchMock).toHaveBeenCalledTimes(1)
const callBody = JSON.parse( const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
)
// RequestContext threadId takes precedence for per-request dynamic IDs // RequestContext threadId takes precedence for per-request dynamic IDs
expect(callBody.conversationId).toBe("ctx-thread-789") expect(callBody.id).toBe("ctx-thread-789")
}) })
it("should fall back to customId when requestContext has no threadId", async () => { it("should fall back to id when requestContext has no threadId", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockConversationResponse()),
json: () => Promise.resolve(createMockConversationResponse()), )
})
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "fallback-custom-id", id: "fallback-custom-id",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
addMemory: "always", addMemory: "always",
}) })
@ -641,22 +604,19 @@ describe("SupermemoryOutputProcessor", () => {
await processor.processOutputResult(args) await processor.processOutputResult(args)
expect(fetchMock).toHaveBeenCalledTimes(1) expect(fetchMock).toHaveBeenCalledTimes(1)
const callBody = JSON.parse( const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
(fetchMock.mock.calls[0]?.[1] as { body: string }).body, // Falls back to id when no RequestContext threadId
) expect(callBody.id).toBe("fallback-custom-id")
// Falls back to customId when no RequestContext threadId
expect(callBody.conversationId).toBe("fallback-custom-id")
}) })
it("should skip system messages when saving", async () => { it("should skip system messages when saving", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockConversationResponse()),
json: () => Promise.resolve(createMockConversationResponse()), )
})
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "conv-456", id: "conv-456",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
addMemory: "always", addMemory: "always",
}) })
@ -676,24 +636,19 @@ describe("SupermemoryOutputProcessor", () => {
await processor.processOutputResult(args) await processor.processOutputResult(args)
const callBody = JSON.parse( const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
(fetchMock.mock.calls[0]?.[1] as { body: string }).body, expect(String(callBody.content).split("\n\n")).toHaveLength(2)
) expect(callBody.content).not.toContain("System:")
expect(callBody.messages).toHaveLength(2)
expect(
callBody.messages.every((m: { role: string }) => m.role !== "system"),
).toBe(true)
}) })
it("should handle messages with array content parts", async () => { it("should handle messages with array content parts", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockConversationResponse()),
json: () => Promise.resolve(createMockConversationResponse()), )
})
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "conv-456", id: "conv-456",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
addMemory: "always", addMemory: "always",
}) })
@ -731,23 +686,16 @@ describe("SupermemoryOutputProcessor", () => {
await processor.processOutputResult(args) await processor.processOutputResult(args)
const callBody = JSON.parse( const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
(fetchMock.mock.calls[0]?.[1] as { body: string }).body, expect(String(callBody.content).split("\n\n")).toHaveLength(2)
)
expect(callBody.messages).toHaveLength(2)
}) })
it("should handle save errors gracefully", async () => { it("should handle save errors gracefully", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () => errorResponse(500))
ok: false,
status: 500,
statusText: "Internal Server Error",
text: () => Promise.resolve("Server error"),
})
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "conv-456", id: "conv-456",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
addMemory: "always", addMemory: "always",
}) })
@ -768,8 +716,8 @@ describe("SupermemoryOutputProcessor", () => {
it("should not save when no messages to save", async () => { it("should not save when no messages to save", async () => {
const processor = new SupermemoryOutputProcessor({ const processor = new SupermemoryOutputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "conv-456", id: "conv-456",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
addMemory: "always", addMemory: "always",
}) })
@ -807,8 +755,8 @@ describe("Factory functions", () => {
describe("createSupermemoryProcessor", () => { describe("createSupermemoryProcessor", () => {
it("should create input processor", () => { it("should create input processor", () => {
const processor = createSupermemoryProcessor({ const processor = createSupermemoryProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
}) })
expect(processor).toBeInstanceOf(SupermemoryInputProcessor) expect(processor).toBeInstanceOf(SupermemoryInputProcessor)
expect(processor.id).toBe("supermemory-input") expect(processor.id).toBe("supermemory-input")
@ -816,8 +764,8 @@ describe("Factory functions", () => {
it("should pass options to processor", () => { it("should pass options to processor", () => {
const processor = createSupermemoryProcessor({ const processor = createSupermemoryProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
apiKey: "custom-key", apiKey: "custom-key",
mode: "full", mode: "full",
}) })
@ -828,8 +776,8 @@ describe("Factory functions", () => {
describe("createSupermemoryOutputProcessor", () => { describe("createSupermemoryOutputProcessor", () => {
it("should create output processor", () => { it("should create output processor", () => {
const processor = createSupermemoryOutputProcessor({ const processor = createSupermemoryOutputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
}) })
expect(processor).toBeInstanceOf(SupermemoryOutputProcessor) expect(processor).toBeInstanceOf(SupermemoryOutputProcessor)
expect(processor.id).toBe("supermemory-output") expect(processor.id).toBe("supermemory-output")
@ -837,8 +785,8 @@ describe("Factory functions", () => {
it("should pass options to processor", () => { it("should pass options to processor", () => {
const processor = createSupermemoryOutputProcessor({ const processor = createSupermemoryOutputProcessor({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "conv-123", id: "conv-123",
apiKey: "custom-key", apiKey: "custom-key",
addMemory: "always", addMemory: "always",
}) })
@ -849,8 +797,8 @@ describe("Factory functions", () => {
describe("createSupermemoryProcessors", () => { describe("createSupermemoryProcessors", () => {
it("should create both input and output processors", () => { it("should create both input and output processors", () => {
const { input, output } = createSupermemoryProcessors({ const { input, output } = createSupermemoryProcessors({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
}) })
expect(input).toBeInstanceOf(SupermemoryInputProcessor) expect(input).toBeInstanceOf(SupermemoryInputProcessor)
expect(output).toBeInstanceOf(SupermemoryOutputProcessor) expect(output).toBeInstanceOf(SupermemoryOutputProcessor)
@ -858,8 +806,8 @@ describe("Factory functions", () => {
it("should share options between processors", () => { it("should share options between processors", () => {
const { input, output } = createSupermemoryProcessors({ const { input, output } = createSupermemoryProcessors({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "conv-123", id: "conv-123",
apiKey: "custom-key", apiKey: "custom-key",
mode: "full", mode: "full",
addMemory: "always", addMemory: "always",
@ -894,8 +842,8 @@ describe("withSupermemory", () => {
expect(() => { expect(() => {
withSupermemory(config, { withSupermemory(config, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
}) })
}).toThrow("SUPERMEMORY_API_KEY is not set") }).toThrow("SUPERMEMORY_API_KEY is not set")
}) })
@ -905,8 +853,8 @@ describe("withSupermemory", () => {
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" } const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
const enhanced = withSupermemory(config, { const enhanced = withSupermemory(config, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
apiKey: "custom-key", apiKey: "custom-key",
}) })
@ -919,8 +867,8 @@ describe("withSupermemory", () => {
it("should inject input and output processors", () => { it("should inject input and output processors", () => {
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" } const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
const enhanced = withSupermemory(config, { const enhanced = withSupermemory(config, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
}) })
expect(enhanced.inputProcessors).toHaveLength(1) expect(enhanced.inputProcessors).toHaveLength(1)
@ -937,8 +885,8 @@ describe("withSupermemory", () => {
customProp: "value", customProp: "value",
} }
const enhanced = withSupermemory(config, { const enhanced = withSupermemory(config, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
}) })
expect(enhanced.id).toBe("test-agent") expect(enhanced.id).toBe("test-agent")
@ -959,8 +907,8 @@ describe("withSupermemory", () => {
} }
const enhanced = withSupermemory(config, { const enhanced = withSupermemory(config, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
}) })
expect(enhanced.inputProcessors).toHaveLength(2) expect(enhanced.inputProcessors).toHaveLength(2)
@ -980,8 +928,8 @@ describe("withSupermemory", () => {
} }
const enhanced = withSupermemory(config, { const enhanced = withSupermemory(config, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
}) })
expect(enhanced.outputProcessors).toHaveLength(2) expect(enhanced.outputProcessors).toHaveLength(2)
@ -1000,8 +948,8 @@ describe("withSupermemory", () => {
} }
const enhanced = withSupermemory(config, { const enhanced = withSupermemory(config, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: TEST_CONFIG.customId, id: TEST_CONFIG.id,
}) })
expect(enhanced.inputProcessors).toHaveLength(2) expect(enhanced.inputProcessors).toHaveLength(2)
@ -1017,8 +965,8 @@ describe("withSupermemory", () => {
it("should pass options to processors", () => { it("should pass options to processors", () => {
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" } const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
const enhanced = withSupermemory(config, { const enhanced = withSupermemory(config, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "conv-123", id: "conv-123",
mode: "full", mode: "full",
addMemory: "always", addMemory: "always",
verbose: true, verbose: true,

View file

@ -1,4 +1,5 @@
import type OpenAI from "openai" import type OpenAI from "openai"
import { jsonResponse, profileBody } from "./v5-fetch"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { withSupermemory } from "../src/openai" import { withSupermemory } from "../src/openai"
@ -18,13 +19,7 @@ describe("OpenAI middleware memory context", () => {
it("replaces prior SDK context in chat system messages", async () => { it("replaces prior SDK context in chat system messages", async () => {
vi.stubGlobal( vi.stubGlobal(
"fetch", "fetch",
vi.fn().mockResolvedValue({ vi.fn(async () => jsonResponse(profileBody(["Fresh profile fact"]))),
ok: true,
json: async () => ({
profile: { static: [{ memory: "Fresh profile fact" }], dynamic: [] },
searchResults: { results: [] },
}),
}),
) )
const originalCreate = vi.fn(() => const originalCreate = vi.fn(() =>
Object.assign(Promise.resolve({ choices: [] }), { Object.assign(Promise.resolve({ choices: [] }), {
@ -35,8 +30,8 @@ describe("OpenAI middleware memory context", () => {
chat: { completions: { create: originalCreate } }, chat: { completions: { create: originalCreate } },
} as unknown as OpenAI } as unknown as OpenAI
const wrapped = withSupermemory(client, { const wrapped = withSupermemory(client, {
containerTag: "user-a", namespace: "user-a",
customId: "conversation-a", id: "conversation-a",
mode: "profile", mode: "profile",
addMemory: "never", addMemory: "never",
}) })

View file

@ -6,8 +6,8 @@ const openai = new OpenAI({
}) })
const openaiWithSupermemory = withSupermemory(openai, { const openaiWithSupermemory = withSupermemory(openai, {
containerTag: "user_id_life", namespace: "user_id_life",
customId: "test-conversation", id: "test-conversation",
verbose: true, verbose: true,
mode: "full", mode: "full",
addMemory: "always", addMemory: "always",

View file

@ -17,8 +17,7 @@ async function testMemoryTool() {
} }
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, { const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, {
projectId: "memory-tool-test", namespace: "memory-tool-test",
memoryContainerTag: "claude_memory_test",
baseUrl: process.env.SUPERMEMORY_BASE_URL, baseUrl: process.env.SUPERMEMORY_BASE_URL,
}) })

View file

@ -0,0 +1,66 @@
// Fake v5 API responses for tests that stub globalThis.fetch.
export const jsonResponse = (body: unknown, status = 200): Response =>
new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
})
export const errorResponse = (status = 500, message = "Server error") =>
jsonResponse({ error: message }, status)
export const profileBody = (
staticMemories: string[] = [],
dynamicMemories: string[] = [],
) => ({
profile: {
static: staticMemories.map((memory, index) => ({
id: `static_${index}`,
memory,
})),
dynamic: dynamicMemories.map((memory, index) => ({
id: `dynamic_${index}`,
memory,
})),
buckets: {},
},
})
export const searchBody = (memories: string[] = []) => ({
results: memories.map((memory, index) => ({
id: `mem_${index}`,
memory,
metadata: {},
similarity: 0.9,
isLatest: true,
isInference: false,
system: { createdAt: "2026-01-01", updatedAt: "2026-01-01" },
})),
searchTime: 1,
})
export const addBody = (id = "doc_1") => ({ id, status: "queued" })
export const requestPath = (input: unknown): string =>
new URL(String(input instanceof Request ? input.url : input)).pathname
export const requestJson = (init?: RequestInit): Record<string, unknown> =>
init?.body ? JSON.parse(String(init.body)) : {}
// Routes by path suffix so a single mock serves profile, search, and add calls.
export const v5Router =
(routes: {
profile?: () => unknown
search?: () => unknown
document?: () => unknown
}) =>
async (input: unknown): Promise<Response> => {
const path = requestPath(input)
if (path.endsWith("/profile"))
return jsonResponse(routes.profile?.() ?? profileBody())
if (path.endsWith("/search"))
return jsonResponse(routes.search?.() ?? searchBody())
if (path.endsWith("/document"))
return jsonResponse(routes.document?.() ?? addBody())
return errorResponse(404, `Unexpected path ${path}`)
}

View file

@ -1,3 +1,4 @@
import { jsonResponse, profileBody } from "./v5-fetch"
import { afterEach, describe, expect, it, vi } from "vitest" import { afterEach, describe, expect, it, vi } from "vitest"
import { createSupermemoryHooks } from "../src/voltagent" import { createSupermemoryHooks } from "../src/voltagent"
@ -7,16 +8,10 @@ describe("VoltAgent memory context", () => {
it("replaces prior SDK context in the prepared system message", async () => { it("replaces prior SDK context in the prepared system message", async () => {
vi.stubGlobal( vi.stubGlobal(
"fetch", "fetch",
vi.fn().mockResolvedValue({ vi.fn(async () => jsonResponse(profileBody(["Fresh profile fact"]))),
ok: true,
json: async () => ({
profile: { static: [{ memory: "Fresh profile fact" }], dynamic: [] },
searchResults: { results: [] },
}),
}),
) )
const hooks = createSupermemoryHooks("user-a", { const hooks = createSupermemoryHooks("user-a", {
customId: "conversation-a", id: "conversation-a",
apiKey: "sm_test_key", apiKey: "sm_test_key",
mode: "profile", mode: "profile",
addMemory: "never", addMemory: "never",

View file

@ -1,43 +1,36 @@
import type Supermemory from "supermemory" import type { Supermemory } from "supermemory"
import type { import type {
LanguageModelV2CallOptions, LanguageModelV2CallOptions,
LanguageModelV2Message, LanguageModelV2Message,
} from "@ai-sdk/provider" } from "@ai-sdk/provider"
import { afterEach, describe, expect, it } from "vitest" import { afterEach, describe, expect, it, vi } from "vitest"
import {
type AddConversationParams,
formatConversationText,
} from "../../src/conversations-client"
import { createLogger } from "../../src/shared" import { createLogger } from "../../src/shared"
import { saveMemoryAfterResponse } from "../../src/vercel/middleware" import { saveMemoryAfterResponse } from "../../src/vercel/middleware"
const originalFetch = globalThis.fetch const addConversationMock = vi.hoisted(() => vi.fn())
vi.mock("../../src/conversations-client", async (importOriginal) => ({
...(await importOriginal<typeof import("../../src/conversations-client")>()),
addConversation: addConversationMock,
}))
const persistMessages = async ( const persistMessages = async (
params: LanguageModelV2CallOptions, params: LanguageModelV2CallOptions,
assistantResponseText: string, assistantResponseText: string,
includeToolCalls = true, includeToolCalls = true,
) => { ) => {
let messages: unknown[] | undefined let captured: AddConversationParams | undefined
addConversationMock.mockImplementation(
const fetchStub: typeof fetch = Object.assign( async (conversation: AddConversationParams) => {
async (input: RequestInfo | URL, init?: RequestInit) => { captured = conversation
const url = typeof input === "string" ? input : input.toString() return { id: "conversation-id", status: "queued" }
expect(url).toContain("/v4/conversations")
const body = typeof init?.body === "string" ? init.body : ""
messages = (JSON.parse(body) as { messages: unknown[] }).messages
return new Response(
JSON.stringify({
id: "document-id",
conversationId: "conversation-id",
status: "done",
}),
{ status: 200 },
)
}, },
{ preconnect: originalFetch.preconnect },
) )
globalThis.fetch = fetchStub
await saveMemoryAfterResponse( await saveMemoryAfterResponse(
{} as Supermemory, {} as Supermemory,
"user-id", "user-id",
@ -50,11 +43,47 @@ const persistMessages = async (
includeToolCalls, includeToolCalls,
) )
return messages expect(captured?.namespace).toBe("user-id")
expect(captured?.id).toBe("conversation-id")
return captured?.messages
} }
afterEach(() => { afterEach(() => {
globalThis.fetch = originalFetch addConversationMock.mockReset()
})
describe("formatConversationText", () => {
it("renders roles, tool calls, and images as plain text", () => {
expect(
formatConversationText([
{ role: "user", content: "Hi" },
{
role: "assistant",
content: "",
tool_calls: [
{
id: "call-1",
type: "function",
function: { name: "search", arguments: '{"q":"x"}' },
},
],
},
{
role: "user",
content: [
{ type: "text", text: "Look" },
{ type: "image_url", imageUrl: { url: "https://x.test/a.png" } },
{
type: "image_url",
imageUrl: { url: "data:image/png;base64,AA" },
},
],
},
]),
).toBe(
'User: Hi\n\nAssistant: [tool call: search({"q":"x"})]\n\nUser: Look [image: https://x.test/a.png] [image]',
)
})
}) })
describe("convertToConversationMessages", () => { describe("convertToConversationMessages", () => {

View file

@ -13,7 +13,7 @@ import "dotenv/config"
const INTEGRATION_CONFIG = { const INTEGRATION_CONFIG = {
apiKey: process.env.SUPERMEMORY_API_KEY || "", apiKey: process.env.SUPERMEMORY_API_KEY || "",
baseUrl: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai", baseUrl: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai",
containerTag: "integration-test-vercel-wrapper", namespace: "integration-test-vercel-wrapper",
} }
const shouldRunIntegration = !!process.env.SUPERMEMORY_API_KEY const shouldRunIntegration = !!process.env.SUPERMEMORY_API_KEY
@ -97,8 +97,8 @@ describe.skipIf(!shouldRunIntegration)(
createIntegrationMockModel() createIntegrationMockModel()
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-generate", id: "test-generate",
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "profile", mode: "profile",
}) })
@ -123,11 +123,11 @@ describe.skipIf(!shouldRunIntegration)(
const { model } = createIntegrationMockModel() const { model } = createIntegrationMockModel()
const fetchSpy = vi.spyOn(globalThis, "fetch") const fetchSpy = vi.spyOn(globalThis, "fetch")
const customId = `test-generate-${Date.now()}` const id = `test-generate-${Date.now()}`
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId, id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "profile", mode: "profile",
addMemory: "always", addMemory: "always",
@ -150,26 +150,25 @@ describe.skipIf(!shouldRunIntegration)(
// Wait for background save to complete // Wait for background save to complete
await new Promise((resolve) => setTimeout(resolve, 2000)) await new Promise((resolve) => setTimeout(resolve, 2000))
// Verify /v4/conversations was called for saving // Verify the conversation was saved as a document
const conversationCalls = fetchSpy.mock.calls.filter( const conversationCalls = fetchSpy.mock.calls.filter(
(call) => (call) =>
typeof call[0] === "string" && typeof call[0] === "string" && call[0].includes("/document"),
call[0].includes("/v4/conversations"),
) )
expect(conversationCalls.length).toBeGreaterThan(0) expect(conversationCalls.length).toBeGreaterThan(0)
fetchSpy.mockRestore() fetchSpy.mockRestore()
}) })
it("should work with customId for grouped memories", async () => { it("should work with id for grouped memories", async () => {
const { model, getCapturedGenerateParams } = const { model, getCapturedGenerateParams } =
createIntegrationMockModel() createIntegrationMockModel()
const customId = `test-conversation-${Date.now()}` const id = `test-conversation-${Date.now()}`
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId, id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "profile", mode: "profile",
}) })
@ -196,8 +195,8 @@ describe.skipIf(!shouldRunIntegration)(
const { model, getCapturedStreamParams } = createIntegrationMockModel() const { model, getCapturedStreamParams } = createIntegrationMockModel()
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-stream", id: "test-stream",
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "profile", mode: "profile",
}) })
@ -230,11 +229,11 @@ describe.skipIf(!shouldRunIntegration)(
const { model } = createIntegrationMockModel() const { model } = createIntegrationMockModel()
const fetchSpy = vi.spyOn(globalThis, "fetch") const fetchSpy = vi.spyOn(globalThis, "fetch")
const customId = `test-stream-${Date.now()}` const id = `test-stream-${Date.now()}`
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId, id,
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "profile", mode: "profile",
addMemory: "always", addMemory: "always",
@ -262,8 +261,7 @@ describe.skipIf(!shouldRunIntegration)(
// Verify save was attempted // Verify save was attempted
const conversationCalls = fetchSpy.mock.calls.filter( const conversationCalls = fetchSpy.mock.calls.filter(
(call) => (call) =>
typeof call[0] === "string" && typeof call[0] === "string" && call[0].includes("/document"),
call[0].includes("/v4/conversations"),
) )
expect(conversationCalls.length).toBeGreaterThan(0) expect(conversationCalls.length).toBeGreaterThan(0)
@ -274,8 +272,8 @@ describe.skipIf(!shouldRunIntegration)(
const { model } = createIntegrationMockModel() const { model } = createIntegrationMockModel()
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-chunks", id: "test-chunks",
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "profile", mode: "profile",
}) })
@ -313,8 +311,8 @@ describe.skipIf(!shouldRunIntegration)(
const fetchSpy = vi.spyOn(globalThis, "fetch") const fetchSpy = vi.spyOn(globalThis, "fetch")
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-profile", id: "test-profile",
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "profile", mode: "profile",
}) })
@ -328,20 +326,19 @@ describe.skipIf(!shouldRunIntegration)(
], ],
}) })
// Verify /v4/profile was called // Verify the profile was fetched
const profileCalls = fetchSpy.mock.calls.filter( const profileCalls = fetchSpy.mock.calls.filter(
(call) => (call) => typeof call[0] === "string" && call[0].includes("/profile"),
typeof call[0] === "string" && call[0].includes("/v4/profile"),
) )
expect(profileCalls.length).toBeGreaterThan(0) expect(profileCalls.length).toBeGreaterThan(0)
// Verify the request body does NOT contain 'q' for profile mode // Verify the profile request has no query
const profileCall = profileCalls[0] const profileCall = profileCalls[0]
if (profileCall?.[1]) { if (profileCall?.[1]) {
const body = JSON.parse( const body = JSON.parse(
(profileCall[1] as RequestInit).body as string, (profileCall[1] as RequestInit).body as string,
) )
expect(body.q).toBeUndefined() expect(body.query).toBeUndefined()
} }
fetchSpy.mockRestore() fetchSpy.mockRestore()
@ -352,8 +349,8 @@ describe.skipIf(!shouldRunIntegration)(
const fetchSpy = vi.spyOn(globalThis, "fetch") const fetchSpy = vi.spyOn(globalThis, "fetch")
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-query", id: "test-query",
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "query", mode: "query",
}) })
@ -367,20 +364,19 @@ describe.skipIf(!shouldRunIntegration)(
], ],
}) })
// Verify /v4/profile was called with query // Verify the search ran with the query
const profileCalls = fetchSpy.mock.calls.filter( const profileCalls = fetchSpy.mock.calls.filter(
(call) => (call) => typeof call[0] === "string" && call[0].includes("/search"),
typeof call[0] === "string" && call[0].includes("/v4/profile"),
) )
expect(profileCalls.length).toBeGreaterThan(0) expect(profileCalls.length).toBeGreaterThan(0)
// Verify the request body contains 'q' // Verify the request body contains the query
const profileCall = profileCalls[0] const profileCall = profileCalls[0]
if (profileCall?.[1]) { if (profileCall?.[1]) {
const body = JSON.parse( const body = JSON.parse(
(profileCall[1] as RequestInit).body as string, (profileCall[1] as RequestInit).body as string,
) )
expect(body.q).toBe("What are my favorite foods?") expect(body.query).toBe("What are my favorite foods?")
} }
fetchSpy.mockRestore() fetchSpy.mockRestore()
@ -391,8 +387,8 @@ describe.skipIf(!shouldRunIntegration)(
const fetchSpy = vi.spyOn(globalThis, "fetch") const fetchSpy = vi.spyOn(globalThis, "fetch")
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-full", id: "test-full",
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "full", mode: "full",
}) })
@ -406,10 +402,9 @@ describe.skipIf(!shouldRunIntegration)(
], ],
}) })
// Verify /v4/profile was called with query // Verify the search ran with the query
const profileCalls = fetchSpy.mock.calls.filter( const profileCalls = fetchSpy.mock.calls.filter(
(call) => (call) => typeof call[0] === "string" && call[0].includes("/search"),
typeof call[0] === "string" && call[0].includes("/v4/profile"),
) )
expect(profileCalls.length).toBeGreaterThan(0) expect(profileCalls.length).toBeGreaterThan(0)
@ -418,7 +413,7 @@ describe.skipIf(!shouldRunIntegration)(
const body = JSON.parse( const body = JSON.parse(
(profileCall[1] as RequestInit).body as string, (profileCall[1] as RequestInit).body as string,
) )
expect(body.q).toBe("Full mode query test") expect(body.query).toBe("Full mode query test")
} }
fetchSpy.mockRestore() fetchSpy.mockRestore()
@ -436,8 +431,8 @@ describe.skipIf(!shouldRunIntegration)(
}) => `<custom-memories>${data.userMemories}</custom-memories>` }) => `<custom-memories>${data.userMemories}</custom-memories>`
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-template", id: "test-template",
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "profile", mode: "profile",
promptTemplate: customTemplate, promptTemplate: customTemplate,
@ -463,8 +458,8 @@ describe.skipIf(!shouldRunIntegration)(
createIntegrationMockModel() createIntegrationMockModel()
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-verbose", id: "test-verbose",
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "profile", mode: "profile",
verbose: true, verbose: true,
@ -490,8 +485,8 @@ describe.skipIf(!shouldRunIntegration)(
// Use the configured base URL (or default) // Use the configured base URL (or default)
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-baseurl", id: "test-baseurl",
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "profile", mode: "profile",
baseUrl: INTEGRATION_CONFIG.baseUrl, baseUrl: INTEGRATION_CONFIG.baseUrl,
@ -508,8 +503,7 @@ describe.skipIf(!shouldRunIntegration)(
// Verify the correct base URL was used // Verify the correct base URL was used
const profileCalls = fetchSpy.mock.calls.filter( const profileCalls = fetchSpy.mock.calls.filter(
(call) => (call) => typeof call[0] === "string" && call[0].includes("/profile"),
typeof call[0] === "string" && call[0].includes("/v4/profile"),
) )
expect(profileCalls.length).toBeGreaterThan(0) expect(profileCalls.length).toBeGreaterThan(0)
@ -530,8 +524,8 @@ describe.skipIf(!shouldRunIntegration)(
) )
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-error", id: "test-error",
apiKey: INTEGRATION_CONFIG.apiKey, apiKey: INTEGRATION_CONFIG.apiKey,
mode: "profile", mode: "profile",
}) })
@ -553,8 +547,8 @@ describe.skipIf(!shouldRunIntegration)(
createIntegrationMockModel() createIntegrationMockModel()
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-invalid-key", id: "test-invalid-key",
apiKey: "invalid-api-key-12345", apiKey: "invalid-api-key-12345",
mode: "profile", mode: "profile",
}) })
@ -576,8 +570,8 @@ describe.skipIf(!shouldRunIntegration)(
const { model } = createIntegrationMockModel() const { model } = createIntegrationMockModel()
const wrapped = withSupermemory(model, { const wrapped = withSupermemory(model, {
containerTag: INTEGRATION_CONFIG.containerTag, namespace: INTEGRATION_CONFIG.namespace,
customId: "test-invalid-strict", id: "test-invalid-strict",
apiKey: "invalid-api-key-12345", apiKey: "invalid-api-key-12345",
mode: "profile", mode: "profile",
skipMemoryOnError: false, skipMemoryOnError: false,

View file

@ -14,12 +14,18 @@ import type {
LanguageModelV2Message, LanguageModelV2Message,
} from "@ai-sdk/provider" } from "@ai-sdk/provider"
import "dotenv/config" import "dotenv/config"
import {
errorResponse,
jsonResponse,
profileBody,
requestPath,
} from "../v5-fetch"
// Test configuration // Test configuration
const TEST_CONFIG = { const TEST_CONFIG = {
apiKey: process.env.SUPERMEMORY_API_KEY || "test-api-key", apiKey: process.env.SUPERMEMORY_API_KEY || "test-api-key",
baseURL: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai", baseUrl: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai",
containerTag: "test-vercel-wrapper", namespace: "test-vercel-wrapper",
} }
// Mock language model for testing // Mock language model for testing
@ -32,20 +38,7 @@ const createMockLanguageModel = (): LanguageModelV2 => ({
doStream: vi.fn(), doStream: vi.fn(),
}) })
// Mock profile API response const createMockProfileResponse = profileBody
const createMockProfileResponse = (
staticMemories: string[] = [],
dynamicMemories: string[] = [],
searchResults: string[] = [],
) => ({
profile: {
static: staticMemories.map((memory) => ({ memory })),
dynamic: dynamicMemories.map((memory) => ({ memory })),
},
searchResults: {
results: searchResults.map((memory) => ({ memory })),
},
})
describe("Unit: withSupermemory", () => { describe("Unit: withSupermemory", () => {
let originalEnv: string | undefined let originalEnv: string | undefined
@ -74,31 +67,31 @@ describe("Unit: withSupermemory", () => {
expect(() => { expect(() => {
withSupermemory(mockModel, { withSupermemory(mockModel, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "test-id", id: "test-id",
}) })
}).toThrow("SUPERMEMORY_API_KEY is not set") }).toThrow("SUPERMEMORY_API_KEY is not set")
}) })
it("should throw error if customId is missing or empty", () => { it("should throw error if id is missing or empty", () => {
process.env.SUPERMEMORY_API_KEY = "test-key" process.env.SUPERMEMORY_API_KEY = "test-key"
const mockModel = createMockLanguageModel() const mockModel = createMockLanguageModel()
// omitted customId (plain JS caller) // omitted id (plain JS caller)
expect(() => { expect(() => {
withSupermemory(mockModel, { withSupermemory(mockModel, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
} as any) } as any)
}).toThrow("customId is required") }).toThrow("id is required")
// empty string // empty string
expect(() => { expect(() => {
withSupermemory(mockModel, { withSupermemory(mockModel, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "", id: "",
}) })
}).toThrow("customId is required") }).toThrow("id is required")
}) })
it("should successfully create wrapped model with valid API key", () => { it("should successfully create wrapped model with valid API key", () => {
@ -106,8 +99,8 @@ describe("Unit: withSupermemory", () => {
const mockModel = createMockLanguageModel() const mockModel = createMockLanguageModel()
const wrappedModel = withSupermemory(mockModel, { const wrappedModel = withSupermemory(mockModel, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "test-id", id: "test-id",
}) })
expect(wrappedModel).toBeDefined() expect(wrappedModel).toBeDefined()
@ -127,8 +120,8 @@ describe("Unit: withSupermemory", () => {
} }
const inner = Object.create(proto) as LanguageModelV2 const inner = Object.create(proto) as LanguageModelV2
const wrappedModel = withSupermemory(inner, { const wrappedModel = withSupermemory(inner, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "test-id", id: "test-id",
}) })
expect(wrappedModel.specificationVersion).toBe("v2") expect(wrappedModel.specificationVersion).toBe("v2")
@ -146,16 +139,14 @@ describe("Unit: withSupermemory", () => {
}) })
it("should cache memories on first call (new turn)", async () => { it("should cache memories on first call (new turn)", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockProfileResponse(["Cached memory"])),
json: () => )
Promise.resolve(createMockProfileResponse(["Cached memory"])),
})
const ctx = createSupermemoryContext({ const ctx = createSupermemoryContext({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
customId: "test-id", id: "test-id",
mode: "profile", mode: "profile",
}) })
@ -171,7 +162,7 @@ describe("Unit: withSupermemory", () => {
await transformParamsWithMemory(params, ctx) await transformParamsWithMemory(params, ctx)
expect(ctx.memoryCache).toBeDefined() expect(ctx.memoryCache).toBeDefined()
const turnKey = `${TEST_CONFIG.containerTag}:test-id:profile:Hello` const turnKey = `${TEST_CONFIG.namespace}:test-id:profile:Hello`
const cachedMemories = ctx.memoryCache.get(turnKey) const cachedMemories = ctx.memoryCache.get(turnKey)
expect(cachedMemories).toBeDefined() expect(cachedMemories).toBeDefined()
expect(cachedMemories).toContain("Cached memory") expect(cachedMemories).toContain("Cached memory")
@ -179,16 +170,14 @@ describe("Unit: withSupermemory", () => {
}) })
it("should use cached memories on continuation step (no new fetch)", async () => { it("should use cached memories on continuation step (no new fetch)", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockProfileResponse(["Cached memory"])),
json: () => )
Promise.resolve(createMockProfileResponse(["Cached memory"])),
})
const ctx = createSupermemoryContext({ const ctx = createSupermemoryContext({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
customId: "test-id", id: "test-id",
mode: "profile", mode: "profile",
}) })
@ -249,19 +238,17 @@ describe("Unit: withSupermemory", () => {
let callCount = 0 let callCount = 0
fetchMock.mockImplementation(() => { fetchMock.mockImplementation(() => {
callCount++ callCount++
return Promise.resolve({ return Promise.resolve(
ok: true, jsonResponse(
json: () => createMockProfileResponse([`Memory from call ${callCount}`]),
Promise.resolve( ),
createMockProfileResponse([`Memory from call ${callCount}`]), )
),
})
}) })
const ctx = createSupermemoryContext({ const ctx = createSupermemoryContext({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
customId: "test-id", id: "test-id",
mode: "profile", mode: "profile",
}) })
@ -303,11 +290,9 @@ describe("Unit: withSupermemory", () => {
}) })
it("replaces the prior SDK memory block instead of accumulating context", async () => { it("replaces the prior SDK memory block instead of accumulating context", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockProfileResponse(["Fresh profile fact"])),
json: () => )
Promise.resolve(createMockProfileResponse(["Fresh profile fact"])),
})
const inner = createMockLanguageModel() const inner = createMockLanguageModel()
vi.mocked(inner.doGenerate).mockResolvedValue({ vi.mocked(inner.doGenerate).mockResolvedValue({
@ -317,8 +302,8 @@ describe("Unit: withSupermemory", () => {
warnings: [], warnings: [],
}) })
const wrapped = withSupermemory(inner, { const wrapped = withSupermemory(inner, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "conversation-a", id: "conversation-a",
mode: "profile", mode: "profile",
addMemory: "never", addMemory: "never",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
@ -353,18 +338,15 @@ describe("Unit: withSupermemory", () => {
}) })
it("keeps concurrent user contexts isolated", async () => { it("keeps concurrent user contexts isolated", async () => {
fetchMock.mockImplementation(async (_url, init) => { fetchMock.mockImplementation(async (url) =>
const body = JSON.parse(String(init?.body ?? "{}")) jsonResponse(
return { createMockProfileResponse([
ok: true, requestPath(url).includes("/ns/user-a/")
json: async () => ? "Fact for Alice"
createMockProfileResponse([ : "Fact for Bob",
body.containerTag === "user-a" ]),
? "Fact for Alice" ),
: "Fact for Bob", )
]),
}
})
const innerA = createMockLanguageModel() const innerA = createMockLanguageModel()
const innerB = createMockLanguageModel() const innerB = createMockLanguageModel()
vi.mocked(innerA.doGenerate).mockResolvedValue({ vi.mocked(innerA.doGenerate).mockResolvedValue({
@ -380,14 +362,14 @@ describe("Unit: withSupermemory", () => {
warnings: [], warnings: [],
}) })
const wrappedA = withSupermemory(innerA, { const wrappedA = withSupermemory(innerA, {
containerTag: "user-a", namespace: "user-a",
customId: "conversation-a", id: "conversation-a",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
addMemory: "never", addMemory: "never",
}) })
const wrappedB = withSupermemory(innerB, { const wrappedB = withSupermemory(innerB, {
containerTag: "user-b", namespace: "user-b",
customId: "conversation-b", id: "conversation-b",
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
addMemory: "never", addMemory: "never",
}) })
@ -427,17 +409,12 @@ describe("Unit: withSupermemory", () => {
}) })
it("should handle API errors gracefully", async () => { it("should handle API errors gracefully", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () => errorResponse(500))
ok: false,
status: 500,
statusText: "Internal Server Error",
text: () => Promise.resolve("Server error"),
})
const ctx = createSupermemoryContext({ const ctx = createSupermemoryContext({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
customId: "test-id", id: "test-id",
mode: "profile", mode: "profile",
}) })
@ -457,9 +434,9 @@ describe("Unit: withSupermemory", () => {
it("should handle empty prompt array", async () => { it("should handle empty prompt array", async () => {
const ctx = createSupermemoryContext({ const ctx = createSupermemoryContext({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
customId: "test-id", id: "test-id",
mode: "query", mode: "query",
}) })
@ -475,9 +452,9 @@ describe("Unit: withSupermemory", () => {
it("should handle user message with empty content array in query mode", async () => { it("should handle user message with empty content array in query mode", async () => {
const ctx = createSupermemoryContext({ const ctx = createSupermemoryContext({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
customId: "test-id", id: "test-id",
mode: "query", mode: "query",
}) })
@ -497,15 +474,14 @@ describe("Unit: withSupermemory", () => {
}) })
it("should not mutate the original params.prompt array", async () => { it("should not mutate the original params.prompt array", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () =>
ok: true, jsonResponse(createMockProfileResponse(["Memory"])),
json: () => Promise.resolve(createMockProfileResponse(["Memory"])), )
})
const ctx = createSupermemoryContext({ const ctx = createSupermemoryContext({
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
apiKey: TEST_CONFIG.apiKey, apiKey: TEST_CONFIG.apiKey,
customId: "test-id", id: "test-id",
mode: "profile", mode: "profile",
}) })
@ -547,12 +523,7 @@ describe("Unit: withSupermemory", () => {
}) })
it("continues without memories when profile fetch fails (default skip)", async () => { it("continues without memories when profile fetch fails (default skip)", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () => errorResponse(500))
ok: false,
status: 500,
statusText: "Internal Server Error",
text: () => Promise.resolve("err"),
})
const inner = createMockLanguageModel() const inner = createMockLanguageModel()
vi.mocked(inner.doGenerate).mockResolvedValue({ vi.mocked(inner.doGenerate).mockResolvedValue({
@ -567,8 +538,8 @@ describe("Unit: withSupermemory", () => {
}) })
const wrapped = withSupermemory(inner, { const wrapped = withSupermemory(inner, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "test-id", id: "test-id",
apiKey: "k", apiKey: "k",
}) })
@ -582,17 +553,12 @@ describe("Unit: withSupermemory", () => {
}) })
it("throws when skipMemoryOnError is false and profile fetch fails", async () => { it("throws when skipMemoryOnError is false and profile fetch fails", async () => {
fetchMock.mockResolvedValue({ fetchMock.mockImplementation(async () => errorResponse(500))
ok: false,
status: 500,
statusText: "Internal Server Error",
text: () => Promise.resolve("err"),
})
const inner = createMockLanguageModel() const inner = createMockLanguageModel()
const wrapped = withSupermemory(inner, { const wrapped = withSupermemory(inner, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "test-id", id: "test-id",
apiKey: "k", apiKey: "k",
skipMemoryOnError: false, skipMemoryOnError: false,
}) })
@ -632,8 +598,8 @@ describe("Unit: withSupermemory", () => {
}) })
const wrapped = withSupermemory(inner, { const wrapped = withSupermemory(inner, {
containerTag: TEST_CONFIG.containerTag, namespace: TEST_CONFIG.namespace,
customId: "test-id", id: "test-id",
apiKey: "k", apiKey: "k",
}) })

View file

@ -140,7 +140,7 @@ Supermemory supports two complementary integration styles:
| Path | When to use | Packages | | Path | When to use | Packages |
|------|-------------|----------| |------|-------------|----------|
| **Tools** | Model explicitly decides when to search, add, list, or forget | `@supermemory/tools/ai-sdk` or `@supermemory/ai-sdk` (TypeScript), `supermemory-openai-sdk` (Python) | | **Tools** | Model explicitly decides when to search, add, list, or forget | `@supermemory/tools/ai-sdk` (TypeScript), `supermemory-openai-sdk` (Python) |
| **Middleware** | Auto-inject profile context before each request and save conversations after | `@supermemory/tools/ai-sdk` (Vercel AI SDK), `@supermemory/tools/openai` (OpenAI), `supermemory-openai-sdk` (Python) | | **Middleware** | Auto-inject profile context before each request and save conversations after | `@supermemory/tools/ai-sdk` (Vercel AI SDK), `@supermemory/tools/openai` (OpenAI), `supermemory-openai-sdk` (Python) |
**Tools (7 canonical operations):** **Tools (7 canonical operations):**
@ -179,7 +179,6 @@ With multiple configured container tags, `searchMemories`, `getProfile`, and `me
**TypeScript (Vercel AI SDK):** **TypeScript (Vercel AI SDK):**
```typescript ```typescript
import { supermemoryTools } from "@supermemory/tools/ai-sdk" import { supermemoryTools } from "@supermemory/tools/ai-sdk"
// or re-exported from "@supermemory/ai-sdk"
const tools = supermemoryTools(process.env.SUPERMEMORY_API_KEY!, { const tools = supermemoryTools(process.env.SUPERMEMORY_API_KEY!, {
containerTags: ["user_123"], containerTags: ["user_123"],

View file

@ -458,7 +458,7 @@ await client.add({
### Vercel AI SDK ### Vercel AI SDK
#### Agent tools (`@supermemory/tools/ai-sdk` / `@supermemory/ai-sdk`) #### Agent tools (`@supermemory/tools/ai-sdk`)
For models that call memory operations explicitly, use the 7-tool set instead of hand-rolling SDK calls: For models that call memory operations explicitly, use the 7-tool set instead of hand-rolling SDK calls: