mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-11 22:51:05 +00:00
fixed merge conflicts
This commit is contained in:
commit
c0bbc16821
16 changed files with 417 additions and 197 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.4
|
||||
bun-version: 1.3.6
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
|
|
|||
3
.github/workflows/publish-ai-sdk.yml
vendored
3
.github/workflows/publish-ai-sdk.yml
vendored
|
|
@ -31,9 +31,6 @@ jobs:
|
|||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
|
|
|||
3
.github/workflows/publish-memory-graph.yml
vendored
3
.github/workflows/publish-memory-graph.yml
vendored
|
|
@ -31,9 +31,6 @@ jobs:
|
|||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
|
|
|||
3
.github/workflows/publish-tools.yml
vendored
3
.github/workflows/publish-tools.yml
vendored
|
|
@ -31,9 +31,6 @@ jobs:
|
|||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
|
|
|||
|
|
@ -127,6 +127,18 @@ const model = withSupermemory(openai("gpt-4"), {
|
|||
// Console output shows memory retrieval details
|
||||
```
|
||||
|
||||
### When Supermemory errors (optional: continue without memories)
|
||||
|
||||
If the Supermemory API returns an error (or is unreachable), memory retrieval fails before the LLM runs. By default that error **propagates** (fails the call).
|
||||
|
||||
To continue the LLM request **without** injected memories instead, opt in with `skipMemoryOnError: true`. Use `verbose: true` if you want console output when that happens.
|
||||
|
||||
```typescript
|
||||
const model = withSupermemory(openai("gpt-5"), "user-123", {
|
||||
skipMemoryOnError: true
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Tools
|
||||
|
|
|
|||
|
|
@ -24,9 +24,12 @@ type Props = {
|
|||
name?: string
|
||||
}
|
||||
|
||||
const CONTAINER_TAGS_TTL_MS = 5 * 60 * 1000
|
||||
|
||||
export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
|
||||
private clientInfo: { name: string; version?: string } | null = null
|
||||
private cachedContainerTags: string[] = []
|
||||
private containerTagsLastFetchedAt: number | null = null
|
||||
|
||||
server = new McpServer({
|
||||
name: "supermemory",
|
||||
|
|
@ -168,8 +171,8 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
|
|||
"supermemory://projects",
|
||||
{},
|
||||
async () => {
|
||||
const client = this.getClient()
|
||||
const projects = await client.getProjects()
|
||||
await this.ensureContainerTagsFresh()
|
||||
const projects = this.cachedContainerTags
|
||||
|
||||
return {
|
||||
contents: [
|
||||
|
|
@ -193,15 +196,19 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
|
|||
refresh: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe("Refresh the list from the server (default: true)"),
|
||||
.default(false)
|
||||
.describe(
|
||||
"Force refresh from the server (default: false; uses cache with TTL)",
|
||||
),
|
||||
}),
|
||||
},
|
||||
// @ts-expect-error - zod type inference issue with MCP SDK
|
||||
async (args: { refresh?: boolean }) => {
|
||||
try {
|
||||
if (args.refresh !== false) {
|
||||
if (args.refresh === true) {
|
||||
await this.refreshContainerTags()
|
||||
} else {
|
||||
await this.ensureContainerTagsFresh()
|
||||
}
|
||||
const projects = this.cachedContainerTags
|
||||
|
||||
|
|
@ -566,6 +573,10 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
|
|||
|
||||
const result = await client.createMemory(content)
|
||||
|
||||
if (!this.cachedContainerTags.includes(result.containerTag)) {
|
||||
await this.refreshContainerTags()
|
||||
}
|
||||
|
||||
// Track memory added event
|
||||
posthog
|
||||
.memoryAdded({
|
||||
|
|
@ -758,10 +769,21 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
|
|||
return this.ctx.id.name || "unknown"
|
||||
}
|
||||
|
||||
private async ensureContainerTagsFresh(): Promise<void> {
|
||||
const now = Date.now()
|
||||
const needsRefresh =
|
||||
this.containerTagsLastFetchedAt === null ||
|
||||
now - this.containerTagsLastFetchedAt > CONTAINER_TAGS_TTL_MS
|
||||
if (needsRefresh) {
|
||||
await this.refreshContainerTags()
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshContainerTags(): Promise<void> {
|
||||
try {
|
||||
const client = this.getClient()
|
||||
this.cachedContainerTags = await client.getProjects()
|
||||
this.containerTagsLastFetchedAt = Date.now()
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch container tags:", error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@
|
|||
"resolveJsonModule": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"baseUrl": "."
|
||||
"paths": {
|
||||
"*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
import { EnsureWorkspace } from "@/components/ensure-workspace"
|
||||
import { MobileBanner } from "@/components/mobile-banner"
|
||||
import { NextAppResearchCta } from "@/components/next-app-research-cta"
|
||||
|
||||
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<MobileBanner />
|
||||
<EnsureWorkspace>{children}</EnsureWorkspace>
|
||||
<NextAppResearchCta />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { Suspense } from "react"
|
|||
import { Toaster } from "@ui/components/sonner"
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app"
|
||||
import { ThemeProvider } from "@/lib/theme-provider"
|
||||
import Script from "next/script"
|
||||
|
||||
const font = Space_Grotesk({
|
||||
subsets: ["latin"],
|
||||
|
|
@ -69,6 +70,11 @@ export default function RootLayout({
|
|||
</QueryProvider>
|
||||
</AutumnProvider>
|
||||
</ThemeProvider>
|
||||
<Script
|
||||
src="https://lobbyside.com/widget.js"
|
||||
data-widget-id="e385c52f-4dd3-4fb2-81eb-da3a78059014"
|
||||
strategy="lazyOnload"
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -71,10 +71,12 @@ export function MemoryGraph({
|
|||
maxNodes={maxNodes}
|
||||
canvasRef={canvasRef}
|
||||
totalCount={totalCount}
|
||||
colors={{
|
||||
bg: "transparent",
|
||||
edgeDerives: "#9ca3af",
|
||||
} as any}
|
||||
colors={
|
||||
{
|
||||
bg: "transparent",
|
||||
edgeDerives: "#9ca3af",
|
||||
} as any
|
||||
}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
144
apps/web/components/next-app-research-cta.tsx
Normal file
144
apps/web/components/next-app-research-cta.tsx
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { Phone, Users, X as XIcon } from "lucide-react"
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSans125ClassName } from "@/lib/fonts"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
|
||||
const STORAGE_KEY = "sm_next_app_research_cta_dismissed_v1"
|
||||
|
||||
const BOOK_CALL_HREF = "https://cal.com/supermemory/growth"
|
||||
|
||||
function ResearchCtaHeroGraphic() {
|
||||
return (
|
||||
<div
|
||||
id="next-app-research-cta-hero"
|
||||
className={cn(
|
||||
"relative flex min-h-[4.5rem] w-full shrink-0 items-center justify-center overflow-hidden rounded-xl py-5",
|
||||
"border border-white/[0.1] bg-gradient-to-b from-[#141c28] to-[#0D121A]",
|
||||
)}
|
||||
aria-hidden
|
||||
>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.45]"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse 85% 90% at 50% 30%, rgba(59, 130, 246, 0.2), transparent 65%)",
|
||||
}}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div
|
||||
className="h-[120%] w-px bg-gradient-to-b from-transparent via-[#5B8DEF]/35 to-transparent"
|
||||
style={{ transform: "rotate(52deg)" }}
|
||||
/>
|
||||
<div
|
||||
className="absolute h-[120%] w-px bg-gradient-to-b from-transparent via-[#9B7AFF]/30 to-transparent"
|
||||
style={{ transform: "rotate(-52deg)" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 flex flex-row items-center justify-center gap-10 px-3">
|
||||
<Phone className="size-[24px] text-[#7EB0FF]" strokeWidth={1.65} />
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"select-none text-[17px] font-light leading-none text-[#6B9FFF]/75",
|
||||
)}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
<Users className="size-[24px] text-[#B49CFB]" strokeWidth={1.65} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function NextAppResearchCta() {
|
||||
const pathname = usePathname()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [dismissed, setDismissed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
setDismissed(localStorage.getItem(STORAGE_KEY) === "1")
|
||||
}, [])
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
localStorage.setItem(STORAGE_KEY, "1")
|
||||
setDismissed(true)
|
||||
analytics.nextAppResearchCtaDismissed()
|
||||
}, [])
|
||||
|
||||
const handleBookClick = useCallback(() => {
|
||||
analytics.nextAppResearchCtaBookCallClicked()
|
||||
}, [])
|
||||
|
||||
if (!mounted || dismissed || pathname.startsWith("/onboarding")) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
id="next-app-research-cta"
|
||||
className={cn(
|
||||
"fixed z-[45] bottom-4 left-4 max-w-[min(calc(100vw-2rem),19rem)]",
|
||||
"rounded-xl border border-white/[0.08] bg-[#0D121A]/95 backdrop-blur-md",
|
||||
"shadow-[0_8px_32px_rgba(0,0,0,0.35)] p-3.5",
|
||||
)}
|
||||
aria-label="Research participant invitation"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<ResearchCtaHeroGraphic />
|
||||
<div className="min-w-0 w-full">
|
||||
<div className="flex items-start gap-1">
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"flex-1 min-w-0 font-medium text-[12px] text-[#FAFAFA] tracking-[-0.12px]",
|
||||
)}
|
||||
>
|
||||
Be part of the next supermemory app
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDismiss}
|
||||
className={cn(
|
||||
"shrink-0 rounded-md p-1 -mr-1 -mt-0.5",
|
||||
"text-muted-foreground hover:text-foreground transition-colors",
|
||||
"cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
)}
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"mt-1 text-[12px] text-[#737373] tracking-[-0.12px]",
|
||||
)}
|
||||
>
|
||||
Share what you want next—we’d love a quick call.
|
||||
</p>
|
||||
<div className="mt-2.5 flex justify-end">
|
||||
<a
|
||||
href={BOOK_CALL_HREF}
|
||||
onClick={handleBookClick}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"inline-flex text-[13px] font-medium text-[#A3A3A3]",
|
||||
"tracking-[-0.13px] underline underline-offset-4 decoration-white/20",
|
||||
"hover:text-[#FAFAFA] hover:decoration-white/40 transition-colors",
|
||||
)}
|
||||
>
|
||||
Book a call
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -46,6 +46,11 @@ export const analytics = {
|
|||
connectionAuthCompleted: () => safeCapture("connection_auth_completed"),
|
||||
connectionAuthFailed: () => safeCapture("connection_auth_failed"),
|
||||
|
||||
nextAppResearchCtaDismissed: () =>
|
||||
safeCapture("next_app_research_cta_dismissed"),
|
||||
nextAppResearchCtaBookCallClicked: () =>
|
||||
safeCapture("next_app_research_cta_book_call_clicked"),
|
||||
|
||||
mcpViewOpened: () => safeCapture("mcp_view_opened"),
|
||||
mcpInstallCmdCopied: () => safeCapture("mcp_install_cmd_copied"),
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"packageManager": "bun@1.3.4",
|
||||
"packageManager": "bun@1.3.6",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"!apps/raycast-extension",
|
||||
|
|
|
|||
|
|
@ -197,7 +197,11 @@ export function MemoryGraph({
|
|||
setViewportVersion((v) => v + 1)
|
||||
}
|
||||
|
||||
const { hasMore: more, isLoadingMore: loading, onLoadMore: load } = loadMoreRef.current
|
||||
const {
|
||||
hasMore: more,
|
||||
isLoadingMore: loading,
|
||||
onLoadMore: load,
|
||||
} = loadMoreRef.current
|
||||
if (!more || loading || !load || !viewportRef.current) return
|
||||
|
||||
const vp = viewportRef.current
|
||||
|
|
@ -205,14 +209,17 @@ export function MemoryGraph({
|
|||
if (currentNodes.length === 0) return
|
||||
|
||||
const topLeft = vp.screenToWorld(0, 0)
|
||||
const bottomRight = vp.screenToWorld(containerSize.width, containerSize.height)
|
||||
const bottomRight = vp.screenToWorld(
|
||||
containerSize.width,
|
||||
containerSize.height,
|
||||
)
|
||||
const viewW = bottomRight.x - topLeft.x
|
||||
const viewH = bottomRight.y - topLeft.y
|
||||
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
let maxX = -Infinity
|
||||
let maxY = -Infinity
|
||||
let minX = Number.POSITIVE_INFINITY
|
||||
let minY = Number.POSITIVE_INFINITY
|
||||
let maxX = Number.NEGATIVE_INFINITY
|
||||
let maxY = Number.NEGATIVE_INFINITY
|
||||
for (const n of currentNodes) {
|
||||
if (n.x < minX) minX = n.x
|
||||
if (n.y < minY) minY = n.y
|
||||
|
|
@ -613,7 +620,6 @@ export function MemoryGraph({
|
|||
colors={colors}
|
||||
/>
|
||||
|
||||
|
||||
{!isLoading && !nodes.some((n) => n.type === "document") && children && (
|
||||
<div style={emptyStateStyle}>{children}</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,7 @@ import {
|
|||
} from "./middleware"
|
||||
import type { PromptTemplate, MemoryPromptData } from "./memory-prompt"
|
||||
|
||||
/**
|
||||
* Configuration options for Supermemory integration
|
||||
*/
|
||||
interface WithSupermemoryConfig {
|
||||
/** The container tag/identifier for memory search (e.g., user ID, project ID) */
|
||||
containerTag: string
|
||||
interface WrapVercelLanguageModelOptions {
|
||||
/** Custom ID to group messages into a single document. Required. */
|
||||
customId: string
|
||||
/** Enable detailed logging of memory search and injection */
|
||||
|
|
@ -29,18 +24,9 @@ interface WithSupermemoryConfig {
|
|||
* - "full": Combines both profile and query-based results
|
||||
*/
|
||||
mode?: "profile" | "query" | "full"
|
||||
/**
|
||||
* Search mode for memory retrieval:
|
||||
* - "memories": Search only memory entries (default)
|
||||
* - "hybrid": Search both memories AND document chunks (recommended for RAG)
|
||||
* - "documents": Search only document chunks
|
||||
*/
|
||||
searchMode?: "memories" | "hybrid" | "documents"
|
||||
/** Maximum number of search results to return when using hybrid/documents mode (default: 10) */
|
||||
searchLimit?: number
|
||||
/**
|
||||
* Memory persistence mode:
|
||||
* - "always": Automatically save conversations as memories (default)
|
||||
* - "always": Automatically save conversations as memories
|
||||
* - "never": Only retrieve memories, don't store new ones
|
||||
*/
|
||||
addMemory?: "always" | "never"
|
||||
|
|
@ -64,6 +50,12 @@ interface WithSupermemoryConfig {
|
|||
* ```
|
||||
*/
|
||||
promptTemplate?: PromptTemplate
|
||||
/**
|
||||
* When Supermemory memory retrieval / injection fails:
|
||||
* - `false` (default): propagate the error.
|
||||
* - `true`: log and call the base model with the original prompt (no memories).
|
||||
*/
|
||||
skipMemoryOnError?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -78,63 +70,44 @@ interface WithSupermemoryConfig {
|
|||
* detection of `model.specificationVersion`.
|
||||
*
|
||||
* @param model - The language model to wrap with supermemory capabilities (V2 or V3)
|
||||
* @param config - Configuration object for Supermemory integration
|
||||
* @param config.containerTag - Required. The container tag/identifier for memory search (e.g., user ID, project ID)
|
||||
* @param config.customId - Required. Custom ID to group messages into a single document
|
||||
* @param config.verbose - Optional flag to enable detailed logging of memory search and injection process (default: false)
|
||||
* @param config.mode - Optional mode for memory search: "profile", "query", or "full" (default: "profile")
|
||||
* @param config.searchMode - Optional search mode: "memories" (default), "hybrid" (memories + chunks), or "documents" (chunks only)
|
||||
* @param config.searchLimit - Optional maximum number of search results when using hybrid/documents mode (default: 10)
|
||||
* @param config.addMemory - Optional mode for memory persistence: "always" (default - saves conversations), "never" (read-only mode)
|
||||
* @param config.apiKey - Optional Supermemory API key to use instead of the environment variable
|
||||
* @param config.baseUrl - Optional base URL for the Supermemory API (default: "https://api.supermemory.ai")
|
||||
* @param containerTag - The container tag/identifier for memory search (e.g., user ID, project ID)
|
||||
* @param options - Optional configuration options for the middleware
|
||||
* @param options.customId - Required custom ID to group messages into a single document
|
||||
* @param options.verbose - Optional flag to enable detailed logging of memory search and injection process (default: false)
|
||||
* @param options.mode - Optional mode for memory search: "profile", "query", or "full" (default: "profile")
|
||||
* @param options.addMemory - Optional mode for memory search: "always", "never" (default: "never")
|
||||
* @param options.apiKey - Optional Supermemory API key to use instead of the environment variable
|
||||
* @param options.baseUrl - Optional base URL for the Supermemory API (default: "https://api.supermemory.ai")
|
||||
* @param options.skipMemoryOnError - When memory retrieval fails: `false` (default) throws; `true` continues without injected memories
|
||||
*
|
||||
* @returns A wrapped language model that automatically includes relevant memories in prompts
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { withSupermemory } from "@supermemory/tools/vercel"
|
||||
* import { withSupermemory } from "@supermemory/tools/ai-sdk"
|
||||
* import { openai } from "@ai-sdk/openai"
|
||||
* import { generateText } from "ai"
|
||||
*
|
||||
* // Basic usage with profile memories
|
||||
* const modelWithMemory = withSupermemory(
|
||||
* openai("gpt-4"),
|
||||
* {
|
||||
* containerTag: "user-123",
|
||||
* customId: "conv-456",
|
||||
* mode: "full",
|
||||
* addMemory: "always"
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* // RAG usage with hybrid search (memories + document chunks)
|
||||
* const ragModel = withSupermemory(
|
||||
* openai("gpt-4"),
|
||||
* {
|
||||
* containerTag: "user-123",
|
||||
* customId: "conv-789",
|
||||
* mode: "full",
|
||||
* searchMode: "hybrid", // Search both memories and document chunks
|
||||
* searchLimit: 15,
|
||||
* }
|
||||
* )
|
||||
* const modelWithMemory = withSupermemory(openai("gpt-4"), "user-123", {
|
||||
* customId: "conversation-456",
|
||||
* mode: "full",
|
||||
* addMemory: "always"
|
||||
* })
|
||||
*
|
||||
* const result = await generateText({
|
||||
* model: ragModel,
|
||||
* messages: [{ role: "user", content: "What's in my documents about quarterly goals?" }]
|
||||
* model: modelWithMemory,
|
||||
* messages: [{ role: "user", content: "What's my favorite programming language?" }]
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @throws {Error} When neither `config.apiKey` nor `process.env.SUPERMEMORY_API_KEY` are set
|
||||
* @throws {Error} When supermemory API request fails
|
||||
* @throws {Error} When neither `options.apiKey` nor `process.env.SUPERMEMORY_API_KEY` are set
|
||||
* @throws {Error} When supermemory memory retrieval fails unless `skipMemoryOnError` is `true`
|
||||
*/
|
||||
const wrapVercelLanguageModel = <T extends LanguageModel>(
|
||||
model: T,
|
||||
config: WithSupermemoryConfig,
|
||||
containerTag: string,
|
||||
options: WrapVercelLanguageModelOptions,
|
||||
): T => {
|
||||
const { containerTag, customId, ...restOptions } = config
|
||||
const providedApiKey = restOptions.apiKey ?? process.env.SUPERMEMORY_API_KEY
|
||||
const providedApiKey = options.apiKey ?? process.env.SUPERMEMORY_API_KEY
|
||||
|
||||
if (!providedApiKey) {
|
||||
throw new Error(
|
||||
|
|
@ -145,127 +118,170 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
|
|||
const ctx = createSupermemoryContext({
|
||||
containerTag,
|
||||
apiKey: providedApiKey,
|
||||
customId,
|
||||
verbose: restOptions.verbose ?? false,
|
||||
mode: restOptions.mode ?? "profile",
|
||||
searchMode: restOptions.searchMode ?? "memories",
|
||||
searchLimit: restOptions.searchLimit ?? 10,
|
||||
addMemory: restOptions.addMemory ?? "always",
|
||||
baseUrl: restOptions.baseUrl,
|
||||
promptTemplate: restOptions.promptTemplate,
|
||||
customId: options.customId,
|
||||
verbose: options.verbose ?? false,
|
||||
mode: options.mode ?? "profile",
|
||||
addMemory: options.addMemory ?? "never",
|
||||
baseUrl: options.baseUrl,
|
||||
promptTemplate: options.promptTemplate,
|
||||
})
|
||||
|
||||
// Use Object.create to preserve prototype chain, then copy own properties
|
||||
const wrappedModel = Object.create(
|
||||
Object.getPrototypeOf(model),
|
||||
Object.getOwnPropertyDescriptors(model),
|
||||
) as T
|
||||
const skipMemoryOnError = options.skipMemoryOnError ?? false
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
wrappedModel.doGenerate = async (
|
||||
params: LanguageModelCallOptions,
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const transformedParams = await transformParamsWithMemory(params, ctx)
|
||||
// Proxy keeps prototype/getter fields (e.g. provider, modelId) that `{ ...model }` drops.
|
||||
return new Proxy(model, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === "doGenerate") {
|
||||
return async (params: LanguageModelCallOptions) => {
|
||||
let modelParams: LanguageModelCallOptions = params
|
||||
try {
|
||||
modelParams = await transformParamsWithMemory(params, ctx)
|
||||
} catch (memoryError) {
|
||||
if (skipMemoryOnError) {
|
||||
ctx.logger.warn(
|
||||
"Supermemory retrieval failed; continuing without injected memories",
|
||||
{
|
||||
error:
|
||||
memoryError instanceof Error
|
||||
? memoryError.message
|
||||
: "Unknown error",
|
||||
},
|
||||
)
|
||||
modelParams = params
|
||||
} else {
|
||||
ctx.logger.error("Error during memory retrieval for generation", {
|
||||
error:
|
||||
memoryError instanceof Error
|
||||
? memoryError.message
|
||||
: "Unknown error",
|
||||
})
|
||||
throw memoryError
|
||||
}
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
const result = await model.doGenerate(transformedParams as any)
|
||||
try {
|
||||
// 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" &&
|
||||
ctx.customId &&
|
||||
userMessage &&
|
||||
userMessage.trim()
|
||||
) {
|
||||
const assistantResponseText = extractAssistantResponseText(
|
||||
result.content as unknown[],
|
||||
)
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.customId,
|
||||
assistantResponseText,
|
||||
params,
|
||||
ctx.logger,
|
||||
ctx.apiKey,
|
||||
ctx.normalizedBaseUrl,
|
||||
)
|
||||
const userMessage = getLastUserMessage(params)
|
||||
if (
|
||||
ctx.addMemory === "always" &&
|
||||
userMessage &&
|
||||
userMessage.trim()
|
||||
) {
|
||||
const assistantResponseText = extractAssistantResponseText(
|
||||
result.content as unknown[],
|
||||
)
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.customId,
|
||||
assistantResponseText,
|
||||
params,
|
||||
ctx.logger,
|
||||
ctx.apiKey,
|
||||
ctx.normalizedBaseUrl,
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error generating response", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error generating response", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
if (prop === "doStream") {
|
||||
return async (params: LanguageModelCallOptions) => {
|
||||
let generatedText = ""
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
wrappedModel.doStream = async (
|
||||
params: LanguageModelCallOptions,
|
||||
): Promise<any> => {
|
||||
let generatedText = ""
|
||||
|
||||
try {
|
||||
const transformedParams = await transformParamsWithMemory(params, ctx)
|
||||
|
||||
const { stream, ...rest } = await model.doStream(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
transformedParams as any,
|
||||
)
|
||||
|
||||
const transformStream = new TransformStream<
|
||||
LanguageModelStreamPart,
|
||||
LanguageModelStreamPart
|
||||
>({
|
||||
transform(chunk, controller) {
|
||||
if (chunk.type === "text-delta") {
|
||||
generatedText += chunk.delta
|
||||
let modelParams: LanguageModelCallOptions = params
|
||||
try {
|
||||
modelParams = await transformParamsWithMemory(params, ctx)
|
||||
} catch (memoryError) {
|
||||
if (skipMemoryOnError) {
|
||||
ctx.logger.warn(
|
||||
"Supermemory retrieval failed; continuing without injected memories",
|
||||
{
|
||||
error:
|
||||
memoryError instanceof Error
|
||||
? memoryError.message
|
||||
: "Unknown error",
|
||||
},
|
||||
)
|
||||
modelParams = params
|
||||
} else {
|
||||
ctx.logger.error("Error during memory retrieval for stream", {
|
||||
error:
|
||||
memoryError instanceof Error
|
||||
? memoryError.message
|
||||
: "Unknown error",
|
||||
})
|
||||
throw memoryError
|
||||
}
|
||||
}
|
||||
controller.enqueue(chunk)
|
||||
},
|
||||
flush: async () => {
|
||||
const userMessage = getLastUserMessage(params)
|
||||
if (
|
||||
ctx.addMemory === "always" &&
|
||||
ctx.customId &&
|
||||
userMessage &&
|
||||
userMessage.trim()
|
||||
) {
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.customId,
|
||||
generatedText,
|
||||
params,
|
||||
ctx.logger,
|
||||
ctx.apiKey,
|
||||
ctx.normalizedBaseUrl,
|
||||
|
||||
try {
|
||||
const { stream, ...rest } = await target.doStream(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
modelParams as any,
|
||||
)
|
||||
|
||||
const transformStream = new TransformStream<
|
||||
LanguageModelStreamPart,
|
||||
LanguageModelStreamPart
|
||||
>({
|
||||
transform(chunk, controller) {
|
||||
if (chunk.type === "text-delta") {
|
||||
generatedText += chunk.delta
|
||||
}
|
||||
controller.enqueue(chunk)
|
||||
},
|
||||
flush: async () => {
|
||||
const userMessage = getLastUserMessage(params)
|
||||
if (
|
||||
ctx.addMemory === "always" &&
|
||||
userMessage &&
|
||||
userMessage.trim()
|
||||
) {
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.customId,
|
||||
generatedText,
|
||||
params,
|
||||
ctx.logger,
|
||||
ctx.apiKey,
|
||||
ctx.normalizedBaseUrl,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
stream: stream.pipeThrough(transformStream),
|
||||
...rest,
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error streaming response", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
stream: stream.pipeThrough(transformStream),
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error streaming response", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return wrappedModel
|
||||
return Reflect.get(target, prop, receiver)
|
||||
},
|
||||
}) as T
|
||||
}
|
||||
|
||||
export {
|
||||
wrapVercelLanguageModel as withSupermemory,
|
||||
type WithSupermemoryConfig,
|
||||
type WrapVercelLanguageModelOptions as WithSupermemoryOptions,
|
||||
type PromptTemplate,
|
||||
type MemoryPromptData,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,10 +119,16 @@ export async function realClaudeMemoryExample() {
|
|||
const toolResults = []
|
||||
|
||||
if (responseData.content) {
|
||||
const memoryToolCalls = responseData.content.filter(
|
||||
(block: any): block is { type: 'tool_use'; id: string; name: 'memory'; input: { command: MemoryCommand; path: string } } =>
|
||||
block.type === "tool_use" && block.name === "memory",
|
||||
)
|
||||
const memoryToolCalls = responseData.content.filter(
|
||||
(
|
||||
block: any,
|
||||
): block is {
|
||||
type: "tool_use"
|
||||
id: string
|
||||
name: "memory"
|
||||
input: { command: MemoryCommand; path: string }
|
||||
} => block.type === "tool_use" && block.name === "memory",
|
||||
)
|
||||
|
||||
const results = await Promise.all(
|
||||
memoryToolCalls.map((block: any) => {
|
||||
|
|
@ -196,10 +202,16 @@ export async function processClaudeResponse(
|
|||
const toolResults = []
|
||||
|
||||
if (claudeResponseData.content) {
|
||||
const memoryToolCalls = claudeResponseData.content.filter(
|
||||
(block: any): block is { type: 'tool_use'; id: string; name: 'memory'; input: { command: MemoryCommand; path: string } } =>
|
||||
block.type === "tool_use" && block.name === "memory",
|
||||
)
|
||||
const memoryToolCalls = claudeResponseData.content.filter(
|
||||
(
|
||||
block: any,
|
||||
): block is {
|
||||
type: "tool_use"
|
||||
id: string
|
||||
name: "memory"
|
||||
input: { command: MemoryCommand; path: string }
|
||||
} => block.type === "tool_use" && block.name === "memory",
|
||||
)
|
||||
|
||||
const results = await Promise.all(
|
||||
memoryToolCalls.map((block: any) =>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue