supermemory/packages/tools/src/conversations-client.ts
MaheshtheDev 5c24e67d60 feat(tools)!: @supermemory/tools 3.0 on the v5 API, retire @supermemory/ai-sdk (#1773)
Every integration (AI SDK, OpenAI, Mastra, VoltAgent, Claude memory) now calls v5 through `supermemory@5`. The config takes one `namespace` instead of `containerTags` or `projectId`, and `withSupermemory` takes `namespace` and `id` instead of `containerTag` and `customId`. No aliases. With no config the tools still use `sm_project_default`.

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

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

Tested against production: add, search, profile, list and delete. 112 unit tests pass; type errors drop from 148 to 141, none new. The `supermemory` dependency pins the rc until 5.0.0 is published.
2026-10-06 17:03:47 +00:00

140 lines
3.8 KiB
TypeScript

import type { AddResponse } from "supermemory"
import { createSupermemoryClient } from "./shared/context"
export interface ConversationMessage {
role: "user" | "assistant" | "system" | "tool"
content: string | ContentPart[]
name?: string
tool_calls?: ToolCall[]
tool_call_id?: 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 to a URL or data URL. */
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 {
id: string
type: "function"
function: {
name: string
arguments: string
}
}
export interface AddConversationParams {
id: string
messages: ConversationMessage[]
namespace: string
metadata?: Record<string, string | number | boolean>
supportingContext?: string
apiKey: string
baseUrl?: string
}
const CONVERSATION_REQUEST_TIMEOUT_SECONDS = 30
const ROLE_LABELS: Record<ConversationMessage["role"], string> = {
user: "User",
assistant: "Assistant",
system: "System",
tool: "Tool",
}
// Data URLs are dropped: inlining base64 would bloat the document text.
const formatContentPart = (part: ContentPart): string => {
if (part.type === "text") return part.text
const url = part.imageUrl.url
return url.startsWith("data:") ? "[image]" : `[image: ${url}]`
}
export function formatConversationText(
messages: ConversationMessage[],
): string {
return messages
.map((message) => {
const content =
typeof message.content === "string"
? message.content
: message.content.map(formatContentPart).join(" ")
const toolCalls = (message.tool_calls ?? [])
.map(
(call) =>
`[tool call: ${call.function.name}(${call.function.arguments})]`,
)
.join(" ")
const label = message.name
? `${ROLE_LABELS[message.role]} (${message.name})`
: ROLE_LABELS[message.role]
return `${label}: ${[content, toolCalls].filter(Boolean).join(" ")}`
})
.join("\n\n")
}
// Same id → same document, so one conversation stays one document.
export async function addConversation(
params: AddConversationParams,
): Promise<AddResponse> {
const client = createSupermemoryClient({
apiKey: params.apiKey,
baseUrl: params.baseUrl,
})
return await client.add(
params.namespace,
{
content: formatConversationText(params.messages),
id: params.id,
dreaming: "instant",
...(params.metadata && { metadata: params.metadata }),
...(params.supportingContext && {
supportingContext: params.supportingContext,
}),
},
{ timeoutInSeconds: CONVERSATION_REQUEST_TIMEOUT_SECONDS, maxRetries: 0 },
)
}