> ## Documentation Index
> Fetch the complete documentation index at: https://docs.praxa.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Federate and test existing memory

> Connect Mem0, LangGraph, Zep, Graphiti, Letta, OpenAI Agents sessions, or a custom store to Praxa and test namespace isolation, provenance, and provider outages.

This tutorial uses the public, dependency-free `@praxa/sdk/memory` entrypoint.
The SDK is read-only. It receives provider clients your backend already owns,
and it requires explicit tenant and subject namespace resolution.

## Prerequisites

Before you begin, prepare:

* a backend-owned provider client and explicit tenant-subject namespace resolver;
* synthetic records for at least two tenants and two subjects per tenant;
* the exact @praxa/sdk memory package version and supported provider contract;
* a provider cleanup procedure plus degraded-state and timeout fixtures;
* an acceptance assertion that proves federated recall preserves source status, provenance, contradictions, bounds, and isolation.

## 1. Install the package

```bash theme={null}
npm install @praxa/sdk@0.3.0
```

The supported portable record kinds are `message`, `fact`, `summary`,
`episode`, `pinned_context`, `document`, `entity`, and `edge`.

## 2. Create a source

<Tabs>
  <Tab title="Mem0">
    Mem0 namespace filters must use positive equality with current camel-case
    keys: `userId`, `agentId`, `appId`, or `runId`.

    ```ts theme={null}
    import { createMem0MemorySource } from "@praxa/sdk/memory";

    const mem0Source = createMem0MemorySource({
      client: mem0,
      mapNamespace: ({ tenantId, subjectId }) => ({
        userId: `${tenantId}:${subjectId}`,
      }),
      threshold: 0.6,
      rerank: true,
    });
    ```
  </Tab>

  <Tab title="LangGraph">
    Only long-term `BaseStore` items are adapted. Checkpoints are intentionally
    excluded.

    ```ts theme={null}
    import { createLangGraphMemorySource } from "@praxa/sdk/memory";

    const langGraphSource = createLangGraphMemorySource({
      store,
      mapNamespace: ({ tenantId, subjectId }) => [
        tenantId,
        subjectId,
        "memory",
      ],
      kind: "document",
    });
    ```
  </Tab>

  <Tab title="Zep">
    Choose one graph scope. It maps to one portable kind: `edges` to `edge`,
    `nodes` to `entity`, or `episodes` to `episode`.

    ```ts theme={null}
    import { createZepMemorySource } from "@praxa/sdk/memory";

    const zepSource = createZepMemorySource({
      client: zep,
      scope: "edges",
      mapNamespace: ({ tenantId, subjectId }) => ({
        graphId: `${tenantId}:${subjectId}`,
      }),
    });
    ```
  </Tab>

  <Tab title="Graphiti">
    Graphiti uses an injected transport so your application owns the exact
    client version and result mapping.

    ```ts theme={null}
    import { createGraphitiMemorySource } from "@praxa/sdk/memory";

    const graphitiSource = createGraphitiMemorySource({
      transport: {
        async search({ query, namespace, limit, signal }) {
          return graphitiSearch({ query, groupId: namespace, limit, signal });
        },
      },
      mapNamespace: ({ tenantId, subjectId }) => `${tenantId}:${subjectId}`,
      retrievalModes: ["graph"],
      supportsAbort: true,
    });
    ```

    Every returned transport record must already contain `sourceRecordId`, a
    Graphiti-compatible `kind`, and non-empty `text`. Provenance is retained
    when your transport supplies it.
  </Tab>

  <Tab title="Letta">
    The adapter reads named blocks and recent messages. It does not expose a
    general archival-memory search API.

    ```ts theme={null}
    import { createLettaMemorySource } from "@praxa/sdk/memory";

    const lettaSource = createLettaMemorySource({
      client: letta,
      include: "both",
      mapNamespace: ({ tenantId, subjectId }) => ({
        agentId: lookupLettaAgent(tenantId, subjectId),
        blockLabels: ["persona", "human"],
      }),
    });
    ```
  </Tab>

  <Tab title="OpenAI Agents">
    The adapter reads official message items from the session resolved for the
    subject. It ranks the recent window newest first.

    ```ts theme={null}
    import { createOpenAIAgentsSessionSource } from "@praxa/sdk/memory";

    const openaiSource = createOpenAIAgentsSessionSource({
      sessionForNamespace: ({ tenantId, subjectId }) =>
        sessions.get(`${tenantId}:${subjectId}`),
    });
    ```
  </Tab>
</Tabs>

## 3. Combine the sources

```ts theme={null}
import { MemoryFederation } from "@praxa/sdk/memory";

const memory = new MemoryFederation({
  sources: [mem0Source, langGraphSource, zepSource],
  maxConcurrency: 3,
  maxSources: 8,
  maxResultsPerSource: 50,
  maxTotalResults: 100,
  defaultTimeoutMs: 5_000,
});

const result = await memory.recall({
  query: "What does this customer prefer?",
  namespace: {
    tenantId: "acme",
    subjectId: "customer-42",
    scope: ["support"],
  },
  limit: 10,
});
```

Inspect both the aggregate and each source:

```ts theme={null}
if (result.status === "failed") {
  throw new Error(JSON.stringify(result.sources));
}

for (const source of result.sources) {
  console.log(source.sourceId, source.status, source.durationMs, source.itemCount);
}

for (const item of result.items) {
  console.log(item.kind, item.text, item.matches.map((match) => match.provider));
}
```

## 4. Add a custom source

```ts theme={null}
import type { MemorySource } from "@praxa/sdk/memory";

const customSource: MemorySource = {
  id: "customer-profile",
  provider: "custom",
  capabilities: {
    retrievalModes: ["exact"],
    recordKinds: ["fact"],
    readOnly: true,
    sourceLocalScores: true,
    supportsAbort: true,
    supportsFilter: false,
  },
  async recall({ query, namespace, signal }) {
    signal.throwIfAborted();
    const row = await profiles.find(namespace.tenantId, namespace.subjectId, query);
    return row === null
      ? []
      : [{
          sourceRecordId: row.id,
          kind: "fact",
          text: row.text,
          provenance: {
            origin: "explicit",
            confidence: 1,
            capturedAt: row.updatedAt,
          },
        }];
  },
};
```

If a custom source includes `provenance`, provide all required fields. Do not
send a partial provenance object and invent missing confidence or timestamps.

## 5. Test the federation engine

This Vitest test proves success plus explicit partial degradation without
calling a real provider:

```ts memory.test.ts theme={null}
import { describe, expect, it } from "vitest";
import {
  MemoryFederation,
  MemorySourceUnavailableError,
  type MemorySource,
} from "@praxa/sdk/memory";

const capabilities = {
  retrievalModes: ["exact"],
  recordKinds: ["fact"],
  readOnly: true,
  sourceLocalScores: true,
  supportsAbort: true,
  supportsFilter: false,
} as const;

const profile: MemorySource = {
  id: "profile",
  provider: "custom",
  capabilities,
  async recall() {
    return [{
      sourceRecordId: "preference-1",
      kind: "fact",
      text: "Prefers concise answers",
      provenance: {
        origin: "explicit",
        confidence: 1,
        capturedAt: "2026-08-13T12:00:00.000Z",
      },
    }];
  },
};

const archive: MemorySource = {
  id: "archive",
  provider: "custom",
  capabilities,
  async recall() {
    throw new MemorySourceUnavailableError("Archive is offline");
  },
};

describe("memory federation", () => {
  it("returns partial context without hiding the outage", async () => {
    const memory = new MemoryFederation({ sources: [profile, archive] });
    const result = await memory.recall({
      query: "answer style",
      namespace: { tenantId: "acme", subjectId: "person-1" },
    });

    expect(result.status).toBe("partial");
    expect(result.items[0]?.text).toBe("Prefers concise answers");
    expect(result.sources.map(({ sourceId, status }) => ({ sourceId, status })))
      .toEqual([
        { sourceId: "profile", status: "ok" },
        { sourceId: "archive", status: "unavailable" },
      ]);
  });
});
```

## 6. Run provider-level canaries

For each real provider, seed data through that provider's normal write API and
then assert:

1. Subject A retrieves its known record.
2. Subject B in the same tenant does not retrieve it.
3. The same subject ID in tenant B does not retrieve it.
4. Disabling the provider produces the expected source status.
5. Matching content from two sources retains both source matches.
6. Contradictory content remains separate.
7. An abort stops underlying work only when the adapter advertises `supportsAbort: true`.

The SDK test proves federation behavior. Provider canaries prove that your
namespace mapper, client permissions, and provider data layout are correct.

## Troubleshooting

| Symptom                              | Resolution                                                                                                      |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| Expected record is absent            | Verify provider-native data shape, namespace mapping, filters, and source status.                               |
| Another subject sees a record        | Stop the rollout and repair server-owned tenant-subject resolution before further tests.                        |
| Provider outage looks like no memory | Expose the source status and keep unavailable distinct from an empty successful recall.                         |
| Delete did not remove provider data  | Use the provider's lifecycle; read-only federation and hosted-candidate deletion do not erase provider sources. |

## Best practices

* Keep provider writes and lifecycle under the provider's documented API.
* Resolve tenant and subject in trusted backend code and test 2x2 isolation.
* Preserve provenance, source matches, contradictions, and per-source status.
* Bound concurrency, result count, context bytes, and source timeout.
* Never promote checkpoint, hidden, or unverified content into portable recall.

## Optimize for production

* Query only providers and record kinds relevant to the current purpose.
* Use bounded parallel recall and give each source an explicit timeout budget.
* Deduplicate normalized content while retaining every source match and contradiction.
* Measure source p50/p95 latency, partial/failed recalls, result precision, context bytes, and isolation failures.

Optimize only after the correctness and isolation matrix passes. Lower latency or cost is not an improvement if verified outcomes, authority checks, or recovery rates regress.

## Cleanup and next steps

1. Delete synthetic provider records using the provider's normal API.
2. Hard-erase disposable hosted candidates and verify content-free receipts when used.
3. Revoke test credentials and remove namespace fixtures for every tenant and subject.
4. Retain only non-content source statuses, identifiers, and test results required for audit.

After cleanup, run the [shared integration test matrix](/tutorials/test-your-integration) and record any environment-specific check that remains pending.

## Frequently asked questions

### What proves this tutorial works?

The minimum observable result is that federated recall preserves source status, provenance, contradictions, bounds, and isolation. A compile, package import, mocked response, or initial admission alone does not prove the complete workflow.

### Can a browser, mobile app, or model prompt hold the credential?

No. Provider clients and Praxa credentials stay in the backend; adapters receive clients, not raw credentials.

### How should an ambiguous mutation be retried?

Persist the exact logical input and idempotency key before the first attempt. Reconcile through authoritative readback or replay the exact request with that same key before creating new work.

### What should we monitor after release?

Monitor per-source latency/status, partial and failed recall rates, result precision, context bytes, provenance coverage, and isolation failures. Alert on authorization bypass, cross-tenant disclosure, repeated conflicts, or cleanup failure.
