Merge branch 'main' into 04-03-refactor_mastra_to_object-based_api_v2.0.0_

This commit is contained in:
Dhravya Shah 2026-04-16 18:45:57 -07:00 committed by GitHub
commit de31895f61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 4794 additions and 215 deletions

View file

@ -0,0 +1,37 @@
name: Publish Agent Framework Python
on:
push:
branches:
- main
paths:
- "packages/agent-framework-python/pyproject.toml"
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: ./packages/agent-framework-python
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install build dependencies
run: pip install hatchling build
- name: Build package
run: python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: packages/agent-framework-python/dist/

View file

@ -0,0 +1,37 @@
name: Publish Cartesia SDK Python
on:
push:
branches:
- main
paths:
- "packages/cartesia-sdk-python/pyproject.toml"
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: ./packages/cartesia-sdk-python
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install build dependencies
run: pip install hatchling build
- name: Build package
run: python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: packages/cartesia-sdk-python/dist/

View file

@ -0,0 +1,44 @@
name: Publish Memory Graph
on:
push:
branches:
- main
paths:
- "packages/memory-graph/package.json"
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: ./packages/memory-graph
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
registry-url: 'https://registry.npmjs.org'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: bun install
- name: Build
run: bun run build
- name: Publish
run: pnpm publish --access public --verbose
env:
NPM_CONFIG_PROVENANCE: true
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -0,0 +1,37 @@
name: Publish OpenAI SDK Python
on:
push:
branches:
- main
paths:
- "packages/openai-sdk-python/pyproject.toml"
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: ./packages/openai-sdk-python
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install build dependencies
run: pip install hatchling build
- name: Build package
run: python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: packages/openai-sdk-python/dist/

View file

@ -0,0 +1,37 @@
name: Publish Pipecat SDK Python
on:
push:
branches:
- main
paths:
- "packages/pipecat-sdk-python/pyproject.toml"
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: ./packages/pipecat-sdk-python
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install build dependencies
run: pip install hatchling build
- name: Build package
run: python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: packages/pipecat-sdk-python/dist/

44
.github/workflows/publish-tools.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: Publish Tools
on:
push:
branches:
- main
paths:
- "packages/tools/package.json"
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: ./packages/tools
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
registry-url: 'https://registry.npmjs.org'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: bun install
- name: Build
run: bun run build
- name: Publish
run: pnpm publish --access public --verbose
env:
NPM_CONFIG_PROVENANCE: true
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -6,6 +6,10 @@ description: "API updates, new endpoints, and SDK releases"
API updates, new endpoints, SDK releases, and developer-focused features.
## April 13, 2026
- **Google Drive scoped sync:** New connections default to a **hosted folder/file picker** after OAuth; only chosen items sync. Use `metadata.syncScope: "full"` to sync the whole Drive. Import jobs **skip** scoped connections until a selection exists.
## March 18, 2026
- **Supermemory CLI:** New command-line tool for managing memories, documents, profiles, tags, connectors, and API keys directly from the terminal.

View file

@ -3,6 +3,14 @@ title: "Changelog"
description: "New updates and improvements to Supermemory"
---
<Update label="April 13, 2026" tags={["Integrations", "API"]}>
### Google Drive: scoped sync by default
New Google Drive connections default to **folder and file** scope: after OAuth, users complete a hosted picker; only selected items sync. Set `metadata.syncScope` to `"full"` on connection creation to sync the entire Drive without the picker. Scoped connections without a saved selection are skipped by import jobs until setup is finished.
</Update>
<Update label="March 18, 2026" tags={["API", "SDK", "Console", "CLI"]}>
### Supermemory CLI

View file

