Skip to main content

Cross-channel agent memory plane

Agent memory is a per-contact store of facts, preferences, goals, and conversation summaries that your agents learn and recall. The memory plane is the layer that makes that store usable when the customer shows up on any channel — voice, SMS, WhatsApp, or chat — instead of only re-reading the current conversation thread. Two services sit under the plane:
  • Resolution — at the start of a turn, the plane maps whatever identifier the channel carries (E.164 phone, contact id, unified identity id) to a single resolved scope: the contact plus their unified identity.
  • Retrieval — the resolved scope builds a tenant-scoped vector filter; candidate items are deduplicated and recency-ranked before the top-K are rendered into the agent’s context.
Without the plane, a voice caller is anonymous until they re-identify themselves, and an SMS conversation that already captured their preferences is invisible to a later phone call.

Identity resolution flow

Each channel hands a different identifier to the runtime, so the plane resolves in up to three steps:
  1. E.164 phone only (voice turns) — resolve phone → contact_id through the registered lookup. When nothing matches, the turn proceeds with whatever scope was already supplied.
  2. Contact id present (SMS / WhatsApp / chat) — resolve contact_id → unified_identity_id so a contact merge can’t strand the turn on the pre-merge id (merges re-point memory in the same transaction).
  3. Caller already supplied a unified identity — that short-circuits both lookups.
Every lookup step is fail-open: a database blip, a missing row, or a lookup that isn’t registered never blocks the turn — the agent just retrieves with the fragments it had. Identity resolution never introduces a blocking call into the turn-critical path.
A contact-to-identity resolution is cached in-process for 5 minutes, so the same contact doesn’t re-query within a conversation.

Retrieval: filter assembly, dedupe, recency

Retrieval runs against the tenant-scoped vector store that backs all agent memory, filtered and ranked in this order:
  1. Tenant scope first. The filter asserts the tenant id before any contact-level clause; the platform’s strict-tenancy check rejects a filter that lacks it, and malformed tenant or contact shapes abort retrieval with an empty result rather than crossing a tenant boundary.
  2. Contact + unified identity. When a resolved identity is present, the filter matches a point by either contact_id or unified_identity_id — so memory re-pointed by a mid-conversation merge stays retrievable, and pre-merge legacy points (which only carry contact_id) are still matched.
  3. Narrowing. Optional agent_id and unified_conversation_id clauses restrict retrieval to one agent’s or one conversation’s notes.
  4. Over-fetch and dedupe. The query fans out for roughly limit × 3 candidates, then drops content-identical items (normalised on trim/lower/first-160-chars) so a fact can’t double-occupy the top-K.
  5. Recency decay + rerank. Scores decay ~5 % per week of age (floored so old items don’t vanish), and an optional reranker re-orders the pool before the top-K are returned.
If anything in steps 1–4 fails or returns nothing, retrieval collapses to an empty list rather than a runtime error.

Rendering memory into the turn

When retrieval produced items, the plane renders them as a single system-level block and hands it back to the agent executor. The block is prefixed with the constant marker
followed by one - <item> line per memory. The executor uses that marker the same way it uses its RAG-context prefix: on every new turn it strips the previously attached memory block before appending the fresh one, so memory never accumulates across a conversation even when the conversation state is persisted between turns. When resolution can’t identify a contact, or retrieval produced no items, no block is attached on that turn — the agent simply runs with the context it already has.

Reading memory through the CDP Profile API

Four endpoints expose a contact’s plane to your systems, one per identifier family that the Profile API already resolves. All four return the same shape, with PII masked by default, rate-limited at 60 requests per minute per tenant.

By user id

GET /api/v1/cdp/profiles/by-user-id/:user_id/memory

By email

GET /api/v1/cdp/profiles/by-email/:email/memory

By phone

GET /api/v1/cdp/profiles/by-phone/:phone/memory

By anonymous id

GET /api/v1/cdp/profiles/by-anonymous-id/:anonymous_id/memory
Auth: owner / admin / developer role with the contacts:read scope. Query params:
memory_type is one of fact, preference, goal, summary. When the contact’s GDPR opt-out flag is set (memory_enabled=false), the response is an explicit empty shape — never a leak of the opted-out plane:
Unknown identifiers return 404 PROFILE_NOT_FOUND. Malformed identifiers return a 422 validation error per the Profile API conventions.

Role-gated PII reveal

Item content is PII-masked by default. Passing ?reveal=true unmasks it only when the caller has owner, admin, or developer role; every reveal writes an audit-log entry so operators can see who unmasked what.
  • 401 — the request didn’t authenticate (missing/expired JWT).
  • 403 — an authenticated caller without one of the three roles or without contacts:read. Reveal is silently ignored for them: the route returns successfully with the content still masked rather than leaking or hard-failing.

The pre-turn identity bridge (internal)

The agent runtime needs to lift a caller’s phone into a contact before the executor turn runs, so it queries one internal endpoint and hands the result to the plane.
Returning:
Phone-only turns supply phone; unmatched phones return 200 with null fields so the runtime proceeds in contact-only retrieval — 404 is deliberately never used for an unmatched row. contact_id-only turns resolve the unified identity. include_memory_enabled=true also returns the contact’s opt-out flag so the runtime can log the gate before retrieval. Server-to-server only: the internal token gate rejects external callers.

Browsing memory in the dashboard

Open AI Agents → Customer Memory (/agents/memory). Pick a contact, and the page runs the same plane surface as the CDP endpoints — you can read items, add a fact manually, and delete items. When a contact has the GDPR opt-out flag set, the dashboard shows the opted-out state explicitly rather than listing items. The plane honours the per-contact memory_enabled flag (settable from the contact’s record or the consent surfaces) before any retrieval fan-out. The gate is consulted on every read and write:
  • An opted-out contact returns an empty plane from the CDP endpoints, the dashboard shows the opt-out state, and the executor attaches no memory block.
  • A contact-level erasure (DELETE /api/v1/contacts/:contact_id/memory) purges every item the tenant holds on the contact.
  • An organisation-level offboard purges every item for the tenant.
The opt-out check is fail-open by design: if the lookup errors it treats the contact as opted-in rather than blocking the turn.

Example: fetch plus reveal behaviour

Fetch a contact’s memory by email, then unmask when you’re an owner:
Sample response:
Error shapes you should expect:

Next steps

  • Cost controls — memory retrieval adds embedding spend; budgets still apply.
  • Tenant isolation — the tenant-first filter contract the plane enforces.
  • Consent management — how memory_enabled maps to consent states and erasure.
  • AI Agents — the runtime that attaches the plane’s context block per turn.