# Memory Service ORM

> Memory Service ORM - datarobot[application-utils]ships a lightweight async ORM over the DataRobot
> Agentic Memory Service. Think of it as a typed, Pydantic v2-style document store that persists
> sessions (documents) and events (log entries) through a bearer-auth REST API, using your
> existingDATAROBOT_ENDPOINTandDATAROBOT_API_TOKENcredentials.

This Markdown file sits beside the HTML page at the same path (with a `.md` suffix). It summarizes the topic and lists links for tools and LLM context.

Companion generated at `2026-09-02T14:13:36.419231+00:00` (UTC).

## Primary page

- [Memory Service ORM](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md): Full documentation for this topic (Markdown sidecar).

## Sections on this page

- [Install](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#install): In-page section heading.
- [Quick-start](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#quick-start): In-page section heading.
- [Environment variables](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#environment-variables): In-page section heading.
- [Core concepts](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#core-concepts): In-page section heading.
- [Memory space (DRMemorySpace)](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#memory-space-drmemoryspace): In-page section heading.
- [Sessions (DRSession)](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#sessions-drsession): In-page section heading.
- [Lifecycle strategies (TTL)](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#lifecycle-strategies-ttl): In-page section heading.
- [Events (DREvent)](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#events-drevent): In-page section heading.
- [Range-key encoding](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#range-key-encoding): In-page section heading.
- [Optimistic concurrency](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#optimistic-concurrency): In-page section heading.
- [Sessions](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#sessions): In-page section heading.
- [Events](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#events): In-page section heading.
- [Batch operations](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#batch-operations): In-page section heading.
- [Emitter participant check](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#emitter-participant-check): In-page section heading.
- [Error types](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#error-types): In-page section heading.
- [Public API](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#public-api): In-page section heading.
- [Client and models](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#client-and-models): In-page section heading.
- [DRMemoryServiceClient](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemoryServiceClient): In-page section heading.
- [base_url](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemoryServiceClient.base_url): In-page section heading.
- [request()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemoryServiceClient.request): In-page section heading.
- [aclose()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemoryServiceClient.aclose): In-page section heading.
- [DRMemorySpace](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace): In-page section heading.
- [id](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace.id): In-page section heading.
- [user_id](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace.user_id): In-page section heading.
- [tenant_id](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace.tenant_id): In-page section heading.
- [post()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace.post): In-page section heading.
- [get()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace.get): In-page section heading.
- [list()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace.list): In-page section heading.
- [patch()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace.patch): In-page section heading.
- [delete()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace.delete): In-page section heading.
- [DRSession](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRSession): In-page section heading.
- [Class variables](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#class-variables): In-page section heading.
- [Read-only properties](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#read-only-properties): In-page section heading.
- [id](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRSession.id): In-page section heading.
- [created_at](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRSession.created_at): In-page section heading.
- [version](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRSession.version): In-page section heading.
- [post()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRSession.post): In-page section heading.
- [get()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRSession.get): In-page section heading.
- [list()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRSession.list): In-page section heading.
- [patch()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRSession.patch): In-page section heading.
- [delete()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRSession.delete): In-page section heading.
- [DREvent](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DREvent): In-page section heading.
- [sequence_id](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DREvent.sequence_id): In-page section heading.
- [created_at](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DREvent.created_at): In-page section heading.
- [post()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DREvent.post): In-page section heading.
- [post_batch()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DREvent.post_batch): In-page section heading.
- [list()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DREvent.list): In-page section heading.
- [last()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DREvent.last): In-page section heading.
- [patch()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DREvent.patch): In-page section heading.
- [delete()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DREvent.delete): In-page section heading.
- [patch_batch()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DREvent.patch_batch): In-page section heading.
- [Field markers and constants](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#field-markers-and-constants): In-page section heading.
- [DRDeduplicationKey](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRDeduplicationKey): In-page section heading.
- [DRRangeKey](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRRangeKey): In-page section heading.
- [Known limitation](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#known-limitation): In-page section heading.
- [DRConcurrencyField](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRConcurrencyField): In-page section heading.
- [datarobot.application_utils.persistence.markers.SYSTEM_PARTICIPANT: str= '000000000000000000000000'](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.markers.SYSTEM_PARTICIPANT): In-page section heading.
- [datarobot.application_utils.persistence.markers.DEFAULT_SESSION_TTL_SECONDS: int= 63072000](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.markers.DEFAULT_SESSION_TTL_SECONDS): In-page section heading.
- [Errors](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#errors): In-page section heading.
- [exceptiondatarobot.application_utils.persistence.DRMemoryServiceError](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemoryServiceError): In-page section heading.
- [exceptiondatarobot.application_utils.persistence.DRMemoryNotFoundError](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemoryNotFoundError): In-page section heading.
- [exceptiondatarobot.application_utils.persistence.DRMemoryBadRequestError](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemoryBadRequestError): In-page section heading.
- [exceptiondatarobot.application_utils.persistence.DRMemoryValidationError](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemoryValidationError): In-page section heading.
- [exceptiondatarobot.application_utils.persistence.DRMemoryConflictError](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemoryConflictError): In-page section heading.
- [exceptiondatarobot.application_utils.persistence.DRMemoryVersionConflictError](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemoryVersionConflictError): In-page section heading.
- [exceptiondatarobot.application_utils.persistence.DRMemoryRateLimitError](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemoryRateLimitError): In-page section heading.
- [exceptiondatarobot.application_utils.persistence.DRMemoryUnavailableError](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemoryUnavailableError): In-page section heading.
- [Running integration tests](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#running-integration-tests): In-page section heading.

## Related documentation

- [Developer documentation](https://docs.datarobot.com/en/docs/api/index.html.md): Linked from this page.
- [API reference](https://docs.datarobot.com/en/docs/api/reference/index.html.md): Linked from this page.
- [chat-history layer](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/chat-history.html.md): Linked from this page.

## Documentation content

`datarobot[application-utils]` ships a lightweight async ORM over the
DataRobot Agentic Memory Service.  Think of it as a typed, Pydantic v2-style
document store that persists sessions (documents) and events (log entries)
through a bearer-auth REST API, using your existing `DATAROBOT_ENDPOINT` and `DATAROBOT_API_TOKEN` credentials.

## Install

```
pip install "datarobot[application-utils]"
```

## Quick-start

```
import asyncio
from typing import Annotated

from datarobot.application_utils.persistence import (
    DRConcurrencyField,
    DRDeduplicationKey,
    DREvent,
    DRMemorySpace,
    DRRangeKey,
    DRSession,
    DRMemoryServiceClient,
    SYSTEM_PARTICIPANT,
)


# ── 1. Define your domain models ─────────────────────────────────────────────


class ChatSession(DRSession):
    """A chat session stored in the Memory Service."""

    __description_prefix__ = "chat"  # stable prefix; part of every query

    tenant: Annotated[str, DRRangeKey]  # description segment 1 (range queries)
    topic: Annotated[str, DRRangeKey]  # description segment 2 (range queries)
    chat_id: Annotated[str, DRDeduplicationKey]  # point-lookup / idempotent create
    rev: Annotated[int, DRConcurrencyField]  # mirrors server version integer
    title: str = ""  # plain metadata (payload only)


class ChatMessage(DREvent, session=ChatSession):
    """A single message in a chat session."""

    __event_type__ = "message"

    score: float = 0.0  # extra body field; round-trips through the wire


# ── 2. Use the ORM ────────────────────────────────────────────────────────────


async def demo() -> None:
    async with DRMemoryServiceClient() as client:
        # Create or adopt an existing space (idempotent via deduplication_key)
        space = await DRMemorySpace.post(
            client,
            description="My agent memory",
            deduplication_key="my-agent-space-v1",
        )

        # Create (or adopt) a session
        session = await ChatSession.post(
            space,
            tenant="acme",
            topic="billing",
            chat_id="billing-chat-001",
            title="Billing enquiry",
        )
        # `rev` is never passed in: it mirrors the server-assigned version.
        assert session.rev == 1

        # Append events
        msg = await ChatMessage.post(
            session,
            content="Hello, I need help with my invoice.",
            emitter_type="user",
            emitter_id="aabbccddeeff001122334455",  # 24-hex ObjectId
            score=0.9,
        )
        print(msg.sequence_id, msg.created_at)

        # List recent messages
        recent = await ChatMessage.last(session, n=10)

        # Fetch by range-key prefix (all billing sessions for "acme")
        billing_sessions = await ChatSession.list(space, tenant="acme", topic="billing")

        # Update session metadata (optimistic-concurrency guard via If-Match)
        await session.patch(title="Resolved billing enquiry")

        # Patch an event (guarded by its createdAt token)
        await msg.patch(score=0.5)


asyncio.run(demo())
```

## Environment variables

| Variable | Description |
| --- | --- |
| DATAROBOT_ENDPOINT | Full DataRobot API base URL, e.g., https://app.datarobot.com/api/v2. |
| DATAROBOT_API_TOKEN | DataRobot API bearer token. |

Both are resolved automatically; pass them as constructor arguments to override.

## Core concepts

### Memory space (DRMemorySpace)

A namespace that owns sessions and events.  Create one per-agent deployment or
application context.  Idempotent via `deduplication_key`.

```
space = await DRMemorySpace.post(client, deduplication_key="my-app-v1")
space2 = await DRMemorySpace.get(client, space.id)
spaces = await DRMemorySpace.list(client, deduplication_key="my-app-v1")
await space.patch(description="Updated description")
await space.delete()
```

### Sessions (DRSession)

Documents stored in a memory space.  Subclass `DRSession` and annotate fields
with ORM markers:

| Marker | Wire field | Purpose |
| --- | --- | --- |
| Annotated[T, DRDeduplicationKey] | deduplicationKey | Unique key; idempotent create. |
| Annotated[T, DRRangeKey] | description segment | Range / prefix queries. |
| Annotated[T, DRConcurrencyField] | version mirror | User-visible version counter. |
| (plain field) | metadata | Arbitrary payload; not queryable. |

Declare range-key fields in query order — a list query must specify a
contiguous leading prefix (see §Range-key encoding below).

#### Lifecycle strategies (TTL)

By default, every `DRSession` subclass sends a single `soft_delete` lifecycle
strategy on creation, triggered by a 2-year TTL ( `DEFAULT_SESSION_TTL_SECONDS`, `63072000` seconds) — the Memory Service’s own maximum for a TTL trigger. Sessions
therefore auto-clean unless you override this.

Override `__lifecycle_strategies__` to use a shorter TTL (or a different strategy):

```
class ChatSession(DRSession):
    __description_prefix__ = "chat"
    __lifecycle_strategies__ = [
        {"type": "soft_delete", "trigger": {"ttl": 30 * 86400}},  # 30 days
    ]
    ...
```

Set it to an empty list to send no lifecycle strategies at all:

```
class ChatSession(DRSession):
    __lifecycle_strategies__ = []
```

Lifecycle strategies are sent on create only; `session.patch(...)` never
touches them. Up to five strategy objects are allowed per session.

### Events (DREvent)

An append-only log under a session.  Bind to a session type with `class MyEvent(DREvent, session=MySession)`.  All plain declared fields map to
the `body` dict.

```
class ChatMessage(DREvent, session=ChatSession):
    __event_type__ = "message"  # "message" | "tool_output" | "status"
    score: float = 0.0
```

Declared fields round-trip through any Pydantic-expressible type — nested
models, `list[Model]`, `Optional[Model]`, enums and dicts all serialize into the
event `body` (and session `metadata`) and validate back on read.  This is what
lets the [chat-history layer](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/chat-history.html.md) nest typed `ToolCall` / `Reasoning` models inside a single message event.

## Range-key encoding

Range keys are encoded in the session `description` field using a hierarchical
path scheme:

```
description = "//" + esc(prefix) + "/" + esc(k1) + "/" + esc(k2) + "/"
```

`esc(v)` percent-encodes `%` (→ `%25`) then `/` (→ `%2F`), so values can
contain arbitrary text including slashes.  The leading `//` and trailing `/` after every segment create an anchored prefix — a substring-match on the
service side is equivalent to a hierarchy prefix query.

Example — two sessions stored under `chat/acme`:

```
//chat/acme/billing/    ← tenant=acme, topic=billing
//chat/acme/support/    ← tenant=acme, topic=support
```

Query `list(tenant="acme")` sends `description=//chat/acme/` which matches both.  Query `list(tenant="acme", topic="billing")` sends `description=//chat/acme/billing/` which matches only the first.

A subclass’`.list` always sends at least its `//<prefix>/` description
filter — even with no range-key arguments — so `ChatSession.list(space)` returns
only `chat` -prefixed sessions, never sessions of another `DRSession` subclass
that happens to share the same space.  (Trailing-slash anchoring keeps prefixes
disjoint, so `//chat/` never matches a `//chatx/…` or `//loc/…` session.)  This
subclass isolation is what lets the [chat-history layer](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/chat-history.html.md) keep
its `Chat` sessions and its `EntityLocator` index sessions (prefix `loc`) in one
space without either bleeding into the other’s `.list` results.

> ⚠️Case-insensitive caveat— the service performs a case-insensitive
> substring match, soAcmeandacmeare treated as the same tenant.
> Values that differ only in case will collide.

## Optimistic concurrency

### Sessions

Session PATCH sends an `If-Match: <version>` header.  If the server’s version
has advanced since you last fetched the session, the service returns HTTP 409
and the ORM raises `DRMemoryVersionConflictError`.  Resolve by re-fetching
( `DRSession.get`) and retrying.

```
try:
    await session.patch(title="New title")
except DRMemoryVersionConflictError:
    session = await ChatSession.get(space, id=session.id)
    await session.patch(title="New title")
```

### Events

Event PATCH uses `createdAt` as a query-string concurrency token instead of
a header.  A stale token yields HTTP 422 and `DRMemoryVersionConflictError`.

```
try:
    await msg.patch(content="Corrected text")
except DRMemoryVersionConflictError:
    # Re-list to obtain fresh tokens
    events = await ChatMessage.list(session)
    msg = next(e for e in events if e.sequence_id == msg.sequence_id)
    await msg.patch(content="Corrected text")
```

## Batch operations

```
# Atomic batch create (up to 200 events)
msgs = await ChatMessage.post_batch(
    session,
    events=[
        {"content": "First", "emitter_type": "agent"},
        {"content": "Second", "emitter_type": "agent", "score": 0.5},
    ],
)

# Atomic batch patch (up to 200 events)
await ChatMessage.patch_batch(
    session,
    updates=[
        (msgs[0], {"score": 0.9}),
        (msgs[1], {"content": "Updated second"}),
    ],
)
```

## Emitter participant check

When `emitter_type="user"`, the emitter’s ObjectId must be in the session’s `participants` list.  The ORM raises `DRMemoryBadRequestError` early (before the
HTTP call) if the emitter is not a participant.

The system sentinel `SYSTEM_PARTICIPANT = "000000000000000000000000"` is a
client-side convention for agent-owned sessions.  It is a valid ObjectId
accepted by the service.

## Error types

| Exception | HTTP status | Cause |
| --- | --- | --- |
| DRMemoryBadRequestError | 400 | Invalid request (emitter not a participant, etc.) |
| DRMemoryNotFoundError | 404 | Resource does not exist |
| DRMemoryConflictError | 409 | Deduplication conflict on create (ORM auto-adopts) |
| DRMemoryVersionConflictError | 409 / 422 | Stale If-Match or createdAt token |
| DRMemoryValidationError | 422 | Schema validation error |
| DRMemoryRateLimitError | 429 | Trial quota / rate limit; retry_after carries the Retry-After seconds (None for the storage cap, which sends no header) |
| DRMemoryUnavailableError | — |  |
| DRMemoryServiceError | other 4xx/5xx | Unexpected error |

## Public API

```
from datarobot.application_utils.persistence import (
    DRMemorySpace,
    DRSession,
    DREvent,
    DRDeduplicationKey,
    DRRangeKey,
    DRConcurrencyField,
    DRMemoryServiceClient,
    SYSTEM_PARTICIPANT,
    DEFAULT_SESSION_TTL_SECONDS,
    DRMemoryServiceError,
    DRMemoryNotFoundError,
    DRMemoryBadRequestError,
    DRMemoryValidationError,
    DRMemoryConflictError,
    DRMemoryVersionConflictError,
    DRMemoryRateLimitError,
    DRMemoryUnavailableError,
)
```

### Client and models

### DRMemoryServiceClient

Async HTTP client for the DataRobot Agentic Memory Service.

Resolves `DATAROBOT_ENDPOINT` and `DATAROBOT_API_TOKEN` from the
environment; constructor arguments take precedence.  Owns an `httpx.AsyncClient` unless one is injected.

The instance itself is lightweight — the resolved base URL plus a headers
dict.  Applications that act on behalf of many principals (a different
API token per end user) are supported by constructing one `DRMemoryServiceClient` per principal over a single shared `http_client`: the shared pool carries the connections, each instance
carries only an identity.

That isolation holds only while the shared client stays identity-free:
construct it with no `auth` ( `request()` also sends `auth=None` as
a guard, since identity always travels in this instance’s headers) and a
cookie jar that stores nothing — `httpx.AsyncClient` persists `Set-Cookie` responses in a jar shared by every request through it,
which would replay one principal’s cookies on another principal’s
requests.  The service authenticates by header, not cookies, so refusing
cookies loses nothing (see the multi-principal example below).

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| endpoint | str \\| None | DataRobot API endpoint (e.g. https://app.datarobot.com/api/v2). Defaults to the DATAROBOT_ENDPOINT environment variable. |
| api_token | str \\| None | DataRobot API token. Defaults to the DATAROBOT_API_TOKEN environment variable. Multi-principal applications must pass this explicitly — the environment fallback is the application’s own credential, not the requesting user’s. |
| base_path | str | Sub-path appended to the endpoint. Defaults to "memory" — the gateway mount path for the Memory Service (/api/v2/memory). |
| http_client | httpx.AsyncClient \\| None | Injected async client. When supplied, the DRMemoryServiceClient will not close it on aclose() — the caller owns its lifetime. This is a supported production pattern (share one connection pool across many per-principal client instances) as well as the hook for testing with respx. A client shared across principals must be identity-free: no auth, and a non-storing cookie jar (see Examples). When omitted, the client creates and owns a pool of its own. |
| timeout | float | Default per-request timeout in seconds. Ignored when http_client is injected — configure the timeout on the injected client instead. |

> [!NOTE] Examples
> One client, one credential (scripts, single-principal apps):
> 
> ```
> import asyncio
> from datarobot.application_utils.persistence import (
>     DRMemorySpace,
>     DRMemoryServiceClient,
> )
> 
> async def main() -> None:
>     async with DRMemoryServiceClient() as client:
>         space = await DRMemorySpace.post(client, description="my-space")
>         print(space.id)
> 
> asyncio.run(main())
> ```
> 
> One shared pool, one thin client per principal (multi-user web apps):
> 
> ```
> import http.cookiejar
> 
> import httpx
> 
> # App startup.  The shared pool must stay identity-free: no auth
> # (identity belongs on each DRMemoryServiceClient, not the
> # transport), and a cookie jar that stores nothing — httpx would
> # otherwise replay one principal's Set-Cookie on another's requests.
> shared_http = httpx.AsyncClient(
>     timeout=30.0,
>     cookies=http.cookiejar.CookieJar(
>         policy=http.cookiejar.DefaultCookiePolicy(allowed_domains=[])
>     ),
> )
> 
> def client_for(user_token: str) -> DRMemoryServiceClient:
>     # Cheap: per-request construction, no new connections.
>     return DRMemoryServiceClient(
>         api_token=user_token,
>         http_client=shared_http,
>     )
> 
> async def shutdown() -> None:
>     await shared_http.aclose()  # close the pool once, at app shutdown
> ```

#### base_url

The resolved Memory Service base URL.

#### request()

Send a request and return the response, mapping errors to typed exceptions.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| method | str | HTTP method ("GET", "POST", "PATCH", "DELETE"). |
| path | str | Path relative to base_url. Must end with "/". |
| params | dict \\| None | Query parameters. |
| json | Any | JSON-serializable request body. |
| extra_headers | dict \\| None | Additional headers (e.g. {"If-Match": "3"}). |

Returns

| Returns | Description |
| --- | --- |
| On 2xx status codes. |  |

Return type: `httpx.Response`

Raises

| Exception | Description |
| --- | --- |
| DRMemoryBadRequestError | HTTP 400 — bad request (e.g. emitter not a participant). |
| DRMemoryNotFoundError | HTTP 404 — resource not found. |
| DRMemoryConflictError | HTTP 409 — deduplication conflict on create. |
| DRMemoryVersionConflictError | HTTP 409 — stale If-Match on session patch(), or HTTP 422 with the event-version detail. |
| DRMemoryValidationError | HTTP 422 — schema validation error. |
| DRMemoryRateLimitError | HTTP 429 — quota or rate limit exceeded; carries retry_after seconds parsed from the Retry-After response header. |
| DRMemoryUnavailableError |  |
| DRMemoryServiceError | Any other 4xx/5xx error. |

#### aclose()

Close the underlying HTTP client (only if owned by this instance).

Return type: `None`

### DRMemorySpace

Represents a DataRobot Agentic Memory Service memory space.

Acts as the container for sessions; every session and event call is scoped
to a space.  Construct via the class methods [post()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace.post) or [get()](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace.get) rather than directly instantiating.

Variables

| Attribute | Type | Description |
| --- | --- | --- |
| description | str \\| None | Human-readable description of the space. |
| deduplication_key | str \\| None | Unique client-assigned key; enables idempotent space creation. |
| llm_model_name | str \\| None | LLM model name for extract_memories lifecycle strategies. |
| llm_base_url | str \\| None | LLM base URL override. |
| custom_instructions | str \\| None | Custom instructions passed to the LLM when extracting memories. |
| created_at | str | ISO-8601 creation timestamp (server-assigned). |

#### id

Server-assigned memory space UUID.

#### user_id

Owner user ID.

#### tenant_id

Tenant UUID.

#### post()

Create a new memory space, or adopt an existing one on a deduplication conflict.

If a `deduplication_key` is supplied and a space with that key already
exists, the existing space is fetched and returned (409 → adopt).

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| client | DRMemoryServiceClient | Transport client. |
| description | str \\| None | Human-readable description (max 1000 chars). |
| deduplication_key | str \\| None | Unique client key for idempotent creation (1–72 chars). |
| llm_model_name | str \\| None | LLM model name for extract_memories strategies. |
| llm_base_url | str \\| None | LLM base URL override. |
| custom_instructions | str \\| None | Custom LLM instructions (max 10 000 chars). |

Returns

| Returns | Description |
| --- | --- |
| The newly created or adopted memory space. |  |

Return type: [DRMemorySpace](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace)

#### get()

Fetch a memory space by its server-assigned ID.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| client | DRMemoryServiceClient | Transport client. |
| space_id | str | UUID of the memory space. |

Return type: [DRMemorySpace](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRMemorySpace)

Raises

| Exception | Description |
| --- | --- |
| DRMemoryNotFoundError | If no space with the given ID exists (or it belongs to another user). |

#### list()

List memory spaces visible to the authenticated user.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| client | DRMemoryServiceClient | Transport client. |
| deduplication_key | str \\| None | Exact-match filter on deduplicationKey. |
| offset | int | Number of spaces to skip (for pagination). |
| limit | int | Maximum number of spaces to return (1–100). |

Return type: `list[DRMemorySpace]`

#### patch()

Update this memory space in place.

Only the supplied keyword arguments are changed; omitted fields keep
their current values on the server.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| description | str \\| None | New description. |
| llm_model_name | str \\| None | New LLM model name. |
| llm_base_url | str \\| None | New LLM base URL. |
| custom_instructions | str \\| None | New custom instructions. |

Raises

| Exception | Description |
| --- | --- |
| ValueError | When no field is supplied, rather than sending a no-op request. |

Return type: `None`

#### delete()

Soft-delete this memory space.

After deletion the space is no longer accessible via `get` or `list`.

Return type: `None`

### DRSession

Abstract base class for Memory Service ORM session models.

Do not instantiate directly; subclass and declare fields with ORM markers.

#### Class variables

__description_prefix__
: Prefix injected at the start of the encoded `description`.  Defaults
  to the subclass name.  Keep it short and stable; it is part of every
  stored description and every list-query filter.

__lifecycle_strategies__
: Lifecycle strategy objects sent on session creation.  Defaults to a single `soft_delete` strategy with a `DEFAULT_SESSION_TTL_SECONDS` (2 year) TTL
  trigger, so sessions auto-clean unless a subclass overrides this.  Override
  with a different strategy list to change the TTL/strategy, or set to `[]` to send no lifecycle strategies at all.

#### Read-only properties

id
: Server-assigned session UUID.

created_at
: ISO-8601 creation timestamp.

version
: Server-assigned version integer (optimistic-concurrency token).

#### id

Server-assigned session UUID (read-only).

#### created_at

ISO-8601 creation timestamp (read-only).

#### version

Current server version integer (read-only; updated on every patch).

#### post()

Create a session, or adopt the existing one on a deduplication conflict.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| space | DRMemorySpace | Memory space to create the session in. * **kwargs (Any) – Session field values. Pass participants=["<objectid>"] to scope to a user; omit to use the system sentinel. A DRConcurrencyField may not be passed — it mirrors the server-assigned version and is populated from the response. |

Returns

| Returns | Description |
| --- | --- |
| The newly created or adopted session. |  |

Return type: [DRSession](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRSession)

Raises

| Exception | Description |
| --- | --- |
| ValueError | On undeclared kwargs, or when a DRConcurrencyField is supplied. |

> [!NOTE] Examples
> ```
> session = await ChatSession.post(
>     space,
>     tenant="acme",
>     topic="billing",
>     chat_id="chat-001",
>     title="Billing enquiry",
> )
> ```

#### get()

Fetch a session by its server-assigned `id` or by `deduplication_key`.

Exactly one of `id=` or the subclass’s `DRDeduplicationKey` field
name must be supplied.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| space | DRMemorySpace | Memory space containing the session. |
| id | str \\| None | Server-assigned session UUID. * **kwargs (Any) – Pass the DRDeduplicationKey field name as a keyword argument for an exact-match point lookup (e.g. chat_id="billing-chat-001"). |

Return type: [DRSession](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DRSession)

Raises

| Exception | Description |
| --- | --- |
| DRMemoryNotFoundError | If no matching session is found. |
| ValueError | If neither id nor a dedup key is supplied, or the subclass has no DRDeduplicationKey field and a keyword arg is provided. |

> [!NOTE] Examples
> ```
> # By server id
> session = await ChatSession.get(space, id="uuid-string")
> 
> # By dedup key
> session = await ChatSession.get(space, chat_id="billing-chat-001")
> ```

#### list()

List sessions matching a range-key prefix and/or participant filter.

The query is always scoped to this subclass by its `__description_prefix__` (so `list()` never returns sessions of a
different subclass sharing the space); range-key kwargs narrow it further.

Range-key kwargs must form a contiguous leading prefix of the
declared `DRRangeKey` fields (e.g. for fields `[tenant, topic]` you
may filter on `tenant=` alone or on `tenant=` + `topic=`, but not
on `topic=` alone).

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| space | DRMemorySpace | Memory space to query. |
| participant | str \\| None | Filter to sessions that include this ObjectId in participants. * **kwargs (Any) – Leading range-key field values for a prefix query. |

Returns

| Returns | Description |
| --- | --- |
| All matching sessions (auto-paginated). |  |

Return type: `list[DRSession]`

Raises

| Exception | Description |
| --- | --- |
| ValueError | If range-key kwargs are not a contiguous leading prefix. |

> [!NOTE] Examples
> ```
> # All sessions for tenant "acme"
> sessions = await ChatSession.list(space, tenant="acme")
> 
> # Scoped to a user
> sessions = await ChatSession.list(space, participant=user_oid)
> 
> # Combined: user + range prefix
> sessions = await ChatSession.list(
>     space, participant=user_oid, tenant="acme", topic="billing"
> )
> ```

#### patch()

Update this session in place.

Pass any combination of metadata fields and/or `DRRangeKey` fields.`participants` and `DRDeduplicationKey` fields cannot be changed.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
|  |  | **kwargs (Any) – Fields to update. |

Raises

| Exception | Description |
| --- | --- |
| DRMemoryVersionConflictError | If the session was updated concurrently (stale If-Match). |

Return type: `None`

> [!NOTE] Examples
> ```
> await session.patch(title="New title")
> await session.patch(topic="support", title="Re: billing")
> ```

#### delete()

Soft-delete this session.

After deletion the session is no longer returned by `get` or `list`.

Return type: `None`

### DREvent

Abstract base class for Memory Service ORM event models.

Do not instantiate directly; subclass with `session=<SessionClass>`.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| content | str | Event text (1–100 000 characters). |
| emitter_type | Literal[```”user”, "agent"]` | Who produced the event. |
| emitter_id | str \\| None | ObjectId of the emitting user. Required when emitter_type="user" and must be a member of the session’s participants. |
| properties |  | (Read-only) |
| -------------------- |  |  |
| sequence_id | int | Server-assigned monotonic integer address (−1 before the event is posted). |
| created_at | str | ISO-8601 timestamp; also serves as the concurrency token for patch(). |

#### sequence_id

Server-assigned event address (read-only; −1 before posting).

#### created_at

ISO-8601 creation timestamp (read-only; also the concurrency token).

#### post()

Append a single event to the session.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| session | DRSession | Session to append the event to. |
| content | str | Event text (1–100 000 characters). |
| emitter_type | Literal[```”user”, "agent"]` | Who produced the event. |
| emitter_id | str \\| None | ObjectId of the emitter. Required when emitter_type="user" and must be in session.participants. * **kwargs (Any) – Any declared body fields (e.g. score=0.9). |

Return type: [DREvent](https://docs.datarobot.com/en/docs/api/reference/sdk/application-utils/memory-orm.html.md#datarobot.application_utils.persistence.DREvent)

#### post_batch()

Atomically append up to 200 events to the session.

All events are appended in list order.  If any event fails validation
the entire batch is rolled back.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| session | DRSession | Session to append the events to. |
| events | list[dict[str, Any]] | Each dict contains the same keyword arguments as post() (content, emitter_type, optionally emitter_id and body fields). |

Return type: `list[DREvent]`

Raises

| Exception | Description |
| --- | --- |
| ValueError | If the batch exceeds 200 events. |

> [!NOTE] Examples
> ```
> msgs = await ChatMessage.post_batch(
>     session=my_session,
>     events=[
>         {"content": "Hi", "emitter_type": "user", "emitter_id": oid},
>         {"content": "Hello", "emitter_type": "agent"},
>     ],
> )
> ```

#### list()

List events under a session, optionally filtered by type.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| session | DRSession | Session to query. |
| type | str \\| None | Event type filter: "message", "tool_output", or "status". None returns all types. |
| offset | int | Number of events to skip. |
| limit | int | Maximum number of events to return (1–100). |

Return type: `list[DREvent]`

#### last()

Return the last `n` events in chronological order.

`lastN` and `offset` are mutually exclusive on the service API; this
method never sends `offset`.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| session | DRSession | Session to query. |
| n | int | Number of tail events to return (1–100). |
| type | str \\| None | Optional event-type filter. |

Return type: `list[DREvent]`

#### patch()

Update this event in place, guarded by its `created_at` token.

At least one field must be supplied.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| content | str \\| None | New event text. |
| emitter_type | Literal[```”user”, "agent"] \| None` | New emitter type. |
| emitter_id | str \\| None | New emitter ObjectId. * **kwargs (Any) – Any declared body fields to update. |

Raises

| Exception | Description |
| --- | --- |
| DRMemoryVersionConflictError | If the event was updated concurrently (stale createdAt token). |

Return type: `None`

#### delete()

Soft-delete this event.

After deletion the event is no longer returned by `list` or `last`.

Return type: `None`

#### patch_batch()

Atomically update up to 200 events, each guarded by its `created_at` token.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| session | DRSession | Session containing the events. All events must belong to this session. |
| updates | list[tuple[DREvent, dict[str, Any]]] | Each tuple is (event_instance, kwargs). The kwargs accept the same arguments as patch(). |

Returns

| Returns | Description |
| --- | --- |
| Updated event instances (in the order of the input list). |  |

Return type: `list[DREvent]`

Raises

| Exception | Description |
| --- | --- |
| ValueError | If the batch exceeds 200 updates or no fields are provided for an item. |
| DRMemoryVersionConflictError | If any event was updated concurrently. |

> [!NOTE] Examples
> ```
> await ChatMessage.patch_batch(
>     session=my_session,
>     updates=[
>         (event_a, {"score": 0.9}),
>         (event_b, {"content": "Updated text"}),
>     ],
> )
> ```

### Field markers and constants

### DRDeduplicationKey

Marker: field maps to the session `deduplicationKey` (point-lookup primary key).

At most one field per `DRSession` subclass may carry this marker.
Enables `MySession.get(space, my_key="value")` exact-match point lookups
and idempotent session creation (409 → adopt the existing session).

### DRRangeKey

Marker: field maps to a segment of the session `description` (range/prefix queries).

Segments are appended to `description` in declaration order, encoded
with the `//prefix/seg1/seg2/` scheme. Values must be non-empty strings.

#### Known limitation

The Memory Service `description` filter is case-insensitive, so values
differing only in case (e.g.`Foo` vs `foo`) will collide when querying.

### DRConcurrencyField

Marker: field is kept in sync with the server `version` integer.

Enables user code to inspect the current optimistic-concurrency version
without calling `.version` directly.  At most one field per `DRSession` subclass may carry this marker.

Whether or not this marker is present, the ORM always tracks the server
version internally for `If-Match` concurrency control on `patch()`.

### datarobot.application_utils.persistence.markers.SYSTEM_PARTICIPANT : str = '000000000000000000000000'

Sentinel ObjectId for sessions that belong to no specific user.
This is a client-side convention; the Memory Service treats it as any
other valid 24-hex ObjectId.

### datarobot.application_utils.persistence.markers.DEFAULT_SESSION_TTL_SECONDS : int = 63072000

Default session TTL in seconds (2 years). Also the Memory Service maximum for a
TTL trigger; used to build the default `soft_delete` lifecycle strategy.

### Errors

### exception datarobot.application_utils.persistence.DRMemoryServiceError

Base exception for all Memory Service ORM errors.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| detail | str | Human-readable error detail. |
| status_code | int \\| None | HTTP status code, if applicable. |
| payload | dict \\| None | Raw response body, if available. |

### exception datarobot.application_utils.persistence.DRMemoryNotFoundError

Raised when the requested resource does not exist (HTTP 404).

### exception datarobot.application_utils.persistence.DRMemoryBadRequestError

Raised on client-side validation errors from the service (HTTP 400).

Common causes: an event emitter is not a session participant, an invalid
ObjectId is supplied for a participant filter.

### exception datarobot.application_utils.persistence.DRMemoryValidationError

Raised on schema validation errors from the service (HTTP 422).

This typically means the request body or query parameters failed the
service’s Pydantic validation (e.g. a field is too long, a required field
is missing, or mutually exclusive parameters are both supplied).

### exception datarobot.application_utils.persistence.DRMemoryConflictError

Raised on a deduplication conflict when creating a session or `DRMemorySpace` (HTTP 409).

The ORM automatically adopts the existing resource (by fetching it via `existing_id`) rather than propagating this exception to callers in
the normal `post()` flow.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| existing_id | str \\| None | Server-assigned ID of the existing resource. |
| location | str \\| None | URL from the service Location header pointing to the existing resource. |

### exception datarobot.application_utils.persistence.DRMemoryVersionConflictError

Raised on an optimistic-concurrency failure.

Surfaces as HTTP 409 for session `patch()` (stale `If-Match` header)
and as HTTP 422 for event `patch()` (stale `createdAt` token).

Resolution: re-read the resource to get the current version, then retry.

### exception datarobot.application_utils.persistence.DRMemoryRateLimitError

Raised when the service rejects a request due to quota or rate limits (HTTP 429).

The Memory Service enforces per-tenant trial quotas: monthly read/write
counts answer `429` with a `Retry-After` header (seconds until the
window resets), while the storage cap answers `429` without `Retry-After` — storage is a level, not a windowed quota, so freeing
data (or upgrading) is the remedy rather than waiting.

Resolution: when `retry_after` is set, wait that many seconds before
retrying or propagate the value to your own HTTP response so the caller
can back off correctly.  When it is `None`, retrying later will not
help by itself — inspect `detail` for the limit that was hit.

Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| retry_after | int \\| None | Whole seconds to wait before retrying, parsed from the Retry-After response header (supports both delta-seconds and HTTP-date forms;integer so it can be propagated verbatim into another Retry-After header). None when the service did not send the header (e.g. the trial storage-cap 429). |

### exception datarobot.application_utils.persistence.DRMemoryUnavailableError

Raised when no HTTP response was received from the service.

Covers request timeouts and transport failures (connection refused, DNS
resolution, TLS errors, protocol violations).  The original `httpx` exception is preserved as `__cause__`.

`status_code` is always `None`: the failure happened before a status
code existed.  Catching `DRMemoryServiceError` therefore covers both
service-side errors and an unreachable service, without importing `httpx` at call sites.

## Running integration tests

Integration tests are skipped by default.  To run them against a live endpoint:

```
export DATAROBOT_ENDPOINT="https://app.datarobot.com/api/v2"
export DATAROBOT_API_TOKEN="<your-token>"

pytest tests/application_utils/persistence/acceptance -m integration -vv
```
