From 0fe2dabceef4af689cb7d70d9d313c9bd6b0fb93 Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Tue, 18 Aug 2026 08:14:07 -0700 Subject: [PATCH 1/2] feat(sdk-playground): reflect SDK-owned memory block in debug view Update the playground to display the current deduplicated replacement block produced by the SDK middleware instead of a browser-side seen-facts delta. Add memory-dedupe helper and ignore local tsbuildinfo. Co-Authored-By: Claude Opus 4.8 --- apps/sdk-playground/src/lib/context-api.ts | 25 ++++--- apps/sdk-playground/src/lib/memory-dedupe.ts | 76 ++++++++++++++++++++ 2 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 apps/sdk-playground/src/lib/memory-dedupe.ts diff --git a/apps/sdk-playground/src/lib/context-api.ts b/apps/sdk-playground/src/lib/context-api.ts index 88cf1a50a..39facb89e 100644 --- a/apps/sdk-playground/src/lib/context-api.ts +++ b/apps/sdk-playground/src/lib/context-api.ts @@ -3,6 +3,7 @@ import { type MiddlewareRuntimeConfig, normalizeMiddlewareConfig, } from "./middleware-config" +import { dedupeProfileForMode } from "./memory-dedupe" export interface MemoryDebugEntry { type: @@ -91,17 +92,6 @@ function summarizeProfile(profile: ContainerContext["profile"]) { } } -function selectProfileForMode( - profile: ContainerContext["profile"], - mode: "profile" | "query" | "full", -): ContainerContext["profile"] { - return { - static: mode === "query" ? [] : profile.static, - dynamic: mode === "query" ? [] : profile.dynamic, - searchResults: mode === "profile" ? [] : profile.searchResults, - } -} - function buildContextPreview( profile: ContainerContext["profile"], mode: "profile" | "query" | "full", @@ -182,7 +172,10 @@ export async function fetchContainerContext( if (!apiKey) throw new Error("Supermemory API key is required") const client = getSupermemoryClient(apiKey) - const profile = await fetchProfileContext(client, containerTag, query) + const profile = dedupeProfileForMode( + query ? "full" : "profile", + await fetchProfileContext(client, containerTag, query), + ) const docsResponse = await client.post<{ documents?: unknown[] @@ -246,7 +239,7 @@ export async function buildMiddlewareMemoryDebug( query, signal, ) - const selectedProfile = selectProfileForMode(profile, memoryMode) + const selectedProfile = dedupeProfileForMode(memoryMode, profile) const summary = summarizeProfile(selectedProfile) return [ @@ -276,6 +269,12 @@ export async function buildMiddlewareMemoryDebug( type: "context_preview", label: "Reconstructed context preview (not middleware capture)", preview: buildContextPreview(selectedProfile, memoryMode, query), + detail: { + totalFacts: + summary.staticCount + + summary.dynamicCount + + summary.searchResultCount, + }, }, config.addMemory === "always" ? { diff --git a/apps/sdk-playground/src/lib/memory-dedupe.ts b/apps/sdk-playground/src/lib/memory-dedupe.ts new file mode 100644 index 000000000..a4ac7d8fe --- /dev/null +++ b/apps/sdk-playground/src/lib/memory-dedupe.ts @@ -0,0 +1,76 @@ +import type { ContainerContext } from "./context-api" + +type ProfileSlice = ContainerContext["profile"] + +/** Normalize a fact for exact comparison within retrieved context. */ +export function normalizeFactKey(text: string): string { + return text + .trim() + .replace(/^\[recent\]\s*/i, "") + .replace(/^\[\d{4}-\d{2}-\d{2}\]\s*/, "") + .trim() + .replace(/\s+/g, " ") + .toLowerCase() +} + +function memoryText(item: unknown): string { + if (typeof item === "string") return item + if (item && typeof item === "object") { + const record = item as Record + if (typeof record.memory === "string") return record.memory + if (typeof record.content === "string") return record.content + if (typeof record.chunk === "string") return record.chunk + } + return "" +} + +/** + * Deduplicate static → dynamic → search (same priority as @supermemory/tools middleware). + */ +export function dedupeProfileForMode( + mode: "profile" | "query" | "full", + profile: ProfileSlice, +): ProfileSlice { + const injectsProfile = mode !== "query" + const staticItems = injectsProfile ? profile.static : [] + const dynamicItems = injectsProfile ? profile.dynamic : [] + const searchItems = profile.searchResults + + const seen = new Set() + const staticOut: unknown[] = [] + const dynamicOut: unknown[] = [] + const searchOut: unknown[] = [] + + for (const item of staticItems) { + const text = memoryText(item).trim() + if (!text) continue + const key = normalizeFactKey(text) + if (!key || seen.has(key)) continue + seen.add(key) + staticOut.push(item) + } + + for (const item of dynamicItems) { + const text = memoryText(item).trim() + if (!text) continue + const key = normalizeFactKey(text) + if (!key || seen.has(key)) continue + seen.add(key) + dynamicOut.push(item) + } + + for (const item of searchItems) { + const text = memoryText(item).trim() + if (!text) continue + const key = normalizeFactKey(text) + if (!key || seen.has(key)) continue + seen.add(key) + searchOut.push(item) + } + + return { + static: staticOut, + dynamic: dynamicOut, + searchResults: mode === "profile" ? [] : searchOut, + } +} From 5d1f557b6709b5c170d02e8125d9743ba4499ec4 Mon Sep 17 00:00:00 2001 From: ved015 <122012786+ved015@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:38:49 +0530 Subject: [PATCH 2/2] fix(sdk-playground): mirror SDK memory reconstruction --- apps/sdk-playground/python/server.py | 112 ++++++++++++----- apps/sdk-playground/src/lib/chat-handlers.ts | 3 +- apps/sdk-playground/src/lib/context-api.ts | 86 +++++-------- apps/sdk-playground/src/lib/memory-dedupe.ts | 123 +++++++++---------- 4 files changed, 175 insertions(+), 149 deletions(-) diff --git a/apps/sdk-playground/python/server.py b/apps/sdk-playground/python/server.py index caf81378f..842e3d7e7 100644 --- a/apps/sdk-playground/python/server.py +++ b/apps/sdk-playground/python/server.py @@ -343,6 +343,8 @@ async def fetch_profile_context( container_tag: str, sm_key: str, query: Optional[str] = None, + *, + include: Optional[list[str]] = None, ) -> dict[str, list[Any]]: from supermemory import AsyncSupermemory @@ -351,10 +353,12 @@ async def fetch_profile_context( base_url=supermemory_base_url(), timeout=HTTP_TIMEOUT_SECONDS, ) - profile_response = await client.profile( - container_tag=container_tag, - **({"q": query} if query else {}), - ) + request: dict[str, Any] = {"container_tag": container_tag} + if query: + request["q"] = query + if include is not None: + request["include"] = include + profile_response = await client.profile(**request) return extract_profile_context(profile_response) @@ -530,6 +534,49 @@ async def fetch_container_context( } +def reconstruct_python_sdk_memory_block( + memory_mode: str, + profile: dict[str, Any], +) -> tuple[dict[str, list[str]], str]: + from supermemory_openai import convert_profile_to_markdown, deduplicate_memories + from supermemory_openai.utils import wrap_memory_context + + deduplicated = deduplicate_memories( + static=profile.get("static", []) if memory_mode != "query" else [], + dynamic=profile.get("dynamic", []) if memory_mode != "query" else [], + search_results=profile.get("searchResults", []), + ) + visible_profile = { + "static": deduplicated.static, + "dynamic": deduplicated.dynamic, + "searchResults": ( + [] if memory_mode == "profile" else deduplicated.search_results + ), + } + + profile_data = "" + if memory_mode != "query": + profile_data = convert_profile_to_markdown( + { + "profile": { + "static": visible_profile["static"], + "dynamic": visible_profile["dynamic"], + }, + "searchResults": {"results": []}, + } + ) + + search_results_memories = "" + if memory_mode != "profile" and visible_profile["searchResults"]: + search_results_memories = ( + "Search results for user's recent message: \n" + + "\n".join(f"- {memory}" for memory in visible_profile["searchResults"]) + ) + + memories = f"{profile_data}\n{search_results_memories}".strip() + return visible_profile, wrap_memory_context(memories) + + def build_middleware_memory_debug( container_tag: str, conversation_id: str, @@ -549,39 +596,20 @@ def build_middleware_memory_debug( } ) else: - profile = context["profile"] - preview_lines = [ - f"[memory mode: {memory_mode}]", - "[post-response snapshot; not the exact middleware prompt]", - ] - if context.get("query"): - preview_lines.append(f"[query: {context['query']}]") - - selected_sections: list[tuple[str, list[Any]]] = [] - if memory_mode in ("profile", "full"): - selected_sections.extend( - ( - ("Static", profile.get("static", [])), - ("Dynamic", profile.get("dynamic", [])), - ) - ) - if memory_mode in ("query", "full"): - selected_sections.append( - ("Search results", profile.get("searchResults", [])) - ) - - for label, items in selected_sections: - if items: - preview_lines.append(f"{label}:") - for item in items[:8]: - preview_lines.append(f"- {display_context_item(item)}") + raw_profile = context["profile"] + profile, memory_block = reconstruct_python_sdk_memory_block( + memory_mode, + raw_profile, + ) debug.extend( ( { "type": "profile_fetch", - "label": "Post-response profile snapshot", + "label": "Post-response context reconstruction", "detail": { + "authoritativeMiddlewareCapture": False, + "timing": "after model response", "endpoint": "POST /v4/profile", "containerTag": container_tag, "customId": conversation_id, @@ -594,8 +622,19 @@ def build_middleware_memory_debug( }, { "type": "context_preview", - "label": "Post-response context preview", - "preview": "\n".join(preview_lines), + "label": ( + "Reconstructed SDK-owned memory block " + "(not middleware capture)" + ), + "preview": memory_block, + "detail": { + "totalFacts": ( + len(profile.get("static", [])) + + len(profile.get("dynamic", [])) + + len(profile.get("searchResults", [])) + ), + "fullLength": len(memory_block), + }, }, ) ) @@ -632,7 +671,12 @@ async def fetch_context_for_debug( ) -> tuple[Optional[dict[str, Any]], Optional[str]]: try: async with asyncio.timeout(CONTEXT_DEBUG_TIMEOUT_SECONDS): - profile = await fetch_profile_context(container_tag, sm_key, query) + profile = await fetch_profile_context( + container_tag, + sm_key, + query, + include=["static", "dynamic"], + ) return ( { "containerTag": container_tag, diff --git a/apps/sdk-playground/src/lib/chat-handlers.ts b/apps/sdk-playground/src/lib/chat-handlers.ts index 824d3d88a..60fffdd19 100644 --- a/apps/sdk-playground/src/lib/chat-handlers.ts +++ b/apps/sdk-playground/src/lib/chat-handlers.ts @@ -424,10 +424,11 @@ export async function runTypeScriptChat( middlewareConfig, request.sdkId === "ts-ai-sdk-middleware" ? { + flavor: "ai-sdk", includeToolCalls: middlewareConfig.includeToolCalls, skipMemoryOnError: middlewareConfig.skipMemoryOnError, } - : undefined, + : { flavor: "openai" }, keys.supermemoryApiKey, signal, ), diff --git a/apps/sdk-playground/src/lib/context-api.ts b/apps/sdk-playground/src/lib/context-api.ts index 39facb89e..9c201a113 100644 --- a/apps/sdk-playground/src/lib/context-api.ts +++ b/apps/sdk-playground/src/lib/context-api.ts @@ -3,7 +3,11 @@ import { type MiddlewareRuntimeConfig, normalizeMiddlewareConfig, } from "./middleware-config" -import { dedupeProfileForMode } from "./memory-dedupe" +import { + type MemoryMode, + type MiddlewareFlavor, + reconstructSdkMemoryBlock, +} from "./memory-dedupe" export interface MemoryDebugEntry { type: @@ -92,34 +96,6 @@ function summarizeProfile(profile: ContainerContext["profile"]) { } } -function buildContextPreview( - profile: ContainerContext["profile"], - mode: "profile" | "query" | "full", - query?: string, -): string { - const lines: string[] = [`[memory mode: ${mode}]`] - if (query) lines.push(`[query: ${query}]`) - if (mode !== "query" && profile.static.length) { - lines.push("Static:") - for (const item of profile.static.slice(0, 8)) { - lines.push(`- ${memoryText(item)}`) - } - } - if (mode !== "query" && profile.dynamic.length) { - lines.push("Dynamic:") - for (const item of profile.dynamic.slice(0, 8)) { - lines.push(`- ${memoryText(item)}`) - } - } - if (mode !== "profile" && profile.searchResults.length) { - lines.push("Search results:") - for (const item of profile.searchResults.slice(0, 8)) { - lines.push(`- ${memoryText(item)}`) - } - } - return lines.join("\n") -} - function normalizeSearchResults(searchResults: unknown): unknown[] { if (!searchResults) return [] if (Array.isArray(searchResults)) return searchResults @@ -144,16 +120,18 @@ async function fetchProfileContext( query?: string, signal?: AbortSignal, ): Promise { - const profileResponse = await client.profile( - { + const profileResponse = await client.post<{ + profile?: { static?: unknown[]; dynamic?: unknown[] } + searchResults?: unknown + }>("/v4/profile", { + body: { containerTag, + include: ["static", "dynamic"], ...(query ? { q: query } : {}), }, - { signal }, - ) - const profileRaw = profileResponse.profile as - | { static?: unknown[]; dynamic?: unknown[] } - | undefined + ...(signal ? { signal } : {}), + }) + const profileRaw = profileResponse.profile return { static: profileRaw?.static ?? [], @@ -172,10 +150,7 @@ export async function fetchContainerContext( if (!apiKey) throw new Error("Supermemory API key is required") const client = getSupermemoryClient(apiKey) - const profile = dedupeProfileForMode( - query ? "full" : "profile", - await fetchProfileContext(client, containerTag, query), - ) + const profile = await fetchProfileContext(client, containerTag, query) const docsResponse = await client.post<{ documents?: unknown[] @@ -216,10 +191,11 @@ export async function fetchContainerContext( export async function buildMiddlewareMemoryDebug( containerTag: string, conversationId: string, - memoryMode: "profile" | "query" | "full", + memoryMode: MemoryMode, lastUserMessage: string, - middlewareConfig?: Partial, - aiSdkExtras?: { + middlewareConfig: Partial | undefined, + sdk: { + flavor: MiddlewareFlavor includeToolCalls?: boolean skipMemoryOnError?: boolean }, @@ -239,7 +215,12 @@ export async function buildMiddlewareMemoryDebug( query, signal, ) - const selectedProfile = dedupeProfileForMode(memoryMode, profile) + const reconstructed = reconstructSdkMemoryBlock( + memoryMode, + profile, + sdk.flavor, + ) + const selectedProfile = reconstructed.profile const summary = summarizeProfile(selectedProfile) return [ @@ -255,11 +236,11 @@ export async function buildMiddlewareMemoryDebug( memoryMode, addMemory: config.addMemory, verbose: config.verbose, - ...(aiSdkExtras?.includeToolCalls !== undefined - ? { includeToolCalls: aiSdkExtras.includeToolCalls } + ...(sdk.includeToolCalls !== undefined + ? { includeToolCalls: sdk.includeToolCalls } : {}), - ...(aiSdkExtras?.skipMemoryOnError !== undefined - ? { skipMemoryOnError: aiSdkExtras.skipMemoryOnError } + ...(sdk.skipMemoryOnError !== undefined + ? { skipMemoryOnError: sdk.skipMemoryOnError } : {}), query: query ?? null, ...summary, @@ -267,13 +248,14 @@ export async function buildMiddlewareMemoryDebug( }, { type: "context_preview", - label: "Reconstructed context preview (not middleware capture)", - preview: buildContextPreview(selectedProfile, memoryMode, query), + label: "Reconstructed SDK-owned memory block (not middleware capture)", + preview: reconstructed.block, detail: { totalFacts: summary.staticCount + summary.dynamicCount + summary.searchResultCount, + fullLength: reconstructed.block.length, }, }, config.addMemory === "always" @@ -286,8 +268,8 @@ export async function buildMiddlewareMemoryDebug( customId: conversationId, addMemory: config.addMemory, verbose: config.verbose, - ...(aiSdkExtras?.includeToolCalls !== undefined - ? { includeToolCalls: aiSdkExtras.includeToolCalls } + ...(sdk.includeToolCalls !== undefined + ? { includeToolCalls: sdk.includeToolCalls } : {}), }, } diff --git a/apps/sdk-playground/src/lib/memory-dedupe.ts b/apps/sdk-playground/src/lib/memory-dedupe.ts index a4ac7d8fe..0a641291f 100644 --- a/apps/sdk-playground/src/lib/memory-dedupe.ts +++ b/apps/sdk-playground/src/lib/memory-dedupe.ts @@ -1,76 +1,75 @@ -import type { ContainerContext } from "./context-api" +import { + deduplicateMemoriesForMode, + type ProfileWithMemories, +} from "../../../../packages/tools/src/tools-shared" +import { wrapMemoryContext } from "../../../../packages/tools/src/shared/memory-context" +import { + convertProfileToMarkdown, + defaultPromptTemplate, +} from "../../../../packages/tools/src/shared/prompt-builder" -type ProfileSlice = ContainerContext["profile"] +export type MemoryMode = "profile" | "query" | "full" +export type MiddlewareFlavor = "ai-sdk" | "openai" -/** Normalize a fact for exact comparison within retrieved context. */ -export function normalizeFactKey(text: string): string { - return text - .trim() - .replace(/^\[recent\]\s*/i, "") - .replace(/^\[\d{4}-\d{2}-\d{2}\]\s*/, "") - .trim() - .replace(/\s+/g, " ") - .toLowerCase() +export interface MemoryProfileSlice { + static: unknown[] + dynamic: unknown[] + searchResults: unknown[] } -function memoryText(item: unknown): string { - if (typeof item === "string") return item - if (item && typeof item === "object") { - const record = item as Record - if (typeof record.memory === "string") return record.memory - if (typeof record.content === "string") return record.content - if (typeof record.chunk === "string") return record.chunk +export interface ReconstructedMemoryBlock { + profile: { + static: string[] + dynamic: string[] + searchResults: string[] } - return "" + block: string } -/** - * Deduplicate static → dynamic → search (same priority as @supermemory/tools middleware). - */ -export function dedupeProfileForMode( - mode: "profile" | "query" | "full", - profile: ProfileSlice, -): ProfileSlice { - const injectsProfile = mode !== "query" - const staticItems = injectsProfile ? profile.static : [] - const dynamicItems = injectsProfile ? profile.dynamic : [] - const searchItems = profile.searchResults - - const seen = new Set() - const staticOut: unknown[] = [] - const dynamicOut: unknown[] = [] - const searchOut: unknown[] = [] - - for (const item of staticItems) { - const text = memoryText(item).trim() - if (!text) continue - const key = normalizeFactKey(text) - if (!key || seen.has(key)) continue - seen.add(key) - staticOut.push(item) +/** Reconstruct the exact SDK-owned block from a post-response profile snapshot. */ +export function reconstructSdkMemoryBlock( + mode: MemoryMode, + profile: MemoryProfileSlice, + flavor: MiddlewareFlavor, +): ReconstructedMemoryBlock { + const deduplicated = deduplicateMemoriesForMode( + mode, + profile as ProfileWithMemories, + ) + const visibleProfile = { + static: deduplicated.static, + dynamic: deduplicated.dynamic, + searchResults: mode === "profile" ? [] : deduplicated.searchResults, } - for (const item of dynamicItems) { - const text = memoryText(item).trim() - if (!text) continue - const key = normalizeFactKey(text) - if (!key || seen.has(key)) continue - seen.add(key) - dynamicOut.push(item) - } + const userMemories = + mode === "query" + ? "" + : convertProfileToMarkdown({ + profile: { + static: visibleProfile.static, + dynamic: visibleProfile.dynamic, + }, + searchResults: { results: [] }, + }) + const generalSearchMemories = + mode !== "profile" && visibleProfile.searchResults.length > 0 + ? `Search results for user's recent message: \n${visibleProfile.searchResults + .map((memory) => `- ${memory}`) + .join("\n")}` + : "" - for (const item of searchItems) { - const text = memoryText(item).trim() - if (!text) continue - const key = normalizeFactKey(text) - if (!key || seen.has(key)) continue - seen.add(key) - searchOut.push(item) - } + const memories = + flavor === "ai-sdk" + ? defaultPromptTemplate({ + userMemories, + generalSearchMemories, + searchResults: [], + }) + : `${userMemories}\n${generalSearchMemories}`.trim() return { - static: staticOut, - dynamic: dynamicOut, - searchResults: mode === "profile" ? [] : searchOut, + profile: visibleProfile, + block: wrapMemoryContext(memories), } }