diff --git a/docs/sdk/api/errors.md b/docs/sdk/api/errors.md index 7f096d6..6747eaf 100644 --- a/docs/sdk/api/errors.md +++ b/docs/sdk/api/errors.md @@ -33,11 +33,11 @@ import { } from '@atomicmemory/atomicmemory-sdk'; try { - await sdk.package(request, site); + await memory.package(request); } catch (err) { if (err instanceof UnsupportedOperationError) { // Provider does not support packaging; fall back - const page = await sdk.search(request, site); + const page = await memory.search(request); return formatFallback(page.results); } if (err instanceof NetworkError) { diff --git a/docs/sdk/api/memory-provider.md b/docs/sdk/api/memory-provider.md index c07ddbf..29a8aab 100644 --- a/docs/sdk/api/memory-provider.md +++ b/docs/sdk/api/memory-provider.md @@ -13,8 +13,8 @@ Every provider must implement these methods. No exceptions. | Method | Obligation | |---|---| -| `name: string` | Stable identifier used for registry lookup and capability reports. Match the key in `context.providers`. | -| `ingest(input)` | Accept one of the declared `ingestModes` (`text`, `message`, `memory`). Return `IngestResult` describing what was created / updated / unchanged. | +| `name: string` | Stable identifier used for registry lookup and capability reports. Match the key used in `MemoryClient`'s `providers` config. | +| `ingest(input)` | Accept one of the declared `ingestModes` (`text`, `messages`, `memory`). Return `IngestResult` describing what was created / updated / unchanged. | | `search(request)` | Return a `SearchResultPage`. Honour `limit`, `cursor`, `scope`. Scope-invisible data must not surface. | | `get(ref)` | Return the memory or `null` — never throw for "not found". | | `delete(ref)` | Idempotent. Deleting a missing memory is not an error. | @@ -28,7 +28,7 @@ Extending `BaseMemoryProvider` is the supported path. It provides scope-validati `capabilities()` is a declaration, not a hope. Apps read it and branch. Misrepresenting capabilities breaks callers in ways that are hard to debug. - If `extensions.package` is `true`, `getExtension('package')` must return a real `Packager`. The default `BaseMemoryProvider.getExtension` returns `this` when the capability is true — so the subclass must implement `package(request)` on itself. -- If `requiredScope.user` is `true`, every request missing `scope.user` should be rejected before any network call. `BaseMemoryProvider.validateScope` does this for you when called. +- `requiredScope` is `{ default: Array, ingest?: ..., search?: ..., ... }`. If `'user'` appears in `default` (or any per-operation override), every request for that operation missing `scope.user` should be rejected before any network call. `BaseMemoryProvider.validateScope` does this for you when called. - `ingestModes` lists the shapes you accept. Rejecting an unlisted mode is fine; accepting one outside the list is not. ## Extension resolution @@ -49,12 +49,12 @@ Subclasses override `getExtension` only when they need to return a different obj | `package` | `Packager` | `package(req)` returning a token-bounded `ContextPackage` | | `temporal` | `TemporalSearch` | `searchAsOf(req)` for point-in-time queries | | `versioning` | `Versioner` | `history(ref)` returning version timeline | -| `updater` | `Updater` | `update(ref, content)` in-place | +| `update` | `Updater` | `update(ref, content)` in-place | | `graph` | `GraphSearch` | `searchGraph(req)` traversing relationships | -| `forgetter` | `Forgetter` | `forget(ref, reason)` explicit deletion with metadata | -| `profiler` | `Profiler` | `profile(scope)` generating user/scope summaries | -| `reflector` | `Reflector` | `reflect(query, scope)` generating insights | -| `batchOps` | `BatchOps` | `batchIngest(inputs)` and similar bulk APIs | +| `forget` | `Forgetter` | `forget(ref, reason)` explicit deletion with metadata | +| `profile` | `Profiler` | `profile(scope)` generating user/scope summaries | +| `reflect` | `Reflector` | `reflect(query, scope)` generating insights | +| `batch` | `BatchOps` | `batchIngest(inputs)` and similar bulk APIs | | `health` | `Health` | `health()` returning a liveness status | If you want to offer a capability not on this list, use `customExtensions` — the registry passes arbitrary objects through `getExtension(name)`. Apps that use custom extensions cast the return value to the expected type. @@ -62,9 +62,9 @@ If you want to offer a capability not on this list, use `customExtensions` — t ## Lifecycle - **`initialize?()`** — optional. Runs once after construction. The right place for health checks, warm-up, long-lived connections. -- **`close?()`** — optional. Runs during SDK teardown. Release connections, flush caches. +- **`close?()`** — optional. Release connections, flush caches. Callers invoke it directly on the provider when they need teardown; `MemoryClient` itself does not drive it. -Both are async. Both may throw; errors propagate to the caller of `AtomicMemorySDK.initialize()` / `close()`. +Both are async. Both may throw; `initialize` errors propagate to the caller of `MemoryClient.initialize()`. ## Error expectations diff --git a/docs/sdk/api/overview.md b/docs/sdk/api/overview.md index 723089c..71e0710 100644 --- a/docs/sdk/api/overview.md +++ b/docs/sdk/api/overview.md @@ -16,15 +16,15 @@ Three pages, each covering something human writing adds that generated docs cann ## What's generated -Method signatures, class members, types, and subpath exports — `AtomicMemorySDK`, `IngestInput`, `SearchRequest`, `ContextPackage`, `StorageManager`, `EmbeddingGenerator`, and the rest — are generated from the SDK's source via its `typedoc` pipeline. +Method signatures, class members, types, and subpath exports — `MemoryClient`, `IngestInput`, `SearchRequest`, `ContextPackage`, `StorageManager`, `EmbeddingGenerator`, and the rest — are generated from the SDK's source via its `typedoc` pipeline. Until the generated reference is published alongside these docs, point users at the SDK repo's types and JSDoc comments directly: -- `src/core/sdk.ts` — `AtomicMemorySDK` class +- `src/client/memory-client.ts` — `MemoryClient` class - `src/memory/types.ts` — public types (`Scope`, `MemoryRef`, `Memory`, `IngestInput`, `SearchRequest`, `SearchResultPage`, `PackageRequest`, `ContextPackage`, `Capabilities`) - `src/memory/provider.ts` — `MemoryProvider` + `BaseMemoryProvider` + extension interfaces - `src/storage/`, `src/embedding/`, `src/search/` — subpath exports -- `src/settings/` — `ExtensionSettings`, `DeveloperSettings` +- `src/browser.ts` — the slim browser entry ## Why this split diff --git a/docs/sdk/concepts/architecture.md b/docs/sdk/concepts/architecture.md index c47b176..20ec076 100644 --- a/docs/sdk/concepts/architecture.md +++ b/docs/sdk/concepts/architecture.md @@ -5,96 +5,74 @@ sidebar_position: 1 # Architecture -`AtomicMemorySDK` is a layered dispatcher. Application code talks to a single class; under the hood, policy, routing, and backend calls are separate concerns that compose cleanly. +`MemoryClient` is a layered dispatcher. Application code talks to a single class; under the hood, routing and backend calls are separate concerns that compose cleanly. ## The layers ```mermaid flowchart TB App["Host app (web, extension, Node)"] - SDK["AtomicMemorySDK\n(src/core/sdk.ts)"] - CM["ContextManager\n(capture + inject gates)"] + MC["MemoryClient\n(src/client/memory-client.ts)"] MS["MemoryService\n(provider routing)"] Reg["ProviderRegistry"] AMP["AtomicMemoryProvider → atomicmemory-core"] M0["Mem0Provider → Mem0"] Custom["Custom MemoryProvider"] - UA["UserAccountsManager"] - WS["WebSDKService"] Store["StorageManager + adapters"] Emb["EmbeddingGenerator\n(transformers.js)"] - App --> SDK - SDK --> CM - CM --> MS - SDK -.-> UA - SDK -.-> WS + App --> MC + MC --> MS MS --> Reg Reg --> AMP Reg --> M0 Reg --> Custom - SDK -.-> Store - SDK -.-> Emb AMP -->|HTTPS| Core["atomicmemory-core"] M0 -->|HTTPS| Mem0BE["Mem0 service"] + App -.-> Store + App -.-> Emb ``` -Solid arrows are the request path; dotted arrows are peer subsystems the SDK composes. Four responsibilities are kept separate on purpose: +Solid arrows are the request path; dotted arrows are peer subsystems that applications compose directly via subpath exports. | Layer | Responsibility | Source of truth | |---|---|---| -| `AtomicMemorySDK` | Public surface. Config validation, lifecycle, facade for all operations. | `src/core/sdk.ts` | -| `ContextManager` | Policy enforcement — capture gating and injection gating before any backend call. | `src/core/context-manager.ts` | +| `MemoryClient` | Public surface. Config validation, lifecycle, facade for all memory operations. | `src/client/memory-client.ts` | | `MemoryService` | Provider routing. Picks the right provider per operation, runs any op-level pipeline. | `src/memory/memory-service.ts` | | `MemoryProvider` | Backend I/O. HTTP calls, mapping to/from wire format, declaring capabilities. | `src/memory/provider.ts` and per-provider directories | -Peer subsystems — storage adapters, embedding generation, user accounts, the web SDK helpers — are composed by `AtomicMemorySDK` but do not sit on the request path. They run alongside. +`StorageManager` and `EmbeddingGenerator` are standalone subsystems reachable through the `./storage` and `./embedding` subpath exports. They run alongside `MemoryClient` when an application needs client-side persistence or local embeddings, and are independent of the memory backend. ## Ingest, end to end ```mermaid sequenceDiagram participant App - participant SDK as AtomicMemorySDK - participant CM as ContextManager - participant UA as UserAccountsManager + participant MC as MemoryClient participant MS as MemoryService participant P as Provider participant BE as Backend - App->>SDK: ingest(input, platform) - SDK->>CM: ingest(input, platform) - CM->>UA: shouldCaptureFromPlatform(platform) - alt blocked - UA-->>CM: false - CM-->>SDK: void - SDK-->>App: undefined - else allowed - UA-->>CM: true - CM->>MS: ingest(input) - MS->>P: ingest(input) - P->>BE: POST /memories - BE-->>P: {id, status} - P-->>MS: IngestResult - MS-->>SDK: IngestResult - SDK-->>App: IngestResult - end + App->>MC: ingest(input) + MC->>MS: ingest(input) + MS->>P: ingest(input) + P->>BE: POST /v1/memories/ingest + BE-->>P: {id, status} + P-->>MS: IngestResult + MS-->>MC: IngestResult + MC-->>App: IngestResult ``` -The gate comes first. If the user has disabled capture from the originating platform, the operation returns `undefined` and nothing reaches the backend — the SDK emits a `captureBlocked` event so instrumentation can observe the decision. - -Search follows the same shape with a different gate (injection). See [Consent and gating](/sdk/concepts/consent-and-gating) for the full search sequence. +Search follows the same shape. Every operation on `MemoryClient` delegates to `MemoryService`, which resolves the active provider and dispatches. ## Why layered -The separation matters for three reasons: - -1. **Swappable backends.** `MemoryService` does not know the wire format. `MemoryProvider` does not know the user's capture preferences. Each layer can change independently, which is what makes backend-agnosticism real and not aspirational. -2. **Clear policy boundary.** Every gate lives in `ContextManager`. There is exactly one place to audit capture and injection decisions, and exactly one place to extend them. -3. **Testable in isolation.** Providers are tested against the `MemoryProvider` contract alone. Policy is tested against `ContextManager` with stub providers. The top-level SDK has a thin integration layer and is tested mostly for lifecycle. +1. **Swappable backends.** `MemoryService` does not know the wire format. `MemoryProvider` does not know the operation-level pipeline. Each layer can change independently, which is what makes backend-agnosticism real and not aspirational. +2. **One integration seam.** Every backend implements `MemoryProvider`. Adding a new backend is a matter of subclassing `BaseMemoryProvider` and registering it. +3. **Testable in isolation.** Providers are tested against the `MemoryProvider` contract alone. The top-level `MemoryClient` has a thin integration layer and is tested mostly for lifecycle and routing. ## Where to go next - [Provider model](/sdk/concepts/provider-model) — the `MemoryProvider` interface and the extension system -- [Consent and gating](/sdk/concepts/consent-and-gating) — what `ContextManager` actually enforces -- [Scopes and identity](/sdk/concepts/scopes-and-identity) — how `UserAccountsManager` resolves who the caller is +- [Scopes and identity](/sdk/concepts/scopes-and-identity) — the `Scope` shape every operation carries +- [Capabilities](/sdk/concepts/capabilities) — how to probe what a provider supports diff --git a/docs/sdk/concepts/capabilities.md b/docs/sdk/concepts/capabilities.md index 59adbdd..93acf49 100644 --- a/docs/sdk/concepts/capabilities.md +++ b/docs/sdk/concepts/capabilities.md @@ -12,10 +12,10 @@ Not every memory backend supports every operation. `atomicmemory-core` ships con Every provider returns a `Capabilities` object from its `capabilities()` method, declaring what it supports: ```typescript -const caps = sdk.capabilities(); +const caps = memory.capabilities(); // { -// ingestModes: ['text', 'message', 'memory'], -// requiredScope: { user: true, agent: false, namespace: false, thread: false }, +// ingestModes: ['text', 'messages', 'memory'], +// requiredScope: { default: ['user'], ingest: ['user', 'namespace'] }, // extensions: { package: true, temporal: true, versioning: true, health: true, ... }, // customExtensions: { ... }, // } @@ -24,8 +24,8 @@ const caps = sdk.capabilities(); Three fields matter in practice: - **`ingestModes`** — which shapes of input the provider accepts. Most providers accept all three; some specialize. -- **`requiredScope`** — which scope fields must be present in a request. For example, a workspace-only provider might require `namespace`; a personal-memory provider might only need `user`. -- **`extensions`** — a record of capability keys to booleans. `package`, `temporal`, `versioning`, `updater`, `graph`, `forgetter`, `profiler`, `reflector`, `batchOps`, `health`. If `extensions.package` is `true`, the provider supports `sdk.package()`. If `false`, calling `sdk.package()` raises `UnsupportedOperationError`. +- **`requiredScope`** — which scope fields must be present in a request. A `default` array lists the fields required across all operations, with optional per-operation overrides keyed by operation name (`ingest`, `search`, `get`, `delete`, `list`, `package`, `update`, ...). A workspace-only provider might require `namespace` in `default`; a personal-memory provider might require only `user`. +- **`extensions`** — a record of capability keys to booleans. `package`, `temporal`, `versioning`, `update`, `graph`, `forget`, `profile`, `reflect`, `batch`, `health`. If `extensions.package` is `true`, the provider supports `memory.package()`. If `false`, calling `memory.package()` raises `UnsupportedOperationError`. ## The extension probe @@ -45,19 +45,19 @@ The extension key matches the capability key (`'package'`, not `'packager'`) — ## Why apps must check -An app that calls `sdk.package()` without first confirming the capability is declaring a dependency on a specific backend. That's fine if you know you're always talking to `atomicmemory-core`, but it defeats the purpose of the SDK's backend-agnostic design. +An app that calls `memory.package()` without first confirming the capability is declaring a dependency on a specific backend. That's fine if you know you're always talking to `atomicmemory-core`, but it defeats the purpose of the SDK's backend-agnostic design. The recommended pattern for portable code: ```typescript -const { extensions } = sdk.capabilities(); +const { extensions } = memory.capabilities(); if (extensions.package) { - const pkg = await sdk.package(request, site); + const pkg = await memory.package(request); return pkg.text; } else { // Graceful degradation: fall back to search + local formatting - const page = await sdk.search(request, site); + const page = await memory.search(request); return formatResults(page.results); } ``` diff --git a/docs/sdk/concepts/consent-and-gating.md b/docs/sdk/concepts/consent-and-gating.md deleted file mode 100644 index f18d3ed..0000000 --- a/docs/sdk/concepts/consent-and-gating.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: Consent and gating -sidebar_position: 7 ---- - -# Consent and gating - -The SDK enforces two policies before any memory operation touches a backend: **capture gating** (when writing) and **injection gating** (when reading). Both live in `ContextManager` and both consult the user's preferences through `UserAccountsManager`. - -Gating is not a feature you turn on. It is the default path. `AtomicMemorySDK.ingest()` and `AtomicMemorySDK.search()` always go through it. - -## Capture gate (on ingest) - -On every `ingest(input, platform)` call, `ContextManager` asks `shouldCaptureFromPlatform(normalizedPlatform)`. If the user has disabled capture from that platform, the operation: - -- Does **not** reach the provider -- Returns `undefined` -- Emits a `captureBlocked` event with `{ platform, reason }` - -There is no error. Blocked capture is a normal, expected outcome, and applications should treat it as such. - -## Injection gate (on search and package) - -`search(request, site)` and `package(request, site)` go through a different gate: `evaluateInjectionGate(query, site)`. The gate returns `{ allowed: boolean }`. When a site is blocked: - -```mermaid -sequenceDiagram - participant App - participant SDK as AtomicMemorySDK - participant CM as ContextManager - participant UA as UserAccountsManager - participant P as Provider - - App->>SDK: search(req, site) - SDK->>CM: search(req, site) - CM->>UA: evaluateInjectionGate(query, site) - alt site blocked - UA-->>CM: { allowed: false } - CM-->>SDK: { results: [] } - SDK-->>App: SearchResultPage { results: [] } - else site allowed - UA-->>CM: { allowed: true } - CM->>P: search(req) - P-->>CM: SearchResultPage - CM-->>SDK: SearchResultPage - SDK-->>App: SearchResultPage - end -``` - -The key shape detail: a blocked search returns a `SearchResultPage` with `results: []`, **not** a thrown exception and not a distinct "blocked" object. Design UI around the empty-results path — show nothing, show a rationale, or fall back to a non-gated source; do not rely on a catch block. - -## `search` vs `searchDirect` - -The SDK exposes two search entry points on purpose: - -| Method | Gates | When to use | -|---|---|---| -| **`search(request, site)`** | Yes — injection gate | Default. Every application call should use this unless you own the gate yourself. | -| **`searchDirect(request)`** | No | Callers that already enforce their own injection policy (browser extensions that have inspected the page and decided injection is safe before asking the SDK). | - -> **Use `search` by default.** `searchDirect` exists for callers that enforce their own injection gate — do not use it in general application code. If you are unsure which you need, you need `search`. - -## The `isCapturePaused` helper - -The `/consent` subpath export ships one small helper: `isCapturePaused(pausedUntil)`. It returns `true` when the given ISO timestamp is in the future. Applications that manage their own pause UX read the current `pausedUntil` from settings and check with this function before presenting capture controls. - -```typescript -import { isCapturePaused } from '@atomicmemory/atomicmemory-sdk/consent'; - -if (isCapturePaused(settings.pausedUntil)) { - // show "capture paused until X" UI -} -``` - -The SDK's built-in capture gate already consults pause state — this helper exists for apps that need to reflect that state in their own UI without reimplementing the check. - -## Designing around the gates - -Two patterns work well: - -- **Silent drop.** Accept `undefined` from `ingest` and empty results from `search` as legitimate outcomes. Show user-facing affordances to reconfigure capture or injection rules. -- **Introspect before calling.** For apps that want to disable UI affordances preemptively, read the user's preferences directly through `UserAccountsManager.getPreferences()` before displaying a "save to memory" button. - -Either pattern is fine. The wrong pattern is wrapping every call in a try/catch expecting a block error — there isn't one. - -## Next - -- [Scopes and identity](/sdk/concepts/scopes-and-identity) — where the preferences that drive these gates come from -- [Architecture](/sdk/concepts/architecture) — where `ContextManager` sits in the layered dispatcher diff --git a/docs/sdk/concepts/embeddings.md b/docs/sdk/concepts/embeddings.md index f68cd34..5f3d55a 100644 --- a/docs/sdk/concepts/embeddings.md +++ b/docs/sdk/concepts/embeddings.md @@ -7,7 +7,7 @@ sidebar_position: 6 The SDK ships **local** embedding generation through `@huggingface/transformers`. Applications can embed text in the browser, in a worker, or in Node — no API calls, no round trip, no per-token cost. -This is the `/embedding` subpath export. It's independent of `AtomicMemorySDK`; the top-level class does not use the embedding module directly (backends embed server-side). The `EmbeddingGenerator` exists for apps that want client-side semantic search over their own data. +This is the `/embedding` subpath export. It's independent of `MemoryClient`; the top-level client does not use the embedding module directly (backends embed server-side). The `EmbeddingGenerator` exists for apps that want client-side semantic search over their own data. ## The pipeline @@ -62,7 +62,7 @@ The default transformers.js build loads its WASM runtime from the CDN. For offli - Offline demos - Combining embeddings from the same model as the server for consistency -If you're using `AtomicMemorySDK` with `atomicmemory-core` or Mem0, you probably do **not** need this module — the backend embeds for you. +If you're using `MemoryClient` with `atomicmemory-core` or Mem0, you probably do **not** need this module — the backend embeds for you. ## Next diff --git a/docs/sdk/concepts/provider-model.md b/docs/sdk/concepts/provider-model.md index 8d7774b..f3c0b28 100644 --- a/docs/sdk/concepts/provider-model.md +++ b/docs/sdk/concepts/provider-model.md @@ -5,9 +5,9 @@ sidebar_position: 2 # Memory providers -> **Disambiguation.** On this page, "provider" means a **memory backend** — a concrete implementation of the `MemoryProvider` interface that the SDK routes operations through. Core's [providers](/platform/providers) page is about **embedding and LLM providers** inside the engine (OpenAI, Ollama, etc.). Different layer, different concept. +> **Disambiguation.** On this page, "provider" means a **memory backend** — a concrete implementation of the `MemoryProvider` interface that `MemoryClient` routes operations through. Core's [providers](/platform/providers) page is about **embedding and LLM providers** inside the engine (OpenAI, Ollama, etc.). Different layer, different concept. -The provider model is the thing that makes `AtomicMemorySDK` backend-agnostic. It has three pieces: an interface every backend implements, a registry the SDK consults at init time, and an extension system for backend-specific capabilities. +The provider model is what makes `MemoryClient` backend-agnostic. It has three pieces: an interface every backend implements, a registry the client consults at init time, and an extension system for backend-specific capabilities. ## The interface @@ -51,50 +51,50 @@ classDiagram Every backend implements the same six core operations: `ingest`, `search`, `get`, `delete`, `list`, `capabilities`. Beyond the core, the provider may opt into any of a fixed menu of **extensions** — `Packager`, `TemporalSearch`, `Versioner`, `Updater`, `GraphSearch`, `Forgetter`, `Profiler`, `Reflector`, `BatchOps`, `Health` — and declare which ones it supports through its `Capabilities` object. -This is why capabilities are runtime-queryable: an app that wants to use `sdk.package()` must first check that the active provider supports the `package` extension, because not every backend does. +This is why capabilities are runtime-queryable: an app that wants to use `memory.package()` must first check that the active provider supports the `package` extension, because not every backend does. ## The registry -Providers are instantiated at init time from config. The registry (`src/memory/providers/registry.ts`) maps provider names to factory functions. The default registry includes `atomicmemory` and `mem0`: +Providers are instantiated at init time from the `providers` config. The registry (`src/memory/providers/registry.ts`) maps provider names to factory functions. The default registry includes `atomicmemory` and `mem0`: ```typescript -context: { +new MemoryClient({ providers: { - default: 'atomicmemory', atomicmemory: { apiUrl: 'http://localhost:3050' }, mem0: { apiUrl: 'http://localhost:8000' }, }, -} + defaultProvider: 'atomicmemory', +}); ``` -`default` names the provider every operation routes to unless overridden. A custom provider is registered the same way — see [Writing a custom provider](/sdk/guides/custom-provider). +`defaultProvider` names the provider every operation routes to unless overridden. When only one provider is configured, it is the default implicitly. A custom provider is registered the same way — see [Writing a custom provider](/sdk/guides/custom-provider). ## Extensions: the probe pattern -When an app calls `sdk.package()`, `MemoryService` asks the active provider "do you implement the `package` extension?" via `getExtension('package')`: +When an app calls `memory.package()`, `MemoryService` asks the active provider "do you implement the `package` extension?" via `getExtension('package')`: ```mermaid sequenceDiagram participant App - participant SDK as AtomicMemorySDK + participant MC as MemoryClient participant MS as MemoryService participant P as Provider participant Pk as Packager ext - App->>SDK: package(req) - SDK->>MS: package(req) + App->>MC: package(req) + MC->>MS: package(req) MS->>P: getExtension('package') alt supported P-->>MS: Packager impl MS->>Pk: package(req) Pk->>Pk: rank + fit to tokenBudget Pk-->>MS: ContextPackage - MS-->>SDK: ContextPackage + MS-->>MC: ContextPackage else not supported P-->>MS: undefined - MS-->>SDK: UnsupportedOperationError + MS-->>MC: UnsupportedOperationError end - SDK-->>App: ContextPackage | error + MC-->>App: ContextPackage | error ``` The key is named after the **capability** (`'package'`), not the interface type (`Packager`). A provider either returns a value that satisfies the `Packager` interface or returns `undefined`, in which case `MemoryService` raises `UnsupportedOperationError`. @@ -111,4 +111,4 @@ A third provider is always possible — you write it. See [Writing a custom prov ## Next - [Capabilities](/sdk/concepts/capabilities) — how to query what a provider supports before calling an extension -- [Scopes and identity](/sdk/concepts/scopes-and-identity) — the identity model every provider receives +- [Scopes and identity](/sdk/concepts/scopes-and-identity) — the scope model every provider receives diff --git a/docs/sdk/concepts/scopes-and-identity.md b/docs/sdk/concepts/scopes-and-identity.md index bf74027..32011d5 100644 --- a/docs/sdk/concepts/scopes-and-identity.md +++ b/docs/sdk/concepts/scopes-and-identity.md @@ -5,9 +5,9 @@ sidebar_position: 4 # Scopes and identity -Every SDK operation carries a **scope** — the partition the memory lives in. Scope is what separates Alice's memory from Bob's, the personal workspace from the team workspace, one agent's lessons from another. The SDK's scope type and Core's scope model align, but the SDK adds a layer on top: identity resolution and platform normalization. +Every SDK operation carries a **scope** — the partition the memory lives in. Scope is what separates Alice's memory from Bob's, the personal workspace from the team workspace, one agent's lessons from another. -> Core is canonical for the HTTP-level scope semantics. See [platform/scope](/platform/scope) for the wire-format view. This page covers what the SDK adds on the client side. +> Core is canonical for the HTTP-level scope semantics. See [platform/scope](/platform/scope) for the wire-format view. This page covers what the SDK presents on the client side. ## The `Scope` type @@ -22,48 +22,22 @@ type Scope = { Every `ingest`, `search`, `get`, `delete`, `list`, and `package` request includes a scope. The fields are additive: the more you specify, the narrower the partition. -The minimum required scope is determined by the active provider's `capabilities().requiredScope`. A personal-memory provider might require only `user`; a workspace-oriented provider might require `user` + `namespace`. +The minimum required scope is determined by the active provider's `capabilities().requiredScope`, which declares a `default` list of required fields plus optional per-operation overrides. A personal-memory provider might require only `['user']`; a workspace-oriented provider might require `['user', 'namespace']` on ingest. -## Identity resolution +## Where identity comes from -Applications usually don't hard-code `user` / `namespace` strings — they look them up from a session. That lookup is `UserAccountsManager`'s job. - -`UserAccountsManager` is configured at SDK construction time: +`MemoryClient` does not resolve user identity. Applications pass whatever `user` / `namespace` strings their own session layer produces: ```typescript -new AtomicMemorySDK({ - userAccounts: { - identity: { - providerType: 'web2', - apiBaseUrl: 'http://localhost:8787', - testMode: true, - }, - preferences: { - providerType: 'web2', - apiBaseUrl: 'http://localhost:8787', - testMode: true, - encryption: { keySource: 'provided', key: new Uint8Array(32) }, - }, - }, - context: { /* ... */ }, +await memory.ingest({ + mode: 'text', + content: 'Prefers aisle seats.', + scope: { user: currentSession.userId }, + provenance: { source: 'manual' }, }); ``` -Three modes are supported: - -| Mode | When to use | Behaviour | -|---|---|---| -| **`testMode: true`** | Local development, tests, demos | Installs a hardcoded test provider with permissive defaults. No network calls for identity. | -| **Hardcoded provider** | Simple apps with a single known user | Returns a fixed identity. Useful for CLIs and server-side jobs. | -| **Real web2 provider** | Production browser / web apps | Calls the configured identity and preferences services over HTTP. | - -`ContextManager` assumes a `UserAccountsManager` is present for `ingest` (which runs the capture gate). Operations that do not gate — pure `searchDirect`, operations under the subpath-primitive topology — do not require one. - -## Platform normalization - -In browser contexts, the originating **platform** (e.g., `chatgpt.com`) is the key the user's capture rules are keyed on. Real platform strings vary (`chat.openai.com`, `chatgpt.com`, with or without subdomain, with or without trailing slash). The SDK normalizes these through `normalizePlatformId` so rules match reliably. - -That normalization is internal, but it matters for understanding what `shouldCaptureFromPlatform(platform)` is actually comparing. +Where `currentSession.userId` comes from — an auth service, a JWT, a hard-coded identifier for a CLI tool — is an application concern. The SDK treats the string as opaque and passes it to the active provider. ## Scope patterns @@ -76,7 +50,11 @@ That normalization is internal, but it matters for understanding what `shouldCap A memory written at `{ user: 'alice', namespace: 'team-acme' }` is not visible to a search at `{ user: 'alice' }` unless the provider opts into upward-narrowing (most do not, on purpose — this is what makes the isolation real). +## Scope validation + +`BaseMemoryProvider.validateScope` rejects requests that are missing scope fields the provider declared required, before any network call is made. Custom providers extend this by overriding `capabilities().requiredScope` to reflect their own partitioning rules. + ## Next -- [Consent and gating](/sdk/concepts/consent-and-gating) — how `UserAccountsManager` provides the preferences the gates consult +- [Provider model](/sdk/concepts/provider-model) — how each provider declares its `requiredScope` - Core's [platform/scope](/platform/scope) — the HTTP-level scope model and visibility rules diff --git a/docs/sdk/concepts/storage-adapters.md b/docs/sdk/concepts/storage-adapters.md index 2d37dc6..d75adf4 100644 --- a/docs/sdk/concepts/storage-adapters.md +++ b/docs/sdk/concepts/storage-adapters.md @@ -5,15 +5,15 @@ sidebar_position: 5 # Storage adapters -The SDK's storage subsystem is a thin, resilient abstraction over key-value storage. It lives behind the `/storage` subpath export and is used both by the SDK internals (embedding cache, settings persistence) and directly by applications that need persistent client-side state. +The SDK's storage subsystem is a thin, resilient abstraction over key-value storage. It lives behind the `/storage` subpath export and is intended for applications that need persistent client-side state — for example, to pair with the `/embedding` and `/search` primitives in a no-backend topology. -Storage is a separate concern from the memory **backend**. The backend holds your memories; storage holds local data — caches, settings, queued writes, bundled knowledge bases. +Storage is a separate concern from the memory **backend**. The backend holds your memories; storage holds local data — caches, application state, queued writes, bundled knowledge bases. `MemoryClient` does not use `StorageManager` internally. ## The subsystem ```mermaid flowchart LR - SDK[AtomicMemorySDK] --> SM[StorageManager] + App[App code] --> SM[StorageManager] SM --> Res[Resilience] Res --> Retry[RetryEngine] Res --> CB[CircuitBreaker] @@ -33,7 +33,7 @@ Three parts matter: ## Shipped adapters -The SDK exports two adapters out of the box (`src/index.ts:44-48`): +The SDK exports two adapters out of the box: | Adapter | When to use | Notes | |---|---|---| @@ -113,4 +113,4 @@ All of this is opt-out, not opt-in. Applications that want stricter failure sema ## Next - [Embeddings](/sdk/concepts/embeddings) — which uses IndexedDB under the hood for its model cache -- [Browser primitives](/sdk/guides/browser-primitives) — composing storage with embeddings and search outside `AtomicMemorySDK` +- [Browser primitives](/sdk/guides/browser-primitives) — composing storage with embeddings and search outside `MemoryClient` diff --git a/docs/sdk/cookbook/knowledge-base.md b/docs/sdk/cookbook/knowledge-base.md deleted file mode 100644 index 4cf1b73..0000000 --- a/docs/sdk/cookbook/knowledge-base.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Shipping a knowledge base -sidebar_position: 1 ---- - -# Shipping a knowledge base - -`AtomicMemorySDK` can load a **knowledge base** — a bundled set of memories shipped with your application — into the active provider on first init. Useful when your product ships with seed content (onboarding hints, product docs, reference snippets) that you want searchable from the moment the user opens it. - -## The method - -```typescript -await sdk.loadKnowledgeBase('production'); -// or -await sdk.loadKnowledgeBase('development'); -await sdk.loadKnowledgeBase('test'); -``` - -The `type` parameter selects which bundle to load — `production`, `development`, or `test`. Each is defined under `src/knowledge-base/` in the SDK source. - -## When to call it - -Load once, early in your app's lifecycle, after `initialize` has completed. Typical pattern: - -```typescript -const sdk = new AtomicMemorySDK({ /* config */ }); -await sdk.initialize(); - -if (!(await hasSeedContent(sdk))) { - await sdk.loadKnowledgeBase('production'); -} -``` - -Where `hasSeedContent` is your own idempotency check — the simplest version is a list with a known `scope` tag like `{ namespace: 'seed' }` and a count check. - -## What gets loaded - -The bundled knowledge bases are plain memory records: `content`, `scope`, `provenance`. They go through `ingest` like any other memory, which means: - -- They pass through the capture gate (so `platform: 'knowledge-base'` must not be blocked in the user's preferences, or you need to use a provider-direct path) -- They get embedded, AUDN-checked, and stored the same way user memories are -- They appear in `list` and `search` results - -## Scoping seed content - -The convention is to give seed memories a dedicated `namespace` like `'seed'` or `'knowledge-base'` so they're easy to identify and prune later: - -```typescript -// Inside your seed data (bundled in SDK or your app) -{ - content: '...', - scope: { user: 'system', namespace: 'seed' }, - provenance: { source: 'knowledge-base' }, -} -``` - -Apps can then search against `{ namespace: 'seed' }` when they want seed-only hits, or the user's own scope when they want personal memory. - -## Eviction and updates - -`loadKnowledgeBase` does not automatically reconcile against previous loads. If your seed content changes between versions, plan for that: - -- **Additive updates** — add new records, let old ones stay. Simplest. -- **Replacement** — delete by `namespace` first, then reload. Requires keeping stable identifiers for the scope. -- **Versioning** — namespace each load (`seed-v1`, `seed-v2`), and configure your app to prefer the latest. Most robust, most code. - -## Next - -- [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend) — the provider that will receive your seed content -- [Scopes and identity](/sdk/concepts/scopes-and-identity) — why namespacing seed data matters diff --git a/docs/sdk/guides/atomicmemory-backend.md b/docs/sdk/guides/atomicmemory-backend.md index d06dbc5..a69e8c6 100644 --- a/docs/sdk/guides/atomicmemory-backend.md +++ b/docs/sdk/guides/atomicmemory-backend.md @@ -10,24 +10,20 @@ sidebar_position: 1 ## Wire it up ```typescript -import { AtomicMemorySDK } from '@atomicmemory/atomicmemory-sdk'; - -const sdk = new AtomicMemorySDK({ - userAccounts: { /* see Scopes and identity */ }, - context: { - providers: { - default: 'atomicmemory', - atomicmemory: { - apiUrl: 'https://core.example.com', - apiKey: process.env.CORE_API_KEY, // optional - timeout: 30000, // ms; optional - apiVersion: 'v1', // default 'v1' - }, +import { MemoryClient } from '@atomicmemory/atomicmemory-sdk'; + +const memory = new MemoryClient({ + providers: { + atomicmemory: { + apiUrl: 'https://core.example.com', + apiKey: process.env.CORE_API_KEY, // optional + timeout: 30000, // ms; optional + apiVersion: 'v1', // default 'v1' }, }, }); -await sdk.initialize(); +await memory.initialize(); ``` ## Health check @@ -35,11 +31,10 @@ await sdk.initialize(); Before traffic flows, confirm the backend is reachable: ```typescript -const health = sdk.capabilities(); -// Inspect health through the Health extension: -const healthExt = sdk.getProviderStatus(); -console.log(healthExt); -// { current: 'atomicmemory', available: ['atomicmemory'] } +const caps = memory.capabilities(); +const status = memory.getProviderStatus(); +console.log(status); +// [{ name: 'atomicmemory', initialized: true, capabilities: { ... } }] ``` For a deeper liveness check, hit the core `/v1/memories/health` endpoint directly — it returns the full config snapshot (embedding / LLM provider, thresholds). @@ -47,20 +42,18 @@ For a deeper liveness check, hit the core `/v1/memories/health` endpoint directl ## One ingest, one search ```typescript -await sdk.ingest( - { - mode: 'text', - content: 'Prefer gRPC over REST for internal service-to-service calls.', - scope: { user: 'alice' }, - provenance: { source: 'manual' }, - }, - 'internal-wiki', -); +await memory.ingest({ + mode: 'text', + content: 'Prefer gRPC over REST for internal service-to-service calls.', + scope: { user: 'alice' }, + provenance: { source: 'manual' }, +}); -const page = await sdk.search( - { query: 'internal service communication', scope: { user: 'alice' }, limit: 5 }, - 'internal-wiki', -); +const page = await memory.search({ + query: 'internal service communication', + scope: { user: 'alice' }, + limit: 5, +}); ``` Every SDK method has a corresponding HTTP endpoint on core. Useful if you're debugging wire traffic or comparing behaviour across clients: @@ -81,10 +74,10 @@ Every SDK method has a corresponding HTTP endpoint on core. Useful if you're deb - `package` — context packaging with token budgets - `temporal` — `searchAsOf` for point-in-time queries - `versioning` — `history()` per memory ref -- `updater` — in-place updates +- `update` — in-place updates - `health` — liveness probe -Run `sdk.capabilities()` at init and cache the result — you'll know exactly what's available without calling around. +Run `memory.capabilities()` at init and cache the result — you'll know exactly what's available without calling around. ## Production checklist diff --git a/docs/sdk/guides/browser-helpers.md b/docs/sdk/guides/browser-helpers.md index cca953c..2b19530 100644 --- a/docs/sdk/guides/browser-helpers.md +++ b/docs/sdk/guides/browser-helpers.md @@ -1,67 +1,39 @@ --- -title: Browser helpers +title: Browser entry sidebar_position: 4 --- -# Browser helpers +# Browser entry -The SDK ships a small set of browser-oriented exports for applications that run in the browser (web apps, extensions, PWAs). This guide covers what those exports actually are — and, just as important, what they are not. +The SDK ships a slim entry point for applications that run in the browser against a backend-hosted memory engine. This guide covers what that entry exports and when to reach for it over the root package. -## What the SDK exports for browsers - -| Export | Subpath | Purpose | -|---|---|---| -| `WebSDKService` | main | Browser-facing helper service composed by `AtomicMemorySDK` | -| `ExtensionSettings` schema | `/settings` | Public schema for webapp-synced capture rules and privacy settings | -| `DeveloperSettings` schema | `/settings` | Public schema for extension-only debug / testing settings | -| `isCapturePaused(pausedUntil)` | `/consent` | Pure function: returns `true` when the given ISO timestamp is in the future | -| `IndexedDBStorageAdapter` | `/storage` | Browser-native IndexedDB adapter for the storage subsystem | - -That's the whole public browser surface. - -## Settings schemas - -`ExtensionSettings` is the shape of user preferences that are synced from the web app — capture rules, domain allowlists, privacy flags. `DeveloperSettings` is for extension-only flags that never leave the client. Both are validated with `zod` schemas exported from the `/settings` subpath: +## The `./browser` subpath ```typescript -import { - ExtensionSettings, - DeveloperSettings, -} from '@atomicmemory/atomicmemory-sdk/settings'; - -const parsed = ExtensionSettings.parse(rawJson); +import { MemoryClient } from '@atomicmemory/atomicmemory-sdk/browser'; ``` -Use these schemas at the boundaries where settings enter your app: on load from storage, on receipt from the sync service. Let the schemas be the validation gate; don't reinvent them. - -## The consent helper +The `./browser` entry exports `MemoryClient`, the `MemoryProvider` interface and base class, both shipped provider adapters, and the core types. It intentionally omits `storage`, `embedding`, `search`, and `utils` — three bundles that pull in IndexedDB helpers, the transformers WASM runtime, and their dependencies. If your app talks to a remote backend and has no need for on-device persistence or local embeddings, this is the smaller import. -```typescript -import { isCapturePaused } from '@atomicmemory/atomicmemory-sdk/consent'; - -if (isCapturePaused(settings.pausedUntil)) { - showPausedUI(); -} -``` +## When to use `./browser` -Twenty lines, one function. The SDK's capture gate already consults pause state — this helper exists so your UI can reflect it without reimplementing the check. +- You're building a browser app or extension that calls `atomicmemory-core` or a remote Mem0 instance. +- The backend handles embeddings and storage server-side. +- Bundle size matters. -## Chrome extension storage - -The SDK does **not** ship a `ChromeStorageAdapter`. If you want to back storage with `chrome.storage.local`, write a custom adapter — see the "Writing a custom adapter" section of [Storage adapters](/sdk/concepts/storage-adapters). The interface is eight methods and the implementation is straightforward. +## When to use the root package -## What this guide deliberately does not cover +- You want client-side semantic search over bundled data without a backend — see [Browser primitives](/sdk/guides/browser-primitives). +- You're composing `StorageManager`, `EmbeddingGenerator`, or `SemanticSearch` directly for custom pipelines. +- You want one import surface that covers both the client and the primitives. -This page describes the SDK's **public contract** for browser consumers. It does not prescribe: +Both entries expose the same `MemoryClient`. The root re-exports everything from `./browser` plus the additional subsystems. -- How to structure a Chrome extension (offscreen documents, manifest v3, background workers) -- Where to instantiate `AtomicMemorySDK` in your extension's architecture -- How to route messages between content scripts and background pages -- Consumption patterns specific to any particular product +## Chrome extension storage -Those are application-architecture concerns. The SDK gives you the exports; you make the architectural calls that fit your product. +The SDK does **not** ship a `ChromeStorageAdapter`. If you want to back storage with `chrome.storage.local`, write a custom adapter — see [Storage adapters](/sdk/concepts/storage-adapters). The interface is small. ## Next +- [Browser primitives](/sdk/guides/browser-primitives) — composing the subpath exports for on-device semantic search - [Storage adapters](/sdk/concepts/storage-adapters) — the interface you implement for Chrome-extension storage -- [Consent and gating](/sdk/concepts/consent-and-gating) — how capture and injection gates consume the settings schemas above diff --git a/docs/sdk/guides/browser-primitives.md b/docs/sdk/guides/browser-primitives.md index d31d0bf..49aab03 100644 --- a/docs/sdk/guides/browser-primitives.md +++ b/docs/sdk/guides/browser-primitives.md @@ -6,10 +6,10 @@ sidebar_position: 6 # Browser primitives :::note -This guide bypasses `AtomicMemorySDK`. You get storage, local embeddings, and semantic search — but no provider, no extensions, no gating or consent layer, no remote sync. If you want a backend-agnostic client, start at [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend) instead. +This guide bypasses `MemoryClient`. You get storage, local embeddings, and semantic search — but no provider, no extensions, no remote sync. If you want a backend-agnostic client, start at [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend) instead. ::: -The `/storage`, `/embedding`, and `/search` subpath exports work on their own. You can build client-side semantic search over your app's data without ever constructing `AtomicMemorySDK`, and without a memory backend of any kind. +The `/storage`, `/embedding`, and `/search` subpath exports work on their own. You can build client-side semantic search over your app's data without ever constructing `MemoryClient`, and without a memory backend of any kind. This is the right path when: @@ -78,9 +78,9 @@ for (const hit of results.slice(0, 3)) { The exact `SemanticSearch` API differs slightly by version — import the types from `@atomicmemory/atomicmemory-sdk/search` and read the shape. The pattern is always: vectorize the query, score it against stored vectors, rank. -## When to graduate to `AtomicMemorySDK` +## When to graduate to `MemoryClient` -The moment you need any of these, switch to the top-level SDK: +The moment you need any of these, switch to the top-level client: - Remote sync to `atomicmemory-core` or Mem0 - Multi-device memory @@ -97,7 +97,6 @@ At that point, the backend owns storage and embedding; the subpath primitives dr | Zero-backend prototyping | Remote sync | | Data stays on the device | Cross-session persistence unless you use IndexedDB | | Full control of the indexing strategy | AUDN, versioning, observability | -| No identity / consent layer | No gating or capture policy | Both modes are legitimate. Pick the one that matches the constraint. diff --git a/docs/sdk/guides/custom-provider.md b/docs/sdk/guides/custom-provider.md index de4fb74..ac2a87a 100644 --- a/docs/sdk/guides/custom-provider.md +++ b/docs/sdk/guides/custom-provider.md @@ -5,7 +5,7 @@ sidebar_position: 3 # Writing a custom provider -The provider interface is deliberately small. If you want to back the SDK with something other than `atomicmemory-core` or Mem0 — an internal memory service, a Pinecone-fronted store, a SQLite-over-disk prototype — you write a `MemoryProvider` and register it. Application code keeps calling the same SDK methods. +The provider interface is deliberately small. If you want to back `MemoryClient` with something other than `atomicmemory-core` or Mem0 — an internal memory service, a Pinecone-fronted store, a SQLite-over-disk prototype — you write a `MemoryProvider` and register it. Application code keeps calling the same client methods. ## Minimum implementation @@ -34,30 +34,27 @@ export class MyProvider extends BaseMemoryProvider { capabilities(): Capabilities { return { - ingestModes: ['text', 'message', 'memory'], + ingestModes: ['text', 'messages', 'memory'], requiredScope: { - user: true, - agent: false, - namespace: false, - thread: false, + default: ['user'], }, extensions: { + update: false, package: false, temporal: false, - versioning: false, - updater: false, graph: false, - forgetter: false, - profiler: false, - reflector: false, - batchOps: false, + forget: false, + profile: false, + reflect: false, + versioning: false, + batch: false, health: true, }, customExtensions: {}, }; } - async ingest(input: IngestInput): Promise { + protected async doIngest(input: IngestInput): Promise { const res = await fetch(`${this.config.endpoint}/ingest`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -67,30 +64,27 @@ export class MyProvider extends BaseMemoryProvider { return res.json(); } - async search(request: SearchRequest): Promise { /* ... */ } - async get(ref: MemoryRef): Promise { /* ... */ } - async delete(ref: MemoryRef): Promise { /* ... */ } - async list(request: ListRequest): Promise { /* ... */ } + protected async doSearch(request: SearchRequest): Promise { /* ... */ } + protected async doGet(ref: MemoryRef): Promise { /* ... */ } + protected async doDelete(ref: MemoryRef): Promise { /* ... */ } + protected async doList(request: ListRequest): Promise { /* ... */ } } ``` -That's it. The six core methods plus `capabilities()` and the provider `name`. Everything else is opt-in. +`BaseMemoryProvider` implements the public `ingest` / `search` / `get` / `delete` / `list` methods for you — they run scope validation and error wrapping, then call the protected `do*` hooks above. You only implement the hooks plus `capabilities()` and `name`. Everything else is opt-in. ## Register the provider Providers are created by factories in the registry. The simplest path is to construct your provider and pass it in via a factory-style registration. See `src/memory/providers/registry.ts` for the shipped registry and the pattern for adding to it. -At SDK construction time, reference your provider by name in `context.providers`: +At `MemoryClient` construction time, reference your provider by name in `providers`: ```typescript -const sdk = new AtomicMemorySDK({ - userAccounts: { /* ... */ }, - context: { - providers: { - default: 'my-provider', - 'my-provider': { endpoint: 'https://memory.internal.example.com' }, - }, +const memory = new MemoryClient({ + providers: { + 'my-provider': { endpoint: 'https://memory.internal.example.com' }, }, + defaultProvider: 'my-provider', }); ``` @@ -99,7 +93,7 @@ const sdk = new AtomicMemorySDK({ Two rules: 1. **Don't lie.** If `capabilities().extensions.package` is `true`, your `getExtension('package')` must return a real `Packager`. The default `BaseMemoryProvider.getExtension` returns `this` when the capability is true and relies on the subclass implementing the extension methods on itself. -2. **Declare `requiredScope` honestly.** If your backend needs `namespace` to partition correctly, set `requiredScope.namespace: true`. `BaseMemoryProvider.validateScope` will reject requests missing required fields before they reach your network code. +2. **Declare `requiredScope` honestly.** The shape is `{ default: Array, ingest?: ..., search?: ..., ... }` — a list of required fields for each operation, falling back to `default`. If your backend needs `namespace` to partition correctly, include it in `default` (or the specific operation key). `BaseMemoryProvider.validateScope` will reject requests missing required fields before they reach your network code. ## Implementing an extension @@ -119,11 +113,11 @@ export class MyProvider extends BaseMemoryProvider implements Packager { } ``` -Set `capabilities().extensions.package = true`. Done — `sdk.package()` now works. +Set `capabilities().extensions.package = true`. Done — `memory.package()` now works. ## Testing -The SDK's own test suite uses a fixture provider (`src/memory/__tests__/memory-service.test.ts`). The pattern works for any custom provider: construct the SDK with your provider as `default`, exercise the public SDK methods, assert on observable behaviour rather than internal state. +The SDK's own test suite uses fixture providers under `src/memory/__tests__/`. The pattern works for any custom provider: construct `MemoryClient` with your provider as the default, exercise the public client methods, assert on observable behaviour rather than internal state. ## Next diff --git a/docs/sdk/guides/mem0-backend.md b/docs/sdk/guides/mem0-backend.md index 8cdd39f..bd80645 100644 --- a/docs/sdk/guides/mem0-backend.md +++ b/docs/sdk/guides/mem0-backend.md @@ -5,30 +5,26 @@ sidebar_position: 2 # Using the Mem0 backend -`Mem0Provider` is the SDK's HTTP client for [Mem0](https://mem0.ai). It demonstrates the backend-agnostic design in practice: the same SDK, the same methods, a different engine behind the interface. +`Mem0Provider` is the SDK's HTTP client for [Mem0](https://mem0.ai). It demonstrates the backend-agnostic design in practice: the same client, the same methods, a different engine behind the interface. > **Support status.** Verify the current support stance with the project before building on this path. Mem0 compatibility is exercised in tests and works for the core operations, but `Mem0Provider`'s position in the roadmap is an open question. Prefer `atomicmemory-core` when you need a first-class path. ## Wire it up ```typescript -import { AtomicMemorySDK } from '@atomicmemory/atomicmemory-sdk'; - -const sdk = new AtomicMemorySDK({ - userAccounts: { /* see Scopes and identity */ }, - context: { - providers: { - default: 'mem0', - mem0: { - apiUrl: 'http://localhost:8000', - apiKey: process.env.MEM0_API_KEY, - timeout: 30000, - }, +import { MemoryClient } from '@atomicmemory/atomicmemory-sdk'; + +const memory = new MemoryClient({ + providers: { + mem0: { + apiUrl: 'http://localhost:8000', + apiKey: process.env.MEM0_API_KEY, + timeout: 30000, }, }, }); -await sdk.initialize(); +await memory.initialize(); ``` ## Capability differences @@ -38,19 +34,19 @@ await sdk.initialize(); - `package` — context packaging with token budgets - `temporal` — point-in-time search - `versioning` — per-memory history -- `updater` — in-place updates -- `graph`, `forgetter`, `profiler`, `reflector`, `batchOps` +- `update` — in-place updates +- `graph`, `forget`, `profile`, `reflect`, `batch` -Code that calls `sdk.package()` on a Mem0-backed SDK will raise `UnsupportedOperationError`. Use the capability-probing pattern from [Capabilities](/sdk/concepts/capabilities) to handle this gracefully: +Code that calls `memory.package()` on a Mem0-backed client will raise `UnsupportedOperationError`. Use the capability-probing pattern from [Capabilities](/sdk/concepts/capabilities) to handle this gracefully: ```typescript -const { extensions } = sdk.capabilities(); +const caps = memory.capabilities(); -if (extensions.package) { - const pkg = await sdk.package(request, site); +if (caps.extensions.package) { + const pkg = await memory.package(request); injectContext(pkg.text); } else { - const page = await sdk.search(request, site); + const page = await memory.search(request); injectContext(formatFallback(page.results)); } ``` diff --git a/docs/sdk/guides/nodejs-server.md b/docs/sdk/guides/nodejs-server.md index 2f5eda0..590dfac 100644 --- a/docs/sdk/guides/nodejs-server.md +++ b/docs/sdk/guides/nodejs-server.md @@ -5,13 +5,13 @@ sidebar_position: 5 # Node.js / server-side -Using the SDK server-side is supported — the package is ESM + CJS, has no browser-only code on its critical path, and the `MemoryStorageAdapter` runs anywhere Node does. +Using the SDK server-side is supported — the package is ESM + CJS, has no browser-only code on its critical path, and the subpath storage primitives run anywhere Node does. -This guide covers the two things that differ from the browser path: storage selection and the identity requirement. +This guide covers the two things that differ from the browser path: storage selection and authorization. ## Storage -Use `MemoryStorageAdapter` for ephemeral / per-process state, or write a custom adapter backed by filesystem / Redis / Postgres. `IndexedDBStorageAdapter` is browser-only. +`IndexedDBStorageAdapter` is browser-only. Use `MemoryStorageAdapter` for ephemeral / per-process state, or write a custom adapter backed by filesystem / Redis / Postgres. ```typescript import { @@ -26,84 +26,58 @@ const storage = new StorageManager([adapter]); await storage.initialize(); ``` -## Identity: the server-side wrinkle +Note: `MemoryClient` does not use `StorageManager` internally — storage lives server-side in the active memory backend. Instantiate `StorageManager` only if your app needs its own local key-value store. -`AtomicMemorySDK.ingest()` runs through the capture gate, which consults `UserAccountsManager`. The SDK assumes a `UserAccountsManager` is configured; without one, `ingest` will throw. +## Authorization -On a server, where there's no end-user session and capture policies are usually enforced upstream, the pragmatic options are: +`MemoryClient` is a thin HTTP client over the active `MemoryProvider`. It does not enforce any gate before calling the backend. Authorization — who can call which endpoint, for which `scope.user` — is your app's responsibility. -1. **`testMode: true`.** The hardcoded test provider returns permissive defaults — capture allowed, injection allowed. Sufficient for server jobs that already enforce authorization at their own boundary. -2. **A minimal hardcoded identity provider.** Configure the user-accounts services with fixed identity + fixed preferences that reflect your server's policy. This is explicit and inspectable. -3. **Skip the facade.** If you don't need the gate, use the `MemoryProvider` directly. This is an advanced path — you lose `ContextManager`'s policy layer — but it's the honest answer for data-pipeline jobs that have no user context. +The recommended shape on a server is: -```typescript -import { AtomicMemorySDK } from '@atomicmemory/atomicmemory-sdk'; - -const sdk = new AtomicMemorySDK({ - userAccounts: { - identity: { - providerType: 'web2', - apiBaseUrl: 'http://localhost:8787', - testMode: true, - }, - preferences: { - providerType: 'web2', - apiBaseUrl: 'http://localhost:8787', - testMode: true, - encryption: { keySource: 'provided', key: new Uint8Array(32) }, - }, - }, - context: { - providers: { - default: 'atomicmemory', - atomicmemory: { apiUrl: process.env.CORE_URL! }, - }, - }, -}); - -await sdk.initialize(); -``` +1. Terminate auth at your HTTP layer (JWT, session cookie, API key). +2. Derive the caller's `scope` from the validated session. +3. Call `memory.ingest` / `memory.search` with that scope. ## An Express example ```typescript import express from 'express'; -import { AtomicMemorySDK } from '@atomicmemory/atomicmemory-sdk'; +import { MemoryClient } from '@atomicmemory/atomicmemory-sdk'; -const sdk = new AtomicMemorySDK({ /* config as above */ }); -await sdk.initialize(); +const memory = new MemoryClient({ + providers: { atomicmemory: { apiUrl: process.env.CORE_URL! } }, +}); +await memory.initialize(); const app = express(); app.use(express.json()); app.post('/memory', async (req, res) => { const { userId, content } = req.body; - await sdk.ingest( - { - mode: 'text', - content, - scope: { user: userId }, - provenance: { source: 'api' }, - }, - 'internal', - ); + await memory.ingest({ + mode: 'text', + content, + scope: { user: userId }, + provenance: { source: 'api' }, + }); res.status(204).end(); }); app.get('/memory/:userId/search', async (req, res) => { - const page = await sdk.search( - { query: req.query.q as string, scope: { user: req.params.userId }, limit: 10 }, - 'internal', - ); + const page = await memory.search({ + query: req.query.q as string, + scope: { user: req.params.userId }, + limit: 10, + }); res.json(page); }); app.listen(3000); ``` -Authorization (who can call `/memory`, for which `userId`) is your app's responsibility. The SDK's gates are about user consent, not access control. +Validating `req.params.userId` against the authenticated session is a load-bearing step — the SDK trusts whatever scope you hand it. ## Next -- [Scopes and identity](/sdk/concepts/scopes-and-identity) — the full `UserAccountsManager` story +- [Scopes and identity](/sdk/concepts/scopes-and-identity) — the `Scope` shape every operation carries - [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend) — production checklist that applies here too diff --git a/docs/sdk/guides/swapping-backends.md b/docs/sdk/guides/swapping-backends.md index e98930e..56cbe64 100644 --- a/docs/sdk/guides/swapping-backends.md +++ b/docs/sdk/guides/swapping-backends.md @@ -11,32 +11,31 @@ Two scenarios come up in practice: runtime swap (flip providers without restarti ## Runtime swap -`AtomicMemorySDK` is initialized with a single `default` provider; swapping means destroying the current SDK and constructing a new one. There is no live reconfigure path, by design — providers hold HTTP clients, caches, and init state, and a clean reconstruction is easier to reason about than partial rewiring. +`MemoryClient` is initialized with a `providers` map and an optional `defaultProvider`; swapping means destroying the current client and constructing a new one. There is no live reconfigure path, by design — providers hold HTTP clients, caches, and init state, and a clean reconstruction is easier to reason about than partial rewiring. ```typescript -async function withProvider(providerName: 'atomicmemory' | 'mem0') { - const sdk = new AtomicMemorySDK({ - userAccounts: { /* unchanged */ }, - context: { - providers: { - default: providerName, - atomicmemory: { apiUrl: 'http://localhost:3050' }, - mem0: { apiUrl: 'http://localhost:8000' }, - }, +import { MemoryClient } from '@atomicmemory/atomicmemory-sdk'; + +async function withProvider(defaultProvider: 'atomicmemory' | 'mem0') { + const memory = new MemoryClient({ + providers: { + atomicmemory: { apiUrl: 'http://localhost:3050' }, + mem0: { apiUrl: 'http://localhost:8000' }, }, + defaultProvider, }); - await sdk.initialize(); - return sdk; + await memory.initialize(); + return memory; } // Somewhere in your app -const sdk = await withProvider('atomicmemory'); -// ... use sdk ... +const memory = await withProvider('atomicmemory'); +// ... use memory ... // Switch: -const sdk2 = await withProvider('mem0'); +const memory2 = await withProvider('mem0'); ``` -If you have an in-flight operation on the old SDK, let it complete before the swap — destroying the client mid-request is not a supported path. +If you have an in-flight operation on the old client, let it complete before the swap — destroying the client mid-request is not a supported path. ## Migration: moving memories between providers @@ -44,8 +43,8 @@ Use `list` on the source, `ingest` on the target. Example: ```typescript async function migrate( - source: AtomicMemorySDK, - target: AtomicMemorySDK, + source: MemoryClient, + target: MemoryClient, scope: { user: string }, ) { let cursor: string | undefined = undefined; @@ -53,16 +52,15 @@ async function migrate( do { const page = await source.list({ scope, limit: 100, cursor }); - for (const memory of page.memories) { - await target.ingest( - { - mode: 'memory', - memory, - scope, - provenance: { source: 'migration' }, - }, - 'migration', - ); + for (const record of page.memories) { + await target.ingest({ + mode: 'memory', + content: record.content, + kind: record.kind, + metadata: record.metadata, + scope, + provenance: { source: 'migration' }, + }); migrated += 1; } cursor = page.cursor; diff --git a/docs/sdk/overview.md b/docs/sdk/overview.md index e5c9049..e3cf585 100644 --- a/docs/sdk/overview.md +++ b/docs/sdk/overview.md @@ -7,35 +7,67 @@ sidebar_position: 1 A TypeScript client for memory, pluggable across backends. -`atomicmemory-sdk` is a platform utility, not a framework. It gives application code a single, typed API for ingest, search, and context assembly, and it stays agnostic to which memory engine sits behind it. The same client can talk to a self-hosted `atomicmemory-core`, a Mem0 service, or a custom backend you wire yourself. +`@atomicmemory/atomicmemory-sdk` is a platform utility, not a framework. It gives application code a single, typed API for ingest, search, and context assembly, and it stays agnostic to which memory engine sits behind it. The same client can talk to a self-hosted `atomicmemory-core`, a Mem0 service, or a custom backend you wire yourself. ## Three-point value prop - **Backend-agnostic via `MemoryProvider`.** Every operation routes through the `MemoryProvider` interface. Swap `atomicmemory-core` for Mem0 — or a provider you write — with a config change, not a rewrite. Apps inspect provider capabilities at runtime and gracefully degrade when a backend does not support an extension (packaging, temporal search, versioning, etc.). - **Browser / extension / Node / worker-ready.** ESM + CJS dual build, subpath exports, and WASM-based local embeddings via `@huggingface/transformers`. The same package runs in a Chrome extension, a Next.js app, a serverless handler, or a web worker. -- **Client-side primitives for composition.** The `/storage`, `/embedding`, and `/search` subpath exports ship standalone — `StorageManager`, `EmbeddingGenerator`, `SemanticSearch`. You can compose memory features directly from these without ever constructing the top-level `AtomicMemorySDK`. The SDK is a bundle of reusable parts, not a single mandatory object. +- **Client-side primitives for composition.** The `/storage`, `/embedding`, and `/search` subpath exports ship standalone — `StorageManager`, `EmbeddingGenerator`, `SemanticSearch`. You can compose memory features directly from these without ever constructing `MemoryClient`. The SDK is a bundle of reusable parts, not a single mandatory object. + +## `MemoryClient` — the canonical public API + +```ts +import { MemoryClient } from '@atomicmemory/atomicmemory-sdk'; + +const memory = new MemoryClient({ + providers: { atomicmemory: { apiUrl: 'http://localhost:3050' } }, +}); +await memory.initialize(); + +await memory.ingest({ + mode: 'messages', + messages: [{ role: 'user', content: 'I prefer aisle seats.' }], + scope: { user: 'u1' }, +}); + +const results = await memory.search({ + query: 'seat preference', + scope: { user: 'u1' }, +}); +``` + +`MemoryClient` composes a `MemoryService` under the hood and exposes the pure memory API: `ingest`, `search`, `package`, `get`, `list`, `delete`, plus `capabilities` / `getExtension` / `getProviderStatus` for capability inspection, and an `atomicmemory` getter that returns the full [AtomicMemory namespace handle](/sdk/api/memory-provider) (lifecycle, audit, lessons, config, agents). The surface is intentionally small — application-layer concerns such as identity resolution, capture policy, or injection gating belong in consumer code, not in the SDK. ## Deployment topologies ```mermaid flowchart LR - subgraph T1[Extension + self-hosted core] - E1[Extension] --> S1[SDK] --> AMP1[AtomicMemoryProvider] --> C1[(core)] + subgraph T1[Extension/webapp via product wrapper] + E1[Product wrapper] --> MC1[MemoryClient] --> AMP1[AtomicMemoryProvider] --> C1[(core)] end - subgraph T2[Web app + hosted core] - W2[Web app] --> S2[SDK] --> AMP2[AtomicMemoryProvider] --> C2[(hosted core)] + subgraph T2[Node / serverless calling core directly] + W2[App code] --> MC2[MemoryClient] --> AMP2[AtomicMemoryProvider] --> C2[(hosted core)] end - subgraph T3[Extension + Mem0] - E3[Extension] --> S3[SDK] --> M0[Mem0Provider] --> MB[(Mem0)] + subgraph T3[Browser app calling Mem0 directly] + E3[App code] --> MC3[MemoryClient] --> M0[Mem0Provider] --> MB[(Mem0)] end - subgraph T4["Subpath primitives (no AtomicMemorySDK)"] + subgraph T4["Subpath primitives (no MemoryClient)"] W4[Browser app] --> SM4[StorageManager + IndexedDB] W4 --> EG4[EmbeddingGenerator] W4 --> SS4[SemanticSearch] end ``` -The first three topologies route through `AtomicMemorySDK`. The fourth does not — it composes the subpath exports directly. `AtomicMemorySDK` always requires a configured `MemoryProvider`; there is no backend-free mode of the top-level class. If you want to build with just the primitives, see [Browser primitives](/sdk/guides/browser-primitives). +The first three topologies route through `MemoryClient`. The fourth composes the subpath exports directly — useful when you want client-side semantic search over your app's data without any backend at all. See [Browser primitives](/sdk/guides/browser-primitives). + +## Browser-safe entry + +For browser applications that talk to a backend-hosted core through `MemoryClient` and don't need on-device storage or embeddings, import from the `./browser` subpath — a slim bundle that omits `storage`, `embedding`, `search`, and `utils`: + +```ts +import { MemoryClient } from '@atomicmemory/atomicmemory-sdk/browser'; +``` ## Where to go from here diff --git a/docs/sdk/quickstart.md b/docs/sdk/quickstart.md index 739f546..826a5d7 100644 --- a/docs/sdk/quickstart.md +++ b/docs/sdk/quickstart.md @@ -5,11 +5,11 @@ sidebar_position: 2 # SDK Quickstart -Install, initialize, and make your first ingest and search — with `AtomicMemorySDK` wired to a running `atomicmemory-core`. +Install, initialize, and make your first ingest and search — with `MemoryClient` wired to a running `atomicmemory-core`. ## Prerequisites -- **Node.js ≥ 18** (the SDK is ESM + CJS) +- **Node.js ≥ 20** (the SDK is ESM + CJS; the core container runs on `node:22-slim`) - **A running `atomicmemory-core`** — follow the [Core Quickstart](/quickstart) if you have not yet brought one up; we assume `http://localhost:3050` below ## Step 1 — Install @@ -18,87 +18,96 @@ Install, initialize, and make your first ingest and search — with `AtomicMemor npm install @atomicmemory/atomicmemory-sdk ``` -{/* -Open question: the package is published with publishConfig.access: "public" to npmjs.org in -Atomicmemory-sdk/package.json, but verify the package is actually live before users follow this -command. If still on GitHub Packages only, add an .npmrc recipe here. -*/} +Also works with `pnpm add` / `yarn add`. ## Step 2 — Initialize -`AtomicMemorySDK` requires a `userAccounts` configuration (identity and preferences) and a `context.providers` configuration (which memory backend to use and how to reach it). +`MemoryClient` needs one thing: a `providers` map telling it which memory backend(s) to use and how to reach them. ```typescript -import { AtomicMemorySDK } from '@atomicmemory/atomicmemory-sdk'; - -const sdk = new AtomicMemorySDK({ - userAccounts: { - identity: { - providerType: 'web2', - apiBaseUrl: 'http://localhost:8787', - testMode: true, - }, - preferences: { - providerType: 'web2', - apiBaseUrl: 'http://localhost:8787', - testMode: true, - encryption: { keySource: 'provided', key: new Uint8Array(32) }, - }, - }, - context: { - providers: { - default: 'atomicmemory', - atomicmemory: { apiUrl: 'http://localhost:3050' }, - }, +import { MemoryClient } from '@atomicmemory/atomicmemory-sdk'; + +const memory = new MemoryClient({ + providers: { + atomicmemory: { apiUrl: 'http://localhost:3050' }, }, }); -await sdk.initialize(); +await memory.initialize(); ``` -The `testMode: true` flag on both user-account services installs the hardcoded test provider — a permissive default that does not require a running identity service. For production configuration see [Scopes and identity](/sdk/concepts/scopes-and-identity). +The `atomicmemory` key is the provider id the client uses to route calls; the nested object is the provider adapter's config. See [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend) for the full config shape (`apiKey`, `timeout`, `apiVersion`). -## Step 3 — Ingest a memory +If you configure more than one provider, pass `defaultProvider` to pick which one receives un-qualified calls: ```typescript -await sdk.ingest( - { - mode: 'text', - content: 'JavaScript closures capture variables from their lexical scope.', - scope: { user: 'demo-user' }, - provenance: { source: 'manual' }, +new MemoryClient({ + providers: { + atomicmemory: { apiUrl: 'http://localhost:3050' }, + mem0: { apiUrl: 'http://localhost:8888', pathPrefix: '' }, }, - 'chatgpt.com', -); + defaultProvider: 'atomicmemory', +}); +``` + +## Step 3 — Ingest a memory + +```typescript +await memory.ingest({ + mode: 'text', + content: 'JavaScript closures capture variables from their lexical scope.', + scope: { user: 'demo-user' }, + provenance: { source: 'manual' }, +}); +``` + +Or from a conversation: + +```typescript +await memory.ingest({ + mode: 'messages', + messages: [ + { role: 'user', content: 'I prefer aisle seats.' }, + { role: 'assistant', content: 'Noted.' }, + ], + scope: { user: 'demo-user' }, +}); ``` -The second argument, `'chatgpt.com'`, is the **platform** the capture originated from. `AtomicMemorySDK` runs this through its capture gate before delegating to the provider — see [Consent and gating](/sdk/concepts/consent-and-gating). +`ingest` takes a single `IngestInput`. Any capture-gating policy ("should this conversation be saved at all?") lives in your application, not in the SDK. ## Step 4 — Search ```typescript -const page = await sdk.search( - { - query: 'How do closures work?', - scope: { user: 'demo-user' }, - limit: 5, - }, - 'chatgpt.com', -); +const page = await memory.search({ + query: 'How do closures work?', + scope: { user: 'demo-user' }, + limit: 5, +}); -for (const { memory, score } of page.results) { - console.log(score.toFixed(3), memory.content); +for (const { memory: record, score } of page.results) { + console.log(score.toFixed(3), record.content); } ``` -`sdk.search(request, site)` gates the result against the user's injection rules for `site`. If the site is blocked, the response is a `SearchResultPage` with `{ results: [] }`, not an exception — design your UI accordingly. +Similarly, `search` takes a single `SearchRequest`. Injection-gating ("should this result ever be shown on this site?") is also an application-layer concern. ## This is the whole round trip -`initialize` → `ingest` → `search`. Every other method (`get`, `delete`, `list`, `package`, `capabilities`, `loadKnowledgeBase`, provider introspection) is reachable on the same `sdk` object. For the full surface, see the [API Reference overview](/sdk/api/overview). +`initialize` → `ingest` → `search`. Every other method (`get`, `delete`, `list`, `package`, `capabilities`, `getExtension`, `getProviderStatus`) is reachable on the same `memory` object. Backend-specific admin — lifecycle reset, audit trails, lesson lists, agent trust — is reachable via the `atomicmemory` namespace handle: + +```typescript +const handle = memory.atomicmemory; +if (handle) { + await handle.lifecycle.resetSource('demo-user', 'manual'); + const stats = await handle.lifecycle.stats('demo-user'); +} +``` + +For the full surface, see the [API Reference overview](/sdk/api/overview). ## What next - Understand the pluggable-backend story in [Provider model](/sdk/concepts/provider-model). - Swap `atomicmemory` for a different backend in [Using the Mem0 backend](/sdk/guides/mem0-backend) or [Writing a custom provider](/sdk/guides/custom-provider). -- Build memory features without the top-level `AtomicMemorySDK` class using [Browser primitives](/sdk/guides/browser-primitives). +- Build memory features without a backend at all using [Browser primitives](/sdk/guides/browser-primitives). diff --git a/sidebars.ts b/sidebars.ts index 09bc739..e64c88f 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -39,7 +39,6 @@ const sidebars: SidebarsConfig = { 'sdk/concepts/scopes-and-identity', 'sdk/concepts/storage-adapters', 'sdk/concepts/embeddings', - 'sdk/concepts/consent-and-gating', ], }, { @@ -66,14 +65,6 @@ const sidebars: SidebarsConfig = { 'sdk/api/errors', ], }, - { - type: 'category', - label: 'Cookbook', - collapsed: true, - items: [ - 'sdk/cookbook/knowledge-base', - ], - }, ], // HTTP API reference — generated from @atomicmemory/atomicmemory-core's