@tanstack/ai is a type-safe, provider-agnostic AI SDK for streaming chat, tool calling, and agents. Spectron adds long-term memory to a chat() call: recall relevant context before a generation, then store the exchange afterwards. There is no dedicated adapter. The JavaScript SDK (@surrealdb/spectron) runs in any server handler that calls chat().
This is an integration guide. There is no first-party TanStack package; the code below wires the Spectron SDK into a @tanstack/ai handler, and you can adapt it to your server shape.
Installation
npm install @surrealdb/spectron @tanstack/ai @tanstack/ai-openaiSpectron holds an API key, so construct the client only on the server, never in a component or loader that ships to the browser.
A memory-aware server handler
Recall context, prepend it to the system prompt, generate, then store the turn:
// src/routes/api/chat.ts
import { Spectron } from "@surrealdb/spectron";
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const spectron = new Spectron({
endpoint: process.env.SPECTRON_ENDPOINT!,
context: "acme-prod",
apiKey: process.env.SPECTRON_API_KEY!,
});
export async function POST(request: Request) {
const { userId, message } = await request.json();
const scope = [`org/acme/user/${userId}`];
// 1. Recall relevant memory as a ready-to-inject context block.
const memory = await spectron.context(message, { scope, k: 8 });
// 2. Generate with the context block in the system prompt.
const stream = chat({
adapter: openaiText("gpt-4o"),
messages: [
{ role: "system", content: `You are a helpful assistant.\n\n## Memory\n${memory}` },
{ role: "user", content: message },
],
});
// 3. Store the exchange so it is available next time.
// (streamToText collects the reply; stream once to the client instead
// if you prefer, and store from a tee.)
return toServerSentEventsResponse(stream);
}To store the turn, collect the reply with streamToText before returning it:
import { chat, streamToText } from "@tanstack/ai";
const text = await streamToText(chat({
adapter: openaiText("gpt-4o"),
messages: [
{ role: "system", content: `You are a helpful assistant.\n\n## Memory\n${memory}` },
{ role: "user", content: message },
],
}));
await spectron.rememberMany(
[
{ role: "user", content: message },
{ role: "assistant", content: text },
],
{ scope },
);Recall as a tool
To let the model decide when to reach for memory, expose recall as a @tanstack/ai tool. Build it with toolDefinition(...).server(...) and pass it to chat:
import { chat, toolDefinition } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
const recall = toolDefinition({
name: "recall",
description: "Search long-term memory for context relevant to a query.",
inputSchema: z.object({ query: z.string() }),
outputSchema: z.object({ context: z.string() }),
}).server(async ({ query }) => {
const context = await spectron.context(query, { scope, k: 8 });
return { context };
});
const stream = chat({
adapter: openaiText("gpt-4o"),
messages: [{ role: "user", content: message }],
tools: [recall],
});The agent loop invokes the tool automatically when the model asks for it.
Scope per user or session
Pass a scope on every call to isolate memory. A scope is a slash path or an array of paths, for example ["org/acme/user/alice"] for one user, or a session-specific path such as ["org/acme/session/abc123"]. Register paths with spectron scopes create before first use.
Next steps
JavaScript SDK: the full client surface
REST API: if you would rather call Spectron over HTTP directly