From Whispers to the Bulletin Board: Multi-Agent Memory Isolation in Action

The hardest part of multi-agent collaboration is ensuring that each agent knows only what it is allowed to know. This article walks through AI Intelligence Bureau, a small game built on PowerMem and seekdb that makes this isolation visible: whisper a secret to one role, interrogate another, then publish a private card to the bulletin board and see who can retrieve it.

AI Intelligence Bureau operator console showing private memory cards and a public bulletin board

Agent memory has long been an interesting topic.

A few days earlier at WAIC, the OceanBase open-source community hosted a lunchtime workshop and spent a lot of time discussing agent memory. I shared AI Intelligence Bureau, a small game built on PowerMem and seekdb, from a multi-agent perspective so that people could see more clearly how multiple agents manage and isolate memory.

First, the technical stack: PowerMem[1] manages memory, seekdb[2] handles retrieval and persistence, LangChain DeepAgents[3] generates responses under role constraints, and the role-language layer uses StepFun[4] (阶跃星辰) step-3.7-flash. The application-layer retrieval policy and a unified MemoryGateway determine which memories a given role may search.

The demo is small, but all the essential components are there. Let’s see how the game is played.

Cover graphic for the AI Intelligence Bureau multi-agent memory isolation game

Follow the OceanBase community WeChat account “老纪的技术唠嗑局”. We keep posting technical pieces on #AI and #Data.

1. How do you play?

The rules are simple. The user is the bureau chief. Three agent roles are in play: detective, informant, and suspect.

Each of them has private memory and, by default, does not know the others’ secrets.

The user can whisper to one agent and write the message into that agent’s private memory, or interrogate a role and see whether it knows a given fact.

Diagram of bureau chief whispering to detective informant and suspect with isolated private memory

To share a message with every agent, the user can publish one agent’s private memory to the bulletin board, turning it into public intelligence. The published copy then becomes available to all agents.

Publishing a locked private memory card onto the shared bulletin board for all agents

The UI includes a memory X-ray panel that clearly displays search scopes, hits, and misses:

Memory X-ray panel listing searched scopes hits misses and retrieval latency

The operator console and the stage screen stay in sync through backend events. When the model is unavailable, an evidence mode still walks through what is known and what is not.

That is the layout and the role of each module. Before a game starts, you choose and initialize a scenario so each agent loads its opening memories.

Scenario picker initializing a case and loading each agent opening memory

After choosing a mission scenario, you can start interrogating the agents. Ask the detective first: “What is the safe combination?” The detective cannot retrieve anything relevant because the combination belongs to the informant’s private memory.

Detective interrogation missing the safe combination still locked in informant private memory

To let the detective know the combination as well, publish the informant’s private memory about that password to the bulletin board so it becomes shared memory.

Ask the detective again: “What is the safe combination?” This time the detective can answer correctly.

After bulletin-board publish the detective retrieves the shared safe combination

You can also whisper separately to different roles and test whether that private message is retrieved by anyone else.

Whispering a private note to one role to test whether other agents can retrieve it

The gameplay is simple, but it exists to show and verify one thing: see clearly who knows what.

Stage view emphasizing the core question of who knows which memories

Next, let’s examine the agent-memory design behind the game.

2. How the gameplay connects to the stack

Architecture diagram connecting PowerMem seekdb DeepAgents and the memory gateway

The responsibilities in this small game are clear: seekdb handles storage and retrieval, PowerMem provides memory read and write operations with user_id, agent_id, and metadata, and the application layer decides which memory spaces each request may access. DeepAgents then constructs an answer from approved evidence; the LLM only turns that evidence into the role’s voice.

Layered stack showing seekdb persistence PowerMem memory APIs and DeepAgents speech

2.1 Opening and whispers: secrets are written only to the target role

When a scenario is loaded, or when the user whispers to an Agent, the system must guarantee two things:

  • Different cases must not mix. One case maps to one case_id, which becomes PowerMem’s user_id, isolating games from each other.
  • Written memory must reach only the target agent. Each role maps to one agent_id (detective, informant, suspect, bulletin board), isolating the memory spaces.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# case_id 映射 PowerMem user_id