@ -6,6 +6,18 @@ icon: "google-drive"
Connect Google Drive to sync documents into your Supermemory knowledge base with OAuth authentication and custom app support.
## Sync scope
**Default for new connections:** after OAuth, the user completes a **folder and file** picker (Google Docs, Sheets, Slides, and PDFs). Only items they select are synced and updated until they change the selection (for example from the Supermemory console).
**Whole Drive:** set `metadata.syncScope` to `"full"` when creating the connection so the entire Drive syncs without the picker.
**Explicit scoped mode:** set `metadata.syncScope` to `"selected"` for the picker flow, or rely on the default for new connects.
<Note>
If you use scoped sync and the user has not finished the picker yet, **scheduled or manual import may skip that connection** until a selection is saved on the connection.
</Note>
## Quick Setup
### 1. Create Google Drive Connection
@ -25,7 +37,8 @@ Connect Google Drive to sync documents into your Supermemory knowledge base with
documentLimit: 3000,
metadata: {
source: 'google-drive',
department: 'engineering'
department: 'engineering',
syncScope: 'selected'
}
});
@ -48,7 +61,8 @@ Connect Google Drive to sync documents into your Supermemory knowledge base with
document_limit=3000,
metadata={
'source': 'google-drive',
'department': 'engineering'
'department': 'engineering',
'syncScope': 'selected',
}
)
@ -68,16 +82,21 @@ Connect Google Drive to sync documents into your Supermemory knowledge base with
"documentLimit": 3000,
"metadata": {
"source": "google-drive",
"department": "engineering"
"department": "engineering",
"syncScope": "selected"
}
}'
```
</Tab>
</Tabs>
<Note>
For **whole Drive** sync, include `"syncScope": "full"` in `metadata` on the same `POST /v3/connections/google-drive` request instead of `"selected"`.
</Note>
### 2. Handle OAuth Callback
After user grants permissions, Google redirects to your callback URL. The connection is automatically established.
After the user grants permissions, Google redirects through Supermemory to finish the connection. With **scoped** sync (`syncScope` omitted or `"selected"`), the user is sent to Supermemorys **hosted file and folder picker**; they must complete that step before imports run. With **`syncScope: "full"`**, Supermemory redirects to your `redirectUrl` (or returns connection details) **without** the picker. You can open the picker again later for an existing connection (Supermemory console, or `POST /v3/connections/{connectionId}/google-drive/hosted-picker` with an authenticated admin session).
### 3. Check Connection Status

View file

@ -161,6 +161,7 @@
"integrations/openai-agents-sdk",
"integrations/agent-framework",
"integrations/mastra",
"integrations/voltagent",
"integrations/langchain",
"integrations/crewai",
"integrations/agno",

View file

@ -0,0 +1,345 @@
---
title: "Cartesia"
sidebarTitle: "Cartesia (Voice)"
description: "Integrate Supermemory with Cartesia for conversational memory in voice AI agents"
icon: "/images/cartesia.svg"
---
Supermemory integrates with [Cartesia](https://cartesia.ai/agents), providing long-term memory capabilities for voice AI agents. Your Cartesia applications will remember past conversations and provide personalized responses based on user history.
## Installation
To use Supermemory with Cartesia, install the required dependencies:
```bash
pip install supermemory-cartesia
```
Set up your API key as an environment variable:
```bash
export SUPERMEMORY_API_KEY=your_supermemory_api_key
```
You can obtain an API key from [console.supermemory.ai](https://console.supermemory.ai).
## Configuration
Supermemory integration is provided through the `SupermemoryCartesiaAgent` wrapper class:
```python
from supermemory_cartesia import SupermemoryCartesiaAgent
from line.llm_agent import LlmAgent, LlmConfig
# Create base LLM agent
base_agent = LlmAgent(
model="anthropic/claude-haiku-4-5-20251001",
api_key=os.getenv("ANTHROPIC_API_KEY"),
config=LlmConfig(
system_prompt="""You are a helpful voice assistant with memory.""",
introduction="Hello! Great to talk with you again!",
),
)
# Wrap with Supermemory
memory_agent = SupermemoryCartesiaAgent(
agent=base_agent,
api_key=os.getenv("SUPERMEMORY_API_KEY"),
container_tag="user-123",
custom_id="session-456", # Required: groups all messages in same document
config=SupermemoryCartesiaAgent.MemoryConfig(
mode="full", # "profile" | "query" | "full"
search_limit=10, # Max memories to retrieve
search_threshold=0.3, # Relevance threshold (0.0-1.0)
),
)
```
## Agent Wrapper Pattern
The `SupermemoryCartesiaAgent` wraps your existing `LlmAgent` to add memory capabilities:
```python
from line.voice_agent_app import VoiceAgentApp
async def get_agent(env, call_request):
# Extract container_tag from call metadata (typically user ID)
container_tag = call_request.metadata.get("user_id", "default-user")
# Create base agent
base_agent = LlmAgent(...)
# Wrap with memory
memory_agent = SupermemoryCartesiaAgent(
agent=base_agent,
container_tag=container_tag,
custom_id=call_request.call_id, # Required: groups all messages in same document
)
return memory_agent
# Create voice agent app
app = VoiceAgentApp(get_agent=get_agent)
```
## How It Works
When integrated with Cartesia Line, Supermemory provides two key functionalities:
### 1. Memory Retrieval
When a `UserTurnEnded` event is detected, Supermemory retrieves relevant memories:
- **Static Profile**: Persistent facts about the user
- **Dynamic Profile**: Recent context and preferences
- **Search Results**: Semantically relevant past memories
### 2. Context Enhancement
Retrieved memories are formatted and injected into the agent's system prompt before processing, giving the model awareness of past conversations.
### 3. Background Storage
Conversations are automatically stored in Supermemory (non-blocking) for future retrieval.
## Memory Modes
| Mode | Static Profile | Dynamic Profile | Search Results | Use Case |
| ----------- | -------------- | --------------- | -------------- | ------------------------------ |
| `"profile"` | Yes | Yes | No | Personalization without search |
| `"query"` | No | No | Yes | Finding relevant past context |
| `"full"` | Yes | Yes | Yes | Complete memory (default) |
## Configuration Options
You can customize how memories are retrieved and used:
### MemoryConfig
```python
SupermemoryCartesiaAgent.MemoryConfig(
mode="full", # Memory mode (default: "full")
search_limit=10, # Max memories to retrieve (default: 10)
search_threshold=0.1, # Similarity threshold 0.0-1.0 (default: 0.1)
system_prompt="Based on previous conversations:\n\n",
)
```
| Parameter | Type | Default | Description |
| ------------------ | ----- | -------------------------------------- | ---------------------------------------------------------- |
| `search_limit` | int | 10 | Maximum number of memories to retrieve per query |
| `search_threshold` | float | 0.1 | Minimum similarity threshold for memory retrieval |
| `mode` | str | "full" | Memory retrieval mode: `"profile"`, `"query"`, or `"full"` |
| `system_prompt` | str | "Based on previous conversations:\n\n" | Prefix text for memory context |
### Agent Parameters
```python
SupermemoryCartesiaAgent(
agent=base_agent, # Required: Cartesia Line LlmAgent
container_tag="user-123", # Required: Primary container tag (e.g., user ID)
custom_id="session-456", # Required: Groups all messages in same document
add_memory="always", # Optional: "always" (default) or "never"
container_tags=["org-acme", "prod"], # Optional: Additional tags
api_key=os.getenv("SUPERMEMORY_API_KEY"), # Optional: defaults to env var
config=MemoryConfig(...), # Optional: memory configuration
base_url=None, # Optional: custom API endpoint
)
```
| Parameter | Type | Required | Description |
| --------------- | ------------ | -------- | ------------------------------------------------------------------ |
| `agent` | LlmAgent | **Yes** | The Cartesia Line agent to wrap |
| `container_tag` | str | **Yes** | Primary container tag for memory scoping (e.g., user ID) |
| `custom_id` | str | **Yes** | Groups all messages in the same document (e.g., call ID, conversation ID) |
| `add_memory` | str | No | Memory persistence mode: "always" (default) or "never" |
| `container_tags`| List[str] | No | Additional container tags for organization (e.g., ["org", "prod"]) |
| `api_key` | str | No | Supermemory API key (or set `SUPERMEMORY_API_KEY` env var) |
| `config` | MemoryConfig | No | Advanced configuration |
| `base_url` | str | No | Custom API endpoint |
## Container Tags
Container tags allow you to organize memories across multiple dimensions:
```python
memory_agent = SupermemoryCartesiaAgent(
agent=base_agent,
container_tag="user-alice", # Primary: user ID
container_tags=["org-acme", "prod"], # Additional: organization, environment
)
```
Memories are stored with all tags:
```json
{
"content": "User: What's the weather?\nAssistant: It's sunny today!",
"container_tags": ["user-alice", "org-acme", "prod"],
"metadata": { "platform": "cartesia" }
}
```
## Automatic Document Grouping
The SDK **automatically groups all messages from the same conversation** into a single Supermemory document using `custom_id`:
```python
memory_agent = SupermemoryCartesiaAgent(
agent=base_agent,
container_tag="user-alice",
custom_id=call_request.call_id, # Required: Groups all messages together
)
```
**How it works:**
- The `custom_id` parameter groups all messages into the same Supermemory document
- Typically you use the call ID or conversation ID from Cartesia
- All messages from that conversation are appended to the same document
- This ensures conversation continuity and proper memory generation
## Example: Basic Voice Agent with Memory
Here's a complete example of a Cartesia Line voice agent with Supermemory integration:
```python
import os
from line.llm_agent import LlmAgent, LlmConfig
from line.voice_agent_app import VoiceAgentApp
from supermemory_cartesia import SupermemoryCartesiaAgent
async def get_agent(env, call_request):
# Extract container_tag from call metadata (typically user ID)
container_tag = call_request.metadata.get("user_id", "default-user")
# Create base LLM agent
base_agent = LlmAgent(
model="anthropic/claude-haiku-4-5-20251001",
api_key=os.getenv("ANTHROPIC_API_KEY"),
config=LlmConfig(
system_prompt="""You are a helpful voice assistant with memory.""",
introduction="Hello! Great to talk with you again!",
),
)
# Wrap with Supermemory
memory_agent = SupermemoryCartesiaAgent(
agent=base_agent,
api_key=os.getenv("SUPERMEMORY_API_KEY"),
container_tag=container_tag,
custom_id=call_request.call_id, # Required: Groups all messages
)
return memory_agent
# Create voice agent app
app = VoiceAgentApp(get_agent=get_agent)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
```
## Example: Advanced Agent with Tools
Here's an example with custom tools and multi-tag support:
```python
import os
from line.llm_agent import LlmAgent, LlmConfig
from line.tools import LoopbackTool
from line.voice_agent_app import VoiceAgentApp
from supermemory_cartesia import SupermemoryCartesiaAgent
# Define custom tool
async def get_weather(location: str) -> str:
return f"The weather in {location} is sunny, 72°F"
weather_tool = LoopbackTool(
name="get_weather",
description="Get current weather for a location",
function=get_weather
)
async def get_agent(env, call_request):
container_tag = call_request.metadata.get("user_id", "default-user")
org_id = call_request.metadata.get("org_id")
# Create LLM agent with tools
base_agent = LlmAgent(
model="gemini/gemini-2.5-flash-preview-09-2025",
tools=[weather_tool],
config=LlmConfig(
system_prompt="You are a personal assistant with memory and tools.",
introduction="Hi! How can I help you today?"
)
)
# Wrap with Supermemory
memory_agent = SupermemoryCartesiaAgent(
agent=base_agent,
api_key=os.getenv("SUPERMEMORY_API_KEY"),
container_tag=container_tag,
custom_id=call_request.call_id, # Required: Groups all messages
container_tags=[org_id] if org_id else None,
config=SupermemoryCartesiaAgent.MemoryConfig(
mode="full",
search_limit=15,
search_threshold=0.15,
)
)
return memory_agent
app = VoiceAgentApp(get_agent=get_agent)
```
## Deployment
To deploy to Cartesia Line, create a `main.py` file in your project root:
```python
import os
import sys
# Add src to path for local imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
from line.llm_agent import LlmAgent, LlmConfig
from line.voice_agent_app import VoiceAgentApp
from supermemory_cartesia import SupermemoryCartesiaAgent
async def get_agent(env, call_request):
"""Create a memory-enabled voice agent."""
container_tag = call_request.metadata.get("user_id", "default-user")
base_agent = LlmAgent(
model="anthropic/claude-haiku-4-5-20251001",
api_key=os.getenv("ANTHROPIC_API_KEY"),
config=LlmConfig(
system_prompt="""You are a helpful voice assistant with memory.
You remember past conversations and can reference them naturally.
Keep responses brief and conversational.""",
introduction="Hello! Great to talk with you again!",
),
)
memory_agent = SupermemoryCartesiaAgent(
agent=base_agent,
api_key=os.getenv("SUPERMEMORY_API_KEY"),
container_tag=container_tag,
custom_id=call_request.call_id, # Required: Groups all messages
)
return memory_agent
app = VoiceAgentApp(get_agent=get_agent)
```
Then deploy with:
```bash
cartesia deploy
```
Make sure to set these environment variables in your Cartesia deployment:
- `SUPERMEMORY_API_KEY` - Your Supermemory API key
- `ANTHROPIC_API_KEY` - Your Anthropic API key (or the key for your chosen LLM provider)

View file

@ -0,0 +1,170 @@
---
title: "VoltAgent"
sidebarTitle: "VoltAgent"
description: "Integrate Supermemory with VoltAgent for long-term memory in AI agents"
icon: "bolt"
---
Supermemory integrates with [VoltAgent](https://github.com/VoltAgent/voltagent), providing long-term memory capabilities for AI agents. Your VoltAgent applications will remember past conversations and provide personalized responses based on user history.
<Card title="@supermemory/tools on npm" icon="npm" href="https://www.npmjs.com/package/@supermemory/tools">
Check out the NPM page for more details
</Card>
## Installation
```bash
npm install @supermemory/tools @voltagent/core
```
Set up your API key as an environment variable:
```bash
export SUPERMEMORY_API_KEY=your_supermemory_api_key
```
You can obtain an API key from [console.supermemory.ai](https://console.supermemory.ai).
## Quick Start
Supermemory provides a `withSupermemory` wrapper that enhances any VoltAgent agent config with automatic memory retrieval and storage:
```typescript
import { withSupermemory } from "@supermemory/tools/voltagent"
import { Agent } from "@voltagent/core"
import { openai } from "@ai-sdk/openai"
// Create an agent with Supermemory memory capabilities
const configWithMemory = withSupermemory({
agentConfig: {
name: "my-agent",
instructions: "You are a helpful assistant.",
model: openai("gpt-4o"),
},
containerTag: "user-123",
customId: "conversation-123",
})
const agent = new Agent(configWithMemory)
// Memories are automatically injected and saved
const result = await agent.generateText({
messages: [{ role: "user", content: "What's my name?" }],
})
```
<Note>
**Memory saving is enabled by default** in the VoltAgent integration. To disable it:
```typescript
const configWithMemory = withSupermemory({
agentConfig: {
name: "my-agent",
instructions: "You are a helpful assistant.",
model: openai("gpt-4o"),
},
containerTag: "user-123",
customId: "conversation-123",
addMemory: "never",
})
```
</Note>
## How It Works
When integrated with VoltAgent, Supermemory hooks into two lifecycle events:
### 1. Memory Retrieval (onPrepareMessages)
Before each LLM call, Supermemory automatically:
- Extracts the user's latest message
- Searches for relevant memories scoped to the `containerTag`
- Injects retrieved memories into the system prompt
### 2. Conversation Saving (onEnd)
After each agent response, the conversation is saved to Supermemory for future retrieval. This requires a `customId` to be set.
## Memory Modes
| Mode | Description | Use Case |
| ----------- | ------------------------------------------- | ------------------------------ |
| `"profile"` | Retrieves the user's complete profile | Personalization without search |
| `"query"` | Searches memories based on the user's message | Finding relevant past context |
| `"full"` | Combines profile AND query-based search | Complete memory (recommended) |
```typescript
const configWithMemory = withSupermemory({
agentConfig: {
name: "my-agent",
instructions: "You are a helpful assistant.",
model: openai("gpt-4o"),
},
containerTag: "user-123",
customId: "conversation-123",
mode: "full",
})
```
## Configuration Options
```typescript
const configWithMemory = withSupermemory({
// Agent configuration
agentConfig: {
name: "my-agent",
instructions: "You are a helpful assistant.",
model: openai("gpt-4o"),
},
// Required
containerTag: "user-123", // User/project ID for scoping memories
// Memory behavior
mode: "full", // "profile" | "query" | "full"
addMemory: "always", // "always" | "never"
customId: "conv-456", // Groups messages into a conversation
// Search tuning
searchMode: "hybrid", // "memories" | "documents" | "hybrid"
threshold: 0.1, // 0.0-1.0 (higher = more accurate)
limit: 10, // Max results to return
rerank: true, // Rerank for best relevance
rewriteQuery: false, // AI-rewrite query (+400ms latency)
// Context
entityContext: "This is John, a software engineer", // Guides memory extraction (max 1500 chars)
metadata: { source: "voltagent" }, // Attached to saved conversations
// API
apiKey: "sk-...", // Falls back to SUPERMEMORY_API_KEY env var
baseUrl: "https://api.supermemory.ai",
})
```
| Parameter | Type | Default | Description |
| ----------------- | -------- | ------------ | -------------------------------------------------------- |
| `agentConfig` | object | **required** | VoltAgent agent configuration object |
| `containerTag` | string | **required** | User/project ID for scoping memories |
| `mode` | string | `"profile"` | Memory retrieval mode |
| `addMemory` | string | `"always"` | Whether to save conversations after each response |
| `customId` | string | **required** | Custom ID to group messages into a conversation |
| `searchMode` | string | — | `"memories"`, `"documents"`, or `"hybrid"` |
| `threshold` | number | `0.1` | Similarity threshold (0 = more results, 1 = more accurate) |
| `limit` | number | `10` | Maximum number of memory results |
| `rerank` | boolean | `false` | Rerank results for relevance |
| `rewriteQuery` | boolean | `false` | AI-rewrite query for better results (+400ms) |
| `entityContext` | string | — | Context for memory extraction (max 1500 chars) |
| `metadata` | object | — | Custom metadata attached to saved conversations |
| `promptTemplate` | function | — | Custom function to format memory data into prompt |
## Search Modes
The `searchMode` option controls what type of results are searched:
| Mode | Description |
| ------------- | ------------------------------------------------------ |
| `"memories"` | Search only memory entries (atomic facts about the user) |
| `"documents"` | Search only document chunks |
| `"hybrid"` | Search both memories AND document chunks (recommended) |

View file

@ -59,6 +59,7 @@ app.get("/", (c) => {
// MCP clients use this to discover the authorization server
app.get("/.well-known/oauth-protected-resource", (c) => {
const apiUrl = c.env.API_URL || DEFAULT_API_URL
const host = c.req.header("x-forwarded-host") || c.req.header("host")
const proto = c.req.header("x-forwarded-proto") || "https"
const resourceUrl = host ? `${proto}://${host}` : "https://mcp.supermemory.ai"

View file

