Guides / Build well

Designing workspaces, peers and sessions

Before you send the first message, decide how your product maps onto Recall. That choice decides whose memories stay apart, who learns about whom, and what you can delete in one call later. This guide covers each concept in depth, shows two agents with different views of the same person, and ends with layouts for common products.

The concepts

ConceptWhat it isRule of thumb
WorkspaceAn isolated memory space. Nothing is read or learned across workspaces.One per app and environment; one per business customer if you sell to businesses; one per save in a game.
PeerAnyone who speaks: a person, an agent, a bot.One per real person or agent, with a stable id from your own database.
SessionOne coherent conversation. Messages, summaries and prompt context belong to it.One per chat thread, ticket, call or scene. Not one per message.
MembershipWhich peers take part in a session, and how each one observes the others there.Set it when you create the session.
ScopeA named group of sessions you want to recall together.One per account, project or case that spans several conversations.
Conclusions and peer cardsWhat Recall learned: facts about a peer as one observer sees them, and a short profile.Recall writes them as it learns. You can read them, add facts and delete them.

Workspaces: where memory stops

A workspace is the hard boundary. Context, search and chat only ever read one workspace, and the same peer id in two workspaces is two unrelated peers. Deleting a workspace removes everything in it.

The price of a boundary is that nothing crosses it. If people move between parts of your product and should be remembered across them, those parts belong in one workspace.

Peers: one stable id per person or agent

Memory belongs to the id. Use the key your own database already has for the person, such as user-8412, and never change it. Ids are 1 to 512 characters of letters, digits, _ and -.

Sessions and membership

A session is one conversation with a beginning and an end: a chat thread, a support ticket, a phone call, a meeting, a game scene. Recall keeps its messages in order, summarises it as it grows, and builds prompt context from it. What it learns outlives the session: the next conversation with the same person starts with everything learned so far.

Too small and too large both hurt. A session per message leaves your model no recent turns or summary to work with. One endless session per user can work for a single-thread companion, but then you cannot delete one conversation, apply a retention period, or keep a work chat and a private chat apart in context.

Membership is who takes part. Writing a message makes the author a member; you can also add peers when you create the session (peers) or later (addPeers). Each member has two settings for that session: observeMe, whether Recall learns from that member's messages there, and observeOthers, whether that member builds its own view of the others. When a member leaves, their messages stay in the session, and so does what they learned while they were there. A session can have at most 10 members with observeOthers on.

Who learns about whom

Everything Recall learns is a view: what one peer, the observer, knows about another, the observed. There are two kinds.

ViewBuilt fromRead it with
Recall's view of AliceEvery message Alice wrote, in any session, while she is observed (the default)alice.chat(...), or context with peerTarget: alice
Bob's view of AliceOnly Alice's messages in sessions where Bob was a member with observeOthers: true, written while he was therebob.chat(..., { target: alice }), or context with peerTarget: alice, peerPerspective: bob

Three settings decide what goes into each view:

SettingWhereEffect
observeMe (default on)Peer configurationOff: nothing is learned from this peer's messages, not by Recall and not by any other peer. The messages are still stored and still appear in context and search.
observeMeMembership, per sessionOverrides the peer's own setting in that one session.
observeOthers (default off)Membership, per sessionOn: this member builds its own view of the others from what they say in this session.

In the example a travel app has a planner agent and a billing agent. The user tells the billing agent they moved; the planner is not there. Agents are peers with observeMe: false, because your prompts define them.

import { Recall } from "@recall-memory/sdk";

const recall = new Recall({ workspaceId: "trips-prod" });

// One peer per real person or agent, with the id your own database already uses.
const user = recall.peer("user-8412", { metadata: { plan: "pro" } });
// Your code defines the agents, so Recall need not learn about them from their own lines.
const agent = { configuration: { observeMe: false } };
const planner = recall.peer("agent-planner", agent);
const billing = recall.peer("agent-billing", agent);

Each agent observes only its own conversation. The membership is set when the session is created: the agent joins with observeOthers, the user with the defaults.

// One session per conversation. Each agent joins with observeOthers, so it builds its own
// view of the people it talks to, from this conversation only.
const trip = recall.session("chat-5531", {
  metadata: { channel: "web" },
  peers: { "user-8412": {}, "agent-planner": { observeOthers: true } },
});
await trip.addMessages([
  user.message("I live in Utrecht and I am vegetarian."),
  planner.message("Noted. I'll only suggest restaurants with good vegetarian menus."),
]);

const invoice = recall.session("chat-5532", {
  metadata: { channel: "email" },
  peers: { "user-8412": {}, "agent-billing": { observeOthers: true } },
});
await invoice.addMessages([
  user.message("I moved to Amersfoort last week, please update my billing address."),
  billing.message("Done. Your next invoice goes to your new address in Amersfoort."),
]);

Two views of one person. Recall's view has the move, because it learns from everything the user says. The planner's view does not: it was never told. In a prompt, pass peerPerspective for the agent that is speaking, so it only knows what it witnessed.

