mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-07 02:58:11 +00:00
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:
parent
10888a94bc
commit
5c24e67d60
81 changed files with 2181 additions and 3417 deletions
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
|
|
@ -41,12 +41,6 @@ jobs:
|
|||
echo "tools=true" >> "$GITHUB_OUTPUT"
|
||||
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
|
||||
echo "sdk_playground=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
|
|
@ -81,21 +75,9 @@ jobs:
|
|||
run: bun run --cwd packages/tools test:unit
|
||||
|
||||
- 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
|
||||
|
||||
- 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
|
||||
if: steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||
run: bun run --cwd apps/sdk-playground check-types:app
|
||||
|
|
|
|||
|
|
@ -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
|
||||
> browser-entered keys only in memory unless you opt into tab-scoped
|
||||
> `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.
|
||||
|
||||
## 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 |
|
||||
| AI SDK + tools | explicit | model calls 7 memory tools via `generateText` |
|
||||
| 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 tools | explicit | `SupermemoryTools` loop |
|
||||
| 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=...
|
||||
```
|
||||
|
||||
The playground scripts build `@supermemory/tools` first and
|
||||
`@supermemory/ai-sdk` second before starting, type-checking, or building the
|
||||
Next.js app. Development mode also watches both workspace packages.
|
||||
The playground scripts build `@supermemory/tools` before starting, type-checking, or building the
|
||||
Next.js app. Development mode also watches the workspace package.
|
||||
|
||||
## Run
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { NextConfig } from "next"
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: ["@supermemory/tools", "@supermemory/ai-sdk"],
|
||||
transpilePackages: ["@supermemory/tools"],
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
"portless": { "name": "sdk.dev.supermemory", "script": "dev:app" },
|
||||
"scripts": {
|
||||
"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: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": "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\"",
|
||||
|
|
@ -18,14 +18,13 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^2.0.22",
|
||||
"@supermemory/ai-sdk": "workspace:*",
|
||||
"@supermemory/tools": "workspace:*",
|
||||
"ai": "^5.0.113",
|
||||
"next": "^16.3.3",
|
||||
"openai": "^4.104.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"supermemory": "^4.25.4"
|
||||
"supermemory": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ dependencies = [
|
|||
"httpx>=0.28.0",
|
||||
"uvicorn[standard]>=0.32.0",
|
||||
"python-dotenv>=1.0.1",
|
||||
"supermemory>=3.50.0",
|
||||
"openai>=1.102.0",
|
||||
"supermemory-openai-sdk[async]",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import re
|
|||
import time
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Literal, Optional
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, Header, Query
|
||||
|
|
@ -27,13 +27,16 @@ CHAT_TIMEOUT_SECONDS = 115.0
|
|||
CONTEXT_DEBUG_TIMEOUT_SECONDS = 10.0
|
||||
DIRECT_SAVE_TIMEOUT_SECONDS = 10.0
|
||||
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_MESSAGES = 64
|
||||
MAX_TOTAL_MESSAGE_LENGTH = 100_000
|
||||
MAX_API_KEY_LENGTH = 1_024
|
||||
MAX_CONTAINER_TAG_LENGTH = 100
|
||||
MAX_NAMESPACE_LENGTH = 100
|
||||
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.
|
||||
|
||||
|
|
@ -82,11 +85,11 @@ class ChatRequest(BaseModel):
|
|||
"py-supermemory-direct",
|
||||
]
|
||||
messages: list[ChatMessage] = Field(min_length=1, max_length=MAX_MESSAGES)
|
||||
containerTag: str = Field(
|
||||
namespace: str = Field(
|
||||
default="sdk-playground",
|
||||
min_length=1,
|
||||
max_length=MAX_CONTAINER_TAG_LENGTH,
|
||||
pattern=CONTAINER_TAG_PATTERN,
|
||||
max_length=MAX_NAMESPACE_LENGTH,
|
||||
pattern=NAMESPACE_PATTERN,
|
||||
)
|
||||
conversationId: str = Field(
|
||||
min_length=1,
|
||||
|
|
@ -114,11 +117,11 @@ class ChatRequest(BaseModel):
|
|||
|
||||
|
||||
class ContextRequest(BaseModel):
|
||||
containerTag: str = Field(
|
||||
namespace: str = Field(
|
||||
default="sdk-playground",
|
||||
min_length=1,
|
||||
max_length=MAX_CONTAINER_TAG_LENGTH,
|
||||
pattern=CONTAINER_TAG_PATTERN,
|
||||
max_length=MAX_NAMESPACE_LENGTH,
|
||||
pattern=NAMESPACE_PATTERN,
|
||||
)
|
||||
query: Optional[str] = Field(default=None, max_length=MAX_MESSAGE_LENGTH)
|
||||
apiKeys: Optional[SupermemoryApiKeys] = None
|
||||
|
|
@ -172,7 +175,7 @@ def public_error(error: Exception, *secrets: str) -> str:
|
|||
|
||||
async def chat_openai_middleware(
|
||||
messages: list[ChatMessage],
|
||||
container_tag: str,
|
||||
namespace: str,
|
||||
conversation_id: str,
|
||||
memory_mode: str,
|
||||
middleware_config: MiddlewareConfig,
|
||||
|
|
@ -189,7 +192,7 @@ async def chat_openai_middleware(
|
|||
max_retries=1,
|
||||
),
|
||||
OpenAIMiddlewareOptions(
|
||||
container_tag=container_tag,
|
||||
container_tag=namespace,
|
||||
custom_id=conversation_id,
|
||||
mode=memory_mode,
|
||||
add_memory=middleware_config.addMemory,
|
||||
|
|
@ -221,7 +224,7 @@ async def chat_openai_middleware(
|
|||
|
||||
async def chat_openai_tools(
|
||||
messages: list[ChatMessage],
|
||||
container_tag: str,
|
||||
namespace: str,
|
||||
sm_key: str,
|
||||
oai_key: str,
|
||||
) -> tuple[str, list[dict[str, Any]]]:
|
||||
|
|
@ -235,7 +238,7 @@ async def chat_openai_tools(
|
|||
)
|
||||
config: dict[str, Any] = {
|
||||
"base_url": supermemory_base_url(),
|
||||
"container_tags": [container_tag],
|
||||
"container_tags": [namespace],
|
||||
}
|
||||
|
||||
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 []
|
||||
|
||||
|
||||
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:
|
||||
if hasattr(item, "model_dump"):
|
||||
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)
|
||||
|
||||
|
||||
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 = readable[:40] or "session"
|
||||
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)
|
||||
|
||||
|
||||
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(
|
||||
container_tag: str,
|
||||
namespace: str,
|
||||
sm_key: str,
|
||||
query: Optional[str] = None,
|
||||
*,
|
||||
include: Optional[list[str]] = None,
|
||||
) -> 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(
|
||||
api_key=sm_key,
|
||||
base_url=supermemory_base_url(),
|
||||
timeout=HTTP_TIMEOUT_SECONDS,
|
||||
)
|
||||
request: dict[str, Any] = {"container_tag": container_tag}
|
||||
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)
|
||||
profile = responses[0].json().get("profile") or {}
|
||||
search = responses[1].json() if len(responses) > 1 else {}
|
||||
return {
|
||||
"static": profile.get("static") or [],
|
||||
"dynamic": profile.get("dynamic") or [],
|
||||
"searchResults": search.get("results") or [],
|
||||
}
|
||||
|
||||
|
||||
async def chat_supermemory_direct(
|
||||
messages: list[ChatMessage],
|
||||
container_tag: str,
|
||||
namespace: str,
|
||||
conversation_id: str,
|
||||
sm_key: str,
|
||||
oai_key: str,
|
||||
) -> 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 supermemory import AsyncSupermemory
|
||||
|
||||
sm_client = AsyncSupermemory(
|
||||
api_key=sm_key,
|
||||
base_url=supermemory_base_url(),
|
||||
timeout=HTTP_TIMEOUT_SECONDS,
|
||||
)
|
||||
openai_client = AsyncOpenAI(
|
||||
api_key=oai_key,
|
||||
timeout=HTTP_TIMEOUT_SECONDS,
|
||||
|
|
@ -387,11 +392,7 @@ async def chat_supermemory_direct(
|
|||
user_messages = [m for m in messages if m.role == "user"]
|
||||
last_user = user_messages[-1].content if user_messages else ""
|
||||
|
||||
profile_response = await sm_client.profile(
|
||||
container_tag=container_tag,
|
||||
**({"q": last_user} if last_user else {}),
|
||||
)
|
||||
profile_context = extract_profile_context(profile_response)
|
||||
profile_context = await fetch_profile_context(namespace, sm_key, last_user)
|
||||
context = "\n".join(
|
||||
(
|
||||
"Profile static: "
|
||||
|
|
@ -418,40 +419,39 @@ async def chat_supermemory_direct(
|
|||
)
|
||||
assistant_text = response.choices[0].message.content or ""
|
||||
|
||||
custom_id = direct_conversation_custom_id(conversation_id)
|
||||
return assistant_text, custom_id, profile_context
|
||||
document_id = direct_conversation_id(conversation_id)
|
||||
return assistant_text, document_id, profile_context
|
||||
|
||||
|
||||
async def save_direct_conversation(
|
||||
messages: list[ChatMessage],
|
||||
assistant_text: str,
|
||||
container_tag: str,
|
||||
custom_id: str,
|
||||
namespace: str,
|
||||
document_id: str,
|
||||
sm_key: str,
|
||||
) -> dict[str, Any]:
|
||||
from supermemory import AsyncSupermemory
|
||||
|
||||
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):
|
||||
response = await client.add(
|
||||
content=conversation_transcript(messages, assistant_text),
|
||||
container_tag=container_tag,
|
||||
custom_id=custom_id,
|
||||
)
|
||||
async with supermemory_http(sm_key, DIRECT_SAVE_TIMEOUT_SECONDS) as http:
|
||||
response = await http.post(
|
||||
namespace_path(namespace, "/document"),
|
||||
json={
|
||||
"content": conversation_transcript(messages, assistant_text),
|
||||
"id": document_id,
|
||||
"dreaming": "instant",
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
accepted = response.json()
|
||||
return {
|
||||
"type": "conversation_save_accepted",
|
||||
"label": "Full conversation accepted for processing",
|
||||
"detail": {
|
||||
"nonFatal": True,
|
||||
"containerTag": container_tag,
|
||||
"customId": custom_id,
|
||||
"documentId": object_field(response, "id"),
|
||||
"status": object_field(response, "status"),
|
||||
"namespace": namespace,
|
||||
"id": document_id,
|
||||
"documentId": accepted.get("id"),
|
||||
"status": accepted.get("status"),
|
||||
},
|
||||
}
|
||||
except Exception as error:
|
||||
|
|
@ -460,77 +460,74 @@ async def save_direct_conversation(
|
|||
"label": "Conversation save unavailable",
|
||||
"detail": {
|
||||
"nonFatal": True,
|
||||
"containerTag": container_tag,
|
||||
"customId": custom_id,
|
||||
"namespace": namespace,
|
||||
"id": document_id,
|
||||
"error": public_error(error, sm_key),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def fetch_container_context(
|
||||
container_tag: str,
|
||||
async def fetch_document_memories(http: Any, namespace: str, document_id: str) -> list[Any]:
|
||||
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,
|
||||
query: Optional[str] = None,
|
||||
) -> dict[str, Any]:
|
||||
if not sm_key:
|
||||
raise RuntimeError("Supermemory API key must be supplied")
|
||||
|
||||
profile_context = await fetch_profile_context(container_tag, sm_key, query)
|
||||
base_url = supermemory_base_url()
|
||||
profile_context = await fetch_profile_context(namespace, sm_key, query)
|
||||
|
||||
import httpx
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
timeout=HTTP_TIMEOUT_SECONDS,
|
||||
follow_redirects=False,
|
||||
) as http:
|
||||
async with supermemory_http(sm_key) as http:
|
||||
docs_response = await http.post(
|
||||
f"{base_url}/v3/documents/documents",
|
||||
headers={
|
||||
"Authorization": f"Bearer {sm_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"containerTags": [container_tag],
|
||||
"limit": 25,
|
||||
namespace_path(namespace, "/list/documents"),
|
||||
params={
|
||||
"limit": DOCUMENT_LIST_LIMIT,
|
||||
"sort": "createdAt",
|
||||
"order": "desc",
|
||||
},
|
||||
json={},
|
||||
)
|
||||
docs_response.raise_for_status()
|
||||
docs = docs_response.json()
|
||||
|
||||
raw_documents = docs.get("documents", []) if isinstance(docs, dict) else []
|
||||
raw_documents = docs.get("documents") or []
|
||||
# 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 = []
|
||||
for doc in raw_documents:
|
||||
record = doc if isinstance(doc, dict) else getattr(doc, "__dict__", {})
|
||||
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
|
||||
for doc, memory_entries in zip(raw_documents, memories):
|
||||
system = doc.get("system") or {}
|
||||
documents.append(
|
||||
{
|
||||
"id": record.get("id"),
|
||||
"title": record.get("title"),
|
||||
"status": record.get("status"),
|
||||
"customId": record.get("customId") or record.get("custom_id"),
|
||||
"createdAt": record.get("createdAt") or record.get("created_at"),
|
||||
"updatedAt": record.get("updatedAt") or record.get("updated_at"),
|
||||
"summary": record.get("summary"),
|
||||
"id": doc.get("id"),
|
||||
"title": doc.get("title"),
|
||||
"status": system.get("status"),
|
||||
"createdAt": system.get("createdAt"),
|
||||
"updatedAt": system.get("updatedAt"),
|
||||
"summary": doc.get("summary"),
|
||||
"memoryEntries": memory_entries,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"containerTag": container_tag,
|
||||
"namespace": namespace,
|
||||
"query": query,
|
||||
"profile": profile_context,
|
||||
"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(
|
||||
container_tag: str,
|
||||
namespace: str,
|
||||
conversation_id: str,
|
||||
memory_mode: str,
|
||||
last_user_message: str,
|
||||
|
|
@ -610,9 +607,11 @@ def build_middleware_memory_debug(
|
|||
"detail": {
|
||||
"authoritativeMiddlewareCapture": False,
|
||||
"timing": "after model response",
|
||||
"endpoint": "POST /v4/profile",
|
||||
"containerTag": container_tag,
|
||||
"customId": conversation_id,
|
||||
"endpoint": (
|
||||
"POST /ns/{namespace}/profile + POST /ns/{namespace}/search"
|
||||
),
|
||||
"namespace": namespace,
|
||||
"id": conversation_id,
|
||||
"memoryMode": memory_mode,
|
||||
"query": context.get("query"),
|
||||
"staticCount": len(profile.get("static", [])),
|
||||
|
|
@ -640,8 +639,8 @@ def build_middleware_memory_debug(
|
|||
)
|
||||
|
||||
save_detail = {
|
||||
"containerTag": container_tag,
|
||||
"customId": f"conversation:{conversation_id}",
|
||||
"namespace": namespace,
|
||||
"id": f"conversation:{conversation_id}",
|
||||
"addMemory": middleware_config.addMemory,
|
||||
"verbose": middleware_config.verbose,
|
||||
}
|
||||
|
|
@ -665,21 +664,16 @@ def build_middleware_memory_debug(
|
|||
|
||||
|
||||
async def fetch_context_for_debug(
|
||||
container_tag: str,
|
||||
namespace: str,
|
||||
query: Optional[str],
|
||||
sm_key: str,
|
||||
) -> tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||
try:
|
||||
async with asyncio.timeout(CONTEXT_DEBUG_TIMEOUT_SECONDS):
|
||||
profile = await fetch_profile_context(
|
||||
container_tag,
|
||||
sm_key,
|
||||
query,
|
||||
include=["static", "dynamic"],
|
||||
)
|
||||
profile = await fetch_profile_context(namespace, sm_key, query)
|
||||
return (
|
||||
{
|
||||
"containerTag": container_tag,
|
||||
"namespace": namespace,
|
||||
"query": query,
|
||||
"profile": profile,
|
||||
},
|
||||
|
|
@ -691,12 +685,12 @@ async def fetch_context_for_debug(
|
|||
|
||||
@app.get("/context")
|
||||
async def context_get(
|
||||
containerTag: Annotated[
|
||||
namespace: Annotated[
|
||||
str,
|
||||
Query(
|
||||
min_length=1,
|
||||
max_length=MAX_CONTAINER_TAG_LENGTH,
|
||||
pattern=CONTAINER_TAG_PATTERN,
|
||||
max_length=MAX_NAMESPACE_LENGTH,
|
||||
pattern=NAMESPACE_PATTERN,
|
||||
),
|
||||
] = "sdk-playground",
|
||||
query: Annotated[Optional[str], Query(max_length=MAX_MESSAGE_LENGTH)] = None,
|
||||
|
|
@ -712,7 +706,7 @@ async def context_get(
|
|||
"X-Supermemory-API-Key header",
|
||||
)
|
||||
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}
|
||||
except Exception as error:
|
||||
return JSONResponse(
|
||||
|
|
@ -731,7 +725,7 @@ async def context_post(req: ContextRequest):
|
|||
try:
|
||||
sm_key = resolve_supermemory_key(req.apiKeys)
|
||||
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}
|
||||
except Exception as error:
|
||||
return JSONResponse(
|
||||
|
|
@ -775,7 +769,7 @@ async def chat(req: ChatRequest):
|
|||
middleware_config = req.middlewareConfig or MiddlewareConfig()
|
||||
text = await chat_openai_middleware(
|
||||
req.messages,
|
||||
req.containerTag,
|
||||
req.namespace,
|
||||
req.conversationId,
|
||||
req.memoryMode or "full",
|
||||
middleware_config,
|
||||
|
|
@ -790,12 +784,12 @@ async def chat(req: ChatRequest):
|
|||
middleware_debug = (middleware_config, last_user, query)
|
||||
elif req.sdkId == "py-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":
|
||||
text, custom_id, profile_context = await chat_supermemory_direct(
|
||||
text, document_id, profile_context = await chat_supermemory_direct(
|
||||
req.messages,
|
||||
req.containerTag,
|
||||
req.namespace,
|
||||
req.conversationId,
|
||||
sm_key,
|
||||
oai_key,
|
||||
|
|
@ -804,19 +798,19 @@ async def chat(req: ChatRequest):
|
|||
(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:
|
||||
raise RuntimeError(f"Unsupported Python SDK: {req.sdkId}")
|
||||
|
||||
if middleware_debug is not None:
|
||||
middleware_config, last_user, query = middleware_debug
|
||||
ctx, context_error = await fetch_context_for_debug(
|
||||
req.containerTag,
|
||||
req.namespace,
|
||||
query,
|
||||
sm_key,
|
||||
)
|
||||
memory_debug = build_middleware_memory_debug(
|
||||
req.containerTag,
|
||||
req.namespace,
|
||||
req.conversationId,
|
||||
req.memoryMode or "full",
|
||||
last_user,
|
||||
|
|
@ -825,12 +819,12 @@ async def chat(req: ChatRequest):
|
|||
middleware_config,
|
||||
)
|
||||
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(
|
||||
req.messages,
|
||||
text,
|
||||
req.containerTag,
|
||||
custom_id,
|
||||
req.namespace,
|
||||
document_id,
|
||||
sm_key,
|
||||
)
|
||||
memory_debug = [
|
||||
|
|
@ -838,7 +832,7 @@ async def chat(req: ChatRequest):
|
|||
"type": "manual_profile",
|
||||
"label": "Profile context used for this response",
|
||||
"detail": {
|
||||
"containerTag": req.containerTag,
|
||||
"namespace": req.namespace,
|
||||
"query": last_user,
|
||||
"staticCount": len(profile_context["static"]),
|
||||
"dynamicCount": len(profile_context["dynamic"]),
|
||||
|
|
|
|||
10
apps/sdk-playground/python/uv.lock
generated
10
apps/sdk-playground/python/uv.lock
generated
|
|
@ -1043,7 +1043,6 @@ dependencies = [
|
|||
{ name = "httpx" },
|
||||
{ name = "openai" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "supermemory" },
|
||||
{ name = "supermemory-openai-sdk", extra = ["async"] },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
|
@ -1054,7 +1053,6 @@ requires-dist = [
|
|||
{ name = "httpx", specifier = ">=0.28.0" },
|
||||
{ name = "openai", specifier = ">=1.102.0" },
|
||||
{ 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 = "uvicorn", extras = ["standard"], specifier = ">=0.32.0" },
|
||||
]
|
||||
|
|
@ -1100,7 +1098,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "supermemory-openai-sdk"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
source = { editable = "../../../packages/openai-sdk-python" }
|
||||
dependencies = [
|
||||
{ name = "openai" },
|
||||
|
|
@ -1116,17 +1114,17 @@ async = [
|
|||
|
||||
[package.metadata]
|
||||
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 = "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" },
|
||||
]
|
||||
provides-extras = ["async"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "black", specifier = ">=24.8.0" },
|
||||
{ name = "black", specifier = ">=26.3.1" },
|
||||
{ name = "flake8", specifier = ">=7.1.2" },
|
||||
{ name = "isort", specifier = ">=5.13.2" },
|
||||
{ name = "mypy", specifier = ">=1.14.1" },
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
assertTrustedBrowserRequest,
|
||||
mayUseEnvironmentKeys,
|
||||
parseApiKeys,
|
||||
parseContainerTag,
|
||||
parseNamespace,
|
||||
parseConversationId,
|
||||
parseIdentifier,
|
||||
parseMemoryMode,
|
||||
|
|
@ -63,7 +63,7 @@ export async function POST(req: Request) {
|
|||
const body = await readJsonObject(req)
|
||||
const sdkId = parseIdentifier(body.sdkId, "sdkId")
|
||||
const messages = parseMessages(body.messages)
|
||||
const containerTag = parseContainerTag(body.containerTag)
|
||||
const namespace = parseNamespace(body.namespace)
|
||||
const conversationId = parseConversationId(body.conversationId)
|
||||
const memoryMode = parseMemoryMode(body.memoryMode)
|
||||
const middlewareConfig = parseMiddlewareConfig(body.middlewareConfig)
|
||||
|
|
@ -97,7 +97,7 @@ export async function POST(req: Request) {
|
|||
body: JSON.stringify({
|
||||
sdkId,
|
||||
messages,
|
||||
containerTag,
|
||||
namespace,
|
||||
conversationId,
|
||||
memoryMode,
|
||||
middlewareConfig,
|
||||
|
|
@ -129,12 +129,11 @@ export async function POST(req: Request) {
|
|||
{
|
||||
sdkId,
|
||||
messages,
|
||||
containerTag,
|
||||
namespace,
|
||||
conversationId,
|
||||
memoryMode,
|
||||
middlewareConfig,
|
||||
apiKeys,
|
||||
containerTags: [containerTag],
|
||||
},
|
||||
apiKeys,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import { resolveSupermemoryApiKey } from "@/lib/api-keys"
|
||||
import { fetchContainerContext } from "@/lib/context-api"
|
||||
import { fetchNamespaceContext } from "@/lib/context-api"
|
||||
import {
|
||||
PlaygroundRequestError,
|
||||
assertTrustedBrowserRequest,
|
||||
mayUseEnvironmentKeys,
|
||||
parseApiKeys,
|
||||
parseContainerTag,
|
||||
parseNamespace,
|
||||
parseOptionalText,
|
||||
readJsonObject,
|
||||
} from "@/lib/request-validation"
|
||||
|
|
@ -15,7 +15,7 @@ export async function GET(req: Request) {
|
|||
try {
|
||||
assertTrustedBrowserRequest(req)
|
||||
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 supermemoryApiKey = resolveSupermemoryApiKey(null, {
|
||||
allowEnvironment: mayUseEnvironmentKeys(req),
|
||||
|
|
@ -32,8 +32,8 @@ export async function GET(req: Request) {
|
|||
)
|
||||
}
|
||||
|
||||
const context = await fetchContainerContext(
|
||||
containerTag,
|
||||
const context = await fetchNamespaceContext(
|
||||
namespace,
|
||||
query,
|
||||
supermemoryApiKey,
|
||||
)
|
||||
|
|
@ -54,7 +54,7 @@ export async function POST(req: Request) {
|
|||
try {
|
||||
assertTrustedBrowserRequest(req)
|
||||
const body = await readJsonObject(req)
|
||||
const containerTag = parseContainerTag(body.containerTag)
|
||||
const namespace = parseNamespace(body.namespace)
|
||||
const query = parseOptionalText(body.query, "query")
|
||||
const supermemoryApiKey = resolveSupermemoryApiKey(
|
||||
parseApiKeys(body.apiKeys),
|
||||
|
|
@ -71,8 +71,8 @@ export async function POST(req: Request) {
|
|||
)
|
||||
}
|
||||
|
||||
const context = await fetchContainerContext(
|
||||
containerTag,
|
||||
const context = await fetchNamespaceContext(
|
||||
namespace,
|
||||
query,
|
||||
supermemoryApiKey,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export default function AgentPlaygroundPage() {
|
|||
const keysReady = supermemoryKeyReady && openAiKeyReady
|
||||
|
||||
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">(
|
||||
"full",
|
||||
)
|
||||
|
|
@ -134,7 +134,7 @@ export default function AgentPlaygroundPage() {
|
|||
...chatHistory,
|
||||
{ role: "user", content: userMessage.content },
|
||||
],
|
||||
containerTag,
|
||||
namespace,
|
||||
conversationId: activeConversationId,
|
||||
memoryMode:
|
||||
selectedSdk.mode === "middleware" ? memoryMode : undefined,
|
||||
|
|
@ -301,16 +301,16 @@ export default function AgentPlaygroundPage() {
|
|||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<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
|
||||
value={containerTag}
|
||||
onChange={(e) => setContainerTag(e.target.value)}
|
||||
value={namespace}
|
||||
onChange={(e) => setNamespace(e.target.value)}
|
||||
maxLength={100}
|
||||
className="rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm w-40"
|
||||
/>
|
||||
</label>
|
||||
<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
|
||||
value={conversationId}
|
||||
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">
|
||||
<ContextPanel
|
||||
containerTag={containerTag}
|
||||
namespace={namespace}
|
||||
lastUserMessage={lastUserMessage}
|
||||
refreshKey={contextRefreshKey}
|
||||
supermemoryApiKey={supermemoryApiKey}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
"use client"
|
||||
|
||||
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({
|
||||
containerTag,
|
||||
namespace,
|
||||
lastUserMessage,
|
||||
refreshKey,
|
||||
supermemoryApiKey,
|
||||
supermemoryKeyReady,
|
||||
}: {
|
||||
containerTag: string
|
||||
namespace: string
|
||||
lastUserMessage?: string
|
||||
refreshKey: number
|
||||
supermemoryApiKey: string
|
||||
supermemoryKeyReady: boolean
|
||||
}) {
|
||||
const [context, setContext] = useState<ContainerContext | null>(null)
|
||||
const [context, setContext] = useState<NamespaceContext | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [useQuery, setUseQuery] = useState(false)
|
||||
|
|
@ -31,9 +31,9 @@ export function ContextPanel({
|
|||
setContext(null)
|
||||
return
|
||||
}
|
||||
const normalizedContainerTag = containerTag.trim()
|
||||
if (!normalizedContainerTag) {
|
||||
setError("Enter a container tag to load context")
|
||||
const normalizedNamespace = namespace.trim()
|
||||
if (!normalizedNamespace) {
|
||||
setError("Enter a namespace to load context")
|
||||
setContext(null)
|
||||
return
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ export function ContextPanel({
|
|||
headers: { "Content-Type": "application/json" },
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
containerTag: normalizedContainerTag,
|
||||
namespace: normalizedNamespace,
|
||||
...(useQuery && lastUserMessage ? { query: lastUserMessage } : {}),
|
||||
...(supermemoryApiKey.trim()
|
||||
? {
|
||||
|
|
@ -75,7 +75,7 @@ export function ContextPanel({
|
|||
}
|
||||
}
|
||||
}, [
|
||||
containerTag,
|
||||
namespace,
|
||||
lastUserMessage,
|
||||
useQuery,
|
||||
supermemoryApiKey,
|
||||
|
|
@ -85,7 +85,7 @@ export function ContextPanel({
|
|||
useEffect(() => {
|
||||
// The counter changes after a successful chat and explicitly refreshes context.
|
||||
void refreshKey
|
||||
if (!supermemoryKeyReady || !containerTag.trim()) {
|
||||
if (!supermemoryKeyReady || !namespace.trim()) {
|
||||
activeRequest.current?.abort()
|
||||
activeRequest.current = null
|
||||
setLoading(false)
|
||||
|
|
@ -102,13 +102,13 @@ export function ContextPanel({
|
|||
window.clearTimeout(timeout)
|
||||
activeRequest.current?.abort()
|
||||
}
|
||||
}, [load, refreshKey, supermemoryKeyReady, containerTag])
|
||||
}, [load, refreshKey, supermemoryKeyReady, namespace])
|
||||
|
||||
useEffect(() => {
|
||||
// A different container must not retain the previous document selection.
|
||||
void containerTag
|
||||
// A different namespace must not retain the previous document selection.
|
||||
void namespace
|
||||
setSelectedDocKey(null)
|
||||
}, [containerTag])
|
||||
}, [namespace])
|
||||
|
||||
const selectedDoc =
|
||||
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 items-center justify-between gap-2">
|
||||
<h2 className="text-xs font-medium uppercase tracking-wider text-zinc-500">
|
||||
Container context
|
||||
Namespace context
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
Refresh
|
||||
|
|
@ -210,12 +210,9 @@ export function ContextPanel({
|
|||
<div className="text-zinc-500 truncate">
|
||||
{doc.title ?? "untitled"}
|
||||
</div>
|
||||
<div className="text-zinc-600">
|
||||
{doc.customId
|
||||
? `session: ${doc.customId}`
|
||||
: "no customId"}
|
||||
{doc.status ? ` · ${doc.status}` : ""}
|
||||
</div>
|
||||
{doc.status && (
|
||||
<div className="text-zinc-600">{doc.status}</div>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
|
|
@ -307,7 +304,7 @@ function MemoryEntryCard({ entry }: { entry: unknown }) {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { generateText, stepCountIs, type ModelMessage } from "ai"
|
||||
import OpenAI from "openai"
|
||||
import { supermemoryTools as aiSdkPackageTools } from "@supermemory/ai-sdk"
|
||||
import { withSupermemory as withSupermemoryAiSdk } from "@supermemory/tools/ai-sdk"
|
||||
import {
|
||||
createToolCallsExecutor,
|
||||
|
|
@ -39,13 +38,11 @@ export interface ChatResult {
|
|||
export interface ChatRequest {
|
||||
sdkId: string
|
||||
messages: ChatMessage[]
|
||||
containerTag: string
|
||||
namespace: string
|
||||
conversationId: string
|
||||
memoryMode?: "profile" | "query" | "full"
|
||||
middlewareConfig?: Partial<MiddlewareRuntimeConfig>
|
||||
apiKeys?: Partial<PlaygroundApiKeys>
|
||||
containerTags?: string[]
|
||||
projectId?: string
|
||||
}
|
||||
|
||||
export class PlaygroundChatTimeoutError extends Error {
|
||||
|
|
@ -118,15 +115,8 @@ function getModelName(): string {
|
|||
return process.env.MODEL_NAME ?? "gpt-4o-mini"
|
||||
}
|
||||
|
||||
function getToolsConfig(
|
||||
containerTags?: string[],
|
||||
projectId?: string,
|
||||
): SupermemoryToolsConfig {
|
||||
return {
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL,
|
||||
...(containerTags?.length ? { containerTags } : {}),
|
||||
...(projectId ? { projectId } : {}),
|
||||
}
|
||||
function getToolsConfig(namespace: string): SupermemoryToolsConfig {
|
||||
return { baseUrl: process.env.SUPERMEMORY_BASE_URL, namespace }
|
||||
}
|
||||
|
||||
function toModelMessages(messages: ChatMessage[]): ModelMessage[] {
|
||||
|
|
@ -168,7 +158,7 @@ function lastUserMessage(messages: ChatMessage[]): string {
|
|||
async function chatAiSdkMiddleware(
|
||||
keys: PlaygroundApiKeys,
|
||||
messages: ChatMessage[],
|
||||
containerTag: string,
|
||||
namespace: string,
|
||||
conversationId: string,
|
||||
memoryMode: "profile" | "query" | "full",
|
||||
middlewareConfig: MiddlewareRuntimeConfig,
|
||||
|
|
@ -176,8 +166,8 @@ async function chatAiSdkMiddleware(
|
|||
): Promise<ChatResult> {
|
||||
const openai = createOpenAI({ apiKey: keys.openaiApiKey })
|
||||
const model = withSupermemoryAiSdk(openai(getModelName()), {
|
||||
containerTag,
|
||||
customId: conversationId,
|
||||
namespace,
|
||||
id: conversationId,
|
||||
apiKey: keys.supermemoryApiKey,
|
||||
mode: memoryMode,
|
||||
addMemory: middlewareConfig.addMemory,
|
||||
|
|
@ -201,7 +191,7 @@ async function chatAiSdkMiddleware(
|
|||
async function chatOpenAiMiddleware(
|
||||
keys: PlaygroundApiKeys,
|
||||
messages: ChatMessage[],
|
||||
containerTag: string,
|
||||
namespace: string,
|
||||
conversationId: string,
|
||||
memoryMode: "profile" | "query" | "full",
|
||||
middlewareConfig: MiddlewareRuntimeConfig,
|
||||
|
|
@ -213,8 +203,8 @@ async function chatOpenAiMiddleware(
|
|||
maxRetries: 1,
|
||||
})
|
||||
const client = withSupermemoryOpenAi(openai, {
|
||||
containerTag,
|
||||
customId: conversationId,
|
||||
namespace,
|
||||
id: conversationId,
|
||||
apiKey: keys.supermemoryApiKey,
|
||||
mode: memoryMode,
|
||||
addMemory: middlewareConfig.addMemory,
|
||||
|
|
@ -242,15 +232,11 @@ async function chatAiSdkTools(
|
|||
keys: PlaygroundApiKeys,
|
||||
toolsFactory: typeof aiSdkTools,
|
||||
messages: ChatMessage[],
|
||||
containerTags?: string[],
|
||||
projectId?: string,
|
||||
namespace: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ChatResult> {
|
||||
const openai = createOpenAI({ apiKey: keys.openaiApiKey })
|
||||
const tools = toolsFactory(
|
||||
keys.supermemoryApiKey,
|
||||
getToolsConfig(containerTags, projectId),
|
||||
)
|
||||
const tools = toolsFactory(keys.supermemoryApiKey, getToolsConfig(namespace))
|
||||
|
||||
const result = await generateText({
|
||||
model: openai(getModelName()),
|
||||
|
|
@ -272,8 +258,7 @@ async function chatAiSdkTools(
|
|||
async function chatOpenAiTools(
|
||||
keys: PlaygroundApiKeys,
|
||||
messages: ChatMessage[],
|
||||
containerTags?: string[],
|
||||
projectId?: string,
|
||||
namespace: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ChatResult> {
|
||||
const openai = new OpenAI({
|
||||
|
|
@ -281,7 +266,7 @@ async function chatOpenAiTools(
|
|||
timeout: MODEL_REQUEST_TIMEOUT_MS,
|
||||
maxRetries: 1,
|
||||
})
|
||||
const config = getToolsConfig(containerTags, projectId)
|
||||
const config = getToolsConfig(namespace)
|
||||
const executeToolCalls = createToolCallsExecutor(
|
||||
keys.supermemoryApiKey,
|
||||
config,
|
||||
|
|
@ -351,7 +336,6 @@ export async function runTypeScriptChat(
|
|||
throw new Error(`Invalid TypeScript chat SDK: ${request.sdkId}`)
|
||||
}
|
||||
|
||||
const containerTags = request.containerTags ?? [request.containerTag]
|
||||
const memoryMode = request.memoryMode ?? "full"
|
||||
const middlewareConfig = normalizeMiddlewareConfig(request.middlewareConfig)
|
||||
|
||||
|
|
@ -361,7 +345,7 @@ export async function runTypeScriptChat(
|
|||
return await chatAiSdkMiddleware(
|
||||
keys,
|
||||
request.messages,
|
||||
request.containerTag,
|
||||
request.namespace,
|
||||
request.conversationId,
|
||||
memoryMode,
|
||||
middlewareConfig,
|
||||
|
|
@ -371,7 +355,7 @@ export async function runTypeScriptChat(
|
|||
return await chatOpenAiMiddleware(
|
||||
keys,
|
||||
request.messages,
|
||||
request.containerTag,
|
||||
request.namespace,
|
||||
request.conversationId,
|
||||
memoryMode,
|
||||
middlewareConfig,
|
||||
|
|
@ -382,25 +366,14 @@ export async function runTypeScriptChat(
|
|||
keys,
|
||||
aiSdkTools,
|
||||
request.messages,
|
||||
containerTags,
|
||||
request.projectId,
|
||||
request.namespace,
|
||||
signal,
|
||||
)
|
||||
case "ts-openai-tools":
|
||||
return await chatOpenAiTools(
|
||||
keys,
|
||||
request.messages,
|
||||
containerTags,
|
||||
request.projectId,
|
||||
signal,
|
||||
)
|
||||
case "ts-ai-sdk-package":
|
||||
return await chatAiSdkTools(
|
||||
keys,
|
||||
aiSdkPackageTools,
|
||||
request.messages,
|
||||
containerTags,
|
||||
request.projectId,
|
||||
request.namespace,
|
||||
signal,
|
||||
)
|
||||
default:
|
||||
|
|
@ -417,7 +390,7 @@ export async function runTypeScriptChat(
|
|||
|
||||
const memoryDebug = await buildBestEffortDebug((signal) =>
|
||||
buildMiddlewareMemoryDebug(
|
||||
request.containerTag,
|
||||
request.namespace,
|
||||
request.conversationId,
|
||||
memoryMode,
|
||||
lastUserMessage(request.messages),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Supermemory from "supermemory"
|
||||
import { Supermemory } from "supermemory"
|
||||
import { supermemoryProfileSearch } from "../../../../packages/tools/src/shared/memory-client"
|
||||
import {
|
||||
type MiddlewareRuntimeConfig,
|
||||
normalizeMiddlewareConfig,
|
||||
|
|
@ -28,8 +29,8 @@ export interface MemoryDebugEntry {
|
|||
preview?: string
|
||||
}
|
||||
|
||||
export interface ContainerContext {
|
||||
containerTag: string
|
||||
export interface NamespaceContext {
|
||||
namespace: string
|
||||
query?: string
|
||||
profile: {
|
||||
static: unknown[]
|
||||
|
|
@ -40,7 +41,6 @@ export interface ContainerContext {
|
|||
id?: string
|
||||
title?: string
|
||||
status?: string
|
||||
customId?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
summary?: string
|
||||
|
|
@ -49,31 +49,21 @@ export interface ContainerContext {
|
|||
pagination?: unknown
|
||||
}
|
||||
|
||||
const REQUEST_TIMEOUT_SECONDS = 10
|
||||
const DOCUMENT_LIST_LIMIT = 25
|
||||
|
||||
function getSupermemoryClient(apiKey: string) {
|
||||
if (!apiKey) throw new Error("Supermemory API key is required")
|
||||
return new Supermemory({
|
||||
apiKey,
|
||||
timeout: 10_000,
|
||||
timeoutInSeconds: REQUEST_TIMEOUT_SECONDS,
|
||||
maxRetries: 1,
|
||||
...(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 {
|
||||
if (typeof item === "string") return item
|
||||
if (item && typeof item === "object") {
|
||||
|
|
@ -85,7 +75,7 @@ function memoryText(item: unknown): string {
|
|||
return JSON.stringify(item)
|
||||
}
|
||||
|
||||
function summarizeProfile(profile: ContainerContext["profile"]) {
|
||||
function summarizeProfile(profile: NamespaceContext["profile"]) {
|
||||
return {
|
||||
staticCount: profile.static.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(
|
||||
lastUserMessage: string,
|
||||
mode: "profile" | "query" | "full",
|
||||
|
|
@ -114,82 +94,77 @@ export function resolveProfileQuery(
|
|||
return lastUserMessage || undefined
|
||||
}
|
||||
|
||||
// Same profile + memories search the middlewares run before the model call.
|
||||
async function fetchProfileContext(
|
||||
client: ReturnType<typeof getSupermemoryClient>,
|
||||
containerTag: string,
|
||||
apiKey: string,
|
||||
namespace: string,
|
||||
query?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ContainerContext["profile"]> {
|
||||
const profileResponse = await client.post<{
|
||||
profile?: { static?: unknown[]; dynamic?: unknown[] }
|
||||
searchResults?: unknown
|
||||
}>("/v4/profile", {
|
||||
body: {
|
||||
containerTag,
|
||||
include: ["static", "dynamic"],
|
||||
...(query ? { q: query } : {}),
|
||||
},
|
||||
...(signal ? { signal } : {}),
|
||||
})
|
||||
const profileRaw = profileResponse.profile
|
||||
): Promise<NamespaceContext["profile"]> {
|
||||
const response = await supermemoryProfileSearch(
|
||||
namespace,
|
||||
query ?? "",
|
||||
process.env.SUPERMEMORY_BASE_URL ?? "",
|
||||
apiKey,
|
||||
signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_SECONDS * 1_000),
|
||||
)
|
||||
|
||||
return {
|
||||
static: profileRaw?.static ?? [],
|
||||
dynamic: profileRaw?.dynamic ?? [],
|
||||
searchResults: normalizeSearchResults(profileResponse.searchResults),
|
||||
static: response.profile.static ?? [],
|
||||
dynamic: response.profile.dynamic ?? [],
|
||||
searchResults: response.searchResults?.results ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchContainerContext(
|
||||
containerTag: string,
|
||||
export async function fetchNamespaceContext(
|
||||
namespace: string,
|
||||
query?: string,
|
||||
supermemoryApiKey?: string,
|
||||
): Promise<ContainerContext> {
|
||||
): Promise<NamespaceContext> {
|
||||
const apiKey =
|
||||
supermemoryApiKey?.trim() || process.env.SUPERMEMORY_API_KEY?.trim()
|
||||
if (!apiKey) throw new Error("Supermemory API key is required")
|
||||
|
||||
const client = getSupermemoryClient(apiKey)
|
||||
const profile = await fetchProfileContext(client, containerTag, query)
|
||||
|
||||
const docsResponse = await client.post<{
|
||||
documents?: unknown[]
|
||||
pagination?: unknown
|
||||
}>("/v3/documents/documents", {
|
||||
body: {
|
||||
containerTags: [containerTag],
|
||||
limit: 25,
|
||||
const [profile, listResponse] = await Promise.all([
|
||||
fetchProfileContext(apiKey, namespace, query),
|
||||
client.list(namespace, "documents", {
|
||||
limit: DOCUMENT_LIST_LIMIT,
|
||||
sort: "createdAt",
|
||||
order: "desc",
|
||||
},
|
||||
})
|
||||
}),
|
||||
])
|
||||
|
||||
const rawDocuments = docsResponse.documents ?? []
|
||||
const documents = rawDocuments.map((doc) => {
|
||||
const record = doc as Record<string, unknown>
|
||||
return {
|
||||
id: record.id as string | undefined,
|
||||
title: record.title as string | undefined,
|
||||
status: record.status as string | undefined,
|
||||
customId: record.customId as string | undefined,
|
||||
createdAt: record.createdAt as string | undefined,
|
||||
updatedAt: record.updatedAt as string | undefined,
|
||||
summary: record.summary as string | undefined,
|
||||
memoryEntries: normalizeMemoryEntries(record),
|
||||
}
|
||||
})
|
||||
// v5 lists omit memories, so each document is fetched with include=memories.
|
||||
const documents = await Promise.all(
|
||||
listResponse.documents.map(async (doc) => {
|
||||
const memories = await client.documents
|
||||
.get(namespace, doc.id, { include: ["memories"] })
|
||||
.then((full) => full.memories ?? [])
|
||||
.catch(() => [])
|
||||
return {
|
||||
id: doc.id,
|
||||
title: doc.title ?? undefined,
|
||||
status: doc.system.status,
|
||||
createdAt: doc.system.createdAt,
|
||||
updatedAt: doc.system.updatedAt,
|
||||
summary: doc.summary ?? undefined,
|
||||
memoryEntries: memories,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
containerTag,
|
||||
namespace,
|
||||
query,
|
||||
profile,
|
||||
documents,
|
||||
pagination: docsResponse.pagination,
|
||||
pagination: listResponse.pagination,
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildMiddlewareMemoryDebug(
|
||||
containerTag: string,
|
||||
namespace: string,
|
||||
conversationId: string,
|
||||
memoryMode: MemoryMode,
|
||||
lastUserMessage: string,
|
||||
|
|
@ -209,12 +184,7 @@ export async function buildMiddlewareMemoryDebug(
|
|||
const apiKey =
|
||||
supermemoryApiKey?.trim() || process.env.SUPERMEMORY_API_KEY?.trim()
|
||||
if (!apiKey) throw new Error("Supermemory API key is required")
|
||||
const profile = await fetchProfileContext(
|
||||
getSupermemoryClient(apiKey),
|
||||
containerTag,
|
||||
query,
|
||||
signal,
|
||||
)
|
||||
const profile = await fetchProfileContext(apiKey, namespace, query, signal)
|
||||
const reconstructed = reconstructSdkMemoryBlock(
|
||||
memoryMode,
|
||||
profile,
|
||||
|
|
@ -230,9 +200,10 @@ export async function buildMiddlewareMemoryDebug(
|
|||
detail: {
|
||||
authoritativeMiddlewareCapture: false,
|
||||
timing: "after model response",
|
||||
endpoint: "POST /v4/profile",
|
||||
containerTag,
|
||||
customId: conversationId,
|
||||
endpoint:
|
||||
"POST /ns/{namespace}/profile + POST /ns/{namespace}/search",
|
||||
namespace,
|
||||
id: conversationId,
|
||||
memoryMode,
|
||||
addMemory: config.addMemory,
|
||||
verbose: config.verbose,
|
||||
|
|
@ -264,8 +235,8 @@ export async function buildMiddlewareMemoryDebug(
|
|||
label: "Conversation save requested by middleware",
|
||||
detail: {
|
||||
confirmed: false,
|
||||
containerTag,
|
||||
customId: conversationId,
|
||||
namespace,
|
||||
id: conversationId,
|
||||
addMemory: config.addMemory,
|
||||
verbose: config.verbose,
|
||||
...(sdk.includeToolCalls !== undefined
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ const MAX_MESSAGE_LENGTH = 20_000
|
|||
const MAX_TOTAL_MESSAGE_LENGTH = 100_000
|
||||
const MAX_IDENTIFIER_LENGTH = 256
|
||||
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 CONTAINER_TAG_PATTERN = /^[a-zA-Z0-9_:-]+$/
|
||||
const NAMESPACE_PATTERN = /^[a-zA-Z0-9_:-]+$/
|
||||
|
||||
export class PlaygroundRequestError extends Error {
|
||||
constructor(
|
||||
|
|
@ -224,24 +224,24 @@ export function parseIdentifier(
|
|||
return resolved
|
||||
}
|
||||
|
||||
export function parseContainerTag(
|
||||
export function parseNamespace(
|
||||
value: unknown,
|
||||
fallback = "sdk-playground",
|
||||
): string {
|
||||
const containerTag = parseIdentifier(value, "containerTag", fallback)
|
||||
if (containerTag.length > MAX_CONTAINER_TAG_LENGTH) {
|
||||
const namespace = parseIdentifier(value, "namespace", fallback)
|
||||
if (namespace.length > MAX_NAMESPACE_LENGTH) {
|
||||
throw new PlaygroundRequestError(
|
||||
`containerTag must be ${MAX_CONTAINER_TAG_LENGTH} characters or fewer`,
|
||||
`namespace must be ${MAX_NAMESPACE_LENGTH} characters or fewer`,
|
||||
400,
|
||||
)
|
||||
}
|
||||
if (!CONTAINER_TAG_PATTERN.test(containerTag)) {
|
||||
if (!NAMESPACE_PATTERN.test(namespace)) {
|
||||
throw new PlaygroundRequestError(
|
||||
"containerTag may only contain letters, numbers, hyphens, underscores, and colons",
|
||||
"namespace may only contain letters, numbers, hyphens, underscores, and colons",
|
||||
400,
|
||||
)
|
||||
}
|
||||
return containerTag
|
||||
return namespace
|
||||
}
|
||||
|
||||
export function parseConversationId(value: unknown): string {
|
||||
|
|
|
|||
|
|
@ -58,15 +58,6 @@ export const CHAT_SDK_REGISTRY: ChatSdkDefinition[] = [
|
|||
description: "OpenAI function calling with the 7 Supermemory tools",
|
||||
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",
|
||||
label: "OpenAI + middleware",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
PARAMETER_DESCRIPTIONS,
|
||||
TOOL_DESCRIPTIONS,
|
||||
} from "../../../../packages/tools/src/tools-shared"
|
||||
} from "../../../../packages/tools/src/tools-descriptions"
|
||||
|
||||
export interface CatalogParameter {
|
||||
name: string
|
||||
|
|
@ -59,10 +59,6 @@ export const TOOL_CATALOG: CatalogTool[] = [
|
|||
pythonName: "get_profile",
|
||||
description: TOOL_DESCRIPTIONS.getProfile,
|
||||
parameters: [
|
||||
{
|
||||
name: "containerTag",
|
||||
description: PARAMETER_DESCRIPTIONS.containerTag,
|
||||
},
|
||||
{
|
||||
name: "query",
|
||||
pythonName: "query",
|
||||
|
|
@ -75,10 +71,6 @@ export const TOOL_CATALOG: CatalogTool[] = [
|
|||
pythonName: "document_list",
|
||||
description: TOOL_DESCRIPTIONS.documentList,
|
||||
parameters: [
|
||||
{
|
||||
name: "containerTag",
|
||||
description: PARAMETER_DESCRIPTIONS.containerTag,
|
||||
},
|
||||
{
|
||||
name: "limit",
|
||||
pythonName: "limit",
|
||||
|
|
@ -102,10 +94,6 @@ export const TOOL_CATALOG: CatalogTool[] = [
|
|||
description: PARAMETER_DESCRIPTIONS.documentId,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "containerTag",
|
||||
description: PARAMETER_DESCRIPTIONS.documentContainerTag,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -136,10 +124,6 @@ export const TOOL_CATALOG: CatalogTool[] = [
|
|||
pythonName: "memory_forget",
|
||||
description: TOOL_DESCRIPTIONS.memoryForget,
|
||||
parameters: [
|
||||
{
|
||||
name: "containerTag",
|
||||
description: PARAMETER_DESCRIPTIONS.containerTag,
|
||||
},
|
||||
{
|
||||
name: "memoryId",
|
||||
pythonName: "memory_id",
|
||||
|
|
@ -150,11 +134,6 @@ export const TOOL_CATALOG: CatalogTool[] = [
|
|||
pythonName: "memory_content",
|
||||
description: PARAMETER_DESCRIPTIONS.memoryContent,
|
||||
},
|
||||
{
|
||||
name: "reason",
|
||||
pythonName: "reason",
|
||||
description: PARAMETER_DESCRIPTIONS.reason,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
115
bun.lock
115
bun.lock
|
|
@ -126,14 +126,13 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^2.0.22",
|
||||
"@supermemory/ai-sdk": "workspace:*",
|
||||
"@supermemory/tools": "workspace:*",
|
||||
"ai": "^5.0.113",
|
||||
"next": "^16.3.3",
|
||||
"openai": "^4.104.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"supermemory": "^4.25.4",
|
||||
"supermemory": "^5.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
|
@ -164,24 +163,6 @@
|
|||
"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": {
|
||||
"name": "docs-test",
|
||||
"version": "1.0.0",
|
||||
|
|
@ -246,14 +227,14 @@
|
|||
},
|
||||
"packages/tools": {
|
||||
"name": "@supermemory/tools",
|
||||
"version": "2.3.0",
|
||||
"version": "3.0.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^2.0.25",
|
||||
"@ai-sdk/openai": "^2.0.23",
|
||||
"ai": "^5.0.29",
|
||||
"lru-cache": "^11.2.6",
|
||||
"openai": "^4.104.0",
|
||||
"supermemory": "^4.25.4",
|
||||
"supermemory": "^5.0.0",
|
||||
"zod": "^4.1.5",
|
||||
},
|
||||
"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/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=="],
|
||||
|
||||
|
|
@ -1526,8 +1507,6 @@
|
|||
|
||||
"@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/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-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=="],
|
||||
|
||||
|
|
@ -3604,7 +3583,7 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -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/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/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/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/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/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/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/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/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/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/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-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/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-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/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/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/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/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/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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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/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/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=="],
|
||||
|
||||
|
|
@ -4390,6 +4351,8 @@
|
|||
|
||||
"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-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=="],
|
||||
|
||||
"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/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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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/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=="],
|
||||
|
||||
|
|
@ -4632,6 +4595,8 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||
|
|
@ -4666,6 +4631,8 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"@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/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/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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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/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/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/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-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/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/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=="],
|
||||
|
||||
"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/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/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=="],
|
||||
|
||||
"public-ip/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="],
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
src/
|
||||
.turbo/
|
||||
.env
|
||||
tsdown.config.ts
|
||||
tsconfig.json
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from "./tools"
|
||||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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"
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"extends": "@total-typescript/tsconfig/bundler/dom/library-monorepo",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@lib/*": ["../lib/*"]
|
||||
},
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
|
|
@ -1,8 +1,18 @@
|
|||
# @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
|
||||
|
||||
|
|
@ -10,12 +20,25 @@ This package provides supermemory tools for AI SDK, OpenAI, and Mastra through d
|
|||
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
|
||||
|
||||
The package provides three submodule imports:
|
||||
- `@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)
|
||||
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).
|
||||
|
||||
### AI SDK Usage
|
||||
|
||||
|
|
@ -30,7 +53,7 @@ const openai = createOpenAI({
|
|||
|
||||
// Create all tools
|
||||
const tools = supermemoryTools(process.env.SUPERMEMORY_API_KEY!, {
|
||||
containerTags: ["your-user-id"],
|
||||
namespace: "your-user-id",
|
||||
})
|
||||
|
||||
// Use with AI SDK
|
||||
|
|
@ -48,17 +71,17 @@ const result = await generateText({
|
|||
|
||||
// Or create individual tools
|
||||
const searchTool = searchMemoriesTool(process.env.SUPERMEMORY_API_KEY!, {
|
||||
projectId: "your-project-id",
|
||||
namespace: "your-user-id",
|
||||
})
|
||||
|
||||
const addTool = addMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
|
||||
projectId: "your-project-id",
|
||||
namespace: "your-user-id",
|
||||
})
|
||||
```
|
||||
|
||||
#### 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.
|
||||
- **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"
|
||||
|
||||
const modelWithMemory = withSupermemory(openai("gpt-5"), {
|
||||
containerTag: "user_id_life",
|
||||
customId: "conversation-456",
|
||||
namespace: "user_id_life",
|
||||
id: "conversation-456",
|
||||
})
|
||||
|
||||
const result = await generateText({
|
||||
|
|
@ -90,8 +113,8 @@ import { withSupermemory } from "@supermemory/tools/ai-sdk"
|
|||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
const modelWithMemory = withSupermemory(openai("gpt-5"), {
|
||||
containerTag: "user_id_life",
|
||||
customId: "conversation-456",
|
||||
namespace: "user_id_life",
|
||||
id: "conversation-456",
|
||||
verbose: true,
|
||||
})
|
||||
|
||||
|
|
@ -105,7 +128,7 @@ console.log(result.text)
|
|||
|
||||
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] System prompt exists: false
|
||||
[supermemory] Found 3 memories
|
||||
|
|
@ -125,14 +148,14 @@ import { openai } from "@ai-sdk/openai"
|
|||
|
||||
// Uses profile mode by default - gets all user profile memories
|
||||
const modelWithMemory = withSupermemory(openai("gpt-4"), {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-456",
|
||||
namespace: "user-123",
|
||||
id: "conversation-456",
|
||||
})
|
||||
|
||||
// Explicitly specify profile mode
|
||||
const modelWithProfile = withSupermemory(openai("gpt-4"), {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-456",
|
||||
namespace: "user-123",
|
||||
id: "conversation-456",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
|
|
@ -149,8 +172,8 @@ import { withSupermemory } from "@supermemory/tools/ai-sdk"
|
|||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
const modelWithQuery = withSupermemory(openai("gpt-4"), {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-456",
|
||||
namespace: "user-123",
|
||||
id: "conversation-456",
|
||||
mode: "query",
|
||||
})
|
||||
|
||||
|
|
@ -167,8 +190,8 @@ import { withSupermemory } from "@supermemory/tools/ai-sdk"
|
|||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
const modelWithFull = withSupermemory(openai("gpt-4"), {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-456",
|
||||
namespace: "user-123",
|
||||
id: "conversation-456",
|
||||
mode: "full",
|
||||
})
|
||||
|
||||
|
|
@ -189,8 +212,8 @@ import { withSupermemory } from "@supermemory/tools/ai-sdk"
|
|||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
const modelWithAutoSave = withSupermemory(openai("gpt-4"), {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-456",
|
||||
namespace: "user-123",
|
||||
id: "conversation-456",
|
||||
addMemory: "always",
|
||||
})
|
||||
|
||||
|
|
@ -204,8 +227,8 @@ const result = await generateText({
|
|||
**Never Save Memories** - Only retrieves memories without storing new ones:
|
||||
```typescript
|
||||
const modelWithNoSave = withSupermemory(openai("gpt-4"), {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-456",
|
||||
namespace: "user-123",
|
||||
id: "conversation-456",
|
||||
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:
|
||||
```typescript
|
||||
const modelWithOptions = withSupermemory(openai("gpt-4"), {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-456",
|
||||
namespace: "user-123",
|
||||
id: "conversation-456",
|
||||
mode: "profile",
|
||||
addMemory: "always",
|
||||
verbose: true,
|
||||
|
|
@ -242,8 +265,8 @@ ${data.generalSearchMemories}
|
|||
`.trim()
|
||||
|
||||
const modelWithCustomPrompt = withSupermemory(openai("gpt-4"), {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-456",
|
||||
namespace: "user-123",
|
||||
id: "conversation-456",
|
||||
mode: "full",
|
||||
promptTemplate: customPrompt,
|
||||
})
|
||||
|
|
@ -270,8 +293,8 @@ import { withSupermemory } from "@supermemory/tools/openai"
|
|||
|
||||
// Create OpenAI client with supermemory middleware
|
||||
const openaiWithSupermemory = withSupermemory(openai, {
|
||||
containerTag: "user-123", // Required: identifies the user/container
|
||||
customId: "conversation-456", // Required: groups messages into the same document
|
||||
namespace: "user-123", // Required: identifies the user or project
|
||||
id: "conversation-456", // Required: groups messages into the same document
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY, // Optional env fallback
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL,
|
||||
mode: "full",
|
||||
|
|
@ -296,8 +319,8 @@ The middleware supports the same configuration options as the AI SDK version:
|
|||
|
||||
```typescript
|
||||
const openaiWithSupermemory = withSupermemory(openai, {
|
||||
containerTag: "user-123", // Required: identifies the user/container
|
||||
customId: "conversation-456", // Required: groups messages for contextual memory
|
||||
namespace: "user-123", // Required: identifies the user or project
|
||||
id: "conversation-456", // Required: groups messages for contextual memory
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY, // Optional; captured per client
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL,
|
||||
mode: "full", // "profile" | "query" | "full"
|
||||
|
|
@ -322,8 +345,8 @@ export async function POST(req: Request) {
|
|||
}
|
||||
|
||||
const openaiWithSupermemory = withSupermemory(openai, {
|
||||
containerTag: "user-123",
|
||||
customId: conversationId,
|
||||
namespace: "user-123",
|
||||
id: conversationId,
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY,
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL,
|
||||
mode: "full",
|
||||
|
|
@ -356,7 +379,7 @@ const toolDefinitions = getToolDefinitions()
|
|||
|
||||
// Create tool executor
|
||||
const executeToolCall = createToolCallExecutor(process.env.SUPERMEMORY_API_KEY!, {
|
||||
projectId: "your-project-id",
|
||||
namespace: "your-user-id",
|
||||
})
|
||||
|
||||
// Use with OpenAI Chat Completions
|
||||
|
|
@ -381,7 +404,7 @@ if (completion.choices[0]?.message.tool_calls) {
|
|||
|
||||
// Or create individual function-based tools
|
||||
const tools = supermemoryTools(process.env.SUPERMEMORY_API_KEY!, {
|
||||
containerTags: ["your-user-id"],
|
||||
namespace: "your-user-id",
|
||||
})
|
||||
|
||||
const searchResult = await tools.searchMemories({
|
||||
|
|
@ -418,8 +441,8 @@ const agent = new Agent(withSupermemory(
|
|||
instructions: "You are a helpful assistant.",
|
||||
},
|
||||
{
|
||||
containerTag: "user-123", // Required: scopes memories to this user
|
||||
customId: "conv-456", // Required: groups messages for contextual memory
|
||||
namespace: "user-123", // Required: scopes memories to this user
|
||||
id: "conv-456", // Required: groups messages for contextual memory
|
||||
mode: "full",
|
||||
}
|
||||
))
|
||||
|
|
@ -438,8 +461,8 @@ import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
|
|||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
const { input, output } = createSupermemoryProcessors({
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
namespace: "user-123",
|
||||
id: "conv-456",
|
||||
mode: "full",
|
||||
verbose: true, // Enable logging
|
||||
})
|
||||
|
|
@ -467,11 +490,11 @@ import { openai } from "@ai-sdk/openai"
|
|||
|
||||
async function main() {
|
||||
const userId = "user-alex-123"
|
||||
const customId = `thread-${Date.now()}`
|
||||
const id = `thread-${Date.now()}`
|
||||
|
||||
const { input, output } = createSupermemoryProcessors({
|
||||
containerTag: userId,
|
||||
customId,
|
||||
namespace: userId,
|
||||
id,
|
||||
mode: "profile", // Fetch user profile memories
|
||||
verbose: true,
|
||||
})
|
||||
|
|
@ -509,22 +532,22 @@ main()
|
|||
```typescript
|
||||
// Profile mode - good for general personalization
|
||||
const { input } = createSupermemoryProcessors({
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
namespace: "user-123",
|
||||
id: "conv-456",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
// Query mode - good for specific lookups
|
||||
const { input } = createSupermemoryProcessors({
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
namespace: "user-123",
|
||||
id: "conv-456",
|
||||
mode: "query",
|
||||
})
|
||||
|
||||
// Full mode - comprehensive context
|
||||
const { input } = createSupermemoryProcessors({
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
namespace: "user-123",
|
||||
id: "conv-456",
|
||||
mode: "full",
|
||||
})
|
||||
```
|
||||
|
|
@ -544,8 +567,8 @@ ${data.generalSearchMemories}
|
|||
`.trim()
|
||||
|
||||
const { input, output } = createSupermemoryProcessors({
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
namespace: "user-123",
|
||||
id: "conv-456",
|
||||
mode: "full",
|
||||
promptTemplate: customTemplate,
|
||||
})
|
||||
|
|
@ -553,7 +576,7 @@ const { input, output } = createSupermemoryProcessors({
|
|||
|
||||
#### 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
|
||||
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"
|
||||
|
||||
const { input, output } = createSupermemoryProcessors({
|
||||
containerTag: "user-123",
|
||||
customId: "fallback-conv", // Used only when RequestContext doesn't provide a threadId
|
||||
namespace: "user-123",
|
||||
id: "fallback-conv", // Used only when RequestContext doesn't provide a threadId
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
|
|
@ -574,7 +597,7 @@ const agent = new Agent({
|
|||
outputProcessors: [output],
|
||||
})
|
||||
|
||||
// Per-request threadId takes precedence over customId
|
||||
// Per-request threadId takes precedence over id
|
||||
const ctx = new RequestContext()
|
||||
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"
|
||||
```
|
||||
|
||||
> **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
|
||||
|
||||
```typescript
|
||||
interface SupermemoryMastraOptions {
|
||||
containerTag: string // Required: User/container tag for scoping memories
|
||||
customId: string // Required: Groups messages into a single document for contextual memory
|
||||
namespace: string // Required: Namespace (e.g. user ID) for scoping memories
|
||||
id: string // Required: Groups messages into a single document for contextual memory
|
||||
apiKey?: string // Supermemory API key (or use SUPERMEMORY_API_KEY env var)
|
||||
baseUrl?: string // Custom API endpoint
|
||||
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
|
||||
|
||||
Both modules accept the same configuration interface:
|
||||
The tool entry points (`ai-sdk` and `openai`) accept the same configuration:
|
||||
|
||||
```typescript
|
||||
interface SupermemoryToolsConfig {
|
||||
baseUrl?: string
|
||||
containerTags?: string[]
|
||||
projectId?: string
|
||||
namespace?: string
|
||||
strict?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
- **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.
|
||||
- **projectId**: Project ID which gets converted to container tag format (mutually exclusive with containerTags)
|
||||
- **namespace**: The one namespace every tool reads and writes (default: `sm_project_default`). `documentDelete` refuses a document that is still processing.
|
||||
- **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
|
||||
|
|
@ -630,11 +700,11 @@ const openrouter = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY })
|
|||
|
||||
const tools = {
|
||||
searchMemories: searchMemoriesTool(apiKey, {
|
||||
containerTags: [userId],
|
||||
namespace: userId,
|
||||
strict: true // ✅ Required for OpenAI strict mode
|
||||
}),
|
||||
addMemory: addMemoryTool(apiKey, {
|
||||
containerTags: [userId],
|
||||
namespace: userId,
|
||||
strict: true
|
||||
}),
|
||||
}
|
||||
|
|
@ -654,8 +724,8 @@ The `withSupermemory` middleware accepts a configuration object as the second ar
|
|||
|
||||
```typescript
|
||||
interface WithSupermemoryOptions {
|
||||
containerTag: string // Required: identifies the user/container
|
||||
customId: string // Required: groups messages into the same document
|
||||
namespace: string // Required: identifies the user or project
|
||||
id: string // Required: groups messages into the same document
|
||||
verbose?: boolean
|
||||
mode?: "profile" | "query" | "full"
|
||||
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)
|
||||
- **customId**: Required. Custom ID to group messages into a single document for contextual memory generation
|
||||
- **namespace**: Required. The namespace to read and write (e.g., user ID, project ID)
|
||||
- **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)
|
||||
- **mode**: Memory search mode - "profile" (default), "query", or "full"
|
||||
- **addMemory**: Automatic memory storage mode - "always" (default) or "never"
|
||||
|
|
@ -677,19 +747,28 @@ interface WithSupermemoryOptions {
|
|||
## Available Tools
|
||||
|
||||
### 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:**
|
||||
- `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)
|
||||
|
||||
### 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:**
|
||||
- `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
|
||||
|
|
@ -713,7 +792,7 @@ const anthropic = new Anthropic({
|
|||
})
|
||||
|
||||
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
|
||||
projectId: 'my-app',
|
||||
namespace: 'my-app',
|
||||
})
|
||||
|
||||
async function chatWithMemory(userMessage: string) {
|
||||
|
|
@ -758,12 +837,13 @@ Claude can perform these memory operations automatically:
|
|||
- **`delete`** - Delete 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
|
||||
|
||||
```env
|
||||
SUPERMEMORY_API_KEY=your_supermemory_api_key
|
||||
ANTHROPIC_API_KEY=your_anthropic_api_key # for Claude Memory Tool
|
||||
SUPERMEMORY_BASE_URL=https://your-custom-url # optional
|
||||
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 the Claude memory tool
|
||||
```
|
||||
|
||||
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`.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@supermemory/tools",
|
||||
"type": "module",
|
||||
"version": "2.3.0",
|
||||
"version": "3.0.0",
|
||||
"description": "Memory tools for AI SDK, OpenAI, Voltagent and Mastra with supermemory",
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
"ai": "^5.0.29",
|
||||
"lru-cache": "^11.2.6",
|
||||
"openai": "^4.104.0",
|
||||
"supermemory": "^4.25.4",
|
||||
"supermemory": "^5.0.0",
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,35 +1,32 @@
|
|||
import Supermemory from "supermemory"
|
||||
import { tool } from "ai"
|
||||
import { z } from "zod"
|
||||
import {
|
||||
CLIENT_OPTIONS,
|
||||
DEFAULT_VALUES,
|
||||
PARAMETER_DESCRIPTIONS,
|
||||
SEARCH_LIMIT_BOUNDS,
|
||||
TOOL_DESCRIPTIONS,
|
||||
clampSearchLimit,
|
||||
deleteDocumentByIdentifier,
|
||||
getContainerTags,
|
||||
createToolsClient,
|
||||
deleteDocument,
|
||||
forgetMemory,
|
||||
getNamespace,
|
||||
getProfileWithSearch,
|
||||
} from "./tools-shared"
|
||||
import { forgetMemoryRequest } from "./shared/forget-memory"
|
||||
import type { SupermemoryToolsConfig } from "./types"
|
||||
|
||||
function createClient(apiKey: string, config?: SupermemoryToolsConfig) {
|
||||
return new Supermemory({
|
||||
apiKey,
|
||||
...CLIENT_OPTIONS,
|
||||
...(config?.baseUrl ? { baseURL: config.baseUrl } : {}),
|
||||
})
|
||||
function toolError(error: unknown) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
}
|
||||
|
||||
// Export individual tool creators
|
||||
export const searchMemoriesTool = (
|
||||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) => {
|
||||
const client = createClient(apiKey, config)
|
||||
|
||||
const containerTags = getContainerTags(config)
|
||||
const client = createToolsClient(apiKey, config)
|
||||
const namespace = getNamespace(config)
|
||||
const strict = config?.strict ?? false
|
||||
|
||||
return tool({
|
||||
|
|
@ -67,9 +64,8 @@ export const searchMemoriesTool = (
|
|||
}),
|
||||
execute: async ({ informationToGet, limit = DEFAULT_VALUES.limit }) => {
|
||||
try {
|
||||
const response = await client.search({
|
||||
q: informationToGet,
|
||||
containerTag: containerTags[0],
|
||||
const response = await client.search(namespace, {
|
||||
query: informationToGet,
|
||||
limit: clampSearchLimit(limit),
|
||||
threshold: DEFAULT_VALUES.searchThreshold,
|
||||
searchMode: "hybrid",
|
||||
|
|
@ -81,10 +77,7 @@ export const searchMemoriesTool = (
|
|||
count: response.results?.length || 0,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -94,9 +87,8 @@ export const addMemoryTool = (
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) => {
|
||||
const client = createClient(apiKey, config)
|
||||
|
||||
const containerTags = getContainerTags(config)
|
||||
const client = createToolsClient(apiKey, config)
|
||||
const namespace = getNamespace(config)
|
||||
|
||||
return tool({
|
||||
description: TOOL_DESCRIPTIONS.addMemory,
|
||||
|
|
@ -105,12 +97,9 @@ export const addMemoryTool = (
|
|||
}),
|
||||
execute: async ({ memory }) => {
|
||||
try {
|
||||
const metadata: Record<string, string | number | boolean> = {}
|
||||
|
||||
const response = await client.add({
|
||||
const response = await client.add(namespace, {
|
||||
content: memory,
|
||||
containerTags,
|
||||
...(Object.keys(metadata).length > 0 && { metadata }),
|
||||
dreaming: "instant",
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
@ -118,10 +107,7 @@ export const addMemoryTool = (
|
|||
memory: response,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -131,38 +117,22 @@ export const getProfileTool = (
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) => {
|
||||
const client = createClient(apiKey, config)
|
||||
|
||||
const containerTags = getContainerTags(config)
|
||||
const strict = config?.strict ?? false
|
||||
const client = createToolsClient(apiKey, config)
|
||||
const namespace = getNamespace(config)
|
||||
|
||||
return tool({
|
||||
description: TOOL_DESCRIPTIONS.getProfile,
|
||||
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),
|
||||
}),
|
||||
execute: async ({ containerTag, query }) => {
|
||||
execute: async ({ query }) => {
|
||||
try {
|
||||
const tag = containerTag || containerTags[0]
|
||||
|
||||
const response = await client.profile({
|
||||
containerTag: tag,
|
||||
...(query && { q: query }),
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
profile: response.profile,
|
||||
searchResults: response.searchResults,
|
||||
...(await getProfileWithSearch(client, namespace, query)),
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -172,18 +142,13 @@ export const documentListTool = (
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) => {
|
||||
const client = createClient(apiKey, config)
|
||||
|
||||
const containerTags = getContainerTags(config)
|
||||
const client = createToolsClient(apiKey, config)
|
||||
const namespace = getNamespace(config)
|
||||
const strict = config?.strict ?? false
|
||||
|
||||
return tool({
|
||||
description: TOOL_DESCRIPTIONS.documentList,
|
||||
inputSchema: z.object({
|
||||
containerTag: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(PARAMETER_DESCRIPTIONS.containerTag),
|
||||
limit: strict
|
||||
? z.coerce
|
||||
.number()
|
||||
|
|
@ -196,28 +161,20 @@ export const documentListTool = (
|
|||
.describe(PARAMETER_DESCRIPTIONS.limit),
|
||||
page: z.coerce.number().optional().describe(PARAMETER_DESCRIPTIONS.page),
|
||||
}),
|
||||
execute: async ({ containerTag, limit, page }) => {
|
||||
execute: async ({ limit, page }) => {
|
||||
try {
|
||||
const scopeTags: [string, ...string[]] = containerTag
|
||||
? [containerTag]
|
||||
: containerTags
|
||||
|
||||
const response = await client.documents.list({
|
||||
containerTags: scopeTags,
|
||||
const response = await client.list(namespace, "documents", {
|
||||
limit: limit || DEFAULT_VALUES.limit,
|
||||
...(page !== undefined && { page }),
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
documents: response.memories,
|
||||
documents: response.documents,
|
||||
pagination: response.pagination,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -227,40 +184,24 @@ export const documentDeleteTool = (
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) => {
|
||||
const client = createClient(apiKey, config)
|
||||
const containerTags = getContainerTags(config)
|
||||
const strict = config?.strict ?? false
|
||||
const client = createToolsClient(apiKey, config)
|
||||
const namespace = getNamespace(config)
|
||||
|
||||
return tool({
|
||||
description: TOOL_DESCRIPTIONS.documentDelete,
|
||||
inputSchema: z.object({
|
||||
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 {
|
||||
const scopeTags: [string, ...string[]] = containerTag
|
||||
? [containerTag]
|
||||
: containerTags
|
||||
await deleteDocumentByIdentifier(client, documentId, scopeTags)
|
||||
await deleteDocument(client, namespace, documentId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Document ${documentId} deleted successfully`,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -270,9 +211,8 @@ export const documentAddTool = (
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) => {
|
||||
const client = createClient(apiKey, config)
|
||||
|
||||
const containerTags = getContainerTags(config)
|
||||
const client = createToolsClient(apiKey, config)
|
||||
const namespace = getNamespace(config)
|
||||
|
||||
return tool({
|
||||
description: TOOL_DESCRIPTIONS.documentAdd,
|
||||
|
|
@ -290,9 +230,8 @@ export const documentAddTool = (
|
|||
if (title) metadata.title = title
|
||||
if (description) metadata.description = description
|
||||
|
||||
const response = await client.documents.add({
|
||||
const response = await client.add(namespace, {
|
||||
content,
|
||||
containerTags,
|
||||
...(Object.keys(metadata).length > 0 && { metadata }),
|
||||
})
|
||||
|
||||
|
|
@ -301,10 +240,7 @@ export const documentAddTool = (
|
|||
document: response,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -314,23 +250,19 @@ export const memoryForgetTool = (
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) => {
|
||||
const containerTags = getContainerTags(config)
|
||||
const client = createToolsClient(apiKey, config)
|
||||
const namespace = getNamespace(config)
|
||||
|
||||
return tool({
|
||||
description: TOOL_DESCRIPTIONS.memoryForget,
|
||||
inputSchema: z.object({
|
||||
containerTag: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(PARAMETER_DESCRIPTIONS.containerTag),
|
||||
memoryId: z.string().optional().describe(PARAMETER_DESCRIPTIONS.memoryId),
|
||||
memoryContent: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(PARAMETER_DESCRIPTIONS.memoryContent),
|
||||
reason: z.string().optional().describe(PARAMETER_DESCRIPTIONS.reason),
|
||||
}),
|
||||
execute: async ({ containerTag, memoryId, memoryContent, reason }) => {
|
||||
execute: async ({ memoryId, memoryContent }) => {
|
||||
try {
|
||||
if (!memoryId && !memoryContent) {
|
||||
return {
|
||||
|
|
@ -339,36 +271,22 @@ export const memoryForgetTool = (
|
|||
}
|
||||
}
|
||||
|
||||
const tag = containerTag || containerTags[0]
|
||||
|
||||
await forgetMemoryRequest(
|
||||
apiKey,
|
||||
{
|
||||
containerTag: tag as string,
|
||||
...(memoryId && { id: memoryId }),
|
||||
...(memoryContent && { content: memoryContent }),
|
||||
...(reason && { reason }),
|
||||
},
|
||||
config?.baseUrl,
|
||||
)
|
||||
await forgetMemory(client, namespace, {
|
||||
...(memoryId && { id: memoryId }),
|
||||
...(memoryContent && { content: memoryContent }),
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Memory forgotten successfully",
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Supermemory tools for AI SDK
|
||||
*/
|
||||
export function supermemoryTools(
|
||||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
|
|
@ -391,4 +309,4 @@ export {
|
|||
type PromptTemplate,
|
||||
type MemoryPromptData,
|
||||
} from "./vercel"
|
||||
export { getContainerTags } from "./tools-shared"
|
||||
export { getNamespace } from "./tools-shared"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const anthropic = new Anthropic({
|
|||
})
|
||||
|
||||
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
|
||||
projectId: "my-app",
|
||||
namespace: "my-app",
|
||||
})
|
||||
|
||||
async function chatWithMemory(userMessage: string) {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
// Mock the Supermemory SDK so the Claude memory tool's document-backed file
|
||||
// operations can be exercised deterministically without any network access.
|
||||
const documentsListMock = vi.fn()
|
||||
const documentsGetMock = vi.fn()
|
||||
const documentsDeleteBulkMock = vi.fn()
|
||||
const addMock = vi.fn()
|
||||
// Mock the v5 SDK so document-backed file operations run without network access.
|
||||
const sdk = vi.hoisted(() => {
|
||||
class NotFoundError extends Error {
|
||||
statusCode = 404
|
||||
}
|
||||
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", () => {
|
||||
return {
|
||||
default: class MockSupermemory {
|
||||
add = addMock
|
||||
memories = { forget: vi.fn() }
|
||||
documents = {
|
||||
list: documentsListMock,
|
||||
get: documentsGetMock,
|
||||
deleteBulk: documentsDeleteBulkMock,
|
||||
}
|
||||
},
|
||||
class Supermemory {
|
||||
add = sdk.add
|
||||
list = sdk.list
|
||||
documents = { get: sdk.get, update: sdk.update, delete: sdk.delete }
|
||||
}
|
||||
return { default: Supermemory, Supermemory, NotFoundError: sdk.NotFoundError }
|
||||
})
|
||||
|
||||
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.
|
||||
const FILE_CONTENT = "line1\nline2\nline3\nline4\nline5"
|
||||
const FILE_DOCUMENT = {
|
||||
id: "document-notes",
|
||||
customId: "memories_notes_txt",
|
||||
id: "memories_notes_txt",
|
||||
filePath: FILE_PATH,
|
||||
content: FILE_CONTENT,
|
||||
}
|
||||
const NEIGHBOUR_DOCUMENT = {
|
||||
id: "document-notes-backup",
|
||||
customId: "memories_notes_backup_txt",
|
||||
id: "memories_notes_backup_txt",
|
||||
filePath: "/memories/notes.backup.txt",
|
||||
content: "backup stuff",
|
||||
}
|
||||
|
||||
// v5 resolves caller-defined IDs inside the namespace.
|
||||
function mockDocuments(documents: (typeof FILE_DOCUMENT)[]) {
|
||||
documentsListMock.mockResolvedValue({
|
||||
memories: documents.map((document) => ({
|
||||
id: document.id,
|
||||
customId: document.customId,
|
||||
containerTags: ["claude_memory"],
|
||||
metadata: {
|
||||
claude_memory_type: "file",
|
||||
file_path: document.filePath,
|
||||
},
|
||||
})),
|
||||
pagination: { totalPages: 1 },
|
||||
})
|
||||
documentsGetMock.mockImplementation(async (id: string) => {
|
||||
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,
|
||||
}
|
||||
})
|
||||
documentsGetMock.mockImplementation(
|
||||
async (_namespace: string, id: string) => {
|
||||
const document = documents.find((candidate) => candidate.id === id)
|
||||
if (!document) throw new sdk.NotFoundError(`Document not found: ${id}`)
|
||||
return {
|
||||
id: document.id,
|
||||
metadata: {
|
||||
source: "claude-memory",
|
||||
claude_memory_type: "file",
|
||||
file_path: document.filePath,
|
||||
},
|
||||
content: document.content,
|
||||
system: { status: "done" },
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function mockDocument(content: string) {
|
||||
|
|
@ -76,7 +70,6 @@ describe("ClaudeMemoryTool view_range", () => {
|
|||
let tool: ClaudeMemoryTool
|
||||
|
||||
beforeEach(() => {
|
||||
documentsListMock.mockReset()
|
||||
documentsGetMock.mockReset()
|
||||
mockDocument(FILE_CONTENT)
|
||||
tool = new ClaudeMemoryTool("test-api-key")
|
||||
|
|
@ -132,9 +125,8 @@ describe("ClaudeMemoryTool exact-file matching", () => {
|
|||
let tool: ClaudeMemoryTool
|
||||
|
||||
beforeEach(() => {
|
||||
documentsListMock.mockReset()
|
||||
documentsGetMock.mockReset()
|
||||
addMock.mockReset()
|
||||
updateMock.mockReset().mockResolvedValue({ id: "doc", status: "queued" })
|
||||
tool = new ClaudeMemoryTool("test-api-key")
|
||||
})
|
||||
|
||||
|
|
@ -176,7 +168,23 @@ describe("ClaudeMemoryTool exact-file matching", () => {
|
|||
})
|
||||
|
||||
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
|
||||
|
||||
beforeEach(() => {
|
||||
documentsListMock.mockReset()
|
||||
documentsGetMock.mockReset()
|
||||
addMock.mockReset()
|
||||
updateMock.mockReset().mockResolvedValue({ id: "doc", status: "queued" })
|
||||
mockDocument(FILE_CONTENT)
|
||||
tool = new ClaudeMemoryTool("test-api-key")
|
||||
})
|
||||
|
|
@ -205,8 +212,8 @@ describe("ClaudeMemoryTool str_replace replacement literalness", () => {
|
|||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(addMock).toHaveBeenCalledTimes(1)
|
||||
const stored = addMock.mock.calls[0]?.[0]?.content as string
|
||||
expect(updateMock).toHaveBeenCalledTimes(1)
|
||||
const stored = updateMock.mock.calls[0]?.[2]?.content as string
|
||||
expect(stored).toContain(`price is ${dollarSequence} today`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import Supermemory from "supermemory"
|
||||
import { deleteDocumentById, getContainerTags } from "./tools-shared"
|
||||
import type { Supermemory } from "supermemory"
|
||||
import {
|
||||
createToolsClient,
|
||||
getNamespace,
|
||||
isNotFoundError,
|
||||
} from "./tools-shared"
|
||||
import type { SupermemoryToolsConfig } from "./types"
|
||||
|
||||
// Claude Memory Tool Types
|
||||
export interface ClaudeMemoryConfig extends SupermemoryToolsConfig {
|
||||
memoryContainerTag?: string
|
||||
}
|
||||
export type ClaudeMemoryConfig = SupermemoryToolsConfig
|
||||
|
||||
export const CLAUDE_MEMORY_SOURCE = "claude-memory"
|
||||
|
||||
export interface MemoryCommand {
|
||||
command: "view" | "create" | "str_replace" | "insert" | "delete" | "rename"
|
||||
|
|
@ -45,40 +48,19 @@ interface ClaudeFileDocument {
|
|||
metadata: ClaudeFileMetadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude Memory Tool - Client-side implementation
|
||||
* Maps Claude's memory tool commands to supermemory document operations
|
||||
*/
|
||||
/** Maps Claude's memory tool commands to Supermemory documents in one namespace. */
|
||||
export class ClaudeMemoryTool {
|
||||
private client: Supermemory
|
||||
private containerTags: string[]
|
||||
private scopeContainerTags: [string, ...string[]]
|
||||
private memoryContainerPrefix: string
|
||||
private namespace: string
|
||||
|
||||
/**
|
||||
* Normalize file path to be used as customId
|
||||
* Converts /memories/file.txt -> memories_file_txt
|
||||
*/
|
||||
private normalizePathToCustomId(path: string): string {
|
||||
return path
|
||||
.replace(/^\//, "") // Remove leading slash
|
||||
.replace(/\//g, "_") // Replace / with _
|
||||
.replace(/\./g, "_") // Replace . with _
|
||||
/** Converts /memories/file.txt -> memories_file_txt */
|
||||
private normalizePathToId(path: string): string {
|
||||
return path.replace(/^\//, "").replace(/\//g, "_").replace(/\./g, "_")
|
||||
}
|
||||
|
||||
constructor(apiKey: string, config?: ClaudeMemoryConfig) {
|
||||
this.client = new Supermemory({
|
||||
apiKey,
|
||||
...(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]
|
||||
this.client = createToolsClient(apiKey, config)
|
||||
this.namespace = getNamespace(config)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -203,93 +185,48 @@ export class ClaudeMemoryTool {
|
|||
*/
|
||||
private async listDirectory(dirPath: string): Promise<MemoryResponse> {
|
||||
try {
|
||||
// Document search returns ranked chunks, not a complete inventory. Walk
|
||||
// every page of the document-list endpoint so files cannot disappear
|
||||
// from a directory merely because they did not rank in a search page.
|
||||
const documents: Supermemory.DocumentListResponse.Memory[] = []
|
||||
// Walk every page so files cannot disappear from a listing.
|
||||
const filePaths: string[] = []
|
||||
let page = 1
|
||||
|
||||
while (true) {
|
||||
const response = await this.client.documents.list({
|
||||
containerTags: this.scopeContainerTags,
|
||||
filters: {
|
||||
AND: [
|
||||
{ key: "claude_memory_type", value: "file" },
|
||||
{
|
||||
key: "file_path",
|
||||
value: dirPath,
|
||||
filterType: "string_contains",
|
||||
},
|
||||
const response = await this.client.list(this.namespace, "documents", {
|
||||
filter: {
|
||||
operator: "and",
|
||||
operands: [
|
||||
{ field: "source", operator: "eq", value: CLAUDE_MEMORY_SOURCE },
|
||||
{ field: "claude_memory_type", operator: "eq", value: "file" },
|
||||
{ field: "file_path", operator: "contains", value: dirPath },
|
||||
],
|
||||
},
|
||||
includeContent: false,
|
||||
limit: 100,
|
||||
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
|
||||
page += 1
|
||||
}
|
||||
|
||||
// Filter files that match the directory path and extract relative paths
|
||||
const files: string[] = []
|
||||
const dirs = new Set<string>()
|
||||
const candidates: Array<{
|
||||
document: Supermemory.DocumentListResponse.Memory
|
||||
filePath: string
|
||||
}> = []
|
||||
|
||||
for (const document of documents) {
|
||||
if (!this.isDocumentInConfiguredScope(document)) continue
|
||||
for (const filePath of filePaths) {
|
||||
const relativePath = filePath.substring(dirPath.length)
|
||||
if (!relativePath) continue
|
||||
|
||||
const filePath = this.getDocumentFilePath(document)
|
||||
if (!filePath || !filePath.startsWith(dirPath)) {
|
||||
continue
|
||||
}
|
||||
candidates.push({ document, filePath })
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
const slashIndex = relativePath.indexOf("/")
|
||||
if (slashIndex > 0) {
|
||||
dirs.add(`${relativePath.substring(0, slashIndex)}/`)
|
||||
} else {
|
||||
files.push(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Format directory listing
|
||||
const entries = [...Array.from(dirs).sort(), ...files.sort()]
|
||||
|
||||
if (entries.length === 0) {
|
||||
|
|
@ -384,19 +321,13 @@ export class ClaudeMemoryTool {
|
|||
fileText: string,
|
||||
): Promise<MemoryResponse> {
|
||||
try {
|
||||
const normalizedId = this.normalizePathToCustomId(filePath)
|
||||
|
||||
const _response = await this.client.add({
|
||||
content: fileText,
|
||||
customId: normalizedId,
|
||||
containerTags: this.containerTags,
|
||||
metadata: {
|
||||
claude_memory_type: "file",
|
||||
file_path: filePath,
|
||||
line_count: fileText.split("\n").length,
|
||||
created_by: "claude_memory_tool",
|
||||
last_modified: new Date().toISOString(),
|
||||
},
|
||||
await this.writeFile(this.normalizePathToId(filePath), fileText, {
|
||||
source: CLAUDE_MEMORY_SOURCE,
|
||||
claude_memory_type: "file",
|
||||
file_path: filePath,
|
||||
line_count: fileText.split("\n").length,
|
||||
created_by: "claude_memory_tool",
|
||||
last_modified: new Date().toISOString(),
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
@ -444,17 +375,10 @@ export class ClaudeMemoryTool {
|
|||
// patterns like $&, $', and $` and silently corrupt the file.
|
||||
const newContent = originalContent.replace(oldStr, () => newStr)
|
||||
|
||||
// Update the document
|
||||
const normalizedId = this.normalizePathToCustomId(filePath)
|
||||
const _updateResponse = await this.client.add({
|
||||
content: newContent,
|
||||
customId: normalizedId,
|
||||
containerTags: this.containerTags,
|
||||
metadata: {
|
||||
...readResult.document.metadata,
|
||||
line_count: newContent.split("\n").length,
|
||||
last_modified: new Date().toISOString(),
|
||||
},
|
||||
await this.writeFile(readResult.document.documentId, newContent, {
|
||||
...readResult.document.metadata,
|
||||
line_count: newContent.split("\n").length,
|
||||
last_modified: new Date().toISOString(),
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
@ -502,17 +426,10 @@ export class ClaudeMemoryTool {
|
|||
lines.splice(insertLine - 1, 0, insertText)
|
||||
const newContent = lines.join("\n")
|
||||
|
||||
// Update the document
|
||||
const normalizedId = this.normalizePathToCustomId(filePath)
|
||||
await this.client.add({
|
||||
content: newContent,
|
||||
customId: normalizedId,
|
||||
containerTags: this.containerTags,
|
||||
metadata: {
|
||||
...readResult.document.metadata,
|
||||
line_count: newContent.split("\n").length,
|
||||
last_modified: new Date().toISOString(),
|
||||
},
|
||||
await this.writeFile(readResult.document.documentId, newContent, {
|
||||
...readResult.document.metadata,
|
||||
line_count: newContent.split("\n").length,
|
||||
last_modified: new Date().toISOString(),
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
@ -541,7 +458,7 @@ export class ClaudeMemoryTool {
|
|||
}
|
||||
}
|
||||
|
||||
await deleteDocumentById(this.client, readResult.document.documentId)
|
||||
await this.deleteFile(readResult.document.documentId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -581,26 +498,17 @@ export class ClaudeMemoryTool {
|
|||
}
|
||||
|
||||
const originalContent = readResult.document.content
|
||||
const newNormalizedId = this.normalizePathToCustomId(newPath)
|
||||
const newNormalizedId = this.normalizePathToId(newPath)
|
||||
|
||||
// Create new document with new path
|
||||
await this.client.add({
|
||||
content: originalContent,
|
||||
customId: newNormalizedId,
|
||||
containerTags: this.containerTags,
|
||||
metadata: {
|
||||
...readResult.document.metadata,
|
||||
file_path: newPath,
|
||||
last_modified: new Date().toISOString(),
|
||||
},
|
||||
await this.writeFile(newNormalizedId, originalContent, {
|
||||
...readResult.document.metadata,
|
||||
file_path: newPath,
|
||||
last_modified: new Date().toISOString(),
|
||||
})
|
||||
|
||||
// Remove the old document so the previous path stops showing up in
|
||||
// listings and search. Skip when both paths normalize to the same
|
||||
// customId — the add above already replaced the content.
|
||||
const oldNormalizedId = this.normalizePathToCustomId(oldPath)
|
||||
if (oldNormalizedId !== newNormalizedId) {
|
||||
await deleteDocumentById(this.client, readResult.document.documentId)
|
||||
// Same normalized ID means the write above already replaced the file in place.
|
||||
if (readResult.document.documentId !== newNormalizedId) {
|
||||
await this.deleteFile(readResult.document.documentId)
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -615,129 +523,74 @@ export class ClaudeMemoryTool {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Get document by file path
|
||||
*/
|
||||
// v5 `add` with an existing id appends; `update` replaces, so writes try update first.
|
||||
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<{
|
||||
success: boolean
|
||||
document?: ClaudeFileDocument
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const normalizedId = this.normalizePathToCustomId(filePath)
|
||||
let page = 1
|
||||
const candidates = new Map<
|
||||
string,
|
||||
Supermemory.DocumentListResponse.Memory
|
||||
>()
|
||||
|
||||
// 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)
|
||||
}
|
||||
const id = this.normalizePathToId(filePath)
|
||||
let document: Awaited<ReturnType<Supermemory["documents"]["get"]>>
|
||||
try {
|
||||
document = await this.client.documents.get(this.namespace, id)
|
||||
} catch (error) {
|
||||
if (isNotFoundError(error)) {
|
||||
return { success: false, error: `File not found: ${filePath}` }
|
||||
}
|
||||
|
||||
if (page >= response.pagination.totalPages) break
|
||||
page += 1
|
||||
throw error
|
||||
}
|
||||
|
||||
const exactMatches: Array<{
|
||||
candidate: Supermemory.DocumentListResponse.Memory
|
||||
document: Supermemory.DocumentGetResponse
|
||||
}> = []
|
||||
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) {
|
||||
// Different paths can normalize to the same ID; only the exact file counts.
|
||||
if (
|
||||
document.metadata?.source !== CLAUDE_MEMORY_SOURCE ||
|
||||
this.getDocumentFilePath(document) !== filePath
|
||||
) {
|
||||
return { success: false, error: `File not found: ${filePath}` }
|
||||
}
|
||||
const { candidate, document } = match
|
||||
const content =
|
||||
typeof document.content === "string"
|
||||
? document.content
|
||||
: typeof document.raw === "string"
|
||||
? document.raw
|
||||
: undefined
|
||||
if (content === undefined) {
|
||||
if (typeof document.content !== "string") {
|
||||
return {
|
||||
success: false,
|
||||
error: `File content unavailable: ${filePath}`,
|
||||
}
|
||||
}
|
||||
const metadata =
|
||||
document.metadata &&
|
||||
typeof document.metadata === "object" &&
|
||||
!Array.isArray(document.metadata)
|
||||
? (document.metadata as ClaudeFileMetadata)
|
||||
: {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
document: { documentId: candidate.id, content, metadata },
|
||||
document: {
|
||||
documentId: id,
|
||||
content: document.content,
|
||||
metadata: document.metadata as ClaudeFileMetadata,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
|
|
@ -761,46 +614,6 @@ export class ClaudeMemoryTool {
|
|||
: 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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
/**
|
||||
* Client for the Supermemory Conversations API
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
import type { AddResponse } from "supermemory"
|
||||
import { createSupermemoryClient } from "./shared/context"
|
||||
|
||||
export interface ConversationMessage {
|
||||
role: "user" | "assistant" | "system" | "tool"
|
||||
|
|
@ -36,7 +31,7 @@ const encodeBase64 = (bytes: Uint8Array): string => {
|
|||
return encoded
|
||||
}
|
||||
|
||||
/** Normalize supported SDK image representations for `/v4/conversations`. */
|
||||
/** Normalize supported SDK image representations to a URL or data URL. */
|
||||
export const toConversationImageUrl = (
|
||||
value: unknown,
|
||||
mediaType = "image/jpeg",
|
||||
|
|
@ -73,75 +68,73 @@ export interface ToolCall {
|
|||
}
|
||||
|
||||
export interface AddConversationParams {
|
||||
conversationId: string
|
||||
id: string
|
||||
messages: ConversationMessage[]
|
||||
containerTags?: string[]
|
||||
namespace: string
|
||||
metadata?: Record<string, string | number | boolean>
|
||||
supportingContext?: string
|
||||
apiKey: string
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
export interface AddConversationResponse {
|
||||
id: string
|
||||
conversationId: string
|
||||
status: string
|
||||
const CONVERSATION_REQUEST_TIMEOUT_SECONDS = 30
|
||||
|
||||
const ROLE_LABELS: Record<ConversationMessage["role"], 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}]`
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a conversation to Supermemory using the /v4/conversations endpoint
|
||||
*
|
||||
* This endpoint supports:
|
||||
* - Structured messages with roles (user, assistant, system, tool)
|
||||
* - Multi-modal content (text, images)
|
||||
* - Tool calls and responses
|
||||
*
|
||||
* @param params - Configuration for adding the conversation
|
||||
* @returns Promise resolving to the conversation response
|
||||
* @throws Error if the API request fails
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const response = await addConversation({
|
||||
* conversationId: "conv-123",
|
||||
* messages: [
|
||||
* { role: "user", content: "Hello!" },
|
||||
* { role: "assistant", content: "Hi there!" }
|
||||
* ],
|
||||
* containerTags: ["user-456"],
|
||||
* apiKey: process.env.SUPERMEMORY_API_KEY,
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function formatConversationText(
|
||||
messages: ConversationMessage[],
|
||||
): string {
|
||||
return messages
|
||||
.map((message) => {
|
||||
const content =
|
||||
typeof message.content === "string"
|
||||
? message.content
|
||||
: message.content.map(formatContentPart).join(" ")
|
||||
const toolCalls = (message.tool_calls ?? [])
|
||||
.map(
|
||||
(call) =>
|
||||
`[tool call: ${call.function.name}(${call.function.arguments})]`,
|
||||
)
|
||||
.join(" ")
|
||||
const label = message.name
|
||||
? `${ROLE_LABELS[message.role]} (${message.name})`
|
||||
: ROLE_LABELS[message.role]
|
||||
return `${label}: ${[content, toolCalls].filter(Boolean).join(" ")}`
|
||||
})
|
||||
.join("\n\n")
|
||||
}
|
||||
|
||||
// Same id → same document, so one conversation stays one document.
|
||||
export async function addConversation(
|
||||
params: AddConversationParams,
|
||||
): Promise<AddConversationResponse> {
|
||||
const baseUrl = params.baseUrl || "https://api.supermemory.ai"
|
||||
const url = `${baseUrl}/v4/conversations`
|
||||
|
||||
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),
|
||||
): Promise<AddResponse> {
|
||||
const client = createSupermemoryClient({
|
||||
apiKey: params.apiKey,
|
||||
baseUrl: params.baseUrl,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "Unknown error")
|
||||
throw new Error(
|
||||
`Failed to add conversation: ${response.status} ${response.statusText}. ${errorText}`,
|
||||
)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
return await client.add(
|
||||
params.namespace,
|
||||
{
|
||||
content: formatConversationText(params.messages),
|
||||
id: params.id,
|
||||
dreaming: "instant",
|
||||
...(params.metadata && { metadata: params.metadata }),
|
||||
...(params.supportingContext && {
|
||||
supportingContext: params.supportingContext,
|
||||
}),
|
||||
},
|
||||
{ timeoutInSeconds: CONVERSATION_REQUEST_TIMEOUT_SECONDS, maxRetries: 0 },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@ export {
|
|||
TOOL_DESCRIPTIONS,
|
||||
PARAMETER_DESCRIPTIONS,
|
||||
DEFAULT_VALUES,
|
||||
getContainerTags,
|
||||
getNamespace,
|
||||
} from "./tools-shared"
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ import type {
|
|||
* Internal context shared between input and output processors.
|
||||
*/
|
||||
interface ProcessorContext {
|
||||
containerTag: string
|
||||
customId: string
|
||||
namespace: string
|
||||
id: string
|
||||
apiKey: string
|
||||
baseUrl: string
|
||||
mode: MemoryMode
|
||||
|
|
@ -65,8 +65,8 @@ function createProcessorContext(
|
|||
const logger = createLogger(options.verbose ?? false)
|
||||
|
||||
return {
|
||||
containerTag: options.containerTag,
|
||||
customId: options.customId,
|
||||
namespace: options.namespace,
|
||||
id: options.id,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
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.
|
||||
*/
|
||||
function getEffectiveCustomId(
|
||||
function getEffectiveId(
|
||||
ctx: ProcessorContext,
|
||||
requestContext?: RequestContext,
|
||||
): string {
|
||||
// Per-request thread ID takes precedence over construction-time customId
|
||||
// Per-request thread ID takes precedence over construction-time id
|
||||
if (requestContext) {
|
||||
const threadId = requestContext.get(MASTRA_THREAD_ID_KEY) as
|
||||
| string
|
||||
|
|
@ -94,8 +94,8 @@ function getEffectiveCustomId(
|
|||
return threadId
|
||||
}
|
||||
}
|
||||
// Fall back to construction-time customId
|
||||
return ctx.customId
|
||||
// Fall back to construction-time id
|
||||
return ctx.id
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,8 +117,8 @@ function getEffectiveCustomId(
|
|||
* model: openai("gpt-4o"),
|
||||
* inputProcessors: [
|
||||
* new SupermemoryInputProcessor({
|
||||
* containerTag: "user-123",
|
||||
* customId: "conv-456",
|
||||
* namespace: "user-123",
|
||||
* id: "conv-456",
|
||||
* mode: "full",
|
||||
* verbose: true,
|
||||
* }),
|
||||
|
|
@ -158,9 +158,9 @@ export class SupermemoryInputProcessor implements Processor {
|
|||
return messageList
|
||||
}
|
||||
|
||||
const effectiveThreadId = getEffectiveCustomId(this.ctx, requestContext)
|
||||
const effectiveThreadId = getEffectiveId(this.ctx, requestContext)
|
||||
const turnKey = MemoryCache.makeTurnKey(
|
||||
this.ctx.containerTag,
|
||||
this.ctx.namespace,
|
||||
effectiveThreadId,
|
||||
this.ctx.mode,
|
||||
queryText || "",
|
||||
|
|
@ -174,13 +174,13 @@ export class SupermemoryInputProcessor implements Processor {
|
|||
}
|
||||
|
||||
this.ctx.logger.info("Starting memory search", {
|
||||
containerTag: this.ctx.containerTag,
|
||||
namespace: this.ctx.namespace,
|
||||
threadId: effectiveThreadId,
|
||||
mode: this.ctx.mode,
|
||||
})
|
||||
|
||||
const memories = await buildMemoriesText({
|
||||
containerTag: this.ctx.containerTag,
|
||||
namespace: this.ctx.namespace,
|
||||
queryText: queryText || "",
|
||||
mode: this.ctx.mode,
|
||||
baseUrl: this.ctx.baseUrl,
|
||||
|
|
@ -212,7 +212,7 @@ export class SupermemoryInputProcessor implements Processor {
|
|||
*
|
||||
* This processor runs once after generation completes (processOutputResult).
|
||||
* 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
|
||||
* ```typescript
|
||||
|
|
@ -226,8 +226,8 @@ export class SupermemoryInputProcessor implements Processor {
|
|||
* model: openai("gpt-4o"),
|
||||
* outputProcessors: [
|
||||
* new SupermemoryOutputProcessor({
|
||||
* containerTag: "user-123",
|
||||
* customId: "conv-456",
|
||||
* namespace: "user-123",
|
||||
* id: "conv-456",
|
||||
* addMemory: "always",
|
||||
* }),
|
||||
* ],
|
||||
|
|
@ -253,7 +253,7 @@ export class SupermemoryOutputProcessor implements Processor {
|
|||
return messages
|
||||
}
|
||||
|
||||
const effectiveCustomId = getEffectiveCustomId(this.ctx, requestContext)
|
||||
const effectiveId = getEffectiveId(this.ctx, requestContext)
|
||||
|
||||
try {
|
||||
const conversationMessages = this.convertToConversationMessages(messages)
|
||||
|
|
@ -264,16 +264,16 @@ export class SupermemoryOutputProcessor implements Processor {
|
|||
}
|
||||
|
||||
const response = await addConversation({
|
||||
conversationId: effectiveCustomId,
|
||||
id: effectiveId,
|
||||
messages: conversationMessages,
|
||||
containerTags: [this.ctx.containerTag],
|
||||
namespace: this.ctx.namespace,
|
||||
apiKey: this.ctx.apiKey,
|
||||
baseUrl: this.ctx.baseUrl,
|
||||
})
|
||||
|
||||
this.ctx.logger.info("Conversation saved successfully", {
|
||||
containerTag: this.ctx.containerTag,
|
||||
customId: effectiveCustomId,
|
||||
namespace: this.ctx.namespace,
|
||||
id: effectiveId,
|
||||
messageCount: conversationMessages.length,
|
||||
responseId: response.id,
|
||||
})
|
||||
|
|
@ -330,7 +330,7 @@ export class SupermemoryOutputProcessor implements Processor {
|
|||
/**
|
||||
* 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
|
||||
*
|
||||
* @example
|
||||
|
|
@ -340,8 +340,8 @@ export class SupermemoryOutputProcessor implements Processor {
|
|||
* import { openai } from "@ai-sdk/openai"
|
||||
*
|
||||
* const processor = createSupermemoryProcessor({
|
||||
* containerTag: "user-123",
|
||||
* customId: "conv-456",
|
||||
* namespace: "user-123",
|
||||
* id: "conv-456",
|
||||
* mode: "full",
|
||||
* verbose: true,
|
||||
* })
|
||||
|
|
@ -363,7 +363,7 @@ export function createSupermemoryProcessor(
|
|||
/**
|
||||
* 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
|
||||
*
|
||||
* @example
|
||||
|
|
@ -373,8 +373,8 @@ export function createSupermemoryProcessor(
|
|||
* import { openai } from "@ai-sdk/openai"
|
||||
*
|
||||
* const processor = createSupermemoryOutputProcessor({
|
||||
* containerTag: "user-123",
|
||||
* customId: "conv-456",
|
||||
* namespace: "user-123",
|
||||
* id: "conv-456",
|
||||
* addMemory: "always",
|
||||
* })
|
||||
*
|
||||
|
|
@ -408,8 +408,8 @@ export function createSupermemoryOutputProcessor(
|
|||
* import { openai } from "@ai-sdk/openai"
|
||||
*
|
||||
* const { input, output } = createSupermemoryProcessors({
|
||||
* containerTag: "user-123",
|
||||
* customId: "conv-456",
|
||||
* namespace: "user-123",
|
||||
* id: "conv-456",
|
||||
* mode: "full",
|
||||
* addMemory: "always",
|
||||
* })
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ export type { RequestContext } from "@mastra/core/request-context"
|
|||
* Configuration options for the Supermemory Mastra processor.
|
||||
*/
|
||||
export interface SupermemoryMastraOptions {
|
||||
/** Container tag/user ID for scoping memories. Required. */
|
||||
containerTag: string
|
||||
/** Custom ID to group messages into a single document for contextual memory generation. Required. */
|
||||
customId: string
|
||||
/** Namespace (e.g. user ID) for scoping memories. Required. */
|
||||
namespace: string
|
||||
/** ID that groups messages into a single document for contextual memory generation. Required. */
|
||||
id: string
|
||||
/** Supermemory API key (falls back to SUPERMEMORY_API_KEY env var) */
|
||||
apiKey?: string
|
||||
/** Custom Supermemory API base URL */
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ interface AgentConfig {
|
|||
* - Output processor: Saves conversations after responses (when addMemory is "always")
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @example
|
||||
|
|
@ -54,8 +54,8 @@ interface AgentConfig {
|
|||
* instructions: "You are a helpful assistant.",
|
||||
* },
|
||||
* {
|
||||
* containerTag: "user-123",
|
||||
* customId: "conv-456",
|
||||
* namespace: "user-123",
|
||||
* id: "conv-456",
|
||||
* mode: "full",
|
||||
* addMemory: "always",
|
||||
* }
|
||||
|
|
@ -74,11 +74,11 @@ export function withSupermemory<T extends AgentConfig>(
|
|||
if (
|
||||
typeof options !== "object" ||
|
||||
options === null ||
|
||||
!options.containerTag ||
|
||||
!options.customId
|
||||
!options.namespace ||
|
||||
!options.id
|
||||
) {
|
||||
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.",
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ import {
|
|||
*
|
||||
* @param openaiClient - The OpenAI client to wrap with SuperMemory 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.customId - Required. Custom ID to group messages into a single document for contextual memory generation
|
||||
* @param options.namespace - Required. The namespace for memory search (e.g., user ID, project ID)
|
||||
* @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.mode - Optional mode for memory search: "profile" (default), "query", or "full"
|
||||
* @param options.addMemory - Optional mode for memory addition: "always" (default), "never"
|
||||
|
|
@ -35,8 +35,8 @@ import {
|
|||
* apiKey: process.env.OPENAI_API_KEY,
|
||||
* })
|
||||
* const openaiWithSupermemory = withSupermemory(openai, {
|
||||
* containerTag: "user-123",
|
||||
* customId: "conversation-456",
|
||||
* namespace: "user-123",
|
||||
* id: "conversation-456",
|
||||
* mode: "full",
|
||||
* addMemory: "always"
|
||||
* })
|
||||
|
|
@ -70,26 +70,26 @@ export function withSupermemory(
|
|||
)
|
||||
}
|
||||
|
||||
if (!options.containerTag) {
|
||||
if (!options.namespace) {
|
||||
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(
|
||||
"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 mode = options.mode ?? "profile"
|
||||
const addMemory = options.addMemory ?? "always"
|
||||
|
||||
const openaiWithSupermemory = createOpenAIMiddleware(
|
||||
openaiClient,
|
||||
containerTag,
|
||||
namespace,
|
||||
{
|
||||
...options,
|
||||
verbose,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type OpenAI from "openai"
|
||||
import { APIPromise } from "openai/core"
|
||||
import Supermemory from "supermemory"
|
||||
import { Supermemory } from "supermemory"
|
||||
import {
|
||||
addConversation,
|
||||
type ContentPart as ConversationContentPart,
|
||||
|
|
@ -10,7 +10,10 @@ import {
|
|||
import {
|
||||
replaceMemoryContext,
|
||||
stripMemoryContext,
|
||||
supermemoryProfileSearch as sharedProfileSearch,
|
||||
wrapMemoryContext,
|
||||
type ProfileSearchResult,
|
||||
type ProfileStructure,
|
||||
} from "../shared"
|
||||
import { deduplicateMemoriesForMode } from "../tools-shared"
|
||||
import { createLogger, type Logger } from "../vercel/logger"
|
||||
|
|
@ -41,10 +44,10 @@ const deferAPIPromise = <T>(
|
|||
}
|
||||
|
||||
export interface OpenAIMiddlewareOptions {
|
||||
/** Container tag/identifier for memory search (e.g., user ID, project ID). Required. */
|
||||
containerTag: string
|
||||
/** Custom ID to group messages into a single document. Required. */
|
||||
customId: string
|
||||
/** Namespace for memory search (e.g., user ID, project ID). Required. */
|
||||
namespace: string
|
||||
/** ID that groups messages into a single document. Required. */
|
||||
id: string
|
||||
verbose?: boolean
|
||||
mode?: "profile" | "query" | "full"
|
||||
addMemory?: "always" | "never"
|
||||
|
|
@ -53,28 +56,6 @@ export interface OpenAIMiddlewareOptions {
|
|||
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 => {
|
||||
if (typeof content === "string") return content.trim()
|
||||
if (!Array.isArray(content)) return ""
|
||||
|
|
@ -313,7 +294,7 @@ const stripResponsesInputMemoryContexts = <T>(input: T): T => {
|
|||
}
|
||||
|
||||
const getSearchResultMemories = (
|
||||
results: SupermemoryProfileSearchResult[] | undefined,
|
||||
results: ProfileSearchResult[] | undefined,
|
||||
): string[] => {
|
||||
return (results ?? []).flatMap((result) => {
|
||||
for (const value of [result.memory, result.chunk]) {
|
||||
|
|
@ -417,72 +398,21 @@ const getLastUserMessage = (
|
|||
return extractTextContent(lastUserMessage?.content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for memories using the SuperMemory profile API.
|
||||
*
|
||||
* 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,
|
||||
const supermemoryProfileSearch = (
|
||||
namespace: string,
|
||||
queryText: string,
|
||||
apiKey: string,
|
||||
baseUrl: string,
|
||||
): Promise<SupermemoryProfileSearch> => {
|
||||
const payload = queryText
|
||||
? JSON.stringify({
|
||||
q: queryText,
|
||||
containerTag: containerTag,
|
||||
include: ["static", "dynamic"],
|
||||
})
|
||||
: JSON.stringify({
|
||||
containerTag: containerTag,
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
mode: "profile" | "query" | "full",
|
||||
): Promise<ProfileStructure> =>
|
||||
sharedProfileSearch(
|
||||
namespace,
|
||||
queryText,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
AbortSignal.timeout(PROFILE_REQUEST_TIMEOUT_MS),
|
||||
mode !== "query",
|
||||
)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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 mode - Memory search mode: "profile" (all memories), "query" (search-based), or "full" (both)
|
||||
* @param apiKey - The Supermemory API key used to authenticate the request
|
||||
|
|
@ -518,7 +448,7 @@ const supermemoryProfileSearch = async (
|
|||
*/
|
||||
const addSystemPrompt = async (
|
||||
messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
||||
containerTag: string,
|
||||
namespace: string,
|
||||
logger: Logger,
|
||||
mode: "profile" | "query" | "full",
|
||||
apiKey: string,
|
||||
|
|
@ -529,17 +459,18 @@ const addSystemPrompt = async (
|
|||
const queryText = mode !== "profile" ? getLastUserMessage(messages) : ""
|
||||
|
||||
const memoriesResponse = await supermemoryProfileSearch(
|
||||
containerTag,
|
||||
namespace,
|
||||
queryText,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
mode,
|
||||
)
|
||||
|
||||
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
|
||||
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
|
||||
|
||||
logger.info("Memory search completed for chat API", {
|
||||
containerTag,
|
||||
namespace,
|
||||
memoryCountStatic,
|
||||
memoryCountDynamic,
|
||||
queryText:
|
||||
|
|
@ -644,80 +575,46 @@ const getConversationContent = (
|
|||
.join("\n\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
// With a conversation id and messages, saves the whole conversation as one document.
|
||||
const addMemoryTool = async (
|
||||
client: Supermemory,
|
||||
containerTag: string,
|
||||
namespace: string,
|
||||
content: string,
|
||||
customId: string | undefined,
|
||||
id: string | undefined,
|
||||
logger: Logger,
|
||||
conversationMessages?: ConversationMessage[],
|
||||
apiKey?: string,
|
||||
baseUrl?: string,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (customId && conversationMessages && apiKey) {
|
||||
const conversationId = customId.replace("conversation:", "")
|
||||
if (id && conversationMessages && apiKey) {
|
||||
const conversationDocumentId = id.replace("conversation:", "")
|
||||
|
||||
const response = await addConversation({
|
||||
conversationId,
|
||||
id: conversationDocumentId,
|
||||
messages: conversationMessages,
|
||||
containerTags: [containerTag],
|
||||
namespace,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
})
|
||||
|
||||
logger.info("Conversation saved successfully via /v4/conversations", {
|
||||
containerTag,
|
||||
customId,
|
||||
logger.info("Conversation saved successfully", {
|
||||
namespace,
|
||||
id,
|
||||
messageCount: conversationMessages.length,
|
||||
responseId: response.id,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to old behavior for non-conversation memories
|
||||
const response = await client.add({
|
||||
const response = await client.add(namespace, {
|
||||
content,
|
||||
containerTags: [containerTag],
|
||||
customId,
|
||||
...(id && { id }),
|
||||
})
|
||||
|
||||
logger.info("Memory saved successfully", {
|
||||
containerTag,
|
||||
customId,
|
||||
namespace,
|
||||
id,
|
||||
contentLength: content.length,
|
||||
memoryId: response.id,
|
||||
})
|
||||
|
|
@ -735,9 +632,9 @@ const addMemoryTool = async (
|
|||
* into OpenAI chat completions and optionally saves new memories. The middleware
|
||||
* 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.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.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")
|
||||
|
|
@ -748,7 +645,7 @@ const addMemoryTool = async (
|
|||
* @example
|
||||
* ```typescript
|
||||
* const openaiWithSupermemory = createOpenAIMiddleware(openai, "user-123", {
|
||||
* customId: "conversation-456",
|
||||
* id: "conversation-456",
|
||||
* mode: "full",
|
||||
* addMemory: "always",
|
||||
* verbose: true
|
||||
|
|
@ -758,7 +655,7 @@ const addMemoryTool = async (
|
|||
*/
|
||||
export function createOpenAIMiddleware(
|
||||
openaiClient: OpenAI,
|
||||
containerTag: string,
|
||||
namespace: string,
|
||||
options?: OpenAIMiddlewareOptions,
|
||||
) {
|
||||
const logger = createLogger(options?.verbose ?? false)
|
||||
|
|
@ -772,10 +669,10 @@ export function createOpenAIMiddleware(
|
|||
const baseUrl = normalizeBaseUrl(options?.baseUrl)
|
||||
const client = new Supermemory({
|
||||
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 addMemory = options?.addMemory ?? "always"
|
||||
|
||||
|
|
@ -789,7 +686,7 @@ export function createOpenAIMiddleware(
|
|||
* and Responses APIs, reducing code duplication.
|
||||
*
|
||||
* @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 mode - Memory search mode
|
||||
* @param context - API context for logging differentiation
|
||||
|
|
@ -797,23 +694,24 @@ export function createOpenAIMiddleware(
|
|||
*/
|
||||
const searchAndFormatMemories = async (
|
||||
queryText: string,
|
||||
containerTag: string,
|
||||
namespace: string,
|
||||
logger: Logger,
|
||||
mode: "profile" | "query" | "full",
|
||||
context: "chat" | "responses",
|
||||
) => {
|
||||
const memoriesResponse = await supermemoryProfileSearch(
|
||||
containerTag,
|
||||
namespace,
|
||||
queryText,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
mode,
|
||||
)
|
||||
|
||||
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
|
||||
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
|
||||
|
||||
logger.info(`Memory search completed for ${context} API`, {
|
||||
containerTag,
|
||||
namespace,
|
||||
memoryCountStatic,
|
||||
memoryCountDynamic,
|
||||
queryText:
|
||||
|
|
@ -889,17 +787,17 @@ export function createOpenAIMiddleware(
|
|||
convertResponsesConversationMessages(cleanedInput)
|
||||
const shouldPersist =
|
||||
addMemory === "always" &&
|
||||
(customId
|
||||
(id
|
||||
? hasPersistableUserConversationMessage(conversationMessages)
|
||||
: Boolean(input.trim()))
|
||||
const memoryCustomId = customId ? `conversation:${customId}` : undefined
|
||||
const documentId = id ? `conversation:${id}` : undefined
|
||||
|
||||
const persistResponsesInput = () =>
|
||||
addMemoryTool(
|
||||
client,
|
||||
containerTag,
|
||||
namespace,
|
||||
input,
|
||||
memoryCustomId,
|
||||
documentId,
|
||||
logger,
|
||||
conversationMessages,
|
||||
apiKey,
|
||||
|
|
@ -928,8 +826,8 @@ export function createOpenAIMiddleware(
|
|||
}
|
||||
|
||||
logger.info("Starting memory search for Responses API", {
|
||||
containerTag,
|
||||
customId,
|
||||
namespace,
|
||||
id,
|
||||
mode,
|
||||
})
|
||||
|
||||
|
|
@ -939,13 +837,7 @@ export function createOpenAIMiddleware(
|
|||
|
||||
const queryText = mode !== "profile" ? input : ""
|
||||
operations.push(
|
||||
searchAndFormatMemories(
|
||||
queryText,
|
||||
containerTag,
|
||||
logger,
|
||||
mode,
|
||||
"responses",
|
||||
),
|
||||
searchAndFormatMemories(queryText, namespace, logger, mode, "responses"),
|
||||
)
|
||||
|
||||
let enhancedInstructions: string
|
||||
|
|
@ -999,21 +891,19 @@ export function createOpenAIMiddleware(
|
|||
)
|
||||
const shouldPersist =
|
||||
addMemory === "always" &&
|
||||
(customId
|
||||
(id
|
||||
? hasPersistableUserConversationMessage(conversationMessages)
|
||||
: Boolean(userMessage.trim()))
|
||||
const memoryContent = customId
|
||||
? getConversationContent(messages)
|
||||
: userMessage
|
||||
const memoryCustomId = customId ? `conversation:${customId}` : undefined
|
||||
const memoryContent = id ? getConversationContent(messages) : userMessage
|
||||
const documentId = id ? `conversation:${id}` : undefined
|
||||
|
||||
if (mode !== "profile" && !userMessage) {
|
||||
if (shouldPersist) {
|
||||
await addMemoryTool(
|
||||
client,
|
||||
containerTag,
|
||||
namespace,
|
||||
memoryContent,
|
||||
memoryCustomId,
|
||||
documentId,
|
||||
logger,
|
||||
conversationMessages,
|
||||
apiKey,
|
||||
|
|
@ -1034,8 +924,8 @@ export function createOpenAIMiddleware(
|
|||
}
|
||||
|
||||
logger.info("Starting memory search", {
|
||||
containerTag,
|
||||
customId,
|
||||
namespace,
|
||||
id,
|
||||
mode,
|
||||
})
|
||||
|
||||
|
|
@ -1045,9 +935,9 @@ export function createOpenAIMiddleware(
|
|||
operations.push(
|
||||
addMemoryTool(
|
||||
client,
|
||||
containerTag,
|
||||
namespace,
|
||||
memoryContent,
|
||||
memoryCustomId,
|
||||
documentId,
|
||||
logger,
|
||||
conversationMessages,
|
||||
apiKey,
|
||||
|
|
@ -1057,7 +947,7 @@ export function createOpenAIMiddleware(
|
|||
}
|
||||
|
||||
operations.push(
|
||||
addSystemPrompt(messages, containerTag, logger, mode, apiKey, baseUrl),
|
||||
addSystemPrompt(messages, namespace, logger, mode, apiKey, baseUrl),
|
||||
)
|
||||
|
||||
let enhancedMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[]
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
import type OpenAI from "openai"
|
||||
import Supermemory from "supermemory"
|
||||
import type { Supermemory } from "supermemory"
|
||||
import {
|
||||
CLIENT_OPTIONS,
|
||||
DEFAULT_VALUES,
|
||||
PARAMETER_DESCRIPTIONS,
|
||||
SEARCH_LIMIT_BOUNDS,
|
||||
TOOL_DESCRIPTIONS,
|
||||
clampSearchLimit,
|
||||
deleteDocumentByIdentifier,
|
||||
getContainerTags,
|
||||
createToolsClient,
|
||||
deleteDocument,
|
||||
forgetMemory,
|
||||
getNamespace,
|
||||
getProfileWithSearch,
|
||||
} from "../tools-shared"
|
||||
import { forgetMemoryRequest } from "../shared/forget-memory"
|
||||
import type { SupermemoryToolsConfig } from "../types"
|
||||
|
||||
/**
|
||||
* Result types for memory operations
|
||||
*/
|
||||
export interface MemorySearchResult {
|
||||
success: boolean
|
||||
results?: Awaited<ReturnType<Supermemory["search"]>>["results"]
|
||||
|
|
@ -31,20 +29,15 @@ export interface MemoryAddResult {
|
|||
|
||||
export interface ProfileResult {
|
||||
success: boolean
|
||||
profile?: {
|
||||
static: string[]
|
||||
dynamic: string[]
|
||||
}
|
||||
searchResults?: Awaited<ReturnType<Supermemory["profile"]>>["searchResults"]
|
||||
profile?: Awaited<ReturnType<Supermemory["profile"]>>["profile"]
|
||||
searchResults?: Awaited<ReturnType<Supermemory["search"]>>["results"]
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface DocumentListResult {
|
||||
success: boolean
|
||||
documents?: Awaited<ReturnType<Supermemory["documents"]["list"]>>["memories"]
|
||||
pagination?: Awaited<
|
||||
ReturnType<Supermemory["documents"]["list"]>
|
||||
>["pagination"]
|
||||
documents?: Awaited<ReturnType<Supermemory["list"]>>["documents"]
|
||||
pagination?: Awaited<ReturnType<Supermemory["list"]>>["pagination"]
|
||||
error?: string
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +49,7 @@ export interface DocumentDeleteResult {
|
|||
|
||||
export interface DocumentAddResult {
|
||||
success: boolean
|
||||
document?: Awaited<ReturnType<Supermemory["documents"]["add"]>>
|
||||
document?: Awaited<ReturnType<Supermemory["add"]>>
|
||||
error?: string
|
||||
}
|
||||
|
||||
|
|
@ -66,9 +59,6 @@ export interface MemoryForgetResult {
|
|||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Function schemas for OpenAI function calling
|
||||
*/
|
||||
export const memoryToolSchemas = {
|
||||
searchMemories: {
|
||||
name: "searchMemories",
|
||||
|
|
@ -118,10 +108,6 @@ export const memoryToolSchemas = {
|
|||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
containerTag: {
|
||||
type: "string",
|
||||
description: PARAMETER_DESCRIPTIONS.containerTag,
|
||||
},
|
||||
query: {
|
||||
type: "string",
|
||||
description: PARAMETER_DESCRIPTIONS.query,
|
||||
|
|
@ -137,10 +123,6 @@ export const memoryToolSchemas = {
|
|||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
containerTag: {
|
||||
type: "string",
|
||||
description: PARAMETER_DESCRIPTIONS.containerTag,
|
||||
},
|
||||
limit: {
|
||||
type: "number",
|
||||
description: PARAMETER_DESCRIPTIONS.limit,
|
||||
|
|
@ -165,10 +147,6 @@ export const memoryToolSchemas = {
|
|||
type: "string",
|
||||
description: PARAMETER_DESCRIPTIONS.documentId,
|
||||
},
|
||||
containerTag: {
|
||||
type: "string",
|
||||
description: PARAMETER_DESCRIPTIONS.documentContainerTag,
|
||||
},
|
||||
},
|
||||
required: ["documentId"],
|
||||
},
|
||||
|
|
@ -203,10 +181,6 @@ export const memoryToolSchemas = {
|
|||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
containerTag: {
|
||||
type: "string",
|
||||
description: PARAMETER_DESCRIPTIONS.containerTag,
|
||||
},
|
||||
memoryId: {
|
||||
type: "string",
|
||||
description: PARAMETER_DESCRIPTIONS.memoryId,
|
||||
|
|
@ -215,39 +189,31 @@ export const memoryToolSchemas = {
|
|||
type: "string",
|
||||
description: PARAMETER_DESCRIPTIONS.memoryContent,
|
||||
},
|
||||
reason: {
|
||||
type: "string",
|
||||
description: PARAMETER_DESCRIPTIONS.reason,
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
},
|
||||
} satisfies OpenAI.FunctionDefinition,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Create a Supermemory client with configuration
|
||||
*/
|
||||
function createClient(apiKey: string, config?: SupermemoryToolsConfig) {
|
||||
const client = new Supermemory({
|
||||
apiKey,
|
||||
...CLIENT_OPTIONS,
|
||||
...(config?.baseUrl && { baseURL: config.baseUrl }),
|
||||
})
|
||||
|
||||
const containerTags = getContainerTags(config)
|
||||
|
||||
return { client, containerTags }
|
||||
return {
|
||||
client: createToolsClient(apiKey, config),
|
||||
namespace: getNamespace(config),
|
||||
}
|
||||
}
|
||||
|
||||
function toolError(error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search memories function
|
||||
*/
|
||||
export function createSearchMemoriesFunction(
|
||||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const { client, containerTags } = createClient(apiKey, config)
|
||||
const { client, namespace } = createClient(apiKey, config)
|
||||
|
||||
return async function searchMemories({
|
||||
informationToGet,
|
||||
|
|
@ -258,9 +224,8 @@ export function createSearchMemoriesFunction(
|
|||
limit?: number
|
||||
}): Promise<MemorySearchResult> {
|
||||
try {
|
||||
const response = await client.search({
|
||||
q: informationToGet,
|
||||
containerTag: containerTags[0],
|
||||
const response = await client.search(namespace, {
|
||||
query: informationToGet,
|
||||
limit: clampSearchLimit(limit),
|
||||
threshold: DEFAULT_VALUES.searchThreshold,
|
||||
searchMode: "hybrid",
|
||||
|
|
@ -272,22 +237,16 @@ export function createSearchMemoriesFunction(
|
|||
count: response.results?.length || 0,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add memory function
|
||||
*/
|
||||
export function createAddMemoryFunction(
|
||||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const { client, containerTags } = createClient(apiKey, config)
|
||||
const { client, namespace } = createClient(apiKey, config)
|
||||
|
||||
return async function addMemory({
|
||||
memory,
|
||||
|
|
@ -295,12 +254,9 @@ export function createAddMemoryFunction(
|
|||
memory: string
|
||||
}): Promise<MemoryAddResult> {
|
||||
try {
|
||||
const metadata: Record<string, string | number | boolean> = {}
|
||||
|
||||
const response = await client.add({
|
||||
const response = await client.add(namespace, {
|
||||
content: memory,
|
||||
containerTags,
|
||||
...(Object.keys(metadata).length > 0 && { metadata }),
|
||||
dreaming: "instant",
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
@ -308,138 +264,92 @@ export function createAddMemoryFunction(
|
|||
memory: response,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get profile function
|
||||
*/
|
||||
export function createGetProfileFunction(
|
||||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const { client, containerTags } = createClient(apiKey, config)
|
||||
const { client, namespace } = createClient(apiKey, config)
|
||||
|
||||
return async function getProfile({
|
||||
containerTag,
|
||||
query,
|
||||
}: {
|
||||
containerTag?: string
|
||||
query?: string
|
||||
}): Promise<ProfileResult> {
|
||||
try {
|
||||
const tag = containerTag || containerTags[0]
|
||||
|
||||
const response = await client.profile({
|
||||
containerTag: tag,
|
||||
...(query && { q: query }),
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
profile: response.profile,
|
||||
searchResults: response.searchResults,
|
||||
...(await getProfileWithSearch(client, namespace, query)),
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List documents function
|
||||
*/
|
||||
export function createDocumentListFunction(
|
||||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const { client, containerTags } = createClient(apiKey, config)
|
||||
const { client, namespace } = createClient(apiKey, config)
|
||||
|
||||
return async function documentList({
|
||||
containerTag,
|
||||
limit,
|
||||
page,
|
||||
}: {
|
||||
containerTag?: string
|
||||
limit?: number
|
||||
page?: number
|
||||
}): Promise<DocumentListResult> {
|
||||
try {
|
||||
const scopeTags: [string, ...string[]] = containerTag
|
||||
? [containerTag]
|
||||
: containerTags
|
||||
|
||||
const response = await client.documents.list({
|
||||
containerTags: scopeTags,
|
||||
const response = await client.list(namespace, "documents", {
|
||||
limit: limit || DEFAULT_VALUES.limit,
|
||||
...(page !== undefined && { page }),
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
documents: response.memories,
|
||||
documents: response.documents,
|
||||
pagination: response.pagination,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete document function
|
||||
*/
|
||||
export function createDocumentDeleteFunction(
|
||||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const { client, containerTags } = createClient(apiKey, config)
|
||||
const { client, namespace } = createClient(apiKey, config)
|
||||
|
||||
return async function documentDelete({
|
||||
documentId,
|
||||
containerTag,
|
||||
}: {
|
||||
documentId: string
|
||||
containerTag?: string
|
||||
}): Promise<DocumentDeleteResult> {
|
||||
try {
|
||||
const scopeTags: [string, ...string[]] = containerTag
|
||||
? [containerTag]
|
||||
: containerTags
|
||||
await deleteDocumentByIdentifier(client, documentId, scopeTags)
|
||||
await deleteDocument(client, namespace, documentId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Document ${documentId} deleted successfully`,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add document function
|
||||
*/
|
||||
export function createDocumentAddFunction(
|
||||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const { client, containerTags } = createClient(apiKey, config)
|
||||
const { client, namespace } = createClient(apiKey, config)
|
||||
|
||||
return async function documentAdd({
|
||||
content,
|
||||
|
|
@ -455,9 +365,8 @@ export function createDocumentAddFunction(
|
|||
if (title) metadata.title = title
|
||||
if (description) metadata.description = description
|
||||
|
||||
const response = await client.documents.add({
|
||||
const response = await client.add(namespace, {
|
||||
content,
|
||||
containerTags,
|
||||
...(Object.keys(metadata).length > 0 && { metadata }),
|
||||
})
|
||||
|
||||
|
|
@ -466,33 +375,23 @@ export function createDocumentAddFunction(
|
|||
document: response,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget memory function
|
||||
*/
|
||||
export function createMemoryForgetFunction(
|
||||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const containerTags = getContainerTags(config)
|
||||
const { client, namespace } = createClient(apiKey, config)
|
||||
|
||||
return async function memoryForget({
|
||||
containerTag,
|
||||
memoryId,
|
||||
memoryContent,
|
||||
reason,
|
||||
}: {
|
||||
containerTag?: string
|
||||
memoryId?: string
|
||||
memoryContent?: string
|
||||
reason?: string
|
||||
}): Promise<MemoryForgetResult> {
|
||||
try {
|
||||
if (!memoryId && !memoryContent) {
|
||||
|
|
@ -502,28 +401,17 @@ export function createMemoryForgetFunction(
|
|||
}
|
||||
}
|
||||
|
||||
const tag = containerTag || containerTags[0]
|
||||
|
||||
await forgetMemoryRequest(
|
||||
apiKey,
|
||||
{
|
||||
containerTag: tag as string,
|
||||
...(memoryId && { id: memoryId }),
|
||||
...(memoryContent && { content: memoryContent }),
|
||||
...(reason && { reason }),
|
||||
},
|
||||
config?.baseUrl,
|
||||
)
|
||||
await forgetMemory(client, namespace, {
|
||||
...(memoryId && { id: memoryId }),
|
||||
...(memoryContent && { content: memoryContent }),
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Memory forgotten successfully",
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
return toolError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,20 +12,20 @@ export class MemoryCache<T = string> {
|
|||
* Generates a cache key for the current turn based on context parameters.
|
||||
* 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 mode - The memory retrieval mode
|
||||
* @param message - The user message content
|
||||
* @returns A unique cache key for this turn
|
||||
*/
|
||||
static makeTurnKey(
|
||||
containerTag: string,
|
||||
namespace: string,
|
||||
threadId: string | undefined,
|
||||
mode: MemoryMode,
|
||||
message: string,
|
||||
): string {
|
||||
const normalizedMessage = message.trim().replace(/\s+/g, " ")
|
||||
return `${containerTag}:${threadId || ""}:${mode}:${normalizedMessage}`
|
||||
return `${namespace}:${threadId || ""}:${mode}:${normalizedMessage}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Supermemory from "supermemory"
|
||||
import { Supermemory } from "supermemory"
|
||||
|
||||
/**
|
||||
* Normalizes a base URL by removing trailing slashes.
|
||||
|
|
@ -36,7 +36,7 @@ export function createSupermemoryClient(
|
|||
return new Supermemory({
|
||||
apiKey: options.apiKey,
|
||||
...(normalizedBaseUrl !== "https://api.supermemory.ai"
|
||||
? { baseURL: normalizedBaseUrl }
|
||||
? { baseUrl: normalizedBaseUrl }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ export type {
|
|||
AddMemoryMode,
|
||||
Logger,
|
||||
ProfileStructure,
|
||||
ProfileEntry,
|
||||
ProfileMarkdownData,
|
||||
SupermemoryBaseOptions,
|
||||
} from "./types"
|
||||
|
|
@ -35,6 +36,7 @@ export {
|
|||
|
||||
// Memory client
|
||||
export {
|
||||
PROFILE_SEARCH_THRESHOLD,
|
||||
supermemoryProfileSearch,
|
||||
buildMemoriesText,
|
||||
extractQueryText,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import { buildMemoriesText } from "./memory-client"
|
||||
import { createLogger } from "./logger"
|
||||
import { profileBody, searchBody, v5Router } from "../../test/v5-fetch"
|
||||
|
||||
const API_KEY = "sm_test_key"
|
||||
const BASE_URL = "https://api.supermemory.ai"
|
||||
const CONTAINER_TAG = "user-123"
|
||||
const NAMESPACE = "user-123"
|
||||
|
||||
const logger = createLogger(false)
|
||||
|
||||
/** Stubs `/v4/profile` so the injected prompt can be asserted without network access. */
|
||||
function mockProfileResponse(body: unknown) {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => body,
|
||||
})
|
||||
// Stubs the v5 profile and search routes so the injected prompt can be asserted offline.
|
||||
function mockV5(profile: string[], search: string[]) {
|
||||
const fetchMock = vi.fn(
|
||||
v5Router({
|
||||
profile: () => profileBody(profile),
|
||||
search: () => searchBody(search),
|
||||
}),
|
||||
)
|
||||
vi.stubGlobal("fetch", fetchMock)
|
||||
return fetchMock
|
||||
}
|
||||
|
|
@ -27,16 +30,13 @@ describe("buildMemoriesText", () => {
|
|||
// results against it would drop a fact present in both, leaving the model
|
||||
// with nothing.
|
||||
it("injects a search result that also exists in the profile in query mode", async () => {
|
||||
mockProfileResponse({
|
||||
profile: {
|
||||
static: [{ memory: "User is allergic to peanuts" }],
|
||||
dynamic: [],
|
||||
},
|
||||
searchResults: { results: [{ memory: "User is allergic to peanuts" }] },
|
||||
})
|
||||
const fetchMock = mockV5(
|
||||
["User is allergic to peanuts"],
|
||||
["User is allergic to peanuts"],
|
||||
)
|
||||
|
||||
const memories = await buildMemoriesText({
|
||||
containerTag: CONTAINER_TAG,
|
||||
namespace: NAMESPACE,
|
||||
queryText: "what should I avoid eating?",
|
||||
mode: "query",
|
||||
baseUrl: BASE_URL,
|
||||
|
|
@ -45,24 +45,18 @@ describe("buildMemoriesText", () => {
|
|||
})
|
||||
|
||||
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 () => {
|
||||
mockProfileResponse({
|
||||
profile: {
|
||||
static: [{ memory: "User is allergic to peanuts" }],
|
||||
dynamic: [],
|
||||
},
|
||||
searchResults: {
|
||||
results: [
|
||||
{ memory: "User is allergic to peanuts" },
|
||||
{ memory: "User prefers async/await" },
|
||||
],
|
||||
},
|
||||
})
|
||||
mockV5(
|
||||
["User is allergic to peanuts"],
|
||||
["User is allergic to peanuts", "User prefers async/await"],
|
||||
)
|
||||
|
||||
const memories = await buildMemoriesText({
|
||||
containerTag: CONTAINER_TAG,
|
||||
namespace: NAMESPACE,
|
||||
queryText: "what should I avoid eating?",
|
||||
mode: "full",
|
||||
baseUrl: BASE_URL,
|
||||
|
|
|
|||
|
|
@ -10,63 +10,67 @@ import type {
|
|||
ProfileStructure,
|
||||
PromptTemplate,
|
||||
} from "./types"
|
||||
import { SupermemoryError } from "supermemory"
|
||||
import { createSupermemoryClient } from "./context"
|
||||
import {
|
||||
convertProfileToMarkdown,
|
||||
defaultPromptTemplate,
|
||||
} from "./prompt-builder"
|
||||
|
||||
/**
|
||||
* Fetches profile and search results from the Supermemory API.
|
||||
*
|
||||
* @param containerTag - The container tag/user ID for scoping memories
|
||||
* @param queryText - Optional query text for semantic search
|
||||
* @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)
|
||||
* @returns The profile structure with static, dynamic, and search results
|
||||
*/
|
||||
function describeSupermemoryError(error: unknown): string {
|
||||
if (error instanceof SupermemoryError && error.statusCode !== undefined) {
|
||||
return `${error.statusCode}. ${error.message}`
|
||||
}
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
// Matches the v4 profile search defaults (memories mode, 0.6 threshold).
|
||||
export const PROFILE_SEARCH_THRESHOLD = 0.6
|
||||
|
||||
export const supermemoryProfileSearch = async (
|
||||
containerTag: string,
|
||||
namespace: string,
|
||||
queryText: string,
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
signal?: AbortSignal,
|
||||
includeProfile = true,
|
||||
): Promise<ProfileStructure> => {
|
||||
const payload = queryText
|
||||
? JSON.stringify({
|
||||
q: queryText,
|
||||
containerTag: containerTag,
|
||||
include: ["static", "dynamic"],
|
||||
})
|
||||
: JSON.stringify({
|
||||
containerTag: containerTag,
|
||||
include: ["static", "dynamic"],
|
||||
})
|
||||
const client = createSupermemoryClient({ apiKey, baseUrl })
|
||||
// No retries: this runs before the LLM call, and the raw v4 fetch never retried.
|
||||
const requestOptions = {
|
||||
maxRetries: 0,
|
||||
...(signal && { abortSignal: signal }),
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/v4/profile`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
const [profileResponse, searchResponse] = await Promise.all([
|
||||
includeProfile
|
||||
? client.profile(namespace, undefined, requestOptions)
|
||||
: undefined,
|
||||
queryText
|
||||
? 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.
|
||||
*/
|
||||
export interface BuildMemoriesTextOptions {
|
||||
containerTag: string
|
||||
namespace: string
|
||||
queryText: string
|
||||
mode: MemoryMode
|
||||
baseUrl: string
|
||||
|
|
@ -95,7 +99,7 @@ export const buildMemoriesText = async (
|
|||
options: BuildMemoriesTextOptions,
|
||||
): Promise<string> => {
|
||||
const {
|
||||
containerTag,
|
||||
namespace,
|
||||
queryText,
|
||||
mode,
|
||||
baseUrl,
|
||||
|
|
@ -106,18 +110,19 @@ export const buildMemoriesText = async (
|
|||
} = options
|
||||
|
||||
const memoriesResponse = await supermemoryProfileSearch(
|
||||
containerTag,
|
||||
namespace,
|
||||
queryText,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
signal,
|
||||
mode !== "query",
|
||||
)
|
||||
|
||||
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
|
||||
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
|
||||
|
||||
logger.info("Memory search completed", {
|
||||
containerTag,
|
||||
namespace,
|
||||
memoryCountStatic,
|
||||
memoryCountDynamic,
|
||||
queryText:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { SearchResponseResultsItem } from "supermemory"
|
||||
|
||||
/**
|
||||
* Data provided to the prompt template function for customizing memory injection.
|
||||
*/
|
||||
|
|
@ -22,14 +24,13 @@ export interface MemoryPromptData {
|
|||
searchResults: MemorySearchResult[]
|
||||
}
|
||||
|
||||
/** A raw query result returned inside `/v4/profile.searchResults.results`. */
|
||||
export interface ProfileSearchResult {
|
||||
/** A raw v5 search result, as returned by `search(namespace)`. */
|
||||
export type ProfileSearchResult = SearchResponseResultsItem
|
||||
|
||||
/** One profile memory, as returned by `profile(namespace)`. */
|
||||
export interface ProfileEntry {
|
||||
id: string
|
||||
memory?: string
|
||||
chunk?: string
|
||||
metadata: Record<string, unknown> | null
|
||||
updatedAt: string
|
||||
similarity: number
|
||||
memory: string
|
||||
}
|
||||
|
||||
/** A visible, deduplicated query result provided to prompt templates. */
|
||||
|
|
@ -81,31 +82,19 @@ export interface Logger {
|
|||
error: (message: string, data?: unknown) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Response structure from the Supermemory profile API.
|
||||
*/
|
||||
/** v5 profile plus an optional memories search for the current query. */
|
||||
export interface ProfileStructure {
|
||||
profile: {
|
||||
/**
|
||||
* Core, stable facts about the user that rarely change.
|
||||
* Examples: name, profession, long-term preferences, goals.
|
||||
*/
|
||||
static?: string[]
|
||||
/**
|
||||
* Recently learned or frequently updated information about the user.
|
||||
* Examples: current projects, recent interests, ongoing topics.
|
||||
*/
|
||||
dynamic?: string[]
|
||||
/** Core, stable facts (name, profession, long-term preferences). */
|
||||
static?: ProfileEntry[]
|
||||
/** Recently learned or frequently updated context. */
|
||||
dynamic?: ProfileEntry[]
|
||||
/** Memories grouped by custom profile bucket. */
|
||||
buckets?: Record<string, string[]>
|
||||
buckets?: Record<string, ProfileEntry[]>
|
||||
}
|
||||
searchResults?: {
|
||||
/**
|
||||
* Memories retrieved based on semantic similarity to the current query.
|
||||
* Most relevant to the immediate conversation context.
|
||||
*/
|
||||
results: ProfileSearchResult[]
|
||||
total: number
|
||||
/** Server-side search duration in milliseconds. */
|
||||
timing: number
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,55 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
// Mock the Supermemory SDK (same pattern as claude-memory.test.ts) so tool
|
||||
// executions can be verified deterministically without network access.
|
||||
const documentsDeleteBulk = vi.fn()
|
||||
const documentsGet = vi.fn()
|
||||
const documentsList = vi.fn()
|
||||
const clientAdd = vi.fn()
|
||||
const clientSearch = vi.fn()
|
||||
const clientOptions: unknown[] = []
|
||||
// Mock the v5 SDK so tool executions are verified without network access.
|
||||
const sdk = vi.hoisted(() => {
|
||||
class NotFoundError extends Error {
|
||||
statusCode = 404
|
||||
}
|
||||
return {
|
||||
NotFoundError,
|
||||
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", () => {
|
||||
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 {
|
||||
default: class MockSupermemory {
|
||||
constructor(options: unknown) {
|
||||
clientOptions.push(options)
|
||||
}
|
||||
add = clientAdd
|
||||
search = clientSearch
|
||||
documents = {
|
||||
deleteBulk: documentsDeleteBulk,
|
||||
get: documentsGet,
|
||||
list: documentsList,
|
||||
add: vi.fn(),
|
||||
}
|
||||
},
|
||||
default: Supermemory,
|
||||
Supermemory,
|
||||
NotFoundError: sdk.NotFoundError,
|
||||
SupermemoryError: Error,
|
||||
}
|
||||
})
|
||||
|
||||
import * as aiSdk from "./ai-sdk"
|
||||
import { ClaudeMemoryTool } from "./claude-memory"
|
||||
import { forgetMemoryRequest } from "./shared/forget-memory"
|
||||
import * as openAi from "./openai/tools"
|
||||
|
||||
const API_KEY = "sm_test_key"
|
||||
const DEFAULT_NS = "sm_project_default"
|
||||
|
||||
type ToolWithExecute = { execute: (args: Record<string, unknown>) => unknown }
|
||||
|
||||
|
|
@ -41,28 +58,39 @@ function executeTool(tool: unknown, args: Record<string, unknown>) {
|
|||
}
|
||||
|
||||
beforeEach(() => {
|
||||
documentsDeleteBulk.mockReset().mockResolvedValue({
|
||||
success: true,
|
||||
deletedCount: 1,
|
||||
errors: [],
|
||||
sdk.add.mockReset().mockResolvedValue({ id: "doc_new", status: "queued" })
|
||||
sdk.search.mockReset().mockResolvedValue({ results: [], searchTime: 1 })
|
||||
sdk.profile.mockReset().mockResolvedValue({
|
||||
profile: {
|
||||
static: [{ id: "mem_s", memory: "Likes tea" }],
|
||||
dynamic: [],
|
||||
buckets: {},
|
||||
},
|
||||
})
|
||||
documentsGet.mockReset().mockResolvedValue({
|
||||
id: "doc_123",
|
||||
customId: "doc_123",
|
||||
containerTags: ["sm_project_default"],
|
||||
})
|
||||
documentsList.mockReset().mockResolvedValue({
|
||||
memories: [{ id: "doc_1", title: "Doc one" }],
|
||||
sdk.list.mockReset().mockResolvedValue({
|
||||
documents: [{ id: "doc_1", title: "Doc one" }],
|
||||
chunks: [],
|
||||
memories: [],
|
||||
pagination: { currentPage: 1, totalItems: 1, totalPages: 1 },
|
||||
})
|
||||
clientAdd.mockReset().mockResolvedValue({ id: "doc_new" })
|
||||
clientSearch.mockReset().mockResolvedValue({ results: [] })
|
||||
clientOptions.length = 0
|
||||
vi.unstubAllGlobals()
|
||||
sdk.documentsGet.mockReset().mockResolvedValue({
|
||||
id: "doc_123",
|
||||
metadata: {},
|
||||
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", () => {
|
||||
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 result = (await executeTool(tool, {
|
||||
informationToGet: "coffee order",
|
||||
|
|
@ -70,9 +98,12 @@ describe("searchMemories", () => {
|
|||
})) as { success: boolean }
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(clientSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ q: "coffee order", limit: 50 }),
|
||||
)
|
||||
expect(sdk.search).toHaveBeenCalledWith(DEFAULT_NS, {
|
||||
query: "coffee order",
|
||||
limit: 50,
|
||||
threshold: 0.6,
|
||||
searchMode: "hybrid",
|
||||
})
|
||||
})
|
||||
|
||||
it("ai-sdk schema rejects out-of-range limits", () => {
|
||||
|
|
@ -90,43 +121,135 @@ describe("searchMemories", () => {
|
|||
).toBe(true)
|
||||
})
|
||||
|
||||
it("openai variant clamps a non-positive limit before calling the SDK", async () => {
|
||||
const search = openAi.createSearchMemoriesFunction(API_KEY)
|
||||
it("openai variant clamps a non-positive limit and uses the configured namespace", async () => {
|
||||
const search = openAi.createSearchMemoriesFunction(API_KEY, {
|
||||
namespace: "user_1",
|
||||
})
|
||||
await search({ informationToGet: "coffee order", limit: 0 })
|
||||
|
||||
expect(clientSearch).toHaveBeenCalledWith(
|
||||
expect(sdk.search).toHaveBeenCalledWith(
|
||||
"user_1",
|
||||
expect.objectContaining({ limit: 1 }),
|
||||
)
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
expect(clientOptions).toHaveLength(2)
|
||||
for (const options of clientOptions) {
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({ timeout: 30_000, maxRetries: 2 }),
|
||||
)
|
||||
}
|
||||
expect(sdk.clientOptions).toHaveLength(2)
|
||||
expect(sdk.clientOptions[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
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", () => {
|
||||
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 result = (await executeTool(tool, { documentId: "doc_123" })) as {
|
||||
success: boolean
|
||||
}
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(documentsGet).toHaveBeenCalledWith("doc_123")
|
||||
expect(documentsDeleteBulk).toHaveBeenCalledWith({ ids: ["doc_123"] })
|
||||
expect(sdk.documentsGet).toHaveBeenCalledWith(DEFAULT_NS, "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", () => {
|
||||
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 result = (await executeTool(tool, {})) as {
|
||||
success: boolean
|
||||
|
|
@ -142,136 +265,140 @@ describe("documentList", () => {
|
|||
const result = await documentList({ limit: 5, page: 3 })
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.documents).toEqual([{ id: "doc_1", title: "Doc one" }])
|
||||
expect(documentsList).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ limit: 5, page: 3 }),
|
||||
)
|
||||
expect(sdk.list).toHaveBeenCalledWith(DEFAULT_NS, "documents", {
|
||||
limit: 5,
|
||||
page: 3,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("memoryForget", () => {
|
||||
function stubFetch(response = new Response(null, { status: 200 })) {
|
||||
const fetchMock = vi.fn().mockResolvedValue(response)
|
||||
vi.stubGlobal("fetch", fetchMock)
|
||||
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",
|
||||
it("forgets an exact memory ID", async () => {
|
||||
const memoryForget = openAi.createMemoryForgetFunction(API_KEY, {
|
||||
namespace: "user_1",
|
||||
})
|
||||
const result = await memoryForget({ memoryId: "mem_1" })
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||||
expect(url).toBe("https://api.supermemory.ai/v4/memories")
|
||||
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)
|
||||
expect(result.success).toBe(true)
|
||||
expect(sdk.forget).toHaveBeenCalledWith("user_1", { ids: ["mem_1"] })
|
||||
expect(sdk.forgetMatching).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("uses a caller-provided signal instead of creating a timeout", async () => {
|
||||
const fetchMock = stubFetch()
|
||||
const controller = new AbortController()
|
||||
|
||||
await forgetMemoryRequest(
|
||||
API_KEY,
|
||||
{ containerTag: "user_1", id: "mem_1" },
|
||||
undefined,
|
||||
{ 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"],
|
||||
it("forgets by content via a dry-run preview, keeping only exact matches", async () => {
|
||||
sdk.forgetMatching.mockResolvedValue({
|
||||
count: 2,
|
||||
matches: [
|
||||
{ id: "mem_exact", memory: "Stale fact" },
|
||||
{ id: "mem_close", memory: "Stale fact about tea" },
|
||||
],
|
||||
errors: [],
|
||||
})
|
||||
|
||||
const tool = aiSdk.memoryForgetTool(API_KEY, { namespace: "user_2" })
|
||||
const result = (await executeTool(tool, {
|
||||
memoryContent: "stale fact",
|
||||
})) as { success: boolean }
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||||
expect(JSON.parse(init.body as string)).toEqual({
|
||||
containerTag: "user_2",
|
||||
content: "stale fact",
|
||||
expect(sdk.forgetMatching).toHaveBeenCalledWith("user_2", {
|
||||
query: "stale fact",
|
||||
dryRun: true,
|
||||
})
|
||||
expect(sdk.forget).toHaveBeenCalledWith("user_2", { ids: ["mem_exact"] })
|
||||
})
|
||||
|
||||
it("openai tool surfaces endpoint failures as tool errors", async () => {
|
||||
stubFetch(new Response("boom", { status: 500, statusText: "Server Error" }))
|
||||
it("does not forget anything when no preview match is exact", async () => {
|
||||
sdk.forgetMatching.mockResolvedValue({
|
||||
count: 1,
|
||||
matches: [{ id: "mem_close", memory: "Something else" }],
|
||||
errors: [],
|
||||
})
|
||||
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" })
|
||||
|
||||
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 () => {
|
||||
const fetchMock = stubFetch()
|
||||
const memoryForget = openAi.createMemoryForgetFunction(API_KEY)
|
||||
|
||||
const result = await memoryForget({})
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
expect(sdk.forget).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("ClaudeMemoryTool", () => {
|
||||
const FILE_PATH = "/memories/prefs.txt"
|
||||
const CUSTOM_ID = "memories_prefs_txt"
|
||||
const DOCUMENT_ID = "doc_file_1"
|
||||
const FILE_ID = "memories_prefs_txt"
|
||||
|
||||
function mockFileDocument(content: string) {
|
||||
const metadata = {
|
||||
claude_memory_type: "file",
|
||||
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"],
|
||||
sdk.documentsGet.mockResolvedValue({
|
||||
id: "doc_file_1",
|
||||
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 () => {
|
||||
mockFileDocument("keep this\nremove this\n")
|
||||
const tool = new ClaudeMemoryTool(API_KEY)
|
||||
|
|
@ -284,7 +411,9 @@ describe("ClaudeMemoryTool", () => {
|
|||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(clientAdd).toHaveBeenCalledWith(
|
||||
expect(sdk.documentsUpdate).toHaveBeenCalledWith(
|
||||
DEFAULT_NS,
|
||||
FILE_ID,
|
||||
expect.objectContaining({ content: "keep this\n" }),
|
||||
)
|
||||
})
|
||||
|
|
@ -314,11 +443,31 @@ describe("ClaudeMemoryTool", () => {
|
|||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(clientAdd).toHaveBeenCalledWith(
|
||||
expect(sdk.documentsUpdate).toHaveBeenCalledWith(
|
||||
DEFAULT_NS,
|
||||
FILE_ID,
|
||||
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 () => {
|
||||
mockFileDocument("contents")
|
||||
const tool = new ClaudeMemoryTool(API_KEY)
|
||||
|
|
@ -329,11 +478,14 @@ describe("ClaudeMemoryTool", () => {
|
|||
})
|
||||
|
||||
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 () => {
|
||||
mockFileDocument("contents")
|
||||
sdk.documentsUpdate.mockRejectedValue(new sdk.NotFoundError("missing"))
|
||||
const tool = new ClaudeMemoryTool(API_KEY)
|
||||
|
||||
const result = await tool.handleCommand({
|
||||
|
|
@ -343,9 +495,39 @@ describe("ClaudeMemoryTool", () => {
|
|||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(clientAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ customId: "memories_renamed_txt" }),
|
||||
expect(sdk.add).toHaveBeenCalledWith(
|
||||
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] })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
39
packages/tools/src/tools-descriptions.ts
Normal file
39
packages/tools/src/tools-descriptions.ts
Normal 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
|
||||
|
|
@ -4,7 +4,7 @@ import {
|
|||
SEARCH_LIMIT_BOUNDS,
|
||||
clampSearchLimit,
|
||||
deduplicateMemoriesForMode,
|
||||
getContainerTags,
|
||||
getNamespace,
|
||||
} from "./tools-shared"
|
||||
|
||||
describe("clampSearchLimit", () => {
|
||||
|
|
@ -32,29 +32,19 @@ describe("clampSearchLimit", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("getContainerTags", () => {
|
||||
it("uses the default project when no config is provided", () => {
|
||||
expect(getContainerTags()).toEqual(["sm_project_default"])
|
||||
describe("getNamespace", () => {
|
||||
it("uses the default namespace when no config is provided", () => {
|
||||
expect(getNamespace()).toBe("sm_project_default")
|
||||
})
|
||||
|
||||
it("converts projectId into a project container tag", () => {
|
||||
expect(getContainerTags({ projectId: "abc" })).toEqual(["sm_project_abc"])
|
||||
it("uses the configured namespace", () => {
|
||||
expect(getNamespace({ namespace: "user_1" })).toBe("user_1")
|
||||
})
|
||||
|
||||
it("uses explicit container tags", () => {
|
||||
expect(getContainerTags({ containerTags: ["tag-a", "tag-b"] })).toEqual([
|
||||
"tag-a",
|
||||
"tag-b",
|
||||
])
|
||||
})
|
||||
|
||||
it("rejects config with both projectId and containerTags", () => {
|
||||
expect(() =>
|
||||
getContainerTags({
|
||||
projectId: "abc",
|
||||
containerTags: ["tag-a"],
|
||||
}),
|
||||
).toThrow("either projectId or containerTags")
|
||||
it("rejects an empty namespace", () => {
|
||||
expect(() => getNamespace({ namespace: " " })).toThrow(
|
||||
"non-empty namespace",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,73 +1,18 @@
|
|||
/**
|
||||
* Shared constants and descriptions for Supermemory tools
|
||||
*/
|
||||
|
||||
import type Supermemory from "supermemory"
|
||||
import { NotFoundError, Supermemory } from "supermemory"
|
||||
import type { MemoryMode } from "./shared/types"
|
||||
import type { SupermemoryToolsConfig } from "./types"
|
||||
|
||||
// Tool 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
|
||||
export { TOOL_DESCRIPTIONS, PARAMETER_DESCRIPTIONS } from "./tools-descriptions"
|
||||
|
||||
// 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 = {
|
||||
includeFullDocs: true,
|
||||
limit: 10,
|
||||
searchThreshold: 0.6,
|
||||
} as const
|
||||
|
||||
// Bounds for the searchMemories `limit` input.
|
||||
export const SEARCH_LIMIT_BOUNDS = { min: 1, max: 50 } as const
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
// The schema constrains well-behaved models; a prompt-injected one can still send anything.
|
||||
export function clampSearchLimit(value: unknown): number {
|
||||
const parsed = Number(value)
|
||||
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
|
||||
// and limit retries so a slow API cannot stall an agent turn indefinitely.
|
||||
// Bound each request so a slow API cannot stall an agent turn indefinitely.
|
||||
export const CLIENT_OPTIONS = {
|
||||
timeout: 30_000,
|
||||
timeoutInSeconds: 30,
|
||||
maxRetries: 2,
|
||||
} as const
|
||||
|
||||
// Container tag constants
|
||||
export const CONTAINER_TAG_CONSTANTS = {
|
||||
projectPrefix: "sm_project_",
|
||||
defaultTags: ["sm_project_default"] as const,
|
||||
} as const
|
||||
export const DEFAULT_NAMESPACE = "sm_project_default"
|
||||
|
||||
/**
|
||||
* Helper function to generate container tags based on config
|
||||
*/
|
||||
export function getContainerTags(config?: {
|
||||
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.",
|
||||
)
|
||||
export function getNamespace(config?: { namespace?: string }): string {
|
||||
if (config?.namespace === undefined) return DEFAULT_NAMESPACE
|
||||
if (config.namespace.trim() === "") {
|
||||
throw new Error("Supermemory tools config requires a non-empty namespace.")
|
||||
}
|
||||
if (config?.projectId !== undefined) {
|
||||
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]
|
||||
return config.namespace
|
||||
}
|
||||
|
||||
/** Delete exactly one document by its internal ID. */
|
||||
export async function deleteDocumentById(
|
||||
client: Supermemory,
|
||||
documentId: string,
|
||||
): Promise<void> {
|
||||
const response = await client.documents.deleteBulk({ ids: [documentId] })
|
||||
if (response.success && response.deletedCount === 1) return
|
||||
|
||||
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 createToolsClient(
|
||||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
): Supermemory {
|
||||
return new Supermemory({
|
||||
apiKey,
|
||||
...CLIENT_OPTIONS,
|
||||
...(config?.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an internal ID or customId inside the effective container-tag union,
|
||||
* then delete the exact internal document ID. Internal IDs take precedence over
|
||||
* customId matches.
|
||||
*/
|
||||
export async function deleteDocumentByIdentifier(
|
||||
client: Supermemory,
|
||||
documentIdentifier: string,
|
||||
containerTags: readonly [string, ...string[]],
|
||||
): Promise<void> {
|
||||
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
|
||||
)
|
||||
export function isNotFoundError(error: unknown): boolean {
|
||||
if (error instanceof NotFoundError) return true
|
||||
if (typeof error !== "object" || error === null) return false
|
||||
const status =
|
||||
"statusCode" in error
|
||||
? error.statusCode
|
||||
: "status" in error
|
||||
? error.status
|
||||
: undefined
|
||||
return status === 404
|
||||
}
|
||||
|
||||
const TERMINAL_DOCUMENT_STATUSES = new Set(["done", "failed"])
|
||||
|
||||
function assertDocumentCanBeDeleted(
|
||||
document: Awaited<ReturnType<Supermemory["documents"]["get"]>>,
|
||||
expectedContainerTags: readonly string[],
|
||||
): void {
|
||||
if (
|
||||
!hasCompleteContainerTagScope(document.containerTags, expectedContainerTags)
|
||||
) {
|
||||
// Accepts a Supermemory ID or caller-defined ID; v5 resolves both inside the namespace.
|
||||
export async function deleteDocument(
|
||||
client: Supermemory,
|
||||
namespace: string,
|
||||
documentId: string,
|
||||
): 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(
|
||||
`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
|
||||
// older SDKs and lightweight client doubles continue to work.
|
||||
const status = (document as { status?: string }).status
|
||||
if (status !== undefined && !TERMINAL_DOCUMENT_STATUSES.has(status)) {
|
||||
throw new Error(
|
||||
`Document ${document.id} cannot be deleted while it is processing or otherwise nonterminal (status: ${status}).`,
|
||||
)
|
||||
const response = await client.documents.delete(namespace, {
|
||||
ids: [document.id],
|
||||
})
|
||||
if (response.count === 1) return
|
||||
|
||||
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(
|
||||
actual: string[] | undefined,
|
||||
expected: readonly string[],
|
||||
): boolean {
|
||||
return (
|
||||
actual !== undefined &&
|
||||
actual.length > 0 &&
|
||||
actual.every((tag) => tag.trim() !== "" && expected.includes(tag))
|
||||
export interface ForgetMemoryParams {
|
||||
id?: string
|
||||
content?: string
|
||||
}
|
||||
|
||||
// Exact-text forget previews semantic matches, then forgets only the exact-text ones by ID.
|
||||
export async function forgetMemory(
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
// v5 profile and search entries are objects; plain strings are still accepted.
|
||||
export interface ProfileWithMemories {
|
||||
static?: Array<MemoryItem | string>
|
||||
dynamic?: Array<MemoryItem | string>
|
||||
|
|
|
|||
|
|
@ -59,25 +59,25 @@ describe("@supermemory/tools", () => {
|
|||
|
||||
it("should create individual tools", () => {
|
||||
const searchTool = aiSdk.searchMemoriesTool(testApiKey, {
|
||||
projectId: "test-project-123",
|
||||
namespace: "test-project-123",
|
||||
})
|
||||
const addTool = aiSdk.addMemoryTool(testApiKey, {
|
||||
projectId: "test-project-123",
|
||||
namespace: "test-project-123",
|
||||
})
|
||||
const profileTool = aiSdk.getProfileTool(testApiKey, {
|
||||
projectId: "test-project-123",
|
||||
namespace: "test-project-123",
|
||||
})
|
||||
const listTool = aiSdk.documentListTool(testApiKey, {
|
||||
projectId: "test-project-123",
|
||||
namespace: "test-project-123",
|
||||
})
|
||||
const deleteTool = aiSdk.documentDeleteTool(testApiKey, {
|
||||
projectId: "test-project-123",
|
||||
namespace: "test-project-123",
|
||||
})
|
||||
const addDocTool = aiSdk.documentAddTool(testApiKey, {
|
||||
projectId: "test-project-123",
|
||||
namespace: "test-project-123",
|
||||
})
|
||||
const forgetTool = aiSdk.memoryForgetTool(testApiKey, {
|
||||
projectId: "test-project-123",
|
||||
namespace: "test-project-123",
|
||||
})
|
||||
|
||||
expect(searchTool).toBeDefined()
|
||||
|
|
@ -111,7 +111,7 @@ describe("@supermemory/tools", () => {
|
|||
],
|
||||
tools: {
|
||||
...aiSdk.supermemoryTools(testApiKey, {
|
||||
projectId: "test-ai-integration",
|
||||
namespace: "test-ai-integration",
|
||||
baseUrl: testBaseUrl,
|
||||
}),
|
||||
},
|
||||
|
|
@ -128,7 +128,7 @@ describe("@supermemory/tools", () => {
|
|||
})
|
||||
|
||||
const tools = aiSdk.supermemoryTools(testApiKey, {
|
||||
projectId: "test-tool-usage",
|
||||
namespace: "test-tool-usage",
|
||||
baseUrl: testBaseUrl,
|
||||
})
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ describe("@supermemory/tools", () => {
|
|||
})
|
||||
|
||||
const tools = aiSdk.supermemoryTools(testApiKey, {
|
||||
projectId: "test-profile-tool",
|
||||
namespace: "test-profile-tool",
|
||||
baseUrl: testBaseUrl,
|
||||
})
|
||||
|
||||
|
|
@ -192,7 +192,7 @@ describe("@supermemory/tools", () => {
|
|||
})
|
||||
|
||||
const tools = aiSdk.supermemoryTools(testApiKey, {
|
||||
projectId: "test-document-tools",
|
||||
namespace: "test-document-tools",
|
||||
baseUrl: testBaseUrl,
|
||||
})
|
||||
|
||||
|
|
@ -222,7 +222,7 @@ describe("@supermemory/tools", () => {
|
|||
describe("new tool operations", () => {
|
||||
it("should get profile with getProfileTool", async () => {
|
||||
const profileTool = aiSdk.getProfileTool(testApiKey, {
|
||||
projectId: "test-profile",
|
||||
namespace: "test-profile",
|
||||
baseUrl: testBaseUrl,
|
||||
})
|
||||
|
||||
|
|
@ -234,7 +234,7 @@ describe("@supermemory/tools", () => {
|
|||
|
||||
it("should list documents with documentListTool", async () => {
|
||||
const listTool = aiSdk.documentListTool(testApiKey, {
|
||||
projectId: "test-list",
|
||||
namespace: "test-list",
|
||||
baseUrl: testBaseUrl,
|
||||
})
|
||||
|
||||
|
|
@ -245,7 +245,7 @@ describe("@supermemory/tools", () => {
|
|||
|
||||
it("should create documentDeleteTool", async () => {
|
||||
const deleteTool = aiSdk.documentDeleteTool(testApiKey, {
|
||||
projectId: "test-delete",
|
||||
namespace: "test-delete",
|
||||
baseUrl: testBaseUrl,
|
||||
})
|
||||
|
||||
|
|
@ -256,7 +256,7 @@ describe("@supermemory/tools", () => {
|
|||
|
||||
it("should create documentAddTool", async () => {
|
||||
const addDocTool = aiSdk.documentAddTool(testApiKey, {
|
||||
projectId: "test-add-doc",
|
||||
namespace: "test-add-doc",
|
||||
baseUrl: testBaseUrl,
|
||||
})
|
||||
|
||||
|
|
@ -267,7 +267,7 @@ describe("@supermemory/tools", () => {
|
|||
|
||||
it("should create memoryForgetTool", async () => {
|
||||
const forgetTool = aiSdk.memoryForgetTool(testApiKey, {
|
||||
projectId: "test-forget",
|
||||
namespace: "test-forget",
|
||||
baseUrl: testBaseUrl,
|
||||
})
|
||||
|
||||
|
|
@ -282,7 +282,7 @@ describe("@supermemory/tools", () => {
|
|||
describe("function-based tools", () => {
|
||||
it("should create function-based tools", () => {
|
||||
const tools = openAi.supermemoryTools(testApiKey, {
|
||||
projectId: "test-openai-functions",
|
||||
namespace: "test-openai-functions",
|
||||
})
|
||||
|
||||
expect(tools).toBeDefined()
|
||||
|
|
@ -292,10 +292,10 @@ describe("@supermemory/tools", () => {
|
|||
|
||||
it("should create individual tool functions", () => {
|
||||
const searchFunction = openAi.createSearchMemoriesFunction(testApiKey, {
|
||||
projectId: "test-individual",
|
||||
namespace: "test-individual",
|
||||
})
|
||||
const addFunction = openAi.createAddMemoryFunction(testApiKey, {
|
||||
projectId: "test-individual",
|
||||
namespace: "test-individual",
|
||||
})
|
||||
|
||||
expect(searchFunction).toBeDefined()
|
||||
|
|
@ -333,7 +333,7 @@ describe("@supermemory/tools", () => {
|
|||
describe("tool execution", () => {
|
||||
it("should create tool call executor", () => {
|
||||
const executor = openAi.createToolCallExecutor(testApiKey, {
|
||||
containerTags: ["test-executor"],
|
||||
namespace: "test-executor",
|
||||
baseUrl: testBaseUrl,
|
||||
})
|
||||
|
||||
|
|
@ -343,7 +343,7 @@ describe("@supermemory/tools", () => {
|
|||
|
||||
it("should create tool calls executor", () => {
|
||||
const executor = openAi.createToolCallsExecutor(testApiKey, {
|
||||
containerTags: ["test-executors"],
|
||||
namespace: "test-executors",
|
||||
baseUrl: testBaseUrl,
|
||||
})
|
||||
|
||||
|
|
@ -355,7 +355,7 @@ describe("@supermemory/tools", () => {
|
|||
describe("individual tool creators", () => {
|
||||
it("should create individual search tool", () => {
|
||||
const searchTool = openAi.createSearchMemoriesTool(testApiKey, {
|
||||
projectId: "test-individual",
|
||||
namespace: "test-individual",
|
||||
})
|
||||
|
||||
expect(searchTool).toBeDefined()
|
||||
|
|
@ -366,7 +366,7 @@ describe("@supermemory/tools", () => {
|
|||
|
||||
it("should create individual add tool", () => {
|
||||
const addTool = openAi.createAddMemoryTool(testApiKey, {
|
||||
projectId: "test-individual",
|
||||
namespace: "test-individual",
|
||||
})
|
||||
|
||||
expect(addTool).toBeDefined()
|
||||
|
|
@ -379,7 +379,7 @@ describe("@supermemory/tools", () => {
|
|||
describe("memory operations", () => {
|
||||
it("should search memories", async () => {
|
||||
const searchFunction = openAi.createSearchMemoriesFunction(testApiKey, {
|
||||
projectId: "test-search",
|
||||
namespace: "test-search",
|
||||
baseUrl: testBaseUrl,
|
||||
})
|
||||
|
||||
|
|
@ -403,7 +403,7 @@ describe("@supermemory/tools", () => {
|
|||
|
||||
it("should add memory", async () => {
|
||||
const addFunction = openAi.createAddMemoryFunction(testApiKey, {
|
||||
containerTags: ["test-add-memory"],
|
||||
namespace: "test-add-memory",
|
||||
baseUrl: testBaseUrl,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,8 @@
|
|||
/**
|
||||
* Supermemory configuration
|
||||
* Only one of `projectId` or `containerTags` can be provided.
|
||||
*/
|
||||
export interface SupermemoryToolsConfig {
|
||||
/**
|
||||
* Custom base URL for the supermemory API
|
||||
*/
|
||||
/** Custom base URL for the supermemory API */
|
||||
baseUrl?: string
|
||||
/**
|
||||
* Array of custom container tags (mutually exclusive with projectId)
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
/** Namespace every tool reads and writes. Defaults to `sm_project_default`. */
|
||||
namespace?: string
|
||||
/** When true, all schema properties are required (OpenAI strict mode). */
|
||||
strict?: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ import { injectMemoriesIntoParams } from "./memory-prompt"
|
|||
const DEFAULT_MEMORY_RETRIEVAL_TIMEOUT_MS = 5000
|
||||
|
||||
interface WrapVercelLanguageModelOptions {
|
||||
/** The container tag/identifier for memory search (e.g., user ID, project ID) */
|
||||
containerTag: string
|
||||
/** Custom ID to group messages into a single document. Required. */
|
||||
customId: string
|
||||
/** The namespace for memory search (e.g., user ID, project ID) */
|
||||
namespace: string
|
||||
/** ID that groups messages into a single document. Required. */
|
||||
id: string
|
||||
/** Enable detailed logging of memory search and injection */
|
||||
verbose?: boolean
|
||||
/**
|
||||
|
|
@ -73,7 +73,7 @@ interface WrapVercelLanguageModelOptions {
|
|||
* Wraps a language model with supermemory middleware to automatically inject relevant memories
|
||||
* 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
|
||||
* a new system prompt with the memories. Pre-LLM profile retrieval uses a fixed internal
|
||||
* 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 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.customId - Required. Custom ID to group messages into a single document for contextual memory generation
|
||||
* @param options.namespace - Required. The namespace for memory search (e.g., user ID, project ID)
|
||||
* @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.mode - Optional mode for memory search: "profile", "query", or "full" (default: "profile")
|
||||
* @param options.addMemory - Optional mode for memory search: "always", "never" (default: "always")
|
||||
|
|
@ -100,8 +100,8 @@ interface WrapVercelLanguageModelOptions {
|
|||
* import { openai } from "@ai-sdk/openai"
|
||||
*
|
||||
* const modelWithMemory = withSupermemory(openai("gpt-4"), {
|
||||
* containerTag: "user-123",
|
||||
* customId: "conversation-456",
|
||||
* namespace: "user-123",
|
||||
* id: "conversation-456",
|
||||
* mode: "full",
|
||||
* addMemory: "always"
|
||||
* })
|
||||
|
|
@ -127,16 +127,16 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
|
|||
)
|
||||
}
|
||||
|
||||
if (!options.customId) {
|
||||
if (!options.id) {
|
||||
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({
|
||||
containerTag: options.containerTag,
|
||||
namespace: options.namespace,
|
||||
apiKey: providedApiKey,
|
||||
customId: options.customId,
|
||||
id: options.id,
|
||||
verbose: options.verbose ?? false,
|
||||
mode: options.mode ?? "profile",
|
||||
addMemory: options.addMemory ?? "always",
|
||||
|
|
@ -192,8 +192,8 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
|
|||
)
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.customId,
|
||||
ctx.namespace,
|
||||
ctx.id,
|
||||
assistantResponseText,
|
||||
params,
|
||||
ctx.logger,
|
||||
|
|
@ -266,8 +266,8 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
|
|||
) {
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.customId,
|
||||
ctx.namespace,
|
||||
ctx.id,
|
||||
generatedText,
|
||||
params,
|
||||
ctx.logger,
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export const injectMemoriesIntoParams = (
|
|||
*/
|
||||
export const addSystemPrompt = async (
|
||||
params: LanguageModelCallOptions,
|
||||
containerTag: string,
|
||||
namespace: string,
|
||||
logger: Logger,
|
||||
mode: "profile" | "query" | "full",
|
||||
baseUrl: string,
|
||||
|
|
@ -120,7 +120,7 @@ export const addSystemPrompt = async (
|
|||
const queryText = extractQueryText(params, mode)
|
||||
|
||||
const memories = await buildMemoriesText({
|
||||
containerTag,
|
||||
namespace,
|
||||
queryText,
|
||||
mode,
|
||||
baseUrl,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Supermemory from "supermemory"
|
||||
import { Supermemory } from "supermemory"
|
||||
import {
|
||||
addConversation,
|
||||
type ContentPart,
|
||||
|
|
@ -150,8 +150,8 @@ export const convertToConversationMessages = (
|
|||
|
||||
export const saveMemoryAfterResponse = async (
|
||||
_client: Supermemory,
|
||||
containerTag: string,
|
||||
customId: string,
|
||||
namespace: string,
|
||||
id: string,
|
||||
assistantResponseText: string,
|
||||
params: LanguageModelCallOptions,
|
||||
logger: Logger,
|
||||
|
|
@ -167,16 +167,16 @@ export const saveMemoryAfterResponse = async (
|
|||
)
|
||||
|
||||
const response = await addConversation({
|
||||
conversationId: customId,
|
||||
id,
|
||||
messages: conversationMessages,
|
||||
containerTags: [containerTag],
|
||||
namespace,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
})
|
||||
|
||||
logger.info("Conversation saved successfully via /v4/conversations", {
|
||||
containerTag,
|
||||
customId,
|
||||
logger.info("Conversation saved successfully", {
|
||||
namespace,
|
||||
id,
|
||||
messageCount: conversationMessages.length,
|
||||
responseId: response.id,
|
||||
})
|
||||
|
|
@ -191,12 +191,12 @@ export const saveMemoryAfterResponse = async (
|
|||
* Configuration options for the Supermemory middleware.
|
||||
*/
|
||||
interface SupermemoryMiddlewareOptions {
|
||||
/** Container tag/identifier for memory search (e.g., user ID, project ID) */
|
||||
containerTag: string
|
||||
/** Namespace for memory search (e.g., user ID, project ID) */
|
||||
namespace: string
|
||||
/** Supermemory API key */
|
||||
apiKey: string
|
||||
/** Custom ID to group messages into a single document. Required. */
|
||||
customId: string
|
||||
/** ID that groups messages into a single document. Required. */
|
||||
id: string
|
||||
/** Enable detailed logging of memory search and injection */
|
||||
verbose?: boolean
|
||||
/**
|
||||
|
|
@ -222,15 +222,15 @@ interface SupermemoryMiddlewareOptions {
|
|||
includeToolCalls?: boolean
|
||||
/** Custom function to format memory data into the system prompt */
|
||||
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
|
||||
}
|
||||
|
||||
interface SupermemoryMiddlewareContext {
|
||||
client: Supermemory
|
||||
logger: Logger
|
||||
containerTag: string
|
||||
customId: string
|
||||
namespace: string
|
||||
id: string
|
||||
mode: MemoryMode
|
||||
addMemory: "always" | "never"
|
||||
includeToolCalls: boolean
|
||||
|
|
@ -249,9 +249,9 @@ export const createSupermemoryContext = (
|
|||
options: SupermemoryMiddlewareOptions,
|
||||
): SupermemoryMiddlewareContext => {
|
||||
const {
|
||||
containerTag,
|
||||
namespace,
|
||||
apiKey,
|
||||
customId,
|
||||
id,
|
||||
verbose = false,
|
||||
mode = "profile",
|
||||
addMemory = "always",
|
||||
|
|
@ -267,15 +267,15 @@ export const createSupermemoryContext = (
|
|||
const client = new Supermemory({
|
||||
apiKey,
|
||||
...(normalizedBaseUrl !== "https://api.supermemory.ai"
|
||||
? { baseURL: normalizedBaseUrl }
|
||||
? { baseUrl: normalizedBaseUrl }
|
||||
: {}),
|
||||
})
|
||||
|
||||
return {
|
||||
client,
|
||||
logger,
|
||||
containerTag,
|
||||
customId,
|
||||
namespace,
|
||||
id,
|
||||
mode,
|
||||
addMemory,
|
||||
includeToolCalls,
|
||||
|
|
@ -297,12 +297,7 @@ const makeTurnKey = (
|
|||
ctx: SupermemoryMiddlewareContext,
|
||||
userMessage: string,
|
||||
): string => {
|
||||
return MemoryCache.makeTurnKey(
|
||||
ctx.containerTag,
|
||||
ctx.customId,
|
||||
ctx.mode,
|
||||
userMessage,
|
||||
)
|
||||
return MemoryCache.makeTurnKey(ctx.namespace, ctx.id, ctx.mode, userMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -341,8 +336,8 @@ export const transformParamsWithMemory = async (
|
|||
}
|
||||
|
||||
ctx.logger.info("Starting memory search", {
|
||||
containerTag: ctx.containerTag,
|
||||
customId: ctx.customId,
|
||||
namespace: ctx.namespace,
|
||||
id: ctx.id,
|
||||
mode: ctx.mode,
|
||||
isNewTurn,
|
||||
cacheHit: false,
|
||||
|
|
@ -362,7 +357,7 @@ export const transformParamsWithMemory = async (
|
|||
let memories: string
|
||||
try {
|
||||
memories = await buildMemoriesText({
|
||||
containerTag: ctx.containerTag,
|
||||
namespace: ctx.namespace,
|
||||
queryText,
|
||||
mode: ctx.mode,
|
||||
baseUrl: ctx.normalizedBaseUrl,
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export const getLastUserMessage = (
|
|||
.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 = (
|
||||
params: LanguageModelCallOptions,
|
||||
): boolean => {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const getOutputText = (output: unknown): string => {
|
|||
* These hooks intercept the agent lifecycle to inject memories
|
||||
* 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
|
||||
* @returns VoltAgent hooks object with onPrepareMessages and onEnd
|
||||
*
|
||||
|
|
@ -61,7 +61,7 @@ const getOutputText = (output: unknown): string => {
|
|||
* const hooks = createSupermemoryHooks("user-123", {
|
||||
* mode: "full",
|
||||
* addMemory: "always",
|
||||
* customId: "conv-456",
|
||||
* id: "conv-456",
|
||||
* })
|
||||
*
|
||||
* const agent = new Agent({
|
||||
|
|
@ -73,10 +73,10 @@ const getOutputText = (output: unknown): string => {
|
|||
* ```
|
||||
*/
|
||||
export function createSupermemoryHooks(
|
||||
containerTag: string,
|
||||
namespace: string,
|
||||
options: SupermemoryVoltAgent,
|
||||
): VoltAgentHooks {
|
||||
const ctx = createSupermemoryContext(containerTag, options)
|
||||
const ctx = createSupermemoryContext(namespace, options)
|
||||
|
||||
return {
|
||||
onPrepareMessages: async (args: HookPrepareMessagesArgs) => {
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
|
|||
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.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.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.baseUrl - Custom Supermemory API base URL
|
||||
* @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.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.filters - Advanced AND/OR filters for search
|
||||
* @param options.include - Control what additional data to include (chunks, documents, etc.)
|
||||
* @param options.filter - Typed metadata filter for search
|
||||
* @param options.include - Extra context per result: documents, related, forgotten
|
||||
* @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.entityContext - Deprecated and ignored; configure entity context on the container tag instead
|
||||
* @param options.searchMode - Search mode: "memories" (atomic facts), "chunks", or "hybrid" (both)
|
||||
* @param options.supportingContext - Context that guides memory extraction for saved conversations
|
||||
* @returns Enhanced agent config with Supermemory hooks injected
|
||||
*
|
||||
* @example
|
||||
|
|
@ -67,8 +67,8 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
|
|||
* instructions: "You are a helpful assistant",
|
||||
* model: openai("gpt-4o"),
|
||||
* },
|
||||
* containerTag: "user-123",
|
||||
* customId: "conversation-123"
|
||||
* namespace: "user-123",
|
||||
* id: "conversation-123"
|
||||
* })
|
||||
*
|
||||
* const agent = new Agent(configWithMemory)
|
||||
|
|
@ -83,14 +83,14 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
|
|||
* instructions: "You are a helpful assistant",
|
||||
* model: openai("gpt-4o"),
|
||||
* },
|
||||
* containerTag: "user-123", // Required: user/project ID
|
||||
* namespace: "user-123", // Required: user/project ID
|
||||
* mode: "full", // "profile" | "query" | "full"
|
||||
* 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)
|
||||
* limit: 15, // Max results to return
|
||||
* rerank: true, // Rerank for best relevance
|
||||
* searchMode: "hybrid", // "memories" | "documents" | "hybrid"
|
||||
* rerank: "order", // Rerank for best relevance
|
||||
* searchMode: "hybrid", // "memories" | "chunks" | "hybrid"
|
||||
* metadata: { // Custom metadata
|
||||
* source: "voltagent",
|
||||
* version: "1.0"
|
||||
|
|
@ -114,8 +114,8 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
|
|||
* instructions: "...",
|
||||
* model: openai("gpt-4o"),
|
||||
* },
|
||||
* containerTag: "user-123",
|
||||
* customId: "conversation-123",
|
||||
* namespace: "user-123",
|
||||
* id: "conversation-123",
|
||||
* mode: "full",
|
||||
* promptTemplate: (data) => `
|
||||
* <user_context>
|
||||
|
|
@ -134,13 +134,10 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
|
|||
export function withSupermemory<T extends VoltAgentConfig>(
|
||||
options: WithSupermemoryOptions<T>,
|
||||
): 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)
|
||||
const supermemoryHooks = createSupermemoryHooks(
|
||||
containerTag,
|
||||
supermemoryOptions,
|
||||
)
|
||||
const supermemoryHooks = createSupermemoryHooks(namespace, supermemoryOptions)
|
||||
|
||||
// Merge with existing hooks if present
|
||||
const mergedHooks = mergeHooks(agentConfig.hooks, supermemoryHooks)
|
||||
|
|
@ -158,7 +155,7 @@ export type {
|
|||
VoltAgentConfig,
|
||||
VoltAgentMessage,
|
||||
VoltAgentHooks,
|
||||
SearchFilters,
|
||||
SearchFilter,
|
||||
IncludeOptions,
|
||||
PromptTemplate,
|
||||
MemoryMode,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* Provides memory retrieval, injection, and storage functionality.
|
||||
*/
|
||||
|
||||
import Supermemory from "supermemory"
|
||||
import { type SearchRequest, Supermemory } from "supermemory"
|
||||
import {
|
||||
addConversation,
|
||||
type ContentPart as ConversationContentPart,
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
extractQueryText,
|
||||
replaceMemoryContext,
|
||||
stripMemoryContext,
|
||||
PROFILE_SEARCH_THRESHOLD,
|
||||
supermemoryProfileSearch,
|
||||
wrapMemoryContext,
|
||||
type Logger,
|
||||
|
|
@ -33,7 +34,7 @@ import {
|
|||
normalizeMemoryFact,
|
||||
} from "../tools-shared"
|
||||
import type {
|
||||
SearchFilters,
|
||||
SearchFilter,
|
||||
SupermemoryVoltAgent,
|
||||
VoltAgentMessage,
|
||||
} from "./types"
|
||||
|
|
@ -44,8 +45,8 @@ import type {
|
|||
export interface SupermemoryMiddlewareContext {
|
||||
client: Supermemory
|
||||
logger: Logger
|
||||
containerTag: string
|
||||
customId: string
|
||||
namespace: string
|
||||
id: string
|
||||
mode: MemoryMode
|
||||
addMemory: "always" | "never"
|
||||
normalizedBaseUrl: string
|
||||
|
|
@ -56,29 +57,22 @@ export interface SupermemoryMiddlewareContext {
|
|||
* user turn (keyed by turnKey) to avoid redundant API calls.
|
||||
*/
|
||||
memoryCache: MemoryCache<string>
|
||||
// New search parameters
|
||||
threshold?: number
|
||||
limit?: number
|
||||
rerank?: boolean
|
||||
rerank?: SupermemoryVoltAgent["rerank"]
|
||||
rewriteQuery?: boolean
|
||||
filters?: SearchFilters
|
||||
include?: {
|
||||
chunks?: boolean
|
||||
documents?: boolean
|
||||
forgottenMemories?: boolean
|
||||
relatedMemories?: boolean
|
||||
summaries?: boolean
|
||||
}
|
||||
// Storage parameters
|
||||
filter?: SearchFilter
|
||||
include?: SupermemoryVoltAgent["include"]
|
||||
metadata?: Record<string, string | number | boolean>
|
||||
searchMode?: "memories" | "documents" | "hybrid"
|
||||
supportingContext?: string
|
||||
searchMode?: SupermemoryVoltAgent["searchMode"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Supermemory middleware context.
|
||||
*/
|
||||
export const createSupermemoryContext = (
|
||||
containerTag: string,
|
||||
namespace: string,
|
||||
options: SupermemoryVoltAgent,
|
||||
): SupermemoryMiddlewareContext => {
|
||||
const apiKey = options.apiKey ?? process.env.SUPERMEMORY_API_KEY
|
||||
|
|
@ -89,7 +83,7 @@ export const createSupermemoryContext = (
|
|||
}
|
||||
|
||||
const {
|
||||
customId,
|
||||
id,
|
||||
mode = "profile",
|
||||
addMemory = "always", // VoltAgent default: save conversations by default for chat apps
|
||||
baseUrl,
|
||||
|
|
@ -98,17 +92,18 @@ export const createSupermemoryContext = (
|
|||
limit,
|
||||
rerank,
|
||||
rewriteQuery,
|
||||
filters,
|
||||
filter,
|
||||
include,
|
||||
metadata,
|
||||
supportingContext,
|
||||
searchMode,
|
||||
verbose = false,
|
||||
} = options
|
||||
|
||||
// Runtime validation: customId is required
|
||||
if (!customId || typeof customId !== "string" || customId.trim() === "") {
|
||||
// Runtime validation: id is required
|
||||
if (!id || typeof id !== "string" || id.trim() === "") {
|
||||
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 (
|
||||
|
|
@ -125,25 +120,20 @@ export const createSupermemoryContext = (
|
|||
}
|
||||
|
||||
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 client = new Supermemory({
|
||||
apiKey,
|
||||
...(normalizedBaseUrl !== "https://api.supermemory.ai"
|
||||
? { baseURL: normalizedBaseUrl }
|
||||
? { baseUrl: normalizedBaseUrl }
|
||||
: {}),
|
||||
})
|
||||
|
||||
return {
|
||||
client,
|
||||
logger,
|
||||
containerTag,
|
||||
customId,
|
||||
namespace,
|
||||
id,
|
||||
mode,
|
||||
addMemory,
|
||||
normalizedBaseUrl,
|
||||
|
|
@ -154,9 +144,10 @@ export const createSupermemoryContext = (
|
|||
limit,
|
||||
rerank,
|
||||
rewriteQuery,
|
||||
filters,
|
||||
filter,
|
||||
include,
|
||||
metadata,
|
||||
supportingContext,
|
||||
searchMode,
|
||||
}
|
||||
}
|
||||
|
|
@ -168,12 +159,7 @@ const makeTurnKey = (
|
|||
ctx: SupermemoryMiddlewareContext,
|
||||
userMessage: string,
|
||||
): string => {
|
||||
return MemoryCache.makeTurnKey(
|
||||
ctx.containerTag,
|
||||
ctx.customId,
|
||||
ctx.mode,
|
||||
userMessage,
|
||||
)
|
||||
return MemoryCache.makeTurnKey(ctx.namespace, ctx.id, ctx.mode, userMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -259,8 +245,8 @@ export const enhanceMessagesWithMemories = async (
|
|||
}
|
||||
|
||||
ctx.logger.info("Starting memory search", {
|
||||
containerTag: ctx.containerTag,
|
||||
customId: ctx.customId,
|
||||
namespace: ctx.namespace,
|
||||
id: ctx.id,
|
||||
mode: ctx.mode,
|
||||
isNewTurn,
|
||||
})
|
||||
|
|
@ -277,7 +263,7 @@ export const enhanceMessagesWithMemories = async (
|
|||
ctx.limit !== undefined ||
|
||||
ctx.rerank !== undefined ||
|
||||
ctx.rewriteQuery !== undefined ||
|
||||
ctx.filters !== undefined ||
|
||||
ctx.filter !== undefined ||
|
||||
ctx.include !== undefined ||
|
||||
ctx.searchMode !== undefined
|
||||
|
||||
|
|
@ -285,7 +271,7 @@ export const enhanceMessagesWithMemories = async (
|
|||
// Profile mode only fetches static/dynamic user data, not query-based search
|
||||
if (useAdvancedSearch && ctx.mode === "profile") {
|
||||
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.',
|
||||
)
|
||||
}
|
||||
|
|
@ -294,25 +280,25 @@ export const enhanceMessagesWithMemories = async (
|
|||
if (useAdvancedSearch && ctx.mode !== "profile") {
|
||||
ctx.logger.info("Using advanced search with custom parameters")
|
||||
|
||||
const searchParams: Supermemory.SearchParams = {
|
||||
q: queryText,
|
||||
containerTag: ctx.containerTag,
|
||||
// v5 defaults are hybrid/0.3; keep the v4 defaults unless the caller overrides them.
|
||||
const searchParams: SearchRequest = {
|
||||
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.rerank !== undefined) searchParams.rerank = ctx.rerank
|
||||
if (ctx.rewriteQuery !== undefined)
|
||||
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.searchMode !== undefined) searchParams.searchMode = ctx.searchMode
|
||||
|
||||
const [response, profileResponse] = await Promise.all([
|
||||
ctx.client.search(searchParams),
|
||||
ctx.client.search(ctx.namespace, searchParams),
|
||||
ctx.mode === "full"
|
||||
? supermemoryProfileSearch(
|
||||
ctx.containerTag,
|
||||
ctx.namespace,
|
||||
"",
|
||||
ctx.normalizedBaseUrl,
|
||||
ctx.apiKey,
|
||||
|
|
@ -379,7 +365,7 @@ export const enhanceMessagesWithMemories = async (
|
|||
}
|
||||
|
||||
return await buildMemoriesText({
|
||||
containerTag: ctx.containerTag,
|
||||
namespace: ctx.namespace,
|
||||
queryText,
|
||||
mode: ctx.mode,
|
||||
baseUrl: ctx.normalizedBaseUrl,
|
||||
|
|
@ -622,17 +608,18 @@ export const saveConversation = async (
|
|||
}
|
||||
|
||||
const response = await addConversation({
|
||||
conversationId: ctx.customId,
|
||||
id: ctx.id,
|
||||
messages: conversationMessages,
|
||||
containerTags: [ctx.containerTag],
|
||||
namespace: ctx.namespace,
|
||||
metadata: ctx.metadata,
|
||||
supportingContext: ctx.supportingContext,
|
||||
apiKey: ctx.apiKey,
|
||||
baseUrl: ctx.normalizedBaseUrl,
|
||||
})
|
||||
|
||||
ctx.logger.info("Conversation saved successfully via /v4/conversations", {
|
||||
containerTag: ctx.containerTag,
|
||||
customId: ctx.customId,
|
||||
ctx.logger.info("Conversation saved successfully", {
|
||||
namespace: ctx.namespace,
|
||||
id: ctx.id,
|
||||
messageCount: conversationMessages.length,
|
||||
responseId: response.id,
|
||||
metadata: ctx.metadata,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@
|
|||
* @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"
|
||||
|
||||
/**
|
||||
|
|
@ -14,10 +19,10 @@ import type { SupermemoryBaseOptions } from "../shared"
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
customId: string
|
||||
id: string
|
||||
|
||||
/**
|
||||
* Threshold / sensitivity for memory selection. 0 is least sensitive (returns
|
||||
|
|
@ -38,12 +43,11 @@ export interface SupermemoryVoltAgent extends SupermemoryBaseOptions {
|
|||
limit?: number
|
||||
|
||||
/**
|
||||
* If true, rerank the results based on the query. This helps ensure the most
|
||||
* relevant results are returned. Default: false
|
||||
* Post-retrieval ranking: "none", "order", or "aggregate". Default: "none"
|
||||
*
|
||||
* 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
|
||||
|
|
@ -54,12 +58,12 @@ export interface SupermemoryVoltAgent extends SupermemoryBaseOptions {
|
|||
rewriteQuery?: boolean
|
||||
|
||||
/**
|
||||
* Advanced filters to apply to the search using AND/OR logic.
|
||||
* Example: { OR: [{ key: "type", value: "note" }, { key: "type", value: "conversation" }] }
|
||||
* Typed metadata filter applied before ranking.
|
||||
* 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.
|
||||
*/
|
||||
filters?: SearchFilters
|
||||
filter?: SearchFilter
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* - "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)
|
||||
*
|
||||
* Note: Only effective when mode is "query" or "full". Ignored in "profile" mode.
|
||||
*/
|
||||
searchMode?: "memories" | "documents" | "hybrid"
|
||||
searchMode?: SearchRequestSearchMode
|
||||
|
||||
/**
|
||||
* @deprecated The conversations API does not accept per-request entity context.
|
||||
* Configure entity context on the container tag instead.
|
||||
*/
|
||||
entityContext?: string
|
||||
/** Context that guides memory extraction for saved conversations. Max 1500 characters. */
|
||||
supportingContext?: string
|
||||
}
|
||||
|
||||
/** Advanced search filters using AND/OR logic. */
|
||||
export type SearchFilters = NonNullable<Supermemory.SearchParams["filters"]>
|
||||
/** Typed v5 metadata filter expression. */
|
||||
export type SearchFilter = FilterExpression
|
||||
|
||||
/** Options for including additional data in search results. */
|
||||
export interface IncludeOptions {
|
||||
/** 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
|
||||
}
|
||||
export type IncludeOptions = SearchRequestInclude
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export type HookEndArgs = OnEndHookArgs
|
|||
|
||||
export type {
|
||||
IncludeOptions,
|
||||
SearchFilters,
|
||||
SearchFilter,
|
||||
SupermemoryVoltAgent,
|
||||
} from "./options"
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { withSupermemory } from "../src/ai-sdk"
|
|||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
const modelWithMemory = withSupermemory(openai("gpt-5"), {
|
||||
containerTag: "user_id_life",
|
||||
customId: "conversation-123",
|
||||
namespace: "user_id_life",
|
||||
id: "conversation-123",
|
||||
verbose: true,
|
||||
mode: "query", // options are profile, query, full (default is profile)
|
||||
addMemory: "always", // options are always, never (default is never)
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ async function chatWithMemoryTool() {
|
|||
|
||||
// Initialize memory tool
|
||||
const memoryTool = createClaudeMemoryTool(SUPERMEMORY_API_KEY, {
|
||||
projectId: "anthropic-sdk-demo",
|
||||
memoryContainerTag: "claude_memory_anthropic",
|
||||
namespace: "anthropic-sdk-demo",
|
||||
})
|
||||
|
||||
// Conversation messages
|
||||
|
|
@ -177,8 +176,7 @@ async function testMemoryOperations() {
|
|||
}
|
||||
|
||||
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, {
|
||||
projectId: "direct-test",
|
||||
memoryContainerTag: "claude_memory_direct",
|
||||
namespace: "direct-test",
|
||||
})
|
||||
|
||||
const testCases = [
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { gateway, streamText, type ModelMessage } from "ai"
|
|||
import { withSupermemory } from "@supermemory/tools/ai-sdk"
|
||||
|
||||
const model = withSupermemory(gateway("google/gemini-2.5-flash"), {
|
||||
containerTag: "user-1",
|
||||
customId: "chat-session",
|
||||
namespace: "user-1",
|
||||
id: "chat-session",
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY ?? "",
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ export async function POST(req: Request) {
|
|||
})
|
||||
|
||||
const openaiWithSupermemory = withSupermemory(openai, {
|
||||
containerTag: "user-123",
|
||||
customId: conversationId,
|
||||
namespace: "user-123",
|
||||
id: conversationId,
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
verbose: true,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ const SUPERMEMORY_USER_ID = "user-1"
|
|||
const gatewayModel = gateway("google/gemini-2.5-flash")
|
||||
|
||||
const supermemoryOptions = {
|
||||
containerTag: SUPERMEMORY_USER_ID,
|
||||
customId: "stream-session",
|
||||
namespace: SUPERMEMORY_USER_ID,
|
||||
id: "stream-session",
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY ?? "",
|
||||
mode: "full" as const,
|
||||
addMemory: "always" as const,
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ export async function directFetchExample() {
|
|||
|
||||
// Initialize the memory tool
|
||||
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
|
||||
projectId: "claude-memory-demo",
|
||||
memoryContainerTag: "claude_memory_demo",
|
||||
namespace: "claude-memory-demo",
|
||||
})
|
||||
|
||||
// Example memory commands that Claude might send
|
||||
|
|
@ -106,8 +105,7 @@ export async function anthropicSdkExample() {
|
|||
|
||||
// Initialize memory tool
|
||||
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
|
||||
projectId: "claude-chat-session",
|
||||
memoryContainerTag: "claude_memory_chat",
|
||||
namespace: "claude-chat-session",
|
||||
})
|
||||
|
||||
// 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!, {
|
||||
projectId: 'my-chat-app',
|
||||
memoryContainerTag: 'claude_memory'
|
||||
namespace: 'my-chat-app',
|
||||
});
|
||||
|
||||
// Memory tool definition for Claude
|
||||
|
|
@ -243,16 +240,15 @@ async function chatWithMemory(userMessage: string) {
|
|||
|
||||
export const curlExamples = `
|
||||
# Test the memory tool using cURL commands against your supermemory API
|
||||
|
||||
# 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 "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"content": "# My Notes\\n\\nThis is a test note for Claude memory tool.",
|
||||
"customId": "/memories/test-note.md",
|
||||
"containerTags": ["claude_memory", "sm_project_test"],
|
||||
"id": "memories_test-note_md",
|
||||
"metadata": {
|
||||
"source": "claude-memory",
|
||||
"claude_memory_type": "file",
|
||||
"file_path": "/memories/test-note.md",
|
||||
"line_count": 3,
|
||||
|
|
@ -260,35 +256,24 @@ curl -X POST "https://api.supermemory.ai/v3/documents" \\
|
|||
}
|
||||
}'
|
||||
|
||||
# 2. Search/read the memory file
|
||||
curl -X POST "https://api.supermemory.ai/v3/search" \\
|
||||
-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
|
||||
}'
|
||||
# 2. Read the memory file
|
||||
curl "https://api.supermemory.ai/ns/my-app/document/memories_test-note_md" \\
|
||||
-H "Authorization: Bearer YOUR_API_KEY"
|
||||
|
||||
# 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 "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"q": "*",
|
||||
"containerTags": ["claude_memory", "sm_project_test"],
|
||||
"limit": 100,
|
||||
"includeFullDocs": false
|
||||
}'
|
||||
-d '{"filter": {"field": "source", "operator": "eq", "value": "claude-memory"}}'
|
||||
|
||||
# 4. Update a memory file (str_replace operation)
|
||||
curl -X PATCH "https://api.supermemory.ai/v3/documents/DOCUMENT_ID" \\
|
||||
# 4. Replace a memory file (str_replace operation)
|
||||
curl -X PATCH "https://api.supermemory.ai/ns/my-app/document/memories_test-note_md" \\
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"content": "# My Updated Notes\\n\\nThis note has been updated using str_replace.",
|
||||
"metadata": {
|
||||
"source": "claude-memory",
|
||||
"claude_memory_type": "file",
|
||||
"file_path": "/memories/test-note.md",
|
||||
"line_count": 3,
|
||||
|
|
@ -297,8 +282,10 @@ curl -X PATCH "https://api.supermemory.ai/v3/documents/DOCUMENT_ID" \\
|
|||
}'
|
||||
|
||||
# 5. Delete a memory file
|
||||
curl -X DELETE "https://api.supermemory.ai/v3/documents/DOCUMENT_ID" \\
|
||||
-H "Authorization: Bearer YOUR_API_KEY"
|
||||
curl -X DELETE "https://api.supermemory.ai/ns/my-app/document" \\
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"ids": ["memories_test-note_md"]}'
|
||||
`
|
||||
|
||||
// =====================================================
|
||||
|
|
|
|||
|
|
@ -22,8 +22,7 @@ export async function handleClaudeMemoryToolCall(
|
|||
},
|
||||
supermemoryApiKey: string,
|
||||
config?: {
|
||||
projectId?: string
|
||||
memoryContainerTag?: string
|
||||
namespace?: string
|
||||
baseUrl?: string
|
||||
},
|
||||
) {
|
||||
|
|
@ -34,8 +33,7 @@ export async function handleClaudeMemoryToolCall(
|
|||
|
||||
// Initialize memory tool
|
||||
const memoryTool = createClaudeMemoryTool(supermemoryApiKey, {
|
||||
projectId: config?.projectId || "claude-chat",
|
||||
memoryContainerTag: config?.memoryContainerTag || "claude_memory",
|
||||
namespace: config?.namespace || "claude-chat",
|
||||
baseUrl: config?.baseUrl,
|
||||
})
|
||||
|
||||
|
|
@ -137,8 +135,7 @@ export async function realClaudeMemoryExample() {
|
|||
console.log(`Path: ${block.input.path}`)
|
||||
|
||||
return handleClaudeMemoryToolCall(block, SUPERMEMORY_API_KEY, {
|
||||
projectId: "python-scraper-help",
|
||||
memoryContainerTag: "claude_memory_debug",
|
||||
namespace: "python-scraper-help",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
|
@ -194,8 +191,7 @@ export async function processClaudeResponse(
|
|||
claudeResponseData: any,
|
||||
supermemoryApiKey: string,
|
||||
config?: {
|
||||
projectId?: string
|
||||
memoryContainerTag?: string
|
||||
namespace?: string
|
||||
baseUrl?: string
|
||||
},
|
||||
): Promise<any[]> {
|
||||
|
|
@ -266,8 +262,7 @@ app.post('/chat-with-memory', async (req, res) => {
|
|||
claudeData,
|
||||
process.env.SUPERMEMORY_API_KEY!,
|
||||
{
|
||||
projectId: conversationId || 'default-chat',
|
||||
memoryContainerTag: 'claude_memory_chat'
|
||||
namespace: conversationId || 'default-chat',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -320,8 +315,7 @@ export async function testWithRealToolCall() {
|
|||
realToolCall,
|
||||
process.env.SUPERMEMORY_API_KEY,
|
||||
{
|
||||
projectId: "python-scraper-debug",
|
||||
memoryContainerTag: "claude_memory_test",
|
||||
namespace: "python-scraper-debug",
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import "dotenv/config"
|
|||
const TEST_CONFIG = {
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY || "test-api-key",
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL,
|
||||
projectId: "test-claude-memory",
|
||||
memoryContainerTag: "claude_memory_test",
|
||||
namespace: "test-claude-memory",
|
||||
}
|
||||
|
||||
describe("Claude Memory Tool", () => {
|
||||
|
|
@ -15,8 +14,7 @@ describe("Claude Memory Tool", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
memoryTool = createClaudeMemoryTool(TEST_CONFIG.apiKey, {
|
||||
projectId: TEST_CONFIG.projectId,
|
||||
memoryContainerTag: TEST_CONFIG.memoryContainerTag,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
baseUrl: TEST_CONFIG.baseUrl,
|
||||
})
|
||||
})
|
||||
|
|
@ -346,8 +344,7 @@ async function runManualTests() {
|
|||
}
|
||||
|
||||
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, {
|
||||
projectId: "manual-test-project",
|
||||
memoryContainerTag: "claude_memory_manual_test",
|
||||
namespace: "manual-test-project",
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ import "dotenv/config"
|
|||
const INTEGRATION_CONFIG = {
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY || "",
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai",
|
||||
containerTag: "integration-test-mastra",
|
||||
customId: "integration-test-conversation",
|
||||
namespace: "integration-test-mastra",
|
||||
id: "integration-test-conversation",
|
||||
}
|
||||
|
||||
const shouldRunIntegration = !!process.env.SUPERMEMORY_API_KEY
|
||||
|
|
@ -103,8 +103,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
describe("SupermemoryInputProcessor", () => {
|
||||
it("should fetch real memories and inject into messageList", async () => {
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: INTEGRATION_CONFIG.customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: INTEGRATION_CONFIG.id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
|
|
@ -134,8 +134,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: INTEGRATION_CONFIG.customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: INTEGRATION_CONFIG.id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "query",
|
||||
|
|
@ -158,8 +158,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
await processor.processInput(args)
|
||||
|
||||
const profileCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
(call) => typeof call[0] === "string" && call[0].includes("/search"),
|
||||
)
|
||||
expect(profileCalls.length).toBeGreaterThan(0)
|
||||
|
||||
|
|
@ -168,7 +167,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const body = JSON.parse(
|
||||
(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()
|
||||
|
|
@ -178,8 +177,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: INTEGRATION_CONFIG.customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: INTEGRATION_CONFIG.id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "full",
|
||||
|
|
@ -197,8 +196,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
await processor.processInput(args)
|
||||
|
||||
const profileCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
(call) => typeof call[0] === "string" && call[0].includes("/search"),
|
||||
)
|
||||
expect(profileCalls.length).toBeGreaterThan(0)
|
||||
|
||||
|
|
@ -207,7 +205,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const body = JSON.parse(
|
||||
(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()
|
||||
|
|
@ -217,8 +215,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: INTEGRATION_CONFIG.customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: INTEGRATION_CONFIG.id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
|
|
@ -238,8 +236,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
|
||||
await processor.processInput(args1)
|
||||
const callsAfterFirst = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
(call) => typeof call[0] === "string" && call[0].includes("/profile"),
|
||||
).length
|
||||
|
||||
const args2: ProcessInputArgs = {
|
||||
|
|
@ -252,8 +249,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
|
||||
await processor.processInput(args2)
|
||||
const callsAfterSecond = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
(call) => typeof call[0] === "string" && call[0].includes("/profile"),
|
||||
).length
|
||||
|
||||
expect(callsAfterSecond).toBe(callsAfterFirst)
|
||||
|
|
@ -268,8 +264,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
}) => `<mastra-memories>${data.userMemories}</mastra-memories>`
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: INTEGRATION_CONFIG.customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: INTEGRATION_CONFIG.id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
|
|
@ -296,11 +292,11 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
it("should save conversation when addMemory is always", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const customId = `test-mastra-${Date.now()}`
|
||||
const id = `test-mastra-${Date.now()}`
|
||||
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
addMemory: "always",
|
||||
|
|
@ -320,8 +316,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
|
||||
const conversationCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" &&
|
||||
call[0].includes("/v4/conversations"),
|
||||
typeof call[0] === "string" && call[0].includes("/document"),
|
||||
)
|
||||
expect(conversationCalls.length).toBeGreaterThan(0)
|
||||
|
||||
|
|
@ -332,8 +327,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-thread",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-thread",
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
addMemory: "never",
|
||||
|
|
@ -353,8 +348,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
|
||||
const conversationCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" &&
|
||||
call[0].includes("/v4/conversations"),
|
||||
typeof call[0] === "string" && call[0].includes("/document"),
|
||||
)
|
||||
expect(conversationCalls.length).toBe(0)
|
||||
|
||||
|
|
@ -365,8 +359,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: INTEGRATION_CONFIG.customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: INTEGRATION_CONFIG.id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
addMemory: "always",
|
||||
|
|
@ -391,8 +385,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
|
||||
const conversationCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" &&
|
||||
call[0].includes("/v4/conversations"),
|
||||
typeof call[0] === "string" && call[0].includes("/document"),
|
||||
)
|
||||
expect(conversationCalls.length).toBeGreaterThan(0)
|
||||
|
||||
|
|
@ -403,8 +396,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
describe("createSupermemoryProcessors", () => {
|
||||
it("should create working input and output processors", async () => {
|
||||
const { input, output } = createSupermemoryProcessors({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `processors-test-${Date.now()}`,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: `processors-test-${Date.now()}`,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
|
|
@ -446,8 +439,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
}
|
||||
|
||||
const enhanced = withSupermemory(config, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `wrapper-test-${Date.now()}`,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: `wrapper-test-${Date.now()}`,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
|
|
@ -499,8 +492,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
}
|
||||
|
||||
const enhanced = withSupermemory(config, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: INTEGRATION_CONFIG.customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: INTEGRATION_CONFIG.id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
|
|
@ -520,8 +513,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
describe("Options", () => {
|
||||
it("verbose mode should not break functionality", async () => {
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: INTEGRATION_CONFIG.customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: INTEGRATION_CONFIG.id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
|
|
@ -546,8 +539,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: INTEGRATION_CONFIG.customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: INTEGRATION_CONFIG.id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
|
|
@ -564,8 +557,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
await processor.processInput(args)
|
||||
|
||||
const profileCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
(call) => typeof call[0] === "string" && call[0].includes("/profile"),
|
||||
)
|
||||
expect(profileCalls.length).toBeGreaterThan(0)
|
||||
|
||||
|
|
@ -579,8 +571,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
describe("Error handling", () => {
|
||||
it("should handle invalid API key gracefully", async () => {
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: INTEGRATION_CONFIG.customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: INTEGRATION_CONFIG.id,
|
||||
apiKey: "invalid-api-key-12345",
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
|
|
@ -602,8 +594,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
|
||||
it("output processor should handle save errors gracefully", async () => {
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "error-test",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "error-test",
|
||||
apiKey: "invalid-api-key-12345",
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
addMemory: "always",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@
|
|||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"
|
||||
import {
|
||||
addBody,
|
||||
errorResponse,
|
||||
jsonResponse,
|
||||
profileBody,
|
||||
requestJson,
|
||||
requestPath,
|
||||
} from "../v5-fetch"
|
||||
import {
|
||||
RequestContext,
|
||||
MASTRA_THREAD_ID_KEY,
|
||||
|
|
@ -28,8 +36,8 @@ import type {
|
|||
const TEST_CONFIG = {
|
||||
apiKey: "test-api-key",
|
||||
baseUrl: "https://api.supermemory.ai",
|
||||
containerTag: "test-mastra-user",
|
||||
customId: "test-conversation",
|
||||
namespace: "test-mastra-user",
|
||||
id: "test-conversation",
|
||||
}
|
||||
|
||||
interface MockAgentConfig {
|
||||
|
|
@ -86,25 +94,8 @@ const createMockMessageList = (): MessageList & {
|
|||
} as unknown as MessageList & { calls: { method: string; args: unknown[] }[] }
|
||||
}
|
||||
|
||||
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 })),
|
||||
},
|
||||
})
|
||||
|
||||
const createMockConversationResponse = () => ({
|
||||
id: "mem-123",
|
||||
conversationId: "conv-456",
|
||||
status: "created",
|
||||
})
|
||||
const createMockProfileResponse = profileBody
|
||||
const createMockConversationResponse = () => addBody("mem-123")
|
||||
|
||||
describe("SupermemoryInputProcessor", () => {
|
||||
let originalEnv: string | undefined
|
||||
|
|
@ -132,8 +123,8 @@ describe("SupermemoryInputProcessor", () => {
|
|||
describe("constructor", () => {
|
||||
it("should create processor with required options", () => {
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
})
|
||||
expect(processor.id).toBe("supermemory-input")
|
||||
expect(processor.name).toBe("Supermemory Memory Injection")
|
||||
|
|
@ -144,8 +135,8 @@ describe("SupermemoryInputProcessor", () => {
|
|||
|
||||
expect(() => {
|
||||
new SupermemoryInputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
})
|
||||
}).toThrow("SUPERMEMORY_API_KEY is not set")
|
||||
})
|
||||
|
|
@ -154,8 +145,8 @@ describe("SupermemoryInputProcessor", () => {
|
|||
delete process.env.SUPERMEMORY_API_KEY
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
apiKey: "custom-key",
|
||||
})
|
||||
expect(processor.id).toBe("supermemory-input")
|
||||
|
|
@ -164,20 +155,18 @@ describe("SupermemoryInputProcessor", () => {
|
|||
|
||||
describe("processInput", () => {
|
||||
it("should inject memories into messageList", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockProfileResponse(
|
||||
["User likes TypeScript"],
|
||||
["Recent interest in AI"],
|
||||
),
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(
|
||||
createMockProfileResponse(
|
||||
["User likes TypeScript"],
|
||||
["Recent interest in AI"],
|
||||
),
|
||||
})
|
||||
),
|
||||
)
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
|
@ -206,15 +195,13 @@ describe("SupermemoryInputProcessor", () => {
|
|||
})
|
||||
|
||||
it("should use cached memories on second call with same message", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(createMockProfileResponse(["Cached memory"])),
|
||||
})
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockProfileResponse(["Cached memory"])),
|
||||
)
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
|
@ -248,18 +235,16 @@ describe("SupermemoryInputProcessor", () => {
|
|||
let callCount = 0
|
||||
fetchMock.mockImplementation(() => {
|
||||
callCount++
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockProfileResponse([`Memory from call ${callCount}`]),
|
||||
),
|
||||
})
|
||||
return Promise.resolve(
|
||||
jsonResponse(
|
||||
createMockProfileResponse([`Memory from call ${callCount}`]),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "query",
|
||||
})
|
||||
|
|
@ -289,8 +274,8 @@ describe("SupermemoryInputProcessor", () => {
|
|||
|
||||
it("should return messageList in query mode when no user message", async () => {
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "query",
|
||||
})
|
||||
|
|
@ -312,16 +297,11 @@ describe("SupermemoryInputProcessor", () => {
|
|||
})
|
||||
|
||||
it("should handle API errors gracefully", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
text: () => Promise.resolve("Server error"),
|
||||
})
|
||||
fetchMock.mockImplementation(async () => errorResponse(500))
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
|
@ -342,14 +322,13 @@ describe("SupermemoryInputProcessor", () => {
|
|||
})
|
||||
|
||||
it("should use threadId from options", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
|
||||
})
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockProfileResponse(["Memory"])),
|
||||
)
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "thread-123",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "thread-123",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
|
@ -368,14 +347,13 @@ describe("SupermemoryInputProcessor", () => {
|
|||
})
|
||||
|
||||
it("should use threadId from requestContext when not in options", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
|
||||
})
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockProfileResponse(["Memory"])),
|
||||
)
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
|
@ -398,14 +376,13 @@ describe("SupermemoryInputProcessor", () => {
|
|||
})
|
||||
|
||||
it("should handle messages with array content parts", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
|
||||
})
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockProfileResponse(["Memory"])),
|
||||
)
|
||||
|
||||
const processor = new SupermemoryInputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "query",
|
||||
})
|
||||
|
|
@ -467,8 +444,8 @@ describe("SupermemoryOutputProcessor", () => {
|
|||
describe("constructor", () => {
|
||||
it("should create processor with required options", () => {
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
})
|
||||
expect(processor.id).toBe("supermemory-output")
|
||||
expect(processor.name).toBe("Supermemory Conversation Save")
|
||||
|
|
@ -477,14 +454,13 @@ describe("SupermemoryOutputProcessor", () => {
|
|||
|
||||
describe("processOutputResult", () => {
|
||||
it("should save conversation when addMemory is always", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockConversationResponse()),
|
||||
})
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockConversationResponse()),
|
||||
)
|
||||
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "conv-456",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "conv-456",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
})
|
||||
|
|
@ -504,29 +480,23 @@ describe("SupermemoryOutputProcessor", () => {
|
|||
await processor.processOutputResult(args)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/v4/conversations"),
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.objectContaining({
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${TEST_CONFIG.apiKey}`,
|
||||
}),
|
||||
}),
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||||
expect(requestPath(url)).toBe(`/ns/${TEST_CONFIG.namespace}/document`)
|
||||
expect(init.method).toBe("POST")
|
||||
expect(new Headers(init.headers).get("authorization")).toBe(
|
||||
`Bearer ${TEST_CONFIG.apiKey}`,
|
||||
)
|
||||
|
||||
const callBody = JSON.parse(
|
||||
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
||||
)
|
||||
expect(callBody.conversationId).toBe("conv-456")
|
||||
expect(callBody.messages).toHaveLength(2)
|
||||
expect(callBody.containerTags).toContain(TEST_CONFIG.containerTag)
|
||||
const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
|
||||
expect(callBody.id).toBe("conv-456")
|
||||
expect(String(callBody.content).split("\n\n")).toHaveLength(2)
|
||||
expect(callBody.dreaming).toBe("instant")
|
||||
})
|
||||
|
||||
it("should not save conversation when addMemory is never", async () => {
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "conv-456",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "conv-456",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "never",
|
||||
})
|
||||
|
|
@ -546,15 +516,14 @@ describe("SupermemoryOutputProcessor", () => {
|
|||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should use customId from options for conversation save", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockConversationResponse()),
|
||||
})
|
||||
it("should use id from options for conversation save", async () => {
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockConversationResponse()),
|
||||
)
|
||||
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "my-custom-id",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "my-custom-id",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
})
|
||||
|
|
@ -572,21 +541,18 @@ describe("SupermemoryOutputProcessor", () => {
|
|||
await processor.processOutputResult(args)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const callBody = JSON.parse(
|
||||
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
||||
)
|
||||
expect(callBody.conversationId).toBe("my-custom-id")
|
||||
const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
|
||||
expect(callBody.id).toBe("my-custom-id")
|
||||
})
|
||||
|
||||
it("should use threadId from requestContext (takes precedence over customId)", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockConversationResponse()),
|
||||
})
|
||||
it("should use threadId from requestContext (takes precedence over id)", async () => {
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockConversationResponse()),
|
||||
)
|
||||
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "fallback-custom-id",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "fallback-custom-id",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
})
|
||||
|
|
@ -608,22 +574,19 @@ describe("SupermemoryOutputProcessor", () => {
|
|||
await processor.processOutputResult(args)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const callBody = JSON.parse(
|
||||
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
||||
)
|
||||
const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
|
||||
// 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 () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockConversationResponse()),
|
||||
})
|
||||
it("should fall back to id when requestContext has no threadId", async () => {
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockConversationResponse()),
|
||||
)
|
||||
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "fallback-custom-id",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "fallback-custom-id",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
})
|
||||
|
|
@ -641,22 +604,19 @@ describe("SupermemoryOutputProcessor", () => {
|
|||
await processor.processOutputResult(args)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const callBody = JSON.parse(
|
||||
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
||||
)
|
||||
// Falls back to customId when no RequestContext threadId
|
||||
expect(callBody.conversationId).toBe("fallback-custom-id")
|
||||
const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
|
||||
// Falls back to id when no RequestContext threadId
|
||||
expect(callBody.id).toBe("fallback-custom-id")
|
||||
})
|
||||
|
||||
it("should skip system messages when saving", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockConversationResponse()),
|
||||
})
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockConversationResponse()),
|
||||
)
|
||||
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "conv-456",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "conv-456",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
})
|
||||
|
|
@ -676,24 +636,19 @@ describe("SupermemoryOutputProcessor", () => {
|
|||
|
||||
await processor.processOutputResult(args)
|
||||
|
||||
const callBody = JSON.parse(
|
||||
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
||||
)
|
||||
expect(callBody.messages).toHaveLength(2)
|
||||
expect(
|
||||
callBody.messages.every((m: { role: string }) => m.role !== "system"),
|
||||
).toBe(true)
|
||||
const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
|
||||
expect(String(callBody.content).split("\n\n")).toHaveLength(2)
|
||||
expect(callBody.content).not.toContain("System:")
|
||||
})
|
||||
|
||||
it("should handle messages with array content parts", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockConversationResponse()),
|
||||
})
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockConversationResponse()),
|
||||
)
|
||||
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "conv-456",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "conv-456",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
})
|
||||
|
|
@ -731,23 +686,16 @@ describe("SupermemoryOutputProcessor", () => {
|
|||
|
||||
await processor.processOutputResult(args)
|
||||
|
||||
const callBody = JSON.parse(
|
||||
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
||||
)
|
||||
expect(callBody.messages).toHaveLength(2)
|
||||
const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
|
||||
expect(String(callBody.content).split("\n\n")).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should handle save errors gracefully", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
text: () => Promise.resolve("Server error"),
|
||||
})
|
||||
fetchMock.mockImplementation(async () => errorResponse(500))
|
||||
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "conv-456",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "conv-456",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
})
|
||||
|
|
@ -768,8 +716,8 @@ describe("SupermemoryOutputProcessor", () => {
|
|||
|
||||
it("should not save when no messages to save", async () => {
|
||||
const processor = new SupermemoryOutputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "conv-456",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "conv-456",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
})
|
||||
|
|
@ -807,8 +755,8 @@ describe("Factory functions", () => {
|
|||
describe("createSupermemoryProcessor", () => {
|
||||
it("should create input processor", () => {
|
||||
const processor = createSupermemoryProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
})
|
||||
expect(processor).toBeInstanceOf(SupermemoryInputProcessor)
|
||||
expect(processor.id).toBe("supermemory-input")
|
||||
|
|
@ -816,8 +764,8 @@ describe("Factory functions", () => {
|
|||
|
||||
it("should pass options to processor", () => {
|
||||
const processor = createSupermemoryProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
apiKey: "custom-key",
|
||||
mode: "full",
|
||||
})
|
||||
|
|
@ -828,8 +776,8 @@ describe("Factory functions", () => {
|
|||
describe("createSupermemoryOutputProcessor", () => {
|
||||
it("should create output processor", () => {
|
||||
const processor = createSupermemoryOutputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
})
|
||||
expect(processor).toBeInstanceOf(SupermemoryOutputProcessor)
|
||||
expect(processor.id).toBe("supermemory-output")
|
||||
|
|
@ -837,8 +785,8 @@ describe("Factory functions", () => {
|
|||
|
||||
it("should pass options to processor", () => {
|
||||
const processor = createSupermemoryOutputProcessor({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "conv-123",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "conv-123",
|
||||
apiKey: "custom-key",
|
||||
addMemory: "always",
|
||||
})
|
||||
|
|
@ -849,8 +797,8 @@ describe("Factory functions", () => {
|
|||
describe("createSupermemoryProcessors", () => {
|
||||
it("should create both input and output processors", () => {
|
||||
const { input, output } = createSupermemoryProcessors({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
})
|
||||
expect(input).toBeInstanceOf(SupermemoryInputProcessor)
|
||||
expect(output).toBeInstanceOf(SupermemoryOutputProcessor)
|
||||
|
|
@ -858,8 +806,8 @@ describe("Factory functions", () => {
|
|||
|
||||
it("should share options between processors", () => {
|
||||
const { input, output } = createSupermemoryProcessors({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "conv-123",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "conv-123",
|
||||
apiKey: "custom-key",
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
|
|
@ -894,8 +842,8 @@ describe("withSupermemory", () => {
|
|||
|
||||
expect(() => {
|
||||
withSupermemory(config, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
})
|
||||
}).toThrow("SUPERMEMORY_API_KEY is not set")
|
||||
})
|
||||
|
|
@ -905,8 +853,8 @@ describe("withSupermemory", () => {
|
|||
|
||||
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
|
||||
const enhanced = withSupermemory(config, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
apiKey: "custom-key",
|
||||
})
|
||||
|
||||
|
|
@ -919,8 +867,8 @@ describe("withSupermemory", () => {
|
|||
it("should inject input and output processors", () => {
|
||||
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
|
||||
const enhanced = withSupermemory(config, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
})
|
||||
|
||||
expect(enhanced.inputProcessors).toHaveLength(1)
|
||||
|
|
@ -937,8 +885,8 @@ describe("withSupermemory", () => {
|
|||
customProp: "value",
|
||||
}
|
||||
const enhanced = withSupermemory(config, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
})
|
||||
|
||||
expect(enhanced.id).toBe("test-agent")
|
||||
|
|
@ -959,8 +907,8 @@ describe("withSupermemory", () => {
|
|||
}
|
||||
|
||||
const enhanced = withSupermemory(config, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
})
|
||||
|
||||
expect(enhanced.inputProcessors).toHaveLength(2)
|
||||
|
|
@ -980,8 +928,8 @@ describe("withSupermemory", () => {
|
|||
}
|
||||
|
||||
const enhanced = withSupermemory(config, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
})
|
||||
|
||||
expect(enhanced.outputProcessors).toHaveLength(2)
|
||||
|
|
@ -1000,8 +948,8 @@ describe("withSupermemory", () => {
|
|||
}
|
||||
|
||||
const enhanced = withSupermemory(config, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: TEST_CONFIG.customId,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: TEST_CONFIG.id,
|
||||
})
|
||||
|
||||
expect(enhanced.inputProcessors).toHaveLength(2)
|
||||
|
|
@ -1017,8 +965,8 @@ describe("withSupermemory", () => {
|
|||
it("should pass options to processors", () => {
|
||||
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
|
||||
const enhanced = withSupermemory(config, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "conv-123",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "conv-123",
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
verbose: true,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type OpenAI from "openai"
|
||||
import { jsonResponse, profileBody } from "./v5-fetch"
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { withSupermemory } from "../src/openai"
|
||||
|
||||
|
|
@ -18,13 +19,7 @@ describe("OpenAI middleware memory context", () => {
|
|||
it("replaces prior SDK context in chat system messages", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
profile: { static: [{ memory: "Fresh profile fact" }], dynamic: [] },
|
||||
searchResults: { results: [] },
|
||||
}),
|
||||
}),
|
||||
vi.fn(async () => jsonResponse(profileBody(["Fresh profile fact"]))),
|
||||
)
|
||||
const originalCreate = vi.fn(() =>
|
||||
Object.assign(Promise.resolve({ choices: [] }), {
|
||||
|
|
@ -35,8 +30,8 @@ describe("OpenAI middleware memory context", () => {
|
|||
chat: { completions: { create: originalCreate } },
|
||||
} as unknown as OpenAI
|
||||
const wrapped = withSupermemory(client, {
|
||||
containerTag: "user-a",
|
||||
customId: "conversation-a",
|
||||
namespace: "user-a",
|
||||
id: "conversation-a",
|
||||
mode: "profile",
|
||||
addMemory: "never",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ const openai = new OpenAI({
|
|||
})
|
||||
|
||||
const openaiWithSupermemory = withSupermemory(openai, {
|
||||
containerTag: "user_id_life",
|
||||
customId: "test-conversation",
|
||||
namespace: "user_id_life",
|
||||
id: "test-conversation",
|
||||
verbose: true,
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ async function testMemoryTool() {
|
|||
}
|
||||
|
||||
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, {
|
||||
projectId: "memory-tool-test",
|
||||
memoryContainerTag: "claude_memory_test",
|
||||
namespace: "memory-tool-test",
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL,
|
||||
})
|
||||
|
||||
|
|
|
|||
66
packages/tools/test/v5-fetch.ts
Normal file
66
packages/tools/test/v5-fetch.ts
Normal 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}`)
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { jsonResponse, profileBody } from "./v5-fetch"
|
||||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import { createSupermemoryHooks } from "../src/voltagent"
|
||||
|
||||
|
|
@ -7,16 +8,10 @@ describe("VoltAgent memory context", () => {
|
|||
it("replaces prior SDK context in the prepared system message", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
profile: { static: [{ memory: "Fresh profile fact" }], dynamic: [] },
|
||||
searchResults: { results: [] },
|
||||
}),
|
||||
}),
|
||||
vi.fn(async () => jsonResponse(profileBody(["Fresh profile fact"]))),
|
||||
)
|
||||
const hooks = createSupermemoryHooks("user-a", {
|
||||
customId: "conversation-a",
|
||||
id: "conversation-a",
|
||||
apiKey: "sm_test_key",
|
||||
mode: "profile",
|
||||
addMemory: "never",
|
||||
|
|
|
|||
|
|
@ -1,43 +1,36 @@
|
|||
import type Supermemory from "supermemory"
|
||||
import type { Supermemory } from "supermemory"
|
||||
import type {
|
||||
LanguageModelV2CallOptions,
|
||||
LanguageModelV2Message,
|
||||
} 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 { 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 (
|
||||
params: LanguageModelV2CallOptions,
|
||||
assistantResponseText: string,
|
||||
includeToolCalls = true,
|
||||
) => {
|
||||
let messages: unknown[] | undefined
|
||||
|
||||
const fetchStub: typeof fetch = Object.assign(
|
||||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = typeof input === "string" ? input : input.toString()
|
||||
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 },
|
||||
)
|
||||
let captured: AddConversationParams | undefined
|
||||
addConversationMock.mockImplementation(
|
||||
async (conversation: AddConversationParams) => {
|
||||
captured = conversation
|
||||
return { id: "conversation-id", status: "queued" }
|
||||
},
|
||||
{ preconnect: originalFetch.preconnect },
|
||||
)
|
||||
|
||||
globalThis.fetch = fetchStub
|
||||
|
||||
await saveMemoryAfterResponse(
|
||||
{} as Supermemory,
|
||||
"user-id",
|
||||
|
|
@ -50,11 +43,47 @@ const persistMessages = async (
|
|||
includeToolCalls,
|
||||
)
|
||||
|
||||
return messages
|
||||
expect(captured?.namespace).toBe("user-id")
|
||||
expect(captured?.id).toBe("conversation-id")
|
||||
return captured?.messages
|
||||
}
|
||||
|
||||
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", () => {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import "dotenv/config"
|
|||
const INTEGRATION_CONFIG = {
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY || "",
|
||||
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
|
||||
|
|
@ -97,8 +97,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
createIntegrationMockModel()
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-generate",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-generate",
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
|
@ -123,11 +123,11 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const { model } = createIntegrationMockModel()
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const customId = `test-generate-${Date.now()}`
|
||||
const id = `test-generate-${Date.now()}`
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
addMemory: "always",
|
||||
|
|
@ -150,26 +150,25 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
// Wait for background save to complete
|
||||
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(
|
||||
(call) =>
|
||||
typeof call[0] === "string" &&
|
||||
call[0].includes("/v4/conversations"),
|
||||
typeof call[0] === "string" && call[0].includes("/document"),
|
||||
)
|
||||
expect(conversationCalls.length).toBeGreaterThan(0)
|
||||
|
||||
fetchSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should work with customId for grouped memories", async () => {
|
||||
it("should work with id for grouped memories", async () => {
|
||||
const { model, getCapturedGenerateParams } =
|
||||
createIntegrationMockModel()
|
||||
|
||||
const customId = `test-conversation-${Date.now()}`
|
||||
const id = `test-conversation-${Date.now()}`
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
|
@ -196,8 +195,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const { model, getCapturedStreamParams } = createIntegrationMockModel()
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-stream",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-stream",
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
|
@ -230,11 +229,11 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const { model } = createIntegrationMockModel()
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const customId = `test-stream-${Date.now()}`
|
||||
const id = `test-stream-${Date.now()}`
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId,
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
addMemory: "always",
|
||||
|
|
@ -262,8 +261,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
// Verify save was attempted
|
||||
const conversationCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" &&
|
||||
call[0].includes("/v4/conversations"),
|
||||
typeof call[0] === "string" && call[0].includes("/document"),
|
||||
)
|
||||
expect(conversationCalls.length).toBeGreaterThan(0)
|
||||
|
||||
|
|
@ -274,8 +272,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const { model } = createIntegrationMockModel()
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-chunks",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-chunks",
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
|
@ -313,8 +311,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-profile",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-profile",
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
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(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
(call) => typeof call[0] === "string" && call[0].includes("/profile"),
|
||||
)
|
||||
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]
|
||||
if (profileCall?.[1]) {
|
||||
const body = JSON.parse(
|
||||
(profileCall[1] as RequestInit).body as string,
|
||||
)
|
||||
expect(body.q).toBeUndefined()
|
||||
expect(body.query).toBeUndefined()
|
||||
}
|
||||
|
||||
fetchSpy.mockRestore()
|
||||
|
|
@ -352,8 +349,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-query",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-query",
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
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(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
(call) => typeof call[0] === "string" && call[0].includes("/search"),
|
||||
)
|
||||
expect(profileCalls.length).toBeGreaterThan(0)
|
||||
|
||||
// Verify the request body contains 'q'
|
||||
// Verify the request body contains the query
|
||||
const profileCall = profileCalls[0]
|
||||
if (profileCall?.[1]) {
|
||||
const body = JSON.parse(
|
||||
(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()
|
||||
|
|
@ -391,8 +387,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-full",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-full",
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
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(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
(call) => typeof call[0] === "string" && call[0].includes("/search"),
|
||||
)
|
||||
expect(profileCalls.length).toBeGreaterThan(0)
|
||||
|
||||
|
|
@ -418,7 +413,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const body = JSON.parse(
|
||||
(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()
|
||||
|
|
@ -436,8 +431,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
}) => `<custom-memories>${data.userMemories}</custom-memories>`
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-template",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-template",
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
promptTemplate: customTemplate,
|
||||
|
|
@ -463,8 +458,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
createIntegrationMockModel()
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-verbose",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-verbose",
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
verbose: true,
|
||||
|
|
@ -490,8 +485,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
|
||||
// Use the configured base URL (or default)
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-baseurl",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-baseurl",
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
|
|
@ -508,8 +503,7 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
|
||||
// Verify the correct base URL was used
|
||||
const profileCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
(call) => typeof call[0] === "string" && call[0].includes("/profile"),
|
||||
)
|
||||
expect(profileCalls.length).toBeGreaterThan(0)
|
||||
|
||||
|
|
@ -530,8 +524,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
)
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-error",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-error",
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
|
@ -553,8 +547,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
createIntegrationMockModel()
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-invalid-key",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-invalid-key",
|
||||
apiKey: "invalid-api-key-12345",
|
||||
mode: "profile",
|
||||
})
|
||||
|
|
@ -576,8 +570,8 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const { model } = createIntegrationMockModel()
|
||||
|
||||
const wrapped = withSupermemory(model, {
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: "test-invalid-strict",
|
||||
namespace: INTEGRATION_CONFIG.namespace,
|
||||
id: "test-invalid-strict",
|
||||
apiKey: "invalid-api-key-12345",
|
||||
mode: "profile",
|
||||
skipMemoryOnError: false,
|
||||
|
|
|
|||
|
|
@ -14,12 +14,18 @@ import type {
|
|||
LanguageModelV2Message,
|
||||
} from "@ai-sdk/provider"
|
||||
import "dotenv/config"
|
||||
import {
|
||||
errorResponse,
|
||||
jsonResponse,
|
||||
profileBody,
|
||||
requestPath,
|
||||
} from "../v5-fetch"
|
||||
|
||||
// Test configuration
|
||||
const TEST_CONFIG = {
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY || "test-api-key",
|
||||
baseURL: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai",
|
||||
containerTag: "test-vercel-wrapper",
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai",
|
||||
namespace: "test-vercel-wrapper",
|
||||
}
|
||||
|
||||
// Mock language model for testing
|
||||
|
|
@ -32,20 +38,7 @@ const createMockLanguageModel = (): LanguageModelV2 => ({
|
|||
doStream: vi.fn(),
|
||||
})
|
||||
|
||||
// Mock profile API response
|
||||
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 })),
|
||||
},
|
||||
})
|
||||
const createMockProfileResponse = profileBody
|
||||
|
||||
describe("Unit: withSupermemory", () => {
|
||||
let originalEnv: string | undefined
|
||||
|
|
@ -74,31 +67,31 @@ describe("Unit: withSupermemory", () => {
|
|||
|
||||
expect(() => {
|
||||
withSupermemory(mockModel, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "test-id",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "test-id",
|
||||
})
|
||||
}).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"
|
||||
|
||||
const mockModel = createMockLanguageModel()
|
||||
|
||||
// omitted customId (plain JS caller)
|
||||
// omitted id (plain JS caller)
|
||||
expect(() => {
|
||||
withSupermemory(mockModel, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
} as any)
|
||||
}).toThrow("customId is required")
|
||||
}).toThrow("id is required")
|
||||
|
||||
// empty string
|
||||
expect(() => {
|
||||
withSupermemory(mockModel, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "",
|
||||
})
|
||||
}).toThrow("customId is required")
|
||||
}).toThrow("id is required")
|
||||
})
|
||||
|
||||
it("should successfully create wrapped model with valid API key", () => {
|
||||
|
|
@ -106,8 +99,8 @@ describe("Unit: withSupermemory", () => {
|
|||
|
||||
const mockModel = createMockLanguageModel()
|
||||
const wrappedModel = withSupermemory(mockModel, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "test-id",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "test-id",
|
||||
})
|
||||
|
||||
expect(wrappedModel).toBeDefined()
|
||||
|
|
@ -127,8 +120,8 @@ describe("Unit: withSupermemory", () => {
|
|||
}
|
||||
const inner = Object.create(proto) as LanguageModelV2
|
||||
const wrappedModel = withSupermemory(inner, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "test-id",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "test-id",
|
||||
})
|
||||
|
||||
expect(wrappedModel.specificationVersion).toBe("v2")
|
||||
|
|
@ -146,16 +139,14 @@ describe("Unit: withSupermemory", () => {
|
|||
})
|
||||
|
||||
it("should cache memories on first call (new turn)", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(createMockProfileResponse(["Cached memory"])),
|
||||
})
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockProfileResponse(["Cached memory"])),
|
||||
)
|
||||
|
||||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-id",
|
||||
id: "test-id",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
|
|
@ -171,7 +162,7 @@ describe("Unit: withSupermemory", () => {
|
|||
await transformParamsWithMemory(params, ctx)
|
||||
|
||||
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)
|
||||
expect(cachedMemories).toBeDefined()
|
||||
expect(cachedMemories).toContain("Cached memory")
|
||||
|
|
@ -179,16 +170,14 @@ describe("Unit: withSupermemory", () => {
|
|||
})
|
||||
|
||||
it("should use cached memories on continuation step (no new fetch)", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(createMockProfileResponse(["Cached memory"])),
|
||||
})
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockProfileResponse(["Cached memory"])),
|
||||
)
|
||||
|
||||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-id",
|
||||
id: "test-id",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
|
|
@ -249,19 +238,17 @@ describe("Unit: withSupermemory", () => {
|
|||
let callCount = 0
|
||||
fetchMock.mockImplementation(() => {
|
||||
callCount++
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockProfileResponse([`Memory from call ${callCount}`]),
|
||||
),
|
||||
})
|
||||
return Promise.resolve(
|
||||
jsonResponse(
|
||||
createMockProfileResponse([`Memory from call ${callCount}`]),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-id",
|
||||
id: "test-id",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
|
|
@ -303,11 +290,9 @@ describe("Unit: withSupermemory", () => {
|
|||
})
|
||||
|
||||
it("replaces the prior SDK memory block instead of accumulating context", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(createMockProfileResponse(["Fresh profile fact"])),
|
||||
})
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockProfileResponse(["Fresh profile fact"])),
|
||||
)
|
||||
|
||||
const inner = createMockLanguageModel()
|
||||
vi.mocked(inner.doGenerate).mockResolvedValue({
|
||||
|
|
@ -317,8 +302,8 @@ describe("Unit: withSupermemory", () => {
|
|||
warnings: [],
|
||||
})
|
||||
const wrapped = withSupermemory(inner, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "conversation-a",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "conversation-a",
|
||||
mode: "profile",
|
||||
addMemory: "never",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
|
|
@ -353,18 +338,15 @@ describe("Unit: withSupermemory", () => {
|
|||
})
|
||||
|
||||
it("keeps concurrent user contexts isolated", async () => {
|
||||
fetchMock.mockImplementation(async (_url, init) => {
|
||||
const body = JSON.parse(String(init?.body ?? "{}"))
|
||||
return {
|
||||
ok: true,
|
||||
json: async () =>
|
||||
createMockProfileResponse([
|
||||
body.containerTag === "user-a"
|
||||
? "Fact for Alice"
|
||||
: "Fact for Bob",
|
||||
]),
|
||||
}
|
||||
})
|
||||
fetchMock.mockImplementation(async (url) =>
|
||||
jsonResponse(
|
||||
createMockProfileResponse([
|
||||
requestPath(url).includes("/ns/user-a/")
|
||||
? "Fact for Alice"
|
||||
: "Fact for Bob",
|
||||
]),
|
||||
),
|
||||
)
|
||||
const innerA = createMockLanguageModel()
|
||||
const innerB = createMockLanguageModel()
|
||||
vi.mocked(innerA.doGenerate).mockResolvedValue({
|
||||
|
|
@ -380,14 +362,14 @@ describe("Unit: withSupermemory", () => {
|
|||
warnings: [],
|
||||
})
|
||||
const wrappedA = withSupermemory(innerA, {
|
||||
containerTag: "user-a",
|
||||
customId: "conversation-a",
|
||||
namespace: "user-a",
|
||||
id: "conversation-a",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "never",
|
||||
})
|
||||
const wrappedB = withSupermemory(innerB, {
|
||||
containerTag: "user-b",
|
||||
customId: "conversation-b",
|
||||
namespace: "user-b",
|
||||
id: "conversation-b",
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "never",
|
||||
})
|
||||
|
|
@ -427,17 +409,12 @@ describe("Unit: withSupermemory", () => {
|
|||
})
|
||||
|
||||
it("should handle API errors gracefully", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
text: () => Promise.resolve("Server error"),
|
||||
})
|
||||
fetchMock.mockImplementation(async () => errorResponse(500))
|
||||
|
||||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-id",
|
||||
id: "test-id",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
|
|
@ -457,9 +434,9 @@ describe("Unit: withSupermemory", () => {
|
|||
|
||||
it("should handle empty prompt array", async () => {
|
||||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-id",
|
||||
id: "test-id",
|
||||
mode: "query",
|
||||
})
|
||||
|
||||
|
|
@ -475,9 +452,9 @@ describe("Unit: withSupermemory", () => {
|
|||
|
||||
it("should handle user message with empty content array in query mode", async () => {
|
||||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-id",
|
||||
id: "test-id",
|
||||
mode: "query",
|
||||
})
|
||||
|
||||
|
|
@ -497,15 +474,14 @@ describe("Unit: withSupermemory", () => {
|
|||
})
|
||||
|
||||
it("should not mutate the original params.prompt array", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
|
||||
})
|
||||
fetchMock.mockImplementation(async () =>
|
||||
jsonResponse(createMockProfileResponse(["Memory"])),
|
||||
)
|
||||
|
||||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-id",
|
||||
id: "test-id",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
|
|
@ -547,12 +523,7 @@ describe("Unit: withSupermemory", () => {
|
|||
})
|
||||
|
||||
it("continues without memories when profile fetch fails (default skip)", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
text: () => Promise.resolve("err"),
|
||||
})
|
||||
fetchMock.mockImplementation(async () => errorResponse(500))
|
||||
|
||||
const inner = createMockLanguageModel()
|
||||
vi.mocked(inner.doGenerate).mockResolvedValue({
|
||||
|
|
@ -567,8 +538,8 @@ describe("Unit: withSupermemory", () => {
|
|||
})
|
||||
|
||||
const wrapped = withSupermemory(inner, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "test-id",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "test-id",
|
||||
apiKey: "k",
|
||||
})
|
||||
|
||||
|
|
@ -582,17 +553,12 @@ describe("Unit: withSupermemory", () => {
|
|||
})
|
||||
|
||||
it("throws when skipMemoryOnError is false and profile fetch fails", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
text: () => Promise.resolve("err"),
|
||||
})
|
||||
fetchMock.mockImplementation(async () => errorResponse(500))
|
||||
|
||||
const inner = createMockLanguageModel()
|
||||
const wrapped = withSupermemory(inner, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "test-id",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "test-id",
|
||||
apiKey: "k",
|
||||
skipMemoryOnError: false,
|
||||
})
|
||||
|
|
@ -632,8 +598,8 @@ describe("Unit: withSupermemory", () => {
|
|||
})
|
||||
|
||||
const wrapped = withSupermemory(inner, {
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "test-id",
|
||||
namespace: TEST_CONFIG.namespace,
|
||||
id: "test-id",
|
||||
apiKey: "k",
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ Supermemory supports two complementary integration styles:
|
|||
|
||||
| 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) |
|
||||
|
||||
**Tools (7 canonical operations):**
|
||||
|
|
@ -179,7 +179,6 @@ With multiple configured container tags, `searchMemories`, `getProfile`, and `me
|
|||
**TypeScript (Vercel AI SDK):**
|
||||
```typescript
|
||||
import { supermemoryTools } from "@supermemory/tools/ai-sdk"
|
||||
// or re-exported from "@supermemory/ai-sdk"
|
||||
|
||||
const tools = supermemoryTools(process.env.SUPERMEMORY_API_KEY!, {
|
||||
containerTags: ["user_123"],
|
||||
|
|
|
|||
|
|
@ -458,7 +458,7 @@ await client.add({
|
|||
|
||||
### 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:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue