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

Update the playground to display the current deduplicated <supermemory>
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 <noreply@anthropic.com>
This commit is contained in:
Dhravya Shah 2026-08-18 08:14:07 -07:00
parent 42f308b224
commit ed15364eb3
4 changed files with 105 additions and 12 deletions

View file

@ -3,3 +3,4 @@
.env.local
node_modules
python/.venv
*.tsbuildinfo

View file

@ -48,6 +48,8 @@ export default function AgentPlaygroundPage() {
)
const keysReady =
supermemoryApiKey.trim().length > 0 && openaiApiKey.trim().length > 0
const chatReady =
keysReady || (hasSupermemoryKey && hasOpenAiKey)
const [sdkId, setSdkId] = useState("ts-ai-sdk-middleware")
const [containerTag, setContainerTag] = useState("sdk-playground")
@ -99,7 +101,7 @@ export default function AgentPlaygroundPage() {
}, [refreshMeta])
const send = async () => {
if (!input.trim() || loading || !selectedSdk?.available || !keysReady) return
if (!input.trim() || loading || !selectedSdk?.available || !chatReady) return
const userMessage: UserOrAssistantMessage = {
kind: "user",
@ -484,9 +486,9 @@ export default function AgentPlaygroundPage() {
send()
}
}}
disabled={loading || !input.trim() || !selectedSdk?.available || !keysReady}
disabled={loading || !selectedSdk?.available || !chatReady}
placeholder={
keysReady
chatReady
? "Message the agent…"
: "Enter API keys above to chat…"
}
@ -495,7 +497,12 @@ export default function AgentPlaygroundPage() {
<button
type="button"
onClick={send}
disabled={loading || !input.trim() || !selectedSdk?.available || !keysReady}
disabled={
loading ||
!input.trim() ||
!selectedSdk?.available ||
!chatReady
}
className="rounded-lg bg-emerald-600 px-5 py-3 text-sm font-medium text-white hover:bg-emerald-500 disabled:opacity-40"
>
Send

View file

@ -3,6 +3,7 @@ import {
type MiddlewareRuntimeConfig,
normalizeMiddlewareConfig,
} from "./middleware-config"
import { dedupeProfileForMode } from "./memory-dedupe"
export interface MemoryDebugEntry {
type: "profile_fetch" | "context_preview" | "conversation_saved" | "manual_profile"
@ -143,11 +144,14 @@ export async function fetchContainerContext(
| { static?: unknown[]; dynamic?: unknown[] }
| undefined
const profile = {
static: profileRaw?.static ?? [],
dynamic: profileRaw?.dynamic ?? [],
searchResults: normalizeSearchResults(profileResponse.searchResults),
}
const profile = dedupeProfileForMode(
query ? "full" : "profile",
{
static: profileRaw?.static ?? [],
dynamic: profileRaw?.dynamic ?? [],
searchResults: normalizeSearchResults(profileResponse.searchResults),
},
)
const docsResponse = await client.post<{
documents?: unknown[]
@ -204,7 +208,8 @@ export async function buildMiddlewareMemoryDebug(
query,
supermemoryApiKey,
)
const summary = summarizeProfile(context.profile)
const dedupedProfile = dedupeProfileForMode(memoryMode, context.profile)
const summary = summarizeProfile(dedupedProfile)
const trace: MemoryDebugEntry[] = [
{
@ -229,8 +234,14 @@ export async function buildMiddlewareMemoryDebug(
},
{
type: "context_preview",
label: "Context injected into prompt",
preview: buildContextPreview(context.profile, memoryMode, query),
label: "Current SDK memory block (replaces the prior block)",
preview: buildContextPreview(dedupedProfile, memoryMode, query),
detail: {
totalFacts:
summary.staticCount +
summary.dynamicCount +
summary.searchResultCount,
},
},
{
type: "conversation_saved",

View file

@ -0,0 +1,74 @@
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
.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<string, unknown>
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 JSON.stringify(item)
}
/**
* 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<string>()
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 (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 (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 (seen.has(key)) continue
seen.add(key)
searchOut.push(item)
}
return {
static: staticOut,
dynamic: dynamicOut,
searchResults: mode === "profile" ? [] : searchOut,
}
}