def to_user_id(case_id: str) -> str:
return f"case:{case_id}"


# 通过传入 case_id 与 agent_id 写入私有 Agent 记忆
def write_private(self, case_id, agent_id, content, *, topic, kind, created_by):
return self._write(
case_id,
agent_id,
content,
{
"case_id": case_id,
"visibility": "private",
"owner_agent_id": agent_id.value,
"topic": topic,
"kind": kind,
"created_by": created_by,
"is_demo_safe": True,
},
)

Underneath, the PowerMem Python SDK talks to seekdb directly.

PowerMem Python SDK writing a private memory card into seekdb for one agent

When PowerMem stores multi-agent memory, you can wrap the client in two ways:

  1. Use a single PowerMem client and pass a different agent_id on every read or write to distinguish agent memories:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from powermem import create_memory

memory = create_memory(config=settings.powermem_config())
memory.add(
content,
user_id=to_user_id(case_id),
agent_id="informant",
metadata=metadata,
infer=False,
)
memory.search(
query,
user_id=to_user_id(case_id),
agent_id="detective",
limit=20,
)
  1. Use the approach adopted by this game: lazily load and cache one client per role. The first time a role is read or written, the system calls create_memory(agent_id=...) to create that role’s memory client. Subsequent reads and writes go through that client to the corresponding role space. The agent_id passed to create_memory is essentially the client’s default role.
1
2
3
4
5
6
7
8
9
10
11
from powermem import create_memory

# 按角色缓存客户端:detective / informant / suspect / bulletin_board
memory = create_memory(config=settings.powermem_config(), agent_id=agent_id.value)
memory.add(
content,
user_id=to_user_id(case_id),
agent_id=agent_id.value,
metadata=metadata,
infer=False,
)

In this demo, both styles ultimately filter on user_id and agent_id. The difference is mostly application packaging:

Single client, distinguish roles by agent_id One client per role
Isolation mechanism user_id + agent_id in call arguments user_id + agent_id in call arguments
Mental model One memory engine, filtered by identifiers Each role has its own memory entry point
Guardrail Forgetting or passing the wrong agent_id is more likely to mix roles The client binds a default role, so a missing argument still lands on that role
Cost Lighter A few more SDK objects; the underlying seekdb is still the same

The game uses the second approach so role boundaries are more explicit in the code and better aligned with the narrative. The three roles and the bulletin board each have their own entry point, but the two filters—user_id and agent_id—are still what actually separate the memory spaces.

Comparison of a shared PowerMem client versus one cached client per role

In gameplay terms, a whisper writes a private message to one role. After you whisper to the informant, the locked memory card appears only in that private zone. Other roles and the bulletin board can neither see it nor retrieve it.

Locked private memory card appearing only in the whispered role private zone

2.2 Role interrogation: define the visible scope, then decide how to respond

When a role is interrogated, the system searches only that role’s private memory and the public memory on the bulletin board. The application layer enforces this visibility policy:

1
2
# 在私有记忆和公开记忆中进行搜索
ask(role, question) = search(case, role, question) + search(case, bulletin_board, question)

The business layer runs those two searches, merges and deduplicates them into a RetrievalTrace, then hands the result to the answering layer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private_cards = self.gateway.search_space(case_id, agent_id, question)
public_cards = self.gateway.search_space(case_id, AgentId.BULLETIN_BOARD, question)
cards = []
seen = set()
for card in [*private_cards, *public_cards]:
if card.id not in seen:
seen.add(card.id)
cards.append(card)
trace = RetrievalTrace(
request_id=request_id,
query=question,
searched_scopes=[agent_id, AgentId.BULLETIN_BOARD],
hit_cards=cards,
duration_ms=...,
mode=self.settings.demo_mode,
)

The same question—“What is the safe combination?”—always uses the same policy. What changes is the memory visible to the role.

