Memory

Two kinds of memory work today. A session holds one conversation: the turns and the tool calls, kept by the box so the next message lands on the same thread. Retrieval holds your documents, chunked and embedded in the box's own Postgres, so a tool can search them by meaning and the model can answer from them. Studio reads both live: Sessions shows every conversation and RAG shows every collection. A third kind, long-term memory for facts that outlive a conversation, is designed and not shipped.

One conversation, three turns. The third question ("that order") makes sense because the box remembered the first two.

What an agent remembers

Kind What it holds, and where
Session The turns of one conversation, the tool state between them and any tool held for a decision. The box keeps it in its own Postgres, in a schema of its own (stackbone_checkpoints), and Studio lists it under Sessions. You open one by sending a session key with the request; the agent needs no code. See Keep the conversation.
Retrieval (RAG) Your documents, split into chunks and stored with an embedding, grouped in collections in the box's Postgres (stackbone_platform.rag_*, pgvector), with the uploaded originals in its object store. A tool calls stackbone.rag.retrieve() and hands the hits to the model; you fill a collection from Studio, from code or from the CLI. See Ground answers.
Long-term memory Planned, not shipped. Facts about a user, a session or the agent that should survive the conversation: a plan, a preferred language. The stackbone.memory surface exists so you can type against it, and every call returns not_implemented. See Keep facts across sessions.

Your own tables are none of these. stackbone.database is where your business data goes (orders, tickets, users), typed by you and migrated by you. Memory is what the runtime keeps about the conversation; retrieval is what it keeps about your documents. The last section compares the three stores.

Keep the conversation

A chat turn is stateless unless you say otherwise. Over the OpenAI or Anthropic wire you replay the whole messages[] array on every call, and the box forgets the turn as soon as it answers. Send an x-stackbone-session header with any stable string you choose and the box keeps the conversation on its side: the next call with the same value lands on the same thread, with the earlier turns, the tool results and any tool it is holding for a person.

# First turn: the box opens the session and answers.
curl http://127.0.0.1:4242/openai/v1/chat/completions \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer local-dev' \
  -H 'x-stackbone-session: customer-4711' \
  -d '{"model":"support","messages":[{"role":"user","content":"Where is order A-1001?"}]}'

# Second turn, same key: no need to replay the first message.
curl http://127.0.0.1:4242/openai/v1/chat/completions \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer local-dev' \
  -H 'x-stackbone-session: customer-4711' \
  -d '{"model":"support","messages":[{"role":"user","content":"Can I still return it?"}]}'

The AG-UI wire needs no header: its threadId is the session key, and every non-empty threadId is durable. The Playground in Studio is an AG-UI client, so every conversation you start there is a session, and New conversation starts another one.

Behind the header, the box does two things. It writes the graph's checkpoint after every turn into its own Postgres, in a schema called stackbone_checkpoints, which is what lets a session survive a restart. And it keeps a sessions row that Studio lists, with each turn linked to its run. The box derives the session id from your key and the agent name, so the same key always names the same session, and Studio can rebuild a conversation on any machine from the key alone.

Every conversation the box keeps, for every agent, newest first.

Open one and Studio prints the transcript: each turn with the user message, the agent reply, its tokens and duration, and a Run detail link to the tool calls behind it. Continue in Playground reopens the same thread and sends the next message on it, even after the session completed. From a terminal, stackbone runs list reads the same turns.

Four rules:

  • A held tool needs a session. A tool listed under interruptOn parks the turn on the checkpoint until a person decides. Without a session key there is nowhere to park, so the turn cannot pause. While a decision is pending, a new message on that session answers 409.
  • Grouping without state. x-stackbone-conversation groups turns into one session row in Studio but keeps the wire stateless: you still replay messages[], and nothing can pause. Send one header or the other, not both.
  • The key never comes back. No wire returns the session id. Keep your key (a customer id, a ticket number) and reuse it.
  • Nothing expires. Sessions and their checkpoints stay until you delete them from the box's Postgres. There is no retention window and no delete from Studio.

The wire contract, both headers and the AG-UI shape are on the agent protocol page.

Ground answers in your documents

Retrieval is a tool like any other. It embeds the question, finds the closest chunks in a collection and hands them to the model, which answers from them. stackbone.rag.retrieve() searches one collection, the one you name in namespace (it defaults to default), and every hit carries its content and a score between 0 and 1.

deep-agents/support/index.ts
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import { defineDeepAgent } from '@stackbone/sdk/deep';
import { stackbone } from '@stackbone/sdk';

const searchDocs = tool(
  async ({ query }: { query: string }) => {
    const result = await stackbone.rag.retrieve({
      text: query,
      topK: 3,
      namespace: 'help-center', // the collection to search
    });
    if (result.error) return `search_failed: ${result.error.code}`;
    if (result.data.length === 0) return 'No matching article in the help center.';
    return result.data.map((hit) => `[${hit.score.toFixed(2)}] ${hit.content}`).join('\n\n');
  },
  {
    name: 'search_docs',
    description: 'Search the help center before answering a question about policies.',
    schema: z.object({ query: z.string() }),
  },
);