// (waitForIdle is for this demo; real apps rarely wait for learning to finish.)
await recall.waitForIdle();
// Recall's view of the user, built from everything they said in any session: Amersfoort.
const recallView = await user.chat("Where do I live?", { reasoningLevel: "minimal" });
// The planner's view holds only what it witnessed. It never heard about the move: Utrecht.
const plannerView = await planner.chat("Where does the user live?", {
  target: user,
  reasoningLevel: "minimal",
});
// In a prompt, read the user through the eyes of the agent that is talking.
const context = await trip.context({ peerTarget: user, peerPerspective: planner, tokens: 2000 });

Which view to read is a product decision. A single assistant reads Recall's view. Separate agents, characters or departments that should not share what they heard read their own. When an agent should know something it did not witness, write the fact into its view yourself with conclusionsOf(user).create(...), as the Game NPCs guide does with rumours.

Scopes: sessions recalled together

Sometimes the right memory is neither one conversation nor everything a person ever said: one customer account with several contacts, one project with many meetings. A scope groups those sessions. Add sessions to it (recall.scope("acct-1042").addSessions([...])) and pass scope to chat or context: the answer then draws only on those sessions. A scope learns from its sessions like an observer of its own, and catches up on sessions that already had messages in the background. Scopes need a key for the whole workspace. Sales and CRM copilots and Workplace assistants show them in use.

Metadata and filters

Workspaces, peers, sessions and messages carry a metadata object that is yours: the plan, the channel, your ticket number. Recall stores it and lets you filter on it; it does not learn from it. Lists and searches take filters: exact matches on metadata keys, and operators such as gt, lt, in and contains. Search also takes peer_perspective, which limits results to what one peer saw (see Search filters).

// Metadata is yours to filter on.
const emailChats = await (
  await recall.sessions({ filters: { metadata: { channel: "email" } } })
).toArray();
// Search only what the billing agent saw: the trip chat is not in the results.
const billingSaw = await recall.search("restaurants", {
  filters: { peer_perspective: "agent-billing" },
});

Metadata is returned by every read and included in exports, so keep secrets and anything you would not show in your own admin screens out of it.

Creating and reading

Writes create what they name. Adding a message creates its workspace, session and author when they do not exist yet, and the get-or-create routes (POST /v3/workspaces, PUT …/peers/{peer}, PUT …/sessions/{session}) do the same. That keeps your code short, but a typo in an id quietly starts a new, empty memory.

An SDK handle without options, such as recall.peer("user-8412"), makes no request, so creating one creates nothing. To check whether something exists, use the read-only routes. They return the same object as the get-or-create calls and answer 404 when it does not exist (see Read without creating):

GET /v3/workspaces/{workspace}
GET /v3/workspaces/{workspace}/peers/{peer}
GET /v3/workspaces/{workspace}/sessions/{session}

Better still, validate ids where they enter your system (the signed-in user, a ticket number from your helpdesk) and derive every Recall id from them in one place in your code.

Scoped keys per tenant

An API key can be limited to one workspace, one peer or one session. Create them under API keys in the dashboard, or from your backend with recall.createKey({ workspaceId: "cust-1042" }) (POST /v3/keys).

KeyGive it toIt can
Account (no scope)Your own backend and admin jobs onlyEverything, in every workspace
WorkspaceThe service that works for one business customerEverything in that workspace, including forgetting a person
PeerCode on a user's device, or one agent that only readsRead the sessions that peer is in and ask from that peer's own point of view. It cannot write messages or delete a peer.
SessionA widget for one conversationRead and write that one session. It cannot delete a peer.

If users hold peer keys, give sessions ids they cannot guess (a UUID rather than support-8412), or create each session with your workspace key first: a peer key can create a session with itself as a member, and creating a session that already exists adds members. See Browser access.

Layouts for common products

ProductWorkspacePeersSessionAlso
Personal assistant or companionOne per environment, e.g. assistant-prodEach user; the assistant with observeMe: falseEach conversation threadRead Recall's view of the user.
Workplace assistant, or any product sold to businessesOne per customer, e.g. cust-1042Each of their users; your agentsEach chat or meetingA workspace key per customer; scopes for their projects.
Customer supportOne per brand and environmentEach customer, by CRM id; the bot; each human agentEach ticket, e.g. ticket-58213A scope per company account when several contacts share one.
GameOne per save or server, e.g. save-7f3aEach player; NPCs with observeMe: falseEach sceneobserveOthers for the NPCs in a scene; read with peerPerspective.
Multi-agent systemOne per deploymentEach agent; each person they serveEach task or runobserveOthers for agents that keep their own view; a key per agent.
Discord or Slack botOne per community, or one for the bot if members should be known across communitiesEach member, by platform user id; the bot with observeMe: falseEach channel or threadRead Recall's view of the member who is talking.
Coding agentsOne per developer or teamThe developer; the agent, not observedEach agent sessionThe integrations set this up for you.

Pitfalls

Next: Privacy, consent and forgetting · Reasoning levels and models