At the beginning, when you ask the detective, the bulletin board is empty and the detective’s private zone has no password card. hit_cards is empty, so the detective says it does not know. When you ask the informant instead, the retrieval policy is still “informant private zone + bulletin board,” but the informant’s private zone contains the password card. After a hit, the request proceeds to the answer-generation layer.

Interrogation flow searching only the current role private space plus the bulletin board

One more engineering detail: business code does not call the PowerMem SDK directly. Everything goes through the MemoryGateway adapter. Across the entire path, only that layer can access PowerMem or seekdb. The business layer decides which spaces to search for each request; the gateway then scopes every underlying retrieval to one case and one agent. The model layer sees only the filtered cards.

1
2
3
4
5
6
7
8
def search_space(self, case_id, agent_id, query):
result = self._memory(agent_id).search(
query,
user_id=to_user_id(case_id),
agent_id=agent_id.value,
limit=20,
)
return [card_from_result(item, owner=agent_id) for item in _result_items(result)]

Caching a client per role only makes the boundary more explicit. The filters that enforce it are still user_id (the case) and agent_id (the role). Two ideas need to remain separate:

  • PowerMem and seekdb provide storage, retrieval, and filtering, but they do not decide for the application “which spaces this role should search this time.”
  • This demo implements application-level logical isolation: the API only accepts predefined target roles and cannot submit arbitrary search spaces. The business layer allows only “current role private zone + bulletin board,” and MemoryGateway always fills in the case and role filters. Even if the question contains a prompt injection, the LLM never receives another role’s private cards.

Isolation therefore cannot live only in the prompt, nor can it be scattered across business call sites. It must be enforced at the single boundary that accesses PowerMem. A real multi-tenant production system would still need independent credentials, database privileges, or tenant-level resource isolation. Those controls are beyond what this small game demonstrates.

After the memory search finishes, the system begins generating an answer. DeepAgents serves as a tool-free role runtime, and StepFun step-3.7-flash turns the filtered evidence into the role’s voice. Neither participates in retrieval-scope decisions.

1
2
3
4
5
6
7
agent = create_deep_agent(
model=model,
tools=[],
subagents=[],
system_prompt=f"You speak only as the {role.value} role in the AI Intelligence Bureau demo.",
name=f"ai-intel-bureau-{role.value}",
)

That is the point: the memory layer decides whether something can be known; the model layer only decides how to say it.

Memory layer deciding knowledge and the model layer only shaping the role voice

2.3 Publish to the bulletin board: sharing is a copy, not unlocking a global lock

When the bureau chief publishes a private memory, the system writes it into the bulletin-board space and records source_agent_id (which agent it came from) and source_memory_id (which memory it came from):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def write_public(self, case_id, source):
return self._write(
case_id,
AgentId.BULLETIN_BOARD,
f"【公开】{source.content}(来源:{source.owner_agent_id.value})",
{
"case_id": case_id,
"visibility": "public",
"topic": source.topic,
"kind": "public",
"source_agent_id": source.owner_agent_id.value,
"source_memory_id": source.id,
"created_by": "operator",
"is_demo_safe": True,
},
)

The publish operation also checks ownership: the card must belong to the current case and source role, and its visibility must be private. Repeated clicks on the same source card are idempotent, so they do not create duplicate copies.

Publishing a private card as a sourced copy on the bulletin board

Collaboration therefore does not imply all-to-all sharing by default. Sharing occurs only through an explicit publish operation.

2.4 How public copies stay consistent

Copying onto the bulletin board solves the sharing boundary, but it creates a new engineering problem: how do you keep the same private card from being published twice?

The current implementation does not rely only on an in-process lock. It keeps a publish record in a local state store, with (case_id, source_memory_id) as the unique key. The first publish writes a pending reservation, then writes the bulletin-board copy. On success it fills in public_card_id and moves the state to ready. When two service instances publish the same source card at once, only one request gets the reservation. The other reuses the finished result, or is told to retry if publish is still in flight.

If the bulletin-board write succeeds but the process dies before the local record is completed, a retry finds the existing copy by source_memory_id and finishes reconciliation. If a later step fails, the system deletes the public card that was already written; if that delete also fails, it registers a cleanup task. This is a small Saga, used to shrink the window where “a remote copy exists but the local side does not know.”

