feat(sdk-playground): reflect SDK-owned memory block in debug view (#1533)

## Stack Context

Part 3 (top) of a 3-PR stack moving memory deduplication into the SDKs. See `sdk-dedup/tools-ts` for full context.

## What?

Update the SDK playground so its debug view reflects the SDK-owned memory block.

- Displays the current deduplicated `<supermemory>` replacement block produced by the SDK middleware, instead of the old browser-side "seen facts" delta.
- Adds a `memory-dedupe` helper and ignores local `*.tsbuildinfo`.

## Why?

The previous debug cards were misleading — they showed an incremental browser-filtered delta while the middleware actually re-injected the full profile. Now the visualization matches what the SDK really sends.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Playground-only visualization and chat gating changes; no production SDK or API behavior.
>
> **Overview**
> The playground **debug trace** now shows the **deduplicated memory block** the SDK middleware would inject (static → dynamic → search, mode-aware), instead of a misleading browser-side “new facts” delta. A new **`memory-dedupe`** helper mirrors `@supermemory/tools` middleware behavior and is applied when fetching container context and building middleware memory debug entries; the context preview card is relabeled to reflect that each turn **replaces** the prior `<supermemory>` block.
>
> **Chat UX:** messaging is enabled when API keys are configured on the **server** (`hasSupermemoryKey` / `hasOpenAiKey` from `/api/chat`), not only when keys are typed in the panel. The message input stays editable while waiting for text; Send still requires non-empty input.
>
> Also ignores `*.tsbuildinfo` in `.gitignore`.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ed15364eb3. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
Dhravya 2026-09-01 06:10:37 +00:00
parent 03773c4f2e
commit 4ad5f0beb1
No known key found for this signature in database
GPG key ID: 135A27003CF4F6CB
4 changed files with 194 additions and 93 deletions

View file

@ -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,

View file

@ -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,
),

View file

@ -3,6 +3,11 @@ import {
type MiddlewareRuntimeConfig,
normalizeMiddlewareConfig,
} from "./middleware-config"
import {
type MemoryMode,
type MiddlewareFlavor,
reconstructSdkMemoryBlock,
} from "./memory-dedupe"
export interface MemoryDebugEntry {
type:
@ -91,45 +96,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",
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
@ -154,16 +120,18 @@ async function fetchProfileContext(
query?: string,
signal?: AbortSignal,
): Promise<ContainerContext["profile"]> {
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 ?? [],
@ -223,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<MiddlewareRuntimeConfig>,
aiSdkExtras?: {
middlewareConfig: Partial<MiddlewareRuntimeConfig> | undefined,
sdk: {
flavor: MiddlewareFlavor
includeToolCalls?: boolean
skipMemoryOnError?: boolean
},
@ -246,7 +215,12 @@ export async function buildMiddlewareMemoryDebug(
query,
signal,
)
const selectedProfile = selectProfileForMode(profile, memoryMode)
const reconstructed = reconstructSdkMemoryBlock(
memoryMode,
profile,
sdk.flavor,
)
const selectedProfile = reconstructed.profile
const summary = summarizeProfile(selectedProfile)
return [
@ -262,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,
@ -274,8 +248,15 @@ 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"
? {
@ -287,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 }
: {}),
},
}

View file

@ -0,0 +1,75 @@
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"
export type MemoryMode = "profile" | "query" | "full"
export type MiddlewareFlavor = "ai-sdk" | "openai"
export interface MemoryProfileSlice {
static: unknown[]
dynamic: unknown[]
searchResults: unknown[]
}
export interface ReconstructedMemoryBlock {
profile: {
static: string[]
dynamic: string[]
searchResults: string[]
}
block: string
}
/** 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,
}
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")}`
: ""
const memories =
flavor === "ai-sdk"
? defaultPromptTemplate({
userMemories,
generalSearchMemories,
searchResults: [],
})
: `${userMemories}\n${generalSearchMemories}`.trim()
return {
profile: visibleProfile,
block: wrapMemoryContext(memories),
}
}