mastra integration (#717)

adds withSupermemory wrapper and input/output processors for
mastra agents:

- input processor fetches and injects memories into system prompt
before llm calls
- output processor saves conversations to supermemory after
responses
- supports profile, query, and full memory search modes
- includes custom prompt templates and requestcontext support

const agent = new Agent(withSupermemory(
{ id: "my-assistant", model: openai("gpt-4o"), instructions:
"..." },
"user-123",
{ mode: "full", addMemory: "always", threadId: "conv-456" }
))

includes docs as well

this pr also reworks how the tools package works into shared modules
This commit is contained in:
nexxeln 2026-02-03 00:43:08 +00:00
parent b7e6eb65b9
commit 9553434c9a
38 changed files with 3702 additions and 428 deletions

View file

@ -153,6 +153,7 @@
"integrations/supermemory-sdk",
"integrations/ai-sdk",
"integrations/openai",
"integrations/mastra",
"integrations/langchain",
"integrations/memory-graph",
"integrations/claude-memory",

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 34 21">
<path fill="currentColor" d="M4.5 11.7a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9M10.4 0a4.5 4.5 0 0 1 4.4 5.5c-.3 1.4-.6 3 .2 4.2l1.3 1.8.3.2q.2 0 .3-.2l1.3-1.9c.8-1.1.5-2.7.2-4a4.5 4.5 0 1 1 8.8 0c-.3 1.3-.6 2.8 0 4l1.3 2a4.5 4.5 0 1 1-4.3 3.5c.3-1.3.6-2.8 0-4l-1.2-2h-.2L21.5 11c-.8 1.2-.5 2.8-.2 4.2a4.5 4.5 0 1 1-8.8.2q.5-2-.4-3.8l-.9-1.3q-.9-1.1-2.4-1.6A4.5 4.5 0 0 1 10.4 0"/>
</svg>

After

Width:  |  Height:  |  Size: 459 B

View file

@ -0,0 +1,450 @@
---
title: "Mastra"
sidebarTitle: "Mastra"
description: "Add persistent memory to Mastra AI agents with Supermemory processors"
icon: "/images/mastra-icon.svg"
---
Integrate Supermemory with [Mastra](https://mastra.ai) to give your AI agents persistent memory. Use the `withSupermemory` wrapper for zero-config setup or processors for fine-grained control.
<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 @mastra/core
```
## Quick Start
Wrap your agent config with `withSupermemory` to add memory capabilities:
```typescript
import { Agent } from "@mastra/core/agent"
import { withSupermemory } from "@supermemory/tools/mastra"
import { openai } from "@ai-sdk/openai"
// Create agent with memory-enhanced config
const agent = new Agent(withSupermemory(
{
id: "my-assistant",
name: "My Assistant",
model: openai("gpt-4o"),
instructions: "You are a helpful assistant.",
},
"user-123", // containerTag - scopes memories to this user
{
mode: "full",
addMemory: "always",
threadId: "conv-456",
}
))
const response = await agent.generate("What do you know about me?")
```
<Note>
**Memory saving is disabled by default.** The wrapper only retrieves existing memories. To automatically save conversations:
```typescript
const agent = new Agent(withSupermemory(
{ id: "my-assistant", model: openai("gpt-4o"), ... },
"user-123",
{
addMemory: "always",
threadId: "conv-456" // Required for conversation grouping
}
))
```
</Note>
---
## How It Works
The Mastra integration uses Mastra's native [Processor](https://mastra.ai/docs/agents/processors) interface:
1. **Input Processor** - Fetches relevant memories from Supermemory and injects them into the system prompt before the LLM call
2. **Output Processor** - Optionally saves the conversation to Supermemory after generation completes
```mermaid
sequenceDiagram
participant User
participant Agent
participant InputProcessor
participant LLM
participant OutputProcessor
participant Supermemory
User->>Agent: Send message
Agent->>InputProcessor: Process input
InputProcessor->>Supermemory: Fetch memories
Supermemory-->>InputProcessor: Return memories
InputProcessor->>Agent: Inject into system prompt
Agent->>LLM: Generate response
LLM-->>Agent: Return response
Agent->>OutputProcessor: Process output
OutputProcessor->>Supermemory: Save conversation (if enabled)
Agent-->>User: Return response
```
---
## Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiKey` | `string` | `SUPERMEMORY_API_KEY` env | Your Supermemory API key |
| `baseUrl` | `string` | `https://api.supermemory.ai` | Custom API endpoint |
| `mode` | `"profile" \| "query" \| "full"` | `"profile"` | Memory search mode |
| `addMemory` | `"always" \| "never"` | `"never"` | Auto-save conversations |
| `threadId` | `string` | - | Conversation ID for grouping messages |
| `verbose` | `boolean` | `false` | Enable debug logging |
| `promptTemplate` | `function` | - | Custom memory formatting |
---
## Memory Search Modes
**Profile Mode (Default)** - Retrieves the user's complete profile without query-based filtering:
```typescript
const agent = new Agent(withSupermemory(config, "user-123", { mode: "profile" }))
```
**Query Mode** - Searches memories based on the user's message:
```typescript
const agent = new Agent(withSupermemory(config, "user-123", { mode: "query" }))
```
**Full Mode** - Combines profile AND query-based search for maximum context:
```typescript
const agent = new Agent(withSupermemory(config, "user-123", { mode: "full" }))
### Mode Comparison
| Mode | Description | Use Case |
|------|-------------|----------|
| `profile` | Static + dynamic user facts | General personalization |
| `query` | Semantic search on user message | Specific Q&A |
| `full` | Both profile and search | Chatbots, assistants |
---
## Saving Conversations
Enable automatic conversation saving with `addMemory: "always"`. A `threadId` is required to group messages:
```typescript
const agent = new Agent(withSupermemory(
{ id: "my-assistant", model: openai("gpt-4o"), instructions: "..." },
"user-123",
{
addMemory: "always",
threadId: "conv-456",
}
))
// All messages in this conversation are saved
await agent.generate("I prefer TypeScript over JavaScript")
await agent.generate("My favorite framework is Next.js")
```
<Warning>
Without a `threadId`, the output processor will log a warning and skip saving. Always provide a `threadId` when using `addMemory: "always"`.
</Warning>
---
## Custom Prompt Templates
Customize how memories are formatted and injected:
```typescript
import { Agent } from "@mastra/core/agent"
import { withSupermemory } from "@supermemory/tools/mastra"
import type { MemoryPromptData } from "@supermemory/tools/mastra"
const claudePrompt = (data: MemoryPromptData) => `
<context>
<user_profile>
${data.userMemories}
</user_profile>
<relevant_memories>
${data.generalSearchMemories}
</relevant_memories>
</context>
`.trim()
const agent = new Agent(withSupermemory(
{ id: "my-assistant", model: openai("gpt-4o"), instructions: "..." },
"user-123",
{
mode: "full",
promptTemplate: claudePrompt,
}
))
```
---
## Direct Processor Usage
For advanced use cases, use processors directly instead of the wrapper:
### Input Processor Only
Inject memories without saving conversations:
```typescript
import { Agent } from "@mastra/core/agent"
import { createSupermemoryProcessor } from "@supermemory/tools/mastra"
import { openai } from "@ai-sdk/openai"
const agent = new Agent({
id: "my-assistant",
name: "My Assistant",
model: openai("gpt-4o"),
inputProcessors: [
createSupermemoryProcessor("user-123", {
mode: "full",
verbose: true,
}),
],
})
```
### Output Processor Only
Save conversations without memory injection:
```typescript
import { Agent } from "@mastra/core/agent"
import { createSupermemoryOutputProcessor } from "@supermemory/tools/mastra"
import { openai } from "@ai-sdk/openai"
const agent = new Agent({
id: "my-assistant",
name: "My Assistant",
model: openai("gpt-4o"),
outputProcessors: [
createSupermemoryOutputProcessor("user-123", {
addMemory: "always",
threadId: "conv-456",
}),
],
})
```
### Both Processors
Use the factory function for shared configuration:
```typescript
import { Agent } from "@mastra/core/agent"
import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
import { openai } from "@ai-sdk/openai"
const { input, output } = createSupermemoryProcessors("user-123", {
mode: "full",
addMemory: "always",
threadId: "conv-456",
verbose: true,
})
const agent = new Agent({
id: "my-assistant",
name: "My Assistant",
model: openai("gpt-4o"),
inputProcessors: [input],
outputProcessors: [output],
})
```
---
## Using RequestContext
Mastra's `RequestContext` can provide `threadId` dynamically:
```typescript
import { Agent } from "@mastra/core/agent"
import { RequestContext, MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context"
import { withSupermemory } from "@supermemory/tools/mastra"
import { openai } from "@ai-sdk/openai"
const agent = new Agent(withSupermemory(
{ id: "my-assistant", model: openai("gpt-4o"), instructions: "..." },
"user-123",
{
mode: "full",
addMemory: "always",
// threadId not set - will use RequestContext
}
))
// Set threadId dynamically via RequestContext
const ctx = new RequestContext()
ctx.set(MASTRA_THREAD_ID_KEY, "dynamic-thread-id")
await agent.generate("Hello!", { requestContext: ctx })
```
---
## Verbose Logging
Enable detailed logging for debugging:
```typescript
const agent = new Agent(withSupermemory(
{ id: "my-assistant", model: openai("gpt-4o"), instructions: "..." },
"user-123",
{ verbose: true }
))
// Console output:
// [supermemory] Starting memory search { containerTag: "user-123", mode: "profile" }
// [supermemory] Found 5 memories
// [supermemory] Injected memories into system prompt { length: 1523 }
```
---
## Working with Existing Processors
The wrapper correctly merges with existing processors in the config:
```typescript
// Supermemory processors are merged correctly:
// - Input: [supermemory, myLogging] (supermemory runs first)
// - Output: [myAnalytics, supermemory] (supermemory runs last)
const agent = new Agent(withSupermemory(
{
id: "my-assistant",
model: openai("gpt-4o"),
inputProcessors: [myLoggingProcessor],
outputProcessors: [myAnalyticsProcessor],
},
"user-123"
))
```
---
## API Reference
### `withSupermemory`
Enhances a Mastra agent config with memory capabilities.
```typescript
function withSupermemory<T extends AgentConfig>(
config: T,
containerTag: string,
options?: SupermemoryMastraOptions
): T
```
**Parameters:**
- `config` - The Mastra agent configuration object
- `containerTag` - User/container ID for scoping memories
- `options` - Configuration options
**Returns:** Enhanced config with Supermemory processors injected
### `createSupermemoryProcessor`
Creates an input processor for memory injection.
```typescript
function createSupermemoryProcessor(
containerTag: string,
options?: SupermemoryMastraOptions
): SupermemoryInputProcessor
```
### `createSupermemoryOutputProcessor`
Creates an output processor for conversation saving.
```typescript
function createSupermemoryOutputProcessor(
containerTag: string,
options?: SupermemoryMastraOptions
): SupermemoryOutputProcessor
```
### `createSupermemoryProcessors`
Creates both processors with shared configuration.
```typescript
function createSupermemoryProcessors(
containerTag: string,
options?: SupermemoryMastraOptions
): {
input: SupermemoryInputProcessor
output: SupermemoryOutputProcessor
}
```
### `SupermemoryMastraOptions`
```typescript
interface SupermemoryMastraOptions {
apiKey?: string
baseUrl?: string
mode?: "profile" | "query" | "full"
addMemory?: "always" | "never"
threadId?: string
verbose?: boolean
promptTemplate?: (data: MemoryPromptData) => string
}
```
---
## Environment Variables
```bash
SUPERMEMORY_API_KEY=your_supermemory_key
```
---
## Error Handling
Processors gracefully handle errors without breaking the agent:
- **API errors** - Logged and skipped; agent continues without memories
- **Missing API key** - Throws immediately with helpful error message
- **Missing threadId** - Warns in console; skips saving
```typescript
// Missing API key throws immediately
const agent = new Agent(withSupermemory(
{ id: "my-assistant", model: openai("gpt-4o"), instructions: "..." },
"user-123",
{ apiKey: undefined } // Will check SUPERMEMORY_API_KEY env
))
// Error: SUPERMEMORY_API_KEY is not set
```
---
## Next Steps
<CardGroup cols={2}>
<Card title="Vercel AI SDK" icon="triangle" href="/integrations/ai-sdk">
Use with Vercel AI SDK for streamlined development
</Card>
<Card title="User Profiles" icon="user" href="/user-profiles">
Learn about user profile management
</Card>
</CardGroup>

View file

@ -15,7 +15,7 @@ const conversation = [
// Get user profile + relevant memories for context
const profile = await client.profile({
containerTag: USER_ID,
q: conversation.at(-1)!.content,
q: conversation.at(-1)?.content,
})
const context = `Static profile:
@ -25,10 +25,10 @@ Dynamic profile:
${profile.profile.dynamic.join("\n")}
Relevant memories:
${profile.searchResults?.results.map((r) => r["content"]).join("\n")}`
${profile.searchResults?.results.map((r) => r.content).join("\n")}`
// Build messages with memory-enriched context
const messages = [
const _messages = [
{ role: "system", content: `User context:\n${context}` },
...conversation,
]

View file

@ -400,7 +400,10 @@ export function Header({
</DropdownMenu>
)}
</div>
<FeedbackModal isOpen={isFeedbackOpen} onClose={() => setIsFeedbackOpen(false)} />
<FeedbackModal
isOpen={isFeedbackOpen}
onClose={() => setIsFeedbackOpen(false)}
/>
</div>
)
}

View file

@ -67,9 +67,9 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
"correct" | "incorrect" | null
>(null)
const [isConfirmed, setIsConfirmed] = useState(false)
const [processingByUrl, setProcessingByUrl] = useState<Record<string, boolean>>(
{},
)
const [processingByUrl, setProcessingByUrl] = useState<
Record<string, boolean>
>({})
const displayedMemoriesRef = useRef<Set<string>>(new Set())
const contextInjectedRef = useRef(false)
const draftsBuiltRef = useRef(false)

View file

@ -36,11 +36,14 @@ export function NewOnboardingModal() {
}
return (
<Dialog open={open} onOpenChange={(isOpen) => {
if (!isOpen) {
setOpen(false)
}
}}>
<Dialog
open={open}
onOpenChange={(isOpen) => {
if (!isOpen) {
setOpen(false)
}
}}
>
<DialogContent onInteractOutside={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle>Experience the new onboarding</DialogTitle>

View file

@ -18,9 +18,7 @@ interface AuthContextType {
user: SessionData["user"] | null
org: Organization | null
setActiveOrg: (orgSlug: string) => Promise<void>
updateOrgMetadata: (
partial: Record<string, unknown>,
) => void
updateOrgMetadata: (partial: Record<string, unknown>) => void
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
@ -39,21 +37,18 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setOrg(activeOrg)
}
const updateOrgMetadata = useCallback(
(partial: Record<string, unknown>) => {
setOrg((prev) => {
if (!prev) return prev
return {
...prev,
metadata: {
...prev.metadata,
...partial,
},
}
})
},
[],
)
const updateOrgMetadata = useCallback((partial: Record<string, unknown>) => {
setOrg((prev) => {
if (!prev) return prev
return {
...prev,
metadata: {
...prev.metadata,
...partial,
},
}
})
}, [])
// biome-ignore lint/correctness/useExhaustiveDependencies: ignoring the setActiveOrg dependency
useEffect(() => {

View file

@ -1,8 +1,8 @@
# @supermemory/tools
Memory tools for AI SDK and OpenAI function calling with supermemory
Memory tools for AI SDK, OpenAI, and Mastra with supermemory
This package provides supermemory tools for both AI SDK and OpenAI function calling through dedicated submodule exports, each with function-based architectures optimized for their respective use cases.
This package provides supermemory tools for AI SDK, OpenAI, and Mastra through dedicated submodule exports, each with function-based architectures optimized for their respective use cases.
## Installation
@ -12,9 +12,10 @@ npm install @supermemory/tools
## Usage
The package provides two submodule imports:
The package provides three submodule imports:
- `@supermemory/tools/ai-sdk` - For use with the AI SDK framework (includes `withSupermemory` middleware)
- `@supermemory/tools/openai` - For use with OpenAI SDK (includes `withSupermemory` middleware and function calling tools)
- `@supermemory/tools/mastra` - For use with Mastra AI agents (includes `withSupermemory` wrapper and processors)
### AI SDK Usage
@ -405,6 +406,194 @@ const addResult = await tools.addMemory({
})
```
### Mastra Usage
Add persistent memory to [Mastra](https://mastra.ai) AI agents. The integration provides processors that:
- **Input Processor**: Fetches relevant memories and injects them into the system prompt before LLM calls
- **Output Processor**: Optionally saves conversations to Supermemory after responses
#### Quick Start with `withSupermemory` Wrapper
The simplest way to add memory to a Mastra agent - wrap your config before creating the Agent:
```typescript
import { Agent } from "@mastra/core/agent"
import { withSupermemory } from "@supermemory/tools/mastra"
import { openai } from "@ai-sdk/openai"
// Create agent with memory-enhanced config
const agent = new Agent(withSupermemory(
{
id: "my-assistant",
name: "My Assistant",
model: openai("gpt-4o"),
instructions: "You are a helpful assistant.",
},
"user-123", // containerTag - scopes memories to this user
{
mode: "full",
addMemory: "always",
threadId: "conv-456",
}
))
const response = await agent.generate("What do you know about me?")
console.log(response.text)
```
#### Direct Processor Usage
For fine-grained control, use processors directly:
```typescript
import { Agent } from "@mastra/core/agent"
import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
import { openai } from "@ai-sdk/openai"
const { input, output } = createSupermemoryProcessors("user-123", {
mode: "full",
addMemory: "always",
threadId: "conv-456",
verbose: true, // Enable logging
})
const agent = new Agent({
id: "my-assistant",
name: "My Assistant",
model: openai("gpt-4o"),
instructions: "You are a helpful assistant with memory.",
inputProcessors: [input],
outputProcessors: [output],
})
const response = await agent.generate("What's my favorite programming language?")
```
#### Complete Example
Here's a full example showing a multi-turn conversation with memory:
```typescript
import { Agent } from "@mastra/core/agent"
import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
import { openai } from "@ai-sdk/openai"
async function main() {
const userId = "user-alex-123"
const threadId = `thread-${Date.now()}`
const { input, output } = createSupermemoryProcessors(userId, {
mode: "profile", // Fetch user profile memories
addMemory: "always", // Save all conversations
threadId,
verbose: true,
})
const agent = new Agent({
id: "memory-assistant",
name: "Memory Assistant",
instructions: `You are a helpful assistant with memory.
Use the memories provided to personalize your responses.`,
model: openai("gpt-4o-mini"),
inputProcessors: [input],
outputProcessors: [output],
})
// First conversation - introduce yourself
console.log("User: Hi! I'm Alex, a TypeScript developer.")
const r1 = await agent.generate("Hi! I'm Alex, a TypeScript developer.")
console.log("Assistant:", r1.text)
// Second conversation - the agent should remember
console.log("\nUser: What do you know about me?")
const r2 = await agent.generate("What do you know about me?")
console.log("Assistant:", r2.text)
}
main()
```
#### Memory Search Modes
- **`profile`** (default): Fetches user profile memories (static facts + dynamic context)
- **`query`**: Searches memories based on the user's message
- **`full`**: Combines both profile and query results
```typescript
// Profile mode - good for general personalization
const { input } = createSupermemoryProcessors("user-123", { mode: "profile" })
// Query mode - good for specific lookups
const { input } = createSupermemoryProcessors("user-123", { mode: "query" })
// Full mode - comprehensive context
const { input } = createSupermemoryProcessors("user-123", { mode: "full" })
```
#### Custom Prompt Templates
Customize how memories are formatted in the system prompt:
```typescript
import { createSupermemoryProcessors, type MemoryPromptData } from "@supermemory/tools/mastra"
const customTemplate = (data: MemoryPromptData) => `
<user_context>
${data.userMemories}
${data.generalSearchMemories}
</user_context>
`.trim()
const { input, output } = createSupermemoryProcessors("user-123", {
mode: "full",
promptTemplate: customTemplate,
})
```
#### Using RequestContext for Dynamic Thread IDs
Instead of hardcoding `threadId`, use Mastra's RequestContext for dynamic values:
```typescript
import { Agent } from "@mastra/core/agent"
import { RequestContext, MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context"
import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
const { input, output } = createSupermemoryProcessors("user-123", {
mode: "profile",
addMemory: "always",
// threadId not set here - will be read from RequestContext
})
const agent = new Agent({
id: "my-assistant",
name: "My Assistant",
model: openai("gpt-4o"),
inputProcessors: [input],
outputProcessors: [output],
})
// Set threadId dynamically per request
const ctx = new RequestContext()
ctx.set(MASTRA_THREAD_ID_KEY, "dynamic-thread-123")
const response = await agent.generate("Hello!", { requestContext: ctx })
```
#### Mastra Configuration Options
```typescript
interface SupermemoryMastraOptions {
apiKey?: string // Supermemory API key (or use SUPERMEMORY_API_KEY env var)
baseUrl?: string // Custom API endpoint
mode?: "profile" | "query" | "full" // Memory search mode (default: "profile")
addMemory?: "always" | "never" // Auto-save conversations (default: "never")
threadId?: string // Conversation ID for grouping messages
verbose?: boolean // Enable debug logging (default: false)
promptTemplate?: (data: MemoryPromptData) => string // Custom memory formatting
}
```
## Configuration
Both modules accept the same configuration interface:

View file

@ -1,7 +1,7 @@
{
"name": "@supermemory/tools",
"type": "module",
"version": "1.3.68",
"version": "1.4.00",
"description": "Memory tools for AI SDK and OpenAI function calling with supermemory",
"scripts": {
"build": "tsdown",
@ -20,13 +20,14 @@
},
"devDependencies": {
"@ai-sdk/provider": "^3.0.0",
"@anthropic-ai/sdk": "^0.65.0",
"@mastra/core": "^1.0.0",
"@total-typescript/tsconfig": "^1.0.4",
"@types/bun": "^1.2.21",
"dotenv": "^16.6.1",
"tsdown": "^0.14.2",
"typescript": "^5.9.2",
"vitest": "^3.2.4",
"@anthropic-ai/sdk": "^0.65.0"
"vitest": "^3.2.4"
},
"peerDependencies": {
"@ai-sdk/provider": "^2.0.0 || ^3.0.0"
@ -41,6 +42,7 @@
".": "./dist/index.js",
"./ai-sdk": "./dist/ai-sdk.js",
"./claude-memory": "./dist/claude-memory.js",
"./mastra": "./dist/mastra.js",
"./openai": "./dist/openai/index.js",
"./package.json": "./package.json"
},

View file

@ -6,7 +6,7 @@ import {
PARAMETER_DESCRIPTIONS,
TOOL_DESCRIPTIONS,
getContainerTags,
} from "./shared"
} from "./tools-shared"
import type { SupermemoryToolsConfig } from "./types"
// Export individual tool creators

View file

@ -1,5 +1,5 @@
import Supermemory from "supermemory"
import { getContainerTags } from "./shared"
import { getContainerTags } from "./tools-shared"
import type { SupermemoryToolsConfig } from "./types"
// Claude Memory Tool Types
@ -57,15 +57,6 @@ export class ClaudeMemoryTool {
.replace(/\./g, "_") // Replace . with _
}
/**
* Convert customId back to file path
* Note: This is lossy since we can't distinguish _ from . or /
* We rely on metadata.file_path for accurate path reconstruction
*/
private customIdToPath(customId: string): string {
return "/" + customId.replace(/_/g, "/")
}
constructor(apiKey: string, config?: ClaudeMemoryConfig) {
this.client = new Supermemory({
apiKey,
@ -182,7 +173,7 @@ export class ClaudeMemoryTool {
// If path ends with / or is exactly /memories, it's a directory listing request
if (path.endsWith("/") || path === "/memories") {
// Normalize path to end with /
const dirPath = path.endsWith("/") ? path : path + "/"
const dirPath = path.endsWith("/") ? path : `${path}/`
return await this.listDirectory(dirPath)
}
@ -227,7 +218,7 @@ export class ClaudeMemoryTool {
const slashIndex = relativePath.indexOf("/")
if (slashIndex > 0) {
// It's a subdirectory
dirs.add(relativePath.substring(0, slashIndex) + "/")
dirs.add(`${relativePath.substring(0, slashIndex)}/`)
} else if (relativePath !== "") {
// It's a file in this directory
files.push(relativePath)
@ -335,7 +326,7 @@ export class ClaudeMemoryTool {
try {
const normalizedId = this.normalizePathToCustomId(filePath)
const response = await this.client.add({
const _response = await this.client.add({
content: fileText,
customId: normalizedId,
containerTags: this.containerTags,
@ -394,7 +385,7 @@ export class ClaudeMemoryTool {
// Update the document
const normalizedId = this.normalizePathToCustomId(filePath)
const updateResponse = await this.client.add({
const _updateResponse = await this.client.add({
content: newContent,
customId: normalizedId,
containerTags: this.containerTags,

View file

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

View file

@ -0,0 +1,28 @@
export { withSupermemory } from "./wrapper"
export {
SupermemoryInputProcessor,
SupermemoryOutputProcessor,
createSupermemoryProcessor,
createSupermemoryOutputProcessor,
createSupermemoryProcessors,
} from "./processor"
export type {
SupermemoryMastraOptions,
Processor,
ProcessInputArgs,
ProcessInputResult,
ProcessOutputResultArgs,
ProcessorMessageResult,
MastraDBMessage,
MastraMessageContentV2,
MessageList,
RequestContext,
InputProcessor,
OutputProcessor,
PromptTemplate,
MemoryMode,
AddMemoryMode,
MemoryPromptData,
} from "./types"

View file

@ -0,0 +1,431 @@
/**
* Mastra Processors for Supermemory Integration
*
* This module provides input and output processors for Mastra agents that enable:
* - Memory injection: Fetches relevant user memories before LLM calls
* - Conversation saving: Persists conversations to Supermemory after responses
*
* Processors integrate with Mastra's processor pipeline:
* - InputProcessor runs before the LLM call, injecting memories into system messages
* - OutputProcessor runs after the LLM responds, optionally saving the conversation
*
* @module
*/
import {
createLogger,
normalizeBaseUrl,
validateApiKey,
MemoryCache,
buildMemoriesText,
extractQueryText,
type Logger,
type MemoryMode,
type PromptTemplate,
} from "../shared"
import {
addConversation,
type ConversationMessage,
} from "../conversations-client"
import { MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context"
import type {
SupermemoryMastraOptions,
Processor,
ProcessInputArgs,
ProcessInputResult,
ProcessOutputResultArgs,
MastraDBMessage,
RequestContext,
} from "./types"
/**
* Internal context shared between input and output processors.
*/
interface ProcessorContext {
containerTag: string
apiKey: string
baseUrl: string
mode: MemoryMode
addMemory: "always" | "never"
threadId?: string
logger: Logger
promptTemplate?: PromptTemplate
memoryCache: MemoryCache<string>
}
/**
* Creates the shared processor context from options.
*/
function createProcessorContext(
containerTag: string,
options: SupermemoryMastraOptions = {},
): ProcessorContext {
const apiKey = validateApiKey(options.apiKey)
const baseUrl = normalizeBaseUrl(options.baseUrl)
const logger = createLogger(options.verbose ?? false)
return {
containerTag,
apiKey,
baseUrl,
mode: options.mode ?? "profile",
addMemory: options.addMemory ?? "never",
threadId: options.threadId,
logger,
promptTemplate: options.promptTemplate,
memoryCache: new MemoryCache<string>(),
}
}
/**
* Gets the effective threadId from options or RequestContext.
*/
function getEffectiveThreadId(
ctx: ProcessorContext,
requestContext?: RequestContext,
): string | undefined {
if (ctx.threadId) {
return ctx.threadId
}
if (requestContext) {
return requestContext.get(MASTRA_THREAD_ID_KEY) as string | undefined
}
return undefined
}
/**
* Input processor that injects memories into the system prompt before LLM calls.
*
* This processor runs once at the start of agent execution (processInput).
* It fetches relevant memories from Supermemory based on the user's message
* and injects them into the system messages.
*
* @example
* ```typescript
* import { Agent } from "@mastra/core/agent"
* import { SupermemoryInputProcessor } from "@supermemory/tools/mastra"
* import { openai } from "@ai-sdk/openai"
*
* const agent = new Agent({
* id: "my-agent",
* name: "My Agent",
* model: openai("gpt-4o"),
* inputProcessors: [
* new SupermemoryInputProcessor("user-123", {
* mode: "full",
* verbose: true,
* }),
* ],
* })
* ```
*/
export class SupermemoryInputProcessor implements Processor {
readonly id = "supermemory-input"
readonly name = "Supermemory Memory Injection"
private ctx: ProcessorContext
constructor(containerTag: string, options: SupermemoryMastraOptions = {}) {
this.ctx = createProcessorContext(containerTag, options)
}
async processInput(args: ProcessInputArgs): Promise<ProcessInputResult> {
const { messages, messageList, requestContext } = args
try {
const queryText = extractQueryText(
messages as unknown as Array<{
role: string
content: string | Array<{ type: string; text?: string }>
}>,
this.ctx.mode,
)
if (this.ctx.mode !== "profile" && !queryText) {
this.ctx.logger.debug("No user message found, skipping memory search")
return messageList
}
const effectiveThreadId = getEffectiveThreadId(this.ctx, requestContext)
const turnKey = MemoryCache.makeTurnKey(
this.ctx.containerTag,
effectiveThreadId,
this.ctx.mode,
queryText || "",
)
const cachedMemories = this.ctx.memoryCache.get(turnKey)
if (cachedMemories) {
this.ctx.logger.debug("Using cached memories", { turnKey })
messageList.addSystem(cachedMemories, "supermemory")
return messageList
}
this.ctx.logger.info("Starting memory search", {
containerTag: this.ctx.containerTag,
threadId: effectiveThreadId,
mode: this.ctx.mode,
})
const memories = await buildMemoriesText({
containerTag: this.ctx.containerTag,
queryText: queryText || "",
mode: this.ctx.mode,
baseUrl: this.ctx.baseUrl,
apiKey: this.ctx.apiKey,
logger: this.ctx.logger,
promptTemplate: this.ctx.promptTemplate,
})
if (memories) {
this.ctx.memoryCache.set(turnKey, memories)
messageList.addSystem(memories, "supermemory")
this.ctx.logger.debug("Injected memories into system prompt", {
length: memories.length,
})
}
return messageList
} catch (error) {
this.ctx.logger.error("Error fetching memories", {
error: error instanceof Error ? error.message : "Unknown error",
})
return messageList
}
}
}
/**
* Output processor that saves conversations to Supermemory after generation completes.
*
* This processor runs once after generation completes (processOutputResult).
* When addMemory is set to "always", it saves the conversation to Supermemory
* using the /v4/conversations API for thread-based storage.
*
* @example
* ```typescript
* import { Agent } from "@mastra/core/agent"
* import { SupermemoryOutputProcessor } from "@supermemory/tools/mastra"
* import { openai } from "@ai-sdk/openai"
*
* const agent = new Agent({
* id: "my-agent",
* name: "My Agent",
* model: openai("gpt-4o"),
* outputProcessors: [
* new SupermemoryOutputProcessor("user-123", {
* addMemory: "always",
* threadId: "conv-456",
* }),
* ],
* })
* ```
*/
export class SupermemoryOutputProcessor implements Processor {
readonly id = "supermemory-output"
readonly name = "Supermemory Conversation Save"
private ctx: ProcessorContext
constructor(containerTag: string, options: SupermemoryMastraOptions = {}) {
this.ctx = createProcessorContext(containerTag, options)
}
async processOutputResult(
args: ProcessOutputResultArgs,
): Promise<MastraDBMessage[]> {
const { messages, messageList, requestContext } = args
if (this.ctx.addMemory !== "always") {
return messages
}
const effectiveThreadId = getEffectiveThreadId(this.ctx, requestContext)
if (!effectiveThreadId) {
this.ctx.logger.warn(
"No threadId provided for conversation save. Provide via options.threadId or RequestContext.",
)
return messages
}
try {
const conversationMessages = this.convertToConversationMessages(messages)
if (conversationMessages.length === 0) {
this.ctx.logger.debug("No messages to save")
return messages
}
const response = await addConversation({
conversationId: effectiveThreadId,
messages: conversationMessages,
containerTags: [this.ctx.containerTag],
apiKey: this.ctx.apiKey,
baseUrl: this.ctx.baseUrl,
})
this.ctx.logger.info("Conversation saved successfully", {
containerTag: this.ctx.containerTag,
conversationId: effectiveThreadId,
messageCount: conversationMessages.length,
responseId: response.id,
})
} catch (error) {
this.ctx.logger.error("Error saving conversation", {
error: error instanceof Error ? error.message : "Unknown error",
})
}
return messages
}
private convertToConversationMessages(
messages: MastraDBMessage[],
): ConversationMessage[] {
const result: ConversationMessage[] = []
for (const msg of messages) {
if (msg.role === "system") {
continue
}
const role = msg.role as "user" | "assistant"
const content = msg.content
if (content.content && typeof content.content === "string") {
result.push({ role, content: content.content })
continue
}
if (content.parts && Array.isArray(content.parts)) {
const textParts = content.parts
.filter(
(part): part is { type: "text"; text: string } =>
part.type === "text" &&
"text" in part &&
typeof part.text === "string",
)
.map((part) => ({
type: "text" as const,
text: part.text,
}))
if (textParts.length > 0) {
result.push({ role, content: textParts })
}
}
}
return result
}
}
/**
* Creates a Supermemory input processor for memory injection.
*
* @param containerTag - The container tag/user ID for scoping memories
* @param options - Configuration options
* @returns Configured SupermemoryInputProcessor instance
*
* @example
* ```typescript
* import { Agent } from "@mastra/core/agent"
* import { createSupermemoryProcessor } from "@supermemory/tools/mastra"
* import { openai } from "@ai-sdk/openai"
*
* const processor = createSupermemoryProcessor("user-123", {
* mode: "full",
* verbose: true,
* })
*
* const agent = new Agent({
* id: "my-agent",
* name: "My Agent",
* model: openai("gpt-4o"),
* inputProcessors: [processor],
* })
* ```
*/
export function createSupermemoryProcessor(
containerTag: string,
options: SupermemoryMastraOptions = {},
): SupermemoryInputProcessor {
return new SupermemoryInputProcessor(containerTag, options)
}
/**
* Creates a Supermemory output processor for saving conversations.
*
* @param containerTag - The container tag/user ID for scoping memories
* @param options - Configuration options
* @returns Configured SupermemoryOutputProcessor instance
*
* @example
* ```typescript
* import { Agent } from "@mastra/core/agent"
* import { createSupermemoryOutputProcessor } from "@supermemory/tools/mastra"
* import { openai } from "@ai-sdk/openai"
*
* const processor = createSupermemoryOutputProcessor("user-123", {
* addMemory: "always",
* threadId: "conv-456",
* })
*
* const agent = new Agent({
* id: "my-agent",
* name: "My Agent",
* model: openai("gpt-4o"),
* outputProcessors: [processor],
* })
* ```
*/
export function createSupermemoryOutputProcessor(
containerTag: string,
options: SupermemoryMastraOptions = {},
): SupermemoryOutputProcessor {
return new SupermemoryOutputProcessor(containerTag, options)
}
/**
* Creates both input and output processors with shared configuration.
*
* Use this when you want both memory injection and conversation saving
* with consistent settings across both processors.
*
* @param containerTag - The container tag/user ID for scoping memories
* @param options - Configuration options shared by both processors
* @returns Object containing both input and output processors
*
* @example
* ```typescript
* import { Agent } from "@mastra/core/agent"
* import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
* import { openai } from "@ai-sdk/openai"
*
* const { input, output } = createSupermemoryProcessors("user-123", {
* mode: "full",
* addMemory: "always",
* threadId: "conv-456",
* })
*
* const agent = new Agent({
* id: "my-agent",
* name: "My Agent",
* model: openai("gpt-4o"),
* inputProcessors: [input],
* outputProcessors: [output],
* })
* ```
*/
export function createSupermemoryProcessors(
containerTag: string,
options: SupermemoryMastraOptions = {},
): {
input: SupermemoryInputProcessor
output: SupermemoryOutputProcessor
} {
return {
input: new SupermemoryInputProcessor(containerTag, options),
output: new SupermemoryOutputProcessor(containerTag, options),
}
}

View file

@ -0,0 +1,47 @@
/**
* Type definitions for Mastra integration.
*
* We import core types directly from @mastra/core to ensure perfect compatibility.
* Custom types are only defined where @mastra/core doesn't export what we need.
*/
import type {
PromptTemplate,
MemoryMode,
AddMemoryMode,
MemoryPromptData,
SupermemoryBaseOptions,
} from "../shared"
// Re-export Mastra core types for consumers
export type {
Processor,
ProcessInputArgs,
ProcessInputResult,
ProcessOutputResultArgs,
ProcessorMessageResult,
InputProcessor,
OutputProcessor,
} from "@mastra/core/processors"
export type {
MastraDBMessage,
MastraMessageContentV2,
MessageList,
} from "@mastra/core/agent"
export type { RequestContext } from "@mastra/core/request-context"
/**
* Configuration options for the Supermemory Mastra processor.
* Extends base options with Mastra-specific settings.
*/
export interface SupermemoryMastraOptions extends SupermemoryBaseOptions {
/**
* When using the output processor, set this to enable automatic conversation saving.
* The threadId is used to group messages into a single conversation.
*/
threadId?: string
}
export type { PromptTemplate, MemoryMode, AddMemoryMode, MemoryPromptData }

View file

@ -0,0 +1,100 @@
/**
* Wrapper utilities for enhancing Mastra agent configurations with Supermemory.
*
* Since Mastra Agent instances have private properties that can't be modified
* after construction, we provide utilities that work with agent configs.
*
* @module
*/
import { validateApiKey } from "../shared"
import {
SupermemoryInputProcessor,
SupermemoryOutputProcessor,
} from "./processor"
import type { SupermemoryMastraOptions, Processor } from "./types"
/**
* Minimal AgentConfig interface representing the properties we need to enhance.
* This avoids a direct dependency on @mastra/core while staying type-safe.
*/
interface AgentConfig {
id: string
name?: string
inputProcessors?: Processor[]
outputProcessors?: Processor[]
[key: string]: unknown
}
/**
* Enhances a Mastra agent configuration with Supermemory memory capabilities.
*
* This function takes an agent config object and returns a new config with
* Supermemory processors injected. Use this before creating your Agent instance.
*
* The enhanced config includes:
* - Input processor: Fetches relevant memories before LLM calls
* - Output processor: Optionally saves conversations after responses
*
* @param config - The Mastra agent configuration to enhance
* @param containerTag - The container tag/user ID for scoping memories
* @param options - Configuration options for memory behavior
* @returns Enhanced agent config with Supermemory processors injected
*
* @example
* ```typescript
* import { Agent } from "@mastra/core/agent"
* import { withSupermemory } from "@supermemory/tools/mastra"
* import { openai } from "@ai-sdk/openai"
*
* const config = withSupermemory(
* {
* id: "my-agent",
* name: "My Agent",
* model: openai("gpt-4o"),
* instructions: "You are a helpful assistant.",
* },
* "user-123",
* {
* mode: "full",
* addMemory: "always",
* threadId: "conv-456",
* }
* )
*
* const agent = new Agent(config)
* ```
*
* @throws {Error} When neither `options.apiKey` nor `process.env.SUPERMEMORY_API_KEY` are set
*/
export function withSupermemory<T extends AgentConfig>(
config: T,
containerTag: string,
options: SupermemoryMastraOptions = {},
): T {
validateApiKey(options.apiKey)
const inputProcessor = new SupermemoryInputProcessor(containerTag, options)
const outputProcessor = new SupermemoryOutputProcessor(containerTag, options)
const existingInputProcessors = config.inputProcessors ?? []
const existingOutputProcessors = config.outputProcessors ?? []
// Supermemory input processor runs first (before other processors)
const mergedInputProcessors: Processor[] = [
inputProcessor,
...existingInputProcessors,
]
// Supermemory output processor runs last (after other processors)
const mergedOutputProcessors: Processor[] = [
...existingOutputProcessors,
outputProcessor,
]
return {
...config,
inputProcessors: mergedInputProcessors,
outputProcessors: mergedOutputProcessors,
}
}

View file

@ -1,7 +1,7 @@
import type OpenAI from "openai"
import Supermemory from "supermemory"
import { addConversation } from "../conversations-client"
import { deduplicateMemories } from "../shared"
import { deduplicateMemories } from "../tools-shared"
import { createLogger, type Logger } from "../vercel/logger"
import { convertProfileToMarkdown } from "../vercel/util"

View file

@ -5,7 +5,7 @@ import {
PARAMETER_DESCRIPTIONS,
TOOL_DESCRIPTIONS,
getContainerTags,
} from "../shared"
} from "../tools-shared"
import type { SupermemoryToolsConfig } from "../types"
/**

View file

@ -0,0 +1,79 @@
import type { MemoryMode } from "./types"
/**
* Generic memory cache for storing per-turn memories to avoid redundant API calls.
* Used to cache memory retrieval results during tool-call loops within the same turn.
*/
export class MemoryCache<T = string> {
private cache: Map<string, T> = new Map()
/**
* Generates a cache key for the current turn based on context parameters.
* Normalizes the message by trimming and collapsing whitespace.
*
* @param containerTag - The container tag/user ID
* @param threadId - Optional thread/conversation ID
* @param mode - The memory retrieval mode
* @param message - The user message content
* @returns A unique cache key for this turn
*/
static makeTurnKey(
containerTag: string,
threadId: string | undefined,
mode: MemoryMode,
message: string,
): string {
const normalizedMessage = message.trim().replace(/\s+/g, " ")
return `${containerTag}:${threadId || ""}:${mode}:${normalizedMessage}`
}
/**
* Retrieves a cached value by key.
*
* @param key - The cache key
* @returns The cached value or undefined if not found
*/
get(key: string): T | undefined {
return this.cache.get(key)
}
/**
* Stores a value in the cache.
*
* @param key - The cache key
* @param value - The value to cache
*/
set(key: string, value: T): void {
this.cache.set(key, value)
}
/**
* Checks if a key exists in the cache.
*
* @param key - The cache key
* @returns True if the key exists
*/
has(key: string): boolean {
return this.cache.has(key)
}
/**
* Clears all cached values.
*/
clear(): void {
this.cache.clear()
}
/**
* Returns the number of cached items.
*/
get size(): number {
return this.cache.size
}
}
/**
* Convenience function to create a turn cache key.
* @see MemoryCache.makeTurnKey
*/
export const makeTurnKey = MemoryCache.makeTurnKey

View file

@ -0,0 +1,61 @@
import Supermemory from "supermemory"
/**
* Normalizes a base URL by removing trailing slashes.
*
* @param url - Optional base URL to normalize
* @returns Normalized URL without trailing slash, or default API URL
*/
export const normalizeBaseUrl = (url?: string): string => {
const defaultUrl = "https://api.supermemory.ai"
if (!url) return defaultUrl
return url.endsWith("/") ? url.slice(0, -1) : url
}
/**
* Options for creating a Supermemory client.
*/
export interface CreateSupermemoryClientOptions {
/** Supermemory API key */
apiKey: string
/** Optional custom base URL */
baseUrl?: string
}
/**
* Creates a configured Supermemory client instance.
*
* @param options - Client configuration options
* @returns Configured Supermemory client
*/
export function createSupermemoryClient(
options: CreateSupermemoryClientOptions,
): Supermemory {
const normalizedBaseUrl = normalizeBaseUrl(options.baseUrl)
return new Supermemory({
apiKey: options.apiKey,
...(normalizedBaseUrl !== "https://api.supermemory.ai"
? { baseURL: normalizedBaseUrl }
: {}),
})
}
/**
* Validates that an API key is provided either via options or environment variable.
*
* @param apiKey - Optional API key from options
* @returns The validated API key
* @throws Error if no API key is available
*/
export function validateApiKey(apiKey?: string): string {
const providedApiKey = apiKey ?? process.env.SUPERMEMORY_API_KEY
if (!providedApiKey) {
throw new Error(
"SUPERMEMORY_API_KEY is not set — provide it via `options.apiKey` or set `process.env.SUPERMEMORY_API_KEY`",
)
}
return providedApiKey
}

View file

@ -0,0 +1,42 @@
// Types
export type {
MemoryPromptData,
PromptTemplate,
MemoryMode,
AddMemoryMode,
Logger,
ProfileStructure,
ProfileMarkdownData,
SupermemoryBaseOptions,
} from "./types"
// Logger
export { createLogger } from "./logger"
// Prompt builder
export {
defaultPromptTemplate,
convertProfileToMarkdown,
formatMemoriesForPrompt,
} from "./prompt-builder"
// Cache
export { MemoryCache, makeTurnKey } from "./cache"
// Context
export {
normalizeBaseUrl,
createSupermemoryClient,
validateApiKey,
type CreateSupermemoryClientOptions,
} from "./context"
// Memory client
export {
supermemoryProfileSearch,
buildMemoriesText,
extractQueryText,
getLastUserMessageText,
type BuildMemoriesTextOptions,
type GenericMessage,
} from "./memory-client"

View file

@ -0,0 +1,45 @@
import type { Logger } from "./types"
/**
* Creates a logger instance that outputs to console when verbose mode is enabled.
*
* @param verbose - When true, logs are written to console; when false, logs are silently ignored
* @returns Logger instance with debug, info, warn, and error methods
*/
export const createLogger = (verbose: boolean): Logger => {
if (!verbose) {
return {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
}
}
return {
debug: (message: string, data?: unknown) => {
console.log(
`[supermemory] ${message}`,
data ? JSON.stringify(data, null, 2) : "",
)
},
info: (message: string, data?: unknown) => {
console.log(
`[supermemory] ${message}`,
data ? JSON.stringify(data, null, 2) : "",
)
},
warn: (message: string, data?: unknown) => {
console.warn(
`[supermemory] ${message}`,
data ? JSON.stringify(data, null, 2) : "",
)
},
error: (message: string, data?: unknown) => {
console.error(
`[supermemory] ${message}`,
data ? JSON.stringify(data, null, 2) : "",
)
},
}
}

View file

@ -0,0 +1,284 @@
import { deduplicateMemories } from "../tools-shared"
import type {
Logger,
MemoryMode,
MemoryPromptData,
ProfileStructure,
PromptTemplate,
} from "./types"
import {
convertProfileToMarkdown,
defaultPromptTemplate,
} from "./prompt-builder"
/**
* Fetches profile and search results from the Supermemory API.
*
* @param containerTag - The container tag/user ID for scoping memories
* @param queryText - Optional query text for semantic search
* @param baseUrl - The API base URL
* @param apiKey - The API key for authentication
* @returns The profile structure with static, dynamic, and search results
*/
export const supermemoryProfileSearch = async (
containerTag: string,
queryText: string,
baseUrl: string,
apiKey: string,
): Promise<ProfileStructure> => {
const payload = queryText
? JSON.stringify({
q: queryText,
containerTag: containerTag,
})
: JSON.stringify({
containerTag: containerTag,
})
try {
const response = await fetch(`${baseUrl}/v4/profile`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: payload,
})
if (!response.ok) {
const errorText = await response.text().catch(() => "Unknown error")
throw new Error(
`Supermemory profile search failed: ${response.status} ${response.statusText}. ${errorText}`,
)
}
return await response.json()
} catch (error) {
if (error instanceof Error) {
throw error
}
throw new Error(`Supermemory API request failed: ${error}`)
}
}
/**
* Options for building memories text.
*/
export interface BuildMemoriesTextOptions {
containerTag: string
queryText: string
mode: MemoryMode
baseUrl: string
apiKey: string
logger: Logger
promptTemplate?: PromptTemplate
}
/**
* Fetches memories from the API, deduplicates them, and formats them into
* the final string to be injected into the system prompt.
*
* @param options - Configuration for building memories text
* @returns The final formatted memories string ready for injection
*/
export const buildMemoriesText = async (
options: BuildMemoriesTextOptions,
): Promise<string> => {
const {
containerTag,
queryText,
mode,
baseUrl,
apiKey,
logger,
promptTemplate = defaultPromptTemplate,
} = options
const memoriesResponse = await supermemoryProfileSearch(
containerTag,
queryText,
baseUrl,
apiKey,
)
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
logger.info("Memory search completed", {
containerTag,
memoryCountStatic,
memoryCountDynamic,
queryText:
queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""),
mode,
})
const deduplicated = deduplicateMemories({
static: memoriesResponse.profile.static,
dynamic: memoriesResponse.profile.dynamic,
searchResults: memoriesResponse.searchResults?.results,
})
logger.debug("Memory deduplication completed", {
static: {
original: memoryCountStatic,
deduplicated: deduplicated.static.length,
},
dynamic: {
original: memoryCountDynamic,
deduplicated: deduplicated.dynamic.length,
},
searchResults: {
original: memoriesResponse.searchResults?.results?.length,
deduplicated: deduplicated.searchResults?.length,
},
})
const userMemories =
mode !== "query"
? convertProfileToMarkdown({
profile: {
static: deduplicated.static,
dynamic: deduplicated.dynamic,
},
searchResults: { results: [] },
})
: ""
const generalSearchMemories =
mode !== "profile"
? `Search results for user's recent message: \n${deduplicated.searchResults
.map((memory) => `- ${memory}`)
.join("\n")}`
: ""
const promptData: MemoryPromptData = {
userMemories,
generalSearchMemories,
}
const memories = promptTemplate(promptData)
if (memories) {
logger.debug("Memory content preview", {
content: memories,
fullLength: memories.length,
})
}
return memories
}
/**
* Generic interface for a message with role and content.
* Framework-agnostic to support both Vercel AI SDK and Mastra.
*/
export interface GenericMessage {
role: string
content: string | Array<{ type: string; text?: string }>
}
/**
* Extracts the query text from messages based on mode.
* For "profile" mode, returns empty string (no query needed).
* For "query" or "full" mode, extracts the last user message text.
*
* This is a framework-agnostic version that works with any message array.
*
* @param messages - Array of messages with role and content
* @param mode - The memory retrieval mode
* @returns The query text for memory search
*/
export const extractQueryText = (
messages: GenericMessage[],
mode: MemoryMode,
): string => {
if (mode === "profile") {
return ""
}
const userMessage = messages
.slice()
.reverse()
.find((msg) => msg.role === "user")
const content = userMessage?.content
if (!content) return ""
if (typeof content === "string") {
return content
}
if (Array.isArray(content)) {
return content
.filter((part) => part.type === "text")
.map((part) => part.text || "")
.join(" ")
}
const objContent = content as unknown as {
content?: string
parts?: Array<{ type: string; text?: string }>
}
if (typeof objContent === "object" && objContent !== null) {
if ("content" in objContent && typeof objContent.content === "string") {
return objContent.content
}
if ("parts" in objContent && Array.isArray(objContent.parts)) {
return objContent.parts
.filter((part) => part.type === "text")
.map((part) => part.text || "")
.join(" ")
}
}
return ""
}
/**
* Extracts text content from the last user message in a message array.
*
* @param messages - Array of messages with role and content
* @returns The last user message text, or undefined if not found
*/
export const getLastUserMessageText = (
messages: GenericMessage[],
): string | undefined => {
const lastUserMessage = messages
.slice()
.reverse()
.find((msg) => msg.role === "user")
if (!lastUserMessage) {
return undefined
}
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(" ")
}
const objContent = content as unknown as {
content?: string
parts?: Array<{ type: string; text?: string }>
}
if (typeof objContent === "object" && objContent !== null) {
if ("content" in objContent && typeof objContent.content === "string") {
return objContent.content
}
if ("parts" in objContent && Array.isArray(objContent.parts)) {
return objContent.parts
.filter((part) => part.type === "text")
.map((part) => part.text || "")
.join(" ")
}
}
return undefined
}

View file

@ -0,0 +1,49 @@
import type {
MemoryPromptData,
PromptTemplate,
ProfileMarkdownData,
} from "./types"
/**
* Default prompt template that formats memories in the original "User Supermemories" format.
*/
export const defaultPromptTemplate: PromptTemplate = (data) =>
`User Supermemories: \n${data.userMemories}\n${data.generalSearchMemories}`.trim()
/**
* Convert profile data to markdown format with sections for static and dynamic memories.
*
* @param data Profile data with string arrays for static and dynamic memories
* @returns Markdown string with profile sections
*/
export function convertProfileToMarkdown(data: ProfileMarkdownData): string {
const sections: string[] = []
if (data.profile.static && data.profile.static.length > 0) {
sections.push("## Static Profile")
sections.push(data.profile.static.map((item) => `- ${item}`).join("\n"))
}
if (data.profile.dynamic && data.profile.dynamic.length > 0) {
sections.push("## Dynamic Profile")
sections.push(data.profile.dynamic.map((item) => `- ${item}`).join("\n"))
}
return sections.join("\n\n")
}
/**
* Formats memories into the final prompt string using the provided template.
*
* @param data - The memory prompt data containing userMemories and generalSearchMemories
* @param template - Optional custom template function (defaults to defaultPromptTemplate)
* @returns The formatted memories string ready for prompt injection
*/
export function formatMemoriesForPrompt(
data: MemoryPromptData,
template: PromptTemplate = defaultPromptTemplate,
): string {
return template(data)
}
export type { MemoryPromptData, PromptTemplate }

View file

@ -0,0 +1,119 @@
/**
* Data provided to the prompt template function for customizing memory injection.
*/
export interface MemoryPromptData {
/**
* Pre-formatted markdown combining static and dynamic profile memories.
* Contains core user facts (name, preferences, goals) and recent context (projects, interests).
*/
userMemories: string
/**
* Pre-formatted search results text for the current query.
* Contains memories retrieved based on semantic similarity to the conversation.
* Empty string if mode is "profile" only.
*/
generalSearchMemories: string
}
/**
* Function type for customizing the memory prompt injection.
* Return the full string to be injected into the system prompt.
*
* @example
* ```typescript
* const promptTemplate: PromptTemplate = (data) => `
* <user_memories>
* Here is some information about your past conversations:
* ${data.userMemories}
* ${data.generalSearchMemories}
* </user_memories>
* `.trim()
* ```
*/
export type PromptTemplate = (data: MemoryPromptData) => string
/**
* Memory retrieval mode:
* - "profile": Retrieves user profile memories (static + dynamic) without query filtering
* - "query": Searches memories based on semantic similarity to the user's message
* - "full": Combines both profile and query-based results
*/
export type MemoryMode = "profile" | "query" | "full"
/**
* Memory persistence mode:
* - "always": Automatically save conversations as memories
* - "never": Only retrieve memories, don't store new ones
*/
export type AddMemoryMode = "always" | "never"
/**
* Logger interface for consistent logging across integrations.
*/
export interface Logger {
debug: (message: string, data?: unknown) => void
info: (message: string, data?: unknown) => void
warn: (message: string, data?: unknown) => void
error: (message: string, data?: unknown) => void
}
/**
* Response structure from the Supermemory profile API.
*/
export interface ProfileStructure {
profile: {
/**
* Core, stable facts about the user that rarely change.
* Examples: name, profession, long-term preferences, goals.
*/
static?: Array<{ memory: string; metadata?: Record<string, unknown> }>
/**
* Recently learned or frequently updated information about the user.
* Examples: current projects, recent interests, ongoing topics.
*/
dynamic?: Array<{ memory: string; metadata?: Record<string, unknown> }>
}
searchResults: {
/**
* Memories retrieved based on semantic similarity to the current query.
* Most relevant to the immediate conversation context.
*/
results: Array<{ memory: string; metadata?: Record<string, unknown> }>
}
}
/**
* Simplified profile data for markdown conversion.
*/
export interface ProfileMarkdownData {
profile: {
/** Core, stable user facts (name, preferences, goals) */
static?: string[]
/** Recently learned or updated information (current projects, interests) */
dynamic?: string[]
}
searchResults: {
/** Query-relevant memories based on semantic similarity */
results: Array<{ memory: string }>
}
}
/**
* Base options shared across all integrations for Supermemory configuration.
*/
export interface SupermemoryBaseOptions {
/** Supermemory API key (falls back to SUPERMEMORY_API_KEY env var) */
apiKey?: string
/** Custom Supermemory API base URL */
baseUrl?: string
/** Optional conversation/thread ID to group messages for contextual memory generation */
threadId?: string
/** Memory retrieval mode */
mode?: MemoryMode
/** Memory persistence mode */
addMemory?: AddMemoryMode
/** Enable detailed logging of memory search and injection */
verbose?: boolean
/** Custom function to format memory data into the system prompt */
promptTemplate?: PromptTemplate
}

View file

@ -55,12 +55,13 @@ export interface MemoryItem {
}
/**
* Profile data structure containing memory items from different sources
* Profile data structure containing memory items from different sources.
* API may return either MemoryItem objects or plain strings.
*/
export interface ProfileWithMemories {
static?: Array<MemoryItem>
dynamic?: Array<MemoryItem>
searchResults?: Array<MemoryItem>
static?: Array<MemoryItem | string>
dynamic?: Array<MemoryItem | string>
searchResults?: Array<MemoryItem | string>
}
/**
@ -101,8 +102,14 @@ export function deduplicateMemories(
const dynamicItems = data.dynamic ?? []
const searchItems = data.searchResults ?? []
const getMemoryString = (item: MemoryItem): string | null => {
if (!item || typeof item.memory !== "string") return null
const getMemoryString = (item: MemoryItem | string): string | null => {
if (!item) return null
// Handle both string format (from API) and object format
if (typeof item === "string") {
const trimmed = item.trim()
return trimmed.length > 0 ? trimmed : null
}
if (typeof item.memory !== "string") return null
const trimmed = item.memory.trim()
return trimmed.length > 0 ? trimmed : null
}
@ -110,7 +117,7 @@ export function deduplicateMemories(
const staticMemories: string[] = []
const seenMemories = new Set<string>()
for (const item of staticItems) {
for (const item of staticItems as Array<MemoryItem | string>) {
const memory = getMemoryString(item)
if (memory !== null) {
staticMemories.push(memory)
@ -120,7 +127,7 @@ export function deduplicateMemories(
const dynamicMemories: string[] = []
for (const item of dynamicItems) {
for (const item of dynamicItems as Array<MemoryItem | string>) {
const memory = getMemoryString(item)
if (memory !== null && !seenMemories.has(memory)) {
dynamicMemories.push(memory)
@ -130,7 +137,7 @@ export function deduplicateMemories(
const searchMemories: string[] = []
for (const item of searchItems) {
for (const item of searchItems as Array<MemoryItem | string>) {
const memory = getMemoryString(item)
if (memory !== null && !seenMemories.has(memory)) {
searchMemories.push(memory)

View file

@ -165,16 +165,16 @@ describe("@supermemory/tools", () => {
(d) => d.function.name === "searchMemories",
)
expect(searchTool).toBeDefined()
expect(searchTool!.type).toBe("function")
expect(searchTool!.function.parameters?.required).toContain(
expect(searchTool?.type).toBe("function")
expect(searchTool?.function.parameters?.required).toContain(
"informationToGet",
)
// Check addMemory
const addTool = definitions.find((d) => d.function.name === "addMemory")
expect(addTool).toBeDefined()
expect(addTool!.type).toBe("function")
expect(addTool!.function.parameters?.required).toContain("memory")
expect(addTool?.type).toBe("function")
expect(addTool?.function.parameters?.required).toContain("memory")
})
})

View file

@ -1,44 +1,2 @@
export interface Logger {
debug: (message: string, data?: unknown) => void
info: (message: string, data?: unknown) => void
warn: (message: string, data?: unknown) => void
error: (message: string, data?: unknown) => void
}
export const createLogger = (verbose: boolean): Logger => {
if (!verbose) {
return {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
}
}
return {
debug: (message: string, data?: unknown) => {
console.log(
`[supermemory] ${message}`,
data ? JSON.stringify(data, null, 2) : "",
)
},
info: (message: string, data?: unknown) => {
console.log(
`[supermemory] ${message}`,
data ? JSON.stringify(data, null, 2) : "",
)
},
warn: (message: string, data?: unknown) => {
console.warn(
`[supermemory] ${message}`,
data ? JSON.stringify(data, null, 2) : "",
)
},
error: (message: string, data?: unknown) => {
console.error(
`[supermemory] ${message}`,
data ? JSON.stringify(data, null, 2) : "",
)
},
}
}
// Re-export logger from shared module for backward compatibility
export { createLogger, type Logger } from "../shared"

View file

@ -1,202 +1,50 @@
import { deduplicateMemories } from "../shared"
import type { Logger } from "./logger"
import {
type LanguageModelCallOptions,
convertProfileToMarkdown,
type ProfileStructure,
} from "./util"
// Re-export shared types and functions
export {
type MemoryPromptData,
type PromptTemplate,
defaultPromptTemplate,
normalizeBaseUrl,
buildMemoriesText,
type BuildMemoriesTextOptions,
} from "../shared"
import type { Logger } from "../shared"
import type { LanguageModelCallOptions } from "./util"
/**
* Data provided to the prompt template function for customizing memory injection.
*/
export interface MemoryPromptData {
/**
* Pre-formatted markdown combining static and dynamic profile memories.
* Contains core user facts (name, preferences, goals) and recent context (projects, interests).
*/
userMemories: string
/**
* Pre-formatted search results text for the current query.
* Contains memories retrieved based on semantic similarity to the conversation.
* Empty string if mode is "profile" only.
*/
generalSearchMemories: string
}
/**
* Function type for customizing the memory prompt injection.
* Return the full string to be injected into the system prompt.
* Extracts the query text from params based on mode.
* For "profile" mode, returns empty string (no query needed).
* For "query" or "full" mode, extracts the last user message text.
*
* @example
* ```typescript
* const promptTemplate: PromptTemplate = (data) => `
* <user_memories>
* Here is some information about your past conversations:
* ${data.userMemories}
* ${data.generalSearchMemories}
* </user_memories>
* `.trim()
* ```
* @param params - The language model call options
* @param mode - The memory retrieval mode
* @returns The query text for memory search
*/
export type PromptTemplate = (data: MemoryPromptData) => string
/**
* Default prompt template that replicates the original behavior.
*/
export const defaultPromptTemplate: PromptTemplate = (data) =>
`User Supermemories: \n${data.userMemories}\n${data.generalSearchMemories}`.trim()
export const normalizeBaseUrl = (url?: string): string => {
const defaultUrl = "https://api.supermemory.ai"
if (!url) return defaultUrl
return url.endsWith("/") ? url.slice(0, -1) : url
}
const supermemoryProfileSearch = async (
containerTag: string,
queryText: string,
baseUrl: string,
apiKey: string,
): Promise<ProfileStructure> => {
const payload = queryText
? JSON.stringify({
q: queryText,
containerTag: containerTag,
})
: JSON.stringify({
containerTag: containerTag,
})
try {
const response = await fetch(`${baseUrl}/v4/profile`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: payload,
})
if (!response.ok) {
const errorText = await response.text().catch(() => "Unknown error")
throw new Error(
`Supermemory profile search failed: ${response.status} ${response.statusText}. ${errorText}`,
)
}
return await response.json()
} catch (error) {
if (error instanceof Error) {
throw error
}
throw new Error(`Supermemory API request failed: ${error}`)
}
}
/**
* Options for building memories text.
*/
export interface BuildMemoriesTextOptions {
containerTag: string
queryText: string
mode: "profile" | "query" | "full"
baseUrl: string
apiKey: string
logger: Logger
promptTemplate?: PromptTemplate
}
/**
* Fetches memories from the API, deduplicates them, and formats them into
* the final string to be injected into the system prompt.
*
* @param options - Configuration for building memories text
* @returns The final formatted memories string ready for injection
*/
export const buildMemoriesText = async (
options: BuildMemoriesTextOptions,
): Promise<string> => {
const {
containerTag,
queryText,
mode,
baseUrl,
apiKey,
logger,
promptTemplate = defaultPromptTemplate,
} = options
const memoriesResponse = await supermemoryProfileSearch(
containerTag,
queryText,
baseUrl,
apiKey,
)
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
logger.info("Memory search completed", {
containerTag,
memoryCountStatic,
memoryCountDynamic,
queryText:
queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""),
mode,
})
const deduplicated = deduplicateMemories({
static: memoriesResponse.profile.static,
dynamic: memoriesResponse.profile.dynamic,
searchResults: memoriesResponse.searchResults?.results,
})
logger.debug("Memory deduplication completed", {
static: {
original: memoryCountStatic,
deduplicated: deduplicated.static.length,
},
dynamic: {
original: memoryCountDynamic,
deduplicated: deduplicated.dynamic.length,
},
searchResults: {
original: memoriesResponse.searchResults?.results?.length,
deduplicated: deduplicated.searchResults?.length,
},
})
const userMemories =
mode !== "query"
? convertProfileToMarkdown({
profile: {
static: deduplicated.static,
dynamic: deduplicated.dynamic,
},
searchResults: { results: [] },
})
: ""
const generalSearchMemories =
mode !== "profile"
? `Search results for user's recent message: \n${deduplicated.searchResults
.map((memory) => `- ${memory}`)
.join("\n")}`
: ""
const promptData: MemoryPromptData = {
userMemories,
generalSearchMemories,
export const extractQueryText = (
params: LanguageModelCallOptions,
mode: "profile" | "query" | "full",
): string => {
if (mode === "profile") {
return ""
}
const memories = promptTemplate(promptData)
if (memories) {
logger.debug("Memory content preview", {
content: memories,
fullLength: memories.length,
})
const userMessage = params.prompt
.slice()
.reverse()
.find((prompt: { role: string }) => prompt.role === "user")
const content = userMessage?.content
if (!content) return ""
if (typeof content === "string") {
return content
}
return memories
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
return (content as any[])
.filter((part) => part.type === "text")
.map((part) => part.text || "")
.join(" ")
}
/**
@ -239,42 +87,6 @@ export const injectMemoriesIntoParams = (
return { ...params, prompt: newPrompt } as LanguageModelCallOptions
}
/**
* Extracts the query text from params based on mode.
* For "profile" mode, returns empty string (no query needed).
* For "query" or "full" mode, extracts the last user message text.
*
* @param params - The language model call options
* @param mode - The memory retrieval mode
* @returns The query text for memory search
*/
export const extractQueryText = (
params: LanguageModelCallOptions,
mode: "profile" | "query" | "full",
): string => {
if (mode === "profile") {
return ""
}
const userMessage = params.prompt
.slice()
.reverse()
.find((prompt: { role: string }) => prompt.role === "user")
const content = userMessage?.content
if (!content) return ""
if (typeof content === "string") {
return content
}
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
return (content as any[])
.filter((part) => part.type === "text")
.map((part) => part.text || "")
.join(" ")
}
/**
* Adds memories to the system prompt by fetching from API and injecting.
* This is the original combined function, now implemented via helpers.
@ -288,8 +100,13 @@ export const addSystemPrompt = async (
mode: "profile" | "query" | "full",
baseUrl: string,
apiKey: string,
promptTemplate: PromptTemplate = defaultPromptTemplate,
promptTemplate?: (data: {
userMemories: string
generalSearchMemories: string
}) => string,
): Promise<LanguageModelCallOptions> => {
const { buildMemoriesText } = await import("../shared")
const queryText = extractQueryText(params, mode)
const memories = await buildMemoriesText({

View file

@ -3,19 +3,21 @@ import {
addConversation,
type ConversationMessage,
} from "../conversations-client"
import { createLogger, type Logger } from "./logger"
import {
createLogger,
normalizeBaseUrl,
MemoryCache,
buildMemoriesText,
type Logger,
type PromptTemplate,
type MemoryMode,
} from "../shared"
import {
type LanguageModelCallOptions,
getLastUserMessage,
filterOutSupermemories,
} from "./util"
import {
buildMemoriesText,
extractQueryText,
injectMemoriesIntoParams,
normalizeBaseUrl,
type PromptTemplate,
} from "./memory-prompt"
import { extractQueryText, injectMemoriesIntoParams } from "./memory-prompt"
const getConversationContent = (params: LanguageModelCallOptions) => {
return params.prompt
@ -175,7 +177,7 @@ interface SupermemoryMiddlewareOptions {
* - "query": Searches memories based on semantic similarity to the user's message
* - "full": Combines both profile and query-based results
*/
mode?: "profile" | "query" | "full"
mode?: MemoryMode
/**
* Memory persistence mode:
* - "always": Automatically save conversations as memories
@ -188,26 +190,21 @@ interface SupermemoryMiddlewareOptions {
promptTemplate?: PromptTemplate
}
/**
* Cached memories string for a user turn.
*/
type MemoryCache = string
interface SupermemoryMiddlewareContext {
client: Supermemory
logger: Logger
containerTag: string
conversationId?: string
mode: "profile" | "query" | "full"
mode: MemoryMode
addMemory: "always" | "never"
normalizedBaseUrl: string
apiKey: string
promptTemplate?: PromptTemplate
/**
* Per-turn memory cache map. Stores the injected memories string for each
* Per-turn memory cache. Stores the injected memories string for each
* user turn (keyed by turnKey) to avoid redundant API calls during tool-call
*/
memoryCache: Map<string, MemoryCache>
memoryCache: MemoryCache<string>
}
export const createSupermemoryContext = (
@ -244,20 +241,24 @@ export const createSupermemoryContext = (
normalizedBaseUrl,
apiKey,
promptTemplate,
memoryCache: new Map<string, MemoryCache>(),
memoryCache: new MemoryCache<string>(),
}
}
/**
* Generates a cache key for the current turn based on context and user message.
* Normalizes the user message by trimming and collapsing whitespace.
* Uses the shared MemoryCache.makeTurnKey implementation.
*/
const makeTurnKey = (
ctx: SupermemoryMiddlewareContext,
userMessage: string,
): string => {
const normalizedMessage = userMessage.trim().replace(/\s+/g, " ")
return `${ctx.containerTag}:${ctx.conversationId || ""}:${ctx.mode}:${normalizedMessage}`
return MemoryCache.makeTurnKey(
ctx.containerTag,
ctx.conversationId,
ctx.mode,
userMessage,
)
}
/**

View file

@ -9,6 +9,12 @@ import type {
LanguageModelV3StreamPart,
} from "@ai-sdk/provider"
// Re-export shared types for backward compatibility
export type {
ProfileStructure,
ProfileMarkdownData,
} from "../shared"
// Union types for dual SDK version support (V2 = SDK 5, V3 = SDK 6)
export type LanguageModel = LanguageModelV2 | LanguageModelV3
export type LanguageModelCallOptions =
@ -21,47 +27,6 @@ export type LanguageModelStreamPart =
| LanguageModelV2StreamPart
| LanguageModelV3StreamPart
/**
* Response structure from the Supermemory profile API.
*/
export interface ProfileStructure {
profile: {
/**
* Core, stable facts about the user that rarely change.
* Examples: name, profession, long-term preferences, goals.
*/
static?: Array<{ memory: string; metadata?: Record<string, unknown> }>
/**
* Recently learned or frequently updated information about the user.
* Examples: current projects, recent interests, ongoing topics.
*/
dynamic?: Array<{ memory: string; metadata?: Record<string, unknown> }>
}
searchResults: {
/**
* Memories retrieved based on semantic similarity to the current query.
* Most relevant to the immediate conversation context.
*/
results: Array<{ memory: string; metadata?: Record<string, unknown> }>
}
}
/**
* Simplified profile data for markdown conversion.
*/
export interface ProfileMarkdownData {
profile: {
/** Core, stable user facts (name, preferences, goals) */
static?: string[]
/** Recently learned or updated information (current projects, interests) */
dynamic?: string[]
}
searchResults: {
/** Query-relevant memories based on semantic similarity */
results: Array<{ memory: string }>
}
}
export type OutputContentItem =
| { type: "text"; text: string }
| { type: "reasoning"; text: string }
@ -79,26 +44,8 @@ export type OutputContentItem =
title: string
}
/**
* Convert profile data to markdown format
* @param data Profile data with string arrays for static and dynamic memories
* @returns Markdown string with profile sections
*/
export function convertProfileToMarkdown(data: ProfileMarkdownData): string {
const sections: string[] = []
if (data.profile.static && data.profile.static.length > 0) {
sections.push("## Static Profile")
sections.push(data.profile.static.map((item) => `- ${item}`).join("\n"))
}
if (data.profile.dynamic && data.profile.dynamic.length > 0) {
sections.push("## Dynamic Profile")
sections.push(data.profile.dynamic.map((item) => `- ${item}`).join("\n"))
}
return sections.join("\n\n")
}
// Re-export convertProfileToMarkdown from shared for backward compatibility
export { convertProfileToMarkdown } from "../shared"
export const getLastUserMessage = (
params: LanguageModelCallOptions,

View file

@ -97,7 +97,7 @@ async function chatWithMemoryTool() {
if (memoryResult.content) {
console.log(
"📄 Content preview:",
memoryResult.content.substring(0, 100) + "...",
`${memoryResult.content.substring(0, 100)}...`,
)
}
}
@ -256,7 +256,7 @@ async function testMemoryOperations() {
} else if (result.content) {
console.log(
"📄 Result:",
result.content.substring(0, 150) + "... (truncated)",
`${result.content.substring(0, 150)}... (truncated)`,
)
}
} else {
@ -275,7 +275,7 @@ async function testMemoryOperations() {
// Run the examples
async function main() {
await testMemoryOperations()
console.log("\n" + "=".repeat(70) + "\n")
console.log(`\n${"=".repeat(70)}\n`)
await chatWithMemoryTool()
}

View file

@ -314,14 +314,14 @@ export async function runAllExamples() {
try {
await directFetchExample()
console.log("\\n" + "=".repeat(70) + "\\n")
console.log(`\\n${"=".repeat(70)}\\n`)
await anthropicSdkExample()
console.log("\\n" + "=".repeat(70))
console.log(`\\n${"=".repeat(70)}`)
console.log("📋 Real Anthropic SDK Integration Template:")
console.log(anthropicIntegrationTemplate)
console.log("\\n" + "=".repeat(70))
console.log(`\\n${"=".repeat(70)}`)
console.log("🔧 cURL Examples for Direct API Testing:")
console.log(curlExamples)
} catch (error) {

View file

@ -326,7 +326,7 @@ export async function runRealExamples() {
// Test with the actual tool call first
await testWithRealToolCall()
console.log("\\n" + "=".repeat(70) + "\\n")
console.log(`\\n${"=".repeat(70)}\\n`)
// Show web integration example
console.log("🌐 Web Framework Integration Example:")
@ -334,7 +334,7 @@ export async function runRealExamples() {
// Only run full API example if both keys are present
if (process.env.ANTHROPIC_API_KEY && process.env.SUPERMEMORY_API_KEY) {
console.log("\\n" + "=".repeat(70) + "\\n")
console.log(`\\n${"=".repeat(70)}\\n`)
await realClaudeMemoryExample()
} else {
console.log(

View file

@ -0,0 +1,646 @@
/**
* Integration tests for the Mastra integration
* Tests processors and wrapper with real Supermemory API calls
*/
import { describe, it, expect, vi } from "vitest"
import {
RequestContext,
MASTRA_THREAD_ID_KEY,
} from "@mastra/core/request-context"
import {
SupermemoryInputProcessor,
SupermemoryOutputProcessor,
createSupermemoryProcessors,
withSupermemory,
} from "../../src/mastra"
import type {
ProcessInputArgs,
ProcessOutputResultArgs,
MessageList,
MastraDBMessage,
MastraMessageContentV2,
Processor,
} from "../../src/mastra"
interface MockAgentConfig {
id: string
name?: string
model?: string
inputProcessors?: Processor[]
outputProcessors?: Processor[]
[key: string]: unknown
}
import "dotenv/config"
const INTEGRATION_CONFIG = {
apiKey: process.env.SUPERMEMORY_API_KEY || "",
baseUrl: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai",
containerTag: "integration-test-mastra",
}
const shouldRunIntegration = !!process.env.SUPERMEMORY_API_KEY
/**
* Helper to create MastraMessageContentV2 from text
*/
function createMessageContent(text: string): MastraMessageContentV2 {
return {
format: 2,
parts: [{ type: "text", text }],
}
}
/**
* Helper to create a MastraDBMessage
*/
function createMessage(
role: "user" | "assistant" | "system",
text: string,
): MastraDBMessage {
return {
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
role,
content: createMessageContent(text),
createdAt: new Date(),
}
}
/**
* Creates a mock MessageList that captures calls for assertion.
*/
const createIntegrationMessageList = (): MessageList & {
calls: { method: string; args: unknown[] }[]
getSystemContent: () => string | undefined
} => {
const calls: { method: string; args: unknown[] }[] = []
return {
calls,
addSystem: vi.fn((content: string, id?: string) => {
calls.push({ method: "addSystem", args: [content, id] })
}),
addUser: vi.fn((content: string) => {
calls.push({ method: "addUser", args: [content] })
}),
addAssistant: vi.fn((content: string) => {
calls.push({ method: "addAssistant", args: [content] })
}),
getSystemContent: () => {
const systemCall = calls.find((c) => c.method === "addSystem")
return systemCall?.args[0] as string | undefined
},
} as unknown as MessageList & {
calls: { method: string; args: unknown[] }[]
getSystemContent: () => string | undefined
}
}
describe.skipIf(!shouldRunIntegration)(
"Integration: Mastra processors with real API",
() => {
describe("SupermemoryInputProcessor", () => {
it("should fetch real memories and inject into messageList", async () => {
const processor = new SupermemoryInputProcessor(
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
},
)
const messageList = createIntegrationMessageList()
const messages: MastraDBMessage[] = [
createMessage("user", "Hello, what do you know about me?"),
]
const args: ProcessInputArgs = {
messages,
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
expect(messageList.addSystem).toHaveBeenCalled()
const systemContent = messageList.getSystemContent()
expect(typeof systemContent).toBe("string")
})
it("should use query mode with user message as search query", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryInputProcessor(
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "query",
},
)
const messageList = createIntegrationMessageList()
const args: ProcessInputArgs = {
messages: [
createMessage(
"user",
"What are my favorite programming languages?",
),
],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
const profileCalls = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" && call[0].includes("/v4/profile"),
)
expect(profileCalls.length).toBeGreaterThan(0)
const profileCall = profileCalls[0]
if (profileCall?.[1]) {
const body = JSON.parse(
(profileCall[1] as RequestInit).body as string,
)
expect(body.q).toBe("What are my favorite programming languages?")
}
fetchSpy.mockRestore()
})
it("should use full mode with both profile and query", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryInputProcessor(
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "full",
},
)
const messageList = createIntegrationMessageList()
const args: ProcessInputArgs = {
messages: [createMessage("user", "Full mode test query")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
const profileCalls = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" && call[0].includes("/v4/profile"),
)
expect(profileCalls.length).toBeGreaterThan(0)
const profileCall = profileCalls[0]
if (profileCall?.[1]) {
const body = JSON.parse(
(profileCall[1] as RequestInit).body as string,
)
expect(body.q).toBe("Full mode test query")
}
fetchSpy.mockRestore()
})
it("should cache memories for repeated calls with same message", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryInputProcessor(
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
},
)
const messages: MastraDBMessage[] = [
createMessage("user", "Cache test message"),
]
const args1: ProcessInputArgs = {
messages,
systemMessages: [],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args1)
const callsAfterFirst = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" && call[0].includes("/v4/profile"),
).length
const args2: ProcessInputArgs = {
messages,
systemMessages: [],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args2)
const callsAfterSecond = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" && call[0].includes("/v4/profile"),
).length
expect(callsAfterSecond).toBe(callsAfterFirst)
fetchSpy.mockRestore()
})
it("should use custom promptTemplate for memory formatting", async () => {
const customTemplate = (data: {
userMemories: string
generalSearchMemories: string
}) => `<mastra-memories>${data.userMemories}</mastra-memories>`
const processor = new SupermemoryInputProcessor(
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
promptTemplate: customTemplate,
},
)
const messageList = createIntegrationMessageList()
const args: ProcessInputArgs = {
messages: [createMessage("user", "Custom template test")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
const systemContent = messageList.getSystemContent()
expect(systemContent).toMatch(/<mastra-memories>.*<\/mastra-memories>/s)
})
})
describe("SupermemoryOutputProcessor", () => {
it("should save conversation when addMemory is always", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const threadId = `test-mastra-${Date.now()}`
const processor = new SupermemoryOutputProcessor(
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
addMemory: "always",
threadId,
},
)
const args: ProcessOutputResultArgs = {
messages: [
createMessage("user", "Hello from Mastra integration test"),
createMessage("assistant", "Hi! I'm responding to the test."),
],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processOutputResult(args)
const conversationCalls = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" &&
call[0].includes("/v4/conversations"),
)
expect(conversationCalls.length).toBeGreaterThan(0)
fetchSpy.mockRestore()
})
it("should not save when addMemory is never", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryOutputProcessor(
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
addMemory: "never",
threadId: "test-thread",
},
)
const args: ProcessOutputResultArgs = {
messages: [
createMessage("user", "This should not be saved"),
createMessage("assistant", "Agreed"),
],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processOutputResult(args)
const conversationCalls = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" &&
call[0].includes("/v4/conversations"),
)
expect(conversationCalls.length).toBe(0)
fetchSpy.mockRestore()
})
it("should use threadId from RequestContext when not in options", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryOutputProcessor(
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
addMemory: "always",
},
)
const contextThreadId = `context-thread-${Date.now()}`
const requestContext = new RequestContext()
requestContext.set(MASTRA_THREAD_ID_KEY, contextThreadId)
const args: ProcessOutputResultArgs = {
messages: [
createMessage("user", "Test with RequestContext threadId"),
createMessage("assistant", "Got it!"),
],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
requestContext,
}
await processor.processOutputResult(args)
const conversationCalls = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" &&
call[0].includes("/v4/conversations"),
)
expect(conversationCalls.length).toBeGreaterThan(0)
fetchSpy.mockRestore()
})
})
describe("createSupermemoryProcessors", () => {
it("should create working input and output processors", async () => {
const { input, output } = createSupermemoryProcessors(
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
addMemory: "always",
threadId: `processors-test-${Date.now()}`,
},
)
const messageList = createIntegrationMessageList()
const inputArgs: ProcessInputArgs = {
messages: [createMessage("user", "Test processors factory")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await input.processInput(inputArgs)
expect(messageList.addSystem).toHaveBeenCalled()
const outputArgs: ProcessOutputResultArgs = {
messages: [
createMessage("user", "Test processors factory"),
createMessage("assistant", "Response"),
],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await output.processOutputResult(outputArgs)
})
})
describe("withSupermemory wrapper", () => {
it("should enhance config with working processors", async () => {
const config: MockAgentConfig = {
id: "test-mastra-agent",
name: "Test Mastra Agent",
model: "gpt-4o",
}
const enhanced = withSupermemory(
config,
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
addMemory: "always",
threadId: `wrapper-test-${Date.now()}`,
},
)
expect(enhanced.id).toBe("test-mastra-agent")
expect(enhanced.name).toBe("Test Mastra Agent")
expect(enhanced.model).toBe("gpt-4o")
expect(enhanced.inputProcessors).toHaveLength(1)
expect(enhanced.outputProcessors).toHaveLength(1)
const inputProcessor = enhanced.inputProcessors?.[0]
expect(inputProcessor?.id).toBe("supermemory-input")
if (inputProcessor?.processInput) {
const messageList = createIntegrationMessageList()
const args: ProcessInputArgs = {
messages: [createMessage("user", "Wrapper test")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await inputProcessor.processInput(args)
expect(messageList.addSystem).toHaveBeenCalled()
}
})
it("should merge with existing processors correctly", async () => {
const existingInputProcessor = {
id: "existing-input",
name: "Existing Input Processor",
processInput: vi.fn().mockResolvedValue(undefined),
}
const existingOutputProcessor = {
id: "existing-output",
name: "Existing Output Processor",
processOutputResult: vi.fn().mockResolvedValue(undefined),
}
const config: MockAgentConfig = {
id: "agent-with-processors",
name: "Agent With Processors",
inputProcessors: [existingInputProcessor],
outputProcessors: [existingOutputProcessor],
}
const enhanced = withSupermemory(
config,
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
},
)
expect(enhanced.inputProcessors).toHaveLength(2)
expect(enhanced.outputProcessors).toHaveLength(2)
expect(enhanced.inputProcessors?.[0]?.id).toBe("supermemory-input")
expect(enhanced.inputProcessors?.[1]?.id).toBe("existing-input")
expect(enhanced.outputProcessors?.[0]?.id).toBe("existing-output")
expect(enhanced.outputProcessors?.[1]?.id).toBe("supermemory-output")
})
})
describe("Options", () => {
it("verbose mode should not break functionality", async () => {
const processor = new SupermemoryInputProcessor(
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
verbose: true,
},
)
const messageList = createIntegrationMessageList()
const args: ProcessInputArgs = {
messages: [createMessage("user", "Verbose mode test")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
expect(messageList.addSystem).toHaveBeenCalled()
})
it("custom baseUrl should be used for API calls", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryInputProcessor(
INTEGRATION_CONFIG.containerTag,
{
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
},
)
const args: ProcessInputArgs = {
messages: [createMessage("user", "Base URL test")],
systemMessages: [],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
const profileCalls = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" && call[0].includes("/v4/profile"),
)
expect(profileCalls.length).toBeGreaterThan(0)
const url = profileCalls[0]?.[0] as string
expect(url.startsWith(INTEGRATION_CONFIG.baseUrl)).toBe(true)
fetchSpy.mockRestore()
})
})
describe("Error handling", () => {
it("should handle invalid API key gracefully", async () => {
const processor = new SupermemoryInputProcessor(
INTEGRATION_CONFIG.containerTag,
{
apiKey: "invalid-api-key-12345",
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
},
)
const messageList = createIntegrationMessageList()
const args: ProcessInputArgs = {
messages: [createMessage("user", "Invalid key test")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
const result = await processor.processInput(args)
expect(result).toBe(messageList)
expect(messageList.addSystem).not.toHaveBeenCalled()
})
it("output processor should handle save errors gracefully", async () => {
const processor = new SupermemoryOutputProcessor(
INTEGRATION_CONFIG.containerTag,
{
apiKey: "invalid-api-key-12345",
baseUrl: INTEGRATION_CONFIG.baseUrl,
addMemory: "always",
threadId: "error-test",
},
)
const args: ProcessOutputResultArgs = {
messages: [
createMessage("user", "Error test"),
createMessage("assistant", "Response"),
],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await expect(processor.processOutputResult(args)).resolves.toBeDefined()
})
})
},
)

View file

@ -0,0 +1,974 @@
/**
* Unit tests for the Mastra integration
* Tests processors, wrapper, and factory functions
*/
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"
import {
RequestContext,
MASTRA_THREAD_ID_KEY,
} from "@mastra/core/request-context"
import {
SupermemoryInputProcessor,
SupermemoryOutputProcessor,
createSupermemoryProcessor,
createSupermemoryOutputProcessor,
createSupermemoryProcessors,
withSupermemory,
} from "../../src/mastra"
import type {
ProcessInputArgs,
ProcessOutputResultArgs,
MessageList,
MastraDBMessage,
MastraMessageContentV2,
Processor,
} from "../../src/mastra"
const TEST_CONFIG = {
apiKey: "test-api-key",
baseUrl: "https://api.supermemory.ai",
containerTag: "test-mastra-user",
}
interface MockAgentConfig {
id: string
name?: string
model?: string
customProp?: string
inputProcessors?: Processor[]
outputProcessors?: Processor[]
[key: string]: unknown
}
/**
* Helper to create MastraMessageContentV2 from text
*/
function createMessageContent(text: string): MastraMessageContentV2 {
return {
format: 2,
parts: [{ type: "text", text }],
}
}
/**
* Helper to create a MastraDBMessage
*/
function createMessage(
role: "user" | "assistant" | "system",
text: string,
): MastraDBMessage {
return {
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
role,
content: createMessageContent(text),
createdAt: new Date(),
}
}
const createMockMessageList = (): MessageList & {
calls: { method: string; args: unknown[] }[]
} => {
const calls: { method: string; args: unknown[] }[] = []
return {
calls,
addSystem: vi.fn((content: string, _id?: string) => {
calls.push({ method: "addSystem", args: [content, _id] })
}),
addUser: vi.fn((content: string) => {
calls.push({ method: "addUser", args: [content] })
}),
addAssistant: vi.fn((content: string) => {
calls.push({ method: "addAssistant", args: [content] })
}),
} as unknown as MessageList & { calls: { method: string; args: unknown[] }[] }
}
const createMockProfileResponse = (
staticMemories: string[] = [],
dynamicMemories: string[] = [],
searchResults: string[] = [],
) => ({
profile: {
static: staticMemories.map((memory) => ({ memory })),
dynamic: dynamicMemories.map((memory) => ({ memory })),
},
searchResults: {
results: searchResults.map((memory) => ({ memory })),
},
})
const createMockConversationResponse = () => ({
id: "mem-123",
conversationId: "conv-456",
status: "created",
})
describe("SupermemoryInputProcessor", () => {
let originalEnv: string | undefined
let originalFetch: typeof globalThis.fetch
let fetchMock: ReturnType<typeof vi.fn>
beforeEach(() => {
originalEnv = process.env.SUPERMEMORY_API_KEY
process.env.SUPERMEMORY_API_KEY = TEST_CONFIG.apiKey
originalFetch = globalThis.fetch
fetchMock = vi.fn()
globalThis.fetch = fetchMock as unknown as typeof fetch
vi.clearAllMocks()
})
afterEach(() => {
if (originalEnv) {
process.env.SUPERMEMORY_API_KEY = originalEnv
} else {
delete process.env.SUPERMEMORY_API_KEY
}
globalThis.fetch = originalFetch
})
describe("constructor", () => {
it("should create processor with default options", () => {
const processor = new SupermemoryInputProcessor(TEST_CONFIG.containerTag)
expect(processor.id).toBe("supermemory-input")
expect(processor.name).toBe("Supermemory Memory Injection")
})
it("should throw error if API key is not set", () => {
delete process.env.SUPERMEMORY_API_KEY
expect(() => {
new SupermemoryInputProcessor(TEST_CONFIG.containerTag)
}).toThrow("SUPERMEMORY_API_KEY is not set")
})
it("should accept API key via options", () => {
delete process.env.SUPERMEMORY_API_KEY
const processor = new SupermemoryInputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: "custom-key",
},
)
expect(processor.id).toBe("supermemory-input")
})
})
describe("processInput", () => {
it("should inject memories into messageList", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: () =>
Promise.resolve(
createMockProfileResponse(
["User likes TypeScript"],
["Recent interest in AI"],
),
),
})
const processor = new SupermemoryInputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
mode: "profile",
},
)
const messageList = createMockMessageList()
const messages: MastraDBMessage[] = [createMessage("user", "Hello")]
const args: ProcessInputArgs = {
messages,
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
expect(messageList.addSystem).toHaveBeenCalled()
const systemCall = messageList.calls.find((c) => c.method === "addSystem")
expect(systemCall).toBeDefined()
expect(systemCall?.args[0]).toContain("TypeScript")
expect(systemCall?.args[1]).toBe("supermemory")
})
it("should use cached memories on second call with same message", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: () =>
Promise.resolve(createMockProfileResponse(["Cached memory"])),
})
const processor = new SupermemoryInputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
mode: "profile",
},
)
const messages: MastraDBMessage[] = [createMessage("user", "Hello")]
const args1: ProcessInputArgs = {
messages,
systemMessages: [],
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args1)
expect(fetchMock).toHaveBeenCalledTimes(1)
const args2: ProcessInputArgs = {
messages,
systemMessages: [],
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args2)
expect(fetchMock).toHaveBeenCalledTimes(1)
})
it("should refetch memories for different user message", async () => {
let callCount = 0
fetchMock.mockImplementation(() => {
callCount++
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve(
createMockProfileResponse([`Memory from call ${callCount}`]),
),
})
})
const processor = new SupermemoryInputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
mode: "query",
},
)
const args1: ProcessInputArgs = {
messages: [createMessage("user", "First message")],
systemMessages: [],
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args1)
expect(fetchMock).toHaveBeenCalledTimes(1)
const args2: ProcessInputArgs = {
messages: [createMessage("user", "Different message")],
systemMessages: [],
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args2)
expect(fetchMock).toHaveBeenCalledTimes(2)
})
it("should return messageList in query mode when no user message", async () => {
const processor = new SupermemoryInputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
mode: "query",
},
)
const messageList = createMockMessageList()
const args: ProcessInputArgs = {
messages: [],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
const result = await processor.processInput(args)
expect(result).toBe(messageList)
expect(fetchMock).not.toHaveBeenCalled()
expect(messageList.addSystem).not.toHaveBeenCalled()
})
it("should handle API errors gracefully", async () => {
fetchMock.mockResolvedValue({
ok: false,
status: 500,
statusText: "Internal Server Error",
text: () => Promise.resolve("Server error"),
})
const processor = new SupermemoryInputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
mode: "profile",
},
)
const messageList = createMockMessageList()
const args: ProcessInputArgs = {
messages: [createMessage("user", "Hello")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
const result = await processor.processInput(args)
expect(result).toBe(messageList)
expect(messageList.addSystem).not.toHaveBeenCalled()
})
it("should use threadId from options", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
})
const processor = new SupermemoryInputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
threadId: "thread-123",
mode: "profile",
},
)
const args: ProcessInputArgs = {
messages: [createMessage("user", "Hello")],
systemMessages: [],
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
expect(fetchMock).toHaveBeenCalled()
})
it("should use threadId from requestContext when not in options", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
})
const processor = new SupermemoryInputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
mode: "profile",
},
)
const requestContext = new RequestContext()
requestContext.set(MASTRA_THREAD_ID_KEY, "ctx-thread-456")
const args: ProcessInputArgs = {
messages: [createMessage("user", "Hello")],
systemMessages: [],
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
requestContext,
}
await processor.processInput(args)
expect(fetchMock).toHaveBeenCalled()
})
it("should handle messages with array content parts", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
})
const processor = new SupermemoryInputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
mode: "query",
},
)
const messages: MastraDBMessage[] = [
{
id: "msg-1",
role: "user",
content: {
format: 2,
parts: [
{ type: "text", text: "Hello " },
{ type: "text", text: "World" },
],
},
createdAt: new Date(),
},
]
const messageList = createMockMessageList()
const args: ProcessInputArgs = {
messages,
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
expect(fetchMock).toHaveBeenCalled()
})
})
})
describe("SupermemoryOutputProcessor", () => {
let originalEnv: string | undefined
let originalFetch: typeof globalThis.fetch
let fetchMock: ReturnType<typeof vi.fn>
beforeEach(() => {
originalEnv = process.env.SUPERMEMORY_API_KEY
process.env.SUPERMEMORY_API_KEY = TEST_CONFIG.apiKey
originalFetch = globalThis.fetch
fetchMock = vi.fn()
globalThis.fetch = fetchMock as unknown as typeof fetch
vi.clearAllMocks()
})
afterEach(() => {
if (originalEnv) {
process.env.SUPERMEMORY_API_KEY = originalEnv
} else {
delete process.env.SUPERMEMORY_API_KEY
}
globalThis.fetch = originalFetch
})
describe("constructor", () => {
it("should create processor with default options", () => {
const processor = new SupermemoryOutputProcessor(TEST_CONFIG.containerTag)
expect(processor.id).toBe("supermemory-output")
expect(processor.name).toBe("Supermemory Conversation Save")
})
})
describe("processOutputResult", () => {
it("should save conversation when addMemory is always", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: () => Promise.resolve(createMockConversationResponse()),
})
const processor = new SupermemoryOutputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
addMemory: "always",
threadId: "conv-456",
},
)
const messages: MastraDBMessage[] = [
createMessage("user", "Hello"),
createMessage("assistant", "Hi there!"),
]
const args: ProcessOutputResultArgs = {
messages,
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processOutputResult(args)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(
expect.stringContaining("/v4/conversations"),
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_CONFIG.apiKey}`,
}),
}),
)
const callBody = JSON.parse(
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
)
expect(callBody.conversationId).toBe("conv-456")
expect(callBody.messages).toHaveLength(2)
expect(callBody.containerTags).toContain(TEST_CONFIG.containerTag)
})
it("should not save conversation when addMemory is never", async () => {
const processor = new SupermemoryOutputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
addMemory: "never",
threadId: "conv-456",
},
)
const args: ProcessOutputResultArgs = {
messages: [
createMessage("user", "Hello"),
createMessage("assistant", "Hi!"),
],
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processOutputResult(args)
expect(fetchMock).not.toHaveBeenCalled()
})
it("should not save when no threadId provided", async () => {
const processor = new SupermemoryOutputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
addMemory: "always",
},
)
const args: ProcessOutputResultArgs = {
messages: [
createMessage("user", "Hello"),
createMessage("assistant", "Hi!"),
],
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processOutputResult(args)
expect(fetchMock).not.toHaveBeenCalled()
})
it("should use threadId from requestContext", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: () => Promise.resolve(createMockConversationResponse()),
})
const processor = new SupermemoryOutputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
addMemory: "always",
},
)
const requestContext = new RequestContext()
requestContext.set(MASTRA_THREAD_ID_KEY, "ctx-thread-789")
const args: ProcessOutputResultArgs = {
messages: [
createMessage("user", "Hello"),
createMessage("assistant", "Hi!"),
],
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
requestContext,
}
await processor.processOutputResult(args)
expect(fetchMock).toHaveBeenCalledTimes(1)
const callBody = JSON.parse(
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
)
expect(callBody.conversationId).toBe("ctx-thread-789")
})
it("should skip system messages when saving", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: () => Promise.resolve(createMockConversationResponse()),
})
const processor = new SupermemoryOutputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
addMemory: "always",
threadId: "conv-456",
},
)
const messages: MastraDBMessage[] = [
createMessage("system", "You are a helpful assistant"),
createMessage("user", "Hello"),
createMessage("assistant", "Hi there!"),
]
const args: ProcessOutputResultArgs = {
messages,
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processOutputResult(args)
const callBody = JSON.parse(
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
)
expect(callBody.messages).toHaveLength(2)
expect(
callBody.messages.every((m: { role: string }) => m.role !== "system"),
).toBe(true)
})
it("should handle messages with array content parts", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: () => Promise.resolve(createMockConversationResponse()),
})
const processor = new SupermemoryOutputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
addMemory: "always",
threadId: "conv-456",
},
)
const messages: MastraDBMessage[] = [
{
id: "msg-1",
role: "user",
content: {
format: 2,
parts: [
{ type: "text", text: "Hello" },
{ type: "text", text: " World" },
],
},
createdAt: new Date(),
},
{
id: "msg-2",
role: "assistant",
content: {
format: 2,
parts: [{ type: "text", text: "Hi!" }],
},
createdAt: new Date(),
},
]
const args: ProcessOutputResultArgs = {
messages,
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processOutputResult(args)
const callBody = JSON.parse(
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
)
expect(callBody.messages).toHaveLength(2)
})
it("should handle save errors gracefully", async () => {
fetchMock.mockResolvedValue({
ok: false,
status: 500,
statusText: "Internal Server Error",
text: () => Promise.resolve("Server error"),
})
const processor = new SupermemoryOutputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
addMemory: "always",
threadId: "conv-456",
},
)
const args: ProcessOutputResultArgs = {
messages: [
createMessage("user", "Hello"),
createMessage("assistant", "Hi!"),
],
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
// Should not throw
await expect(processor.processOutputResult(args)).resolves.toBeDefined()
})
it("should not save when no messages to save", async () => {
const processor = new SupermemoryOutputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: TEST_CONFIG.apiKey,
addMemory: "always",
threadId: "conv-456",
},
)
const args: ProcessOutputResultArgs = {
messages: [],
messageList: createMockMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processOutputResult(args)
expect(fetchMock).not.toHaveBeenCalled()
})
})
})
describe("Factory functions", () => {
let originalEnv: string | undefined
beforeEach(() => {
originalEnv = process.env.SUPERMEMORY_API_KEY
process.env.SUPERMEMORY_API_KEY = TEST_CONFIG.apiKey
})
afterEach(() => {
if (originalEnv) {
process.env.SUPERMEMORY_API_KEY = originalEnv
} else {
delete process.env.SUPERMEMORY_API_KEY
}
})
describe("createSupermemoryProcessor", () => {
it("should create input processor", () => {
const processor = createSupermemoryProcessor(TEST_CONFIG.containerTag)
expect(processor).toBeInstanceOf(SupermemoryInputProcessor)
expect(processor.id).toBe("supermemory-input")
})
it("should pass options to processor", () => {
const processor = createSupermemoryProcessor(TEST_CONFIG.containerTag, {
apiKey: "custom-key",
mode: "full",
})
expect(processor).toBeInstanceOf(SupermemoryInputProcessor)
})
})
describe("createSupermemoryOutputProcessor", () => {
it("should create output processor", () => {
const processor = createSupermemoryOutputProcessor(
TEST_CONFIG.containerTag,
)
expect(processor).toBeInstanceOf(SupermemoryOutputProcessor)
expect(processor.id).toBe("supermemory-output")
})
it("should pass options to processor", () => {
const processor = createSupermemoryOutputProcessor(
TEST_CONFIG.containerTag,
{
apiKey: "custom-key",
addMemory: "always",
threadId: "conv-123",
},
)
expect(processor).toBeInstanceOf(SupermemoryOutputProcessor)
})
})
describe("createSupermemoryProcessors", () => {
it("should create both input and output processors", () => {
const { input, output } = createSupermemoryProcessors(
TEST_CONFIG.containerTag,
)
expect(input).toBeInstanceOf(SupermemoryInputProcessor)
expect(output).toBeInstanceOf(SupermemoryOutputProcessor)
})
it("should share options between processors", () => {
const { input, output } = createSupermemoryProcessors(
TEST_CONFIG.containerTag,
{
apiKey: "custom-key",
mode: "full",
addMemory: "always",
threadId: "conv-123",
},
)
expect(input.id).toBe("supermemory-input")
expect(output.id).toBe("supermemory-output")
})
})
})
describe("withSupermemory", () => {
let originalEnv: string | undefined
beforeEach(() => {
originalEnv = process.env.SUPERMEMORY_API_KEY
process.env.SUPERMEMORY_API_KEY = TEST_CONFIG.apiKey
})
afterEach(() => {
if (originalEnv) {
process.env.SUPERMEMORY_API_KEY = originalEnv
} else {
delete process.env.SUPERMEMORY_API_KEY
}
})
describe("API key validation", () => {
it("should throw error if API key is not set", () => {
delete process.env.SUPERMEMORY_API_KEY
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
expect(() => {
withSupermemory(config, TEST_CONFIG.containerTag)
}).toThrow("SUPERMEMORY_API_KEY is not set")
})
it("should accept API key via options", () => {
delete process.env.SUPERMEMORY_API_KEY
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag, {
apiKey: "custom-key",
})
expect(enhanced).toBeDefined()
expect(enhanced.inputProcessors).toHaveLength(1)
})
})
describe("processor injection", () => {
it("should inject input and output processors", () => {
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag)
expect(enhanced.inputProcessors).toHaveLength(1)
expect(enhanced.outputProcessors).toHaveLength(1)
expect(enhanced.inputProcessors?.[0]?.id).toBe("supermemory-input")
expect(enhanced.outputProcessors?.[0]?.id).toBe("supermemory-output")
})
it("should preserve original config properties", () => {
const config: MockAgentConfig = {
id: "test-agent",
name: "Test Agent",
model: "gpt-4",
customProp: "value",
}
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag)
expect(enhanced.id).toBe("test-agent")
expect(enhanced.name).toBe("Test Agent")
expect(enhanced.model).toBe("gpt-4")
expect(enhanced.customProp).toBe("value")
})
it("should prepend input processor to existing processors", () => {
const existingInputProcessor: Processor = {
id: "existing-input",
name: "Existing Input",
}
const config: MockAgentConfig = {
id: "test-agent",
name: "Test Agent",
inputProcessors: [existingInputProcessor],
}
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag)
expect(enhanced.inputProcessors).toHaveLength(2)
expect(enhanced.inputProcessors?.[0]?.id).toBe("supermemory-input")
expect(enhanced.inputProcessors?.[1]?.id).toBe("existing-input")
})
it("should append output processor to existing processors", () => {
const existingOutputProcessor: Processor = {
id: "existing-output",
name: "Existing Output",
}
const config: MockAgentConfig = {
id: "test-agent",
name: "Test Agent",
outputProcessors: [existingOutputProcessor],
}
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag)
expect(enhanced.outputProcessors).toHaveLength(2)
expect(enhanced.outputProcessors?.[0]?.id).toBe("existing-output")
expect(enhanced.outputProcessors?.[1]?.id).toBe("supermemory-output")
})
it("should handle configs with both existing input and output processors", () => {
const existingInput: Processor = { id: "existing-input" }
const existingOutput: Processor = { id: "existing-output" }
const config: MockAgentConfig = {
id: "test-agent",
name: "Test Agent",
inputProcessors: [existingInput],
outputProcessors: [existingOutput],
}
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag)
expect(enhanced.inputProcessors).toHaveLength(2)
expect(enhanced.outputProcessors).toHaveLength(2)
expect(enhanced.inputProcessors?.[0]?.id).toBe("supermemory-input")
expect(enhanced.inputProcessors?.[1]?.id).toBe("existing-input")
expect(enhanced.outputProcessors?.[0]?.id).toBe("existing-output")
expect(enhanced.outputProcessors?.[1]?.id).toBe("supermemory-output")
})
})
describe("options passthrough", () => {
it("should pass options to processors", () => {
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag, {
mode: "full",
addMemory: "always",
threadId: "conv-123",
verbose: true,
})
expect(enhanced.inputProcessors).toHaveLength(1)
expect(enhanced.outputProcessors).toHaveLength(1)
})
})
})

View file

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