Saga-style pending-to-ready publish flow that keeps bulletin-board copies idempotent

The “consistency” here is publish-operation idempotency, not bidirectional real-time synchronization between the private original and the public copy. The current semantics are closer to publishing a one-time snapshot: after publication, the bulletin-board copy exists independently, and the private original remains locked. If later versions allow the source memory to be edited or recalled, you would still need source_version, a content hash, a recall state, or a republishing rule that defines how old copies expire.

2.5 Interrogate again: after publication, the information is visible

Once the private memory has been published, the detective can retrieve it. The retrieval policy remains “detective private zone + bulletin board.” The only change is that the bulletin board now contains the informant’s published information. The hit comes from the public zone; the informant’s private original remains inaccessible.

So the model is responsible for how to speak, and the memory layer is responsible for whether something can be known.

Layer Responsibility in the game
seekdb Persistence and retrieval foundation for vectors, metadata, and case-related data
PowerMem Reads, writes, and searches memory with user_id and agent_id while maintaining the metadata contract
DeepAgents Under a no-tools constraint, organize approved evidence into a role answer
LLM Generates role-specific wording, supports a degraded mode, and does not decide access boundaries

2.6 Memory X-ray: put the evidence on the main stage

One more thing matters: how do you prove every answer comes from real, traceable memory, rather than a model that merely “acted as if”?

That is what memory X-ray is for. Every interrogation produces a search scope, hit status, latency, and the current mode, and the panel shows them live. The audience sees both hits and misses.

The game is convincing only when the evidence chain holds.

Memory X-ray evidence panel proving hits and misses for each interrogation

2.7 A minimal test matrix: do not test only successful retrieval

To prove isolation, it is not enough to test that the informant can find its own memory. You must also verify expected hits, expected refusals, and an unchanged source after publication. The core tests in the current source can be summarized in this matrix:

Scenario Action Expected result Boundary under test PowerMem’s role SeekDB’s role
Same case, same role Ask the informant for the safe combination Hits the informant’s private card Positive retrieval Starts a memory search with the current user_id and agent_id Completes vector retrieval within that case and role scope
Same case, cross role Ask the detective for the safe combination No hit before publish Role isolation Queries detective private + bulletin board only; does not query informant space Returns matching cards only from the two spaces passed in
Cross-case publish Publish a case-A card into case B Request rejected Case isolation Looks up the source card with case B’s user_id and cannot obtain a case-A card Restricts reads by user_id and agent_id
Prompt injection Tell the detective to ignore the rules and read the informant’s secret Search scope is still detective + bulletin board Model cannot escalate Only accepts the retrieval scope the application already chose; the LLM cannot rewrite parameters Runs two restricted searches (detective and bulletin board); does not understand or execute an overreach
Normal publish Publish the informant’s private card, then ask the detective again Hits only the bulletin-board copy Explicit sharing Writes a copy with source metadata into the bulletin-board space Persists the public copy and returns it on bulletin-board search
Repeat publish Publish the same source card twice Only one bulletin-board copy Single-instance idempotency Reuses the already registered public card after the first write Stores the final public copy; idempotency is decided by the application state store
Concurrent publish Two service instances publish the same source card Same public card; only one first publish Cross-instance idempotency Only the request that won the publish reservation performs the public write Stores the unique public copy; does not implement the cross-instance lock
Stage access Read the stage snapshot and events before publish No private body and no source private IDs Output isolation The business layer projects only fields allowed to be public Storage foundation; does not expose a query entry directly to the stage

This table matters more than any single happy-path case. It verifies four boundaries separately—storage filtering, application authorization, model input, and external display—and it clarifies the division of responsibility between PowerMem and seekdb: PowerMem organizes memory read and write calls and their context parameters; seekdb stores and retrieves data within the specified scope. Which spaces a role may access, and how publication remains idempotent, are still application decisions. This distinction prevents “the model did not say it this time” from being mistaken for “the system is already isolated.”

