mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-29 06:36:43 +00:00
179 lines
6.1 KiB
TypeScript
179 lines
6.1 KiB
TypeScript
// Packs the built package and installs it next to each supported AI SDK major, then typechecks and runs a consumer app.
|
|
import { mkdtemp, rm } from "node:fs/promises"
|
|
import { tmpdir } from "node:os"
|
|
import path from "node:path"
|
|
import { $ } from "bun"
|
|
|
|
const ROOT = path.resolve(import.meta.dir, "../..")
|
|
const FINISHED = { unified: "stop", raw: "stop" }
|
|
const TOOL_CALLS = { unified: "tool-calls", raw: "tool_calls" }
|
|
const USAGE = {
|
|
inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
|
|
outputTokens: { total: 1, text: 1, reasoning: 0 },
|
|
}
|
|
|
|
const VERSIONS = [
|
|
{
|
|
ai: "5",
|
|
mock: "MockLanguageModelV2",
|
|
stop: "stop",
|
|
toolCalls: "tool-calls",
|
|
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
},
|
|
{
|
|
ai: "6",
|
|
mock: "MockLanguageModelV3",
|
|
stop: FINISHED,
|
|
toolCalls: TOOL_CALLS,
|
|
usage: USAGE,
|
|
},
|
|
{
|
|
ai: "7",
|
|
mock: "MockLanguageModelV4",
|
|
stop: FINISHED,
|
|
toolCalls: TOOL_CALLS,
|
|
usage: USAGE,
|
|
},
|
|
]
|
|
|
|
const fixture = (v: (typeof VERSIONS)[number]) => `
|
|
import { generateText, stepCountIs, streamText } from "ai"
|
|
import { ${v.mock} } from "ai/test"
|
|
import { supermemoryTools, withSupermemory } from "@supermemory/tools/ai-sdk"
|
|
|
|
let searchCalls = 0
|
|
const saved: string[] = []
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
async fetch(req) {
|
|
const route = new URL(req.url).pathname
|
|
if (route.endsWith("/document")) saved.push(String((await req.json()).content))
|
|
if (route.endsWith("/profile"))
|
|
return Response.json({ profile: { static: [{ id: "s0", memory: "User prefers dark mode" }], dynamic: [], buckets: {} } })
|
|
if (route.endsWith("/search")) {
|
|
searchCalls++
|
|
return Response.json({ results: [], searchTime: 1 })
|
|
}
|
|
return Response.json({ id: "doc_1", status: "queued" })
|
|
},
|
|
})
|
|
const baseUrl = \`http://127.0.0.1:\${server.port}\`
|
|
|
|
const prompts: string[] = []
|
|
let calls = 0
|
|
const model = new ${v.mock}({
|
|
supportedUrls: { "image/*": [/^https:/] },
|
|
doGenerate: async (options) => {
|
|
prompts.push(JSON.stringify(options.prompt))
|
|
const callTool = calls++ === 0 && (options.tools?.length ?? 0) > 0
|
|
return {
|
|
content: callTool
|
|
? [{ type: "tool-call", toolCallId: "c1", toolName: "searchMemories", input: JSON.stringify({ informationToGet: "theme" }) }]
|
|
: [{ type: "text", text: "ok" }],
|
|
finishReason: callTool ? ${JSON.stringify(v.toolCalls)} : ${JSON.stringify(v.stop)},
|
|
usage: ${JSON.stringify(v.usage)},
|
|
warnings: [],
|
|
} as never
|
|
},
|
|
})
|
|
|
|
const tools = supermemoryTools("sm_test", { baseUrl })
|
|
const withTools = await generateText({ model, tools, prompt: "what theme?", stopWhen: stepCountIs(3) })
|
|
if (withTools.text !== "ok" || searchCalls !== 1) throw new Error(\`tools: text=\${withTools.text} searchCalls=\${searchCalls}\`)
|
|
|
|
const wrapped = withSupermemory(model, { apiKey: "sm_test", namespace: "user_1", id: "conv_1", baseUrl })
|
|
const withMemory = await generateText({ model: wrapped, prompt: "hi" })
|
|
if (withMemory.text !== "ok" || !prompts.at(-1)?.includes("User prefers dark mode"))
|
|
throw new Error("withSupermemory: memory was not injected into the prompt")
|
|
|
|
// Saving is fire-and-forget, so wait for the document request to land.
|
|
const savedAfter = async (count: number) => {
|
|
for (let i = 0; i < 50 && saved.length < count; i++) await Bun.sleep(20)
|
|
return saved[count - 1] ?? ""
|
|
}
|
|
await savedAfter(1)
|
|
await generateText({ model: wrapped, messages: [{ role: "user", content: [{ type: "image", image: new URL("https://example.com/cat.png") }] }] })
|
|
if (!(await savedAfter(saved.length + 1)).includes("[image")) throw new Error("withSupermemory: image-only turn was not saved")
|
|
await generateText({ model: wrapped, messages: [{ role: "user", content: [{ type: "text", text: "look" }, { type: "image", image: new Uint8Array([137, 80, 78, 71]), mediaType: "image/png" }] }] })
|
|
if (!(await savedAfter(saved.length + 1)).includes("[image")) throw new Error("withSupermemory: image was dropped from a mixed turn")
|
|
|
|
// Type-only: the wrapped model and tools must also be accepted by streamText.
|
|
const _typecheckStream = () => streamText({ model: wrapped, tools, prompt: "hi" })
|
|
|
|
server.stop(true)
|
|
console.log("ai@${v.ai}: tools and withSupermemory OK")
|
|
`
|
|
|
|
const work = await mkdtemp(path.join(tmpdir(), "sm-tools-compat-"))
|
|
try {
|
|
await $`bun run build`.cwd(ROOT).quiet()
|
|
const packed = await $`npm pack --silent --pack-destination ${work}`
|
|
.cwd(ROOT)
|
|
.text()
|
|
const tarball = path.join(work, packed.trim().split("\n").at(-1) ?? "")
|
|
|
|
for (const v of VERSIONS) {
|
|
const app = path.join(work, `ai-v${v.ai}`)
|
|
await Bun.write(
|
|
path.join(app, "package.json"),
|
|
JSON.stringify({
|
|
type: "module",
|
|
dependencies: {
|
|
"@supermemory/tools": `file:${tarball}`,
|
|
ai: `^${v.ai}`,
|
|
typescript: "^5.9.2",
|
|
zod: "^4.1.5",
|
|
"@types/bun": "^1.2.21",
|
|
},
|
|
}),
|
|
)
|
|
await Bun.write(
|
|
path.join(app, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: {
|
|
target: "ES2022",
|
|
module: "ESNext",
|
|
moduleResolution: "bundler",
|
|
strict: true,
|
|
noEmit: true,
|
|
skipLibCheck: true,
|
|
types: ["bun"],
|
|
},
|
|
files: ["app.ts"],
|
|
}),
|
|
)
|
|
await Bun.write(path.join(app, "app.ts"), fixture(v))
|
|
await $`npm install --silent --no-audit --no-fund`.cwd(app).quiet()
|
|
const installed = (
|
|
await Bun.file(path.join(app, "node_modules/ai/package.json")).json()
|
|
).version
|
|
const nested = await Bun.file(
|
|
path.join(
|
|
app,
|
|
"node_modules/@supermemory/tools/node_modules/ai/package.json",
|
|
),
|
|
).exists()
|
|
if (nested)
|
|
throw new Error(
|
|
`ai@${v.ai}: a second copy of ai was installed under @supermemory/tools`,
|
|
)
|
|
const aiProvider = (
|
|
await Bun.file(path.join(app, "node_modules/ai/package.json")).json()
|
|
).dependencies["@ai-sdk/provider"]
|
|
const topProvider = (
|
|
await Bun.file(
|
|
path.join(app, "node_modules/@ai-sdk/provider/package.json"),
|
|
).json()
|
|
).version
|
|
if (aiProvider.split(".")[0] !== topProvider.split(".")[0])
|
|
throw new Error(
|
|
`ai@${installed}: @ai-sdk/provider@${topProvider} was installed, but ai uses ${aiProvider}`,
|
|
)
|
|
const tsc = await $`npx tsc -p .`.cwd(app).nothrow().quiet()
|
|
if (tsc.exitCode !== 0)
|
|
throw new Error(`ai@${installed}: typecheck failed\n${tsc.stdout}`)
|
|
await $`bun app.ts`.cwd(app)
|
|
}
|
|
} finally {
|
|
await rm(work, { recursive: true, force: true })
|
|
}
|