Merge branch 'fix/python-sdks-v4-api' into rebuild/pr1435

This commit is contained in:
ved015 2026-08-24 21:33:20 +05:30
commit ed58861f69
13 changed files with 538 additions and 385 deletions

View file

@ -18,7 +18,7 @@ Supermemory integrates with [VoltAgent](https://github.com/VoltAgent/voltagent),
## Installation
```bash
npm install @supermemory/tools @voltagent/core
npm install @supermemory/tools @voltagent/core ai@^6 @ai-sdk/openai@^3
```
Set up your API key as an environment variable:
@ -52,9 +52,7 @@ const configWithMemory = withSupermemory({
const agent = new Agent(configWithMemory)
// Memories are automatically injected and saved
const result = await agent.generateText({
messages: [{ role: "user", content: "What's my name?" }],
})
const result = await agent.generateText("What's my name?")
```
<Note>
@ -131,14 +129,13 @@ const configWithMemory = withSupermemory({
// Search tuning
searchMode: "hybrid", // "memories" | "documents" | "hybrid"
threshold: 0.1, // 0.0-1.0 (higher = more accurate)
limit: 10, // Max results to return
threshold: 0.6, // 0.0-1.0 (higher = more accurate)
limit: 10, // Integer from 1 to 100
rerank: true, // Rerank for best relevance
rewriteQuery: false, // AI-rewrite query (+400ms latency)
// Context
entityContext: "This is John, a software engineer", // Guides memory extraction (max 1500 chars)
metadata: { source: "voltagent" }, // Attached to saved conversations
metadata: { source: "voltagent" }, // Attached to saved conversations
// API
apiKey: "sk-...", // Falls back to SUPERMEMORY_API_KEY env var
@ -154,14 +151,16 @@ const configWithMemory = withSupermemory({
| `addMemory` | string | `"always"` | Whether to save conversations after each response |
| `customId` | string | **required** | Custom ID to group messages into a conversation |
| `searchMode` | string | — | `"memories"`, `"documents"`, or `"hybrid"` |
| `threshold` | number | `0.1` | Similarity threshold (0 = more results, 1 = more accurate) |
| `limit` | number | `10` | Maximum number of memory results |
| `threshold` | number | | Similarity threshold (0 = more results, 1 = more accurate) |
| `limit` | number | — | Maximum number of memory results (integer from 1 to 100) |
| `rerank` | boolean | `false` | Rerank results for relevance |
| `rewriteQuery` | boolean | `false` | AI-rewrite query for better results (+400ms) |
| `entityContext` | string | — | Context for memory extraction (max 1500 chars) |
| `entityContext` | string | — | Deprecated and ignored. [Configure it on the container tag instead](/concepts/customization#entity-context). |
| `metadata` | object | — | Custom metadata attached to saved conversations |
| `promptTemplate` | function | — | Custom function to format memory data into prompt |
When `threshold` or `limit` is omitted, the selected Supermemory backend route applies its own default. Set them explicitly when you need consistent search tuning across modes.
## Search Modes
The `searchMode` option controls what type of results are searched:
@ -171,4 +170,3 @@ The `searchMode` option controls what type of results are searched:
| `"memories"` | Search only memory entries (atomic facts about the user) |
| `"documents"` | Search only document chunks |
| `"hybrid"` | Search both memories AND document chunks (recommended) |

View file

@ -14,10 +14,53 @@ export interface ConversationMessage {
tool_call_id?: string
}
export interface ContentPart {
type: "text" | "image_url"
text?: string
image_url?: { url: string }
export type ContentPart =
| { type: "text"; text: string }
| { type: "image_url"; imageUrl: { url: string } }
const BASE64_ALPHABET =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
const encodeBase64 = (bytes: Uint8Array): string => {
let encoded = ""
for (let index = 0; index < bytes.length; index += 3) {
const first = bytes[index] ?? 0
const second = bytes[index + 1]
const third = bytes[index + 2]
const value = (first << 16) | ((second ?? 0) << 8) | (third ?? 0)
encoded += BASE64_ALPHABET[(value >> 18) & 63]
encoded += BASE64_ALPHABET[(value >> 12) & 63]
encoded += second === undefined ? "=" : BASE64_ALPHABET[(value >> 6) & 63]
encoded += third === undefined ? "=" : BASE64_ALPHABET[value & 63]
}
return encoded
}
/** Normalize supported SDK image representations for `/v4/conversations`. */
export const toConversationImageUrl = (
value: unknown,
mediaType = "image/jpeg",
): string | null => {
if (typeof URL !== "undefined" && value instanceof URL) {
return value.toString()
}
if (typeof value === "string") {
const trimmed = value.trim()
if (!trimmed) return null
return /^[a-z][a-z\d+.-]*:/i.test(trimmed)
? trimmed
: `data:${mediaType};base64,${trimmed}`
}
const bytes =
value instanceof Uint8Array
? value
: value instanceof ArrayBuffer
? new Uint8Array(value)
: null
return bytes && bytes.length > 0
? `data:${mediaType};base64,${encodeBase64(bytes)}`
: null
}
export interface ToolCall {
@ -34,7 +77,6 @@ export interface AddConversationParams {
messages: ConversationMessage[]
containerTags?: string[]
metadata?: Record<string, string | number | boolean>
entityContext?: string
apiKey: string
baseUrl?: string
}
@ -89,7 +131,6 @@ export async function addConversation(
messages: params.messages,
containerTags: params.containerTags,
metadata: params.metadata,
entityContext: params.entityContext,
}),
redirect: "error",
signal: AbortSignal.timeout(CONVERSATION_REQUEST_TIMEOUT_MS),

View file

@ -2,7 +2,7 @@ export type { SupermemoryToolsConfig } from "./types"
export type { OpenAIMiddlewareOptions } from "./openai"
export type { SupermemoryVoltAgent } from "./voltagent"
export type { SupermemoryVoltAgent } from "./voltagent/options"
export {
TOOL_DESCRIPTIONS,

View file

@ -1,6 +1,11 @@
import type OpenAI from "openai"
import { APIPromise } from "openai/core"
import Supermemory from "supermemory"
import { addConversation } from "../conversations-client"
import {
addConversation,
type ContentPart as ConversationContentPart,
type ConversationMessage,
} from "../conversations-client"
import { deduplicateMemoriesForMode } from "../tools-shared"
import { createLogger, type Logger } from "../vercel/logger"
import { convertProfileToMarkdown } from "../vercel/util"
@ -12,6 +17,53 @@ const normalizeBaseUrl = (url?: string): string => {
const PROFILE_REQUEST_TIMEOUT_MS = 30_000
const deferAPIPromise = <T>(
start: () => Promise<{ request: APIPromise<T> }>,
): APIPromise<T> => {
const ready = start()
const responsePromise = ready.then(async ({ request }) => ({
response: await request.asResponse(),
options: {} as never,
controller: new AbortController(),
}))
return new APIPromise<T>(responsePromise, async () => {
const { request } = await ready
return await request
})
}
const convertConversationContent = (
content: unknown,
): string | ConversationContentPart[] => {
if (typeof content === "string") return content
if (!Array.isArray(content)) return ""
const converted: ConversationContentPart[] = []
for (const value of content) {
if (!value || typeof value !== "object") continue
const part = value as {
type?: unknown
text?: unknown
image_url?: { url?: unknown }
}
if (part.type === "text" && typeof part.text === "string") {
converted.push({ type: "text", text: part.text })
} else if (
part.type === "image_url" &&
typeof part.image_url?.url === "string"
) {
converted.push({
type: "image_url",
imageUrl: { url: part.image_url.url },
})
}
}
return converted
}
export interface OpenAIMiddlewareOptions {
/** Container tag/identifier for memory search (e.g., user ID, project ID). Required. */
containerTag: string
@ -100,9 +152,11 @@ const supermemoryProfileSearch = async (
? JSON.stringify({
q: queryText,
containerTag: containerTag,
include: ["static", "dynamic"],
})
: JSON.stringify({
containerTag: containerTag,
include: ["static", "dynamic"],
})
try {
@ -336,27 +390,24 @@ const addMemoryTool = async (
const conversationId = customId.replace("conversation:", "")
// Convert OpenAI messages to conversation format
const conversationMessages = messages.map((msg) => ({
role: msg.role as "user" | "assistant" | "system" | "tool",
content:
typeof msg.content === "string"
? msg.content
: Array.isArray(msg.content)
? msg.content
.filter((c) => c.type === "text")
.map((c) => ({
type: "text" as const,
text: (c as { type: "text"; text: string }).text,
}))
: "",
...("name" in msg && msg.name && { name: msg.name }),
...("tool_calls" in msg &&
msg.tool_calls && { tool_calls: msg.tool_calls }),
...("tool_call_id" in msg &&
msg.tool_call_id && {
tool_call_id: msg.tool_call_id,
}),
}))
const conversationMessages: ConversationMessage[] = messages.map(
(msg) => ({
role:
msg.role === "developer"
? "system"
: msg.role === "function"
? "tool"
: msg.role,
content: convertConversationContent(msg.content),
...("name" in msg && msg.name && { name: msg.name }),
...("tool_calls" in msg &&
msg.tool_calls && { tool_calls: msg.tool_calls }),
...("tool_call_id" in msg &&
msg.tool_call_id && {
tool_call_id: msg.tool_call_id,
}),
}),
)
const response = await addConversation({
conversationId,
@ -538,7 +589,7 @@ export function createOpenAIMiddleware(
return memories
}
const createResponsesWithMemory = async (
const prepareResponsesWithMemory = async (
params: Parameters<typeof originalResponsesCreate>[0],
requestOptions?: OpenAI.RequestOptions,
) => {
@ -552,11 +603,13 @@ export function createOpenAIMiddleware(
if (mode !== "profile" && !input) {
logger.debug("No input found for Responses API, skipping memory search")
return originalResponsesCreate.call(
openaiClient.responses,
params,
requestOptions,
)
return {
request: originalResponsesCreate.call(
openaiClient.responses,
params,
requestOptions,
),
}
}
logger.info("Starting memory search for Responses API", {
@ -594,31 +647,58 @@ export function createOpenAIMiddleware(
? `${params.instructions || ""}\n\n${memories}`.trim()
: params.instructions
return originalResponsesCreate.call(
openaiClient.responses,
{
...params,
instructions: enhancedInstructions,
},
requestOptions,
)
return {
request: originalResponsesCreate.call(
openaiClient.responses,
{
...params,
instructions: enhancedInstructions,
},
requestOptions,
),
}
}
const createWithMemory = async (
const createResponsesWithMemory = (
params: Parameters<typeof originalResponsesCreate>[0],
requestOptions?: OpenAI.RequestOptions,
) => deferAPIPromise(() => prepareResponsesWithMemory(params, requestOptions))
const prepareCreateWithMemory = async (
params: OpenAI.Chat.Completions.ChatCompletionCreateParams,
requestOptions?: OpenAI.RequestOptions,
) => {
const messages = Array.isArray(params.messages) ? params.messages : []
const userMessage = getLastUserMessage(messages)
const hasUserMessage = messages.some((message) => message.role === "user")
const shouldPersist =
addMemory === "always" &&
(customId ? hasUserMessage : Boolean(userMessage.trim()))
const memoryContent = customId
? getConversationContent(messages)
: userMessage
const memoryCustomId = customId ? `conversation:${customId}` : undefined
if (mode !== "profile") {
const userMessage = getLastUserMessage(messages)
if (!userMessage) {
logger.debug("No user message found, skipping memory search")
return originalCreate.call(
if (mode !== "profile" && !userMessage) {
if (shouldPersist) {
await addMemoryTool(
client,
containerTag,
memoryContent,
memoryCustomId,
logger,
messages,
apiKey,
baseUrl,
)
}
logger.debug("No textual user message found, skipping memory search")
return {
request: originalCreate.call(
openaiClient.chat.completions,
params,
requestOptions,
)
),
}
}
@ -630,27 +710,19 @@ export function createOpenAIMiddleware(
const operations: Promise<unknown>[] = []
if (addMemory === "always") {
const userMessage = getLastUserMessage(messages)
if (userMessage?.trim()) {
const content = customId
? getConversationContent(messages)
: userMessage
const memoryCustomId = customId ? `conversation:${customId}` : undefined
operations.push(
addMemoryTool(
client,
containerTag,
content,
memoryCustomId,
logger,
messages,
apiKey,
baseUrl,
),
)
}
if (shouldPersist) {
operations.push(
addMemoryTool(
client,
containerTag,
memoryContent,
memoryCustomId,
logger,
messages,
apiKey,
baseUrl,
),
)
}
operations.push(
@ -658,18 +730,27 @@ export function createOpenAIMiddleware(
)
const results = await Promise.all(operations)
const enhancedMessages = results[results.length - 1] // Enhanced messages result is always last
const enhancedMessages = results[
results.length - 1
] as OpenAI.Chat.Completions.ChatCompletionMessageParam[] // Enhanced messages result is always last
return originalCreate.call(
openaiClient.chat.completions,
{
...params,
messages: enhancedMessages,
},
requestOptions,
)
return {
request: originalCreate.call(
openaiClient.chat.completions,
{
...params,
messages: enhancedMessages,
},
requestOptions,
),
}
}
const createWithMemory = (
params: OpenAI.Chat.Completions.ChatCompletionCreateParams,
requestOptions?: OpenAI.RequestOptions,
) => deferAPIPromise(() => prepareCreateWithMemory(params, requestOptions))
openaiClient.chat.completions.create =
createWithMemory as typeof originalCreate

View file

@ -32,9 +32,11 @@ export const supermemoryProfileSearch = async (
? JSON.stringify({
q: queryText,
containerTag: containerTag,
include: ["static", "dynamic"],
})
: JSON.stringify({
containerTag: containerTag,
include: ["static", "dynamic"],
})
try {

View file

@ -2,7 +2,7 @@ import {
type LanguageModel,
type LanguageModelCallOptions,
type LanguageModelStreamPart,
getLastUserMessage,
hasPersistableUserContent,
} from "./util"
import {
createSupermemoryContext,
@ -182,11 +182,9 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
const result = await target.doGenerate(modelParams as any)
const userMessage = getLastUserMessage(params)
if (
ctx.addMemory === "always" &&
userMessage &&
userMessage.trim()
hasPersistableUserContent(params)
) {
const assistantResponseText = extractAssistantResponseText(
result.content as unknown[],
@ -261,11 +259,9 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
controller.enqueue(chunk)
},
flush: async () => {
const userMessage = getLastUserMessage(params)
if (
ctx.addMemory === "always" &&
userMessage &&
userMessage.trim()
hasPersistableUserContent(params)
) {
saveMemoryAfterResponse(
ctx.client,

View file

@ -3,6 +3,7 @@ import {
addConversation,
type ContentPart,
type ConversationMessage,
toConversationImageUrl,
} from "../conversations-client"
import {
createLogger,
@ -105,13 +106,12 @@ export const convertToConversationMessages = (
})
} else if (
content.type === "file" &&
typeof content.data === "string" &&
content.mediaType.startsWith("image/")
) {
contentParts.push({
type: "image_url",
image_url: { url: content.data },
})
const url = toConversationImageUrl(content.data, content.mediaType)
if (url) {
contentParts.push({ type: "image_url", imageUrl: { url } })
}
} else if (
includeToolCalls &&
content.type === "tool-call" &&

View file

@ -3,11 +3,8 @@ import type {
LanguageModelV2CallOptions,
LanguageModelV2Message,
LanguageModelV2StreamPart,
LanguageModelV3,
LanguageModelV3CallOptions,
LanguageModelV3Message,
LanguageModelV3StreamPart,
} from "@ai-sdk/provider"
import { toConversationImageUrl } from "../conversations-client"
// Re-export shared types for backward compatibility
export type {
@ -15,17 +12,23 @@ export type {
ProfileMarkdownData,
} from "../shared"
// Union types for dual SDK version support (V2 = SDK 5, V3 = SDK 6)
export type LanguageModel = LanguageModelV2 | LanguageModelV3
export type LanguageModelCallOptions =
| LanguageModelV2CallOptions
| LanguageModelV3CallOptions
export type LanguageModelMessage =
| LanguageModelV2Message
| LanguageModelV3Message
export type LanguageModelStreamPart =
| LanguageModelV2StreamPart
| LanguageModelV3StreamPart
// Provider v2 does not export V3 names, so keep the public declaration on the
// common V2 surface and structurally accept V3 models at the wrapper boundary.
type LanguageModelV3Compat = Omit<
LanguageModelV2,
"specificationVersion" | "doGenerate" | "doStream"
> & {
readonly specificationVersion: "v3"
// biome-ignore lint/suspicious/noExplicitAny: Bridges mutually exclusive provider major declarations.
doGenerate(...args: any[]): PromiseLike<any>
// biome-ignore lint/suspicious/noExplicitAny: Bridges mutually exclusive provider major declarations.
doStream(...args: any[]): PromiseLike<any>
}
export type LanguageModel = LanguageModelV2 | LanguageModelV3Compat
export type LanguageModelCallOptions = LanguageModelV2CallOptions
export type LanguageModelMessage = LanguageModelV2Message
export type LanguageModelStreamPart = LanguageModelV2StreamPart
export type OutputContentItem =
| { type: "text"; text: string }
@ -73,6 +76,38 @@ export const getLastUserMessage = (
.join(" ")
}
/** Whether the prompt contains user content that `/v4/conversations` can store. */
export const hasPersistableUserContent = (
params: LanguageModelCallOptions,
): boolean => {
return params.prompt.some((message) => {
if (message.role !== "user") return false
const content: unknown = message.content
if (typeof content === "string") {
return Boolean(content.trim())
}
if (!Array.isArray(content)) return false
return content.some((value) => {
if (!value || typeof value !== "object") return false
const part = value as {
type?: unknown
text?: unknown
mediaType?: unknown
data?: unknown
}
if (part.type === "text" && typeof part.text === "string") {
return Boolean(part.text.trim())
}
return (
part.type === "file" &&
typeof part.mediaType === "string" &&
part.mediaType.startsWith("image/") &&
toConversationImageUrl(part.data, part.mediaType) !== null
)
})
})
}
export const filterOutSupermemories = (content: string) => {
return content.split("User Supermemories: ")[0]
}

View file

@ -18,6 +18,32 @@ import {
saveConversation,
} from "./middleware"
const getInputMessages = (input: unknown): VoltAgentMessage[] => {
if (typeof input === "string") {
return input.trim() ? [{ role: "user", content: input }] : []
}
if (Array.isArray(input)) return input as VoltAgentMessage[]
if (
input &&
typeof input === "object" &&
"messages" in input &&
Array.isArray(input.messages)
) {
return input.messages as VoltAgentMessage[]
}
return []
}
const getOutputText = (output: unknown): string => {
if (typeof output === "string") return output
if (!output || typeof output !== "object") return ""
if ("text" in output && typeof output.text === "string") return output.text
if ("content" in output && typeof output.content === "string") {
return output.content
}
return ""
}
/**
* Creates Supermemory hooks for VoltAgent agents.
*
@ -41,7 +67,6 @@ import {
* const agent = new Agent({
* name: "my-agent",
* instructions: "You are a helpful assistant",
* llm: new VercelAIProvider(),
* model: openai("gpt-4o"),
* hooks
* })
@ -54,16 +79,12 @@ export function createSupermemoryHooks(
const ctx = createSupermemoryContext(containerTag, options)
return {
onPrepareMessages: async (
args: HookPrepareMessagesArgs,
): Promise<{ messages: VoltAgentMessage[] }> => {
onPrepareMessages: async (args: HookPrepareMessagesArgs) => {
try {
// VoltAgent passes user messages in args.context.input.messages
// and the prepared messages (system + conversation) in args.messages
const contextInput = args.context?.input as
| { messages?: VoltAgentMessage[] }
| undefined
const inputMessages = contextInput?.messages || []
// VoltAgent 2.x supplies canonical UI messages directly on the hook.
const inputMessages = (args.rawMessages ??
args.messages) as unknown as VoltAgentMessage[]
const preparedMessages = args.messages as unknown as VoltAgentMessage[]
ctx.logger.debug("onPrepareMessages called", {
messageCount: args.messages.length,
@ -74,7 +95,7 @@ export function createSupermemoryHooks(
const enhancedMessages = await enhanceMessagesWithMemories(
inputMessages,
ctx,
args.messages,
preparedMessages,
)
ctx.logger.debug("Messages enhanced with memories", {
@ -82,7 +103,9 @@ export function createSupermemoryHooks(
enhancedCount: enhancedMessages.length,
})
return { messages: enhancedMessages }
return {
messages: enhancedMessages as unknown as typeof args.messages,
}
} catch (error) {
ctx.logger.error("Error in onPrepareMessages", {
error: error instanceof Error ? error.message : "Unknown error",
@ -102,19 +125,8 @@ export function createSupermemoryHooks(
let messages: VoltAgentMessage[] = []
if (args.context?.input && args.output) {
const inputData = args.context.input as
| { messages?: VoltAgentMessage[] }
| undefined
const inputMessages = inputData?.messages || []
const outputData = args.output as
| string
| { text?: string; content?: string }
| undefined
const outputText =
typeof outputData === "string"
? outputData
: outputData?.text || outputData?.content
const inputMessages = getInputMessages(args.context.input)
const outputText = getOutputText(args.output)
if (inputMessages.length > 0 && outputText) {
messages = [

View file

@ -43,15 +43,15 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
* @param options.apiKey - Supermemory API key (falls back to SUPERMEMORY_API_KEY env var)
* @param options.baseUrl - Custom Supermemory API base URL
* @param options.promptTemplate - Custom function to format memory data into prompt
* @param options.threshold - Search sensitivity: 0 (more results) to 1 (more accurate). Default: 0.1
* @param options.limit - Maximum number of memory results to return. Default: 10
* @param options.threshold - Search sensitivity: 0 (more results) to 1 (more accurate)
* @param options.limit - Maximum number of memory results to return (integer from 1 to 100)
* @param options.rerank - If true, rerank results for relevance. Default: false
* @param options.rewriteQuery - If true, AI-rewrite query for better results (+400ms latency). Default: false
* @param options.filters - Advanced AND/OR filters for search
* @param options.include - Control what additional data to include (chunks, documents, etc.)
* @param options.metadata - Optional metadata to attach to saved conversations
* @param options.searchMode - Search mode: "memories" (atomic facts), "documents" (chunks), or "hybrid" (both)
* @param options.entityContext - Context for memory extraction (max 1500 chars), guides how memories are understood
* @param options.entityContext - Deprecated and ignored; configure entity context on the container tag instead
* @returns Enhanced agent config with Supermemory hooks injected
*
* @example
@ -59,14 +59,12 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
* ```typescript
* import { withSupermemory } from "@supermemory/tools/voltagent"
* import { Agent } from "@voltagent/core"
* import { VercelAIProvider } from "@voltagent/vercel-ai"
* import { openai } from "@ai-sdk/openai"
*
* const configWithMemory = withSupermemory({
* agentConfig: {
* name: "my-agent",
* instructions: "You are a helpful assistant",
* llm: new VercelAIProvider(),
* model: openai("gpt-4o"),
* },
* containerTag: "user-123",
@ -83,7 +81,6 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
* agentConfig: {
* name: "my-agent",
* instructions: "You are a helpful assistant",
* llm: new VercelAIProvider(),
* model: openai("gpt-4o"),
* },
* containerTag: "user-123", // Required: user/project ID
@ -94,7 +91,6 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
* limit: 15, // Max results to return
* rerank: true, // Rerank for best relevance
* searchMode: "hybrid", // "memories" | "documents" | "hybrid"
* entityContext: "This is John, a software engineer saving technical discussions",
* metadata: { // Custom metadata
* source: "voltagent",
* version: "1.0"
@ -104,9 +100,9 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
* const agent = new Agent(configWithMemory)
*
* // Use the agent - memories are automatically injected
* const result = await agent.generateText({
* messages: [{ role: "user", content: "What's my favorite programming language?" }]
* })
* const result = await agent.generateText(
* "What's my favorite programming language?",
* )
* ```
*
* @example
@ -116,7 +112,6 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
* agentConfig: {
* name: "my-agent",
* instructions: "...",
* llm: new VercelAIProvider(),
* model: openai("gpt-4o"),
* },
* containerTag: "user-123",
@ -138,7 +133,7 @@ interface WithSupermemoryOptions<T extends VoltAgentConfig>
*/
export function withSupermemory<T extends VoltAgentConfig>(
options: WithSupermemoryOptions<T>,
): T {
): T & { hooks: NonNullable<VoltAgentConfig["hooks"]> } {
const { agentConfig, containerTag, ...supermemoryOptions } = options
// Create Supermemory hooks (internally creates its own context, validates API key)

View file

@ -7,7 +7,9 @@
import Supermemory from "supermemory"
import {
addConversation,
type ContentPart as ConversationContentPart,
type ConversationMessage,
toConversationImageUrl,
} from "../conversations-client"
import {
createLogger,
@ -62,7 +64,6 @@ export interface SupermemoryMiddlewareContext {
// Storage parameters
metadata?: Record<string, string | number | boolean>
searchMode?: "memories" | "documents" | "hybrid"
entityContext?: string
}
/**
@ -93,7 +94,6 @@ export const createSupermemoryContext = (
include,
metadata,
searchMode,
entityContext,
verbose = false,
} = options
@ -103,8 +103,25 @@ export const createSupermemoryContext = (
"customId is required and must be a non-empty string — provide it via `options.customId`",
)
}
if (
threshold !== undefined &&
(!Number.isFinite(threshold) || threshold < 0 || threshold > 1)
) {
throw new Error("threshold must be between 0 and 1")
}
if (
limit !== undefined &&
(!Number.isInteger(limit) || limit < 1 || limit > 100)
) {
throw new Error("limit must be an integer between 1 and 100")
}
const logger = createLogger(verbose)
if (options.entityContext !== undefined) {
logger.warn(
"entityContext is not supported by /v4/conversations and will be ignored; configure it on the container tag instead.",
)
}
const normalizedBaseUrl = normalizeBaseUrl(baseUrl)
const client = new Supermemory({
@ -133,7 +150,6 @@ export const createSupermemoryContext = (
include,
metadata,
searchMode,
entityContext,
}
}
@ -160,6 +176,21 @@ const isNewUserTurn = (messages: VoltAgentMessage[]): boolean => {
return lastMessage?.role === "user"
}
type VoltAgentContentPart = {
type: string
text?: string
[key: string]: unknown
}
const getMessageContent = (
message: VoltAgentMessage,
): string | VoltAgentContentPart[] => {
if (typeof message.content === "string" || Array.isArray(message.content)) {
return message.content
}
return Array.isArray(message.parts) ? message.parts : ""
}
/**
* Extracts the last user message text from messages array.
*/
@ -173,7 +204,7 @@ const getLastUserMessage = (messages: VoltAgentMessage[]): string => {
return ""
}
const content = lastUserMessage.content
const content = getMessageContent(lastUserMessage)
if (typeof content === "string") {
return content
@ -234,7 +265,7 @@ export const enhanceMessagesWithMemories = async (
const genericMessages = messages.map((msg) => ({
role: msg.role,
content: msg.content,
content: getMessageContent(msg),
}))
const queryText = extractQueryText(genericMessages, ctx.mode)
@ -388,40 +419,58 @@ const convertToConversationMessages = (
messages: VoltAgentMessage[],
): ConversationMessage[] => {
const conversationMessages: ConversationMessage[] = []
const convertPart = (
part: VoltAgentContentPart,
): ConversationContentPart | null => {
if (part.type === "text" && typeof part.text === "string" && part.text) {
return { type: "text", text: part.text }
}
if (part.type === "file") {
const mediaType = part.mediaType
const url =
typeof mediaType === "string" && mediaType.startsWith("image/")
? toConversationImageUrl(part.url ?? part.data, mediaType)
: null
if (url) return { type: "image_url", imageUrl: { url } }
}
if (part.type === "image") {
const mediaType =
typeof part.mediaType === "string" ? part.mediaType : "image/jpeg"
const url = toConversationImageUrl(part.image, mediaType)
if (url) return { type: "image_url", imageUrl: { url } }
}
if (part.type === "image_url") {
const imageUrl =
typeof part.imageUrl === "object" && part.imageUrl
? (part.imageUrl as { url?: unknown })
: typeof part.image_url === "object" && part.image_url
? (part.image_url as { url?: unknown })
: undefined
if (typeof imageUrl?.url === "string") {
return { type: "image_url", imageUrl: { url: imageUrl.url } }
}
}
return null
}
for (const msg of messages) {
if (msg.role === "system") {
continue
}
if (typeof msg.content === "string") {
if (msg.content) {
conversationMessages.push({
role: msg.role as "user" | "assistant" | "tool",
content: msg.content,
})
}
} else if (Array.isArray(msg.content)) {
const contentParts = msg.content
.map((c) => {
if (c.type === "text" && c.text) {
return {
type: "text" as const,
text: c.text,
}
}
// Handle image URLs if present
if (c.type === "image_url" && typeof c.image_url === "object") {
const imageUrl = c.image_url as { url?: string }
if (imageUrl.url) {
return {
type: "image_url" as const,
image_url: { url: imageUrl.url },
}
}
}
return null
})
const structuredParts = Array.isArray(msg.parts)
? msg.parts
: Array.isArray(msg.content)
? msg.content
: undefined
if (structuredParts) {
const contentParts = structuredParts
.map(convertPart)
.filter((part) => part !== null)
if (contentParts.length > 0) {
@ -430,6 +479,13 @@ const convertToConversationMessages = (
content: contentParts,
})
}
} else if (typeof msg.content === "string") {
if (msg.content) {
conversationMessages.push({
role: msg.role as "user" | "assistant" | "tool",
content: msg.content,
})
}
}
}
@ -460,7 +516,6 @@ export const saveConversation = async (
messages: conversationMessages,
containerTags: [ctx.containerTag],
metadata: ctx.metadata,
entityContext: ctx.entityContext,
apiKey: ctx.apiKey,
baseUrl: ctx.normalizedBaseUrl,
})

View file

@ -0,0 +1,109 @@
/**
* Peer-free configuration types for the VoltAgent integration.
*
* This module intentionally avoids importing @voltagent/core so the root
* @supermemory/tools declarations remain usable when the optional peer is absent.
*/
import type Supermemory from "supermemory"
import type { SupermemoryBaseOptions } from "../shared"
/**
* Configuration options for the Supermemory VoltAgent integration.
* Extends base options with VoltAgent-specific settings.
*/
export interface SupermemoryVoltAgent extends SupermemoryBaseOptions {
/**
* Custom ID to group messages into a single document.
* Ensures related messages are added to the same document for that conversation.
*/
customId: string
/**
* Threshold / sensitivity for memory selection. 0 is least sensitive (returns
* most memories, more results), 1 is most sensitive (returns fewer memories,
* more accurate results). When omitted, the selected backend route applies
* its own default.
*
* Note: Only effective when mode is "query" or "full". Ignored in "profile" mode.
*/
threshold?: number
/**
* Maximum number of memory results to return. Must be an integer between 1
* and 100. When omitted, the selected backend route applies its own default.
*
* Note: Only effective when mode is "query" or "full". Ignored in "profile" mode.
*/
limit?: number
/**
* If true, rerank the results based on the query. This helps ensure the most
* relevant results are returned. Default: false
*
* Note: Only effective when mode is "query" or "full". Ignored in "profile" mode.
*/
rerank?: boolean
/**
* If true, rewrites the query to make it easier to find memories. This increases
* latency by about 400ms. Default: false
*
* Note: Only effective when mode is "query" or "full". Ignored in "profile" mode.
*/
rewriteQuery?: boolean
/**
* Advanced filters to apply to the search using AND/OR logic.
* Example: { OR: [{ key: "type", value: "note" }, { key: "type", value: "conversation" }] }
*
* Note: Only effective when mode is "query" or "full". Ignored in "profile" mode.
*/
filters?: SearchFilters
/**
* Control what additional data to include in search results.
*
* Note: Only effective when mode is "query" or "full". Ignored in "profile" mode.
*/
include?: IncludeOptions
/**
* Optional metadata to attach to saved documents/conversations.
* Can include strings, numbers, or booleans.
*/
metadata?: Record<string, string | number | boolean>
/**
* Search mode controlling what type of results to search.
* - "memories": Search only memory entries (atomic facts)
* - "documents": Search only document chunks
* - "hybrid": Search both memories AND document chunks (recommended)
*
* Note: Only effective when mode is "query" or "full". Ignored in "profile" mode.
*/
searchMode?: "memories" | "documents" | "hybrid"
/**
* @deprecated The conversations API does not accept per-request entity context.
* Configure entity context on the container tag instead.
*/
entityContext?: string
}
/** Advanced search filters using AND/OR logic. */
export type SearchFilters = NonNullable<Supermemory.SearchParams["filters"]>
/** Options for including additional data in search results. */
export interface IncludeOptions {
/** Fetch chunks from documents associated with found memories. */
chunks?: boolean
/** Include full document information in results. */
documents?: boolean
/** Include explicitly forgotten or expired memories. */
forgottenMemories?: boolean
/** Include parent/child memories from the memory graph. */
relatedMemories?: boolean
/** Include document summaries in results. */
summaries?: boolean
}

View file

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