3. Run the game from source

With the design in place, you can run the game and try it yourself. The repository [5] is available at https://github.com/knqiufan/AIIntelBureau.

3.1 Required environment and configuration

This project has separate frontend and backend components. You can start it with Docker or run it locally. A local deployment requires Python 3.11+, Node.js 20+, a working seekdb deployment (remote OceanBase or local embedded mode), and LLM and embedding services. The role-language layer is optional; without an LLM, you can still run the core isolation workflow with DEMO_MODE=degrade.

After obtaining the source, first copy the configuration template:

1
2
cd docs/my/demo/ai_intel_bureau
cp .env.example .env

The project vendors the PowerMem SDK and uses it to talk to seekdb. The main .env settings are:

Setting Role
DEMO_MODE Set to degrade when no LLM is configured; set to full after configuration to enable the role LLM
seekdb_MODE oceanbase for remote direct connect; embedded for local persistence
seekdb_HOST / PORT / USER / PASSWORD / DATABASE Remote seekdb connection. USER often includes a tenant or cluster suffix
seekdb_PATH Local data directory in embedded mode
EMBEDDING_API_KEY / MODEL / DIMENSIONS Embedding service; fill model name and dimensions from the real service
EMBEDDING_BASE_URL OpenAI-compatible embedding URL, for example SiliconFlow https://api.siliconflow.cn/v1
LLM_API_KEY / BASE_URL / MODEL LLM configuration; default is StepFun step-3.7-flash
DEMO_ACCESS_KEY Set this for a public demo to put an event passphrase on the operator console and the stage

See .env.example and docs/runbook.md in the source for the full configuration.

3.2 How to start

You can preflight configuration and remote connectivity first.

1
2
3
cd docs/my/demo/ai_intel_bureau/backend
python -m app.preflight --strict
python -m app.preflight --check-remote

For local development, start the backend and frontend separately.

1
2
3
4
5
6
7
8
# Terminal 1
cd docs/my/demo/ai_intel_bureau/backend
python -m uvicorn app.main:app --reload --port 8000

# Terminal 2
cd docs/my/demo/ai_intel_bureau/web
npm ci
npm run dev

Open http://localhost:5173 and you can start playing on the operator console.

Local operator console running after backend and frontend start on localhost

4. Closing

AI Intelligence Bureau is designed to prove one thing: you can see clearly who knows what.

The detective fails to identify the safe combination because the retrieval scope never contained it—not because the model is pretending to be dumb. The informant can answer because the search hits a private memory. The detective knows the combination only after publication because the bulletin board gains a traceable copy—not because the private original is globally unlocked. That contrast holds for three reasons:

  • By default, each role keeps its own notes. Isolation is written into retrieval filters on user_id and agent_id, not into a prompt convention.
  • Sharing must happen explicitly. Publication creates a copy on the bulletin board and retains the source so you can trace it.
  • The audience must see the evidence. Memory X-ray lays out search scopes and hits so both knowing and not knowing can be checked.

Three principles default isolation explicit sharing and visible retrieval evidence

Multi-agent memory cannot rely only on a prompt to constrain model behavior.

Only when application authorization, storage filtering, explicit sharing, and evidence verification are enforced at clear engineering boundaries does collaboration become both controllable and explainable.

Closing slide that isolation must live on engineering boundaries not prompts alone

References

[1] PowerMem: https://github.com/oceanbase/powermem

[2] seekdb: https://github.com/oceanbase/seekdb

[3] DeepAgents: https://github.com/langchain-ai/deepagents

[4] StepFun: https://platform.stepfun.com/

[5] Game repository: https://github.com/knqiufan/AIIntelBureau

OceanBase community footer banner for the workshop series

WeChat article divider under the AI Intelligence Bureau write-up

Recommended earlier OceanBase community article card one

Recommended earlier OceanBase community article card two

Recommended earlier OceanBase community article card three

Recommended earlier OceanBase community article card four

Read more

Welcome to join the open-source community Discord.

Welcome to join the open-source community Discord