--- title: 'Memory' description: 'What an agent remembers today: the conversation inside a session and the documents it retrieves from, both in your box and both visible from Studio. Long-term facts are planned.' position: 9 --- # 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. ![A session's detail in Studio for the support agent: completed, three turns, 33,628 tokens, the session id, a Continue in Playground button, and the transcript of the three turns, each with the user message, the agent reply, its tokens, its duration and a Run detail link.](/images/memory/session-detail.png) _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](#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](#ground-answers-in-your-documents). | | **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](#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](#memory-retrieval-and-your-database) 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. ```sh # 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. ![The Sessions screen in Studio: Agent and Status filters, and a table with one row per conversation showing the agent, its status, the number of turns, the last activity, the tokens spent and a preview of the first message.](/images/memory/sessions.png) _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`](/docs/cli/reference/runs) 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](/docs/cli/protocol/auth#session-keys) 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. ```ts // 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: ```text You are the support agent for Acme. Use search_docs for any question about policies and answer from what it returns. ``` ![The Studio Playground with the support agent asked how long a return takes and when the refund arrives: an unfolded tool_use block calling search_docs with the query "returns policy", the tool_result listing three chunks with their scores, and the agent's answer quoting the 30-day window and the 5-day refund.](/images/memory/playground-retrieval.png) _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 --collection `](/docs/cli/reference/rag), plus `collections create`, `list`, `query`, `jobs`, `retry` and `cancel`. | A script, or a first load from your laptop. | ![The RAG screen in Studio: the help-center collection selected in the left rail with 3 docs and 4 chunks, a stat strip reading docs 3, chunks 4, dimensions 1536, a JSONB filter box, and the documents table listing three markdown files with status succeeded, their chunk count, metadata and preview, and View run, Download and Delete actions.](/images/memory/rag-explorer.png) _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 Search a collection panel over the RAG screen: help-center selected, the question "How long do I have to return an item?", topK 10, a header reading 4 hits, dim 1536, embed_with openai/text-embedding-3-small, and the first two hits from the returns document with scores 0.6186 and 0.4615.](/images/memory/rag-search.png) _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). ```ts // 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`](/docs/sdk/data/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](/docs/home/features/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`](/docs/sdk/data/memory). | ## Read more - [`stackbone.rag`](/docs/sdk/data/rag): parse, chunk, ingest, retrieve, delete, and the errors each can return. - [A retrieval-backed support agent](/docs/examples/agents/retrieval-backed): a full agent you can copy. - [Agent protocol](/docs/cli/protocol/auth#session-keys): the session headers, tool approvals and the AG-UI thread. - [API](/docs/home/features/api#chat-with-an-agent-from-any-client) and [Agents](/docs/home/features/agents): the wires and the screens around them. - [Storage](/docs/home/features/storage): where the uploaded originals go. - [Governance](/docs/home/features/governance): the rest of Studio.