@ -11,6 +11,7 @@ import {
} from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { useOnboardingContext, type MemoryFormData } from "../layout"
import { useAuth } from "@lib/auth-context"
import { analytics } from "@/lib/analytics"
export const WELCOME_STEPS = [
@ -51,13 +52,17 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) {
const searchParams = useSearchParams()
const { name, setName, memoryFormData, setMemoryFormData } =
useOnboardingContext()
const { organizations } = useAuth()
const hasOrgs = Array.isArray(organizations) && organizations.length > 0
const stepParam = searchParams.get("step")
const currentStep: WelcomeStep = WELCOME_STEPS.includes(
const resolvedStep: WelcomeStep = WELCOME_STEPS.includes(
stepParam as WelcomeStep,
)
? (stepParam as WelcomeStep)
: "input"
const currentStep: WelcomeStep =
resolvedStep === "input" && hasOrgs ? "greeting" : resolvedStep
const [isSubmitting, setIsSubmitting] = useState(false)
const [showWelcomeContent, setShowWelcomeContent] = useState(false)

View file

@ -1,5 +1,6 @@
"use client"
import { useRef } from "react"
import { motion, AnimatePresence } from "motion/react"
import { cn } from "@lib/utils"
@ -102,66 +103,70 @@ export default function WelcomePage() {
} = useWelcomeContext()
const { refetchOrganizations, setActiveOrg } = useAuth()
const submitLockRef = useRef(false)
const handleSubmit = async () => {
localStorage.setItem("username", name)
if (name.trim()) {
setIsSubmitting(true)
const trimmed = name.trim()
if (!trimmed) return
if (submitLockRef.current) return
submitLockRef.current = true
localStorage.setItem("username", trimmed)
setIsSubmitting(true)
try {
await authClient.updateUser({
displayUsername: name.trim(),
username: generateUsername(name.trim()),
try {
await authClient.updateUser({
displayUsername: trimmed,
username: generateUsername(trimmed),
})
const refetchResult = await refetchOrganizations()
const refetchData = (
refetchResult as { data?: unknown[] | null | undefined }
)?.data
const existingOrgs = Array.isArray(refetchData) ? refetchData : []
if (existingOrgs.length > 0) {
analytics.onboardingNameSubmitted({
name_length: trimmed.length,
})
goToStep("greeting")
return
}
const refetchResult = await refetchOrganizations()
const refetchData = (
refetchResult as { data?: unknown[] | null | undefined }
)?.data
const existingOrgs = Array.isArray(refetchData) ? refetchData : []
if (existingOrgs.length > 0) {
analytics.onboardingNameSubmitted({
name_length: name.trim().length,
})
goToStep("greeting")
return
}
const uniqueSlug = generateOrgSlug(name.trim())
const completedAt = new Date().toISOString()
const newOrg = await authClient.organization.create({
name: name.trim(),
slug: uniqueSlug,
metadata: {
signupSource: "consumer",
webOnboarding: {
completedAt: null,
steps: {
welcomeInput: {
startedAt: completedAt,
completedAt,
data: {},
},
const uniqueSlug = generateOrgSlug(trimmed)
const completedAt = new Date().toISOString()
const newOrg = await authClient.organization.create({
name: trimmed,
slug: uniqueSlug,
metadata: {
signupSource: "consumer",
webOnboarding: {
completedAt: null,
steps: {
welcomeInput: {
startedAt: completedAt,
completedAt,
data: {},
},
},
},
})
},
})
await setActiveOrg(newOrg.slug)
await setActiveOrg(newOrg.slug)
analytics.onboardingNameSubmitted({ name_length: name.trim().length })
goToStep("greeting")
} catch (error) {
console.error("Onboarding submit failed:", error)
toast.error(
error instanceof Error
? error.message
: "Could not set up your workspace. Please try again.",
)
} finally {
setIsSubmitting(false)
}
analytics.onboardingNameSubmitted({ name_length: trimmed.length })
goToStep("greeting")
} catch (error) {
console.error("Onboarding submit failed:", error)
toast.error(
error instanceof Error
? error.message
: "Could not set up your workspace. Please try again.",
)
} finally {
submitLockRef.current = false
setIsSubmitting(false)
}
}
@ -212,6 +217,7 @@ export default function WelcomePage() {
showUserSupermemory={
currentStep === "features" || currentStep === "memories"
}
showSkipOnboarding={currentStep !== "input"}
name={name}
/>

View file

@ -1,14 +1,30 @@
"use client"
import { Logo } from "@ui/assets/Logo"
import { Button } from "@ui/components/button"
import { useRouter } from "next/navigation"
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
import { analytics } from "@/lib/analytics"
export function InitialHeader({
showUserSupermemory,
showSkipOnboarding,
name,
}: {
showUserSupermemory?: boolean
showSkipOnboarding?: boolean
name?: string
}) {
const router = useRouter()
const { markOrgOnboarded, isLoading } = useOrgOnboarding()
const userName = name ? `${name.split(" ")[0]}'s` : "My"
const handleSkip = () => {
markOrgOnboarded()
analytics.onboardingCompleted()
router.push("/")
}
return (
<div className="flex p-6 justify-between items-center">
<div className="flex items-center z-10!">
@ -24,13 +40,24 @@ export function InitialHeader({
</div>
)}
</div>
<Button
variant="newDefault"
className="rounded-2xl text-base gap-1 h-11! z-10!"
size={"lg"}
>
Memory API <span className="text-xs mt-[4px]"></span>
</Button>
<div className="flex items-center gap-3 z-10!">
{showSkipOnboarding && !isLoading && (
<button
type="button"
onClick={handleSkip}
className="text-sm text-white/40 hover:text-white/70 transition-colors cursor-pointer"
>
Skip Onboarding
</button>
)}
<Button
variant="newDefault"
className="rounded-2xl text-base gap-1 h-11!"
size="lg"
>
Memory API <span className="text-xs mt-[4px]"></span>
</Button>
</div>
</div>
)
}

View file

@ -8,11 +8,20 @@ import { useRouter } from "next/navigation"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
import { analytics } from "@/lib/analytics"
export function SetupHeader() {
const { user } = useAuth()
const router = useRouter()
const localStorageUsername = useLocalStorageUsername()
const { markOrgOnboarded, isLoading: isOrgLoading } = useOrgOnboarding()
const handleSkip = () => {
markOrgOnboarded()
analytics.onboardingCompleted()
router.push("/")
}
const displayName =
user?.displayUsername || localStorageUsername || user?.name || ""
@ -59,9 +68,21 @@ export function SetupHeader() {
</span>
<span className="text-white/50 font-medium shrink-0">Setup</span>
</nav>
{user && (
<UserProfileMenu className="z-10" avatarClassName="border-border" />
)}
<div className="flex items-center gap-3 z-10">
{!isOrgLoading && (
<button
type="button"
onClick={handleSkip}
className={cn(
"text-sm text-white/40 hover:text-white/70 transition-colors cursor-pointer",
dmSansClassName(),
)}
>
Skip Onboarding
</button>
)}
{user && <UserProfileMenu avatarClassName="border-border" />}
</div>
</motion.div>
)
}

View file

@ -1,4 +1,5 @@
import { motion } from "motion/react"
import { cn } from "@lib/utils"
import { LabeledInput } from "@ui/input/labeled-input"
import { Button } from "@ui/components/button"
@ -17,7 +18,10 @@ export function InputStep({
}: InputStepProps) {
return (
<motion.div
className="text-center min-w-[250px] flex flex-col"
className={cn(
"text-center min-w-[250px] flex flex-col",
isSubmitting && "pointer-events-none",
)}
style={{ gap: "24px" }}
initial={{
opacity: 0,
@ -53,20 +57,27 @@ export function InputStep({
className="w-full flex-1"
inputProps={{
defaultValue: name,
disabled: isSubmitting,
onKeyDown: (e) => {
if (e.key === "Enter") {
handleSubmit()
}
if (e.key !== "Enter") return
e.preventDefault()
if (isSubmitting) return
handleSubmit()
},
className: "!text-white placeholder:!text-[#525966] !h-[40px] pl-4",
}}
onChange={(e) => setName((e.target as HTMLInputElement).value)}
onChange={(e) => {
if (isSubmitting) return
setName((e.target as HTMLInputElement).value)
}}
style={{
background:
"linear-gradient(0deg, rgba(91, 126, 245, 0.04) 0%, rgba(91, 126, 245, 0.04) 100%)",
}}
/>
<Button
type="button"
disabled={isSubmitting}
className={`rounded-[8px] w-8 h-8 p-2 absolute right-1 border-[0.5px] border-[#161F2C] hover:cursor-pointer hover:scale-[0.95] active:scale-[0.95] transition-transform ${
isSubmitting ? "scale-[0.90]" : ""
}`}

495
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,232 @@
# Supermemory Cartesia SDK
Memory-enhanced voice agents with [Supermemory](https://supermemory.ai) and [Cartesia Line](https://cartesia.ai/agents).
## Installation
```bash
pip install supermemory-cartesia
```
## Quick Start
```python
import os
from line.llm_agent import LlmAgent, LlmConfig
from line.voice_agent_app import VoiceAgentApp
from supermemory_cartesia import SupermemoryCartesiaAgent
async def get_agent(env, call_request):
# Extract container_tag from call metadata (typically user ID)
container_tag = call_request.metadata.get("user_id", "default-user")
# Create base LLM agent
base_agent = LlmAgent(
model="gemini/gemini-2.5-flash-preview-09-2025",
config=LlmConfig(
system_prompt="You are a helpful voice assistant with memory.",
introduction="Hello! Great to talk with you again!"
)
)
# Wrap with Supermemory
memory_agent = SupermemoryCartesiaAgent(
agent=base_agent,
api_key=os.getenv("SUPERMEMORY_API_KEY"),
container_tag=container_tag,
custom_id=call_request.call_id,
)
return memory_agent
# Create voice agent app
app = VoiceAgentApp(get_agent=get_agent)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
```
## Configuration
### Parameters
| Parameter | Type | Required | Description |
| --------------- | ------------ | -------- | ------------------------------------------------------------------ |
| `agent` | LlmAgent | **Yes** | The Cartesia Line agent to wrap |
| `container_tag` | str | **Yes** | Primary container tag for memory scoping (e.g., user ID) |
| `custom_id` | str | **Yes** | Custom ID for grouping conversation messages into a single document|
| `add_memory` | Literal | No | Memory persistence mode: "always" (default) or "never" |
| `container_tags`| List[str] | No | Additional container tags for organization (e.g., ["org", "prod"]) |
| `api_key` | str | No | Supermemory API key (or set `SUPERMEMORY_API_KEY` env var) |
| `config` | MemoryConfig | No | Advanced configuration |
| `base_url` | str | No | Custom API endpoint |
### Advanced Configuration
```python
from supermemory_cartesia import SupermemoryCartesiaAgent
memory_agent = SupermemoryCartesiaAgent(
agent=base_agent,
container_tag="user-123",
custom_id="conversation-456",
add_memory="always", # "always" (default) or "never"
container_tags=["org-acme", "prod"], # Optional: additional tags
config=SupermemoryCartesiaAgent.MemoryConfig(
search_limit=10, # Max memories to retrieve
search_threshold=0.1, # Similarity threshold
mode="full", # "profile", "query", or "full"
system_prompt="Based on previous conversations, I recall:\n\n",
),
)
# Read-only mode - retrieve memories but don't save new ones
read_only_agent = SupermemoryCartesiaAgent(
agent=base_agent,
container_tag="user-123",
custom_id="conversation-456",
add_memory="never", # Only retrieve, don't save
)
```
### Memory Modes
| Mode | Static Profile | Dynamic Profile | Search Results |
| ----------- | -------------- | --------------- | -------------- |
| `"profile"` | Yes | Yes | No |
| `"query"` | No | No | Yes |
| `"full"` | Yes | Yes | Yes |
## How It Works
1. **Intercepts events** - Listens for `UserTurnEnded` events from Cartesia Line
2. **Retrieves memories** - Queries Supermemory `/v4/profile` API with user's message
3. **Enriches context** - Adds memories to event history as system message
4. **Stores messages** - Sends conversation to Supermemory (background, non-blocking)
5. **Passes to agent** - Forwards enriched event to wrapped LlmAgent
### What Gets Stored
User and assistant messages are sent to Supermemory:
```json
{
"content": "User: What's the weather?\nAssistant: It's sunny today!",
"container_tags": ["user-123", "org-acme", "prod"],
"metadata": { "platform": "cartesia" }
}
```
## Architecture
Cartesia Line uses an event-driven architecture:
```
User Speaks (Audio)
[Ink STT] → Automatic speech recognition
UserTurnEnded Event {content: "user message", history: [...]}
┌──────────────────────────────────────────────┐
│ SUPERMEMORY CARTESIA AGENT (Wrapper) │
│ │
│ process(env, event): │
│ 1. Intercept UserTurnEnded │
│ 2. Extract user message │
│ 3. Query Supermemory API │
│ 4. Enrich event.history with memories │
│ 5. Pass to wrapped LlmAgent │
│ 6. Store conversation (async background) │
└──────────────────────────────────────────────┘
AgentSendText Event {text: "response"}
[Sonic TTS] → Ultra-fast speech synthesis
Audio Output
```
## Comparison with Pipecat SDK
| Aspect | Pipecat | Cartesia Line |
| ----------------------- | ------------------------------ | ---------------------------- |
| **Integration Pattern** | Extends `FrameProcessor` | Wrapper around `LlmAgent` |
| **Event Handling** | `process_frame()` method | `process()` method |
| **Events** | `LLMContextFrame`, `LLMMessagesFrame` | `UserTurnEnded`, `CallStarted` |
| **Context Object** | `LLMContext.get_messages()` | `event.history` |
| **Memory Injection** | Modify `context.add_message()` | Modify `event.history` |
## Full Example with Tools
```python
import os
from line.llm_agent import LlmAgent, LlmConfig
from line.tools import LoopbackTool
from line.voice_agent_app import VoiceAgentApp
from supermemory_cartesia import SupermemoryCartesiaAgent
# Define custom tools
async def get_weather(location: str) -> str:
return f"The weather in {location} is sunny, 72°F"
weather_tool = LoopbackTool(
name="get_weather",
description="Get current weather for a location",
function=get_weather
)
async def get_agent(env, call_request):
container_tag = call_request.metadata.get("user_id", "default-user")
org_id = call_request.metadata.get("org_id")
# Create LLM agent with tools
base_agent = LlmAgent(
model="gemini/gemini-2.5-flash-preview-09-2025",
tools=[weather_tool],
config=LlmConfig(
system_prompt="You are a personal assistant with memory and tools.",
introduction="Hi! How can I help you today?"
)
)
# Wrap with Supermemory
memory_agent = SupermemoryCartesiaAgent(
agent=base_agent,
api_key=os.getenv("SUPERMEMORY_API_KEY"),
container_tag=container_tag,
custom_id=call_request.call_id,
container_tags=[org_id] if org_id else None,
config=SupermemoryCartesiaAgent.MemoryConfig(
mode="full",
search_limit=15,
search_threshold=0.15,
)
)
return memory_agent
app = VoiceAgentApp(get_agent=get_agent)
```
## Development
```bash
# Clone repository
git clone https://github.com/supermemoryai/supermemory
cd supermemory/packages/cartesia-sdk-python
# Install in development mode
pip install -e ".[dev]"
# Run tests
pytest
# Format code
black .
isort .
```
## License
MIT

View file

@ -0,0 +1,78 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "supermemory-cartesia"
version = "0.1.0"
description = "Supermemory integration for Cartesia Line - memory-enhanced voice agents"
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
authors = [
{ name = "Supermemory", email = "support@supermemory.ai" }
]
keywords = [
"supermemory",
"cartesia",
"line",
"memory",
"conversational-ai",
"llm",
"voice-ai",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
dependencies = [
"supermemory>=3.16.0",
"cartesia-line>=0.2.0",
"pydantic>=2.10.0",
"loguru>=0.7.3",
]
[project.optional-dependencies]
dev = [
"pytest>=8.3.5",
"pytest-asyncio>=0.24.0",
"mypy>=1.14.1",
"black>=24.8.0",
"isort>=5.13.2",
]
[project.urls]
Homepage = "https://supermemory.ai"
Documentation = "https://docs.supermemory.ai"
Repository = "https://github.com/supermemoryai/supermemory"
[tool.hatch.build.targets.wheel]
packages = ["src/supermemory_cartesia"]
[tool.hatch.build.targets.sdist]
include = [
"/src",
"/tests",
"/README.md",
"/LICENSE",
]
[tool.black]
line-length = 100
target-version = ["py310"]
[tool.isort]
profile = "black"
line_length = 100
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true

View file

@ -0,0 +1,5 @@
# Core dependencies for supermemory-cartesia
supermemory>=3.16.0
pydantic>=2.10.0
loguru>=0.7.3
cartesia-line>=0.2.0

View file

@ -0,0 +1,67 @@
"""Supermemory Cartesia SDK - Memory-enhanced voice agents with Cartesia Line.
This package provides seamless integration between Supermemory and Cartesia Line,
enabling persistent memory and context enhancement for voice AI applications.
Example:
```python
from supermemory_cartesia import SupermemoryCartesiaAgent, MemoryConfig
from line.llm_agent import LlmAgent, LlmConfig
# Create base LLM agent
base_agent = LlmAgent(
model="gemini/gemini-2.5-flash-preview-09-2025",
config=LlmConfig(
system_prompt="You are a helpful assistant.",
introduction="Hello!"
)
)
# Wrap with Supermemory
memory_agent = SupermemoryCartesiaAgent(
agent=base_agent,
api_key=os.getenv("SUPERMEMORY_API_KEY"),
container_tag="user-123",
)
```
"""
from .agent import SupermemoryCartesiaAgent
# Export MemoryConfig as a top-level class for convenience
MemoryConfig = SupermemoryCartesiaAgent.MemoryConfig
from .exceptions import (
APIError,
ConfigurationError,
MemoryRetrievalError,
MemoryStorageError,
NetworkError,
SupermemoryCartesiaError,
)
from .utils import (
deduplicate_memories,
format_memories_to_text,
format_relative_time,
get_last_user_message,
)
__version__ = "0.1.0"
__all__ = [
# Main agent
"SupermemoryCartesiaAgent",
"MemoryConfig",
# Exceptions
"SupermemoryCartesiaError",
"ConfigurationError",
"MemoryRetrievalError",
"MemoryStorageError",
"APIError",
"NetworkError",
# Utilities
"get_last_user_message",
"deduplicate_memories",
"format_memories_to_text",
"format_relative_time",
]

View file

@ -0,0 +1,446 @@
"""Supermemory Cartesia Line agent integration.
This module provides a memory-enhanced agent wrapper that integrates with
Cartesia Line voice agents, adding persistent memory and context enrichment.
"""
import asyncio
import os
import re
from typing import Any, AsyncGenerator, Dict, List, Literal, Optional
from loguru import logger
from pydantic import BaseModel, Field
from .exceptions import ConfigurationError, MemoryRetrievalError
from .utils import deduplicate_memories, format_memories_to_text
try:
import supermemory
except ImportError:
supermemory = None # type: ignore
try:
from line.events import Event
except ImportError:
Event = Any # type: ignore
# XML tags for memory injection
MEMORY_TAG_START = "<user_memories>"
MEMORY_TAG_END = "</user_memories>"
class SupermemoryCartesiaAgent:
"""Memory-enhanced wrapper for Cartesia Line agents.
This wrapper intercepts UserTurnEnded events, retrieves relevant memories
from Supermemory, and enriches the conversation history before passing to
the wrapped agent.
Example:
```python
from line.llm_agent import LlmAgent, LlmConfig
from supermemory_cartesia import SupermemoryCartesiaAgent
base_agent = LlmAgent(
model="anthropic/claude-haiku-4-5-20251001",
config=LlmConfig(
system_prompt="You are a helpful assistant.",
introduction="Hello! How can I help you today?"
)
)
memory_agent = SupermemoryCartesiaAgent(
agent=base_agent,
api_key=os.getenv("SUPERMEMORY_API_KEY"),
container_tag="user-123",
custom_id="conversation-456",
)
```
"""
class MemoryConfig(BaseModel):
"""Configuration for memory retrieval.
Attributes:
search_limit: Maximum memories to retrieve per query.
search_threshold: Minimum similarity threshold (0.0-1.0).
system_prompt: Prefix text for memory context.
mode: "profile", "query", or "full".
"""
search_limit: int = Field(default=10, ge=1)
search_threshold: float = Field(default=0.1, ge=0.0, le=1.0)
system_prompt: str = Field(default="Based on previous conversations:\n\n")
mode: Literal["profile", "query", "full"] = Field(default="full")
def __init__(
self,
*,
agent: Any,
api_key: Optional[str] = None,
container_tag: str,
custom_id: str,
add_memory: Literal["always", "never"] = "always",
container_tags: Optional[List[str]] = None,
config: Optional[MemoryConfig] = None,
base_url: Optional[str] = None,
):
"""Initialize the Supermemory Cartesia agent wrapper.
Args:
agent: The inner Cartesia Line agent to wrap.
api_key: Supermemory API key (or SUPERMEMORY_API_KEY env var).
container_tag: Primary container tag for memory scoping (e.g., user ID).
custom_id: Required. Custom ID to store all conversation messages in the same document.
Useful for grouping multi-turn conversations (e.g., call ID, conversation ID).
add_memory: Memory persistence mode - "always" (default) or "never".
container_tags: Optional list of additional container tags for
organization/categorization (e.g., ["org-acme", "prod"]).
config: Memory retrieval configuration.
base_url: Optional custom Supermemory API URL.
Raises:
ConfigurationError: If API key, container_tag, or custom_id is missing.
"""
self.agent = agent
self.container_tag = container_tag
self.custom_id = custom_id
self.add_memory = add_memory
# Build container tags list: primary tag first, then additional tags
self.container_tags = [container_tag]
if container_tags:
self.container_tags.extend(container_tags)
self.config = config or SupermemoryCartesiaAgent.MemoryConfig()
self.api_key = api_key or os.getenv("SUPERMEMORY_API_KEY")
if not self.api_key:
raise ConfigurationError(
"API key required. Set SUPERMEMORY_API_KEY or pass api_key."
)
if not container_tag:
raise ConfigurationError("container_tag is required")
if not custom_id or not custom_id.strip():
raise ConfigurationError(
"custom_id is required and must be a non-empty string. "
"This ensures messages are grouped into the same document for a conversation."
)
self._supermemory_client = None
if supermemory is not None:
try:
self._supermemory_client = supermemory.AsyncSupermemory(
api_key=self.api_key,
base_url=base_url,
)
logger.info(f"[Supermemory] Initialized client for container_tag={container_tag}, all_tags={self.container_tags}")
except Exception as e:
logger.error(f"[Supermemory] Failed to initialize client: {e}")
self._messages_sent_count: int = 0
self._last_query: Optional[str] = None
self._background_tasks: set = set() # Track background tasks to prevent GC
async def _retrieve_memories(self, query: str) -> Dict[str, Any]:
"""Retrieve memories from Supermemory."""
if self._supermemory_client is None:
raise MemoryRetrievalError("Supermemory client not initialized")
try:
# Use primary container tag for profile retrieval
kwargs: Dict[str, Any] = {"container_tag": self.container_tags[0]}
if self.config.mode != "profile" and query:
kwargs["q"] = query
kwargs["threshold"] = self.config.search_threshold
kwargs["extra_body"] = {"limit": self.config.search_limit}
logger.info(f"[Supermemory] Retrieving memories for query: {query[:50]}...")
response = await asyncio.wait_for(
self._supermemory_client.profile(**kwargs),
timeout=10.0
)
static_count = len(response.profile.static) if response.profile.static else 0
dynamic_count = len(response.profile.dynamic) if response.profile.dynamic else 0
search_count = len(response.search_results.results) if response.search_results and response.search_results.results else 0
logger.info(f"[Supermemory] Retrieved memories - static: {static_count}, dynamic: {dynamic_count}, search: {search_count}")
search_results = []
if response.search_results and response.search_results.results:
search_results = response.search_results.results
return {
"profile": {
"static": response.profile.static or [],
"dynamic": response.profile.dynamic or [],
},
"search_results": search_results,
}
except asyncio.TimeoutError:
logger.warning("[Supermemory] Profile API timed out after 10s")
raise MemoryRetrievalError("Profile API timed out")
except Exception as e:
logger.error(f"[Supermemory] Error retrieving memories: {e}")
raise MemoryRetrievalError("Failed to retrieve memories", e)
async def _store_messages(self, messages: List[Dict[str, Any]]) -> None:
"""Store messages in Supermemory."""
if self._supermemory_client is None or not messages or self.add_memory == "never":
return
try:
# Format as conversation transcript
lines = []
for msg in messages:
role = msg.get("role", "")
content = msg.get("content", "")
if role == "user":
lines.append(f"User: {content}")
elif role == "assistant":
lines.append(f"Assistant: {content}")
logger.info(f"[Supermemory] Storing {len(messages)} messages to containers={self.container_tags}")
# Build kwargs for add() call
add_kwargs: Dict[str, Any] = {
"content": "\n".join(lines),
"container_tags": self.container_tags,
"metadata": {"platform": "cartesia"},
}
# Use custom_id for document grouping (required field)
add_kwargs["custom_id"] = self.custom_id
logger.info(f"[Supermemory] Using custom_id={self.custom_id} for document grouping")
await self._supermemory_client.add(**add_kwargs)
logger.info(f"[Supermemory] Successfully stored {len(messages)} messages")
except Exception as e:
logger.error(f"[Supermemory] Error storing messages: {e}")
def _build_memory_message(self, memories_data: Dict[str, Any]) -> Optional[str]:
"""Build memory context from retrieved data."""
profile = memories_data["profile"]
deduplicated = deduplicate_memories(
static=profile["static"],
dynamic=profile["dynamic"],
search_results=memories_data["search_results"],
)
total = (
len(deduplicated["static"])
+ len(deduplicated["dynamic"])
+ len(deduplicated["search_results"])
)
if total == 0:
return None
include_profile = self.config.mode in ("profile", "full")
include_search = self.config.mode in ("query", "full")
memory_text = format_memories_to_text(
deduplicated,
system_prompt=self.config.system_prompt,
include_static=include_profile,
include_dynamic=include_profile,
include_search=include_search,
)
if not memory_text:
return None
return f"{MEMORY_TAG_START}\n{memory_text}\n{MEMORY_TAG_END}"
def _extract_user_message(self, event: Any) -> Optional[str]:
"""Extract user text from a UserTurnEnded event."""
if not hasattr(event, 'content'):
return None
content = event.content
if isinstance(content, str):
return content
if isinstance(content, list):
texts = []
for item in content:
if hasattr(item, 'content') and isinstance(item.content, str):
texts.append(item.content)
elif isinstance(item, str):
texts.append(item)
return " ".join(texts) if texts else None
if hasattr(content, 'content'):
return str(content.content)
return str(content)
def _extract_conversation_from_history(self, history: list) -> List[Dict[str, str]]:
"""Extract messages from Cartesia event history."""
messages = []
seen = set()
for item in history:
if isinstance(item, dict):
if item.get("role") in ("user", "assistant"):
content = item.get("content", "")
if content and content not in seen:
messages.append(item)
seen.add(content)
continue
event_type = getattr(item, 'type', None) or type(item).__name__
if event_type in ('user_turn_ended', 'UserTurnEnded'):
nested = getattr(item, 'content', [])
if isinstance(nested, list):
for n in nested:
if hasattr(n, 'content') and isinstance(n.content, str):
if n.content not in seen:
messages.append({"role": "user", "content": n.content})
seen.add(n.content)
elif event_type in ('agent_turn_ended', 'AgentTurnEnded'):
nested = getattr(item, 'content', [])
if isinstance(nested, list):
texts = [n.content for n in nested if hasattr(n, 'content') and isinstance(n.content, str)]
if texts:
content = " ".join(texts)
if content not in seen:
messages.append({"role": "assistant", "content": content})
seen.add(content)
elif event_type in ('user_text_sent', 'UserTextSent'):
content = getattr(item, 'content', '')
if content and isinstance(content, str) and content not in seen:
messages.append({"role": "user", "content": content})
seen.add(content)
elif event_type in ('agent_text_sent', 'AgentTextSent'):
content = getattr(item, 'content', '')
if content and isinstance(content, str) and content not in seen:
messages.append({"role": "assistant", "content": content})
seen.add(content)
return messages
async def _enrich_event_with_memories(self, event: Any) -> tuple[Any, Optional[str]]:
"""Enrich event by retrieving memories.
Returns:
Tuple of (event, memory_context) - memory_context is None if no memories found.
The event is returned unchanged; memory injection happens at the agent level.
"""
user_message = self._extract_user_message(event)
if not user_message:
logger.warning("[Supermemory] Could not extract user message from event")
return event, None
if user_message == self._last_query:
return event, None
self._last_query = user_message
logger.info(f"[Supermemory] Processing user message: {user_message[:50]}...")
try:
memories_data = await self._retrieve_memories(user_message)
memory_context = self._build_memory_message(memories_data)
if not memory_context:
logger.info("[Supermemory] No memories found for context injection")
return event, None
logger.info("[Supermemory] Retrieved memory context for injection")
return event, memory_context
except MemoryRetrievalError as e:
logger.warning(f"[Supermemory] Memory retrieval failed: {e}")
return event, None
except Exception as e:
logger.error(f"[Supermemory] Error in memory enrichment: {e}")
return event, None
async def process(self, env: Any, event: Event) -> AsyncGenerator[Event, None]:
"""Process events with memory enrichment.
Args:
env: Turn environment from Cartesia Line.
event: Input event to process.
Yields:
Output events from the wrapped agent.
"""
try:
if type(event).__name__ == "UserTurnEnded":
logger.info("[Supermemory] Processing UserTurnEnded event")
event, memory_context = await self._enrich_event_with_memories(event)
# Clean up old memory context and inject new one if available
if hasattr(self.agent, 'config'):
original_prompt = getattr(self.agent.config, 'system_prompt', '')
# Always remove old memory context if present to prevent stale data
if MEMORY_TAG_START in original_prompt:
original_prompt = re.sub(
rf'{re.escape(MEMORY_TAG_START)}.*?{re.escape(MEMORY_TAG_END)}\s*',
'',
original_prompt,
flags=re.DOTALL
)
logger.debug("[Supermemory] Removed old memory context from system prompt")
# Inject new memory context if available
if memory_context:
self.agent.config.system_prompt = f"{memory_context}\n\n{original_prompt}"
logger.info("[Supermemory] Injected new memory context into system prompt")
else:
# No new memories, but we cleaned up old ones
self.agent.config.system_prompt = original_prompt
logger.debug("[Supermemory] No new memories to inject, using clean prompt")
# Store conversation in background
if hasattr(event, 'history') and event.history:
messages = self._extract_conversation_from_history(event.history)
unsent = messages[self._messages_sent_count:]
if unsent:
logger.info(f"[Supermemory] Queuing {len(unsent)} messages for storage")
task = asyncio.create_task(self._store_messages(unsent))
self._background_tasks.add(task)
task.add_done_callback(self._background_tasks.discard)
self._messages_sent_count = len(messages)
else:
# No history yet, store just the current user message
user_content = self._extract_user_message(event)
if user_content:
logger.info(f"[Supermemory] No history, storing current user message: {user_content[:50]}...")
task = asyncio.create_task(self._store_messages([{"role": "user", "content": user_content}]))
self._background_tasks.add(task)
task.add_done_callback(self._background_tasks.discard)
self._messages_sent_count = 1 # CRITICAL: Increment counter to prevent duplicate storage
async for output in self.agent.process(env, event):
yield output
else:
async for output in self.agent.process(env, event):
yield output
except Exception as e:
logger.error(f"[Supermemory] Error in process: {e}")
async for output in self.agent.process(env, event):
yield output
def reset_memory_tracking(self) -> None:
"""Reset memory tracking for a new conversation."""
self._messages_sent_count = 0
self._last_query = None
logger.info("[Supermemory] Reset memory tracking state")

View file

@ -0,0 +1,58 @@
"""Custom exceptions for Supermemory Cartesia integration."""
from typing import Optional
class SupermemoryCartesiaError(Exception):
"""Base exception for all Supermemory Cartesia errors."""
def __init__(self, message: str, original_error: Optional[Exception] = None):
super().__init__(message)
self.message = message
self.original_error = original_error
def __str__(self) -> str:
if self.original_error:
return f"{self.message}: {self.original_error}"
return self.message
class ConfigurationError(SupermemoryCartesiaError):
"""Raised when there are configuration issues (e.g., missing API key, invalid params)."""
class MemoryRetrievalError(SupermemoryCartesiaError):
"""Raised when memory retrieval operations fail."""
class MemoryStorageError(SupermemoryCartesiaError):
"""Raised when memory storage operations fail."""
class APIError(SupermemoryCartesiaError):
"""Raised when Supermemory API requests fail."""
def __init__(
self,
message: str,
status_code: Optional[int] = None,
response_text: Optional[str] = None,
original_error: Optional[Exception] = None,
):
super().__init__(message, original_error)
self.status_code = status_code
self.response_text = response_text
def __str__(self) -> str:
parts = [self.message]
if self.status_code:
parts.append(f"Status: {self.status_code}")
if self.response_text:
parts.append(f"Response: {self.response_text}")
if self.original_error:
parts.append(f"Cause: {self.original_error}")
return " | ".join(parts)
class NetworkError(SupermemoryCartesiaError):
"""Raised when network operations fail."""

View file

@ -0,0 +1,134 @@
"""Utility functions for Supermemory Cartesia integration."""
from datetime import datetime, timezone
from typing import Any, Dict, List, Union
def get_last_user_message(messages: List[Dict[str, str]]) -> str | None:
"""Extract the last user message content from a list of messages."""
for msg in reversed(messages):
if msg["role"] == "user":
return msg["content"]
return None
def format_relative_time(iso_timestamp: str) -> str:
"""Convert ISO timestamp to relative time string.
Format rules:
- [just now] - within 30 minutes
- [Xmins ago] - 30-60 minutes
- [X hrs ago] - less than 1 day
- [Xd ago] - less than 1 week
- [X Jul] - more than 1 week, same year
- [X Jul, 2023] - different year
"""
try:
dt = datetime.fromisoformat(iso_timestamp.replace("Z", "+00:00"))
now = datetime.now(timezone.utc)
diff = now - dt
seconds = diff.total_seconds()
minutes = seconds / 60
hours = seconds / 3600
days = seconds / 86400
if minutes < 30:
return "just now"
elif minutes < 60:
return f"{int(minutes)}mins ago"
elif hours < 24:
return f"{int(hours)} hrs ago"
elif days < 7:
return f"{int(days)}d ago"
elif dt.year == now.year:
return f"{dt.day} {dt.strftime('%b')}"
else:
return f"{dt.day} {dt.strftime('%b')}, {dt.year}"
except Exception:
return ""
def deduplicate_memories(
static: List[str],
dynamic: List[str],
search_results: List[Dict[str, Any]],
) -> Dict[str, Union[List[str], List[Dict[str, Any]]]]:
"""Deduplicate memories. Priority: static > dynamic > search.
Args:
static: List of static memory strings.
dynamic: List of dynamic memory strings.
search_results: List of search result dicts with 'memory' and 'updatedAt'.
"""
seen = set()
def unique_strings(memories: List[str]) -> List[str]:
out = []
for m in memories:
if m not in seen:
seen.add(m)
out.append(m)
return out
def unique_search(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
out = []
for r in results:
memory = r.get("memory", "")
if memory and memory not in seen:
seen.add(memory)
out.append(r)
return out
return {
"static": unique_strings(static),
"dynamic": unique_strings(dynamic),
"search_results": unique_search(search_results),
}
def format_memories_to_text(
memories: Dict[str, Union[List[str], List[Dict[str, Any]]]],
system_prompt: str = "Based on previous conversations, I recall:\n\n",
include_static: bool = True,
include_dynamic: bool = True,
include_search: bool = True,
) -> str:
"""Format deduplicated memories into a text string for injection.
Search results include temporal context (e.g., '3d ago') from updatedAt.
"""
sections = []
static = memories["static"]
dynamic = memories["dynamic"]
search_results = memories["search_results"]
if include_static and static:
sections.append("## User Profile (Persistent)")
sections.append("\n".join(f"- {item}" for item in static))
if include_dynamic and dynamic:
sections.append("## Recent Context")
sections.append("\n".join(f"- {item}" for item in dynamic))
if include_search and search_results:
sections.append("## Relevant Memories")
lines = []
for item in search_results:
if isinstance(item, dict):
memory = item.get("memory", "")
updated_at = item.get("updatedAt", "")
time_str = format_relative_time(updated_at) if updated_at else ""
if time_str:
lines.append(f"- [{time_str}] {memory}")
else:
lines.append(f"- {memory}")
else:
lines.append(f"- {item}")
sections.append("\n".join(lines))
if not sections:
return ""
return f"{system_prompt}\n" + "\n\n".join(sections)

View file

@ -1,7 +1,7 @@
{
"name": "@supermemory/tools",
"type": "module",
"version": "2.0.0",
"version": "1.5.0",
"description": "Memory tools for AI SDK and OpenAI function calling with supermemory",
"scripts": {
"build": "tsdown",
@ -22,6 +22,7 @@
"devDependencies": {
"@ai-sdk/provider": "^3.0.0",
"@anthropic-ai/sdk": "^0.65.0",
"@voltagent/core": "^2.6.12",
"@mastra/core": "^1.0.0",
"@total-typescript/tsconfig": "^1.0.4",
"@types/bun": "^1.2.21",
@ -31,7 +32,13 @@
"vitest": "^3.2.4"
},
"peerDependencies": {
"@ai-sdk/provider": "^2.0.0 || ^3.0.0"
"@ai-sdk/provider": "^2.0.0 || ^3.0.0",
"@voltagent/core": "^2.6.12"
},
"peerDependenciesMeta": {
"@voltagent/core": {
"optional": true
}
},
"main": "./dist/index.js",
"module": "./dist/index.js",
@ -45,6 +52,7 @@
"./claude-memory": "./dist/claude-memory.js",
"./mastra": "./dist/mastra.js",
"./openai": "./dist/openai/index.js",
"./voltagent": "./dist/voltagent/index.js",
"./package.json": "./package.json"
},
"repository": {

View file

@ -34,6 +34,7 @@ export interface AddConversationParams {
messages: ConversationMessage[]
containerTags?: string[]
metadata?: Record<string, string | number | boolean>
entityContext?: string
apiKey: string
baseUrl?: string
}
@ -86,6 +87,7 @@ export async function addConversation(
messages: params.messages,
containerTags: params.containerTags,
metadata: params.metadata,
entityContext: params.entityContext,
}),
})

View file

@ -1,3 +1,5 @@
export type { SupermemoryToolsConfig } from "./types"
export type { OpenAIMiddlewareOptions } from "./openai"
export type { SupermemoryVoltAgent } from "./voltagent"

View file

@ -119,98 +119,113 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
promptTemplate: options?.promptTemplate,
})
const wrappedModel = {
...model,
// Proxy keeps prototype/getter fields (e.g. provider, modelId) that `{ ...model }` drops.
return new Proxy(model, {
get(target, prop, receiver) {
if (prop === "doGenerate") {
return async (params: LanguageModelCallOptions) => {
try {
const transformedParams = await transformParamsWithMemory(
params,
ctx,
)
doGenerate: async (params: LanguageModelCallOptions) => {
try {
const transformedParams = await transformParamsWithMemory(params, ctx)
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
const result = await target.doGenerate(transformedParams as any)
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
const result = await model.doGenerate(transformedParams as any)
const userMessage = getLastUserMessage(params)
if (ctx.addMemory === "always" && userMessage && userMessage.trim()) {
const assistantResponseText = extractAssistantResponseText(
result.content as unknown[],
)
saveMemoryAfterResponse(
ctx.client,
ctx.containerTag,
ctx.conversationId,
assistantResponseText,
params,
ctx.logger,
ctx.apiKey,
ctx.normalizedBaseUrl,
)
}
return result
} catch (error) {
ctx.logger.error("Error generating response", {
error: error instanceof Error ? error.message : "Unknown error",
})
throw error
}
},
doStream: async (params: LanguageModelCallOptions) => {
let generatedText = ""
try {
const transformedParams = await transformParamsWithMemory(params, ctx)
const { stream, ...rest } = await model.doStream(
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
transformedParams as any,
)
const transformStream = new TransformStream<
LanguageModelStreamPart,
LanguageModelStreamPart
>({
transform(chunk, controller) {
if (chunk.type === "text-delta") {
generatedText += chunk.delta
}
controller.enqueue(chunk)
},
flush: async () => {
const userMessage = getLastUserMessage(params)
if (
ctx.addMemory === "always" &&
userMessage &&
userMessage.trim()
) {
const assistantResponseText = extractAssistantResponseText(
result.content as unknown[],
)
saveMemoryAfterResponse(
ctx.client,
ctx.containerTag,
ctx.conversationId,
generatedText,
assistantResponseText,
params,
ctx.logger,
ctx.apiKey,
ctx.normalizedBaseUrl,
)
}
},
})
return {
stream: stream.pipeThrough(transformStream),
...rest,
return result
} catch (error) {
ctx.logger.error("Error generating response", {
error: error instanceof Error ? error.message : "Unknown error",
})
throw error
}
}
} catch (error) {
ctx.logger.error("Error streaming response", {
error: error instanceof Error ? error.message : "Unknown error",
})
throw error
}
},
} as T
return wrappedModel
if (prop === "doStream") {
return async (params: LanguageModelCallOptions) => {
let generatedText = ""
try {
const transformedParams = await transformParamsWithMemory(
params,
ctx,
)
const { stream, ...rest } = await target.doStream(
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
transformedParams as any,
)
const transformStream = new TransformStream<
LanguageModelStreamPart,
LanguageModelStreamPart
>({
transform(chunk, controller) {
if (chunk.type === "text-delta") {
generatedText += chunk.delta
}
controller.enqueue(chunk)
},
flush: async () => {
const userMessage = getLastUserMessage(params)
if (
ctx.addMemory === "always" &&
userMessage &&
userMessage.trim()
) {
saveMemoryAfterResponse(
ctx.client,
ctx.containerTag,
ctx.conversationId,
generatedText,
params,
ctx.logger,
ctx.apiKey,
ctx.normalizedBaseUrl,
)
}
},
})
return {
stream: stream.pipeThrough(transformStream),
...rest,
}
} catch (error) {
ctx.logger.error("Error streaming response", {
error: error instanceof Error ? error.message : "Unknown error",
})
throw error
}
}
}
return Reflect.get(target, prop, receiver)
},
}) as T
}
export {

View file

@ -0,0 +1,207 @@
/**
* VoltAgent hooks for Supermemory integration.
*
* Provides onPrepareMessages and onEnd hooks that inject memories
* and save conversations.
*/
import type {
VoltAgentHooks,
HookPrepareMessagesArgs,
HookEndArgs,
VoltAgentMessage,
SupermemoryVoltAgent,
} from "./types"
import {
createSupermemoryContext,
enhanceMessagesWithMemories,
saveConversation,
} from "./middleware"
/**
* Creates Supermemory hooks for VoltAgent agents.
*
* 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 options - Configuration options for memory behavior
* @returns VoltAgent hooks object with onPrepareMessages and onEnd
*
* @example
* ```typescript
* import { createSupermemoryHooks } from "@supermemory/tools/voltagent"
*
* const hooks = createSupermemoryHooks("user-123", {
* mode: "full",
* addMemory: "always",
* customId: "conv-456",
* })
*
* const agent = new Agent({
* name: "my-agent",
* instructions: "You are a helpful assistant",
* llm: new VercelAIProvider(),
* model: openai("gpt-4o"),
* hooks
* })
* ```
*/
export function createSupermemoryHooks(
containerTag: string,
options: SupermemoryVoltAgent,
): VoltAgentHooks {
const ctx = createSupermemoryContext(containerTag, options)
return {
onPrepareMessages: async (
args: HookPrepareMessagesArgs,
): Promise<{ messages: VoltAgentMessage[] }> => {
try {
// VoltAgent passes user messages in args.context.input.messages
// and the prepared messages (system + conversation) in args.messages
const contextInput = args.context?.input as
| { messages?: VoltAgentMessage[] }
| undefined
const inputMessages = contextInput?.messages || []
ctx.logger.debug("onPrepareMessages called", {
messageCount: args.messages.length,
inputMessageCount: inputMessages.length,
agentName: args.agent.name,
})
const enhancedMessages = await enhanceMessagesWithMemories(
inputMessages,
ctx,
args.messages,
)
ctx.logger.debug("Messages enhanced with memories", {
originalCount: args.messages.length,
enhancedCount: enhancedMessages.length,
})
return { messages: enhancedMessages }
} catch (error) {
ctx.logger.error("Error in onPrepareMessages", {
error: error instanceof Error ? error.message : "Unknown error",
})
return { messages: args.messages }
}
},
onEnd: async (args: HookEndArgs): Promise<void> => {
try {
ctx.logger.debug("onEnd called", {
agentName: args.agent.name,
hasContext: !!args.context,
hasOutput: !!args.output,
})
let messages: VoltAgentMessage[] = []
if (args.context?.input && args.output) {
const inputData = args.context.input as
| { messages?: VoltAgentMessage[] }
| undefined
const inputMessages = inputData?.messages || []
const outputData = args.output as
| string
| { text?: string; content?: string }
| undefined
const outputText =
typeof outputData === "string"
? outputData
: outputData?.text || outputData?.content
if (inputMessages.length > 0 && outputText) {
messages = [
...inputMessages,
{ role: "assistant" as const, content: outputText },
]
}
}
if (messages.length === 0) {
ctx.logger.debug("No messages to save, skipping")
return
}
saveConversation(messages, ctx).catch((error) => {
ctx.logger.error("Background conversation save failed", {
error: error instanceof Error ? error.message : "Unknown error",
})
})
} catch (error) {
ctx.logger.error("Error in onEnd", {
error: error instanceof Error ? error.message : "Unknown error",
})
}
},
}
}
/**
* Merges Supermemory hooks with existing hooks from an agent config.
* Preserves existing hooks and adds Supermemory hooks.
*
* @param existingHooks - Existing hooks from agent config (if any)
* @param supermemoryHooks - Supermemory hooks to merge
* @returns Merged hooks object
*/
export function mergeHooks(
existingHooks: VoltAgentHooks | undefined,
supermemoryHooks: VoltAgentHooks,
): VoltAgentHooks {
if (!existingHooks) {
return supermemoryHooks
}
const mergedHooks: VoltAgentHooks = { ...existingHooks }
if (existingHooks.onPrepareMessages && supermemoryHooks.onPrepareMessages) {
const existingOnPrepareMessages = existingHooks.onPrepareMessages
const supermemoryOnPrepareMessages = supermemoryHooks.onPrepareMessages
mergedHooks.onPrepareMessages = async (args) => {
const resultAfterExisting = await existingOnPrepareMessages(args)
const messagesAfterExisting =
resultAfterExisting?.messages || args.messages
return await supermemoryOnPrepareMessages({
...args,
messages: messagesAfterExisting,
})
}
} else if (supermemoryHooks.onPrepareMessages) {
mergedHooks.onPrepareMessages = supermemoryHooks.onPrepareMessages
}
if (existingHooks.onEnd && supermemoryHooks.onEnd) {
const existingOnEnd = existingHooks.onEnd
const supermemoryOnEnd = supermemoryHooks.onEnd
mergedHooks.onEnd = async (args) => {
await supermemoryOnEnd(args)
await existingOnEnd(args)
}
} else if (supermemoryHooks.onEnd) {
mergedHooks.onEnd = supermemoryHooks.onEnd
}
if (existingHooks.onStart && supermemoryHooks.onStart) {
const existingOnStart = existingHooks.onStart
const supermemoryOnStart = supermemoryHooks.onStart
mergedHooks.onStart = async (args) => {
await existingOnStart(args)
await supermemoryOnStart(args)
}
} else if (supermemoryHooks.onStart) {
mergedHooks.onStart = supermemoryHooks.onStart
}
return mergedHooks
}

View file

@ -0,0 +1,179 @@
/**
* VoltAgent integration for Supermemory.
*
* Provides a wrapper function that enhances VoltAgent agent configurations
* with Supermemory hooks for automatic memory injection and storage.
*
* @module
*/
import { createSupermemoryHooks, mergeHooks } from "./hooks"
import type { VoltAgentConfig, SupermemoryVoltAgent } from "./types"
/**
* Configuration options for withSupermemory.
*/
interface WithSupermemoryOptions<T extends VoltAgentConfig>
extends SupermemoryVoltAgent {
/**
* The VoltAgent agent configuration to enhance
*/
agentConfig: T
/**
* Required. The container tag/user ID for scoping memories (e.g., "user-123")
*/
containerTag: string
}
/**
* Enhances a VoltAgent agent configuration with Supermemory memory capabilities.
*
* The function injects hooks that automatically:
* - Retrieve relevant memories before LLM calls (via onPrepareMessages)
* - Inject memories into the system prompt
* - Optionally save conversations after completion (via onEnd)
*
* @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.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.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). Default: 0.1
* @param options.limit - Maximum number of memory results to return. Default: 10
* @param options.rerank - If true, rerank results for relevance. Default: false
* @param options.rewriteQuery - If true, AI-rewrite query for better results (+400ms latency). Default: false
* @param options.filters - Advanced AND/OR filters for search
* @param options.include - Control what additional data to include (chunks, documents, etc.)
* @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 - Context for memory extraction (max 1500 chars), guides how memories are understood
* @returns Enhanced agent config with Supermemory hooks injected
*
* @example
* Basic usage with profile memories:
* ```typescript
* import { withSupermemory } from "@supermemory/tools/voltagent"
* import { Agent } from "@voltagent/core"
* import { VercelAIProvider } from "@voltagent/vercel-ai"
* import { openai } from "@ai-sdk/openai"
*
* const configWithMemory = withSupermemory({
* agentConfig: {
* name: "my-agent",
* instructions: "You are a helpful assistant",
* llm: new VercelAIProvider(),
* model: openai("gpt-4o"),
* },
* containerTag: "user-123",
* customId: "conversation-123"
* })
*
* const agent = new Agent(configWithMemory)
* ```
*
* @example
* Advanced usage with full memory mode and conversation saving:
* ```typescript
* const configWithMemory = withSupermemory({
* agentConfig: {
* name: "my-agent",
* instructions: "You are a helpful assistant",
* llm: new VercelAIProvider(),
* model: openai("gpt-4o"),
* },
* containerTag: "user-123", // Required: user/project ID
* mode: "full", // "profile" | "query" | "full"
* addMemory: "always", // "always" | "never"
* customId: "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"
* entityContext: "This is John, a software engineer saving technical discussions",
* metadata: { // Custom metadata
* source: "voltagent",
* version: "1.0"
* }
* })
*
* const agent = new Agent(configWithMemory)
*
* // Use the agent - memories are automatically injected
* const result = await agent.generateText({
* messages: [{ role: "user", content: "What's my favorite programming language?" }]
* })
* ```
*
* @example
* Custom prompt template:
* ```typescript
* const configWithMemory = withSupermemory({
* agentConfig: {
* name: "my-agent",
* instructions: "...",
* llm: new VercelAIProvider(),
* model: openai("gpt-4o"),
* },
* containerTag: "user-123",
* customId: "conversation-123",
* mode: "full",
* promptTemplate: (data) => `
* <user_context>
* ${data.userMemories}
* ${data.generalSearchMemories}
* </user_context>
* `.trim()
* })
*
* const agent = new Agent(configWithMemory)
* ```
*
* @throws {Error} When neither `options.apiKey` nor `process.env.SUPERMEMORY_API_KEY` are set
* @throws {Error} When Supermemory API request fails
*/
export function withSupermemory<T extends VoltAgentConfig>(
options: WithSupermemoryOptions<T>,
): T {
const { agentConfig, containerTag, ...supermemoryOptions } = options
// Create Supermemory hooks (internally creates its own context, validates API key)
const supermemoryHooks = createSupermemoryHooks(
containerTag,
supermemoryOptions,
)
// Merge with existing hooks if present
const mergedHooks = mergeHooks(agentConfig.hooks, supermemoryHooks)
// Return enhanced config with merged hooks
return {
...agentConfig,
hooks: mergedHooks,
}
}
// Export types for consumers
export type {
SupermemoryVoltAgent,
VoltAgentConfig,
VoltAgentMessage,
VoltAgentHooks,
SearchFilters,
IncludeOptions,
PromptTemplate,
MemoryMode,
AddMemoryMode,
MemoryPromptData,
} from "./types"
export type { WithSupermemoryOptions }
// Note: WithSupermemoryOptions is exported above separately because it's generic
// Export hook creation utilities for advanced use cases
export { createSupermemoryHooks } from "./hooks"

View file

@ -0,0 +1,481 @@
/**
* Middleware utilities for VoltAgent integration with Supermemory.
*
* Provides memory retrieval, injection, and storage functionality.
*/
import Supermemory from "supermemory"
import {
addConversation,
type ConversationMessage,
} from "../conversations-client"
import {
createLogger,
normalizeBaseUrl,
MemoryCache,
buildMemoriesText,
extractQueryText,
type Logger,
type MemoryMode,
} from "../shared"
import type { SupermemoryVoltAgent, VoltAgentMessage } from "./types"
/**
* Context for Supermemory middleware operations.
*/
export interface SupermemoryMiddlewareContext {
client: Supermemory
logger: Logger
containerTag: string
customId: string
mode: MemoryMode
addMemory: "always" | "never"
normalizedBaseUrl: string
apiKey: string
promptTemplate?: (data: {
userMemories: string
generalSearchMemories: string
searchResults: Array<{ memory: string; metadata?: Record<string, unknown> }>
}) => string
/**
* Per-turn memory cache. Stores the injected memories string for each
* user turn (keyed by turnKey) to avoid redundant API calls.
*/
memoryCache: MemoryCache<string>
// New search parameters
threshold?: number
limit?: number
rerank?: boolean
rewriteQuery?: boolean
filters?: { OR: Array<unknown> } | { AND: Array<unknown> }
include?: {
chunks?: boolean
documents?: boolean
forgottenMemories?: boolean
relatedMemories?: boolean
summaries?: boolean
}
// Storage parameters
metadata?: Record<string, string | number | boolean>
searchMode?: "memories" | "documents" | "hybrid"
entityContext?: string
}
/**
* Creates a Supermemory middleware context.
*/
export const createSupermemoryContext = (
containerTag: string,
options: SupermemoryVoltAgent,
): SupermemoryMiddlewareContext => {
const apiKey = options.apiKey ?? process.env.SUPERMEMORY_API_KEY
if (!apiKey) {
throw new Error(
"SUPERMEMORY_API_KEY is not set — provide it via `options.apiKey` or set `process.env.SUPERMEMORY_API_KEY`",
)
}
const {
customId,
mode = "profile",
addMemory = "always", // VoltAgent default: save conversations by default for chat apps
baseUrl,
promptTemplate,
threshold,
limit,
rerank,
rewriteQuery,
filters,
include,
metadata,
searchMode,
entityContext,
} = options
// Runtime validation: customId is required
if (!customId || typeof customId !== "string" || customId.trim() === "") {
throw new Error(
"customId is required and must be a non-empty string — provide it via `options.customId`",
)
}
const logger = createLogger(false) // VoltAgent SDK doesn't use verbose
const normalizedBaseUrl = normalizeBaseUrl(baseUrl)
const client = new Supermemory({
apiKey,
...(normalizedBaseUrl !== "https://api.supermemory.ai"
? { baseURL: normalizedBaseUrl }
: {}),
})
return {
client,
logger,
containerTag,
customId,
mode,
addMemory,
normalizedBaseUrl,
apiKey,
promptTemplate,
memoryCache: new MemoryCache<string>(),
threshold,
limit,
rerank,
rewriteQuery,
filters,
include,
metadata,
searchMode,
entityContext,
}
}
/**
* Generates a cache key for the current turn based on context and user message.
*/
const makeTurnKey = (
ctx: SupermemoryMiddlewareContext,
userMessage: string,
): string => {
return MemoryCache.makeTurnKey(
ctx.containerTag,
ctx.customId,
ctx.mode,
userMessage,
)
}
/**
* Checks if this is a new user turn (last message is from user).
*/
const isNewUserTurn = (messages: VoltAgentMessage[]): boolean => {
const lastMessage = messages.at(-1)
return lastMessage?.role === "user"
}
/**
* Extracts the last user message text from messages array.
*/
const getLastUserMessage = (messages: VoltAgentMessage[]): string => {
const lastUserMessage = messages
.slice()
.reverse()
.find((msg) => msg.role === "user")
if (!lastUserMessage) {
return ""
}
const content = lastUserMessage.content
if (typeof content === "string") {
return content
}
if (Array.isArray(content)) {
return content
.filter((part) => part.type === "text")
.map((part) => part.text || "")
.join(" ")
}
return ""
}
/**
* Retrieves and injects memories into messages.
* Returns enhanced messages with memories injected into system prompt.
*
* @param searchMessages - Messages to search for user input (VoltAgent's input messages)
* @param ctx - Supermemory middleware context
* @param systemMessages - System messages to inject memories into (VoltAgent's prepared messages)
*/
export const enhanceMessagesWithMemories = async (
searchMessages: VoltAgentMessage[],
ctx: SupermemoryMiddlewareContext,
systemMessages?: VoltAgentMessage[],
): Promise<VoltAgentMessage[]> => {
const messagesToEnhance = systemMessages || searchMessages
const messages = searchMessages
const userMessage = getLastUserMessage(messages)
if (ctx.mode !== "profile" && !userMessage) {
ctx.logger.debug("No user message found, skipping memory search")
return messagesToEnhance
}
const turnKey = makeTurnKey(ctx, userMessage || "")
const isNewTurn = isNewUserTurn(messages)
const cachedMemories = ctx.memoryCache.get(turnKey)
if (!isNewTurn && cachedMemories) {
ctx.logger.debug("Using cached memories", { turnKey })
return injectMemoriesIntoMessages(
messagesToEnhance,
cachedMemories,
ctx.logger,
)
}
ctx.logger.info("Starting memory search", {
containerTag: ctx.containerTag,
customId: ctx.customId,
mode: ctx.mode,
isNewTurn,
})
const genericMessages = messages.map((msg) => ({
role: msg.role,
content: msg.content,
}))
const queryText = extractQueryText(genericMessages, ctx.mode)
const useAdvancedSearch =
ctx.threshold !== undefined ||
ctx.limit !== undefined ||
ctx.rerank !== undefined ||
ctx.rewriteQuery !== undefined ||
ctx.filters !== undefined ||
ctx.include !== undefined ||
ctx.searchMode !== undefined
let memories: string
if (useAdvancedSearch && ctx.mode !== "profile") {
ctx.logger.info("Using advanced search with custom parameters")
const searchParams: {
q: string
containerTag: string
threshold?: number
limit?: number
rerank?: boolean
rewriteQuery?: boolean
filters?: { OR: Array<unknown> } | { AND: Array<unknown> }
include?: {
chunks?: boolean
documents?: boolean
forgottenMemories?: boolean
relatedMemories?: boolean
summaries?: boolean
}
searchMode?: "memories" | "documents" | "hybrid"
} = {
q: queryText,
containerTag: ctx.containerTag,
}
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.include !== undefined) searchParams.include = ctx.include
if (ctx.searchMode !== undefined) searchParams.searchMode = ctx.searchMode
const response = await ctx.client.search.memories(searchParams)
// Hybrid search returns both memory entries (`memory` field) and
// document chunks (`chunk` field). Handle both.
type SearchResult = {
memory?: string
chunk?: string
metadata?: Record<string, unknown>
}
const formattedMemories = response.results
.map((result: SearchResult) => {
const text = result.memory || result.chunk
return text ? `- ${text}` : null
})
.filter(Boolean)
.join("\n")
memories = ctx.promptTemplate
? ctx.promptTemplate({
userMemories: "",
generalSearchMemories: formattedMemories,
searchResults: response.results as Array<{
memory: string
metadata?: Record<string, unknown>
}>,
})
: `The following are relevant memories and context about this user retrieved from previous interactions. Use these to personalize your response:\n\n${formattedMemories}`
} else {
memories = await buildMemoriesText({
containerTag: ctx.containerTag,
queryText,
mode: ctx.mode,
baseUrl: ctx.normalizedBaseUrl,
apiKey: ctx.apiKey,
logger: ctx.logger,
promptTemplate: ctx.promptTemplate,
})
}
ctx.memoryCache.set(turnKey, memories)
ctx.logger.debug("Cached memories for turn", { turnKey })
return injectMemoriesIntoMessages(messagesToEnhance, memories, ctx.logger)
}
/**
* Injects memories into messages by appending to existing system prompt
* or creating a new one. Pure function - does not mutate the original messages.
*
* VoltAgent uses AI SDK v5's UIMessage format which requires `id` and `parts`
* (not just `content`). We must conform to this format for messages to
* actually reach the LLM.
*/
const injectMemoriesIntoMessages = (
messages: VoltAgentMessage[],
memories: string,
logger: Logger,
): VoltAgentMessage[] => {
const systemMessageIndex = messages.findIndex((msg) => msg.role === "system")
if (systemMessageIndex !== -1) {
logger.debug("Added memories to existing system message")
const newMessages = [...messages]
const systemMessage = newMessages[systemMessageIndex]
if (!systemMessage) {
return messages
}
// Extract existing text from parts (UIMessage format) or content fallback
const parts = (
systemMessage as { parts?: Array<{ type: string; text?: string }> }
).parts
const existingContent = parts
? parts
.filter((p) => p.type === "text")
.map((p) => p.text || "")
.join("\n")
: typeof systemMessage.content === "string"
? systemMessage.content
: ""
const newContent = `${existingContent}\n\n${memories}`
newMessages[systemMessageIndex] = {
...systemMessage,
content: newContent,
// Update parts array to match - this is what the LLM actually reads
parts: [{ type: "text", text: newContent }],
} as VoltAgentMessage
return newMessages
}
logger.debug("Created system message with memories")
return [
{
id: crypto.randomUUID(),
role: "system" as const,
content: memories,
parts: [{ type: "text", text: memories }],
} as VoltAgentMessage,
...messages,
]
}
/**
* Converts VoltAgent messages to conversation format for storage.
*/
const convertToConversationMessages = (
messages: VoltAgentMessage[],
): ConversationMessage[] => {
const conversationMessages: ConversationMessage[] = []
for (const msg of messages) {
if (msg.role === "system") {
continue
}
if (typeof msg.content === "string") {
if (msg.content) {
conversationMessages.push({
role: msg.role as "user" | "assistant" | "tool",
content: msg.content,
})
}
} else if (Array.isArray(msg.content)) {
const contentParts = msg.content
.map((c) => {
if (c.type === "text" && c.text) {
return {
type: "text" as const,
text: c.text,
}
}
// Handle image URLs if present
if (c.type === "image_url" && typeof c.image_url === "object") {
const imageUrl = c.image_url as { url?: string }
if (imageUrl.url) {
return {
type: "image_url" as const,
image_url: { url: imageUrl.url },
}
}
}
return null
})
.filter((part) => part !== null)
if (contentParts.length > 0) {
conversationMessages.push({
role: msg.role as "user" | "assistant" | "tool",
content: contentParts,
})
}
}
}
return conversationMessages
}
/**
* Saves conversation to Supermemory (fire-and-forget).
*/
export const saveConversation = async (
messages: VoltAgentMessage[],
ctx: SupermemoryMiddlewareContext,
): Promise<void> => {
if (ctx.addMemory !== "always") {
return
}
try {
const conversationMessages = convertToConversationMessages(messages)
if (conversationMessages.length === 0) {
ctx.logger.debug("No messages to save")
return
}
const response = await addConversation({
conversationId: ctx.customId,
messages: conversationMessages,
containerTags: [ctx.containerTag],
metadata: ctx.metadata,
entityContext: ctx.entityContext,
apiKey: ctx.apiKey,
baseUrl: ctx.normalizedBaseUrl,
})
ctx.logger.info("Conversation saved successfully via /v4/conversations", {
containerTag: ctx.containerTag,
customId: ctx.customId,
messageCount: conversationMessages.length,
responseId: response.id,
metadata: ctx.metadata,
})
} catch (error) {
ctx.logger.error("Error saving conversation", {
error: error instanceof Error ? error.message : "Unknown error",
})
}
}

View file

@ -0,0 +1,210 @@
/**
* Type definitions for VoltAgent integration.
*
* VoltAgent uses hooks to intercept and modify agent behavior. We integrate
* Supermemory by providing hooks that inject memories before LLM calls.
*/
import type {
PromptTemplate,
MemoryMode,
AddMemoryMode,
MemoryPromptData,
SupermemoryBaseOptions,
} from "../shared"
/**
* Configuration options for the Supermemory VoltAgent integration.
* Extends base options with VoltAgent-specific settings.
*/
export interface SupermemoryVoltAgent
extends Omit<SupermemoryBaseOptions, "verbose"> {
/**
* Custom ID to group messages into a single document.
* Ensures related messages are added to the same document for that conversation.
*/
customId: string
/**
* Threshold / sensitivity for memory selection. 0 is least sensitive (returns
* most memories, more results), 1 is most sensitive (returns fewer memories,
* more accurate results). Default: 0.1
*/
threshold?: number
/**
* Maximum number of memory results to return. Default: 10
*/
limit?: number
/**
* If true, rerank the results based on the query. This helps ensure the most
* relevant results are returned. Default: false
*/
rerank?: boolean
/**
* If true, rewrites the query to make it easier to find memories. This increases
* latency by about 400ms. Default: false
*/
rewriteQuery?: boolean
/**
* Advanced filters to apply to the search using AND/OR logic.
* Example: { OR: [{ metadata: { type: "note" } }, { metadata: { type: "conversation" } }] }
*/
filters?: SearchFilters
/**
* Control what additional data to include in search results
*/
include?: IncludeOptions
/**
* Optional metadata to attach to saved documents/conversations.
* Can include strings, numbers, or booleans.
*/
metadata?: Record<string, string | number | boolean>
/**
* Search mode controlling what type of results to search.
* - "memories": Search only memory entries (atomic facts)
* - "documents": Search only document chunks
* - "hybrid": Search both memories AND document chunks (recommended)
*/
searchMode?: "memories" | "documents" | "hybrid"
/**
* Context for memory extraction when saving conversations.
* Helps guide how memories are extracted and understood from content.
* Max 1500 characters.
* Example: "This is John, saving items in a personal knowledge management system"
*/
entityContext?: string
}
/**
* Advanced search filters using AND/OR logic
*/
export type SearchFilters = { OR: Array<unknown> } | { AND: Array<unknown> }
/**
* Options for including additional data in search results
*/
export interface IncludeOptions {
/**
* If true, fetch and return chunks from documents associated with found memories.
* Performs vector search on chunks within those documents.
*/
chunks?: boolean
/**
* If true, include full document information in results
*/
documents?: boolean
/**
* If true, include forgotten memories in search results. Forgotten memories are
* memories that have been explicitly forgotten or have passed their expiration date.
*/
forgottenMemories?: boolean
/**
* If true, include related memories (parents/children in the memory graph)
*/
relatedMemories?: boolean
/**
* If true, include document summaries in results
*/
summaries?: boolean
}
/**
* VoltAgent message format (simplified to avoid direct dependency).
* Compatible with VoltAgent's Message type.
*/
export interface VoltAgentMessage {
role: "system" | "user" | "assistant" | "tool"
content:
| string
| Array<{ type: string; text?: string; [key: string]: unknown }>
[key: string]: unknown
}
/**
* Minimal VoltAgent AgentConfig interface representing properties we enhance.
* This avoids a direct dependency on @voltagent/core while staying type-safe.
*/
export interface VoltAgentConfig {
name: string
instructions?: string
model?: unknown
llm?: unknown
hooks?: VoltAgentHooks
[key: string]: unknown
}
/**
* VoltAgent hooks interface (simplified).
* Hooks allow intercepting agent lifecycle events.
*/
export interface VoltAgentHooks {
onStart?: (args: HookStartArgs) => void | Promise<void>
onPrepareMessages?: (
args: HookPrepareMessagesArgs,
) =>
| { messages?: VoltAgentMessage[] }
| Promise<{ messages?: VoltAgentMessage[] }>
onEnd?: (args: HookEndArgs) => void | Promise<void>
[key: string]: unknown
}
/**
* Arguments passed to onStart hook.
*/
export interface HookStartArgs {
agent: {
name: string
[key: string]: unknown
}
context?: {
messages?: VoltAgentMessage[]
[key: string]: unknown
}
[key: string]: unknown
}
/**
* Arguments passed to onPrepareMessages hook.
*/
export interface HookPrepareMessagesArgs {
messages: VoltAgentMessage[]
agent: {
name: string
[key: string]: unknown
}
context?: {
[key: string]: unknown
}
[key: string]: unknown
}
/**
* Arguments passed to onEnd hook.
*/
export interface HookEndArgs {
agent: {
name: string
[key: string]: unknown
}
context?: {
input?: unknown
[key: string]: unknown
}
output?: unknown
[key: string]: unknown
}
// Re-export shared types for convenience
export type { PromptTemplate, MemoryMode, AddMemoryMode, MemoryPromptData }

View file

@ -1,27 +1,17 @@
import { streamText, type ModelMessage } from "ai"
import { openai } from "@ai-sdk/openai"
import { withSupermemory } from "../../../../../src/vercel"
import { gateway, streamText, type ModelMessage } from "ai"
import { withSupermemory } from "@supermemory/tools/ai-sdk"
const model = withSupermemory(openai("gpt-4"), "user-123", {
const model = withSupermemory(gateway("google/gemini-2.5-flash"), "user-1", {
apiKey: process.env.SUPERMEMORY_API_KEY ?? "",
mode: "full",
addMemory: "always",
conversationId: "chat-session",
verbose: true,
baseUrl: process.env.SUPERMEMORY_BASE_URL,
})
export async function POST(req: Request) {
const { messages }: { messages: ModelMessage[] } = await req.json()
// Commented out generateText implementation
// const { response } = await generateText({
// model,
// system: "You are a helpful assistant.",
// messages,
// })
// return Response.json({ messages: response.messages })
// New streaming implementation
const result = await streamText({
const result = streamText({
model,
system: "You are a helpful assistant.",
messages,

View file

@ -1,6 +1,5 @@
import { OpenAI } from "openai"
//import { withSupermemory } from "@supermemory/tools/openai"
import { withSupermemory } from "../../../../../src/openai"
import { withSupermemory } from "@supermemory/tools/openai"
export const runtime = "nodejs"

View file

@ -1,22 +1,51 @@
import { convertToModelMessages, streamText, type UIMessage } from "ai"
import { openai } from "@ai-sdk/openai"
import { withSupermemory } from "../../../../../src/vercel"
import { convertToModelMessages, gateway, streamText, type UIMessage } from "ai"
import { withTracing } from "@posthog/ai"
import { withSupermemory } from "../../../../../src/ai-sdk"
import { PostHog } from "posthog-node"
const model = withSupermemory(openai("gpt-4"), "user-123", {
mode: "full",
addMemory: "always",
conversationId: "chat-session",
verbose: true,
const SUPERMEMORY_USER_ID = "user-1"
const gatewayModel = gateway("google/gemini-2.5-flash")
const supermemoryOptions = {
apiKey: process.env.SUPERMEMORY_API_KEY ?? "",
mode: "full" as const,
addMemory: "always" as const,
baseUrl: process.env.SUPERMEMORY_BASE_URL,
})
}
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json()
const posthogApiKey = process.env.POSTHOG_API_KEY
const phClient = posthogApiKey
? new PostHog(posthogApiKey, {
host: process.env.POSTHOG_HOST ?? "https://us.i.posthog.com",
})
: null
const innerModel = phClient
? withTracing(gatewayModel, phClient, {
posthogDistinctId: SUPERMEMORY_USER_ID,
posthogProperties: { route: "api/stream" },
})
: gatewayModel
const model = withSupermemory(
innerModel,
SUPERMEMORY_USER_ID,
supermemoryOptions,
)
const result = streamText({
model,
system: "You are a helpful assistant.",
messages: convertToModelMessages(messages),
messages: await convertToModelMessages(messages),
onFinish: phClient
? async () => {
await phClient.shutdown()
}
: undefined,
})
return result.toUIMessageStreamResponse()

View file

@ -1,14 +1,13 @@
"use client"
import { DefaultChatTransport } from "ai"
import { useChat } from "@ai-sdk/react"
import { DefaultChatTransport } from "ai"
import { useState } from "react"
export default function Page() {
const [input, setInput] = useState("")
const { messages, sendMessage, status } = useChat({
// @ts-expect-error - Type mismatch between ai and @ai-sdk/react versions
transport: new DefaultChatTransport({
api: "/api/stream",
}),

File diff suppressed because it is too large Load diff

View file

@ -9,23 +9,28 @@
"lint": "eslint"
},
"dependencies": {
"@ai-sdk/react": "3.0.160",
"@posthog/ai": "^7.3.0",
"@supermemory/tools": "1.4.1",
"ai": "6.0.158",
"next": "16.0.0",
"openai": "^4.104.0",
"posthog-node": "^5.0.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"next": "16.0.0",
"ai": "^4.0.0",
"@ai-sdk/openai": "^1.0.0",
"openai": "^4.104.0",
"supermemory": "^1.0.0",
"@supermemory/tools": "workspace:*"
"zod": "^4.1.8"
},
"devDependencies": {
"typescript": "^5",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "16.0.0"
"eslint-config-next": "16.0.0",
"tailwindcss": "^4",
"typescript": "^5"
},
"overrides": {
"ai": "6.0.158"
}
}

View file

@ -86,6 +86,25 @@ describe("Unit: withSupermemory", () => {
expect(wrappedModel).toBeDefined()
expect(wrappedModel.specificationVersion).toBe("v2")
})
it("should preserve provider, modelId, and spec when they live on the prototype", () => {
process.env.SUPERMEMORY_API_KEY = "test-key"
const proto: LanguageModelV2 = {
specificationVersion: "v2",
provider: "gateway",
modelId: "google/gemini-2.5-flash",
supportedUrls: {},
doGenerate: vi.fn(),
doStream: vi.fn(),
}
const inner = Object.create(proto) as LanguageModelV2
const wrappedModel = withSupermemory(inner, TEST_CONFIG.containerTag)
expect(wrappedModel.specificationVersion).toBe("v2")
expect(wrappedModel.provider).toBe("gateway")
expect(wrappedModel.modelId).toBe("google/gemini-2.5-flash")
})
})
describe("Memory caching", () => {

View file

@ -7,6 +7,7 @@ export default defineConfig({
"src/claude-memory.ts",
"src/openai/index.ts",
"src/mastra.ts",
"src/voltagent/index.ts",
],
format: "esm",
sourcemap: false,