Memory

Three 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. Long-term memory keeps explicit facts and recent call summaries per person in that Postgres. Studio reads sessions and retrieval live. Its Memory screen lets you inspect and erase customer memory for each agent.

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 Durable facts about one person, plus recent call summaries, in stackbone_platform.memory_*. stackbone.memory.add() updates individual facts, recall() reads them, and deleteAll() erases the person's facts and calls. 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 $STACKBONE_API_KEY" \
  -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 $STACKBONE_API_KEY" \
  -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, so a session survives a restart. It also 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. You write and publish it 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

A session remembers a conversation. It does not remember that this customer is on the Pro plan or prefers Spanish. stackbone.memory keeps those explicit facts per agent and person across sessions, together with a short history of calls.

await stackbone.memory.add('Prefers dark mode and a Spanish UI.', {
  agentId: 'support',
  userId: 'user_42',
  call: { startedAt: new Date().toISOString(), outcome: 'scheduled' },
});

const { data } = await stackbone.memory.recall({
  agentId: 'support',
  userId: 'user_42',
  calls: 5,
});

One model call receives the current facts under short stable handles and the new transcript, then returns per-fact add, update, or delete operations plus a bounded summary. The SDK validates the whole response before applying those operations and inserting the call record in one transaction. Untouched facts retain their identity and creation time. The box stores everything in its own Postgres; there is no external memory service or vector index. Memory uses openai/gpt-5.6-luna with high reasoning effort by default. You can change both values through the client configuration or their environment fallbacks. Read stackbone.memory for the full request, result, and configuration shapes.

Deep agents can declare independent memory read/save defaults and extraction instructions in code. The box settings contract lets Studio override each field and reset it to the current code value. Both flags default to off. See Per-agent defaults and overrides for the declaration and HTTP contract.

In Studio, open an agent in Catalog and choose its Memory tab. You can change reading, saving and extraction instructions. Each field shows where its current value comes from. Reset removes that field's Studio override and restores its current code value or the default.

Choose the customer identity field once, such as phoneNumber, email, customerId or a custom literal key. Studio asks you to confirm that choice. The key stays fixed when you reset settings or erase a customer's memory. Your integration must provide the selected field as structured identity data. Standard OpenAI user, Anthropic metadata.user_id, AG-UI forwardedProps.customerId and SDK customerId supply only the customerId field. A turn without the selected identity continues without memory tools.

Open the global Memory screen to inspect retained records. Choose an agent, then search by phone, email or another customer identifier. Open a customer to read facts and retained interaction summaries. Delete all asks you to confirm both the customer and agent, then erases that customer's memory for that agent.

The model chooses when to use memory_read or memory_save, according to the current independent flags. Text does not automatically recall or save. Save-only mode returns an acknowledgement without exposing stored facts or old summaries. The runtime binds both tools to the agent and person; different customers using the same session key also receive separate conversation history. See Text memory tools and customer identity. Voice can read memory at call start and save the heard conversation after the call, according to the independent settings. See Voice.

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. stackbone.memory stores per-person facts and recent call summaries in the box's Postgres.
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.

All three share the box's Postgres with your tables, and the DB Explorer in Studio shows the 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_* and memory_* 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 A model provider on the box and the platform migrations. The box advertises memory.basic; no separate memory service or key is required. See stackbone.memory.

Read more

BUILT WITH ❤️ FROM CANADA AND SPAIN