export default defineDeepAgent({
  name: 'support',
  model: 'openai/gpt-4o-mini',
  tools: [searchDocs],
});

The instruction is a prompt in the catalogue, owned by this agent and keyed by its name, written and published in Studio:

You are the support agent for Acme.
Use search_docs for any question about policies and answer from what it returns.

A grounded turn in the Playground: the query the model chose, the chunks that came back, and the answer built from them.

You can fill a collection three ways. They all land in the same tables, so the agent finds a hand-uploaded document the same way it finds one your code ingested:

From How Good for
Studio RAG screen: New collection, then Upload document (.txt, .md or .pdf, up to 25 MB). The upload returns at once; the box parses, chunks, embeds and stores in the background, and the document's row shows the status of that job with a link to its run. An operator feeding a knowledge base with no code.
Code stackbone.rag.ingest({ id, collection, chunks }) for a one-off, inside a 'use step' for anything slow, or ingestDocuments() from @stackbone/sdk/workflow to run the built-in ingest workflow. Re-ingesting the same id replaces its chunks, so a re-crawl converges instead of duplicating. A crawler, a webhook or a nightly reindex.
CLI stackbone rag ingest <path> --collection <name>, plus collections create, list, query, jobs, retry and cancel. A script, or a first load from your laptop.

A collection in Studio: what is in it, how each document's ingest went, and the original to download.

Search on the same screen runs the query your tool would run: pick the collection, type the question, and the hits come back with their scores, their metadata and the model that embedded them. Use it to check why the agent answered what it did, or to see that a document did not chunk the way you expected.

The same query the tool makes, run by hand from Studio.

Three contracts keep the code path and the Studio path in agreement. The box embeds every chunk in a collection with one model, openai/text-embedding-3-small. Studio's uploads and searches always use it, and so does your code unless you pass another model on the stackbone.rag call (rag.embeddingModel in agent.yaml parses but changes nothing). If you do pass another model, feed the collection from code only, because a document uploaded by hand would never match a query your tool embeds differently. The vector dimension (1536) is part of the schema, so any model you pick must return 1536 dimensions, and switching means re-ingesting everything. A query reaches one collection only, so name it in the tool.

Keep facts across sessions

Status: planned. Nothing in this section works yet. Build on a session or on retrieval today, and read this to know what is coming.

A session remembers a conversation. It does not remember that this customer is on the Pro plan or prefers Spanish. That is what stackbone.memory is designed for: explicit facts an agent stores and searches by meaning, scoped to a user ('user', the default), to one conversation ('session') or to the agent itself ('agent', shared by every caller).

// Both calls compile and both return `not_implemented` today.
await stackbone.memory.add('Prefers dark mode and a Spanish UI.', { userId: 'user_42' });

const { data } = await stackbone.memory.search('What plan is the user on?', {
  userId: 'user_42',
  limit: 5,
});

The surface sits on the ambient stackbone client so the types are already there, and the intended backend is an external memory service (mem0) rather than the box's Postgres. Read stackbone.memory for the methods and the scopes. The Memory entry in Studio's sidebar is dimmed and non-navigable for the same reason.

Memory, retrieval and your database

Pick the store by what you need to do:

You want to… Reach for
Follow up on the previous message without replaying it A session. The runtime writes it on every turn, into stackbone_checkpoints in the box's Postgres.
Answer from a manual, a policy or a help center Retrieval. Studio, your ingest code or the CLI fill it, into stackbone_platform.rag_* in the same Postgres.
Remember a preference or a fact about a user, across sessions Long-term memory, once it ships. Until then, keep the fact in a table of your own and read it in a tool.
Store an order, a ticket or a user record and query it with SQL Your database. Your code writes it through stackbone.database, into your own tables in the same Postgres, with a schema you migrate yourself.

Two of them share the box's Postgres with your tables, and the DB Explorer in Studio shows all three schemas side by side. But only your tables are yours to shape: the runtime owns stackbone_checkpoints, and the platform provisions and owns the rag_* tables. Read them if you like; do not migrate them. See Database for what lives where.

What it needs

Capability What the box needs
Durable sessions @langchain/langgraph-checkpoint-postgres in the workspace. stackbone init adds it. Without it the box logs a warning at boot and serves every turn stateless, session key or not.
A session over the wire An x-stackbone-session header on the OpenAI and Anthropic wires; a threadId on AG-UI; nothing extra in the Playground.
Retrieval A model provider on the box (it embeds through the same endpoint the agents use). The platform provisions the rag_* schema on every install; there is no vector service to run.
Uploads from Studio The box's workflow runtime, which runs the ingest in the background. A workspace scaffolded by stackbone init has it. If an upload answers rag_ingest_unavailable, that runtime is not mounted on this box.
Long-term memory The mem0 wiring, once it ships. See stackbone.memory.

Read more

BUILT WITH ❤️ FROM CANADA AND SPAIN